1be7029f8f
## 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>
315 lines
11 KiB
Vue
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>
|
|
|