112 lines
4.0 KiB
TypeScript
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;
|