fix(board): 실패 뱃지 잘림(스크롤 컨테이너 헤드룸) + 구 단계별 현황 섹션 제거 + ETA 48h+ 일 표기
- 흐름 컨테이너 pt/px 헤드룸 — -top/-right 돌출 뱃지가 overflow-x-auto 에 잘리던 문제 - 단계별 현황 details = 흐름 보드가 대체(R2 통합안 의도) — 전용 파생값/헬퍼/chevron 동반 제거 - etaShort: 48시간 이상은 일 단위 (약 131시간 → 약 5.5일) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -246,8 +246,8 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- 흐름 노드 -->
|
||||
<div class="flex items-stretch overflow-x-auto pb-1">
|
||||
<!-- 흐름 노드 — pt/px 헤드룸 = 실패 뱃지(-top/-right 돌출)가 스크롤 컨테이너에 잘리지 않게 -->
|
||||
<div class="flex items-stretch overflow-x-auto pt-2.5 pb-1 px-2 -mx-2">
|
||||
{#each mainNodes as n, i (n.def.key)}
|
||||
{#if i > 0}
|
||||
<div class="flex items-center text-faint text-sm px-1.5 shrink-0" aria-hidden="true">→</div>
|
||||
|
||||
@@ -37,10 +37,14 @@ export function etaPhrase(minutes: number): string {
|
||||
return `약 ${text}시간 후 소진 예상`;
|
||||
}
|
||||
|
||||
/** ETA 분 → 칩용 짧은 표기 ("약 4.6시간" / "약 12분") */
|
||||
/** ETA 분 → 칩용 짧은 표기 ("약 12분" / "약 4.6시간" / 48h+ = "약 5.5일") */
|
||||
export function etaShort(minutes: number): string {
|
||||
if (minutes < 60) return `약 ${Math.max(1, Math.round(minutes))}분`;
|
||||
const hours = minutes / 60;
|
||||
if (hours >= 48) {
|
||||
const days = hours / 24;
|
||||
return `약 ${days >= 10 ? Math.round(days) : Math.round(days * 10) / 10}일`;
|
||||
}
|
||||
const text = hours >= 10 ? String(Math.round(hours)) : String(Math.round(hours * 10) / 10);
|
||||
return `약 ${text}시간`;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import Skeleton from '$lib/components/ui/Skeleton.svelte';
|
||||
import {
|
||||
Scale, FileText, Pin, ChevronRight, GraduationCap, Upload, Newspaper,
|
||||
Scale, FileText, Pin, GraduationCap, Upload, Newspaper,
|
||||
} from 'lucide-svelte';
|
||||
import { addToast } from '$lib/stores/toast';
|
||||
|
||||
@@ -133,17 +133,6 @@
|
||||
// 백엔드 미배포/실패 시 store=null → 보드 자체가 조용히 생략 (silent 비차단).
|
||||
let queue = $derived<QueueOverview | null>($queueOverview);
|
||||
|
||||
// 머신 담당 단계 라벨 — STAGE_LABEL 재사용 + overview 전용 단계 보강
|
||||
// (backend services/queue_overview.py _STAGE_ORDER 와 동기), 미지 키는 raw
|
||||
const QUEUE_STAGE_LABEL: Record<string, string> = {
|
||||
...STAGE_LABEL,
|
||||
summarize: '요약', chunk: '청크', markdown: '마크다운',
|
||||
fulltext: '전문', deep_summary: '심층분석',
|
||||
};
|
||||
function queueStageLabel(stage: string): string {
|
||||
return QUEUE_STAGE_LABEL[stage] ?? stage;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
void refreshQueueOverview();
|
||||
const handle = setInterval(() => void refreshQueueOverview(), 30_000);
|
||||
@@ -191,35 +180,10 @@
|
||||
let pipelineRows = $derived(
|
||||
summary ? buildPipelineRows(summary.pipeline_status, summary.queue_lag ?? []) : []
|
||||
);
|
||||
let pipelineMax = $derived(Math.max(1, ...pipelineRows.map((r) => r.total)));
|
||||
let totalFailed = $derived(summary?.failed_count ?? 0);
|
||||
let totalPending = $derived(pipelineRows.reduce((s, r) => s + r.pending, 0));
|
||||
let totalProcessing = $derived(pipelineRows.reduce((s, r) => s + r.processing, 0));
|
||||
|
||||
let pipelineManualClosed = $state(false);
|
||||
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 '';
|
||||
if (sec < 60) return `${sec}초 전`;
|
||||
if (sec < 3600) return `${Math.floor(sec / 60)}분 전`;
|
||||
if (sec < 86400) return `${Math.floor(sec / 3600)}시간 전`;
|
||||
return `${Math.floor(sec / 86400)}일 전`;
|
||||
}
|
||||
|
||||
function formatTime(dateStr: string) {
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return ''; // 빈 문자열/유효하지 않은 created_at → 'Invalid Date' 회피
|
||||
@@ -463,80 +427,6 @@
|
||||
<ProcessingFlowBoard overview={queue} />
|
||||
{/if}
|
||||
|
||||
<!-- ═══ 단계 상세 (기존 stage 테이블 — 접힘 강등, 실패 있을 때 자동 펼침) ═══ -->
|
||||
<details
|
||||
class="mt-5"
|
||||
open={pipelineOpen}
|
||||
ontoggle={(e) => { if (!e.currentTarget.open) pipelineManualClosed = true; }}
|
||||
>
|
||||
<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 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">
|
||||
{#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>
|
||||
{/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>
|
||||
{/if}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -548,7 +438,3 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<style>
|
||||
details[open] :global(.details-chevron) { transform: rotate(90deg); }
|
||||
details summary::-webkit-details-marker { display: none; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user