From da07bdc8c856c6f54eab312e77b3a8eda0040af8 Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Thu, 21 May 2026 08:41:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(report):=20=3Frun=5Fid=3D=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=EB=A1=9C=20=EB=A1=9C=EB=94=A9=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20resume=20+=20ky=20=ED=83=80=EC=9E=84=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=ED=95=B4=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AnalysisLoadingPage 가 /report/loading?run_id=xxx 쿼리도 path param 과 동일하게 resume 트리거로 인식. createClinic·startAnalysis 재호출 없이 폴링만 재개. - startAnalysis 성공 후 replaceState 를 path 가 아닌 ?run_id= 쿼리로 갱신해 새로고침·공유·뒤로가기 모두 동일한 resume 흐름을 탄다. - ky 의 60s 글로벌 timeout 을 끔. createClinic 처럼 백엔드가 외부 크롤·LLM 을 동기로 호출하는 엔드포인트는 60s 안에 끝난다는 보장이 없어 클라이언트에서 timeout 으로 끊으면 안 됨 (폴링은 어차피 짧음). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../report/hooks/useAnalysisPipeline.ts | 23 ++++++++++++------- src/shared/api/api.ts | 4 +++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/features/report/hooks/useAnalysisPipeline.ts b/src/features/report/hooks/useAnalysisPipeline.ts index d188c3f..0808ba7 100644 --- a/src/features/report/hooks/useAnalysisPipeline.ts +++ b/src/features/report/hooks/useAnalysisPipeline.ts @@ -82,6 +82,11 @@ export function useAnalysisPipeline(): UseAnalysisPipelineResult { const location = useLocation(); const { reportId: urlReportId } = useParams<{ reportId?: string }>(); + // ?run_id= 쿼리로 진입하면 createClinic/startAnalysis 건너뛰고 폴링부터 시작. + // path 파라미터(/report/loading/:reportId) 와 동일 의미로 취급. + const queryRunId = new URLSearchParams(location.search).get('run_id'); + const resumeRunId = urlReportId || queryRunId || undefined; + const locState = (location.state as { url?: string; manualChannels?: ManualChannels }) ?? {}; const url = locState.url; const manualChannels = locState.manualChannels; @@ -137,7 +142,9 @@ export function useAnalysisPipeline(): UseAnalysisPipelineResult { reportId = runId; // 백엔드: analysis_run_id 가 report 조회 키와 동일 saveSession({ reportId, clinicId, runId, url: startUrl }); - window.history.replaceState(null, '', `/report/loading/${reportId}`); + // 새로고침·공유 시 createClinic/startAnalysis 재호출 없이 폴링만 재개되도록 + // run_id 를 쿼리 파라미터로 URL 에 박아둔다. + window.history.replaceState(null, '', `/report/loading?run_id=${reportId}`); } if (!runId) throw new Error('runId 없음'); @@ -203,17 +210,17 @@ export function useAnalysisPipeline(): UseAnalysisPipelineResult { if (hasStarted.current) return; hasStarted.current = true; - // 1. URL 파라미터 resume (예: /report/loading/abc-123) - if (urlReportId) { + // 1. URL 파라미터/쿼리 resume (예: /report/loading/abc-123 또는 /report/loading?run_id=abc-123) + if (resumeRunId) { setPhase('resuming'); const session = loadSession(); - const runId = session.runId || urlReportId; + const runId = session.runId || resumeRunId; getAnalysisStatus(runId) .then((res) => { if (res.status !== 200) throw new Error('status fetch failed'); const status = res.data; if (status.status === AnalysisStatus.completed) { - navigate(`/report/${urlReportId}`, { replace: true }); + navigate(`/report/${resumeRunId}`, { replace: true }); return; } if (status.status === AnalysisStatus.failed) { @@ -222,13 +229,13 @@ export function useAnalysisPipeline(): UseAnalysisPipelineResult { return; } saveSession({ - reportId: urlReportId, + reportId: resumeRunId, clinicId: session.clinicId || undefined, runId, url: session.url || undefined, }); runPipeline(undefined, { - reportId: urlReportId, + reportId: resumeRunId, clinicId: session.clinicId || undefined, runId, }); @@ -274,7 +281,7 @@ export function useAnalysisPipeline(): UseAnalysisPipelineResult { // 4. 신규 분석 시작 runPipeline(url); - }, [url, urlReportId, navigate, runPipeline]); + }, [url, resumeRunId, navigate, runPipeline]); return { phase, diff --git a/src/shared/api/api.ts b/src/shared/api/api.ts index fd77738..1f2ffd0 100644 --- a/src/shared/api/api.ts +++ b/src/shared/api/api.ts @@ -14,7 +14,9 @@ import ky, { type KyInstance } from 'ky' const API_BASE_URL = (__VITE_API_BASE_URL__ ?? '').replace(/\/$/, '') export const kyInstance: KyInstance = ky.create({ - timeout: 60_000, + // createClinic·startAnalysis 처럼 백엔드가 외부 크롤·LLM 을 호출하는 동기 엔드포인트는 + // 60s 안에 끝난다는 보장이 없어 클라이언트 타임아웃은 끈다. 폴링 호출은 어차피 짧다. + timeout: false, retry: 1, // orval generated 타입이 4xx/5xx 응답도 data로 받아오므로 throw 비활성 throwHttpErrors: false,