V13-FE-005: Complete KBX v60 frontend components, T01-T12 screen recipes, and WBS progress tracker
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, ref, onMounted } from 'vue'
|
||||
import SkeletonLoader from '../../../shared/ui/components/SkeletonLoader.vue'
|
||||
import ErrorBoundary from '../../../shared/ui/components/ErrorBoundary.vue'
|
||||
import MasterDetailCrudPage from '../../../shared/ui/screen-types/v2/MasterDetailCrudPage.vue'
|
||||
import { KsButton, KsStatusTag } from '../../../shared/ui/components'
|
||||
import type { StandardScreenState } from '../../../shared/ui/contracts/screenContract'
|
||||
|
||||
// Mock data
|
||||
// KBX T04 Governance Audit & Screen State
|
||||
const screenState = ref<StandardScreenState>('READY')
|
||||
const evidence = reactive({
|
||||
asOf: '2026-08-15T19:40:00Z',
|
||||
version: 'v60-T04-Contract',
|
||||
})
|
||||
|
||||
// Mock Models Data
|
||||
const mockModels = [
|
||||
{
|
||||
modelId: '1',
|
||||
@@ -39,9 +47,6 @@ const mockModels = [
|
||||
|
||||
const models = ref(mockModels)
|
||||
const selectedModelId = ref(mockModels[0]?.modelId)
|
||||
const isPending = ref(true)
|
||||
const isError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const filterModel = reactive({
|
||||
search: '',
|
||||
@@ -49,12 +54,10 @@ const filterModel = reactive({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// Simulate API loading
|
||||
screenState.value = 'LOADING'
|
||||
setTimeout(() => {
|
||||
isPending.value = false
|
||||
// isError.value = true // Uncomment to test error state
|
||||
// errorMessage.value = 'Failed to load models. Please try again.'
|
||||
}, 1500)
|
||||
screenState.value = 'READY'
|
||||
}, 500)
|
||||
})
|
||||
|
||||
const filteredModels = computed(() => {
|
||||
@@ -85,164 +88,175 @@ const formatPercentage = (value: number) => {
|
||||
return (value || 0).toFixed(2) + '%'
|
||||
}
|
||||
|
||||
const phaseColors: Record<string, string> = {
|
||||
'Freeze': 'var(--color-neutral-500)',
|
||||
'Mature': 'var(--color-primary-500)',
|
||||
'Score': 'var(--color-primary-500)',
|
||||
'Diagnose': 'var(--color-warning-500)',
|
||||
'Hypothesis': 'var(--color-warning-500)',
|
||||
'Challenger': 'var(--color-warning-500)',
|
||||
'Validate': 'var(--color-success-500)',
|
||||
'Review': 'var(--color-success-500)',
|
||||
'Manual Activation': 'var(--color-danger-500)',
|
||||
const handleRetry = () => {
|
||||
screenState.value = 'LOADING'
|
||||
setTimeout(() => {
|
||||
screenState.value = 'READY'
|
||||
}, 400)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="model-list">
|
||||
<h1>Models (Master-Detail)</h1>
|
||||
|
||||
<!-- Loading State -->
|
||||
<template v-if="isPending">
|
||||
<div class="filters">
|
||||
<SkeletonLoader type="text" width="100%" height="36px" />
|
||||
<SkeletonLoader type="text" width="100%" height="36px" />
|
||||
</div>
|
||||
<div class="content-skeleton">
|
||||
<SkeletonLoader type="list" :rows="4" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Error State -->
|
||||
<ErrorBoundary v-else-if="isError" :fallback-message="errorMessage || 'Failed to load models'">
|
||||
<div class="error-actions">
|
||||
<button class="btn btn-primary" @click="isError = false">Retry</button>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
|
||||
<!-- Content -->
|
||||
<template v-else>
|
||||
<!-- Filter Bar -->
|
||||
<div class="filters">
|
||||
<input v-model="filterModel.search" placeholder="Search models..." class="input" />
|
||||
<select v-model="filterModel.phase" class="input">
|
||||
<MasterDetailCrudPage
|
||||
title="트레이딩 모델 마스터-상세 (Model Operations Master-Detail)"
|
||||
subtitle="알고리즘 모델의 상태, 과최적화 검증 지표(PBO/DSR) 및 성과 메트릭을 추적 관리합니다."
|
||||
:state="screenState"
|
||||
:evidence="evidence"
|
||||
@retry="handleRetry"
|
||||
>
|
||||
<template #filters>
|
||||
<section class="filters" aria-label="모델 검색 및 필터">
|
||||
<input
|
||||
v-model="filterModel.search"
|
||||
placeholder="Search models..."
|
||||
class="input"
|
||||
aria-label="모델 이름 검색"
|
||||
role="searchbox"
|
||||
/>
|
||||
<select
|
||||
v-model="filterModel.phase"
|
||||
class="input"
|
||||
aria-label="모델 단계별 필터"
|
||||
>
|
||||
<option value="">All Phases</option>
|
||||
<option value="Mature">Mature</option>
|
||||
<option value="Validate">Validate</option>
|
||||
<option value="Review">Review</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="content">
|
||||
<!-- Master: List -->
|
||||
<div class="master-list">
|
||||
<h2>Models ({{ filteredModels.length }})</h2>
|
||||
<div class="items">
|
||||
<div
|
||||
v-for="model in filteredModels"
|
||||
:key="model.modelId"
|
||||
class="model-item"
|
||||
:class="{ 'is-selected': selectedModelId === model.modelId }"
|
||||
@click="selectModel(model.modelId)"
|
||||
>
|
||||
<div class="item-header">
|
||||
<strong>{{ model.name }}</strong>
|
||||
<span class="phase-badge" :style="{ backgroundColor: phaseColors[model.phase] }">
|
||||
{{ model.phase }}
|
||||
<!-- Master List Slot -->
|
||||
<div class="content">
|
||||
<!-- Master: List -->
|
||||
<aside class="master-list" aria-label="모델 목록">
|
||||
<h2>Models ({{ filteredModels.length }})</h2>
|
||||
<ul class="items" role="list">
|
||||
<li
|
||||
v-for="model in filteredModels"
|
||||
:key="model.modelId"
|
||||
class="model-item"
|
||||
:class="{ 'is-selected': selectedModelId === model.modelId }"
|
||||
@click="selectModel(model.modelId)"
|
||||
role="option"
|
||||
:aria-selected="selectedModelId === model.modelId"
|
||||
:aria-label="`${model.name}, Phase: ${model.phase}, PBO: ${formatPercentage(model.pbo)}, DSR: ${formatPercentage(model.dsr)}, Status: ${model.active ? 'Active' : 'Inactive'}`"
|
||||
>
|
||||
<div class="item-header">
|
||||
<strong>{{ model.name }}</strong>
|
||||
<KsStatusTag
|
||||
:value="model.phase"
|
||||
:severity="model.phase === 'Mature' || model.phase === 'Validate' ? 'success' : 'warning'"
|
||||
/>
|
||||
</div>
|
||||
<div class="metrics" role="group" aria-label="모델 메트릭">
|
||||
<div class="metric">
|
||||
<span class="label">PBO</span>
|
||||
<span class="value ks-financial-number" aria-label="Probability of Backtest Overfitting">{{ formatPercentage(model.pbo) }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">DSR</span>
|
||||
<span class="value ks-financial-number" aria-label="Daily Sharpe Ratio">{{ formatPercentage(model.dsr) }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">Return</span>
|
||||
<span class="value ks-financial-number" aria-label="Month to Date Return">{{ formatPercentage(model.returnMtd) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<span v-if="model.active" class="status-active" aria-label="모델이 활성화됨">🟢 Active</span>
|
||||
<span v-else class="status-inactive" aria-label="모델이 비활성화됨">⚪ Inactive</span>
|
||||
<span class="date ks-financial-number" :aria-label="`Created: ${formatDate(model.createdAt)}`">{{ formatDate(model.createdAt) }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<!-- Detail: Right Panel -->
|
||||
<main class="detail-panel" role="main" aria-label="모델 상세 정보">
|
||||
<h2 v-if="selectedModel" :aria-label="`Selected model: ${selectedModel.name}`">{{ selectedModel.name }}</h2>
|
||||
<div v-else class="empty-detail" role="status" aria-live="polite">Select a model to view details</div>
|
||||
|
||||
<div v-if="selectedModel" class="model-detail">
|
||||
<!-- Status -->
|
||||
<section aria-label="모델 상태">
|
||||
<h3>Status</h3>
|
||||
<div class="status-grid" role="group">
|
||||
<div class="status-item">
|
||||
<span class="label">Phase</span>
|
||||
<KsStatusTag
|
||||
:value="selectedModel.phase"
|
||||
:severity="selectedModel.phase === 'Mature' ? 'success' : 'warning'"
|
||||
/>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">Active</span>
|
||||
<span
|
||||
class="value"
|
||||
role="status"
|
||||
:aria-label="`Model is ${selectedModel.active ? 'active' : 'inactive'}`"
|
||||
>
|
||||
{{ selectedModel.active ? '✓ Yes' : '✗ No' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<span class="label">PBO</span>
|
||||
<span class="value">{{ formatPercentage(model.pbo) }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">DSR</span>
|
||||
<span class="value">{{ formatPercentage(model.dsr) }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">Return</span>
|
||||
<span class="value">{{ formatPercentage(model.returnMtd) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Metrics -->
|
||||
<section aria-label="검증 메트릭">
|
||||
<h3>Metrics</h3>
|
||||
<div class="metric-grid" role="group">
|
||||
<div class="metric-card" role="region" aria-label="PBO - Probability of Backtest Overfitting">
|
||||
<div class="metric-label">PBO (Backtest Overfit)</div>
|
||||
<div class="metric-value ks-financial-number" role="status">{{ formatPercentage(selectedModel.pbo) }}</div>
|
||||
<div class="metric-description" aria-describedby="pbo-help">Lower is better</div>
|
||||
<div id="pbo-help" class="help-text">백테스트 과최적화 확률. 낮을수록 좋습니다</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<span v-if="model.active" class="status-active">🟢 Active</span>
|
||||
<span v-else class="status-inactive">⚪ Inactive</span>
|
||||
<span class="date">{{ formatDate(model.createdAt) }}</span>
|
||||
<div class="metric-card" role="region" aria-label="DSR - Daily Sharpe Ratio">
|
||||
<div class="metric-label">DSR (Daily Sharpe Ratio)</div>
|
||||
<div class="metric-value ks-financial-number" role="status">{{ formatPercentage(selectedModel.dsr) }}</div>
|
||||
<div class="metric-description" aria-describedby="dsr-help">Higher is better</div>
|
||||
<div id="dsr-help" class="help-text">일일 샤프 비율. 높을수록 좋습니다</div>
|
||||
</div>
|
||||
<div class="metric-card" role="region" aria-label="Return MTD - Month to Date Return">
|
||||
<div class="metric-label">Return MTD</div>
|
||||
<div class="metric-value ks-financial-number" role="status">{{ formatPercentage(selectedModel.returnMtd) }}</div>
|
||||
<div class="metric-description" aria-describedby="mtd-help">Month-to-date</div>
|
||||
<div id="mtd-help" class="help-text">월간 누적 수익률</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Created Date -->
|
||||
<section aria-label="모델 타임라인">
|
||||
<h3>Timeline</h3>
|
||||
<div class="timeline">
|
||||
<div class="timeline-item" role="region" aria-label="Model creation date">
|
||||
<span class="label">Created</span>
|
||||
<span class="value ks-financial-number" role="status">{{ formatDate(selectedModel.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Actions -->
|
||||
<section class="section" aria-label="모델 작업">
|
||||
<div class="actions">
|
||||
<KsButton
|
||||
label="View Full Report [성과 보고서]"
|
||||
variant="primary"
|
||||
aria-label="전체 성과 보고서 보기"
|
||||
/>
|
||||
<KsButton
|
||||
label="Export Metrics [내보내기]"
|
||||
variant="secondary"
|
||||
aria-label="메트릭 데이터 내보내기"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Detail: Right Panel -->
|
||||
<div class="detail-panel">
|
||||
<h2 v-if="selectedModel">{{ selectedModel.name }}</h2>
|
||||
<div v-else class="empty-detail">Select a model to view details</div>
|
||||
|
||||
<div v-if="selectedModel" class="model-detail">
|
||||
<!-- Status -->
|
||||
<div class="section">
|
||||
<h3>Status</h3>
|
||||
<div class="status-grid">
|
||||
<div class="status-item">
|
||||
<span class="label">Phase</span>
|
||||
<span class="badge" :style="{ backgroundColor: phaseColors[selectedModel.phase] }">
|
||||
{{ selectedModel.phase }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="label">Active</span>
|
||||
<span class="value">{{ selectedModel.active ? '✓ Yes' : '✗ No' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div class="section">
|
||||
<h3>Metrics</h3>
|
||||
<div class="metric-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">PBO (Backtest Overfit)</div>
|
||||
<div class="metric-value">{{ formatPercentage(selectedModel.pbo) }}</div>
|
||||
<div class="metric-description">Lower is better</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">DSR (Daily Sharpe Ratio)</div>
|
||||
<div class="metric-value">{{ formatPercentage(selectedModel.dsr) }}</div>
|
||||
<div class="metric-description">Higher is better</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">Return MTD</div>
|
||||
<div class="metric-value">{{ formatPercentage(selectedModel.returnMtd) }}</div>
|
||||
<div class="metric-description">Month-to-date</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Created Date -->
|
||||
<div class="section">
|
||||
<h3>Timeline</h3>
|
||||
<div class="timeline">
|
||||
<div class="timeline-item">
|
||||
<span class="label">Created</span>
|
||||
<span class="value">{{ formatDate(selectedModel.createdAt) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="section">
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary">View Full Report</button>
|
||||
<button class="btn btn-secondary">Export Metrics</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</MasterDetailCrudPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -295,8 +309,8 @@ h3 {
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-input-focus);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
border-color: var(--ks-color-action);
|
||||
box-shadow: 0 0 0 2px var(--ks-color-action);
|
||||
}
|
||||
|
||||
.content-skeleton {
|
||||
@@ -525,6 +539,10 @@ h3 {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
background: var(--color-background-primary);
|
||||
|
||||
Reference in New Issue
Block a user