o2o-castad-frontend/src/pages/Ssulbox/SsulMakingContent.tsx

112 lines
4.0 KiB
TypeScript

import React from 'react';
import { useTranslation } from 'react-i18next';
import { SCEN, Scen, scenName } from './ssulData';
/** 생성 잡의 현재 상태. GenerationFlow 가 폴링해 내려준다 */
export interface SsulJob {
taskId: number;
scenario: Scen;
status: 'queued' | 'running' | 'done' | 'error';
/** 0~4. 0=준비, 4=영상 합성 완료 */
step: number;
error: string | null;
}
interface SsulMakingContentProps {
job: SsulJob | null;
/** 실패 후 처음부터 다시 */
onRetry: () => void;
}
/** 백엔드 generator 가 뱉는 4단계. `ssul_content.step` 과 1:1 대응 */
const TOTAL_STEPS = 4;
const STEP_LABEL_KEYS = [
'ssulbox.making.step1',
'ssulbox.making.step2',
'ssulbox.making.step3',
'ssulbox.making.step4',
] as const;
/**
* 썰박스 생성 진행 화면.
*
* 원본은 SSE 로 진행률만 받아 막대 하나를 그렸지만, castad 는 폴링으로
* `step`(0~4)을 받으므로 **지금 무슨 작업 중인지 단계명까지** 보여준다.
* 수 분~십수 분이 걸리는 작업이라 진행바만 있으면 멈춘 것처럼 보인다.
*
* 잡 소유는 GenerationFlow 다. 이 컴포넌트는 표시만 하므로
* 사이드바로 이동해 언마운트돼도 폴링이 끊기지 않는다.
* 그래서 "백그라운드로 전환" 버튼을 두지 않는다 — 사이드바로 그냥 이동하면
* 되고, 버튼이 있으면 마치 전환해야 계속 도는 것처럼 오해를 준다.
*/
const SsulMakingContent: React.FC<SsulMakingContentProps> = ({ job, onRetry }) => {
const { t } = useTranslation();
if (!job) return null;
const palette = SCEN[job.scenario];
const failed = job.status === 'error';
// step 은 "완료한 단계 수"라 진행률은 step/4. queued(0) 면 0%
const percent = Math.round((Math.min(job.step, TOTAL_STEPS) / TOTAL_STEPS) * 100);
// 진행 중인 단계는 완료 수 + 1 (마지막 단계 완료 시엔 더 올리지 않음)
const currentIndex = Math.min(job.step, TOTAL_STEPS - 1);
return (
<div className="ssul-scope ssul-making">
<span className="ssul-making__scenario" style={{ color: palette.hex }}>
<span aria-hidden="true">{palette.emoji}</span> {t(scenName(job.scenario))}
</span>
{failed ? (
<>
<h3 className="ssul-making__title warn">{t('ssulbox.making.failTitle')}</h3>
<p className="ssul-making__desc">{job.error || t('ssulbox.making.failDefault')}</p>
{/* 백엔드가 실패 시 크레딧을 환불한다 — 사용자가 손해 보지 않았음을 알린다 */}
<p className="ssul-making__refund">{t('ssulbox.making.refunded')}</p>
<button type="button" className="ssul-making__btn" onClick={onRetry}>
{t('ssulbox.making.retry')}
</button>
</>
) : (
<>
<h3 className="ssul-making__title">{t('ssulbox.making.title')}</h3>
{/*
단계 사이 구간에서는 진행바가 멈춰 있어 고장처럼 보인다.
스피너는 "살아 있다"는 신호 전용이라 진행률을 표현하지 않는다.
색은 시나리오 대표색을 따른다(민트 고정이면 삼국지·오디세이와 부딪힌다).
*/}
<div
className="ssul-making__spinner"
style={{ ['--scn' as string]: palette.hex }}
aria-hidden="true"
/>
<div
className="ssul-making__bar"
role="progressbar"
aria-valuenow={percent}
aria-valuemin={0}
aria-valuemax={100}
>
<div
className="ssul-making__fill"
style={{ width: `${percent}%`, background: palette.hex }}
/>
</div>
<div className="ssul-making__meta">
<span>{t(STEP_LABEL_KEYS[currentIndex])}</span>
<span className="ssul-making__pct">{percent}%</span>
</div>
<p className="ssul-making__desc">{t('ssulbox.making.takesTime')}</p>
</>
)}
</div>
);
};
export default SsulMakingContent;