refactor(fe): viewport-fit zero-scroll layout for 11 pages (Part 2)
deploy / deploy (push) Failing after 51s
deploy / notify (push) Successful in 1s

## 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:
2026-08-16 15:00:03 +09:00
parent 07ad98ec12
commit 1be7029f8f
17 changed files with 1727 additions and 674 deletions
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import EditFormPage from '../../../shared/ui/screen-types/v2/EditFormPage.vue'
import QueryStateBoundary from '../../../shared/ui/QueryStateBoundary.vue'
import { KsButton, KsCheckbox, KsFormGrid, KsFormSection, KsNumberField } from '../../../shared/ui/components'
import { KsButton, KsCheckbox, KsFormGrid, KsFormSection, KsNumberField, KsStatusTag } from '../../../shared/ui/components'
import PolicyTracePanel from '../components/PolicyTracePanel.vue'
import { useEvaluateResearchSellPolicy } from '../queries'
import type { ResearchSellPolicyCommand } from '../api'
@@ -26,7 +26,6 @@ const dirty = computed(() => true) // Form always has potential changes until su
const screenState = computed<StandardScreenState>(() => {
if (isBusy.value) return 'PROCESSING'
if (mutation.error.value) return 'ERROR'
if (!mutation.data.value) return 'READY'
return 'READY'
})
@@ -68,8 +67,25 @@ function run() {
function retry() {
if (lastCommand.value) mutation.mutate(lastCommand.value)
else run()
}
function getActionSeverity(action: string): 'success' | 'danger' | 'warning' | 'info' {
if (action === 'SELL' || action === 'FORCE_SELL') return 'danger'
if (action === 'HOLD' || action === 'PASS') return 'success'
if (action === 'REBALANCE') return 'warning'
return 'info'
}
function formatPercent(val: number | undefined): string {
if (val === undefined || val === null) return '0.0%'
return (val * 100).toFixed(1) + '%'
}
onMounted(() => {
run()
})
</script>
<template>
@@ -82,6 +98,11 @@ function retry() {
@submit="run"
@retry="retry"
>
<!-- Page Top Action Bar -->
<template #actions>
<KsButton label="⚡ 매도 정책 평가 실행" variant="primary" :loading="isBusy" @click="run" />
</template>
<!-- Form Input Section -->
<KsFormSection title="연구 입력 벡터">
<KsFormGrid :columns="2" aria-label="연구 입력 벡터">
@@ -98,19 +119,26 @@ function retry() {
:loading="isBusy"
:error="mutation.error.value as Error | null"
:empty="!mutation.data.value"
empty-title="매도 정책 평가 대기 "
empty-message="좌측 연구 입력 벡터 파라미터를 조정한 [ 매도 정책 평가 실행] 버튼을 클릭하세요."
empty-icon="📊"
empty-action-label=" 정책 평가 실행"
@retry="retry"
>
<div v-if="mutation.data.value" class="result-content">
<h3>정책 평가 결과</h3>
<div class="result-header">
<h3>정책 평가 결과</h3>
<KsStatusTag :value="mutation.data.value.action" :severity="getActionSeverity(mutation.data.value.action)" />
</div>
<dl class="ks-stack">
<div class="result-row"><dt>행동</dt><dd>{{ mutation.data.value.action }}</dd></div>
<div class="result-row"><dt>정책</dt><dd>{{ mutation.data.value.policyId }}</dd></div>
<div class="result-row"><dt>사유</dt><dd>{{ mutation.data.value.reasonCode }}</dd></div>
<div class="result-row"><dt>Lot 매도비율</dt><dd>{{ mutation.data.value.sellRatioOfLot }}</dd></div>
<div class="result-row"><dt>매도 종목 비중</dt><dd>{{ mutation.data.value.targetSecurityPortfolioWeightAfter }}</dd></div>
<div class="result-row"><dt>재진입 가능</dt><dd>{{ mutation.data.value.reentryEligible }}</dd></div>
<div class="result-row"><dt>결정 계약</dt><dd>{{ mutation.data.value.decisionContractVersion }}</dd></div>
<div class="result-row"><dt>정책 추적</dt><dd>{{ mutation.data.value.policyTrace.length }}단계</dd></div>
<div class="result-row"><dt>권고 행동</dt><dd><KsStatusTag :value="mutation.data.value.action" :severity="getActionSeverity(mutation.data.value.action)" /></dd></div>
<div class="result-row"><dt>적용 정책 ID</dt><dd><code>{{ mutation.data.value.policyId }}</code></dd></div>
<div class="result-row"><dt>판단 사유 코드</dt><dd><code>{{ mutation.data.value.reasonCode }}</code></dd></div>
<div class="result-row"><dt>Lot 매도 비율</dt><dd class="ks-financial-number">{{ formatPercent(mutation.data.value.sellRatioOfLot) }}</dd></div>
<div class="result-row"><dt>매도 종목 비중</dt><dd class="ks-financial-number">{{ formatPercent(mutation.data.value.targetSecurityPortfolioWeightAfter) }}</dd></div>
<div class="result-row"><dt>재진입 가능 여부</dt><dd>{{ mutation.data.value.reentryEligible ? '✅ 가능' : '❌ 불가능' }}</dd></div>
<div class="result-row"><dt>결정 계약 버전</dt><dd><code>{{ mutation.data.value.decisionContractVersion }}</code></dd></div>
<div class="result-row"><dt>정책 추적 단계</dt><dd>{{ mutation.data.value.policyTrace.length }}단계 추적 완료</dd></div>
</dl>
<PolicyTracePanel
:entries="mutation.data.value.policyTrace"
@@ -126,20 +154,28 @@ function retry() {
.result-content {
display: flex;
flex-direction: column;
gap: var(--spacing-3);
gap: var(--ks-space-3);
}
.result-content h3 {
.result-header {
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--ks-color-border-strong);
padding-bottom: 8px;
}
.result-header h3 {
margin: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
font-size: var(--ks-font-section);
font-weight: 700;
color: var(--ks-color-text);
}
.ks-stack {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
gap: 4px;
margin: 0;
padding: 0;
}
@@ -147,8 +183,10 @@ function retry() {
.result-row {
display: flex;
justify-content: space-between;
padding: var(--spacing-2) 0;
border-bottom: 1px solid var(--color-border-secondary);
align-items: center;
padding: 6px 0;
border-bottom: 1px dashed var(--ks-color-border);
font-size: var(--ks-font-body);
}
.result-row:last-child {
@@ -156,15 +194,26 @@ function retry() {
}
.result-row dt {
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
font-weight: 600;
color: var(--ks-color-text-muted);
flex-shrink: 0;
}
.result-row dd {
margin: 0;
text-align: right;
color: var(--color-text-secondary);
font-family: monospace;
color: var(--ks-color-text);
font-weight: 600;
}
.result-row 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);
font-size: 11px;
}
</style>