import { useCallback, useEffect, useRef, useState } from 'react'; import { P2vAuthError, P2vJob, P2vNotFoundError, getPosterJob, getStylingJob, isP2vActive, } from '../utils/p2vApi'; interface UseP2vJobResult { job: P2vJob | null; error: string | null; /** 401 — 화면이 접근 키 입력을 띄워야 한다 */ authNeeded: boolean; /** 404 — 서버에 없는 잡이다. 호출부가 화면을 처음으로 되돌려야 한다 */ notFound: boolean; /** 즉시 1회 조회하고, 멈춰 있던 폴링을 다시 켠다 (승인·재시도 직후에 쓴다) */ refresh: () => void; } const POLL_INTERVAL = 2500; /** * P2V 잡 폴링 훅. * * castad 에는 웹소켓이 없고 썰박스도 폴링(`waitForSsulComplete`)이라 같은 방식을 쓴다. * 다만 썰박스는 "끝날 때까지 기다리는" 재귀 함수인 반면 이쪽은 **중간 상태를 계속 * 그려야** 한다 — 검수 게이트에서 멈춰 사용자 입력을 기다리기 때문이다. * * 폴링은 queued/running 에서만 돈다. awaiting_review·done·failed 는 사용자가 뭔가 * 하기 전까지 절대 안 바뀌므로 계속 두드리면 서버만 때린다. 승인·재시도 후에는 * 호출부가 refresh() 로 다시 켠다. */ export function useP2vJob(kind: 'f1' | 'f2', id: string | null): UseP2vJobResult { const [job, setJob] = useState(null); const [error, setError] = useState(null); const [authNeeded, setAuthNeeded] = useState(false); const [notFound, setNotFound] = useState(false); const timerRef = useRef | null>(null); const stop = useCallback(() => { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } }, []); const fetchOnce = useCallback(async (jobId: string) => { try { const next = kind === 'f1' ? await getPosterJob(jobId) : await getStylingJob(jobId); setJob(next); setError(null); setAuthNeeded(false); setNotFound(false); if (!isP2vActive(next)) stop(); } catch (e) { if (e instanceof P2vAuthError) { setAuthNeeded(true); stop(); // 키가 없으면 계속 두드려봐야 401만 쌓인다 return; } if (e instanceof P2vNotFoundError) { setNotFound(true); stop(); // 없는 잡은 다시 생기지 않는다 return; } setError((e as Error).message); } }, [kind, stop]); useEffect(() => { if (!id) { setJob(null); setError(null); setNotFound(false); stop(); return; } // 잡이 바뀌면 이전 잡의 상태가 한 틱 남아 보이므로 먼저 비운다 setJob(null); setNotFound(false); fetchOnce(id); timerRef.current = setInterval(() => fetchOnce(id), POLL_INTERVAL); return stop; }, [id, fetchOnce, stop]); const refresh = useCallback(() => { if (!id) return; fetchOnce(id); if (!timerRef.current) { timerRef.current = setInterval(() => fetchOnce(id), POLL_INTERVAL); } }, [id, fetchOnce]); return { job, error, authNeeded, notFound, refresh }; }