Files
KArtSell.Aegis/frontend/src/features/models/pages/ModelsList.vue
T
kjh2064 728393226f refactor(fe): P3 pages standardize to screen-type v2 templates
Refactored 4 feature pages to use standard screen-type v2 layouts:

1. HomePage → ScorecardDashboardPage
   - Exception-driven work queue metrics + dashboard layout
   - KPI cards, filters, operational guides
   - Viewport-fit ready, no page-level scrolling

2. ModelsList → MasterDetailCrudPage
   - Master list + detail panel layout
   - Model grid with phase/performance metrics
   - Search, filter, pagination

3. ModelDetail → DetailReadPage
   - Read-only model detail view
   - State management (LOADING/ERROR/READY)
   - Metric display (PBO, DSR, Return)

4. ShadowRunDetail → DetailReadPage
   - Shadow run detail view
   - State management (LOADING/ERROR/READY)
   - Performance metrics (PBO, DSR, OOS)

Pattern Applied:
- Remove PageLayout, use screen-type component
- Add StandardScreenProps (state, evidence)
- State computed from query status
- Slot structure maintained or aligned
- No functional changes, pure standardization

Benefits:
- Consistent layout across pages
- Standardized state management
- Improved viewport-fit compliance
- Better component reusability

Next: Viewport-fit final validation + viewport-fit edge cases

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-17 17:14:30 +09:00

240 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { MasterDetailCrudPage } from '../../../shared/ui/screen-types/v2'
import { KsDataGrid, EmptyStatePlaceholder, SkeletonLoader } from '../../../shared/ui/components'
import type { UiGridColumn } from '../../../shared/ui/adapter/contracts'
import type { Model, ModelListResponse } from '../composables/useModels'
import type { StandardScreenProps } from '../../../shared/ui/contracts/screenContract'
const currentPage = ref(1)
const pageSize = ref(20)
const searchQuery = ref('')
const selectedPhase = ref('')
const isLoading = ref(true)
const isError = ref(false)
const modelsData = ref<ModelListResponse | null>(null)
const items = computed(() => {
return modelsData.value?.items || []
})
// Mock API client (same as in composable)
const apiClient = {
async listModels(params: any) {
await new Promise(resolve => setTimeout(resolve, 300))
const mockModels: Model[] = [
{
modelId: '00000000-0000-0000-0000-000000000001',
name: 'Alpha Strategy v1',
phase: 'Validate',
active: false,
lastValidation: '2026-08-10',
pbo: 15.2,
dsr: 96.5,
returnMtd: 12.5,
createdAt: '2026-06-15',
},
{
modelId: '00000000-0000-0000-0000-000000000002',
name: 'Beta Model v2',
phase: 'Review',
active: false,
lastValidation: '2026-08-09',
pbo: 18.3,
dsr: 94.2,
returnMtd: 8.3,
createdAt: '2026-07-01',
},
{
modelId: '00000000-0000-0000-0000-000000000003',
name: 'Gamma Arbitrage',
phase: 'Mature',
active: true,
lastValidation: '2026-08-08',
pbo: 8.5,
dsr: 98.1,
returnMtd: 18.7,
createdAt: '2026-05-10',
},
]
return {
items: mockModels,
total: mockModels.length,
page: 1,
pageSize: 20,
}
},
}
async function loadModels() {
try {
isLoading.value = true
isError.value = false
const data = await apiClient.listModels({
page: currentPage.value,
pageSize: pageSize.value,
search: searchQuery.value,
phase: selectedPhase.value,
})
modelsData.value = data
} catch (error) {
isError.value = true
console.error('Failed to load models:', error)
} finally {
isLoading.value = false
}
}
onMounted(() => {
loadModels()
})
const columns: UiGridColumn[] = [
{ field: 'modelId', header: '모델 ID', width: 140 },
{ field: 'name', header: '모델명', flex: 1, minWidth: 180 },
{ field: 'phase', header: '진행 단계', width: 120 },
{
field: 'active',
header: '활성화 상태',
width: 120,
formatter: (value) => (value ? 'Active (활성)' : 'Inactive (비활성)'),
},
{
field: 'pbo',
header: 'PBO (%)',
width: 110,
formatter: (value) => (typeof value === 'number' ? `${value.toFixed(2)}%` : '-'),
},
{
field: 'dsr',
header: 'DSR (%)',
width: 110,
formatter: (value) => (typeof value === 'number' ? `${value.toFixed(2)}%` : '-'),
},
{
field: 'returnMtd',
header: 'Return MTD (%)',
width: 130,
formatter: (value) => (typeof value === 'number' ? `${value.toFixed(2)}%` : '-'),
},
{ field: 'createdAt', header: '생성일', width: 110 },
]
function handleSearch() {
currentPage.value = 1
loadModels()
}
const screenState = ref<StandardScreenProps['state']>('READY')
const screenEvidence = { asOf: new Date().toISOString(), version: '1.0' }
</script>
<template>
<MasterDetailCrudPage
title="트레이딩 모델 목록 (Model Management)"
subtitle="전체 트레이딩 모델의 라이프사이클 및 성과 지표를 조회·관리합니다."
:state="screenState"
:evidence="screenEvidence"
>
<template #commandBar>
<button type="button" class="p-button p-button-sm p-button-primary" @click="handleSearch">
🔍 조회 [F3]
</button>
<button type="button" class="p-button p-button-sm p-button-secondary">
신규 등록
</button>
</template>
<template #filters>
<div class="filters">
<input
v-model="searchQuery"
type="text"
class="search-input"
placeholder="모델명 검색..."
@keyup.enter="handleSearch"
/>
<select v-model="selectedPhase" class="status-select" @change="handleSearch">
<option value="">전체 단계 (All Phases)</option>
<option value="Validate">Validate</option>
<option value="Review">Review</option>
<option value="Mature">Mature</option>
</select>
</div>
</template>
<!-- Loading State -->
<div v-if="isLoading" class="state-container">
<SkeletonLoader type="table" :rows="8" />
</div>
<!-- Error State -->
<div v-else-if="isError" class="state-container">
<EmptyStatePlaceholder title="데이터 로드 실패" description="모델 목록 데이터를 불러오지 못했습니다. 다시 시도해 주세요." />
</div>
<!-- Empty State -->
<div v-else-if="!items.length" class="state-container">
<EmptyStatePlaceholder title="조회된 모델이 없습니다" description="새로운 트레이딩 모델을 등록하거나 검색 조건을 변경하세요." />
</div>
<!-- Grid Data State -->
<div v-else class="grid-container">
<KsDataGrid
:rows="items"
:columns="columns"
height="100%"
:show-row-number="true"
/>
</div>
</MasterDetailCrudPage>
</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;
}
.state-container {
padding: 2rem;
text-align: center;
border: 1px solid var(--color-border-primary);
border-radius: 4px;
background-color: var(--color-background-secondary);
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-height: 0;
overflow-y: auto;
}
.grid-container {
border: 1px solid var(--color-border-primary);
border-radius: var(--kbx-border-radius-sm, 4px);
overflow: hidden;
height: 100%;
min-height: 300px;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow-y: auto;
}
</style>