playreel/frontend/components/stage-stepper.tsx
2026-09-08 13:48:20 +09:00

64 lines
2.3 KiB
TypeScript

"use client";
import type { Job } from "@/lib/api";
/* 프로덕트 wizard-stepper 패턴: 완료=민트 체크, 현재=민트 링+글로우, 대기=회색 번호 */
const STAGE_LABELS: Record<string, string> = {
detect: "분석",
narration_text: "나레이션",
tts: "음성",
bgm: "음악",
motion: "모션 선정",
i2v: "애니메이션",
render: "렌더",
transfer: "변환",
};
const Check = () => (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 6 9 17l-5-5" />
</svg>
);
export default function StageStepper({ job, labels }: { job: Pick<Job, "stages" | "status">; labels?: Record<string, string> }) {
const L = labels ?? STAGE_LABELS;
const entries = Object.entries(job.stages);
// 검수 대기 중에는 "아직 끝나지 않은 첫 스테이지"가 현재다.
// 스테이지명을 박아두면 게이트가 늘 때마다 여기가 조용히 틀어진다.
const firstPending = entries.find(([, st]) => st.status !== "done")?.[0];
return (
<div className="wizard-stepper">
{entries.map(([key, st], i) => {
const cls =
st.status === "done" ? "done" :
st.status === "failed" ? "failed" :
st.status === "running" || (job.status === "awaiting_review" && key === firstPending) ? "current" :
"pending";
return (
<div key={key} style={{ display: "contents" }}>
{i > 0 && (
<div className={entries[i - 1][1].status === "done" ? "wizard-step-line done" : "wizard-step-line"} />
)}
<div className={`wizard-step ${cls}`}>
<div className="wizard-stepper-node">
{st.status === "done" ? <Check /> : i + 1}
</div>
<div className="wizard-stepper-label">{L[key] ?? key}</div>
</div>
</div>
);
})}
</div>
);
}
/** 스테이지 진행률 (%) — 선형 진행바용 */
export function stageProgress(job: Pick<Job, "stages">): number {
const sts = Object.values(job.stages);
const done = sts.filter((s) => s.status === "done").length;
const running = sts.some((s) => s.status === "running") ? 0.5 : 0;
return Math.round(((done + running) / sts.length) * 100);
}