feat(ui): 단계별 현황 재설계 — 완료 가시화 + 빈 단계 숨김 (사용자 피드백)
'대기만 보이고 성공은 안 보인다' 피드백 반영: - overview 에 stages[] 노출 (stage 별 done_today + oldest_pending_age, SQL 1필드 추가) - 게이지 의미 전환: 단계 간 대기량 비교(amber) → 단계 내 오늘 진척(완료=green 비율, 가득 찬 초록 = 다 끝남) + 처리 중 pulse dot - 움직임 없는 단계는 행 제거, 하단 '비어 있음: ...' 한 줄로 - 라벨 누수 fix: details 가 구 STAGE_LABEL 을 쓰던 것 → queueStageLabel 통일 (deep_summary/markdown/summarize/chunk/fulltext 한글화) - 헤더: 오늘 N 완료(성공 가시화) · 실패(error) · 대기. 데이터 소스 = overview 단일화 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,9 +56,19 @@ export interface QueueTotals {
|
||||
failed: number;
|
||||
}
|
||||
|
||||
export interface QueueStageRow {
|
||||
stage: string;
|
||||
pending: number;
|
||||
processing: number;
|
||||
failed: number;
|
||||
done_today: number;
|
||||
oldest_pending_age_sec: number | null;
|
||||
}
|
||||
|
||||
export interface QueueOverview {
|
||||
machines: MachineOverview[];
|
||||
summarize_eta: SummarizeEta;
|
||||
trend_24h: TrendPoint[];
|
||||
stages: QueueStageRow[];
|
||||
totals: QueueTotals;
|
||||
}
|
||||
|
||||
@@ -199,7 +199,20 @@
|
||||
let totalProcessing = $derived(pipelineRows.reduce((s, r) => s + r.processing, 0));
|
||||
|
||||
let pipelineManualClosed = $state(false);
|
||||
let pipelineOpen = $derived(pipelineManualClosed ? false : totalFailed > 0);
|
||||
let pipelineOpen = $derived(
|
||||
pipelineManualClosed ? false : (queue?.totals.failed ?? totalFailed) > 0
|
||||
);
|
||||
|
||||
// 단계별 현황 (2026-06-11 피드백 재설계: 완료가 보여야 한다 — overview.stages 단일 소스)
|
||||
// active = 오늘 움직임이 있는 단계만, idle = 전부 0 인 단계는 한 줄로 숨김.
|
||||
let stageRows = $derived(queue?.stages ?? []);
|
||||
let activeStageRows = $derived(
|
||||
stageRows.filter((r) => r.pending + r.processing + r.failed + r.done_today > 0)
|
||||
);
|
||||
let idleStageRows = $derived(
|
||||
stageRows.filter((r) => r.pending + r.processing + r.failed + r.done_today === 0)
|
||||
);
|
||||
let stageDoneToday = $derived(stageRows.reduce((s, r) => s + r.done_today, 0));
|
||||
|
||||
function formatAge(sec: number | null): string {
|
||||
if (sec == null || sec <= 0) return '';
|
||||
@@ -517,44 +530,67 @@
|
||||
<summary class="flex items-center justify-between px-5 py-3.5 bg-surface border border-default rounded-card cursor-pointer hover:bg-surface-hover transition-colors select-none list-none">
|
||||
<span class="text-sm font-semibold text-text flex items-center gap-2">
|
||||
<ChevronRight size={14} class="transition-transform details-chevron" />
|
||||
단계 상세
|
||||
단계별 현황
|
||||
</span>
|
||||
<span class="text-xs text-dim flex items-center gap-2.5">
|
||||
{#if totalFailed > 0}<span class="text-error font-medium">실패 {totalFailed}</span>{/if}
|
||||
{#if totalPending > 0}<span>대기 {totalPending}</span>{/if}
|
||||
{#if totalFailed === 0 && totalPending === 0}<span>처리 완료</span>{/if}
|
||||
{#if queue}
|
||||
{#if stageDoneToday > 0}<span class="text-success">오늘 {stageDoneToday.toLocaleString()} 완료</span>{/if}
|
||||
{#if queue.totals.failed > 0}<span class="text-error font-medium">실패 {queue.totals.failed}</span>{/if}
|
||||
{#if queue.totals.pending > 0}<span>대기 {queue.totals.pending.toLocaleString()}</span>{/if}
|
||||
{#if stageDoneToday === 0 && queue.totals.failed === 0 && queue.totals.pending === 0}<span>모든 단계 한가함</span>{/if}
|
||||
{:else}
|
||||
{#if totalFailed > 0}<span class="text-error font-medium">실패 {totalFailed}</span>{/if}
|
||||
{#if totalPending > 0}<span>대기 {totalPending}</span>{/if}
|
||||
{/if}
|
||||
</span>
|
||||
</summary>
|
||||
|
||||
<div class="mt-2 px-5 py-4 bg-surface border border-default rounded-card">
|
||||
<p class="text-xs text-dim mb-3">최근 24시간</p>
|
||||
{#if pipelineRows.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each pipelineRows as row (row.stage)}
|
||||
<div>
|
||||
<div class="flex items-center justify-between text-xs mb-1.5">
|
||||
<span class="text-dim">
|
||||
{row.label}
|
||||
{#if row.oldestPendingAgeSec && row.oldestPendingAgeSec > 600}
|
||||
<span class="ml-1 text-warning" title="가장 오래된 pending 의 경과 시간">({formatAge(row.oldestPendingAgeSec)})</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="text-dim tabular-nums">
|
||||
대기 <span class="text-text">{row.pending}</span> ·
|
||||
처리 <span class="text-text">{row.processing}</span> ·
|
||||
실패 <span class={row.failed > 0 ? 'text-error font-medium' : ''}>{row.failed}</span>
|
||||
</span>
|
||||
{#if queue}
|
||||
{#if activeStageRows.length > 0}
|
||||
<div class="space-y-3.5">
|
||||
{#each activeStageRows as row (row.stage)}
|
||||
{@const total = row.done_today + row.pending + row.processing}
|
||||
{@const donePct = total > 0 ? (row.done_today / total) * 100 : 0}
|
||||
{@const procPct = total > 0 ? (row.processing / total) * 100 : 0}
|
||||
<div>
|
||||
<div class="flex items-baseline justify-between text-xs mb-1.5 gap-2">
|
||||
<span class="font-medium text-text flex items-center gap-1.5 whitespace-nowrap">
|
||||
{queueStageLabel(row.stage)}
|
||||
{#if row.processing > 0}
|
||||
<span class="inline-block w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||
<span class="text-accent font-normal">처리 중 {row.processing}</span>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="text-dim tabular-nums flex items-center gap-2.5 whitespace-nowrap">
|
||||
{#if row.done_today > 0}<span class="text-success">오늘 {row.done_today.toLocaleString()} 완료</span>{/if}
|
||||
{#if row.pending > 0}<span>대기 {row.pending.toLocaleString()}</span>{/if}
|
||||
{#if row.failed > 0}<span class="text-error font-medium">실패 {row.failed}</span>{/if}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 게이지 = 이 단계의 오늘 진척 (완료 / 완료+대기) — 가득 찬 초록 = 다 끝남 -->
|
||||
<div class="flex h-1.5 w-full overflow-hidden rounded-sm bg-bg" title="오늘 완료 {row.done_today.toLocaleString()} / 잔여 {row.pending.toLocaleString()}">
|
||||
{#if donePct > 0}<div class="bg-success/70 h-full" style="width: {donePct}%"></div>{/if}
|
||||
{#if procPct > 0}<div class="bg-accent h-full" style="width: {Math.max(procPct, 1)}%"></div>{/if}
|
||||
</div>
|
||||
{#if row.pending > 0 && row.oldest_pending_age_sec && row.oldest_pending_age_sec > 600}
|
||||
<p class="text-[10px] mt-1 tabular-nums {row.oldest_pending_age_sec > 21600 ? 'text-warning' : 'text-faint'}">
|
||||
가장 오래 기다린 항목 {formatAge(row.oldest_pending_age_sec)}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex h-1.5 w-full overflow-hidden rounded-sm bg-bg">
|
||||
{#if row.pending > 0}<div class="bg-warning h-full" style="width: {(row.pending / pipelineMax) * 100}%"></div>{/if}
|
||||
{#if row.processing > 0}<div class="bg-accent h-full" style="width: {(row.processing / pipelineMax) * 100}%"></div>{/if}
|
||||
{#if row.failed > 0}<div class="bg-error h-full" style="width: {(row.failed / pipelineMax) * 100}%"></div>{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs text-dim text-center py-3">대기·처리·실패 없음 — 모든 단계가 한가합니다</p>
|
||||
{/if}
|
||||
{#if idleStageRows.length > 0}
|
||||
<p class="text-[11px] text-faint mt-4 pt-3 border-t border-default">
|
||||
비어 있음: {idleStageRows.map((r) => queueStageLabel(r.stage)).join(' · ')}
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-xs text-dim text-center py-3">처리 작업 없음</p>
|
||||
<p class="text-xs text-dim text-center py-3">현황을 불러오지 못했습니다</p>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
Reference in New Issue
Block a user