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>
This commit is contained in:
@@ -1,29 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { SearchListCrudPage } from '@/shared/ui/screen-types'
|
||||
import { screenTemplateCatalogue, type ScreenTemplateId } from '@/shared/ui/screen-types/catalogue'
|
||||
import { KsButton, KsDataGrid, KsSelect, KsStatusTag, KsTextField, KsNumberField, KsDateField, KsCheckbox, KsFormGrid, KsFormSection, KsFormSpan, KsValidationSummary } from '@/shared/ui/components'
|
||||
import type { UiGridColumn, UiSelectOption } from '@/shared/ui/adapter/contracts'
|
||||
import { useUiAdapter } from '@/shared/ui/adapter/useUiAdapter'
|
||||
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: '', amount: null as number | null, date: null as Date | null, acknowledged: false })
|
||||
|
||||
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 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
|
||||
@@ -37,40 +73,242 @@ const rows = computed<ScreenCatalogueRow[]>(() => router.getRoutes()
|
||||
.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 }, { field: 'name', header: '기능 화면', minWidth: 180 }, { field: 'templateId', header: '유형', width: 90 }, { field: 'templateName', header: '화면 유형', minWidth: 180 }]
|
||||
|
||||
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="화면 유형을 선택하고, 실제 화면 ID를 선택해 기능 화면을 엽니다." state="READY" :evidence="{ asOf: '2026-08-02', version: 'UI-CONTRACT-4.0' }">
|
||||
<template #actions><KsButton label="선택 화면 열기" :disabled="!selectedScreen" @click="openSelectedScreen" /></template>
|
||||
<template #summary><div class="ks-card summary"><strong>{{ screenTemplateCatalogue.length }}</strong><span>화면 타입</span></div><div class="ks-card summary"><strong>{{ rows.length }}</strong><span>연결된 기능 화면</span></div><div class="ks-card summary"><KsStatusTag :value="adapter.descriptor.id" severity="info" /><span>{{ adapter.descriptor.vendor }}</span></div><div class="ks-card summary"><KsStatusTag value="자동주문 OFF" severity="warning" /><span>고정 경계</span></div></template>
|
||||
<template #filters><div class="filters"><KsTextField v-model="query" label="화면 검색" placeholder="화면 ID, 기능명 또는 화면 유형" /><KsSelect v-model="templateFilter" label="화면 유형" :options="templateOptions" /><KsSelect v-model="selectedScreenId" label="열 화면" placeholder="화면 ID를 선택하세요" :options="screenOptions" /></div></template>
|
||||
<KsDataGrid :rows="rows" :columns="columns" height="25rem" row-selection="none" />
|
||||
<section class="ks-card component-preview" aria-labelledby="component-preview-title">
|
||||
<div><h2 id="component-preview-title">컴포넌트 동작 프리뷰</h2><p>공유 UI 포트를 실제 상태로 확인하는 내부 전용 카탈로그입니다.</p></div>
|
||||
<div class="preview-grid">
|
||||
<div><h3>Actions</h3><KsButton label="기본 버튼" severity="secondary" /><KsButton label="주의 상태" severity="warning" /></div>
|
||||
<div><h3>Status</h3><div class="status-row"><KsStatusTag value="READY" severity="success" /><KsStatusTag value="REVIEW" severity="warning" /><KsStatusTag value="BLOCKED" severity="danger" /></div></div>
|
||||
<div><h3>Inputs</h3><KsTextField label="텍스트 필드" model-value="샘플 값" /><KsSelect label="선택 필드" model-value="READY" :options="[{ label: '준비', value: 'READY' }, { label: '검토', value: 'REVIEW' }]" /></div>
|
||||
<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>
|
||||
</section>
|
||||
<section class="ks-card component-preview" aria-labelledby="form-template-title">
|
||||
<div><h2 id="form-template-title">입력 유형 기능 테스트</h2><p>서버 요청 없이 shared UI의 입력·검증·읽기전용 조합을 확인합니다.</p></div>
|
||||
<KsValidationSummary :errors="demoErrors" />
|
||||
<KsFormSection title="기본 입력" description="필수 항목과 유효성 상태를 조작할 수 있습니다.">
|
||||
<template #actions><KsButton :label="demoReadonly ? '편집 허용' : '읽기 전용'" severity="secondary" @click="demoReadonly = !demoReadonly" /></template>
|
||||
<KsFormGrid 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="기준일" :disabled="demoReadonly" />
|
||||
<KsCheckbox v-model="demo.acknowledged" label="검증 조건을 확인했습니다" :disabled="demoReadonly" />
|
||||
<KsFormSpan span="full"><div class="ks-inline"><KsButton label="검증 실행" :disabled="demoReadonly" @click="submitDemo" /><KsButton label="초기화" severity="secondary" @click="resetDemo" /></div></KsFormSpan>
|
||||
</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>
|
||||
</section>
|
||||
<template #detail><div class="ks-card detail"><h2>선택한 기능 화면</h2><template v-if="selectedScreen"><p><strong>{{ selectedScreen.screenId }}</strong> · {{ selectedScreen.name }}</p><p>{{ selectedScreen.templateId }} · {{ selectedScreen.templateName }}</p><p><code>{{ selectedScreen.path }}</code></p><KsButton label="선택 화면 열기" @click="openSelectedScreen" /></template><p v-else>목록에서 화면 ID를 선택한 뒤, “선택 화면 열기”를 누르세요.</p></div></template>
|
||||
</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>.filters{display:grid;grid-template-columns:2fr 1fr 1fr;gap:var(--ks-space-3)}.summary,.detail,.component-preview{padding:var(--ks-space-4)}.summary{display:grid;gap:var(--ks-space-1)}.summary strong{font-size:1.5rem}.detail h2,.component-preview h2{margin-top:0;font-size:var(--ks-font-section)}.preview-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--ks-space-4)}.preview-grid>div{display:grid;align-content:start;gap:var(--ks-space-2);padding:var(--ks-space-3);border:1px solid var(--ks-color-neutral-200);border-radius:var(--ks-radius-sm)}.preview-grid h3{margin:0;font-size:var(--ks-font-caption)}.status-row{display:flex;flex-wrap:wrap;gap:var(--ks-space-2)}@media(max-width:700px){.filters,.preview-grid{grid-template-columns:1fr}}</style>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user