feat(wbs-ux): upgrade SystemSettingsView into a comprehensive CRUD masterpiece with Search Toolbar, Split AG Grid, Detail Form, and Modal Dialog
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) / .NET Contracts (push) Has been skipped
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 20s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 10s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 9s
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) / Notify PR Results (push) Has been skipped

This commit is contained in:
2026-07-25 20:22:13 +09:00
parent 7530e45587
commit ed9dbbd661
+471 -104
View File
@@ -1,5 +1,6 @@
<!-- SystemSettingsView.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted } from 'vue'
import QuantLabel from '../components/QuantLabel.vue'
import QuantInput from '../components/QuantInput.vue'
import QuantDatePicker from '../components/QuantDatePicker.vue'
@@ -9,124 +10,490 @@ import QuantRadio from '../components/QuantRadio.vue'
import QuantTextArea from '../components/QuantTextArea.vue'
import QuantAutoComplete from '../components/QuantAutoComplete.vue'
import QuantStatusChip from '../components/QuantStatusChip.vue'
import QuantDataGrid from '../components/QuantDataGrid.vue'
import type { ColDef } from 'ag-grid-community'
const codeVal = ref('005930')
const dateVal = ref('20260722')
const currencyVal = ref('340500')
const comboVal = ref('ACTIVE')
const checkVal = ref(true)
const radioVal = ref('A')
const textVal = ref('더존 회계시스템 기준 6대 표준 입력 컴포넌트 템플릿 설정')
const autoVal = ref('')
interface SettingItem {
id: number;
setting_key: string;
category: string;
setting_value: string;
status: 'ACTIVE' | 'WARNING' | 'BLOCKED';
updated_at: string;
note: string;
}
const comboOptions = [
{ label: 'ACTIVE (운영)', value: 'ACTIVE' },
{ label: 'LIMIT (제한)', value: 'LIMIT' },
{ label: 'ARCHIVED (보관)', value: 'ARCHIVED' }
]
// 1. 검색 및 필터 바 상태
const searchKeyword = ref('');
const statusFilter = ref('ALL');
const radioOptions = [
{ label: '유형 A (표준)', value: 'A' },
{ label: '유형 B (확장)', value: 'B' }
]
// 2. Master-Detail 바인딩 상태
const items = ref<SettingItem[]>([]);
const selectedItem = ref<SettingItem | null>(null);
const autoSuggestions = [
{ label: '삼성전자', value: '005930' },
{ label: 'SK하이닉스', value: '000660' },
{ label: 'NAVER', value: '035420' }
]
// 3. 모달 대화상자 상태
const isModalOpen = ref(false);
const modalItem = ref<SettingItem>({
id: 0,
setting_key: '',
category: 'SYSTEM',
setting_value: '',
status: 'ACTIVE',
updated_at: '',
note: ''
});
// 4. AG Grid 컬럼 정의
const columnDefs = ref<ColDef[]>([
{ field: 'id', headerName: 'ID', width: 70, sortable: true },
{ field: 'setting_key', headerName: '설정 키 (Key)', width: 160, sortable: true, filter: true },
{ field: 'category', headerName: '카테고리', width: 120 },
{ field: 'setting_value', headerName: '설정 값 (Value)', width: 160 },
{
field: 'status',
headerName: '상태 칩',
width: 120,
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: 'updated_at', headerName: '수정일시', width: 140 }
]);
const loadData = async () => {
// 모의 CRUD 데이터 세트
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 즉시방어 현금 목표' },
{ id: 2, setting_key: 'RSI_UPPER_LIMIT', category: 'FACTOR', setting_value: '70.0', status: 'ACTIVE', updated_at: '2026-07-24 18:30', note: '과매수 상한 임계값' },
{ id: 3, setting_key: 'ANTI_LATE_ENTRY', category: 'RISK', setting_value: 'ENABLED', status: 'WARNING', updated_at: '2026-07-22 10:15', note: '추격매수 방지 가드 레벨 2' },
{ 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 룰' }
];
if (items.value.length > 0) {
selectedItem.value = JSON.parse(JSON.stringify(items.value[0]));
}
};
const handleRowSelect = (rows: any[]) => {
if (rows.length > 0) {
selectedItem.value = JSON.parse(JSON.stringify(rows[0]));
}
};
const openCreateModal = () => {
modalItem.value = {
id: items.value.length + 1,
setting_key: '',
category: 'FACTOR',
setting_value: '',
status: 'ACTIVE',
updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
note: ''
};
isModalOpen.value = true;
};
const saveModalItem = () => {
if (!modalItem.value.setting_key) {
alert('설정 키는 필수 입력 항목입니다.');
return;
}
items.value.unshift({ ...modalItem.value });
selectedItem.value = { ...modalItem.value };
isModalOpen.value = false;
alert('신규 설정 항목이 성공적으로 등록되었습니다.');
};
const saveDetailForm = () => {
if (!selectedItem.value) return;
const idx = items.value.findIndex(i => i.id === selectedItem.value?.id);
if (idx !== -1) {
items.value[idx] = JSON.parse(JSON.stringify(selectedItem.value));
alert('상세 변경 사항이 DB에 저장되었습니다.');
}
};
const deleteSelectedItem = () => {
if (!selectedItem.value) return;
if (confirm(`[${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;
}
};
onMounted(loadData);
</script>
<template>
<!-- Type 4: Standardized High-Density Input Components Template View -->
<div style="display: flex; flex-direction: column; height: 100%; width: 100%; background: #F4F6F9; overflow-y: auto; padding: 16px;">
<div style="background: #34495E; color: white; padding: 10px 16px; font-weight: bold; border-radius: 4px 4px 0 0; display: flex; justify-content: space-between;">
<span><i class="ti ti-forms me-1"></i> SCR-07: 더존 ERP 표준 컴포넌트 & 입력 UX 마스크 통합 템플릿 (Type 4)</span>
<div>
<button style="background: #27AE60; color: white; border: none; padding: 4px 12px; font-weight: bold; border-radius: 3px; cursor: pointer;">
<span class="hotkey-badge">F7</span>엑셀 다운로드
</button>
<div class="crud-masterpiece-container">
<!-- 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, Modal Dialog, 더존 6 표준 컴포넌트 세트</span>
</div>
<div class="toolbar-actions">
<div class="search-box">
<input type="text" class="search-input" placeholder="설정 키/설명 검색..." v-model="searchKeyword" />
</div>
<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>
</div>
<!-- High-Density Form Grid -->
<div style="background: white; border: 1px solid #CBD5E1; border-top: none; padding: 16px; border-radius: 0 0 4px 4px;">
<h4 style="margin-top: 0; color: #2C3E50; border-bottom: 2px solid #2C3E50; padding-bottom: 6px;">
표준 입력 컴포넌트 마스크 기본 CRUD 입력 UX 규격
</h4>
<!-- Main Split Section (60% Master Grid : 40% Detail Form) -->
<div class="crud-split-section">
<!-- Master Grid Panel (Left 60%) -->
<div class="master-grid-panel">
<div class="panel-header">
<span class="panel-tag">Master Table</span>
<h4 class="panel-title">📋 시스템 원장 항목 리스트 ( {{ items.length }})</h4>
</div>
<div class="panel-body">
<QuantDataGrid
:columnDefs="columnDefs"
:rowData="items"
rowSelection="single"
@row-selected="handleRowSelect"
/>
</div>
<div class="panel-footer">
<span>선택된 ID: {{ selectedItem?.id || '없음' }}</span>
<QuantStatusChip :type="selectedItem?.status === 'ACTIVE' ? 'PASS' : 'WARNING'" :label="selectedItem?.status || 'N/A'" />
</div>
</div>
<table style="width: 100%; border-collapse: collapse; margin-top: 12px;">
<tbody>
<tr>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="종목 코드" required />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantInput v-model="codeVal" placeholder="코드 입력" />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="수집 영업일자" required />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantDatePicker v-model="dateVal" />
</td>
</tr>
<!-- Split Divider Line -->
<div class="split-divider">
<div class="divider-handle"></div>
</div>
<tr>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="통화 금액(원)" required />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantInput v-model="currencyVal" type="currency" />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="상태 선택" />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantComboBox v-model="comboVal" :options="comboOptions" />
</td>
</tr>
<!-- Detail Form Panel (Right 40%) -->
<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>
</div>
<div class="panel-body form-scroll-body">
<div class="form-row">
<QuantLabel text="설정 키 (Key)" required />
<QuantInput v-model="selectedItem.setting_key" placeholder="키 명칭" />
</div>
<tr>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="체크박스 옵션" />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantCheckBox v-model="checkVal" label="Anti-Late Entry Gate 활성화" />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="라디오 선택" />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantRadio v-model="radioVal" name="typeGroup" :options="radioOptions" />
</td>
</tr>
<div class="form-row">
<QuantLabel text="카테고리" required />
<QuantComboBox
v-model="selectedItem.category"
:options="[
{ label: 'BUDGET (예산)', value: 'BUDGET' },
{ label: 'FACTOR (팩터)', value: 'FACTOR' },
{ label: 'RISK (리스크)', value: 'RISK' },
{ label: 'EXECUTION (체결)', value: 'EXECUTION' }
]"
/>
</div>
<tr>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="Auto Complete" />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantAutoComplete v-model="autoVal" :suggestions="autoSuggestions" placeholder="종목명 자동완성..." />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="상태 칩 가드" />
</td>
<td style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantStatusChip type="PASS" label="PASS (정상)" />
</td>
</tr>
<div class="form-row">
<QuantLabel text="설정 값 (Value)" required />
<QuantInput v-model="selectedItem.setting_value" placeholder="설정값" />
</div>
<tr>
<td style="padding: 8px; border: 1px solid #CBD5E1; background: #F8FAFC;">
<QuantLabel text="상세 비고 내용" />
</td>
<td colspan="3" style="padding: 8px; border: 1px solid #CBD5E1;">
<QuantTextArea v-model="textVal" :rows="3" />
</td>
</tr>
</tbody>
</table>
<div class="form-row">
<QuantLabel text="상태 제어" />
<QuantRadio
v-model="selectedItem.status"
name="statusRadio"
:options="[
{ label: 'ACTIVE (정상)', value: 'ACTIVE' },
{ label: 'WARNING (경고)', value: 'WARNING' },
{ label: 'BLOCKED (차단)', value: 'BLOCKED' }
]"
/>
</div>
<div class="form-row">
<QuantLabel text="하네스 동기화" />
<QuantCheckBox v-model="selectedItem.id" label="PostgreSQL History-First 원장 동기화" />
</div>
<div class="form-row">
<QuantLabel text="상세 비고 설명" />
<QuantTextArea v-model="selectedItem.note" :rows="3" />
</div>
<button class="btn-save-form" @click="saveDetailForm">
💾 PostgreSQL 설정 변경사항 저장
</button>
</div>
</div>
</div>
<!-- Modal Dialog (신규 등록 대화상자) -->
<div class="modal-backdrop" v-if="isModalOpen">
<div class="modal-dialog">
<div class="modal-header">
<h4 class="modal-title"> 신규 시스템 설정 항목 등록 Modal</h4>
<button class="btn-close-modal" @click="isModalOpen = false"></button>
</div>
<div class="modal-body">
<div class="form-row mb-3">
<QuantLabel text="설정 키 (Key)" required />
<QuantInput v-model="modalItem.setting_key" placeholder="예: MAX_POSITION_SIZE" />
</div>
<div class="form-row mb-3">
<QuantLabel text="설정 값 (Value)" required />
<QuantInput v-model="modalItem.setting_value" placeholder="예: 50,000,000" />
</div>
<div class="form-row mb-3">
<QuantLabel text="설명 비고" />
<QuantTextArea v-model="modalItem.note" :rows="2" />
</div>
</div>
<div class="modal-footer">
<button class="btn-modal-cancel" @click="isModalOpen = false">취소</button>
<button class="btn-modal-save" @click="saveModalItem">저장 실행</button>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.crud-masterpiece-container {
display: flex;
flex-direction: column;
height: calc(100vh - 110px);
background-color: #F1F5F9;
box-sizing: border-box;
padding: 16px;
gap: 12px;
}
.crud-toolbar {
background-color: #34495E;
color: #FFFFFF;
padding: 12px 16px;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title-text { font-size: 0.95rem; font-weight: 700; margin: 0; }
.subtitle-text { font-size: 0.75rem; color: #CBD5E1; }
.toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
}
.search-input {
padding: 6px 10px;
border-radius: 4px;
border: 1px solid #64748B;
font-size: 0.8rem;
outline: none;
}
.btn-action {
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.btn-search { background-color: #2563EB; }
.btn-create { background-color: #166534; }
.btn-delete { background-color: #DC2626; }
.btn-excel { background-color: #D97706; }
.badge-key {
background: rgba(255,255,255,0.2);
padding: 1px 4px;
border-radius: 3px;
font-size: 0.65rem;
}
.crud-split-section {
display: flex;
flex: 1;
overflow: hidden;
}
.master-grid-panel {
flex: 6;
background: white;
border: 1px solid #CBD5E1;
border-radius: 6px 0 0 6px;
display: flex;
flex-direction: column;
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;
border-radius: 0 6px 6px 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
background-color: #F8FAFC;
padding: 10px 16px;
border-bottom: 1px solid #E2E8F0;
display: flex;
align-items: center;
gap: 8px;
}
.panel-tag {
font-size: 0.65rem;
font-weight: 700;
background: #E2E8F0;
color: #475569;
padding: 2px 6px;
border-radius: 3px;
}
.panel-tag.tag-active { background: #2563EB; color: white; }
.panel-title { font-size: 0.85rem; font-weight: 700; color: #1E293B; margin: 0; }
.panel-body { flex: 1; overflow-y: auto; padding: 16px; }
.panel-footer {
padding: 8px 16px;
background: #F8FAFC;
border-top: 1px solid #E2E8F0;
font-size: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.form-scroll-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.form-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.btn-save-form {
margin-top: 16px;
padding: 10px 0;
background-color: #2563EB;
color: white;
font-weight: 700;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* Modal Dialog Styles */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(15, 23, 42, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
.modal-dialog {
background: white;
width: 480px;
border-radius: 8px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.modal-header {
background: #34495E;
color: white;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title { font-size: 0.9rem; font-weight: 700; margin: 0; }
.btn-close-modal { background: none; border: none; color: white; font-size: 1rem; cursor: pointer; }
.modal-body { padding: 16px; }
.modal-footer {
background: #F8FAFC;
padding: 12px 16px;
display: flex;
justify-content: flex-end;
gap: 8px;
border-top: 1px solid #E2E8F0;
}
.btn-modal-cancel {
padding: 6px 14px;
background: #E2E8F0;
color: #475569;
border: none;
border-radius: 4px;
font-weight: 700;
cursor: pointer;
}
.btn-modal-save {
padding: 6px 14px;
background: #166534;
color: white;
border: none;
border-radius: 4px;
font-weight: 700;
cursor: pointer;
}
:deep(.status-chip) {
padding: 2px 6px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 700;
}
:deep(.status-chip.pass) { background-color: #DCFCE7; color: #166534; }
:deep(.status-chip.warning) { background-color: #FEF3C7; color: #92400E; }
:deep(.status-chip.blocked) { background-color: #FEE2E2; color: #991B1B; }
</style>