refactor(fe): ApprovalQueue - MasterDetailCrudPage + UX improvements

- Migrate from ApprovalWorkbenchPage to MasterDetailCrudPage (more appropriate structure)
- Move Approve/Reject buttons to detail header (always visible)
- Compact Validation Metrics (reduced padding and font size)
- Optimize Review Comment textarea (80-120px height)
- Improve layout flow: fixed header + scrollable content in detail panel
- Title: Approval Queue (Maker-Checker Governance)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 01:06:49 +09:00
parent a5a952fd43
commit 53e91acc52
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { reactive, computed, ref, onMounted } from 'vue'
import ApprovalWorkbenchPage from '../../../shared/ui/screen-types/v2/ApprovalWorkbenchPage.vue'
import KsSplitter from '../../../shared/ui/components/KsSplitter.vue'
import MasterDetailCrudPage from '../../../shared/ui/screen-types/v2/MasterDetailCrudPage.vue'
import { KsButton, KsStatusTag } from '../../../shared/ui/components'
import type { StandardScreenState } from '../../../shared/ui/contracts/screenContract'
@@ -126,28 +125,33 @@ const handleRetry = () => {
</script>
<template>
<ApprovalWorkbenchPage
title="승인 워크벤치 (Maker-Checker Approval Workbench)"
<MasterDetailCrudPage
title="Approval Queue (Maker-Checker Governance)"
subtitle="모델 승격, 정책 변경 및 예외 조치 요청에 대한 Maker-Checker 거버넌스 승인을 수행합니다."
:state="screenState"
:evidence="evidence"
storageKey="ks_splitter_ratio_approval_queue"
@retry="handleRetry"
>
<template #summary>
<div class="stat stat-pending">
<span class="label">Pending</span>
<span class="value ks-financial-number">{{ statusStats.pending }}</span>
</div>
<div class="stat stat-approved">
<span class="label">Approved</span>
<span class="value ks-financial-number">{{ statusStats.approved }}</span>
</div>
<div class="stat stat-rejected">
<span class="label">Rejected</span>
<span class="value ks-financial-number">{{ statusStats.rejected }}</span>
<!-- Summary Stats -->
<template #actions>
<div class="stats">
<div class="stat stat-pending">
<span class="label">Pending</span>
<span class="value ks-financial-number">{{ statusStats.pending }}</span>
</div>
<div class="stat stat-approved">
<span class="label">Approved</span>
<span class="value ks-financial-number">{{ statusStats.approved }}</span>
</div>
<div class="stat stat-rejected">
<span class="label">Rejected</span>
<span class="value ks-financial-number">{{ statusStats.rejected }}</span>
</div>
</div>
</template>
<!-- Filters -->
<template #filters>
<div class="filters">
<select v-model="filterModel.status" class="input" aria-label="승인 상태 필터">
@@ -165,13 +169,11 @@ const handleRetry = () => {
</div>
</template>
<!-- Master-Detail Layout: KsSplitter -->
<KsSplitter storageKey="ks_splitter_ratio_approval_queue" initialRatio="25">
<template #left>
<!-- Request Master List -->
<aside class="request-list" aria-label="승인 요청 목록">
<h2>Requests ({{ filteredRequests.length }})</h2>
<div class="items" role="list">
<!-- Master List -->
<template #master>
<aside class="request-list" aria-label="승인 요청 목록">
<h2>Requests ({{ filteredRequests.length }})</h2>
<div class="items" role="list">
<div
v-for="req in filteredRequests"
:key="req.requestId"
@@ -196,14 +198,34 @@ const handleRetry = () => {
</div>
</div>
</aside>
</template>
</template>
<!-- Detail Workbench Panel -->
<template #right>
<main class="detail-panel" aria-label="선택 요청 상세 검토">
<h2 v-if="selectedRequest">{{ selectedRequest.modelName }}</h2>
<!-- Detail Panel -->
<template #detail>
<div class="detail-panel" aria-label="선택 요청 상세 검토">
<!-- Header with Actions (fixed at top) -->
<div v-if="selectedRequest" class="detail-header">
<h2>{{ selectedRequest.modelName }}</h2>
<div v-if="canApprove || canReject" class="header-actions">
<KsButton
label="Approve"
variant="primary"
:disabled="!canApprove"
@click="handleApprove"
size="sm"
/>
<KsButton
label="Reject"
variant="secondary"
:disabled="!canReject"
@click="handleReject"
size="sm"
/>
</div>
</div>
<div v-else class="empty-detail">Select a request from list</div>
<!-- Scrollable Content -->
<div v-if="selectedRequest" class="request-detail">
<!-- Request Info -->
<div class="section">
@@ -247,24 +269,10 @@ const handleRetry = () => {
</div>
</div>
<!-- Review Section (if pending) -->
<!-- Review Comment Section (if pending) -->
<div v-if="canApprove || canReject" class="section">
<h3>Review & Approval (Maker-Checker Audit)</h3>
<textarea v-model="reviewComment" placeholder="Enter review comment for audit trail..." class="textarea" aria-label="승인 심사 의견"></textarea>
<div class="actions">
<KsButton
label="Approve [승인]"
variant="primary"
:disabled="!canApprove"
@click="handleApprove"
/>
<KsButton
label="Reject [반려]"
variant="secondary"
:disabled="!canReject"
@click="handleReject"
/>
</div>
<h3>Review Comment (Maker-Checker Audit)</h3>
<textarea v-model="reviewComment" placeholder="Enter your review comment for audit trail..." class="textarea" aria-label="승인 심사 의견"></textarea>
</div>
<!-- Review History -->
@@ -287,10 +295,9 @@ const handleRetry = () => {
</div>
</div>
</div>
</main>
</template>
</KsSplitter>
</ApprovalWorkbenchPage>
</div>
</template>
</MasterDetailCrudPage>
</template>
<style scoped>
@@ -490,11 +497,32 @@ h3 {
.detail-panel {
border: var(--border-width-1) solid var(--color-border-primary);
border-radius: var(--border-radius-lg);
padding: var(--spacing-4);
background: var(--color-background-secondary);
height: 100%;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-3);
padding: var(--spacing-4);
border-bottom: var(--border-width-1) solid var(--color-border-secondary);
flex-shrink: 0;
}
.detail-header h2 {
flex: 1;
margin: 0;
}
.header-actions {
display: flex;
gap: var(--spacing-2);
flex-shrink: 0;
}
.empty-detail {
@@ -509,6 +537,8 @@ h3 {
gap: var(--spacing-4);
flex: 1;
min-height: 0;
overflow-y: auto;
padding: var(--spacing-4);
}
.section {
@@ -552,7 +582,7 @@ h3 {
}
.metric-card {
padding: var(--spacing-3);
padding: var(--spacing-2) var(--spacing-2);
background: var(--color-background-primary);
border: var(--border-width-1) solid var(--color-border-secondary);
border-radius: var(--border-radius-base);
@@ -560,14 +590,14 @@ h3 {
}
.metric-label {
font-size: var(--font-size-xs);
font-size: 11px;
color: var(--color-text-tertiary);
font-weight: var(--font-weight-medium);
margin-bottom: var(--spacing-1);
margin-bottom: 4px;
}
.metric-value {
font-size: var(--font-size-lg);
font-size: var(--font-size-base);
font-weight: var(--font-weight-bold);
color: var(--color-primary-600);
}
@@ -582,7 +612,8 @@ h3 {
font-size: var(--font-size-sm);
font-family: var(--font-sans);
resize: vertical;
min-height: 100px;
min-height: 80px;
max-height: 120px;
transition: all var(--transition-fast);
}