jun-wiki

View My GitHub Profile

Posts (Latest 10 updated) :
Read all
Contents:
  1. 한 줄 요약
  2. 흐름 상세 (코드 연결)
    1. 1) 버튼 클릭 → 다이얼로그 오픈 & 로딩 진입
    2. 2) 프리뷰 생성 요청 (POST)
    3. 3) 응답 분기 처리
    4. 4) (필요할 때만) 폴링 시작
    5. 5) 상태 업데이트 유틸 (리스트 & 상세 동시 반영)
    6. 6) 렌더링 (다이얼로그 내부)
    7. 7) 정리(클린업)
  3. 백엔드 쪽 현재 동작

11월이다

졸리다

그리고 춥다

TIL 쓸 시간도 부족하다

그래서 오늘 한일 적겠다

사실 깃 커밋 내용이나 이런데에 적어야하는데 너무 긴 것 같기도 하다

한 줄 요약

  • 클릭 → POST 생성 → (processing면) GET 폴링 → completed되면 video/audio 표시 → 다이얼로그 닫힐 때 타이머 정리

흐름 상세 (코드 연결)

1) 버튼 클릭 → 다이얼로그 오픈 & 로딩 진입

// AdvancedTranslationEditor.tsx
const handlePreview = async (translation: Translation) => {
  setPreviewTranslation(translation)      // 어떤 세그먼트 미리볼지 고정
  setIsPreviewOpen(true)                  // Dialog 열기
  setIsPreviewProcessing(true)            // 로딩 스피너 ON
  ...
}

2) 프리뷰 생성 요청 (POST)

const segId = translation.segmentId ?? translation.id;
const res = await createSegmentPreview(projectID, languageCode, segId, {
  text: translation.translated,
});
  • 이 호출은 frontend/src/api/editor.tscreateSegmentPreview()가 수행:

    // POST /api/editor/projects/{projectId}/languages/{lang}/segments/{segmentId}/preview
    
  • 백엔드(현재 구현)는 editor_router.pycreate_segment_preview()가 즉시 completed 상태와 함께 샘플 videoUrl/audioUrl을 돌려줌 (나중에 워커 붙이면 processing을 돌려주도록 바뀔 자리)

3) 응답 분기 처리

// 즉시 완료인 경우
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("미리보기 생성 실패");

4) (필요할 때만) 폴링 시작

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.pyget_preview()가 인메모리 PREVIEWS에서 상태/URL을 반환

5) 상태 업데이트 유틸 (리스트 & 상세 동시 반영)

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 },
        }
  );
};
  • 이걸로 목록 카드다이얼로그가 항상 같은 프리뷰 상태를 보게 됨

6) 렌더링 (다이얼로그 내부)

<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>

7) 정리(클린업)

  • 모달 닫기 / 언마운트 시 타이머, 인터벌을 항상 해제:
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을 반환.