From 1be7029f8f232c38175df40d84ec04f00f8f95e7 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Sun, 16 Aug 2026 15:00:03 +0900 Subject: [PATCH] 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 --- .../features/approval/pages/ApprovalQueue.vue | 73 +-- .../data-quality/pages/DataQualityPage.vue | 45 +- frontend/src/features/home/pages/HomePage.vue | 523 +++++++----------- .../marketData/pages/IngestionStatus.vue | 48 +- .../marketData/pages/MarketDataIngestion.vue | 213 ++++--- .../pages/ModelOperationsPage.vue | 1 + .../src/features/models/pages/ModelsList.vue | 6 + .../portfolio/pages/RebalanceForm.vue | 461 +++++++++++++-- .../portfolio/pages/RiskDashboard.vue | 115 ++-- .../sell-decision/pages/SellDecisionPage.vue | 97 +++- .../shadow-run/pages/ShadowRunList.vue | 2 +- .../system/pages/CommonCodeManagementPage.vue | 494 +++++++++++++++++ .../ui-standard/pages/UiStandardPage.vue | 308 +++++++++-- .../features/wbs/pages/WbsWorkspacePage.vue | 2 +- .../screen-types/v2/BatchOperationsPageV2.vue | 3 +- .../ui/screen-types/v2/SearchListCrudPage.vue | 7 +- .../ui/screen-types/v2/StepWizardPage.vue | 3 +- 17 files changed, 1727 insertions(+), 674 deletions(-) create mode 100644 frontend/src/features/system/pages/CommonCodeManagementPage.vue diff --git a/frontend/src/features/approval/pages/ApprovalQueue.vue b/frontend/src/features/approval/pages/ApprovalQueue.vue index e0f2b0f5..96fe3310 100644 --- a/frontend/src/features/approval/pages/ApprovalQueue.vue +++ b/frontend/src/features/approval/pages/ApprovalQueue.vue @@ -305,82 +305,11 @@ const handleRetry = () => {

Select a request from list to review and approve

- - - - + + diff --git a/frontend/src/features/model-operations/pages/ModelOperationsPage.vue b/frontend/src/features/model-operations/pages/ModelOperationsPage.vue index 22df81c7..8539e369 100644 --- a/frontend/src/features/model-operations/pages/ModelOperationsPage.vue +++ b/frontend/src/features/model-operations/pages/ModelOperationsPage.vue @@ -46,5 +46,6 @@ const handleRetry = () => { flex: 1; height: 100%; min-height: 0; + overflow-y: auto; } diff --git a/frontend/src/features/models/pages/ModelsList.vue b/frontend/src/features/models/pages/ModelsList.vue index 224e7bad..c043f34d 100644 --- a/frontend/src/features/models/pages/ModelsList.vue +++ b/frontend/src/features/models/pages/ModelsList.vue @@ -210,6 +210,9 @@ function handleSearch() { display: flex; align-items: center; justify-content: center; + flex: 1; + min-height: 0; + overflow-y: auto; } .grid-container { @@ -220,5 +223,8 @@ function handleSearch() { min-height: 300px; display: flex; flex-direction: column; + flex: 1; + min-height: 0; + overflow-y: auto; } diff --git a/frontend/src/features/portfolio/pages/RebalanceForm.vue b/frontend/src/features/portfolio/pages/RebalanceForm.vue index 11bdf394..fe9f4b76 100644 --- a/frontend/src/features/portfolio/pages/RebalanceForm.vue +++ b/frontend/src/features/portfolio/pages/RebalanceForm.vue @@ -2,20 +2,23 @@ import { computed, ref, reactive } from 'vue' import StepWizardPage from '../../../shared/ui/screen-types/v2/StepWizardPage.vue' import DataGridShell from '../../../shared/ui/DataGridShell.vue' -import { KsButton, KsFormGrid, KsFormSection, KsNumberField, KsStatusTag, KsTextField } from '../../../shared/ui/components' +import { KsButton, KsCheckbox, KsFormGrid, KsFormSection, KsNumberField, KsStatusTag, KsTextField } from '../../../shared/ui/components' import type { UiGridColumn } from '../../../shared/ui/adapter/contracts' import { formatCurrency, formatPercent, formatQuantity } from '../../../shared/formatters/financial' import type { StandardScreenState } from '../../../shared/ui/contracts/screenContract' // KBX T06 Governance Audit & Screen State const screenState = ref('READY') +const currentStep = ref(1) + const evidence = reactive({ - asOf: '2026-08-15T19:44:00Z', + asOf: new Date().toISOString(), version: 'v60-T06-Contract', }) interface Position { symbol: string + name: string quantity: number marketPrice: number marketValue: number @@ -27,39 +30,125 @@ interface TargetWeight { targetPercent: number } +interface SimulationItem { + symbol: string + currentQuantity: number + marketPrice: number + currentValue: number + currentWeight: number + targetWeight: number + targetValue: number + deltaValue: number + tradeAction: 'BUY' | 'SELL' | 'HOLD' + estimatedShares: number +} + interface JobResult { jobId: string status: string + submittedAt: string estimatedTradeCount: number estimatedCost: number + totalRebalanceValue: number } const currentPositions = ref([ - { symbol: 'AAPL', quantity: 100, marketPrice: 150.25, marketValue: 15025, weightPercent: 35.3 }, - { symbol: 'MSFT', quantity: 80, marketPrice: 320.50, marketValue: 25640, weightPercent: 60.2 }, - { symbol: 'GOOGL', quantity: 50, marketPrice: 140.75, marketValue: 7037.5, weightPercent: 16.5 }, + { symbol: 'AAPL', name: '애플', quantity: 100, marketPrice: 185.20, marketValue: 18520, weightPercent: 35.3 }, + { symbol: 'NVDA', name: '엔비디아', quantity: 30, marketPrice: 875.50, marketValue: 26265, weightPercent: 50.1 }, + { symbol: 'MSFT', name: '마이크로소프트', quantity: 18, marketPrice: 415.00, marketValue: 7470, weightPercent: 14.6 }, ]) +const driftThreshold = ref(3) +const targetWeights = ref([ + { symbol: 'AAPL', targetPercent: 30 }, + { symbol: 'NVDA', targetPercent: 40 }, + { symbol: 'MSFT', targetPercent: 30 }, +]) + +const jobResult = ref(null) + const positionColumns: UiGridColumn[] = [ - { field: 'symbol', header: '종목코드' }, - { field: 'quantity', header: '수량', formatter: value => formatQuantity(value as number, 0) }, + { field: 'symbol', header: '종목코드', width: 90 }, + { field: 'quantity', header: '현재수량', formatter: value => formatQuantity(value as number, 0) }, { field: 'marketPrice', header: '현재가', formatter: value => formatCurrency(value as number, 'USD') }, { field: 'marketValue', header: '평가금액', formatter: value => formatCurrency(value as number, 'USD') }, - { field: 'weightPercent', header: '비중', formatter: value => formatPercent((value as number) / 100, 1) } + { field: 'weightPercent', header: '현재비중', formatter: value => formatPercent((value as number) / 100, 1) } ] -const driftThreshold = ref(5) -const targetWeights = ref([ - { symbol: 'AAPL', targetPercent: 40 }, - { symbol: 'MSFT', targetPercent: 35 }, - { symbol: 'GOOGL', targetPercent: 25 }, -]) -const jobResult = ref(null) +const simulationColumns: UiGridColumn[] = [ + { field: 'symbol', header: '종목코드', width: 90 }, + { field: 'currentWeight', header: '현재비중', formatter: value => formatPercent((value as number) / 100, 1) }, + { field: 'targetWeight', header: '목표비중', formatter: value => formatPercent((value as number) / 100, 1) }, + { field: 'tradeAction', header: '매매구분', width: 90 }, + { field: 'estimatedShares', header: '예상주문주수', formatter: value => formatQuantity(value as number, 0) }, + { field: 'deltaValue', header: '예상조정금액', formatter: value => formatCurrency(value as number, 'USD') } +] const totalValue = computed(() => { return currentPositions.value.reduce((sum, p) => sum + p.marketValue, 0) }) +const totalTargetWeight = computed(() => { + return targetWeights.value.reduce((sum, t) => sum + (t.targetPercent || 0), 0) +}) + +const isWeightValid = computed(() => { + return Math.abs(totalTargetWeight.value - 100) < 0.01 +}) + +const weightStatusSeverity = computed(() => { + if (isWeightValid.value) return 'success' + if (totalTargetWeight.value > 100) return 'danger' + return 'warning' +}) + +const weightStatusText = computed(() => { + if (isWeightValid.value) return '✅ 100.0% 정상' + if (totalTargetWeight.value > 100) return `⚠️ ${totalTargetWeight.value.toFixed(1)}% (100% 초과)` + return `⚠️ ${totalTargetWeight.value.toFixed(1)}% (100% 미달)` +}) + +const simulationItems = computed(() => { + const total = totalValue.value + return targetWeights.value.map(target => { + const pos = currentPositions.value.find(p => p.symbol === target.symbol) + const currentVal = pos ? pos.marketValue : 0 + const currentQty = pos ? pos.quantity : 0 + const price = pos ? pos.marketPrice : 150 + const currentW = total > 0 ? (currentVal / total) * 100 : 0 + const targetW = target.targetPercent || 0 + const targetVal = total * (targetW / 100) + const deltaV = targetVal - currentVal + + let tradeAction: 'BUY' | 'SELL' | 'HOLD' = 'HOLD' + if (deltaV > 50) tradeAction = 'BUY' + else if (deltaV < -50) tradeAction = 'SELL' + + const estimatedShares = price > 0 ? Math.round(Math.abs(deltaV) / price) : 0 + + return { + symbol: target.symbol, + currentQuantity: currentQty, + marketPrice: price, + currentValue: currentVal, + currentWeight: currentW, + targetWeight: targetW, + targetValue: targetVal, + deltaValue: deltaV, + tradeAction, + estimatedShares + } + }) +}) + +const totalEstimatedTrades = computed(() => { + return simulationItems.value.filter(item => item.tradeAction !== 'HOLD').length +}) + +const totalRebalanceValue = computed(() => { + return simulationItems.value.reduce((sum, item) => sum + Math.abs(item.deltaValue), 0) +}) + const addTarget = () => { targetWeights.value.push({ symbol: '', targetPercent: 0 }) } @@ -68,13 +157,30 @@ const removeTarget = (idx: number) => { targetWeights.value.splice(idx, 1) } +const nextStep = () => { + if (currentStep.value === 1 && !isWeightValid.value) return + if (currentStep.value < 3) currentStep.value++ +} + +const prevStep = () => { + if (currentStep.value > 1) currentStep.value-- +} + const triggerRebalance = () => { jobResult.value = { jobId: `REBAL-${Date.now()}`, status: 'Queued', - estimatedTradeCount: 3, - estimatedCost: 127.35, + submittedAt: new Date().toLocaleTimeString('ko-KR'), + estimatedTradeCount: totalEstimatedTrades.value, + estimatedCost: totalRebalanceValue.value * 0.0015, + totalRebalanceValue: totalRebalanceValue.value } + currentStep.value = 3 +} + +const resetWizard = () => { + jobResult.value = null + currentStep.value = 1 } const handleRetry = () => { @@ -84,57 +190,300 @@ const handleRetry = () => { + + + diff --git a/frontend/src/features/portfolio/pages/RiskDashboard.vue b/frontend/src/features/portfolio/pages/RiskDashboard.vue index fad02011..44a9a0ad 100644 --- a/frontend/src/features/portfolio/pages/RiskDashboard.vue +++ b/frontend/src/features/portfolio/pages/RiskDashboard.vue @@ -15,38 +15,50 @@ - + @@ -83,9 +95,9 @@ - + - + diff --git a/frontend/src/features/system/pages/CommonCodeManagementPage.vue b/frontend/src/features/system/pages/CommonCodeManagementPage.vue new file mode 100644 index 00000000..adcc9058 --- /dev/null +++ b/frontend/src/features/system/pages/CommonCodeManagementPage.vue @@ -0,0 +1,494 @@ + + + + + diff --git a/frontend/src/features/ui-standard/pages/UiStandardPage.vue b/frontend/src/features/ui-standard/pages/UiStandardPage.vue index 7ce925fe..3fc1ea82 100644 --- a/frontend/src/features/ui-standard/pages/UiStandardPage.vue +++ b/frontend/src/features/ui-standard/pages/UiStandardPage.vue @@ -1,29 +1,65 @@ + - + + + diff --git a/frontend/src/features/wbs/pages/WbsWorkspacePage.vue b/frontend/src/features/wbs/pages/WbsWorkspacePage.vue index 4eadb3a6..8c6926c3 100644 --- a/frontend/src/features/wbs/pages/WbsWorkspacePage.vue +++ b/frontend/src/features/wbs/pages/WbsWorkspacePage.vue @@ -21,4 +21,4 @@ const items: WbsItem[] = [ - + diff --git a/frontend/src/shared/ui/screen-types/v2/BatchOperationsPageV2.vue b/frontend/src/shared/ui/screen-types/v2/BatchOperationsPageV2.vue index 4bdb3c01..0ddcc3b3 100644 --- a/frontend/src/shared/ui/screen-types/v2/BatchOperationsPageV2.vue +++ b/frontend/src/shared/ui/screen-types/v2/BatchOperationsPageV2.vue @@ -1,3 +1,4 @@ - + + diff --git a/frontend/src/shared/ui/screen-types/v2/SearchListCrudPage.vue b/frontend/src/shared/ui/screen-types/v2/SearchListCrudPage.vue index 48b71bca..a9518698 100644 --- a/frontend/src/shared/ui/screen-types/v2/SearchListCrudPage.vue +++ b/frontend/src/shared/ui/screen-types/v2/SearchListCrudPage.vue @@ -11,10 +11,13 @@ defineEmits<{ retry: [] }>()