From 81e8824b395a9f999863ae55a34e6a9994457399 Mon Sep 17 00:00:00 2001 From: Jaeho Date: Thu, 4 Jun 2026 02:19:55 +0900 Subject: [PATCH 1/2] =?UTF-8?q?refactor(interview):=20=EC=A7=88=EB=AC=B8?= =?UTF-8?q?=20TTS=20=EC=9E=AC=EC=83=9D=20=EB=A1=9C=EC=A7=81=20=ED=9B=85=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=C2=B7=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=EA=B3=B5=EC=9A=A9=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QuestionBubble 의 TTS 로드·자동재생·토글 로직을 useTtsPlayback 훅으로, 카테고리 ENUM→한글 매핑을 categoryLabel 로 분리해 몰입형 스테이지와 공유할 수 있게 한다. 동작 변화 없음. Co-Authored-By: Claude Opus 4.8 --- .../features/interview/lib/categoryLabel.ts | 12 ++++ .../interview/lib/media/useTtsPlayback.tsx | 65 +++++++++++++++++ .../interview/ui/live/QuestionBubble.tsx | 69 ++++--------------- 3 files changed, 89 insertions(+), 57 deletions(-) create mode 100644 frontend/src/features/interview/lib/categoryLabel.ts create mode 100644 frontend/src/features/interview/lib/media/useTtsPlayback.tsx diff --git a/frontend/src/features/interview/lib/categoryLabel.ts b/frontend/src/features/interview/lib/categoryLabel.ts new file mode 100644 index 00000000..de7f41d2 --- /dev/null +++ b/frontend/src/features/interview/lib/categoryLabel.ts @@ -0,0 +1,12 @@ +// 면접 질문 카테고리 ENUM → 한국어 라벨. 라이브 스테이지와 채팅 버블이 공유한다. +const CATEGORY_LABEL: Record = { + CS_FUNDAMENTAL: 'CS 기초', + PROJECT_DEEP_DIVE: '프로젝트 심화', + TECH_CHOICE: '기술 선택', + BEHAVIORAL: '인성·행동', +} + +export function categoryLabel(category?: string | null): string | null { + if (!category) return null + return CATEGORY_LABEL[category] ?? category +} diff --git a/frontend/src/features/interview/lib/media/useTtsPlayback.tsx b/frontend/src/features/interview/lib/media/useTtsPlayback.tsx new file mode 100644 index 00000000..dea542a5 --- /dev/null +++ b/frontend/src/features/interview/lib/media/useTtsPlayback.tsx @@ -0,0 +1,65 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import { useMessageAudio } from './useMessageAudio' + +// 질문 TTS 오디오의 로드·자동재생·토글을 캡슐화한다. +// QuestionBubble(채팅)과 StageQuestion(몰입형 스테이지)이 공유한다. +// audioNode 를 렌더 트리에 그대로 끼워 넣으면 된다. +export function useTtsPlayback({ + sessionId, + messageId, + enabled, + autoPlay = false, +}: { + sessionId?: number + messageId?: number + enabled: boolean + autoPlay?: boolean +}) { + const { url, load } = useMessageAudio(sessionId, messageId) + const audioRef = useRef(null) + const [playing, setPlaying] = useState(false) + const wantPlay = useRef(false) + const autoTried = useRef(false) + + // 최신 질문이면 오디오를 받아 자동재생 시도(브라우저 차단 시 조용히 폴백). + useEffect(() => { + if (!autoPlay || !enabled || autoTried.current) return + autoTried.current = true + wantPlay.current = true + void load() + }, [autoPlay, enabled, load]) + + // object URL 이 준비되면 대기 중이던 재생 요청을 반영. + useEffect(() => { + if (url && wantPlay.current) { + wantPlay.current = false + audioRef.current?.play().catch(() => {}) + } + }, [url]) + + const toggle = useCallback(async () => { + const el = audioRef.current + if (!url) { + wantPlay.current = true + await load() + return + } + if (!el) return + if (el.paused) el.play().catch(() => {}) + else el.pause() + }, [url, load]) + + const audioNode = + enabled && url ? ( +