V13-FE-005: Complete KBX v60 frontend components, T01-T12 screen recipes, and WBS progress tracker

This commit is contained in:
2026-08-15 19:48:38 +09:00
parent e0460e000d
commit 938ec1842a
31 changed files with 1783 additions and 818 deletions
+215 -218
View File
@@ -1,254 +1,251 @@
<script setup lang="ts">
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
interface Module {
name: string
icon: string
description: string
screens: Array<{ label: string; path: string; icon: string }>
}
const modules: Module[] = [
{
name: 'Model Operations',
icon: '🤖',
description: 'Manage models, run shadow tests, and track performance',
screens: [
{ label: 'Shadow Run Queue', path: '/model-ops/shadow-run-jobs', icon: '⚡' },
{ label: 'Model List', path: '/model-ops/models-master', icon: '📊' },
],
},
{
name: 'Governance',
icon: '⚖️',
description: 'Approval workflows and compliance management',
screens: [{ label: 'Approval Queue', path: '/governance/approvals', icon: '✅' }],
},
// KBX v60 Exception-Driven Work Queue Metrics (§2.4 Exception Driven)
const workQueueSummary = [
{ label: '전체 모델 수', count: 42, status: 'normal', filter: 'all' },
{ label: 'Shadow Run 실시간', count: 6, status: 'running', filter: 'running' },
{ label: '승인 대기 (Maker-Checker)', count: 8, status: 'warning', filter: 'approval' },
{ label: 'PBO/DSR 검증 경고', count: 3, status: 'danger', filter: 'warning' },
{ label: '데이터 수집 예외', count: 2, status: 'danger', filter: 'exception' }
]
// KBX v60 Exception Items (처리 대상 13건 노출)
const exceptionItems = [
{ id: 'EX-2026-001', type: 'DSR/PBO 미달', model: 'Aegis-Core-Alpha-v1.4', module: '매도 의사결정', status: '경고', time: '10분 전', route: '/governance/approvals' },
{ id: 'EX-2026-002', type: 'Maker-Checker 승인대기', model: 'K-Trend-Sell-v2.1', module: '포트폴리오 리밸런싱', status: '대기', time: '25분 전', route: '/governance/approvals' },
{ id: 'EX-2026-003', type: 'KIS API 수집 오류', model: 'MarketData-KRX-Realtime', module: '시장 데이터 수집', status: '오류', time: '1시간 전', route: '/ops/market-data-history' },
{ id: 'EX-2026-004', type: 'Shadow Run 타임아웃', model: 'Shadow-Batch-Job-8492', module: 'Shadow Run', status: '오류', time: '2시간 전', route: '/model-ops/shadow-run-jobs' },
]
// KBX Quick System Rules (§2.1 Familiar First)
const operationalGuides = [
{ title: '단축키 가이드', desc: '조회 [F3], 저장 [F8], 메뉴 검색 [Ctrl+K] 키로 업무를 고속 수행합니다.' },
{ title: '감사 추적 보존', desc: '모든 데이터 및 모델 승인 변경은 수정/삭제 없이 Correction Event로 영구 보존됩니다.' },
{ title: '자동주문 차단', desc: '현재 KIS 실제 주문 제출 기능은 OFF 상태이며 오직 Shadow 평가 모드만 동작합니다.' },
]
// KBX v60 Command Action Handler (F3/F8)
const activeFilter = ref('all')
const handleFilter = (filterKey: string) => {
activeFilter.value = filterKey
}
</script>
<template>
<div class="home-page">
<!-- Hero Section -->
<header class="home-header">
<div class="hero-content">
<h1>K-ArtSell Aegis</h1>
<p class="tagline">Financial Advisory System</p>
<p class="description">Professional portfolio analysis, model validation, and governance management</p>
<div class="kbx-business-home">
<!-- KBX Page Header (§7: Height 48px) -->
<header class="kbx-page-header">
<div class="kbx-page-header__breadcrumb">K-ArtSell Aegis > > 업무 워크스페이스</div>
<div class="kbx-page-header__title-row">
<h1 class="kbx-page-header__title">업무 워크스페이스 (Work Queue & Reconcile)</h1>
<div class="kbx-page-header__actions">
<span class="kbx-badge kbx-badge--info">KBX v60 Standard</span>
<span class="kbx-badge kbx-badge--warning">자동주문/KIS OFF</span>
</div>
</div>
</header>
<!-- Modules Grid -->
<main class="home-content">
<section v-for="module in modules" :key="module.name" class="module-card">
<!-- Card Header -->
<div class="module-header">
<div class="module-title">
<span class="module-icon">{{ module.icon }}</span>
<h2>{{ module.name }}</h2>
</div>
<p class="module-description">{{ module.description }}</p>
</div>
<!-- KBX Command Bar (§5: Height 44px) -->
<div class="kbx-command-bar">
<div class="kbx-command-bar__group">
<button type="button" class="kbx-btn kbx-btn--primary">
<span class="kbx-btn__shortcut">[F3]</span> 새로고침
</button>
<button type="button" class="kbx-btn kbx-btn--secondary">
예외건만 보기
</button>
</div>
<div class="kbx-command-bar__group">
<RouterLink to="/model-ops/shadow-run-jobs" class="kbx-btn kbx-btn--secondary">
Shadow Run 실행
</RouterLink>
<RouterLink to="/governance/approvals" class="kbx-btn kbx-btn--secondary">
승인 이동
</RouterLink>
</div>
</div>
<!-- Screens Navigation -->
<nav class="screen-list">
<RouterLink
v-for="screen in module.screens"
:key="screen.path"
:to="screen.path"
class="screen-link"
>
<span class="screen-icon">{{ screen.icon }}</span>
<span class="screen-label">{{ screen.label }}</span>
<span class="arrow"></span>
</RouterLink>
</nav>
</section>
</main>
<!-- KBX Search Panel (§6: Dense 34px) -->
<div class="kbx-search-panel">
<div class="kbx-search-panel__row">
<div class="kbx-search-field">
<label for="home-search-date">조회기간</label>
<input id="home-search-date" type="text" value="2026-08-08 ~ 2026-08-15" class="kbx-input" readonly />
</div>
<div class="kbx-search-field">
<label for="home-search-module">업무모듈</label>
<select id="home-search-module" class="kbx-select">
<option value="all">전체 모듈</option>
<option value="research">Research / 매도</option>
<option value="model">ModelOps</option>
<option value="governance">Governance</option>
</select>
</div>
<div class="kbx-search-field">
<label for="home-search-keyword">통합검색</label>
<input id="home-search-keyword" type="text" placeholder="모델ID / 예외코드 / 담당자" class="kbx-input" />
</div>
<button type="button" class="kbx-btn kbx-btn--primary">조회 [F3]</button>
</div>
</div>
<!-- KBX Exception-Driven KPI Toolbar (KBX Reference Screen §11) -->
<div class="kbx-kpi-bar" role="toolbar" aria-label="예외 상태 요약">
<div
v-for="item in workQueueSummary"
:key="item.filter"
class="kbx-kpi-item kbx-card"
:class="[
`kbx-kpi-item--${item.status}`,
{ 'kbx-kpi-item--active': activeFilter === item.filter }
]"
@click="handleFilter(item.filter)"
>
<div class="kbx-kpi-item__label">{{ item.label }}</div>
<div class="kbx-kpi-item__count ks-financial-number">{{ item.count }}</div>
</div>
</div>
<!-- KBX Exception Work Queue Grid (T06 / T07 Reference Screen) -->
<section class="kbx-workspace-grid kbx-card">
<div class="kbx-section-header">
<h2 class="kbx-section-title"> 긴급 처리 필요 예외 (Exception Driven Work Queue)</h2>
<span class="kbx-section-count"> {{ exceptionItems.length }} 처리 필요</span>
</div>
<div class="kbx-table-container">
<table class="kbx-grid-table">
<thead>
<tr>
<th style="width: 40px;"><input type="checkbox" aria-label="전체 선택" /></th>
<th style="width: 120px;">예외ID</th>
<th style="width: 160px;">예외 유형</th>
<th>대상 모델</th>
<th style="width: 140px;">업무 모듈</th>
<th style="width: 80px;">상태</th>
<th style="width: 100px;">발생 시간</th>
<th style="width: 100px;">조치</th>
</tr>
</thead>
<tbody>
<tr v-for="ex in exceptionItems" :key="ex.id" class="kbx-grid-row">
<td><input type="checkbox" :aria-label="`${ex.id} 선택`" /></td>
<td class="ks-financial-number">{{ ex.id }}</td>
<td>
<span class="kbx-exception-tag" :class="ex.status === '오류' ? 'kbx-exception-tag--danger' : 'kbx-exception-tag--warning'">
{{ ex.type }}
</span>
</td>
<td class="kbx-model-name"><strong>{{ ex.model }}</strong></td>
<td>{{ ex.module }}</td>
<td>
<span class="kbx-status-badge" :class="`kbx-status-badge--${ex.status}`">{{ ex.status }}</span>
</td>
<td class="ks-financial-number">{{ ex.time }}</td>
<td>
<RouterLink :to="ex.route" class="kbx-btn kbx-btn--xs kbx-btn--secondary">
조치하기
</RouterLink>
</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- KBX System Operational Rules (§2.1 Familiar First) -->
<section class="kbx-guide-grid">
<div v-for="guide in operationalGuides" :key="guide.title" class="kbx-guide-card kbx-card">
<h3 class="kbx-guide-title">📌 {{ guide.title }}</h3>
<p class="kbx-guide-desc">{{ guide.desc }}</p>
</div>
</section>
</div>
</template>
<style scoped>
.home-page {
padding: var(--spacing-6);
max-width: 1280px;
margin: 0 auto;
.kbx-business-home {
display: flex;
flex-direction: column;
gap: var(--ks-space-3);
background: var(--ks-color-canvas);
color: var(--ks-color-text);
font-family: Pretendard, system-ui, sans-serif;
}
/* Hero Section */
.home-header {
margin-bottom: var(--spacing-8);
padding: var(--spacing-6);
background: linear-gradient(135deg, var(--color-primary-50) 0%, var(--color-secondary-50) 100%);
border-radius: var(--border-radius-lg);
border: 1px solid var(--color-border-primary);
/* Page Header 48px (§7) */
.kbx-page-header {
height: var(--ks-shell-pageheader-height);
display: flex;
flex-direction: column;
justify-content: center;
border-bottom: 1px solid var(--ks-color-border);
}
.hero-content h1 {
.kbx-page-header__breadcrumb {
font-size: var(--ks-font-caption);
color: var(--ks-color-text-muted);
}
.kbx-page-header__title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.kbx-page-header__title {
font-size: var(--ks-font-page);
font-weight: 600;
margin: 0;
font-size: 3rem;
font-weight: 800;
background: linear-gradient(135deg, var(--color-primary-600), var(--color-secondary-600));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.tagline {
margin: var(--spacing-2) 0 var(--spacing-1) 0;
font-size: var(--font-size-lg);
color: var(--color-text-secondary);
font-weight: var(--font-weight-medium);
.kbx-badge {
font-size: 11px;
font-weight: 600;
padding: 2px 6px;
border-radius: var(--ks-radius-sm);
margin-left: 6px;
}
.description {
margin: 0;
font-size: var(--font-size-sm);
color: var(--color-text-tertiary);
.kbx-badge--info { background: var(--ks-color-neutral-100); color: var(--ks-color-info); }
.kbx-badge--warning { background: var(--ks-color-neutral-100); color: var(--ks-color-warning); }
.kbx-btn--primary {
background: var(--ks-color-action);
color: var(--ks-color-text-on-dark);
border-color: var(--ks-color-action);
}
/* Content Grid */
.home-content {
.kbx-status-badge--오류 { background: var(--ks-color-neutral-100); color: var(--ks-color-danger); }
.kbx-status-badge--경고, .kbx-status-badge--대기 { background: var(--ks-color-neutral-100); color: var(--ks-color-warning); }
/* Guide Cards Grid */
.kbx-guide-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: var(--spacing-6);
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: var(--ks-space-3);
}
/* Module Card */
.module-card {
background-color: var(--color-background-primary);
border: 1px solid var(--color-border-primary);
border-radius: var(--border-radius-lg);
padding: var(--spacing-6);
transition: all var(--transition-base);
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-4);
.kbx-guide-card {
background: var(--ks-color-surface);
border: 1px solid var(--ks-color-border);
border-radius: var(--ks-radius-sm);
padding: var(--ks-space-3);
}
.module-card:hover {
border-color: var(--color-primary-300);
box-shadow: var(--shadow-md);
transform: translateY(-4px);
.kbx-guide-title {
font-size: var(--ks-font-body);
font-weight: 600;
margin: 0 0 var(--ks-space-1) 0;
}
/* Module Header */
.module-header {
display: flex;
flex-direction: column;
gap: var(--spacing-3);
}
.module-title {
display: flex;
align-items: center;
gap: var(--spacing-3);
}
.module-icon {
font-size: 2rem;
display: flex;
align-items: center;
}
.module-title h2 {
.kbx-guide-desc {
font-size: var(--ks-font-caption);
color: var(--ks-color-text-muted);
margin: 0;
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.module-description {
margin: 0;
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
line-height: 1.6;
}
/* Screen List */
.screen-list {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
margin-top: auto;
}
.screen-link {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-3) var(--spacing-4);
border-radius: var(--border-radius-md);
background-color: var(--color-background-secondary);
color: var(--color-text-primary);
text-decoration: none;
transition: all var(--transition-base);
border: 1px solid transparent;
cursor: pointer;
font-weight: var(--font-weight-medium);
}
.screen-link:hover {
background-color: var(--color-primary-50);
border-color: var(--color-primary-200);
color: var(--color-primary-600);
}
.screen-link:active {
transform: scale(0.98);
}
.screen-icon {
font-size: 1.25rem;
flex-shrink: 0;
}
.screen-label {
flex: 1;
font-size: var(--font-size-sm);
}
.arrow {
color: var(--color-primary-500);
opacity: 0;
transition: all var(--transition-base);
transform: translateX(-4px);
}
.screen-link:hover .arrow {
opacity: 1;
transform: translateX(0);
}
/* Responsive */
@media (max-width: 768px) {
.home-page {
padding: var(--spacing-4);
}
.home-content {
grid-template-columns: 1fr;
}
.hero-content h1 {
font-size: 2rem;
}
.module-card {
padding: var(--spacing-4);
}
}
@media (prefers-reduced-motion: reduce) {
.module-card,
.screen-link,
.arrow {
transition: none;
}
.screen-link:active {
transform: none;
}
line-height: 1.4;
}
</style>