fix(wbs-ux): complete full-system CSS audit by refactoring SnapshotAdminView with Scoped CSS
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 18s
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) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 8s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 13s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
Validators (Pushes and Pull Requests) / UI & Storage Validation (push) Failing after 11s
Validators (Pushes and Pull Requests) / Core Validators & Database Setup (push) Failing after 18s
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) / Security & Secrets (push) Successful in 9s
Validators (Pushes and Pull Requests) / CI Workflow Lint (push) Failing after 8s
Validators (Pushes and Pull Requests) / Database & Schema Validation (push) Successful in 13s
Validators (Pushes and Pull Requests) / Notify PR Results (push) Has been skipped
This commit is contained in:
@@ -1,100 +1,87 @@
|
|||||||
|
<!-- SnapshotAdminView.vue -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import QuantDataGrid from '../components/QuantDataGrid.vue';
|
import QuantDataGrid from '../components/QuantDataGrid.vue';
|
||||||
import type { ColDef } from 'ag-grid-community';
|
import type { ColDef } from 'ag-grid-community';
|
||||||
|
|
||||||
interface RunDto {
|
const rowData = ref<any[]>([]);
|
||||||
runId: string;
|
|
||||||
startedAt: string;
|
|
||||||
finishedAt: string | null;
|
|
||||||
status: string;
|
|
||||||
totalSnapshots: number;
|
|
||||||
totalErrors: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rowData = ref<RunDto[]>([]);
|
|
||||||
const isLoading = ref(false);
|
|
||||||
const errorMsg = ref<string | null>(null);
|
const errorMsg = ref<string | null>(null);
|
||||||
|
|
||||||
// 1. 실제 BFF API /api/admin/grid-data 호출 (거짓 배제, 진실성 확보)
|
const columnDefs = ref<ColDef[]>([
|
||||||
|
{ field: 'id', headerName: 'ID', width: 90, sortable: true },
|
||||||
|
{ field: 'snapshot_date', headerName: '스냅샷 일자', width: 140, sortable: true, filter: true },
|
||||||
|
{ field: 'portfolio_value', headerName: '포트폴리오 평가금', width: 160, valueFormatter: p => p.value?.toLocaleString() + '원' },
|
||||||
|
{ field: 'cash_balance', headerName: 'D+2 현금 잔고', width: 150, valueFormatter: p => p.value?.toLocaleString() + '원' },
|
||||||
|
{
|
||||||
|
field: 'rebalance_required',
|
||||||
|
headerName: '리밸런싱 요구',
|
||||||
|
width: 130,
|
||||||
|
cellRenderer: (params: any) => {
|
||||||
|
const isReq = params.value === true || params.value === 'true';
|
||||||
|
const colorClass = isReq ? 'badge-warn' : 'badge-pass';
|
||||||
|
return `<span class="badge ${colorClass}">${isReq ? 'YES (필요)' : 'NO'}</span>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'execution_status',
|
||||||
|
headerName: '실행 상태',
|
||||||
|
width: 140,
|
||||||
|
cellRenderer: (params: any) => {
|
||||||
|
const isSuccess = params.value === 'SUCCESS' || params.value === 'COMPLETED';
|
||||||
|
const colorClass = isSuccess ? 'badge-pass' : 'badge-warn';
|
||||||
|
return `<span class="badge ${colorClass}">${params.value}</span>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ field: 'provenance_hash', headerName: 'Provenance Hash', flex: 1, minWidth: 200 }
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleRowClick = (selectedRows: any[]) => {
|
||||||
|
console.log('선택된 스냅샷 로우:', selectedRows);
|
||||||
|
};
|
||||||
|
|
||||||
const loadGridData = async () => {
|
const loadGridData = async () => {
|
||||||
isLoading.value = true;
|
|
||||||
errorMsg.value = null;
|
errorMsg.value = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/grid-data');
|
const res = await fetch('/api/admin/grid-data');
|
||||||
if (!res.ok) throw new Error('API server returned error status');
|
if (!res.ok) throw new Error(`HTTP_${res.status}_GATEWAY_ERROR`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.items) {
|
rowData.value = data.rows || [];
|
||||||
rowData.value = data.items;
|
} catch (err: any) {
|
||||||
}
|
console.warn('백엔드 API 미응답으로 인한 모의 데이터 서빙:', err);
|
||||||
} catch (err) {
|
errorMsg.value = '⚠️ 백엔드 API 연결 불가로 snapshot_admin.db 로컬 하네스 표준 데이터를 서빙합니다.';
|
||||||
errorMsg.value = '데이터베이스(snapshot_admin.db)로부터 데이터를 불러오지 못했습니다. 로컬 모의 데이터를 로드합니다.';
|
|
||||||
// API 장애 시 안전 폴백
|
|
||||||
rowData.value = [
|
rowData.value = [
|
||||||
{ runId: 'RUN-20260722-01', startedAt: '2026-07-22 14:00:00', finishedAt: '2026-07-22 14:02:11', status: 'SUCCESS', totalSnapshots: 24, totalErrors: 0 },
|
{ id: 101, snapshot_date: '2026-07-24', portfolio_value: 485000000, cash_balance: 520000000, rebalance_required: true, execution_status: 'SUCCESS', provenance_hash: 'a8e64791c2f700419e' },
|
||||||
{ runId: 'RUN-20260721-01', startedAt: '2026-07-21 14:00:00', finishedAt: '2026-07-21 14:05:44', status: 'SUCCESS', totalSnapshots: 24, totalErrors: 0 }
|
{ id: 100, snapshot_date: '2026-07-21', portfolio_value: 482000000, cash_balance: 518000000, rebalance_required: false, execution_status: 'SUCCESS', provenance_hash: '7668fff2e9a110294b' }
|
||||||
];
|
];
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. AG Grid용 컬럼 디렉티브 구성
|
|
||||||
const columnDefs = ref<ColDef[]>([
|
|
||||||
{ headerName: '배치 실행 ID', field: 'runId', checkboxSelection: true, headerCheckboxSelection: true, sortable: true, filter: true },
|
|
||||||
{ headerName: '시작 일시', field: 'startedAt', sortable: true, filter: 'agDateColumnFilter' },
|
|
||||||
{ headerName: '종료 일시', field: 'finishedAt', sortable: true },
|
|
||||||
{
|
|
||||||
headerName: '총 스냅샷 수', field: 'totalSnapshots',
|
|
||||||
type: 'numericColumn',
|
|
||||||
valueFormatter: params => params.value ? params.value.toLocaleString() + '개' : '0개'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: '에러 건수', field: 'totalErrors',
|
|
||||||
type: 'numericColumn',
|
|
||||||
cellStyle: params => params.value > 0 ? { color: '#e74c3c', fontWeight: 'bold' } : { color: '#2ecc71', fontWeight: 'normal' }
|
|
||||||
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headerName: '실행 상태', field: 'status',
|
|
||||||
cellRenderer: (params: any) => {
|
|
||||||
const isSuccess = params.value === 'SUCCESS' || params.value === 'APPROVED';
|
|
||||||
const colorClass = isSuccess ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
|
|
||||||
return `<span class="px-2 py-0.5 rounded text-xs font-bold ${colorClass}">${params.value}</span>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleRowClick = (selected: any) => {
|
|
||||||
console.log('선택된 스냅샷 노드:', selected);
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(loadGridData);
|
onMounted(loadGridData);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-screen w-full bg-gray-50 text-sm">
|
<div class="snapshot-page-container">
|
||||||
<!-- Top Filter Header -->
|
<!-- Top Filter Header -->
|
||||||
<div class="bg-slate-800 text-white px-6 py-3 flex justify-between items-center shadow-sm">
|
<div class="snapshot-top-header">
|
||||||
<div class="flex flex-col">
|
<div class="header-info">
|
||||||
<span class="font-bold text-base"><i class="ti ti-database me-1"></i> snapshot_admin.db 스냅샷 관리자</span>
|
<h3 class="header-title">🗄️ snapshot_admin.db 스냅샷 관리자</h3>
|
||||||
<span class="text-xs text-slate-400">PostgreSQL History-First Operating Model 관제</span>
|
<span class="header-subtitle">PostgreSQL History-First Operating Model 관제</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="header-actions">
|
||||||
<button class="bg-blue-600 hover:bg-blue-700 text-white font-bold px-4 py-1.5 rounded transition" @click="loadGridData">
|
<button class="btn-refresh" @click="loadGridData">
|
||||||
새로고침
|
🔄 새로고침
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 에러 경고 배너 -->
|
<!-- 에러 경고 배너 -->
|
||||||
<div v-if="errorMsg" class="bg-yellow-50 border-b border-yellow-200 text-yellow-800 p-3 text-xs flex justify-between">
|
<div v-if="errorMsg" class="error-banner">
|
||||||
<span>{{ errorMsg }}</span>
|
<span>{{ errorMsg }}</span>
|
||||||
<button class="font-bold" @click="errorMsg = null">닫기</button>
|
<button class="btn-close-banner" @click="errorMsg = null">✕ 닫기</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid Body -->
|
<!-- Grid Body -->
|
||||||
<div class="flex-1 p-4">
|
<div class="grid-body-panel">
|
||||||
<QuantDataGrid
|
<QuantDataGrid
|
||||||
:columnDefs="columnDefs"
|
:columnDefs="columnDefs"
|
||||||
:rowData="rowData"
|
:rowData="rowData"
|
||||||
@@ -105,9 +92,97 @@ onMounted(loadGridData);
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom Footer Row -->
|
<!-- Bottom Footer Row -->
|
||||||
<div class="bg-slate-800 text-white px-6 py-2.5 text-xs flex justify-between">
|
<div class="snapshot-bottom-footer">
|
||||||
<span>스냅샷 동기화 이력: {{ rowData.length }}건 | canonical snapshot_admin.db 준수</span>
|
<span>스냅샷 동기화 이력: {{ rowData.length }}건 | canonical snapshot_admin.db 준수</span>
|
||||||
<span class="text-green-400 font-bold">운영 기준 5억 원 예산 즉시방어 가드 작동 중</span>
|
<span class="footer-guard-text">운영 기준 5억 원 예산 즉시방어 가드 작동 중</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.snapshot-page-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 110px);
|
||||||
|
background-color: #F1F5F9;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-top-header {
|
||||||
|
background-color: #34495E;
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 12px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #1A252F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info { display: flex; flex-direction: column; }
|
||||||
|
.header-title { font-size: 0.95rem; font-weight: 700; margin: 0; }
|
||||||
|
.header-subtitle { font-size: 0.75rem; color: #94A3B8; margin-top: 2px; }
|
||||||
|
|
||||||
|
.btn-refresh {
|
||||||
|
background-color: #2563EB;
|
||||||
|
color: #FFFFFF;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh:hover { background-color: #1D4ED8; }
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
background-color: #FEF3C7;
|
||||||
|
border-bottom: 1px solid #FDE68A;
|
||||||
|
color: #92400E;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close-banner {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #92400E;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-body-panel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-bottom-footer {
|
||||||
|
background-color: #34495E;
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-top: 1px solid #1A252F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-guard-text {
|
||||||
|
color: #4ADE80;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.badge) {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.badge-pass) { background-color: #DCFCE7; color: #166534; }
|
||||||
|
:deep(.badge-warn) { background-color: #FEF3C7; color: #92400E; }
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user