diff --git a/src/features/interview-page/interview/model/useQuestionAudio.ts b/src/features/interview-page/interview/model/useQuestionAudio.ts index a800dab..aedd8b5 100644 --- a/src/features/interview-page/interview/model/useQuestionAudio.ts +++ b/src/features/interview-page/interview/model/useQuestionAudio.ts @@ -4,6 +4,8 @@ import type { QuestionAudioStatus } from "@/widgets/interview-page/interview/typ const AUDIO_WAIT_MS = 180_000; const RETRY_DELAYS_MS = [1_000, 2_000, 4_000]; +// 서버가 long-poll 없이 PENDING을 바로 돌려줘도 요청이 몰리지 않게 하는 최소 간격 +const PENDING_MIN_INTERVAL_MS = 1_000; const FAILURE_MESSAGE = "음성 재생에 실패했습니다."; interface UseQuestionAudioParams { @@ -81,8 +83,13 @@ export const useQuestionAudio = ({ sessionId, questionId, enabled, autoplay }: U await Promise.resolve(); updateStatus(operation, "loading"); let errorCount = 0; + const wait = (delay: number) => new Promise((resolve) => { + releaseRetry = resolve; + retryTimer = window.setTimeout(resolve, delay); + }); while (isCurrent(operation)) { if (Date.now() - startedAt >= AUDIO_WAIT_MS) { fail(); return; } + const requestedAt = Date.now(); const result = await getQuestionAudio({ sessionId, questionId, signal: controller.signal }); if (!isCurrent(operation) || ("aborted" in result)) return; if (Date.now() - startedAt >= AUDIO_WAIT_MS) { fail(); return; } @@ -95,14 +102,16 @@ export const useQuestionAudio = ({ sessionId, questionId, enabled, autoplay }: U console.warn("질문 음성 조회 실패", result.errorMessage); const delay = RETRY_DELAYS_MS[errorCount++]; if (delay === undefined) { fail(); return; } - await new Promise((resolve) => { - releaseRetry = resolve; - retryTimer = window.setTimeout(resolve, delay); - }); + await wait(delay); continue; } const data = result.data; - if (data.status === "PENDING") { errorCount = 0; continue; } + if (data.status === "PENDING") { + errorCount = 0; + const remaining = PENDING_MIN_INTERVAL_MS - (Date.now() - requestedAt); + if (remaining > 0) await wait(remaining); + continue; + } window.clearTimeout(deadline); if (data.status === "FAILED") { console.warn("질문 음성 생성 실패", data.errorCode); diff --git a/tests/interview-browser-fixture.mjs b/tests/interview-browser-fixture.mjs index 12936f1..f7111e9 100644 --- a/tests/interview-browser-fixture.mjs +++ b/tests/interview-browser-fixture.mjs @@ -176,7 +176,7 @@ export async function fixture({ multi = false, answerMode = "voice", timeout = f if (pathname.endsWith("/me")) return json({ data: { id: 7, name: "테스트", nickname: "테스트" } }); if (pathname.endsWith("/audio")) { const id = Number(pathname.split("/").at(-2)); - audioRequests.push({ id, wait: requestUrl.searchParams.get("wait"), authorization: request.headers().authorization }); + audioRequests.push({ id, wait: requestUrl.searchParams.get("wait"), authorization: request.headers().authorization, at: Date.now() }); const count = audioCounts.get(id) ?? 0; audioCounts.set(id, count + 1); if (holdAudio && id === 101) await new Promise((resolve) => audioReleases.push(resolve)); diff --git a/tests/question-audio.browser.mjs b/tests/question-audio.browser.mjs index 6d4910e..1ec97d6 100644 --- a/tests/question-audio.browser.mjs +++ b/tests/question-audio.browser.mjs @@ -88,6 +88,9 @@ try { const f = await fixture({ audioResponses: statuses }); await stop(f.page).waitFor(); assert.equal(f.audioRequests.length, 3); + for (let i = 1; i < f.audioRequests.length; i++) { + assert.ok(f.audioRequests[i].at - f.audioRequests[i - 1].at >= 900, "PENDING 직후 재요청은 최소 간격을 둔다"); + } assert.equal(await plays(f.page), 1); await close(f, `${statuses.join(" -> ")}: polling reaches native playback`); }