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

This commit is contained in:
2026-07-25 20:31:13 +09:00
parent e8550dc583
commit 07e5908319
+300 -109
View File
@@ -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 {