1be7029f8f
## Summary - BatchOperationsPageV2: add overflow-y: auto (ShadowRunQueue, DataQualityPage) - ModelsList: add flex:1 + min-height:0 + overflow-y:auto - ShadowRunList: change height to 100% (from calc(100vh - 210px)) - ModelOperationsPage: add overflow-y: auto - WbsWorkspacePage: add flex:1 + min-height:0 + overflow-y:auto - IngestionStatus, CommonCodeManagementPage: already fitted (via component inheritance) - MarketDataIngestion: already fitted (EditFormPage) - HomePage, RebalanceForm, UiStandardPage: already fitted (earlier session) Total: 11 pages viewport-fit, 7 pages already compliant Still needed: - ModelDetail, ShadowRunDetail: need PageLayout wrapping or refactoring Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
162 lines
4.8 KiB
Vue
162 lines
4.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, reactive, h } from 'vue'
|
|
import BatchOperationsPageV2 from '../../../shared/ui/screen-types/v2/BatchOperationsPageV2.vue'
|
|
import DataGridShell from '../../../shared/ui/DataGridShell.vue'
|
|
import { KsButton, KsSelect, KsStatusTag, KsTextField } from '../../../shared/ui/components'
|
|
import type { StandardScreenState } from '../../../shared/ui/contracts/screenContract'
|
|
import type { UiGridColumn } from '../../../shared/ui/adapter/contracts'
|
|
import { formatAsOf } from '../../../shared/formatters/financial'
|
|
|
|
const screenState = ref<StandardScreenState>('READY')
|
|
const evidence = reactive({
|
|
asOf: new Date().toISOString(),
|
|
version: 'v60-DAT03-Contract',
|
|
})
|
|
|
|
const filterModel = reactive({
|
|
source: '',
|
|
status: '',
|
|
})
|
|
|
|
const statusOptions = [
|
|
{ label: '전체 상태', value: '' },
|
|
{ label: '정상 (PASSED)', value: 'PASSED' },
|
|
{ label: '격리 (QUARANTINED)', value: 'QUARANTINED' },
|
|
]
|
|
|
|
// Production/Demo Data Quality Audits
|
|
const mockQualityRuns = [
|
|
{
|
|
runId: 'DQ-20260815-01',
|
|
source: 'KIS_DAILY_OHLCV',
|
|
session: '2026-08-15',
|
|
status: 'PASSED',
|
|
rowCount: 2850,
|
|
failedRows: 0,
|
|
sourceWatermark: '2026-08-15 15:30:00',
|
|
datasetId: 'DS-KOSPI-CORE-v2',
|
|
completedAt: '2026-08-15T16:00:00Z'
|
|
},
|
|
{
|
|
runId: 'DQ-20260815-02',
|
|
source: 'KIS_MINUTE_5M',
|
|
session: '2026-08-15',
|
|
status: 'PASSED',
|
|
rowCount: 145000,
|
|
failedRows: 0,
|
|
sourceWatermark: '2026-08-15 15:30:00',
|
|
datasetId: 'DS-INTRADAY-v1',
|
|
completedAt: '2026-08-15T16:05:00Z'
|
|
},
|
|
{
|
|
runId: 'DQ-20260814-03',
|
|
source: 'MACRO_INDICATORS',
|
|
session: '2026-08-14',
|
|
status: 'QUARANTINED',
|
|
rowCount: 120,
|
|
failedRows: 3,
|
|
sourceWatermark: '2026-08-14 23:59:59',
|
|
datasetId: 'DS-GLOBAL-MACRO-v3',
|
|
completedAt: '2026-08-15T01:10:00Z'
|
|
},
|
|
{
|
|
runId: 'DQ-20260814-02',
|
|
source: 'CORPORATE_ACTIONS',
|
|
session: '2026-08-14',
|
|
status: 'PASSED',
|
|
rowCount: 45,
|
|
failedRows: 0,
|
|
sourceWatermark: '2026-08-14 18:00:00',
|
|
datasetId: 'DS-EVENT-CORP-v1',
|
|
completedAt: '2026-08-14T19:30:00Z'
|
|
}
|
|
]
|
|
|
|
const filteredRows = computed(() => {
|
|
return mockQualityRuns.filter(r => {
|
|
const matchesSource = !filterModel.source || r.source.toLowerCase().includes(filterModel.source.toLowerCase())
|
|
const matchesStatus = !filterModel.status || r.status === filterModel.status
|
|
return matchesSource && matchesStatus
|
|
})
|
|
})
|
|
|
|
const columns = computed<UiGridColumn[]>(() => [
|
|
{ field: 'runId', header: '검증 ID', minWidth: 130, flex: 2 },
|
|
{ field: 'source', header: '데이터 소스', minWidth: 160, flex: 2 },
|
|
{ field: 'session', header: '영업 세션', minWidth: 100, flex: 1 },
|
|
{ field: 'status', header: '품질 상태', minWidth: 120, flex: 1 },
|
|
{ field: 'rowCount', header: '전체 행', minWidth: 90, flex: 1, formatter: val => Number(val).toLocaleString() },
|
|
{ field: 'failedRows', header: '결함 행', minWidth: 80, flex: 1, formatter: val => Number(val) > 0 ? `⚠️ ${val}` : '0' },
|
|
{ field: 'sourceWatermark', header: '워터마크', minWidth: 150, flex: 2 },
|
|
{ field: 'datasetId', header: '생성 데이터셋 ID', minWidth: 160, flex: 2 },
|
|
{ field: 'completedAt', header: '검증 시각', minWidth: 160, flex: 2, formatter: val => val ? formatAsOf(String(val)) : '—' }
|
|
])
|
|
|
|
const handleRetry = () => {
|
|
screenState.value = 'LOADING'
|
|
setTimeout(() => {
|
|
screenState.value = 'READY'
|
|
}, 300)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<BatchOperationsPageV2
|
|
title="데이터 품질 운영 (Data Quality Lineage & Audits)"
|
|
subtitle="Raw→PIT→DQ→Dataset lineage를 정밀 검증합니다. QUARANTINED(격리) 상태 데이터는 추천 및 리밸런싱에 사용이 자동 차단됩니다."
|
|
:state="screenState"
|
|
:evidence="evidence"
|
|
@retry="handleRetry"
|
|
>
|
|
<template #filters>
|
|
<KsTextField
|
|
v-model="filterModel.source"
|
|
label="소스명"
|
|
placeholder="소스명 검색 (예: KIS_DAILY...)"
|
|
class="ks-set-md"
|
|
/>
|
|
<KsSelect
|
|
v-model="filterModel.status"
|
|
label="품질 상태"
|
|
:options="statusOptions"
|
|
class="ks-set-md"
|
|
/>
|
|
</template>
|
|
|
|
<template #actions>
|
|
<KsButton
|
|
label="⚡ 데이터 품질 검증 실행"
|
|
variant="primary"
|
|
aria-label="수동 품질 검증 배치 트리거"
|
|
/>
|
|
</template>
|
|
|
|
<DataGridShell
|
|
:rows="filteredRows"
|
|
:columns="columns"
|
|
empty-message="조회 조건에 해당하는 데이터 품질 검증 이력이 없습니다."
|
|
height="100%"
|
|
/>
|
|
</BatchOperationsPageV2>
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
.filters {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--ks-space-3);
|
|
width: 100%;
|
|
}
|
|
|
|
.search-input {
|
|
min-width: 200px;
|
|
max-width: 350px;
|
|
width: 100%;
|
|
}
|
|
|
|
.status-select {
|
|
min-width: 150px;
|
|
}
|
|
</style>
|