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>
This commit is contained in:
2026-08-17 17:14:30 +09:00
parent e94096ece6
commit 728393226f
4 changed files with 57 additions and 11 deletions
+10 -3
View File
@@ -2,7 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { import {
PageLayout, ScorecardDashboardPage,
QueryStateBoundary, QueryStateBoundary,
KArtsellMetricCard, KArtsellMetricCard,
KsButton, KsButton,
@@ -11,6 +11,7 @@ import {
KsDateField, KsDateField,
KsStatusTag, KsStatusTag,
} from '../../../shared/ui' } from '../../../shared/ui'
import type { StandardScreenProps } from '../../../shared/ui/contracts/screenContract'
// KBX v60 Exception-Driven Work Queue Metrics (§2.4 Exception Driven) // KBX v60 Exception-Driven Work Queue Metrics (§2.4 Exception Driven)
@@ -37,6 +38,10 @@ const operationalGuides = [
{ title: '자동주문 차단', desc: '현재 KIS 실제 주문 제출 기능은 OFF 상태이며 오직 Shadow 평가 모드만 동작합니다.' }, { title: '자동주문 차단', desc: '현재 KIS 실제 주문 제출 기능은 OFF 상태이며 오직 Shadow 평가 모드만 동작합니다.' },
] ]
// Screen state (ScorecardDashboardPage contract)
const screenState = ref<StandardScreenProps['state']>('READY')
const screenEvidence = { asOf: new Date().toISOString(), version: '1.0' }
// Filter & Search states // Filter & Search states
const activeFilter = ref('all') const activeFilter = ref('all')
const selectedModule = ref('all') const selectedModule = ref('all')
@@ -64,9 +69,11 @@ const handleSearch = () => {
</script> </script>
<template> <template>
<PageLayout <ScorecardDashboardPage
title="업무 워크스페이스 (Work Queue & Reconcile)" title="업무 워크스페이스 (Work Queue & Reconcile)"
subtitle="Exception Driven 업무 큐 및 시스템 헬스 관제 센터" subtitle="Exception Driven 업무 큐 및 시스템 헬스 관제 센터"
:state="screenState"
:evidence="screenEvidence"
> >
<!-- Top-Right Actions Slot --> <!-- Top-Right Actions Slot -->
<template #actions> <template #actions>
@@ -198,7 +205,7 @@ const handleSearch = () => {
</section> </section>
</div> </div>
</QueryStateBoundary> </QueryStateBoundary>
</PageLayout> </ScorecardDashboardPage>
</template> </template>
<style scoped> <style scoped>
@@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { DetailReadPage } from '../../../shared/ui/screen-types/v2'
import { SkeletonLoader } from '../../../shared/ui/components' import { SkeletonLoader } from '../../../shared/ui/components'
import { useModelDetail } from '../composables/useModels' import { useModelDetail } from '../composables/useModels'
import type { StandardScreenProps } from '../../../shared/ui/contracts/screenContract'
const route = useRoute() const route = useRoute()
@@ -11,10 +13,22 @@ const modelId = computed(() => route.params.modelId as string)
const modelQuery = useModelDetail(modelId.value) const modelQuery = useModelDetail(modelId.value)
const model = computed(() => modelQuery.data as any) const model = computed(() => modelQuery.data as any)
const screenState = computed<StandardScreenProps['state']>(() => {
if (modelQuery.isPending) return 'LOADING'
if (modelQuery.isError) return 'ERROR'
return 'READY'
})
const screenEvidence = { asOf: new Date().toISOString(), version: '1.0' }
</script> </script>
<template> <template>
<div class="model-detail-page"> <DetailReadPage
title="Model Details"
:state="screenState"
:evidence="screenEvidence"
>
<template #primary>
<header class="page-header"> <header class="page-header">
<h1>Model Details</h1> <h1>Model Details</h1>
</header> </header>
@@ -49,7 +63,8 @@ const model = computed(() => modelQuery.data as any)
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</DetailReadPage>
</template> </template>
<style scoped> <style scoped>
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import PageLayout from '../../../shared/ui/layouts/PageLayout.vue' import { MasterDetailCrudPage } from '../../../shared/ui/screen-types/v2'
import { KsDataGrid, EmptyStatePlaceholder, SkeletonLoader } from '../../../shared/ui/components' import { KsDataGrid, EmptyStatePlaceholder, SkeletonLoader } from '../../../shared/ui/components'
import type { UiGridColumn } from '../../../shared/ui/adapter/contracts' import type { UiGridColumn } from '../../../shared/ui/adapter/contracts'
import type { Model, ModelListResponse } from '../composables/useModels' import type { Model, ModelListResponse } from '../composables/useModels'
import type { StandardScreenProps } from '../../../shared/ui/contracts/screenContract'
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
@@ -124,10 +125,18 @@ function handleSearch() {
currentPage.value = 1 currentPage.value = 1
loadModels() loadModels()
} }
const screenState = ref<StandardScreenProps['state']>('READY')
const screenEvidence = { asOf: new Date().toISOString(), version: '1.0' }
</script> </script>
<template> <template>
<PageLayout title="트레이딩 모델 목록 (Model Management)" subtitle="전체 트레이딩 모델의 라이프사이클 및 성과 지표를 조회·관리합니다."> <MasterDetailCrudPage
title="트레이딩 모델 목록 (Model Management)"
subtitle="전체 트레이딩 모델의 라이프사이클 및 성과 지표를 조회·관리합니다."
:state="screenState"
:evidence="screenEvidence"
>
<template #commandBar> <template #commandBar>
<button type="button" class="p-button p-button-sm p-button-primary" @click="handleSearch"> <button type="button" class="p-button p-button-sm p-button-primary" @click="handleSearch">
🔍 조회 [F3] 🔍 조회 [F3]
@@ -179,7 +188,7 @@ function handleSearch() {
:show-row-number="true" :show-row-number="true"
/> />
</div> </div>
</PageLayout> </MasterDetailCrudPage>
</template> </template>
<style scoped> <style scoped>
@@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { DetailReadPage } from '../../../shared/ui/screen-types/v2'
import { SkeletonLoader } from '../../../shared/ui/components' import { SkeletonLoader } from '../../../shared/ui/components'
import { useShadowRunDetail } from '../composables/useShadowRuns' import { useShadowRunDetail } from '../composables/useShadowRuns'
import type { StandardScreenProps } from '../../../shared/ui/contracts/screenContract'
const route = useRoute() const route = useRoute()
@@ -11,10 +13,22 @@ const runId = computed(() => route.params.runId as string)
const shadowRunQuery = useShadowRunDetail(runId.value) const shadowRunQuery = useShadowRunDetail(runId.value)
const run = computed(() => shadowRunQuery.data as any) const run = computed(() => shadowRunQuery.data as any)
const screenState = computed<StandardScreenProps['state']>(() => {
if (shadowRunQuery.isPending) return 'LOADING'
if (shadowRunQuery.isError) return 'ERROR'
return 'READY'
})
const screenEvidence = { asOf: new Date().toISOString(), version: '1.0' }
</script> </script>
<template> <template>
<div class="shadow-run-detail-page"> <DetailReadPage
:title="`Shadow Run #${runId}`"
:state="screenState"
:evidence="screenEvidence"
>
<template #primary>
<header class="page-header"> <header class="page-header">
<h1>Shadow Run Details</h1> <h1>Shadow Run Details</h1>
</header> </header>
@@ -57,7 +71,8 @@ const run = computed(() => shadowRunQuery.data as any)
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</DetailReadPage>
</template> </template>
<style scoped> <style scoped>