Files
KArtSell.Aegis/frontend/src/features/ui-standard/pages/UiStandardPage.vue
T
kjh2064 1be7029f8f
deploy / deploy (push) Failing after 51s
deploy / notify (push) Successful in 1s
refactor(fe): viewport-fit zero-scroll layout for 11 pages (Part 2)
## 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>
2026-08-16 15:00:03 +09:00

315 lines
11 KiB
Vue

<script setup lang="ts">
import { computed, ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import SearchListCrudPage from '../../../shared/ui/screen-types/v2/SearchListCrudPage.vue'
import DataGridShell from '../../../shared/ui/DataGridShell.vue'
import { screenTemplateCatalogue, type ScreenTemplateId } from '../../../shared/ui/screen-types/catalogue'
import {
KsButton,
KsSelect,
KsStatusTag,
KsTextField,
KsNumberField,
KsDateField,
KsCheckbox,
KsFormGrid,
KsFormSection,
KsFormSpan,
KsValidationSummary,
KArtsellMetricCard
} from '../../../shared/ui/components'
import type { UiGridColumn, UiSelectOption } from '../../../shared/ui/adapter/contracts'
import { useUiAdapter } from '../../../shared/ui/adapter/useUiAdapter'
import type { StandardScreenState } from '../../../shared/ui/contracts/screenContract'
const adapter = useUiAdapter()
const router = useRouter()
const screenState = ref<StandardScreenState>('READY')
const evidence = reactive({
asOf: new Date().toISOString(),
version: 'v60-STD-Contract',
})
const query = ref('')
const templateFilter = ref<unknown>('ALL')
type ScreenCatalogueRow = { screenId: string; name: string; templateId: ScreenTemplateId; templateName: string; path: string }
const selectedScreenId = ref<unknown>(null)
const demo = ref<{ name: string; amount: number | null; date: string | null; acknowledged: boolean }>({
name: '홍길동',
amount: 1500000,
date: new Date().toISOString().split('T')[0],
acknowledged: true
})
const demoReadonly = ref(false)
const submitted = ref(false)
const demoErrors = computed(() => submitted.value ? [
...(demo.value.name.trim() ? [] : [{ field: 'name', message: '이름을 입력하세요.' }]),
...(demo.value.amount !== null && demo.value.amount > 0 ? [] : [{ field: 'amount', message: '0보다 큰 금액을 입력하세요.' }]),
...(demo.value.acknowledged ? [] : [{ field: 'acknowledged', message: '확인 항목에 동의하세요.' }])
] : [])
function submitDemo(): void { submitted.value = true }
function resetDemo(): void { demo.value = { name: '', amount: null, date: null, acknowledged: false }; submitted.value = false }
const templateOptions: UiSelectOption[] = [
{ label: '전체 화면 유형', value: 'ALL' },
...screenTemplateCatalogue.map(template => ({ label: `${template.id} · ${template.name}`, value: template.id }))
]
const rows = computed<ScreenCatalogueRow[]>(() => router.getRoutes()
.map(route => {
const screenId = route.meta.screenId
const templateId = route.meta.templateId
const name = route.meta.title
if (route.path === '/internal/ui-standard' || route.meta.internalOnly === true || typeof screenId !== 'string' || typeof templateId !== 'string' || typeof name !== 'string') return null
const template = screenTemplateCatalogue.find(item => item.id === templateId)
return template ? { screenId, name, templateId: template.id, templateName: template.name, path: route.path } : null
})
.filter((row): row is ScreenCatalogueRow => row !== null)
.filter(row => templateFilter.value === 'ALL' || row.templateId === templateFilter.value)
.filter(row => !query.value || `${row.screenId} ${row.name} ${row.templateId} ${row.templateName}`.toLowerCase().includes(query.value.toLowerCase()))
)
const screenOptions = computed<UiSelectOption[]>(() => rows.value.map(row => ({ label: `${row.screenId} · ${row.name}`, value: row.screenId })))
const selectedScreen = computed(() => rows.value.find(row => row.screenId === selectedScreenId.value) ?? null)
const columns: UiGridColumn[] = [
{ field: 'screenId', header: '화면 ID', width: 130, flex: 1 },
{ field: 'name', header: '기능 화면명', minWidth: 180, flex: 2 },
{ field: 'templateId', header: '유형 ID', width: 100, flex: 1 },
{ field: 'templateName', header: '화면 유형 명칭', minWidth: 180, flex: 2 }
]
function openSelectedScreen(): void { if (selectedScreen.value) void router.push(selectedScreen.value.path) }
function handleRetry(): void { screenState.value = 'READY' }
</script>
<template>
<SearchListCrudPage
title="표준 UI 패턴 및 카탈로그 콘솔"
subtitle="KBX v16.0 헌법 표준 UI 공통 컴포넌트, 표준 화면 레시피, 디자인 시스템 토큰을 검증합니다."
:state="screenState"
:evidence="evidence"
@retry="handleRetry"
>
<template #actions>
<KsButton
label="🚀 선택 화면 열기"
variant="primary"
:disabled="!selectedScreen"
@click="openSelectedScreen"
/>
</template>
<template #summary>
<div class="metrics-row">
<KArtsellMetricCard
title="등록 화면 레시피"
:value="`${screenTemplateCatalogue.length} 개`"
status-text="표준 스펙"
status-type="up"
/>
<KArtsellMetricCard
title="연결된 기능 화면"
:value="`${rows.length} 개`"
status-text="정상 라우팅"
status-type="up"
/>
<KArtsellMetricCard
title="UI 어댑터 벤더"
:value="adapter.descriptor.vendor"
status-text="v4.0 샌드박스"
status-type="flat"
/>
</div>
</template>
<template #filters>
<KsTextField
v-model="query"
label="화면 검색"
placeholder="화면 ID, 기능명 또는 유형"
class="ks-set-md"
/>
<KsSelect
v-model="templateFilter"
label="화면 유형"
:options="templateOptions"
class="ks-set-md"
/>
<KsSelect
v-model="selectedScreenId"
label="이동할 화면"
placeholder="화면 ID를 선택하세요"
:options="screenOptions"
class="ks-set-md"
/>
</template>
<div class="ks-standard-content">
<KsFormSection title="1. 라우트 연결 화면 카탈로그">
<DataGridShell
:rows="rows"
:columns="columns"
empty-message="검색 조건에 해당하는 표준 화면 카탈로그가 없습니다."
height="220px"
/>
</KsFormSection>
<KsFormSection title="2. 공통 UI 컴포넌트 프리뷰">
<div class="preview-grid">
<div class="preview-card">
<h3>버튼 유형 (KsButton Variants)</h3>
<div class="btn-group">
<KsButton label="주요 처리 (Primary)" variant="primary" />
<KsButton label="보조 작업 (Secondary)" variant="secondary" />
<KsButton label="위험/삭제 (Danger)" variant="danger" />
</div>
</div>
<div class="preview-card">
<h3>상태 뱃지 (KsStatusTag Severities)</h3>
<div class="status-row">
<KsStatusTag value="정상 PASSED" severity="success" />
<KsStatusTag value="검토 REVIEW" severity="warning" />
<KsStatusTag value="격리 QUARANTINED" severity="danger" />
<KsStatusTag value="대기 QUEUED" severity="info" />
</div>
</div>
<div class="preview-card">
<h3> 컨트롤 (KsTextField & KsSelect)</h3>
<div class="input-stack">
<KsTextField label="텍스트 필드" model-value="K-ArtSell Aegis" />
<KsSelect label="선택 필드" model-value="READY" :options="[{ label: '준비 READY', value: 'READY' }, { label: '검토 REVIEW', value: 'REVIEW' }]" />
</div>
</div>
</div>
</KsFormSection>
<KsFormSection title="3. 폼 입력 및 검증 테스트">
<template #actions>
<KsButton
:label="demoReadonly ? '🔓 편집 허용' : '🔒 읽기 전용 전환'"
variant="secondary"
@click="demoReadonly = !demoReadonly"
/>
</template>
<KsValidationSummary v-if="demoErrors.length" :errors="demoErrors" />
<KsFormGrid :columns="2" aria-label="기능 테스트 입력">
<KsTextField v-model="demo.name" label="담당자 이름" :disabled="demoReadonly" :error="submitted && !demo.name.trim() ? '필수 입력입니다.' : undefined" />
<KsNumberField v-model="demo.amount" label="설정 금액 ($)" :disabled="demoReadonly" :error="submitted && (!demo.amount || demo.amount <= 0) ? '0보다 커야 합니다.' : undefined" />
<KsDateField v-model="demo.date" label="평가 기준일" type="range" :disabled="demoReadonly" />
<KsCheckbox v-model="demo.acknowledged" label="KBX 거버넌스 헌법 준수 동의" :disabled="demoReadonly" />
<KsFormSpan span="full">
<div class="ks-inline">
<KsButton label="⚡ 폼 검증 실행" variant="primary" :disabled="demoReadonly" @click="submitDemo" />
<KsButton label="🔄 초기화" variant="secondary" @click="resetDemo" />
</div>
</KsFormSpan>
</KsFormGrid>
</KsFormSection>
</div>
<template #detail>
<div class="ks-card detail-card">
<h2>선택한 기능 화면 상세</h2>
<template v-if="selectedScreen">
<p><strong>화면 ID:</strong> <code>{{ selectedScreen.screenId }}</code></p>
<p><strong>화면명:</strong> {{ selectedScreen.name }}</p>
<p><strong>유형:</strong> {{ selectedScreen.templateId }} · {{ selectedScreen.templateName }}</p>
<p><strong>경로:</strong> <code>{{ selectedScreen.path }}</code></p>
<KsButton label="🚀 화면 바로가기" variant="primary" @click="openSelectedScreen" />
</template>
<p v-else class="ks-muted">목록에서 화면 ID를 선택한 , 선택 화면 열기 누르세요.</p>
</div>
</template>
</SearchListCrudPage>
</template>
<style scoped>
.metrics-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--ks-space-3);
margin-bottom: var(--ks-space-2);
}
.ks-standard-content {
display: flex;
flex-direction: column;
gap: var(--ks-space-4);
flex: 1;
min-height: 0;
overflow-y: auto;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: var(--ks-space-3);
}
.preview-card {
padding: var(--ks-space-3);
background: var(--ks-color-canvas);
border: 1px solid var(--ks-color-border);
border-radius: var(--ks-radius-md);
display: flex;
flex-direction: column;
gap: var(--ks-space-2);
}
.preview-card h3 {
margin: 0;
font-size: var(--ks-font-caption);
font-weight: 700;
color: var(--ks-color-text-muted);
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: var(--ks-space-2);
}
.status-row {
display: flex;
flex-wrap: wrap;
gap: var(--ks-space-2);
align-items: center;
}
.input-stack {
display: flex;
flex-direction: column;
gap: var(--ks-space-2);
}
.detail-card {
padding: var(--ks-space-4);
display: flex;
flex-direction: column;
gap: var(--ks-space-2);
}
.detail-card h2 {
margin: 0;
font-size: var(--ks-font-section);
font-weight: 700;
}
.detail-card code {
font-family: var(--ks-font-mono, monospace);
background: var(--ks-color-canvas);
padding: 2px 6px;
border-radius: 3px;
border: 1px solid var(--ks-color-border);
}
</style>