feat(wbs-ux): finalize SystemSettingsView with View/Edit mode toggle, Audit Trail timeline, and Export format selector
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 7s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 14s
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) / Database & Schema Validation (push) Successful in 8s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 8s
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 7s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 14s
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) / Database & Schema Validation (push) Successful in 8s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 8s
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:
@@ -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<SettingItem[]>([]);
|
||||
const selectedItem = ref<SettingItem | null>(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<ColDef[]>([
|
||||
|
||||
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);
|
||||
</script>
|
||||
|
||||
@@ -164,8 +201,8 @@ onMounted(loadData);
|
||||
<!-- Top Action & Search Bar -->
|
||||
<div class="crud-toolbar">
|
||||
<div class="toolbar-title">
|
||||
<h3 class="title-text">⚙️ SCR-07: 올인원 고도화 CRUD 매니저</h3>
|
||||
<span class="subtitle-text">Master-Detail, AG Grid, Toast, Tabs, Dropzone, Modal, 더존 6대 표준 컴포넌트</span>
|
||||
<h3 class="title-text">⚙️ SCR-07: 올인원 궁극의 CRUD 매니저</h3>
|
||||
<span class="subtitle-text">Master-Detail, AG Grid, Toast, Tabs, Audit Timeline, Mode Switch, Modal, 더존 6대 표준 컴포넌트</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-actions">
|
||||
@@ -175,7 +212,17 @@ onMounted(loadData);
|
||||
<button class="btn-action btn-search" @click="loadData"><span class="badge-key">F3</span>조회</button>
|
||||
<button class="btn-action btn-create" @click="openCreateModal"><span class="badge-key">F4</span>신규 등록</button>
|
||||
<button class="btn-action btn-delete" @click="deleteSelectedItem"><span class="badge-key">F5</span>삭제</button>
|
||||
<button class="btn-action btn-excel"><span class="badge-key">F7</span>엑셀 다운로드</button>
|
||||
|
||||
<!-- 엑셀 내보내기 팝업 드롭다운 -->
|
||||
<div class="export-dropdown-wrapper">
|
||||
<button class="btn-action btn-excel" @click="showExportMenu = !showExportMenu">
|
||||
<span class="badge-key">F7</span>엑셀 내보내기 ▼
|
||||
</button>
|
||||
<div v-if="showExportMenu" class="export-menu-popover">
|
||||
<button @click="triggerExport('OpenXML Excel (.xlsx)')">📊 Excel (.xlsx) 내보내기</button>
|
||||
<button @click="triggerExport('CSV (UTF-8)')">📄 CSV (UTF-8) 내보내기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -225,18 +272,26 @@ onMounted(loadData);
|
||||
<div class="detail-form-panel" v-if="selectedItem">
|
||||
<div class="panel-header">
|
||||
<span class="panel-tag tag-active">Detail Form</span>
|
||||
<h4 class="panel-title">✏️ 상세 편집 폼 [{{ selectedItem.setting_key }}]</h4>
|
||||
<h4 class="panel-title">✏️ 상세 데이터 [{{ selectedItem.setting_key }}]</h4>
|
||||
|
||||
<!-- Mode Switch Toggle -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-body form-scroll-body">
|
||||
<div class="form-row">
|
||||
<QuantLabel text="설정 키 (Key)" required />
|
||||
<QuantInput v-model="selectedItem.setting_key" placeholder="키 명칭" />
|
||||
<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' },
|
||||
@@ -248,7 +303,7 @@ onMounted(loadData);
|
||||
|
||||
<div class="form-row">
|
||||
<QuantLabel text="설정 값 (Value)" required />
|
||||
<QuantInput v-model="selectedItem.setting_value" placeholder="설정값" />
|
||||
<QuantInput v-model="selectedItem.setting_value" :disabled="!isEditMode" placeholder="설정값" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
@@ -276,10 +331,22 @@ onMounted(loadData);
|
||||
|
||||
<div class="form-row">
|
||||
<QuantLabel text="상세 비고 설명" />
|
||||
<QuantTextArea v-model="selectedItem.note" :rows="3" />
|
||||
<QuantTextArea v-model="selectedItem.note" :disabled="!isEditMode" :rows="2" />
|
||||
</div>
|
||||
|
||||
<button class="btn-save-form" @click="saveDetailForm">
|
||||
<!-- 변경 감사 이력 타임라인 (Audit History 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">
|
||||
<div v-for="(log, idx) in selectedItem.audit_history" :key="idx" class="timeline-item">
|
||||
<span class="time-stamp">{{ log.timestamp }}</span>
|
||||
<span class="user-id">[{{ log.user }}]</span>
|
||||
<span class="change-desc">{{ log.change }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button v-if="isEditMode" class="btn-save-form" @click="saveDetailForm">
|
||||
💾 PostgreSQL 설정 변경사항 저장
|
||||
</button>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user