From 262f250a3a1c9fff773f54863de4035313f80d33 Mon Sep 17 00:00:00 2001 From: gituser Date: Mon, 19 Oct 2026 14:20:59 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EC=A7=88=EB=AC=B8=20=EC=9D=8C=EC=84=B1?= =?UTF-8?q?=20PENDING=20=EC=9D=91=EB=8B=B5=20=ED=9B=84=20=EC=9E=AC?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=97=90=20=EC=B5=9C=EC=86=8C=201=EC=B4=88?= =?UTF-8?q?=20=EA=B0=84=EA=B2=A9=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버가 long-poll 대기 없이 PENDING을 바로 돌려주면 대기 없이 재요청이 반복되던 문제를 막는다. 기존 오류 재시도 대기 로직을 공용 wait로 정리했다. Co-Authored-By: Claude Opus 5.5 --- .../interview/model/useQuestionAudio.ts | 19 ++++++++++++++----- tests/interview-browser-fixture.mjs | 2 +- tests/question-audio.browser.mjs | 3 +++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/features/interview-page/interview/model/useQuestionAudio.ts b/src/features/interview-page/interview/model/useQuestionAudio.ts index a800dab9..aedd8b5c 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 12936f1c..f7111e9d 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 6d4910e1..1ec97d6d 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`); }