refactor(fe): SellDecisionPage - EditFormPage pattern
- Migrate from PageLayout to EditFormPage (standard form pattern) - Add state management (PROCESSING/ERROR/READY) - Separate form input and result preview sections - Use #preview slot for policy evaluation results - Add evidence tracking (asOf, version) - Add dirty state (form has potential changes) - Improve result display: semantic HTML (dl/dt/dd) - Add CSS styles for result presentation Result: Consistent form pattern across all research tools Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import PageLayout from '../../../shared/ui/layouts/PageLayout.vue'
|
import EditFormPage from '../../../shared/ui/screen-types/v2/EditFormPage.vue'
|
||||||
import QueryStateBoundary from '../../../shared/ui/QueryStateBoundary.vue'
|
import QueryStateBoundary from '../../../shared/ui/QueryStateBoundary.vue'
|
||||||
import { KsButton, KsCheckbox, KsFormGrid, KsFormSection, KsNumberField } from '../../../shared/ui/components'
|
import { KsButton, KsCheckbox, KsFormGrid, KsFormSection, KsNumberField } from '../../../shared/ui/components'
|
||||||
import PolicyTracePanel from '../components/PolicyTracePanel.vue'
|
import PolicyTracePanel from '../components/PolicyTracePanel.vue'
|
||||||
import { useEvaluateResearchSellPolicy } from '../queries'
|
import { useEvaluateResearchSellPolicy } from '../queries'
|
||||||
import type { ResearchSellPolicyCommand } from '../api'
|
import type { ResearchSellPolicyCommand } from '../api'
|
||||||
|
import type { StandardScreenState } from '../../../shared/ui/contracts/screenContract'
|
||||||
|
|
||||||
const mutation = useEvaluateResearchSellPolicy()
|
const mutation = useEvaluateResearchSellPolicy()
|
||||||
const hardImpairmentApproved = ref(false)
|
const hardImpairmentApproved = ref(false)
|
||||||
@@ -14,7 +15,20 @@ const gapBelowFloorAtr = ref(1.6)
|
|||||||
const consecutiveCloseBreaches = ref(0)
|
const consecutiveCloseBreaches = ref(0)
|
||||||
const lastCommand = ref<ResearchSellPolicyCommand | null>(null)
|
const lastCommand = ref<ResearchSellPolicyCommand | null>(null)
|
||||||
|
|
||||||
|
const evidence = {
|
||||||
|
asOf: new Date().toISOString(),
|
||||||
|
version: 'v60-T13-ResearchSellPolicy',
|
||||||
|
}
|
||||||
|
|
||||||
const isBusy = computed(() => mutation.isPending.value)
|
const isBusy = computed(() => mutation.isPending.value)
|
||||||
|
const dirty = computed(() => true) // Form always has potential changes until submitted
|
||||||
|
|
||||||
|
const screenState = computed<StandardScreenState>(() => {
|
||||||
|
if (isBusy.value) return 'PROCESSING'
|
||||||
|
if (mutation.error.value) return 'ERROR'
|
||||||
|
if (!mutation.data.value) return 'READY'
|
||||||
|
return 'READY'
|
||||||
|
})
|
||||||
|
|
||||||
function createCommand(): ResearchSellPolicyCommand {
|
function createCommand(): ResearchSellPolicyCommand {
|
||||||
const asOf = new Date().toISOString()
|
const asOf = new Date().toISOString()
|
||||||
@@ -55,45 +69,102 @@ function run() {
|
|||||||
function retry() {
|
function retry() {
|
||||||
if (lastCommand.value) mutation.mutate(lastCommand.value)
|
if (lastCommand.value) mutation.mutate(lastCommand.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageLayout title="매도 정책 연구 콘솔" subtitle="순수 정책 계약과 우선순위를 확인하는 연구 전용 화면입니다. 고객 제안·공개·주문 기능과 연결되지 않습니다." status="RESEARCH_CANDIDATE_NOT_PRODUCTION · 자동주문 OFF">
|
<EditFormPage
|
||||||
<form @submit.prevent="run">
|
title="매도 정책 연구 콘솔"
|
||||||
<KsFormSection title="연구 입력 벡터">
|
subtitle="순수 정책 계약과 우선순위를 확인하는 연구 전용 화면입니다. 고객 제안·공개·주문 기능과 연결되지 않습니다."
|
||||||
<KsFormGrid :columns="2" aria-label="연구 입력 벡터">
|
:state="screenState"
|
||||||
<KsCheckbox v-model="hardImpairmentApproved" label="Hard impairment 승인" :disabled="isBusy" />
|
:dirty="dirty"
|
||||||
<KsCheckbox v-model="capitalFloorBreached" label="자본바닥 위반" :disabled="isBusy" />
|
:evidence="evidence"
|
||||||
<KsNumberField v-model="gapBelowFloorAtr" label="보호선 이탈 ATR" :min="0" :max-fraction-digits="2" :disabled="isBusy" />
|
@submit="run"
|
||||||
<KsNumberField v-model="consecutiveCloseBreaches" label="연속 종가 이탈" :min="0" :max-fraction-digits="0" :disabled="isBusy" />
|
@retry="retry"
|
||||||
</KsFormGrid>
|
>
|
||||||
<template #actions>
|
<!-- Form Input Section -->
|
||||||
<KsButton type="submit" label="정책 평가" :loading="isBusy" />
|
<KsFormSection title="연구 입력 벡터">
|
||||||
</template>
|
<KsFormGrid :columns="2" aria-label="연구 입력 벡터">
|
||||||
</KsFormSection>
|
<KsCheckbox v-model="hardImpairmentApproved" label="Hard impairment 승인" :disabled="isBusy" />
|
||||||
</form>
|
<KsCheckbox v-model="capitalFloorBreached" label="자본바닥 위반" :disabled="isBusy" />
|
||||||
|
<KsNumberField v-model="gapBelowFloorAtr" label="보호선 이탈 ATR" :min="0" :max-fraction-digits="2" :disabled="isBusy" />
|
||||||
|
<KsNumberField v-model="consecutiveCloseBreaches" label="연속 종가 이탈" :min="0" :max-fraction-digits="0" :disabled="isBusy" />
|
||||||
|
</KsFormGrid>
|
||||||
|
</KsFormSection>
|
||||||
|
|
||||||
<QueryStateBoundary
|
<!-- Result Preview Section -->
|
||||||
:loading="isBusy"
|
<template #preview>
|
||||||
:error="mutation.error.value as Error | null"
|
<QueryStateBoundary
|
||||||
:empty="!mutation.data.value"
|
:loading="isBusy"
|
||||||
@retry="retry"
|
:error="mutation.error.value as Error | null"
|
||||||
>
|
:empty="!mutation.data.value"
|
||||||
<dl v-if="mutation.data.value" class="ks-stack">
|
@retry="retry"
|
||||||
<dt>행동</dt><dd>{{ mutation.data.value.action }}</dd>
|
>
|
||||||
<dt>정책</dt><dd>{{ mutation.data.value.policyId }}</dd>
|
<div v-if="mutation.data.value" class="result-content">
|
||||||
<dt>사유</dt><dd>{{ mutation.data.value.reasonCode }}</dd>
|
<h3>정책 평가 결과</h3>
|
||||||
<dt>Lot 매도비율</dt><dd>{{ mutation.data.value.sellRatioOfLot }}</dd>
|
<dl class="ks-stack">
|
||||||
<dt>매도 후 종목 비중</dt><dd>{{ mutation.data.value.targetSecurityPortfolioWeightAfter }}</dd>
|
<div class="result-row"><dt>행동</dt><dd>{{ mutation.data.value.action }}</dd></div>
|
||||||
<dt>재진입 가능</dt><dd>{{ mutation.data.value.reentryEligible }}</dd>
|
<div class="result-row"><dt>정책</dt><dd>{{ mutation.data.value.policyId }}</dd></div>
|
||||||
<dt>결정 계약</dt><dd>{{ mutation.data.value.decisionContractVersion }}</dd>
|
<div class="result-row"><dt>사유</dt><dd>{{ mutation.data.value.reasonCode }}</dd></div>
|
||||||
<dt>정책 추적</dt><dd>{{ mutation.data.value.policyTrace.length }}단계</dd>
|
<div class="result-row"><dt>Lot 매도비율</dt><dd>{{ mutation.data.value.sellRatioOfLot }}</dd></div>
|
||||||
</dl>
|
<div class="result-row"><dt>매도 후 종목 비중</dt><dd>{{ mutation.data.value.targetSecurityPortfolioWeightAfter }}</dd></div>
|
||||||
<PolicyTracePanel
|
<div class="result-row"><dt>재진입 가능</dt><dd>{{ mutation.data.value.reentryEligible }}</dd></div>
|
||||||
v-if="mutation.data.value"
|
<div class="result-row"><dt>결정 계약</dt><dd>{{ mutation.data.value.decisionContractVersion }}</dd></div>
|
||||||
:entries="mutation.data.value.policyTrace"
|
<div class="result-row"><dt>정책 추적</dt><dd>{{ mutation.data.value.policyTrace.length }}단계</dd></div>
|
||||||
:schema-version="mutation.data.value.policyTraceSchemaVersion"
|
</dl>
|
||||||
/>
|
<PolicyTracePanel
|
||||||
</QueryStateBoundary>
|
:entries="mutation.data.value.policyTrace"
|
||||||
</PageLayout>
|
:schema-version="mutation.data.value.policyTraceSchemaVersion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</QueryStateBoundary>
|
||||||
|
</template>
|
||||||
|
</EditFormPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.result-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--spacing-2) 0;
|
||||||
|
border-bottom: 1px solid var(--color-border-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row dt {
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-row dd {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user