refactor(wbs-ux): isolate business state into useSystemSettings composable for 100% clean presentation architecture
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 11s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Calibration & Performance (push) Has been skipped
Validators (Pushes and Pull Requests) / Operational Report & Decision Packet (push) Has been skipped
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 7s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 18s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 8s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped

This commit is contained in:
2026-07-25 20:38:17 +09:00
parent e1c597ad0a
commit 716c1c1760
2 changed files with 232 additions and 334 deletions
@@ -0,0 +1,182 @@
// useSystemSettings.ts
import { ref, computed, onMounted } from 'vue'
export interface AuditLog {
timestamp: string;
user: string;
change: string;
}
export interface SettingItem {
id: number;
setting_key: string;
category: string;
domain: 'OMS' | 'WMS' | 'ERP';
setting_value: string;
status: 'ACTIVE' | 'WARNING' | 'BLOCKED';
updated_at: string;
note: string;
lock_version: number;
maker_checker: 'APPROVED' | 'PENDING' | 'REJECTED';
attachments?: string[];
audit_history?: AuditLog[];
}
export function useSystemSettings() {
const activeDomain = ref<'ALL' | 'OMS' | 'WMS' | 'ERP'>('ALL');
const searchKeyword = ref('');
const activeCategoryTab = ref('ALL');
const lastDraftSavedTime = ref<string | null>(null);
const isOmsModalOpen = ref(false);
const isWmsModalOpen = ref(false);
const isErpModalOpen = ref(false);
const isModalOpen = ref(false);
const isGuideModalOpen = ref(false);
const isEditMode = ref(false);
const masterPanelWidth = ref(60);
const isDraggingSplitter = ref(false);
const pingMs = ref(4);
const isBatchRunning = ref(false);
const batchProgress = ref(0);
const dbConnections = ref(4);
const aiRecommendation = ref<string | null>('🤖 AI AX Recommendation: RSI 상한값을 70.0에서 68.5로 보정 시 슬리피지 0.14% 감소가 예측됩니다.');
const liveLogs = ref<string[]>([
'[14:00:01] [INFO] SignalR WebSocket Hub connected to wss://localhost:5173/hub',
'[14:00:05] [INFO] DbUp Migration schema verified (v2026.07.25_001)',
'[14:02:10] [INFO] OMS Order Router: Waterfall Priority Table active',
'[14:02:15] [WARN] WMS Vault Safety Check: D+2 Cash level 500,000,000 KRW OK'
]);
const items = ref<SettingItem[]>([]);
const selectedItem = ref<SettingItem | null>(null);
const toastMessage = ref<{ text: string; type: 'success' | 'warning' | 'error' } | null>(null);
const showToast = (text: string, type: 'success' | 'warning' | 'error' = 'success') => {
toastMessage.value = { text, type };
setTimeout(() => {
toastMessage.value = null;
}, 3000);
};
const filteredItems = computed(() => {
return items.value.filter(item => {
const matchDomain = activeDomain.value === 'ALL' || item.domain === activeDomain.value;
const matchCategory = activeCategoryTab.value === 'ALL' || item.category === activeCategoryTab.value;
const matchSearch = !searchKeyword.value ||
item.setting_key.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.note.toLowerCase().includes(searchKeyword.value.toLowerCase());
return matchDomain && matchCategory && matchSearch;
});
});
const loadData = async () => {
items.value = [
{
id: 1, setting_key: 'D2_CASH_TARGET', category: 'BUDGET', domain: 'WMS', setting_value: '500,000,000', status: 'ACTIVE', updated_at: '2026-07-25 14:00', note: 'D+2 즉시방어 현금 목표', lock_version: 3, maker_checker: 'APPROVED', attachments: ['budget_spec_v1.pdf'],
audit_history: [
{ timestamp: '2026-07-25 14:00', user: 'admin_kjh', change: '설정값 4.8억 -> 5.0억 변경' },
{ timestamp: '2026-07-20 09:00', user: 'system', change: '신규 등록' }
]
},
{
id: 2, setting_key: 'RSI_UPPER_LIMIT', category: 'FACTOR', domain: 'OMS', setting_value: '70.0', status: 'ACTIVE', updated_at: '2026-07-24 18:30', note: '과매수 상한 임계값', lock_version: 1, maker_checker: 'APPROVED', attachments: [],
audit_history: [{ timestamp: '2026-07-24 18:30', user: 'quant_dev', change: '상한 75 -> 70 보정' }]
},
{
id: 3, setting_key: 'ANTI_LATE_ENTRY', category: 'RISK', domain: 'WMS', setting_value: 'ENABLED', status: 'WARNING', updated_at: '2026-07-22 10:15', note: '추격매수 방지 가드 레벨 2', lock_version: 5, maker_checker: 'PENDING', attachments: ['gate_audit.log'],
audit_history: [{ timestamp: '2026-07-22 10:15', user: 'risk_officer', change: '경고 플래그 활성화' }]
},
{
id: 4, setting_key: 'WATERFALL_SELL_PRIORITY', category: 'EXECUTION', domain: 'OMS', setting_value: 'STRICT', status: 'BLOCKED', updated_at: '2026-07-20 09:00', note: '단일 sell priority waterfall 룰', lock_version: 2, maker_checker: 'APPROVED', attachments: [],
audit_history: []
},
{
id: 5, setting_key: 'ERP_TAX_ACCOUNT_CODE', category: 'ACCOUNTING', domain: 'ERP', setting_value: '1110-CASH', status: 'ACTIVE', updated_at: '2026-07-25 10:00', note: 'ERP 세무 현금 계정 맵핑', lock_version: 1, maker_checker: 'APPROVED', attachments: ['tax_map.xlsx'],
audit_history: []
}
];
if (items.value.length > 0) {
selectedItem.value = JSON.parse(JSON.stringify(items.value[0]));
}
};
const cloneSelectedItem = () => {
if (!selectedItem.value) return;
const cloned: SettingItem = {
...JSON.parse(JSON.stringify(selectedItem.value)),
id: items.value.length + 1,
setting_key: `${selectedItem.value.setting_key}_COPY`,
lock_version: 1,
updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
audit_history: [{ timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '), user: 'admin_kjh', change: '기존 항목 기반 복제 생성' }]
};
items.value.unshift(cloned);
selectedItem.value = cloned;
showToast(`📋 [${cloned.setting_key}] 항목이 복제 생성되었습니다.`, 'success');
};
const runFactorBatch = () => {
if (isBatchRunning.value) return;
isBatchRunning.value = true;
batchProgress.value = 0;
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [OMS/WMS] Executing Batch Engine Pipeline...`);
showToast('⚡ OMS/WMS/ERP 팩터 재계산 비동기 배치 작업이 시작되었습니다.', 'success');
const timer = setInterval(() => {
batchProgress.value += 25;
if (batchProgress.value >= 100) {
clearInterval(timer);
isBatchRunning.value = false;
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [SUCCESS] Batch Engine finished in 1.2s`);
showToast('✅ 팩터 재계산 배치가 성공적으로 완료되었습니다.', 'success');
}
}, 300);
};
const saveDetailForm = () => {
if (!selectedItem.value) return;
const idx = items.value.findIndex(i => i.id === selectedItem.value?.id);
if (idx !== -1) {
selectedItem.value.lock_version += 1;
if (!selectedItem.value.audit_history) selectedItem.value.audit_history = [];
selectedItem.value.audit_history.unshift({
timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '),
user: 'admin_kjh',
change: `수정 완료 (Ver -> v${selectedItem.value.lock_version})`
});
items.value[idx] = JSON.parse(JSON.stringify(selectedItem.value));
isEditMode.value = false;
lastDraftSavedTime.value = new Date().toLocaleTimeString();
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [UPDATE] Saved key '${selectedItem.value.setting_key}' (Lock Ver v${selectedItem.value.lock_version})`);
showToast(`PostgreSQL 원장 설정이 저장되었습니다 (Lock Ver: v${selectedItem.value.lock_version}).`, 'success');
}
};
const deleteSelectedItem = () => {
if (!selectedItem.value) return;
if (confirm(`[${selectedItem.value.setting_key}] 항목을 삭제하시겠습니까?`)) {
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [DELETE] Deleted key: ${selectedItem.value.setting_key}`);
items.value = items.value.filter(i => i.id !== selectedItem.value?.id);
selectedItem.value = items.value.length > 0 ? items.value[0] : null;
showToast('항목이 삭제되었습니다.', 'warning');
}
};
const setSplitRatio = (percent: number) => {
masterPanelWidth.value = percent;
showToast(`스플릿 분할 비율이 ${percent}% : ${100 - percent}% 로 조정되었습니다.`, 'success');
};
onMounted(loadData);
return {
activeDomain, searchKeyword, activeCategoryTab, lastDraftSavedTime,
isOmsModalOpen, isWmsModalOpen, isErpModalOpen, isModalOpen, isGuideModalOpen, isEditMode,
masterPanelWidth, isDraggingSplitter, pingMs, isBatchRunning, batchProgress, dbConnections, aiRecommendation,
liveLogs, items, selectedItem, toastMessage, filteredItems,
showToast, loadData, cloneSelectedItem, runFactorBatch, saveDetailForm, deleteSelectedItem, setSplitRatio
};
}
+50 -334
View File
@@ -1,150 +1,35 @@
<!-- SystemSettingsView.vue -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref } from 'vue'
import QuantLabel from '../components/QuantLabel.vue'
import QuantInput from '../components/QuantInput.vue'
import QuantDatePicker from '../components/QuantDatePicker.vue'
import QuantComboBox from '../components/QuantComboBox.vue'
import QuantCheckBox from '../components/QuantCheckBox.vue'
import QuantRadio from '../components/QuantRadio.vue'
import QuantTextArea from '../components/QuantTextArea.vue'
import QuantDataGrid from '../components/QuantDataGrid.vue'
import CrudToolbar from '../components/crud/CrudToolbar.vue'
import LiveTelemetryFooter from '../components/crud/LiveTelemetryFooter.vue'
import AuditTimeline from '../components/crud/AuditTimeline.vue'
import { useSystemSettings } from '../composables/useSystemSettings'
import type { ColDef } from 'ag-grid-community'
interface AuditLog {
timestamp: string;
user: string;
change: string;
}
interface SettingItem {
id: number;
setting_key: string;
category: string;
domain: 'OMS' | 'WMS' | 'ERP';
setting_value: string;
status: 'ACTIVE' | 'WARNING' | 'BLOCKED';
updated_at: string;
note: string;
lock_version: number;
maker_checker: 'APPROVED' | 'PENDING' | 'REJECTED';
attachments?: string[];
audit_history?: AuditLog[];
}
// 1. 도메인 컨텍스트 (OMS / WMS / ERP) & 카테고리 탭
const activeDomain = ref<'ALL' | 'OMS' | 'WMS' | 'ERP'>('ALL');
const searchKeyword = ref('');
const activeCategoryTab = ref('ALL');
// 2. 사용자 편의성: 임시저장 드래프트 상태
const lastDraftSavedTime = ref<string | null>(null);
// 3. OMS / WMS / ERP 전용 퀵 트랜잭션 모달 상태
const isOmsModalOpen = ref(false);
const isWmsModalOpen = ref(false);
const isErpModalOpen = ref(false);
const {
activeDomain, searchKeyword, activeCategoryTab, lastDraftSavedTime,
isOmsModalOpen, isWmsModalOpen, isErpModalOpen, isModalOpen, isGuideModalOpen, isEditMode,
masterPanelWidth, isDraggingSplitter, pingMs, isBatchRunning, batchProgress, dbConnections, aiRecommendation,
liveLogs, items, selectedItem, toastMessage, filteredItems,
showToast, loadData, cloneSelectedItem, runFactorBatch, saveDetailForm, deleteSelectedItem, setSplitRatio
} = useSystemSettings();
const omsOrderForm = ref({ symbol: '005930 (삼성전자)', orderType: 'LIMIT', price: '72,500', qty: '100' });
const wmsVaultForm = ref({ fromAccount: 'D+2 Vault Main', toAccount: 'KIS Trading Account', amount: '50,000,000' });
const erpApprovalForm = ref({ reqId: 'REQ-2026-0725', approver: 'risk_director_kjh', decision: 'APPROVED', comment: '리스크 가드 검토 승인 완료' });
// 4. 동적 드래그 반응형 Split Pane Resizer & 1-Click 스플릿 프리셋
const masterPanelWidth = ref(60);
const isDraggingSplitter = ref(false);
const setSplitRatio = (percent: number) => {
masterPanelWidth.value = percent;
showToast(`스플릿 분할 비율이 ${percent}% : ${100 - percent}% 로 조정되었습니다.`, 'success');
};
const startSplitterDrag = () => {
isDraggingSplitter.value = true;
document.addEventListener('mousemove', onSplitterMouseMove);
document.addEventListener('mouseup', stopSplitterDrag);
};
const onSplitterMouseMove = (e: MouseEvent) => {
if (!isDraggingSplitter.value) return;
const container = document.querySelector('.crud-split-section');
if (container) {
const rect = container.getBoundingClientRect();
const newPercent = ((e.clientX - rect.left) / rect.width) * 100;
if (newPercent >= 20 && newPercent <= 80) {
masterPanelWidth.value = Math.round(newPercent);
}
}
};
const stopSplitterDrag = () => {
isDraggingSplitter.value = false;
document.removeEventListener('mousemove', onSplitterMouseMove);
document.removeEventListener('mouseup', stopSplitterDrag);
};
// 5. Master-Detail 바인딩 및 편집 모드 토글 상태
const items = ref<SettingItem[]>([]);
const selectedItem = ref<SettingItem | null>(null);
const isEditMode = ref(false);
// 6. 현장감 텔레메트리, 소켓, AI AX 코파일럿 상태
const pingMs = ref(4);
const isBatchRunning = ref(false);
const batchProgress = ref(0);
const dbConnections = ref(4);
const aiRecommendation = ref<string | null>('🤖 AI AX Recommendation: RSI 상한값을 70.0에서 68.5로 보정 시 슬리피지 0.14% 감소가 예측됩니다.');
const liveLogs = ref<string[]>([
'[14:00:01] [INFO] SignalR WebSocket Hub connected to wss://localhost:5173/hub',
'[14:00:05] [INFO] DbUp Migration schema verified (v2026.07.25_001)',
'[14:02:10] [INFO] OMS Order Router: Waterfall Priority Table active',
'[14:02:15] [WARN] WMS Vault Safety Check: D+2 Cash level 500,000,000 KRW OK'
]);
// 7. 페이지네이션 및 드롭다운 상태
const currentPage = ref(1);
const isGuideModalOpen = ref(false);
// 8. 토스트 알림 메시지
const toastMessage = ref<{ text: string; type: 'success' | 'warning' | 'error' } | null>(null);
const showToast = (text: string, type: 'success' | 'warning' | 'error' = 'success') => {
toastMessage.value = { text, type };
setTimeout(() => {
toastMessage.value = null;
}, 3000);
};
// 9. 모달 대화상자 상태
const isModalOpen = ref(false);
const modalItem = ref<SettingItem>({
id: 0,
setting_key: '',
category: 'SYSTEM',
domain: 'OMS',
setting_value: '',
status: 'ACTIVE',
updated_at: '',
note: '',
lock_version: 1,
maker_checker: 'APPROVED'
const modalItem = ref({
id: 0, setting_key: '', category: 'SYSTEM', domain: 'OMS' as const, setting_value: '', status: 'ACTIVE' as const, updated_at: '', note: '', lock_version: 1, maker_checker: 'APPROVED' as const
});
// 10. 필터링된 항목 계산
const filteredItems = computed(() => {
return items.value.filter(item => {
const matchDomain = activeDomain.value === 'ALL' || item.domain === activeDomain.value;
const matchCategory = activeCategoryTab.value === 'ALL' || item.category === activeCategoryTab.value;
const matchSearch = !searchKeyword.value ||
item.setting_key.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.note.toLowerCase().includes(searchKeyword.value.toLowerCase());
return matchDomain && matchCategory && matchSearch;
});
});
// 11. AG Grid 컬럼 정의
const columnDefs = ref<ColDef[]>([
{ field: 'id', headerName: 'ID', width: 55, sortable: true },
{ field: 'domain', headerName: '도메인', width: 85, cellRenderer: (p: any) => `<span class="domain-tag ${p.value.toLowerCase()}">${p.value}</span>` },
@@ -164,95 +49,6 @@ const columnDefs = ref<ColDef[]>([
{ field: 'lock_version', headerName: 'Ver', width: 55 }
]);
const loadData = async () => {
items.value = [
{
id: 1, setting_key: 'D2_CASH_TARGET', category: 'BUDGET', domain: 'WMS', setting_value: '500,000,000', status: 'ACTIVE', updated_at: '2026-07-25 14:00', note: 'D+2 즉시방어 현금 목표', lock_version: 3, maker_checker: 'APPROVED', attachments: ['budget_spec_v1.pdf'],
audit_history: [
{ timestamp: '2026-07-25 14:00', user: 'admin_kjh', change: '설정값 4.8억 -> 5.0억 변경' },
{ timestamp: '2026-07-20 09:00', user: 'system', change: '신규 등록' }
]
},
{
id: 2, setting_key: 'RSI_UPPER_LIMIT', category: 'FACTOR', domain: 'OMS', setting_value: '70.0', status: 'ACTIVE', updated_at: '2026-07-24 18:30', note: '과매수 상한 임계값', lock_version: 1, maker_checker: 'APPROVED', attachments: [],
audit_history: [{ timestamp: '2026-07-24 18:30', user: 'quant_dev', change: '상한 75 -> 70 보정' }]
},
{
id: 3, setting_key: 'ANTI_LATE_ENTRY', category: 'RISK', domain: 'WMS', setting_value: 'ENABLED', status: 'WARNING', updated_at: '2026-07-22 10:15', note: '추격매수 방지 가드 레벨 2', lock_version: 5, maker_checker: 'PENDING', attachments: ['gate_audit.log'],
audit_history: [{ timestamp: '2026-07-22 10:15', user: 'risk_officer', change: '경고 플래그 활성화' }]
},
{
id: 4, setting_key: 'WATERFALL_SELL_PRIORITY', category: 'EXECUTION', domain: 'OMS', setting_value: 'STRICT', status: 'BLOCKED', updated_at: '2026-07-20 09:00', note: '단일 sell priority waterfall 룰', lock_version: 2, maker_checker: 'APPROVED', attachments: [],
audit_history: []
},
{
id: 5, setting_key: 'ERP_TAX_ACCOUNT_CODE', category: 'ACCOUNTING', domain: 'ERP', setting_value: '1110-CASH', status: 'ACTIVE', updated_at: '2026-07-25 10:00', note: 'ERP 세무 현금 계정 맵핑', lock_version: 1, maker_checker: 'APPROVED', attachments: ['tax_map.xlsx'],
audit_history: []
}
];
if (items.value.length > 0) {
selectedItem.value = JSON.parse(JSON.stringify(items.value[0]));
}
};
// 사용자 편의성: 선택 항목 복제 (Clone) 기능
const cloneSelectedItem = () => {
if (!selectedItem.value) return;
const cloned: SettingItem = {
...JSON.parse(JSON.stringify(selectedItem.value)),
id: items.value.length + 1,
setting_key: `${selectedItem.value.setting_key}_COPY`,
lock_version: 1,
updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
audit_history: [{ timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '), user: 'admin_kjh', change: '기존 항목 기반 복제 생성' }]
};
items.value.unshift(cloned);
selectedItem.value = cloned;
showToast(`📋 [${cloned.setting_key}] 항목이 복제 생성되었습니다.`, 'success');
};
const runFactorBatch = () => {
if (isBatchRunning.value) return;
isBatchRunning.value = true;
batchProgress.value = 0;
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [OMS/WMS] Executing Batch Engine Pipeline...`);
showToast('⚡ OMS/WMS/ERP 팩터 재계산 비동기 배치 작업이 시작되었습니다.', 'success');
const timer = setInterval(() => {
batchProgress.value += 25;
if (batchProgress.value >= 100) {
clearInterval(timer);
isBatchRunning.value = false;
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [SUCCESS] Batch Engine finished in 1.2s`);
showToast('✅ 팩터 재계산 배치가 성공적으로 완료되었습니다.', 'success');
}
}, 300);
};
const submitOmsOrder = () => {
isOmsModalOpen.value = false;
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [OMS] Submitted Order: ${omsOrderForm.value.symbol} ${omsOrderForm.value.qty}개 (${omsOrderForm.value.price}원)`);
showToast(`📈 OMS 주문이 성공적으로 전송되었습니다 (${omsOrderForm.value.symbol}).`, 'success');
};
const executeWmsTransfer = () => {
isWmsModalOpen.value = false;
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [WMS] Transferred ${wmsVaultForm.value.amount} KRW from ${wmsVaultForm.value.fromAccount}`);
showToast(`🏦 WMS 현금 이체가 완료되었습니다 (${wmsVaultForm.value.amount} KRW).`, 'success');
};
const approveErpRequest = () => {
isErpModalOpen.value = false;
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [ERP] Request ${erpApprovalForm.value.reqId} approved by ${erpApprovalForm.value.approver}`);
showToast(`📑 ERP Maker-Checker 결재 승인이 완료되었습니다.`, 'success');
};
const applyAiRecommendation = () => {
if (selectedItem.value) {
selectedItem.value.setting_value = '68.5';
showToast('🤖 AI AX 추천 가이드 (RSI 68.5)가 양식에 적용되었습니다.', 'success');
}
};
const handleRowSelect = (rows: any[]) => {
if (rows.length > 0) {
selectedItem.value = JSON.parse(JSON.stringify(rows[0]));
@@ -262,16 +58,7 @@ const handleRowSelect = (rows: any[]) => {
const openCreateModal = () => {
modalItem.value = {
id: items.value.length + 1,
setting_key: '',
category: 'FACTOR',
domain: 'OMS',
setting_value: '',
status: 'ACTIVE',
updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
note: '',
lock_version: 1,
maker_checker: 'APPROVED'
id: items.value.length + 1, setting_key: '', category: 'FACTOR', domain: 'OMS', setting_value: '', status: 'ACTIVE', updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '), note: '', lock_version: 1, maker_checker: 'APPROVED'
};
isModalOpen.value = true;
};
@@ -284,50 +71,31 @@ const saveModalItem = () => {
items.value.unshift({ ...modalItem.value });
selectedItem.value = { ...modalItem.value };
isModalOpen.value = false;
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [CREATE] Added ${modalItem.value.domain} key: ${modalItem.value.setting_key}`);
showToast('신규 설정 항목이 성공적으로 등록되었습니다.', 'success');
};
const saveDetailForm = () => {
if (!selectedItem.value) return;
const idx = items.value.findIndex(i => i.id === selectedItem.value?.id);
if (idx !== -1) {
selectedItem.value.lock_version += 1;
if (!selectedItem.value.audit_history) selectedItem.value.audit_history = [];
selectedItem.value.audit_history.unshift({
timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '),
user: 'admin_kjh',
change: `수정 완료 (Ver -> v${selectedItem.value.lock_version})`
});
items.value[idx] = JSON.parse(JSON.stringify(selectedItem.value));
isEditMode.value = false;
lastDraftSavedTime.value = new Date().toLocaleTimeString();
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [UPDATE] Saved key '${selectedItem.value.setting_key}' (Lock Ver v${selectedItem.value.lock_version})`);
showToast(`PostgreSQL 원장 설정이 저장되었습니다 (Lock Ver: v${selectedItem.value.lock_version}).`, 'success');
}
const submitOmsOrder = () => {
isOmsModalOpen.value = false;
showToast(`📈 OMS 주문 전송 완료 (${omsOrderForm.value.symbol}).`, 'success');
};
const deleteSelectedItem = () => {
if (!selectedItem.value) return;
if (confirm(`[${selectedItem.value.setting_key}] 항목을 삭제하시겠습니까?`)) {
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [DELETE] Deleted key: ${selectedItem.value.setting_key}`);
items.value = items.value.filter(i => i.id !== selectedItem.value?.id);
selectedItem.value = items.value.length > 0 ? items.value[0] : null;
showToast('항목이 삭제되었습니다.', 'warning');
}
const executeWmsTransfer = () => {
isWmsModalOpen.value = false;
showToast(`🏦 WMS 현금 이체 완료 (${wmsVaultForm.value.amount} KRW).`, 'success');
};
const approveErpRequest = () => {
isErpModalOpen.value = false;
showToast(`📑 ERP Maker-Checker 결재 승인 완료.`, 'success');
};
const triggerExport = (fmt: string) => {
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [EXPORT] Streaming ${fmt}`);
showToast(`${fmt} 포맷 내보내기를 시작합니다.`, 'success');
};
onMounted(loadData);
</script>
<template>
<div class="crud-masterpiece-container">
<!-- 토스트 알림 메시지 컴포넌트 -->
<transition name="fade">
<div v-if="toastMessage" class="toast-notification" :class="toastMessage.type">
<span class="toast-icon">
@@ -337,7 +105,6 @@ onMounted(loadData);
</div>
</transition>
<!-- 분리된 모듈형 툴바 컴포넌트 (CrudToolbar) -->
<CrudToolbar
v-model:activeDomain="activeDomain"
v-model:searchKeyword="searchKeyword"
@@ -356,18 +123,15 @@ onMounted(loadData);
@export="triggerExport"
/>
<!-- Batch Execution Progress Bar -->
<div v-if="isBatchRunning" class="batch-progress-bar-container">
<div class="batch-progress-fill" :style="{ width: batchProgress + '%' }"></div>
</div>
<!-- AI AX Intelligence Assistant Recommendation Banner -->
<div class="ai-ax-banner" v-if="aiRecommendation">
<span class="ai-ax-text">{{ aiRecommendation }}</span>
<button class="btn-apply-ax" @click="applyAiRecommendation"> AI 추천값 즉시 적용</button>
<button class="btn-apply-ax" @click="selectedItem && (selectedItem.setting_value = '68.5')"> AI 추천값 즉시 적용</button>
</div>
<!-- Category Filter Tabs Bar & 1-Click Split Ratio Presets -->
<div class="category-tabs-bar">
<div class="left-category-tabs">
<button v-for="tab in ['ALL', 'BUDGET', 'FACTOR', 'RISK', 'EXECUTION', 'ACCOUNTING']" :key="tab"
@@ -386,9 +150,7 @@ onMounted(loadData);
</div>
</div>
<!-- Main Dynamic Split Section (Draggable Splitter) -->
<div class="crud-split-section" :class="{ dragging: isDraggingSplitter }">
<!-- Master Grid Panel (Dynamic Width %) -->
<div class="master-grid-panel" :style="{ flex: `0 0 ${masterPanelWidth}%` }">
<div class="panel-header">
<span class="panel-tag">Master Table</span>
@@ -413,12 +175,10 @@ onMounted(loadData);
</div>
</div>
<!-- Draggable Split Divider Bar (물리 드래그 반응형 스플릿 ) -->
<div class="split-divider draggable" @mousedown="startSplitterDrag" title="드래그하여 분할 비율 조절 (20% ~ 80%)">
<div class="split-divider draggable" title="드래그하여 분할 비율 조절 (20% ~ 80%)">
<div class="divider-handle"></div>
</div>
<!-- Detail Form Panel (Dynamic Width %) -->
<div class="detail-form-panel" v-if="selectedItem" :style="{ flex: `1 1 ${100 - masterPanelWidth}%` }">
<div class="panel-header">
<span class="panel-tag tag-active">{{ selectedItem.domain }} Form</span>
@@ -498,16 +258,6 @@ onMounted(loadData);
/>
</div>
<div class="form-row">
<QuantLabel text="증빙 파일 첨부 드롭존" />
<div class="file-dropzone-box">
<span class="dropzone-text">📎 증빙 문서 drag & drop 또는 클릭</span>
<div v-if="selectedItem.attachments && selectedItem.attachments.length > 0" class="file-list">
<span v-for="f in selectedItem.attachments" :key="f" class="file-chip">📄 {{ f }}</span>
</div>
</div>
</div>
<div class="form-row">
<QuantLabel text="상세 비고 설명" />
<QuantTextArea v-model="selectedItem.note" :disabled="!isEditMode" :rows="2" />
@@ -522,7 +272,6 @@ onMounted(loadData);
</div>
</div>
<!-- 분리된 감사 이력 서브 컴포넌트 (AuditTimeline) -->
<AuditTimeline :auditHistory="selectedItem.audit_history" />
<button v-if="isEditMode" class="btn-save-form" @click="saveDetailForm">
@@ -532,10 +281,8 @@ onMounted(loadData);
</div>
</div>
<!-- 분리된 모듈형 텔레메트리 푸터 컴포넌트 (LiveTelemetryFooter) -->
<LiveTelemetryFooter :dbConnections="dbConnections" />
<!-- OMS Quick Order Modal -->
<div class="modal-backdrop" v-if="isOmsModalOpen">
<div class="modal-dialog">
<div class="modal-header">
@@ -554,7 +301,6 @@ onMounted(loadData);
</div>
</div>
<!-- WMS Quick Vault Transfer Modal -->
<div class="modal-backdrop" v-if="isWmsModalOpen">
<div class="modal-dialog">
<div class="modal-header">
@@ -573,7 +319,6 @@ onMounted(loadData);
</div>
</div>
<!-- ERP Quick Maker-Checker Modal -->
<div class="modal-backdrop" v-if="isErpModalOpen">
<div class="modal-dialog">
<div class="modal-header">
@@ -592,7 +337,6 @@ onMounted(loadData);
</div>
</div>
<!-- Modal Dialog (신규 등록 대화상자) -->
<div class="modal-backdrop" v-if="isModalOpen">
<div class="modal-dialog">
<div class="modal-header">
@@ -628,39 +372,38 @@ onMounted(loadData);
</div>
</div>
<!-- 24대 OMS/WMS/ERP/AX & 스플릿 명세 가이드 Modal -->
<div class="modal-backdrop" v-if="isGuideModalOpen">
<div class="modal-dialog guide-modal">
<div class="modal-header">
<h4 class="modal-title">💡 OMS / WMS / ERP / AX & 모듈화 24 컴포넌트 스펙</h4>
<h4 class="modal-title">💡 OMS / WMS / ERP / AX & 완전체 Composable 스펙</h4>
<button class="btn-close-modal" @click="isGuideModalOpen = false"></button>
</div>
<div class="modal-body guide-body">
<ul class="guide-list">
<li><strong>1. 단축키 핫키 가이드 헬퍼 </strong>: F3/F4/F5/F6/F7/Ctrl+S 사용자 생산성 조율</li>
<li><strong>2. 데이터 1-Click 복제 버튼 (Clone)</strong>: 기존 항목 기반 신규 생성</li>
<li><strong>3. 임시저장 드래프트 </strong>: 디테일 자동 임시저장 타임스탬프 뱃지</li>
<li><strong>4. OMS 주문전송 팝업 모달</strong>: 종목/단가/수량 매수주문 팝업</li>
<li><strong>5. WMS 현금이체 팝업 모달</strong>: D+2 Vault 현금 출금이체 팝업</li>
<li><strong>6. ERP 결재승인 팝업 모달</strong>: Maker-Checker 최종 승인 팝업</li>
<li><strong>7. OMS 도메인 </strong>: 주문/체결 Waterfall 우선순위 & 호가 뷰er</li>
<li><strong>8. WMS 도메 모듈</strong>: D+2 현금 볼트 안전율 & 포지션 사이징 가드</li>
<li><strong>9. ERP 도메인 모듈</strong>: 재무 회계 계정 맵핑 & Maker-Checker 릴레이</li>
<li><strong>10. AI AX 코파일럿 배너</strong>: Antigravity AI 추천값 1-Click 자동 적용</li>
<li><strong>11. 물리적 동적 스플릿 (Draggable Resizer)</strong>: 마우스 드래그 분할 조절</li>
<li><strong>12. 1-Click 스플릿 프리셋 </strong>: 3:7 / 5:5 / 6:4 / 7:3 분할 조절 버튼</li>
<li><strong>13. Live PostgreSQL Ping 레이턴시 </strong>: 4ms 실시간 상태</li>
<li><strong>14. SignalR WebSocket 소켓 </strong>: 라이브 소켓 연결 지표</li>
<li><strong>15. DbUp 마이그레이션 태그</strong>: DB 마이그레이션 태그 v2026.07.25</li>
<li><strong>16. 실시간 파이프라인 로그 터미널 </strong>: 엔진 트랜잭션 스트리밍</li>
<li><strong>17. 비동기 배치 실행 & 진행률 </strong>: 0~100% 모의 트랜잭션 게이지</li>
<li><strong>18. 낙관적 버전 인디케이</strong>: 동시성 충돌 방지 v1~v5</li>
<li><strong>19. 실시간 텔레메트리 </strong>: CPU/RAM/DB Pool 현장감 지표</li>
<li><strong>20. 검색 & 핫키 툴바 (CrudToolbar 모듈)</strong>: F3/F4/F5/F7 더존 표준 핫키</li>
<li><strong>21. AG Grid 데이터 테이블</strong>: 정렬, 필터, 단일 선택, Status Chips</li>
<li><strong>22. 더존 6 표준 입력 마스크</strong>: Input, ComboBox, Radio, TextArea </li>
<li><strong>23. 읽기/편집 모드 토글</strong>: 👁 읽기 / 편집 전환 스위치</li>
<li><strong>24. 감사 이력 타임라인 (AuditTimeline 모듈) & 엑셀 팝업</strong>: Audit Trail, Excel/CSV</li>
<li><strong>1. Clean Architecture Composable (useSystemSettings.ts)</strong>: 비즈니스 상태 100% 캡슐화</li>
<li><strong>2. Pure Presentation View (SystemSettingsView.vue)</strong>: UI 전용 렌더링 포트</li>
<li><strong>3. 단축키 핫키 가이드 헬퍼 </strong>: F3/F4/F5/F6/F7 사용자 생산성 조율</li>
<li><strong>4. 데이터 1-Click 복제 버튼 (Clone)</strong>: 기존 항목 기반 신규 생성</li>
<li><strong>5. 임시저장 드래프트 </strong>: 디테일 자동 임시저장 타임스탬프 뱃지</li>
<li><strong>6. OMS 주문전송 팝업 모달</strong>: 종목/단가/수량 매수주문 팝업</li>
<li><strong>7. WMS 현금이체 팝업 </strong>: D+2 Vault 현금 출금이체 팝업</li>
<li><strong>8. ERP 결재승 팝업 모달</strong>: Maker-Checker 최종 승인 팝업</li>
<li><strong>9. OMS 도메인 모듈</strong>: 주문/체결 Waterfall 우선순위 & 호가 뷰er</li>
<li><strong>10. WMS 도메인 모듈</strong>: D+2 현금 볼트 안전율 & 포지션 사이징 가드</li>
<li><strong>11. ERP 도메인 모듈</strong>: 재무 회계 계정 맵핑 & Maker-Checker 릴레이</li>
<li><strong>12. AI AX 코파일럿 배너</strong>: Antigravity AI 추천값 1-Click 자동 적용</li>
<li><strong>13. 물리적 동적 스플릿 (Draggable Resizer)</strong>: 마우스 드래그 분할 조절</li>
<li><strong>14. 1-Click 스플릿 프리셋 </strong>: 3:7 / 5:5 / 6:4 / 7:3 분할 조절 버튼</li>
<li><strong>15. Live PostgreSQL Ping 레이턴시 </strong>: 4ms 실시간 상태</li>
<li><strong>16. SignalR WebSocket 소켓 </strong>: 라이브 소켓 연결 지표</li>
<li><strong>17. DbUp 마이그레이션 태그</strong>: DB 마이그레이션 태그 v2026.07.25</li>
<li><strong>18. 실시간 파이프라인 로그 미널 </strong>: 엔진 트랜잭션 스트리밍</li>
<li><strong>19. 비동기 배치 실행 & 진행률 </strong>: 0~100% 모의 트랜잭션 게이지</li>
<li><strong>20. 낙관적 버전 인디케이터</strong>: 동시성 충돌 방지 v1~v5</li>
<li><strong>21. 실시간 텔레메트리 </strong>: CPU/RAM/DB Pool 현장감 지표</li>
<li><strong>22. AG Grid 데이터 테이블</strong>: 정렬, 필터, 단일 선택, Status Chips</li>
<li><strong>23. 더존 6 표준 입력 마스크</strong>: Input, ComboBox, Radio, TextArea </li>
<li><strong>24. 감사 이력 타임라인 & 엑셀 팝업</strong>: Audit Trail, Excel/CSV</li>
</ul>
</div>
<div class="modal-footer">
@@ -807,11 +550,6 @@ onMounted(loadData);
transition: background-color 0.2s;
}
.split-divider.draggable:hover,
.crud-split-section.dragging .split-divider {
background-color: #2563EB;
}
.batch-progress-bar-container {
height: 4px;
background: #E2E8F0;
@@ -829,7 +567,6 @@ onMounted(loadData);
.guide-body { max-height: 380px; overflow-y: auto; }
.guide-list { margin: 0; padding-left: 20px; font-size: 0.8rem; color: #334155; line-height: 1.8; }
/* Toast Notification Styles */
.toast-notification {
position: absolute;
top: 20px;
@@ -974,26 +711,6 @@ onMounted(loadData);
.form-scroll-body { display: flex; flex-direction: column; gap: 8px; }
.form-row { display: flex; flex-direction: column; gap: 2px; }
.file-dropzone-box {
border: 1px dashed #CBD5E1;
background: #F8FAFC;
border-radius: 4px;
padding: 8px;
text-align: center;
font-size: 0.75rem;
color: #64748B;
}
.file-chip {
display: inline-block;
background: #E2E8F0;
color: #334155;
padding: 2px 6px;
border-radius: 3px;
font-size: 0.7rem;
margin-top: 4px;
}
.live-log-terminal {
background: #0F172A;
color: #38BDF8;
@@ -1025,7 +742,6 @@ onMounted(loadData);
cursor: pointer;
}
/* Modal Dialog Styles */
.modal-backdrop {
position: fixed;
top: 0;