feat(wbs-ux): inject 4 live operational presence features into SystemSettingsView (DB Ping, Batch Progress, Lock Guard, Telemetry Bar)
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) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 21s
Validators (Pushes and Pull Requests) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / WBS & Audit Validations (push) Has been skipped
Validators (Pushes and Pull Requests) / .NET Contracts (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) / CI Workflow Lint (push) Failing after 9s

This commit is contained in:
2026-07-25 20:26:26 +09:00
parent bab4a61bbc
commit a46744bd18
+182 -70
View File
@@ -27,6 +27,7 @@ interface SettingItem {
status: 'ACTIVE' | 'WARNING' | 'BLOCKED';
updated_at: string;
note: string;
lock_version: number; // 낙관적 락 버전
attachments?: string[];
audit_history?: AuditLog[];
}
@@ -40,12 +41,18 @@ const items = ref<SettingItem[]>([]);
const selectedItem = ref<SettingItem | null>(null);
const isEditMode = ref(false);
// 3. 페이지네이션 및 엑셀 드롭다운 상태
// 3. 현장감 텔레메트리 & 배치 프로그레스 상태
const pingMs = ref(4);
const isBatchRunning = ref(false);
const batchProgress = ref(0);
const dbConnections = ref(4);
// 4. 페이지네이션 및 엑셀 드롭다운 상태
const currentPage = ref(1);
const showExportMenu = ref(false);
const isGuideModalOpen = ref(false); // 12대 컴포넌트 명세 팝업 모달
const isGuideModalOpen = ref(false);
// 4. 토스트 알림 시스템 상태
// 5. 토스트 알림 시스템 상태
const toastMessage = ref<{ text: string; type: 'success' | 'warning' | 'error' } | null>(null);
const showToast = (text: string, type: 'success' | 'warning' | 'error' = 'success') => {
toastMessage.value = { text, type };
@@ -54,7 +61,7 @@ const showToast = (text: string, type: 'success' | 'warning' | 'error' = 'succes
}, 3000);
};
// 5. 모달 대화상자 상태
// 6. 모달 대화상자 상태
const isModalOpen = ref(false);
const modalItem = ref<SettingItem>({
id: 0,
@@ -63,10 +70,11 @@ const modalItem = ref<SettingItem>({
setting_value: '',
status: 'ACTIVE',
updated_at: '',
note: ''
note: '',
lock_version: 1
});
// 6. 필터링된 항목 계산
// 7. 필터링된 항목 계산
const filteredItems = computed(() => {
return items.value.filter(item => {
const matchTab = activeTab.value === 'ALL' || item.category === activeTab.value;
@@ -77,43 +85,44 @@ const filteredItems = computed(() => {
});
});
// 7. AG Grid 컬럼 정의
// 8. AG Grid 컬럼 정의
const columnDefs = ref<ColDef[]>([
{ field: 'id', headerName: 'ID', width: 70, sortable: true },
{ field: 'setting_key', headerName: '설정 키 (Key)', width: 180, sortable: true, filter: true },
{ field: 'category', headerName: '카테고리', width: 120 },
{ field: 'setting_value', headerName: '설정 값 (Value)', width: 150 },
{ 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: 'status',
headerName: '상태 칩',
width: 120,
width: 110,
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 }
{ field: 'lock_version', headerName: 'Ver', width: 60 },
{ field: 'updated_at', headerName: '수정일시', width: 130 }
]);
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: 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'],
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: [],
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: [],
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'],
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'],
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: [],
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: [],
audit_history: []
}
];
@@ -122,6 +131,21 @@ const loadData = async () => {
}
};
const runFactorBatch = () => {
if (isBatchRunning.value) return;
isBatchRunning.value = true;
batchProgress.value = 0;
showToast('⚡ 팩터 재계산 비동기 배치 작업이 트리거되었습니다.', 'success');
const timer = setInterval(() => {
batchProgress.value += 25;
if (batchProgress.value >= 100) {
clearInterval(timer);
isBatchRunning.value = false;
showToast('✅ 팩터 재계산 배치가 성공적으로 완료되었습니다 (소요시간: 1.2s)', 'success');
}
}, 300);
};
const handleRowSelect = (rows: any[]) => {
if (rows.length > 0) {
selectedItem.value = JSON.parse(JSON.stringify(rows[0]));
@@ -137,7 +161,8 @@ const openCreateModal = () => {
setting_value: '',
status: 'ACTIVE',
updated_at: new Date().toISOString().slice(0, 16).replace('T', ' '),
note: ''
note: '',
lock_version: 1
};
isModalOpen.value = true;
};
@@ -157,15 +182,16 @@ const saveDetailForm = () => {
if (!selectedItem.value) return;
const idx = items.value.findIndex(i => i.id === selectedItem.value?.id);
if (idx !== -1) {
selectedItem.value.lock_version += 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: '설정값 및 노티 정보 수정 커밋'
change: `설정값 변경 (Ver -> v${selectedItem.value.lock_version})`
});
items.value[idx] = JSON.parse(JSON.stringify(selectedItem.value));
isEditMode.value = false;
showToast('PostgreSQL 원장 설정 변경사항이 저장되었습니다.', 'success');
showToast(`PostgreSQL 원장 설정 변경사항이 저장되었습니다 (Lock Ver: v${selectedItem.value.lock_version}).`, 'success');
}
};
@@ -201,22 +227,28 @@ onMounted(loadData);
<!-- Top Action & Search Bar -->
<div class="crud-toolbar">
<div class="toolbar-title">
<h3 class="title-text"> SCR-07: 올인원 12 CRUD 마스터피스 매니저</h3>
<span class="subtitle-text">Master-Detail, AG Grid, Toast, Tabs, Audit Timeline, Mode Switch, Modal, 더존 6 표준 컴포넌트</span>
<div class="title-with-chip">
<h3 class="title-text"> SCR-07: 현장감 퀀트 CRUD 매니저</h3>
<span class="db-ping-chip">🟢 PostgreSQL Live ({{ pingMs }}ms)</span>
</div>
<span class="subtitle-text">Master-Detail, AG Grid, Batch Progress, Lock Guard, Telemetry, Toast, 더존 컴포넌트</span>
</div>
<div class="toolbar-actions">
<button class="btn-guide-spec" @click="isGuideModalOpen = true">💡 12대 CRUD 명세 팝업</button>
<button class="btn-batch-trigger" @click="runFactorBatch" :disabled="isBatchRunning">
{{ isBatchRunning ? `⏳ 계산중 ${batchProgress}%` : '⚡ 팩터 배치 실행' }}
</button>
<button class="btn-guide-spec" @click="isGuideModalOpen = true">💡 16대 명세 팝업</button>
<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-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>
<div class="export-dropdown-wrapper">
<button class="btn-action btn-excel" @click="showExportMenu = !showExportMenu">
<span class="badge-key">F7</span>엑셀 내보내기
<span class="badge-key">F7</span>엑셀
</button>
<div v-if="showExportMenu" class="export-menu-popover">
<button @click="triggerExport('OpenXML Excel (.xlsx)')">📊 Excel (.xlsx) 내보내기</button>
@@ -226,6 +258,11 @@ onMounted(loadData);
</div>
</div>
<!-- Batch Execution Progress Bar -->
<div v-if="isBatchRunning" class="batch-progress-bar-container">
<div class="batch-progress-fill" :style="{ width: batchProgress + '%' }"></div>
</div>
<!-- Category Filter Tabs Bar -->
<div class="category-tabs-bar">
<button v-for="tab in ['ALL', 'BUDGET', 'FACTOR', 'RISK', 'EXECUTION']" :key="tab"
@@ -274,10 +311,13 @@ onMounted(loadData);
<span class="panel-tag tag-active">Detail Form</span>
<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>
<!-- Optimistic Lock Version Badge & Mode Switch Toggle -->
<div class="lock-mode-wrapper">
<span class="lock-ver-badge">🔒 Ver: v{{ selectedItem.lock_version }}</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>
</div>
</div>
</div>
@@ -347,12 +387,21 @@ onMounted(loadData);
</div>
<button v-if="isEditMode" class="btn-save-form" @click="saveDetailForm">
💾 PostgreSQL 설정 변경사항 저장
💾 PostgreSQL 설정 변경사항 저장 (Ver v{{ selectedItem.lock_version + 1 }})
</button>
</div>
</div>
</div>
<!-- Live Telemetry Footer Bar -->
<div class="live-telemetry-bar">
<span class="telemetry-item">🖥 CPU: <strong>12%</strong></span>
<span class="telemetry-item">💾 RAM: <strong>512MB / 4GB</strong></span>
<span class="telemetry-item">🔌 DB Pool: <strong>{{ dbConnections }}/20 Active</strong></span>
<span class="telemetry-item">📬 Outbox Queue: <strong>0 Pending</strong></span>
<span class="telemetry-item"> Hangfire Jobs: <strong>Healthy</strong></span>
</div>
<!-- Modal Dialog (신규 등록 대화상자) -->
<div class="modal-backdrop" v-if="isModalOpen">
<div class="modal-dialog">
@@ -381,27 +430,31 @@ onMounted(loadData);
</div>
</div>
<!-- 12 CRUD 컴포넌트 명세 가이드 Modal -->
<!-- 16 CRUD & 현장감 컴포넌트 명세 가이드 Modal -->
<div class="modal-backdrop" v-if="isGuideModalOpen">
<div class="modal-dialog guide-modal">
<div class="modal-header">
<h4 class="modal-title">💡 화면 이식된 12 CRUD 컴포넌트 라인업</h4>
<h4 class="modal-title">💡 화면 현장감 넘치는 16 CRUD 컴포넌트 스펙</h4>
<button class="btn-close-modal" @click="isGuideModalOpen = false"></button>
</div>
<div class="modal-body guide-body">
<ul class="guide-list">
<li><strong>1. 검색 & 핫키 툴바</strong>: F3/F4/F5/F7 더존 표준 핫키 탑재</li>
<li><strong>2. 카테고리 </strong>: ALL, BUDGET, FACTOR 1-Click 필터</li>
<li><strong>3. 6:4 Master-Detail Split Pane</strong>: 8px 물리 구분선 뷰포트</li>
<li><strong>4. AG Grid 데이터 테이블</strong>: 바인딩, 필터, 정렬, Status Chips</li>
<li><strong>5. 페이지네이션 </strong>: 건수 페이지 제어 </li>
<li><strong>6. 더존 6 표준 입력 컴포넌트</strong>: Input, ComboBox, Radio </li>
<li><strong>7. 읽기/편집 모드 토글</strong>: 👁/ 안전 모드 스위치</li>
<li><strong>8. 파일 첨부 드롭존</strong>: Drag & drop 증빙 서류 </li>
<li><strong>9. 감사 이력 타임라인</strong>: 수정자/일시 Audit Trail 스캔</li>
<li><strong>10. 신규 등록 Modal</strong>: 팝업 대화상자 세트</li>
<li><strong>11. 플로팅 토스트 알림</strong>: 3 자동소멸 성공/경고 알림</li>
<li><strong>12. 내보내기 팝업</strong>: Excel (.xlsx) / CSV 선택 메뉴</li>
<li><strong>1. Live PostgreSQL Ping 레이턴시 </strong>: 4ms 실시간 상태</li>
<li><strong>2. 팩터 재계산 비동기 배치 버튼 & 진행률 </strong>: 0~100% 모의 트랜잭션</li>
<li><strong>3. 낙관적 버전 인디케이터 (Lock Guard)</strong>: 동시성 충돌 방지 v1~v5</li>
<li><strong>4. 실시간 텔레메트리 시스템 </strong>: CPU/RAM/DB Pool 현장감 지표</li>
<li><strong>5. 검색 & 핫키 </strong>: F3/F4/F5/F7 더존 표준 핫키</li>
<li><strong>6. 카테고리 </strong>: ALL/BUDGET/FACTOR/RISK 1-Click 필터</li>
<li><strong>7. 6:4 Master-Detail Split Pane</strong>: 8px 물리 구분선 뷰포트</li>
<li><strong>8. AG Grid 데이터 테이블</strong>: 정렬, 필터, 단일 선택, Status Chips</li>
<li><strong>9. 페이지네이션 </strong>: Page 1 of 1 건수 페이지 컨트롤러</li>
<li><strong>10. 더존 6 표준 입력 마스크</strong>: Input, ComboBox, Radio, TextArea </li>
<li><strong>11. 읽기/편집 모드 토글</strong>: 👁 읽기 / 편집 전환 스위치</li>
<li><strong>12. 증빙 파일 첨부 드롭존</strong>: Drag & drop 파일 첨부 </li>
<li><strong>13. 감사 이력 타임라인</strong>: 수정자/일시 Audit Trail 스캔</li>
<li><strong>14. 신규 등록 Modal</strong>: 팝업 대화상자 세트</li>
<li><strong>15. 플로팅 토스트 알림</strong>: 3 자동소멸 피드백</li>
<li><strong>16. 내보내기 드롭다운 팝업</strong>: Excel (.xlsx) / CSV 내보내기</li>
</ul>
</div>
<div class="modal-footer">
@@ -420,10 +473,46 @@ onMounted(loadData);
background-color: #F1F5F9;
box-sizing: border-box;
padding: 16px;
gap: 10px;
gap: 8px;
position: relative;
}
.title-with-chip { display: flex; align-items: center; gap: 10px; }
.db-ping-chip {
background: #166534;
color: #DCFCE7;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.7rem;
font-weight: 700;
}
.btn-batch-trigger {
background: #E67E22;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
cursor: pointer;
}
.btn-batch-trigger:disabled { background: #D35400; cursor: not-allowed; }
.batch-progress-bar-container {
height: 4px;
background: #E2E8F0;
border-radius: 2px;
overflow: hidden;
}
.batch-progress-fill {
height: 100%;
background: #2563EB;
transition: width 0.3s ease;
}
.btn-guide-spec {
background: #8E44AD;
color: white;
@@ -435,8 +524,6 @@ onMounted(loadData);
cursor: pointer;
}
.btn-guide-spec:hover { background: #7D3C98; }
.guide-modal { width: 560px; }
.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; }
@@ -468,7 +555,7 @@ onMounted(loadData);
.crud-toolbar {
background-color: #34495E;
color: #FFFFFF;
padding: 10px 16px;
padding: 8px 16px;
border-radius: 6px;
display: flex;
justify-content: space-between;
@@ -481,7 +568,7 @@ onMounted(loadData);
.toolbar-actions {
display: flex;
align-items: center;
gap: 8px;
gap: 6px;
}
.search-input {
@@ -493,7 +580,7 @@ onMounted(loadData);
}
.btn-action {
padding: 6px 12px;
padding: 6px 10px;
border: none;
border-radius: 4px;
font-size: 0.75rem;
@@ -537,8 +624,6 @@ onMounted(loadData);
cursor: pointer;
}
.export-menu-popover button:hover { background-color: #F1F5F9; color: #2563EB; }
.badge-key {
background: rgba(255,255,255,0.2);
padding: 1px 4px;
@@ -550,7 +635,7 @@ onMounted(loadData);
display: flex;
gap: 6px;
background: white;
padding: 6px 12px;
padding: 4px 12px;
border: 1px solid #CBD5E1;
border-radius: 6px;
}
@@ -611,28 +696,43 @@ onMounted(loadData);
.panel-header {
background-color: #F8FAFC;
padding: 10px 16px;
padding: 8px 14px;
border-bottom: 1px solid #E2E8F0;
display: flex;
align-items: center;
gap: 8px;
}
.mode-switch-wrapper {
.lock-mode-wrapper {
margin-left: auto;
display: flex;
gap: 4px;
align-items: center;
gap: 8px;
}
.lock-ver-badge {
font-size: 0.65rem;
font-weight: 700;
color: #1E293B;
background: #E2E8F0;
padding: 2px 6px;
border-radius: 4px;
}
.mode-switch-wrapper {
display: flex;
gap: 2px;
background: #CBD5E1;
padding: 2px;
border-radius: 4px;
}
.mode-btn {
font-size: 0.7rem;
font-size: 0.65rem;
font-weight: 700;
border: none;
background: none;
padding: 2px 8px;
padding: 2px 6px;
border-radius: 3px;
color: #64748B;
cursor: pointer;
@@ -641,7 +741,6 @@ onMounted(loadData);
.mode-btn.active {
background: white;
color: #1E293B;
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
.panel-tag {
@@ -656,10 +755,10 @@ onMounted(loadData);
.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: 12px; }
.panel-body { flex: 1; overflow-y: auto; padding: 10px; }
.panel-footer {
padding: 8px 16px;
padding: 6px 14px;
background: #F8FAFC;
border-top: 1px solid #E2E8F0;
font-size: 0.75rem;
@@ -676,14 +775,14 @@ onMounted(loadData);
border-radius: 3px;
}
.form-scroll-body { display: flex; flex-direction: column; gap: 10px; }
.form-row { display: flex; flex-direction: column; gap: 4px; }
.form-scroll-body { display: flex; flex-direction: column; gap: 8px; }
.form-row { display: flex; flex-direction: column; gap: 2px; }
.file-dropzone-box {
border: 1px dashed #CBD5E1;
background: #F8FAFC;
border-radius: 4px;
padding: 10px;
padding: 8px;
text-align: center;
font-size: 0.75rem;
color: #64748B;
@@ -701,18 +800,18 @@ onMounted(loadData);
.audit-timeline-section {
border-top: 1px solid #E2E8F0;
padding-top: 10px;
margin-top: 6px;
padding-top: 8px;
margin-top: 4px;
}
.timeline-title {
font-size: 0.75rem;
font-weight: 700;
color: #475569;
margin: 0 0 6px 0;
margin: 0 0 4px 0;
}
.timeline-list { display: flex; flex-direction: column; gap: 4px; }
.timeline-list { display: flex; flex-direction: column; gap: 2px; }
.timeline-item {
font-size: 0.7rem;
color: #64748B;
@@ -725,8 +824,8 @@ onMounted(loadData);
.change-desc { color: #475569; }
.btn-save-form {
margin-top: 12px;
padding: 10px 0;
margin-top: 8px;
padding: 8px 0;
background-color: #2563EB;
color: white;
font-weight: 700;
@@ -735,6 +834,19 @@ onMounted(loadData);
cursor: pointer;
}
.live-telemetry-bar {
background: #1E293B;
color: #94A3B8;
padding: 4px 16px;
border-radius: 4px;
font-size: 0.7rem;
display: flex;
gap: 16px;
align-items: center;
}
.telemetry-item strong { color: #F1F5F9; }
/* Modal Dialog Styles */
.modal-backdrop {
position: fixed;