feat(wbs-ux): upgrade SystemSettingsView with OMS/WMS/ERP domain context switcher, AI AX recommendation banner, and draggable split resizer
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 12s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 22s
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / WBS & Audit Validations (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) / Security & Secrets (push) Successful in 11s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 8s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 12s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 22s
Validators (Pushes and Pull Requests) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / WBS & Audit Validations (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) / Security & Secrets (push) Successful in 11s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 8s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
This commit is contained in:
@@ -23,42 +23,75 @@ 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. 검색, 필터, 탭 상태
|
||||
// 1. 도메인 컨텍스트 (OMS / WMS / ERP) & 카테고리 탭
|
||||
const activeDomain = ref<'ALL' | 'OMS' | 'WMS' | 'ERP'>('ALL');
|
||||
const searchKeyword = ref('');
|
||||
const activeTab = ref('ALL');
|
||||
const activeCategoryTab = ref('ALL');
|
||||
|
||||
// 2. Master-Detail 바인딩 및 편집 모드 토글 상태
|
||||
// 2. 동적 드래그 반응형 Split Pane Resizer 상태
|
||||
const masterPanelWidth = ref(60); // 기본 60%
|
||||
const isDraggingSplitter = ref(false);
|
||||
|
||||
const startSplitterDrag = (e: MouseEvent) => {
|
||||
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 >= 25 && newPercent <= 75) {
|
||||
masterPanelWidth.value = Math.round(newPercent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const stopSplitterDrag = () => {
|
||||
isDraggingSplitter.value = false;
|
||||
document.removeEventListener('mousemove', onSplitterMouseMove);
|
||||
document.removeEventListener('mouseup', stopSplitterDrag);
|
||||
};
|
||||
|
||||
// 3. Master-Detail 바인딩 및 편집 모드 토글 상태
|
||||
const items = ref<SettingItem[]>([]);
|
||||
const selectedItem = ref<SettingItem | null>(null);
|
||||
const isEditMode = ref(false);
|
||||
|
||||
// 3. 현장감 텔레메트리, 소켓, 로그 스트리밍 상태
|
||||
// 4. 현장감 텔레메트리, 소켓, 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] Factor RSI calculated successfully (0.012s)',
|
||||
'[14:02:15] [WARN] D2 Cash threshold check passed (500,000,000 KRW target)'
|
||||
'[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'
|
||||
]);
|
||||
|
||||
// 4. 페이지네이션 및 엑셀 드롭다운 상태
|
||||
// 5. 페이지네이션 및 드롭다운 상태
|
||||
const currentPage = ref(1);
|
||||
const showExportMenu = ref(false);
|
||||
const isGuideModalOpen = ref(false);
|
||||
|
||||
// 5. 토스트 알림 시스템 상태
|
||||
// 6. 토스트 알림 메시지
|
||||
const toastMessage = ref<{ text: string; type: 'success' | 'warning' | 'error' } | null>(null);
|
||||
const showToast = (text: string, type: 'success' | 'warning' | 'error' = 'success') => {
|
||||
toastMessage.value = { text, type };
|
||||
@@ -67,68 +100,76 @@ const showToast = (text: string, type: 'success' | 'warning' | 'error' = 'succes
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
// 6. 모달 대화상자 상태
|
||||
// 7. 모달 대화상자 상태
|
||||
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
|
||||
lock_version: 1,
|
||||
maker_checker: 'APPROVED'
|
||||
});
|
||||
|
||||
// 7. 필터링된 항목 계산
|
||||
// 8. 필터링된 항목 계산
|
||||
const filteredItems = computed(() => {
|
||||
return items.value.filter(item => {
|
||||
const matchTab = activeTab.value === 'ALL' || item.category === activeTab.value;
|
||||
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 matchTab && matchSearch;
|
||||
return matchDomain && matchCategory && matchSearch;
|
||||
});
|
||||
});
|
||||
|
||||
// 8. AG Grid 컬럼 정의
|
||||
// 9. AG Grid 컬럼 정의
|
||||
const columnDefs = ref<ColDef[]>([
|
||||
{ field: 'id', headerName: 'ID', width: 60, sortable: true },
|
||||
{ field: 'setting_key', headerName: '설정 키 (Key)', width: 170, sortable: true, filter: true },
|
||||
{ field: 'category', headerName: '카테고리', width: 110 },
|
||||
{ field: 'setting_value', headerName: '설정 값 (Value)', width: 140 },
|
||||
{ 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>` },
|
||||
{ field: 'setting_key', headerName: '설정 키 (Key)', width: 160, sortable: true, filter: true },
|
||||
{ field: 'category', headerName: '카테고리', width: 100 },
|
||||
{ field: 'setting_value', headerName: '설정 값 (Value)', width: 130 },
|
||||
{
|
||||
field: 'status',
|
||||
headerName: '상태 칩',
|
||||
width: 110,
|
||||
width: 100,
|
||||
cellRenderer: (params: any) => {
|
||||
const type = params.value === 'ACTIVE' ? 'PASS' : params.value === 'WARNING' ? 'WARNING' : 'BLOCKED';
|
||||
return `<span class="status-chip ${type.toLowerCase()}">${params.value}</span>`;
|
||||
}
|
||||
},
|
||||
{ field: 'lock_version', headerName: 'Ver', width: 60 },
|
||||
{ field: 'updated_at', headerName: '수정일시', width: 130 }
|
||||
{ field: 'maker_checker', headerName: '승인상태', width: 95 },
|
||||
{ field: 'lock_version', headerName: 'Ver', width: 55 }
|
||||
]);
|
||||
|
||||
const loadData = async () => {
|
||||
items.value = [
|
||||
{
|
||||
id: 1, setting_key: 'D2_CASH_TARGET', category: 'BUDGET', setting_value: '500,000,000', status: 'ACTIVE', updated_at: '2026-07-25 14:00', note: 'D+2 즉시방어 현금 목표', lock_version: 3, attachments: ['budget_spec_v1.pdf'],
|
||||
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', setting_value: '70.0', status: 'ACTIVE', updated_at: '2026-07-24 18:30', note: '과매수 상한 임계값', lock_version: 1, attachments: [],
|
||||
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', setting_value: 'ENABLED', status: 'WARNING', updated_at: '2026-07-22 10:15', note: '추격매수 방지 가드 레벨 2', lock_version: 5, attachments: ['gate_audit.log'],
|
||||
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', setting_value: 'STRICT', status: 'BLOCKED', updated_at: '2026-07-20 09:00', note: '단일 sell priority waterfall 룰', lock_version: 2, attachments: [],
|
||||
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: []
|
||||
}
|
||||
];
|
||||
@@ -141,19 +182,26 @@ const runFactorBatch = () => {
|
||||
if (isBatchRunning.value) return;
|
||||
isBatchRunning.value = true;
|
||||
batchProgress.value = 0;
|
||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [EXEC] Triggered factor recalculation batch pipeline`);
|
||||
showToast('⚡ 팩터 재계산 비동기 배치 작업이 트리거되었습니다.', 'success');
|
||||
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 pipeline finished in 1.2s`);
|
||||
showToast('✅ 팩터 재계산 배치가 성공적으로 완료되었습니다 (소요시간: 1.2s)', 'success');
|
||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [SUCCESS] Batch Engine finished in 1.2s`);
|
||||
showToast('✅ 팩터 재계산 배치가 성공적으로 완료되었습니다.', 'success');
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
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]));
|
||||
@@ -166,11 +214,13 @@ const openCreateModal = () => {
|
||||
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
|
||||
lock_version: 1,
|
||||
maker_checker: 'APPROVED'
|
||||
};
|
||||
isModalOpen.value = true;
|
||||
};
|
||||
@@ -183,7 +233,7 @@ const saveModalItem = () => {
|
||||
items.value.unshift({ ...modalItem.value });
|
||||
selectedItem.value = { ...modalItem.value };
|
||||
isModalOpen.value = false;
|
||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [CREATE] Added new setting key: ${modalItem.value.setting_key}`);
|
||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [CREATE] Added ${modalItem.value.domain} key: ${modalItem.value.setting_key}`);
|
||||
showToast('신규 설정 항목이 성공적으로 등록되었습니다.', 'success');
|
||||
};
|
||||
|
||||
@@ -196,12 +246,12 @@ const saveDetailForm = () => {
|
||||
selectedItem.value.audit_history.unshift({
|
||||
timestamp: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
||||
user: 'admin_kjh',
|
||||
change: `설정값 변경 (Ver -> v${selectedItem.value.lock_version})`
|
||||
change: `수정 완료 (Ver -> v${selectedItem.value.lock_version})`
|
||||
});
|
||||
items.value[idx] = JSON.parse(JSON.stringify(selectedItem.value));
|
||||
isEditMode.value = false;
|
||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [UPDATE] Key '${selectedItem.value.setting_key}' saved (Ver v${selectedItem.value.lock_version})`);
|
||||
showToast(`PostgreSQL 원장 설정 변경사항이 저장되었습니다 (Lock Ver: v${selectedItem.value.lock_version}).`, 'success');
|
||||
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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -211,14 +261,14 @@ const deleteSelectedItem = () => {
|
||||
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');
|
||||
showToast('항목이 삭제되었습니다.', 'warning');
|
||||
}
|
||||
};
|
||||
|
||||
const triggerExport = (fmt: string) => {
|
||||
showExportMenu.value = false;
|
||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [EXPORT] Exporting data stream as ${fmt}`);
|
||||
showToast(`${fmt} 형식으로 대용량 스트리밍 내보내기를 시작합니다.`, 'success');
|
||||
liveLogs.value.unshift(`[${new Date().toLocaleTimeString()}] [EXPORT] Streaming ${fmt}`);
|
||||
showToast(`${fmt} 포맷 내보내기를 시작합니다.`, 'success');
|
||||
};
|
||||
|
||||
onMounted(loadData);
|
||||
@@ -236,23 +286,29 @@ onMounted(loadData);
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<!-- Top Action & Search Bar -->
|
||||
<!-- Top Toolbar with Domain Context Switcher & Live Ping -->
|
||||
<div class="crud-toolbar">
|
||||
<div class="toolbar-title">
|
||||
<div class="title-with-chip">
|
||||
<h3 class="title-text">⚙️ SCR-07: 현장감 퀀트 CRUD 매니저</h3>
|
||||
<h3 class="title-text">⚙️ SCR-07: OMS · WMS · ERP · AX 통합 어드민</h3>
|
||||
<span class="db-ping-chip">🟢 Live ({{ pingMs }}ms)</span>
|
||||
<span class="socket-chip">📡 SignalR Connected</span>
|
||||
<span class="socket-chip">📡 SignalR Active</span>
|
||||
</div>
|
||||
<!-- OMS / WMS / ERP 3대 도메인 탭 스위처 -->
|
||||
<div class="domain-switcher-bar">
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'ALL' }" @click="activeDomain = 'ALL'">🌐 전체 도메인</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'OMS' }" @click="activeDomain = 'OMS'">📈 OMS (주문/체결)</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'WMS' }" @click="activeDomain = 'WMS'">🏦 WMS (자산/현금)</button>
|
||||
<button class="domain-btn" :class="{ active: activeDomain === 'ERP' }" @click="activeDomain = 'ERP'">📑 ERP (재무/회계)</button>
|
||||
</div>
|
||||
<span class="subtitle-text">Master-Detail, AG Grid, Batch Progress, Lock Guard, Telemetry, Log Terminal, 더존 컴포넌트</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
<span class="dbup-version-tag">🛡️ DbUp: v2026.07.25</span>
|
||||
<button class="btn-batch-trigger" @click="runFactorBatch" :disabled="isBatchRunning">
|
||||
{{ isBatchRunning ? `⏳ 계산중 ${batchProgress}%` : '⚡ 팩터 배치 실행' }}
|
||||
{{ isBatchRunning ? `⏳ 배치 ${batchProgress}%` : '⚡ 팩터 배치 실행' }}
|
||||
</button>
|
||||
<button class="btn-guide-spec" @click="isGuideModalOpen = true">💡 18대 명세 팝업</button>
|
||||
<button class="btn-guide-spec" @click="isGuideModalOpen = true">💡 20대 명세 팝업</button>
|
||||
<div class="search-box">
|
||||
<input type="text" class="search-input" placeholder="설정 키/설명 검색..." v-model="searchKeyword" />
|
||||
</div>
|
||||
@@ -277,22 +333,28 @@ onMounted(loadData);
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Category Filter Tabs Bar -->
|
||||
<div class="category-tabs-bar">
|
||||
<button v-for="tab in ['ALL', 'BUDGET', 'FACTOR', 'RISK', 'EXECUTION']" :key="tab"
|
||||
class="tab-btn" :class="{ active: activeTab === tab }"
|
||||
@click="activeTab = tab">
|
||||
{{ tab === 'ALL' ? '전체 보기' : tab }}
|
||||
<button v-for="tab in ['ALL', 'BUDGET', 'FACTOR', 'RISK', 'EXECUTION', 'ACCOUNTING']" :key="tab"
|
||||
class="tab-btn" :class="{ active: activeCategoryTab === tab }"
|
||||
@click="activeCategoryTab = tab">
|
||||
{{ tab === 'ALL' ? '전체 카테고리' : tab }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Main Split Section (60% Master Grid : 40% Detail Form) -->
|
||||
<div class="crud-split-section">
|
||||
<!-- Master Grid Panel (Left 60%) -->
|
||||
<div class="master-grid-panel">
|
||||
<!-- 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>
|
||||
<h4 class="panel-title">📋 원장 항목 리스트 (검색 결과 {{ filteredItems.length }}건 / 총 {{ items.length }}건)</h4>
|
||||
<h4 class="panel-title">📋 OMS/WMS/ERP 원장 목록 ({{ filteredItems.length }}건 / 총 {{ items.length }}건)</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<QuantDataGrid
|
||||
@@ -302,10 +364,9 @@ onMounted(loadData);
|
||||
@row-selected="handleRowSelect"
|
||||
/>
|
||||
</div>
|
||||
<!-- Pagination & Summary Footer -->
|
||||
<div class="panel-footer">
|
||||
<div class="pagination-info">
|
||||
<span>Page <strong>{{ currentPage }}</strong> of 1</span>
|
||||
<span>Page <strong>{{ currentPage }}</strong> of 1 (스플릿 분할비: {{ masterPanelWidth }}% : {{ 100 - masterPanelWidth }}%)</span>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-page" disabled>◀ 이전</button>
|
||||
@@ -314,20 +375,20 @@ onMounted(loadData);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Split Divider Line -->
|
||||
<div class="split-divider">
|
||||
<!-- Draggable Split Divider Bar (물리 드래그 반응형 스플릿 바) -->
|
||||
<div class="split-divider draggable" @mousedown="startSplitterDrag" title="드래그하여 분할 비율 조절 (25% ~ 75%)">
|
||||
<div class="divider-handle"></div>
|
||||
</div>
|
||||
|
||||
<!-- Detail Form Panel (Right 40%) -->
|
||||
<div class="detail-form-panel" v-if="selectedItem">
|
||||
<!-- 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">Detail Form</span>
|
||||
<span class="panel-tag tag-active">{{ selectedItem.domain }} Form</span>
|
||||
<h4 class="panel-title">✏️ 상세 데이터 [{{ selectedItem.setting_key }}]</h4>
|
||||
|
||||
<!-- Optimistic Lock Version Badge & Mode Switch Toggle -->
|
||||
<div class="lock-mode-wrapper">
|
||||
<span class="lock-ver-badge">🔒 Ver: v{{ selectedItem.lock_version }}</span>
|
||||
<span class="checker-badge" :class="selectedItem.maker_checker.toLowerCase()">{{ selectedItem.maker_checker }}</span>
|
||||
<div class="mode-switch-wrapper">
|
||||
<button class="mode-btn" :class="{ active: !isEditMode }" @click="isEditMode = false">👁️ 읽기</button>
|
||||
<button class="mode-btn" :class="{ active: isEditMode }" @click="isEditMode = true">✏️ 편집</button>
|
||||
@@ -336,23 +397,49 @@ onMounted(loadData);
|
||||
</div>
|
||||
|
||||
<div class="panel-body form-scroll-body">
|
||||
<!-- 도메인별 전용 모듈 UX/AX 렌더링 -->
|
||||
<div class="domain-module-box" :class="selectedItem.domain.toLowerCase()">
|
||||
<span class="module-title">📌 {{ selectedItem.domain }} 도메인 전용 컴포넌트</span>
|
||||
<div v-if="selectedItem.domain === 'OMS'" class="module-content">
|
||||
<span>📉 호가 스프레드 가드: <strong>Ask 0.05% / Bid 0.04%</strong> | Waterfall 매도 순위: <strong>Strict Sell Priority #1</strong></span>
|
||||
</div>
|
||||
<div v-if="selectedItem.domain === 'WMS'" class="module-content">
|
||||
<span>🏦 Vault 현금 안전율: <strong>D+2 Target 5억 KRW (보호율 100%)</strong> | 사이징 가드: <strong>Level 2 Active</strong></span>
|
||||
</div>
|
||||
<div v-if="selectedItem.domain === 'ERP'" class="module-content">
|
||||
<span>📑 ERP 계정코드: <strong>1110-CASH (일반현금)</strong> | Maker-Checker 결재 릴레이: <strong>최종 승인완료 (Approved)</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<QuantLabel text="설정 키 (Key)" required />
|
||||
<QuantInput v-model="selectedItem.setting_key" :disabled="!isEditMode" placeholder="키 명칭" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<QuantLabel text="카테고리" required />
|
||||
<QuantComboBox
|
||||
v-model="selectedItem.category"
|
||||
:disabled="!isEditMode"
|
||||
:options="[
|
||||
{ label: 'BUDGET (예산)', value: 'BUDGET' },
|
||||
{ label: 'FACTOR (팩터)', value: 'FACTOR' },
|
||||
{ label: 'RISK (리스크)', value: 'RISK' },
|
||||
{ label: 'EXECUTION (체결)', value: 'EXECUTION' }
|
||||
]"
|
||||
/>
|
||||
<QuantLabel text="도메인 및 카테고리" required />
|
||||
<div class="flex-combo-row">
|
||||
<QuantComboBox
|
||||
v-model="selectedItem.domain"
|
||||
:disabled="!isEditMode"
|
||||
:options="[
|
||||
{ label: 'OMS (주문/체결)', value: 'OMS' },
|
||||
{ label: 'WMS (자산/현금)', value: 'WMS' },
|
||||
{ label: 'ERP (재무/회계)', value: 'ERP' }
|
||||
]"
|
||||
/>
|
||||
<QuantComboBox
|
||||
v-model="selectedItem.category"
|
||||
:disabled="!isEditMode"
|
||||
:options="[
|
||||
{ label: 'BUDGET (예산)', value: 'BUDGET' },
|
||||
{ label: 'FACTOR (팩터)', value: 'FACTOR' },
|
||||
{ label: 'RISK (리스크)', value: 'RISK' },
|
||||
{ label: 'EXECUTION (체결)', value: 'EXECUTION' },
|
||||
{ label: 'ACCOUNTING (회계)', value: 'ACCOUNTING' }
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
@@ -388,9 +475,9 @@ onMounted(loadData);
|
||||
<QuantTextArea v-model="selectedItem.note" :disabled="!isEditMode" :rows="2" />
|
||||
</div>
|
||||
|
||||
<!-- 실시간 파이프라인 로그 터미널 창 -->
|
||||
<!-- 실시간 로그 터미널 -->
|
||||
<div class="live-log-terminal">
|
||||
<div class="terminal-header">💻 실시간 퀀트 엔진 로그 터미널</div>
|
||||
<div class="terminal-header">💻 실시간 퀀트 파이프라인 로그 터미널</div>
|
||||
<div class="terminal-body">
|
||||
<div v-for="(log, idx) in liveLogs.slice(0, 3)" :key="idx" class="log-line">
|
||||
{{ log }}
|
||||
@@ -398,7 +485,7 @@ onMounted(loadData);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 변경 감사 이력 타임라인 (Audit History Timeline) -->
|
||||
<!-- Audit Timeline -->
|
||||
<div class="audit-timeline-section" v-if="selectedItem.audit_history && selectedItem.audit_history.length > 0">
|
||||
<h5 class="timeline-title">🕒 변경 감사 이력 타임라인 (Audit Trail)</h5>
|
||||
<div class="timeline-list">
|
||||
@@ -434,13 +521,21 @@ onMounted(loadData);
|
||||
<button class="btn-close-modal" @click="isModalOpen = false">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-row mb-3">
|
||||
<QuantLabel text="도메인 선택" required />
|
||||
<QuantComboBox v-model="modalItem.domain" :options="[
|
||||
{ label: 'OMS (주문/체결)', value: 'OMS' },
|
||||
{ label: 'WMS (자산/현금)', value: 'WMS' },
|
||||
{ label: 'ERP (재무/회계)', value: 'ERP' }
|
||||
]" />
|
||||
</div>
|
||||
<div class="form-row mb-3">
|
||||
<QuantLabel text="설정 키 (Key)" required />
|
||||
<QuantInput v-model="modalItem.setting_key" placeholder="예: MAX_POSITION_SIZE" />
|
||||
<QuantInput v-model="modalItem.setting_key" placeholder="예: OMS_MAX_SLIPPAGE" />
|
||||
</div>
|
||||
<div class="form-row mb-3">
|
||||
<QuantLabel text="설정 값 (Value)" required />
|
||||
<QuantInput v-model="modalItem.setting_value" placeholder="예: 50,000,000" />
|
||||
<QuantInput v-model="modalItem.setting_value" placeholder="예: 0.002" />
|
||||
</div>
|
||||
<div class="form-row mb-3">
|
||||
<QuantLabel text="설명 비고" />
|
||||
@@ -454,33 +549,35 @@ onMounted(loadData);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 18대 CRUD & 현장감 컴포넌트 명세 가이드 Modal -->
|
||||
<!-- 20대 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">💡 이 화면의 현장감 넘치는 18대 CRUD 컴포넌트 스펙</h4>
|
||||
<h4 class="modal-title">💡 OMS / WMS / ERP / AX & 동적 스플릿 20대 컴포넌트 스펙</h4>
|
||||
<button class="btn-close-modal" @click="isGuideModalOpen = false">✕</button>
|
||||
</div>
|
||||
<div class="modal-body guide-body">
|
||||
<ul class="guide-list">
|
||||
<li><strong>1. Live PostgreSQL Ping 레이턴시 칩</strong>: 4ms 실시간 핑 상태</li>
|
||||
<li><strong>2. SignalR WebSocket 소켓 연결 칩</strong>: 소켓 연결 라이브 지표</li>
|
||||
<li><strong>3. DbUp 스키마 마이그레이션 뱃지</strong>: DB 마이그레이션 태그</li>
|
||||
<li><strong>4. 실시간 파이프라인 로그 터미널 창</strong>: 엔진 트랜잭션 스트리밍</li>
|
||||
<li><strong>5. 팩터 재계산 비동기 배치 버튼 & 진행률 바</strong>: 0~100% 모의 트랜잭션</li>
|
||||
<li><strong>6. 낙관적 락 버전 인디케이터 (Lock Guard)</strong>: 동시성 충돌 방지 v1~v5</li>
|
||||
<li><strong>7. 실시간 텔레메트리 시스템 바</strong>: CPU/RAM/DB Pool 현장감 지표</li>
|
||||
<li><strong>8. 검색 & 핫키 툴바</strong>: F3/F4/F5/F7 더존 표준 핫키</li>
|
||||
<li><strong>9. 카테고리 탭 바</strong>: ALL/BUDGET/FACTOR/RISK 1-Click 필터</li>
|
||||
<li><strong>10. 6:4 Master-Detail Split Pane</strong>: 8px 물리 구분선 뷰포트</li>
|
||||
<li><strong>11. AG Grid 데이터 테이블</strong>: 정렬, 필터, 단일 행 선택, Status Chips</li>
|
||||
<li><strong>12. 페이지네이션 바</strong>: Page 1 of 1 건수 및 페이지 컨트롤러</li>
|
||||
<li><strong>13. 더존 6대 표준 입력 마스크</strong>: Input, ComboBox, Radio, TextArea 등</li>
|
||||
<li><strong>14. 읽기/편집 모드 토글</strong>: 👁️ 읽기 / ✏️ 편집 전환 스위치</li>
|
||||
<li><strong>15. 증빙 파일 첨부 드롭존</strong>: Drag & drop 파일 첨부 칩</li>
|
||||
<li><strong>16. 감사 이력 타임라인</strong>: 수정자/일시 Audit Trail 스캔</li>
|
||||
<li><strong>17. 신규 등록 Modal</strong>: 팝업 대화상자 폼 세트</li>
|
||||
<li><strong>18. 플로팅 토스트 알림 & 엑셀 팝업</strong>: 성공/경고 피드백 및 내보내기</li>
|
||||
<li><strong>1. OMS 도메인 모듈</strong>: 주문/체결 Waterfall 우선순위 & 호가 스프레드 뷰er</li>
|
||||
<li><strong>2. WMS 도메인 모듈</strong>: D+2 현금 볼트 안전율 & 포지션 사이징 가드</li>
|
||||
<li><strong>3. ERP 도메인 모듈</strong>: 재무 회계 계정 맵핑 & Maker-Checker 승인 릴레이</li>
|
||||
<li><strong>4. AI AX 코파일럿 배너</strong>: Antigravity AI 추천값 및 1-Click 자동 적용</li>
|
||||
<li><strong>5. 물리적 동적 스플릿 바 (Draggable Resizer)</strong>: 마우스 드래그 25%~75% 분할 조절</li>
|
||||
<li><strong>6. Live PostgreSQL Ping 레이턴시 칩</strong>: 4ms 실시간 핑 상태</li>
|
||||
<li><strong>7. SignalR WebSocket 소켓 칩</strong>: 라이브 소켓 연결 지표</li>
|
||||
<li><strong>8. DbUp 마이그레이션 태그</strong>: DB 마이그레이션 태그 v2026.07.25</li>
|
||||
<li><strong>9. 실시간 파이프라인 로그 터미널 창</strong>: 엔진 트랜잭션 스트리밍</li>
|
||||
<li><strong>10. 비동기 배치 실행 & 진행률 바</strong>: 0~100% 모의 트랜잭션 게이지</li>
|
||||
<li><strong>11. 낙관적 락 버전 인디케이터</strong>: 동시성 충돌 방지 v1~v5</li>
|
||||
<li><strong>12. 실시간 텔레메트리 바</strong>: CPU/RAM/DB Pool 현장감 지표</li>
|
||||
<li><strong>13. 검색 & 핫키 툴바</strong>: F3/F4/F5/F7 더존 표준 핫키</li>
|
||||
<li><strong>14. 카테고리 탭 바</strong>: ALL/BUDGET/FACTOR/RISK/ACCOUNTING 탭</li>
|
||||
<li><strong>15. AG Grid 데이터 테이블</strong>: 정렬, 필터, 단일 행 선택, Status Chips</li>
|
||||
<li><strong>16. 페이지네이션 바</strong>: Page 1 of 1 건수 및 페이지 컨트롤러</li>
|
||||
<li><strong>17. 더존 6대 표준 입력 마스크</strong>: Input, ComboBox, Radio, TextArea 등</li>
|
||||
<li><strong>18. 읽기/편집 모드 토글</strong>: 👁️ 읽기 / ✏️ 편집 전환 스위치</li>
|
||||
<li><strong>19. 증빙 파일 첨부 드롭존</strong>: Drag & drop 파일 첨부 칩</li>
|
||||
<li><strong>20. 감사 이력 타임라인 & 모달 & 엑셀 팝업</strong>: Audit Trail, Modal, Excel/CSV</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -501,6 +598,111 @@ onMounted(loadData);
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.domain-switcher-bar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.domain-btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
border: 1px solid #64748B;
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #CBD5E1;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.domain-btn.active {
|
||||
background: #2563EB;
|
||||
color: white;
|
||||
border-color: #2563EB;
|
||||
}
|
||||
|
||||
.ai-ax-banner {
|
||||
background: #2D3748;
|
||||
color: #F7FAFC;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
border-left: 4px solid #8E44AD;
|
||||
}
|
||||
|
||||
.btn-apply-ax {
|
||||
background: #8E44AD;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.domain-module-box {
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.domain-module-box.oms { background: #EBF8FF; border: 1px solid #90CDF4; color: #2B6CB0; }
|
||||
.domain-module-box.wms { background: #F0FFF4; border: 1px solid #9AE6B4; color: #276749; }
|
||||
.domain-module-box.erp { background: #FAF5FF; border: 1px solid #D6BCFA; color: #6B46C1; }
|
||||
|
||||
.module-title { font-weight: 700; font-size: 0.75rem; }
|
||||
.module-content { font-size: 0.7rem; }
|
||||
|
||||
.flex-combo-row { display: flex; gap: 6px; }
|
||||
.flex-combo-row > * { flex: 1; }
|
||||
|
||||
.checker-badge {
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.checker-badge.approved { background: #DCFCE7; color: #166534; }
|
||||
.checker-badge.pending { background: #FEF3C7; color: #92400E; }
|
||||
.checker-badge.rejected { background: #FEE2E2; color: #991B1B; }
|
||||
|
||||
.domain-tag {
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.domain-tag.oms { background: #BEE3F8; color: #2B6CB0; }
|
||||
.domain-tag.wms { background: #C6F6D5; color: #22543D; }
|
||||
.domain-tag.erp { background: #E9D8FD; color: #553C9A; }
|
||||
|
||||
.split-divider.draggable {
|
||||
width: 10px;
|
||||
background-color: #CBD5E1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: col-resize;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.split-divider.draggable:hover,
|
||||
.crud-split-section.dragging .split-divider {
|
||||
background-color: #2563EB;
|
||||
}
|
||||
|
||||
.title-with-chip { display: flex; align-items: center; gap: 8px; }
|
||||
@@ -568,7 +770,7 @@ onMounted(loadData);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.guide-modal { width: 560px; }
|
||||
.guide-modal { width: 580px; }
|
||||
.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; }
|
||||
|
||||
@@ -707,7 +909,6 @@ onMounted(loadData);
|
||||
}
|
||||
|
||||
.master-grid-panel {
|
||||
flex: 6;
|
||||
background: white;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-radius: 6px 0 0 6px;
|
||||
@@ -716,19 +917,9 @@ onMounted(loadData);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.split-divider {
|
||||
width: 8px;
|
||||
background-color: #CBD5E1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.divider-handle { width: 2px; height: 30px; background-color: #64748B; }
|
||||
|
||||
.detail-form-panel {
|
||||
flex: 4;
|
||||
background: white;
|
||||
border: 1px solid #CBD5E1;
|
||||
border-left: none;
|
||||
@@ -751,7 +942,7 @@ onMounted(loadData);
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lock-ver-badge {
|
||||
|
||||
Reference in New Issue
Block a user