refactor(fe): ApprovalQueue - EditFormPage pattern

- Restructure detail panel as approval form (EditFormPage pattern)
- Separate concerns: Header (read-only) + Content (form) + Footer (actions)
- Use form/fieldset semantics for read-only vs editable sections
- Request Details and Metrics as disabled fieldsets (read-only)
- Review Comment as textarea form input
- Footer buttons (Approve/Reject) with validation
- CSS refactor: approval-form, form-header/content/footer structure
- Improved accessibility and semantic HTML

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 01:21:39 +09:00
parent 4d1bdb467e
commit 8ae6bf6429
@@ -200,33 +200,114 @@ const handleRetry = () => {
</aside>
</template>
<!-- Detail Panel -->
<!-- Detail Panel (EditForm Pattern) -->
<template #detail>
<div class="detail-panel" aria-label="선택 요청 상세 검토">
<!-- Header with Actions (fixed at top) -->
<div v-if="selectedRequest" class="detail-header">
<form v-if="selectedRequest" class="approval-form" @submit.prevent="handleApprove">
<!-- Header: Read-only Title -->
<div class="form-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>
<small class="form-meta">{{ getActionLabel(selectedRequest.action) }}</small>
</div>
<div v-else class="empty-detail">Select a request from list</div>
<!-- Scrollable Content -->
<div v-if="selectedRequest" class="request-detail">
<!-- Content: Read-only Info + Form Input -->
<div class="form-content">
<!-- Request Details (read-only) -->
<fieldset class="form-section" disabled>
<legend>Request Details</legend>
<div class="detail-grid">
<div class="detail-item">
<span class="label">Request ID</span>
<span class="value ks-financial-number">{{ selectedRequest.requestId }}</span>
</div>
<div class="detail-item">
<span class="label">Action</span>
<span class="value">{{ getActionLabel(selectedRequest.action) }}</span>
</div>
<div class="detail-item">
<span class="label">Requested By</span>
<span class="value">{{ selectedRequest.requesterName }}</span>
</div>
<div class="detail-item">
<span class="label">Requested At</span>
<span class="value ks-financial-number">{{ formatDate(selectedRequest.requestedAt) }}</span>
</div>
</div>
</fieldset>
<!-- Validation Metrics (read-only) -->
<fieldset v-if="selectedRequest.metadata.pbo" class="form-section" disabled>
<legend>Validation Metrics</legend>
<div class="metric-grid">
<div class="metric-card">
<div class="metric-label">PBO</div>
<div class="metric-value ks-financial-number">{{ selectedRequest.metadata.pbo?.toFixed(2) }}%</div>
</div>
<div class="metric-card">
<div class="metric-label">DSR</div>
<div class="metric-value ks-financial-number">{{ selectedRequest.metadata.dsr?.toFixed(2) }}%</div>
</div>
<div class="metric-card">
<div class="metric-label">OOS</div>
<div class="metric-value ks-financial-number">{{ selectedRequest.metadata.oos?.toFixed(2) }}%</div>
</div>
</div>
</fieldset>
<!-- Review Comment (form input) -->
<fieldset v-if="canApprove || canReject" class="form-section">
<legend>Review & Approval Comment</legend>
<textarea
v-model="reviewComment"
placeholder="Enter your review comment for audit trail..."
class="form-textarea"
aria-label="승인 심사 의견"
required
></textarea>
</fieldset>
<!-- Review History (read-only) -->
<fieldset v-if="selectedRequest.reviewedAt" class="form-section" disabled>
<legend>Review History</legend>
<div class="history-item">
<div class="history-header">
<strong>{{ selectedRequest.reviewerName }}</strong>
<KsStatusTag
:value="selectedRequest.status.toUpperCase()"
:severity="selectedRequest.status === 'approved' ? 'success' : 'danger'"
/>
</div>
<div class="history-date ks-financial-number">{{ formatDate(selectedRequest.reviewedAt) }}</div>
<div v-if="selectedRequest.reviewComment" class="history-comment">
{{ selectedRequest.reviewComment }}
</div>
</div>
</fieldset>
</div>
<!-- Footer: Action Buttons -->
<div v-if="canApprove || canReject" class="form-footer">
<KsButton
label="Approve [승인]"
variant="primary"
:disabled="!canApprove || !reviewComment.trim()"
type="submit"
/>
<KsButton
label="Reject [반려]"
variant="secondary"
:disabled="!canReject || !reviewComment.trim()"
@click.prevent="handleReject"
/>
</div>
</form>
<!-- Empty State -->
<div v-else class="empty-state">
<p>Select a request from list to review and approve</p>
</div>
<!-- Request Details (old content - DELETE) -->
<div v-if="selectedRequest" class="request-detail" style="display: none;">
<!-- Request Info -->
<div class="section">
<h3>Request Details</h3>
@@ -494,7 +575,8 @@ h3 {
color: var(--color-text-tertiary);
}
.detail-panel {
/* Approval Form (EditForm Pattern) */
.approval-form {
border: var(--border-width-1) solid var(--color-border-primary);
border-radius: var(--border-radius-lg);
background: var(--color-background-secondary);
@@ -502,43 +584,97 @@ h3 {
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-2);
.form-header {
padding: var(--spacing-3);
border-bottom: var(--border-width-1) solid var(--color-border-secondary);
flex-shrink: 0;
}
.detail-header h2 {
flex: 1;
margin: 0;
.form-header h2 {
margin: 0 0 4px 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.header-actions {
display: flex;
gap: var(--spacing-2);
flex-shrink: 0;
}
.empty-detail {
text-align: center;
padding: var(--spacing-8);
.form-meta {
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
}
.request-detail {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
.form-content {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: var(--spacing-3);
display: flex;
flex-direction: column;
gap: var(--spacing-3);
}
.form-section {
display: flex;
flex-direction: column;
gap: var(--spacing-2);
border: none;
padding: 0;
margin: 0;
}
.form-section:disabled {
opacity: 1;
}
.form-section legend {
padding: 0 0 6px 0;
margin: 0;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
border-bottom: var(--border-width-1) solid var(--color-border-secondary);
}
.form-textarea {
width: 100%;
padding: var(--spacing-2) var(--spacing-3);
border: var(--border-width-1) solid var(--color-input-border);
border-radius: var(--border-radius-base);
background: var(--color-input-background);
color: var(--color-text-primary);
font-size: var(--font-size-sm);
font-family: var(--font-sans);
resize: vertical;
min-height: 60px;
max-height: 80px;
transition: all var(--transition-fast);
}
.form-textarea:focus {
outline: none;
border-color: var(--ks-color-action);
box-shadow: 0 0 0 2px var(--ks-color-action);
}
.form-footer {
display: flex;
gap: var(--spacing-2);
padding: var(--spacing-3);
border-top: var(--border-width-1) solid var(--color-border-secondary);
flex-shrink: 0;
background: var(--color-background-primary);
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: var(--spacing-8);
text-align: center;
color: var(--color-text-tertiary);
}
.section {