11월이다
졸리다
그리고 춥다
TIL 쓸 시간도 부족하다
그래서 오늘 한일 적겠다
사실 깃 커밋 내용이나 이런데에 적어야하는데 너무 긴 것 같기도 하다
// AdvancedTranslationEditor.tsx
const handlePreview = async (translation: Translation) => {
setPreviewTranslation(translation) // 어떤 세그먼트 미리볼지 고정
setIsPreviewOpen(true) // Dialog 열기
setIsPreviewProcessing(true) // 로딩 스피너 ON
...
}
const segId = translation.segmentId ?? translation.id;
const res = await createSegmentPreview(projectID, languageCode, segId, {
text: translation.translated,
});
이 호출은 frontend/src/api/editor.ts의 createSegmentPreview()가 수행:
// POST /api/editor/projects/{projectId}/languages/{lang}/segments/{segmentId}/preview
백엔드(현재 구현)는 editor_router.py의 create_segment_preview()가 즉시 completed 상태와 함께 샘플 videoUrl/audioUrl을 돌려줌 (나중에 워커 붙이면 processing을 돌려주도록 바뀔 자리)
// 즉시 완료인 경우
if (res.status === "completed") {
patchPreviewOn(translation.id, {
status: "completed",
jobId: res.previewId,
videoUrl: res.videoUrl,
audioUrl: res.audioUrl,
updatedAt: res.updatedAt,
});
setIsPreviewProcessing(false); // 스피너 OFF → 영상/오디오 렌더
return;
}
// 처리중인 경우 (워커 연동 시)
if (res.status === "processing" && res.previewId) {
patchPreviewOn(translation.id, {
status: "processing",
jobId: res.previewId,
});
beginPreviewPolling(translation.id, res.previewId); // 아래 4)로 이어짐
return;
}
// 그 외는 실패 처리
patchPreviewOn(translation.id, { status: "failed" });
setIsPreviewProcessing(false);
toast.error("미리보기 생성 실패");
const beginPreviewPolling = (id: string, previewID: string) => {
if (previewPollerRef.current) window.clearInterval(previewPollerRef.current);
previewPollerRef.current = window.setInterval(async () => {
const res = await getSegmentPreview(previewID); // GET /api/editor/preview/{id}
if (res.status === "completed") {
window.clearInterval(previewPollerRef.current!);
patchPreviewOn(id, {
status: "completed",
videoUrl: res.videoUrl,
audioUrl: res.audioUrl,
updatedAt: res.updatedAt,
});
setIsPreviewProcessing(false);
} else if (res.status === "failed") {
window.clearInterval(previewPollerRef.current!);
patchPreviewOn(id, { status: "failed" });
setIsPreviewProcessing(false);
toast.error("미리보기 생성 실패");
}
// processing이면 인터벌 유지
}, 800);
};
getSegmentPreview()는 frontend/src/api/editor.ts에서 정의된 GET 호출editor_router.py의 get_preview()가 인메모리 PREVIEWS에서 상태/URL을 반환const patchPreviewOn = (id, patch) => {
// 목록(editedTranslations) 갱신
setEditedTranslations((prev) =>
prev.map((t) =>
t.id === id
? {
...t,
preview: { ...(t.preview ?? { status: "pending" }), ...patch },
}
: t
)
);
// 다이얼로그 상세(previewTranslation) 갱신
setPreviewTranslation((prev) =>
!prev || prev.id !== id
? prev
: {
...prev,
preview: { ...(prev.preview ?? { status: "pending" }), ...patch },
}
);
};
<Dialog open={isPreviewOpen} onOpenChange={handlePreviewOpenChange}>
<DialogContent>
{isPreviewProcessing || !previewTranslation ? (
// 스피너
) : (
// 완료되면 video/audio에 URL 바인딩
<video ...>
<source src={previewTranslation.preview?.videoUrl ?? fallback} />
</video>
<audio src={previewTranslation.preview?.audioUrl ?? fallback} controls />
)}
</DialogContent>
</Dialog>
const handlePreviewOpenChange = (open: boolean) => {
if (!open) {
if (previewTimerRef.current) window.clearTimeout(previewTimerRef.current);
if (previewPollerRef.current)
window.clearInterval(previewPollerRef.current);
setIsPreviewProcessing(false);
setPreviewTranslation(null);
}
setIsPreviewOpen(open);
};
useEffect(() => {
return () => {
if (previewTimerRef.current) window.clearTimeout(previewTimerRef.current);
if (previewPollerRef.current)
window.clearInterval(previewPollerRef.current);
};
}, []);
POST /api/editor/projects/.../preview
지금은 즉시 completed 로 응답(샘플 URL). 나중에 워커 붙이면 processing으로 바꾸고 PREVIEWS[preview_id]의 상태를 워커가 바꿔주면 됨.GET /api/editor/preview/{id}
PREVIEWS에서 상태/URL을 반환.