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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user