diff --git a/src/frontend/src/views/SystemSettingsView.vue b/src/frontend/src/views/SystemSettingsView.vue index b29f931d..b9fc1dc3 100644 --- a/src/frontend/src/views/SystemSettingsView.vue +++ b/src/frontend/src/views/SystemSettingsView.vue @@ -13,6 +13,12 @@ import QuantStatusChip from '../components/QuantStatusChip.vue' import QuantDataGrid from '../components/QuantDataGrid.vue' import type { ColDef } from 'ag-grid-community' +interface AuditLog { + timestamp: string; + user: string; + change: string; +} + interface SettingItem { id: number; setting_key: string; @@ -22,19 +28,21 @@ interface SettingItem { updated_at: string; note: string; attachments?: string[]; + audit_history?: AuditLog[]; } // 1. 검색, 필터, 탭 상태 const searchKeyword = ref(''); const activeTab = ref('ALL'); -// 2. Master-Detail 바인딩 상태 +// 2. Master-Detail 바인딩 및 편집 모드 토글 상태 const items = ref([]); const selectedItem = ref(null); +const isEditMode = ref(false); // Readonly vs Edit Mode Toggle -// 3. 페이지네이션 상태 +// 3. 페이지네이션 및 엑셀 드롭다운 상태 const currentPage = ref(1); -const pageSize = ref(10); +const showExportMenu = ref(false); // 4. 토스트 알림 시스템 상태 const toastMessage = ref<{ text: string; type: 'success' | 'warning' | 'error' } | null>(null); @@ -88,10 +96,25 @@ const columnDefs = ref([ 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 즉시방어 현금 목표', attachments: ['budget_spec_v1.pdf'] }, - { id: 2, setting_key: 'RSI_UPPER_LIMIT', category: 'FACTOR', setting_value: '70.0', status: 'ACTIVE', updated_at: '2026-07-24 18:30', note: '과매수 상한 임계값', attachments: [] }, - { id: 3, setting_key: 'ANTI_LATE_ENTRY', category: 'RISK', setting_value: 'ENABLED', status: 'WARNING', updated_at: '2026-07-22 10:15', note: '추격매수 방지 가드 레벨 2', attachments: ['gate_audit.log'] }, - { 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 룰', attachments: [] } + { + 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 즉시방어 현금 목표', 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: '과매수 상한 임계값', 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', 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 룰', attachments: [], + audit_history: [] + } ]; if (items.value.length > 0) { selectedItem.value = JSON.parse(JSON.stringify(items.value[0])); @@ -101,6 +124,7 @@ const loadData = async () => { const handleRowSelect = (rows: any[]) => { if (rows.length > 0) { selectedItem.value = JSON.parse(JSON.stringify(rows[0])); + isEditMode.value = false; // 선택 시 읽기 모드로 재설정 } }; @@ -132,7 +156,15 @@ const saveDetailForm = () => { if (!selectedItem.value) return; const idx = items.value.findIndex(i => i.id === selectedItem.value?.id); if (idx !== -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: '설정값 및 노티 정보 수정 커밋' + }); items.value[idx] = JSON.parse(JSON.stringify(selectedItem.value)); + isEditMode.value = false; showToast('PostgreSQL 원장 설정 변경사항이 저장되었습니다.', 'success'); } }; @@ -146,6 +178,11 @@ const deleteSelectedItem = () => { } }; +const triggerExport = (fmt: string) => { + showExportMenu.value = false; + showToast(`${fmt} 형식으로 대용량 스트리밍 내보내기를 시작합니다.`, 'success'); +}; + onMounted(loadData); @@ -164,8 +201,8 @@ onMounted(loadData);
-

⚙️ SCR-07: 올인원 고도화 CRUD 매니저

- Master-Detail, AG Grid, Toast, Tabs, Dropzone, Modal, 더존 6대 표준 컴포넌트 +

⚙️ SCR-07: 올인원 궁극의 CRUD 매니저

+ Master-Detail, AG Grid, Toast, Tabs, Audit Timeline, Mode Switch, Modal, 더존 6대 표준 컴포넌트
@@ -175,7 +212,17 @@ onMounted(loadData); - + + +
+ +
+ + +
+
@@ -225,18 +272,26 @@ onMounted(loadData);
Detail Form -

✏️ 상세 편집 폼 [{{ selectedItem.setting_key }}]

+

✏️ 상세 데이터 [{{ selectedItem.setting_key }}]

+ + +
+ + +
+
- +
- +
@@ -276,10 +331,22 @@ onMounted(loadData);
- +
-
@@ -397,6 +464,35 @@ onMounted(loadData); .btn-delete { background-color: #DC2626; } .btn-excel { background-color: #D97706; } +.export-dropdown-wrapper { position: relative; } +.export-menu-popover { + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + background: white; + border: 1px solid #CBD5E1; + border-radius: 4px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + display: flex; + flex-direction: column; + z-index: 100; + width: 180px; +} + +.export-menu-popover button { + padding: 8px 12px; + background: none; + border: none; + text-align: left; + font-size: 0.75rem; + font-weight: 700; + color: #334155; + cursor: pointer; +} + +.export-menu-popover button:hover { background-color: #F1F5F9; color: #2563EB; } + .badge-key { background: rgba(255,255,255,0.2); padding: 1px 4px; @@ -476,6 +572,32 @@ onMounted(loadData); gap: 8px; } +.mode-switch-wrapper { + margin-left: auto; + display: flex; + gap: 4px; + background: #E2E8F0; + padding: 2px; + border-radius: 4px; +} + +.mode-btn { + font-size: 0.7rem; + font-weight: 700; + border: none; + background: none; + padding: 2px 8px; + border-radius: 3px; + color: #64748B; + cursor: pointer; +} + +.mode-btn.active { + background: white; + color: #1E293B; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); +} + .panel-tag { font-size: 0.65rem; font-weight: 700; @@ -531,6 +653,31 @@ onMounted(loadData); margin-top: 4px; } +.audit-timeline-section { + border-top: 1px solid #E2E8F0; + padding-top: 10px; + margin-top: 6px; +} + +.timeline-title { + font-size: 0.75rem; + font-weight: 700; + color: #475569; + margin: 0 0 6px 0; +} + +.timeline-list { display: flex; flex-direction: column; gap: 4px; } +.timeline-item { + font-size: 0.7rem; + color: #64748B; + display: flex; + gap: 6px; +} + +.time-stamp { font-family: monospace; font-weight: 700; color: #2563EB; } +.user-id { font-weight: 700; color: #334155; } +.change-desc { color: #475569; } + .btn-save-form { margin-top: 12px; padding: 10px 0;