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,