diff --git a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx b/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx index fbb3eb6..c9ed16b 100644 --- a/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx +++ b/jobdri/src/app/mockApply/[mockApplyId]/(jd)/jd-review/page.tsx @@ -28,14 +28,72 @@ import { type JdReviewSection, } from "@/components/mockApply/jd/jdReviewSections"; -function subscribeToSessionStorage(onStoreChange: () => void) { - const handleStorage = () => onStoreChange(); +export function subscribeToNotificationStream( + onMessage: (data: ApiNotificationItem) => void, + onError?: (error: unknown) => void, +) { + const headers = getAuthHeaders(); + const ctrl = new AbortController(); + + if (!headers.Authorization) { + console.warn("로그인 토큰이 없어 실시간 알림을 연결하지 않습니다."); + return () => ctrl.abort(); + } - window.addEventListener("storage", handleStorage); + const tokenOnly = headers.Authorization.replace("Bearer ", ""); - return () => { - window.removeEventListener("storage", handleStorage); - }; + console.log( + "🔌 SSE 연결 시도 중... URL:", + `${API_BASE_URL}/api/notifications/stream`, + ); + + fetchEventSource( + `${API_BASE_URL}/api/notifications/stream?accessToken=${tokenOnly}`, + { + method: "GET", + headers: { + ...headers, + Accept: "text/event-stream", + }, + signal: ctrl.signal, + + onopen(response) { + if (response.ok) { + console.log("🟢 SSE 연결 성공!! 서버와 스트림 연결됨."); + } else { + console.error( + "🔴 SSE 연결 응답 이상:", + response.status, + response.statusText, + ); + } + }, + + onmessage(event) { + console.log("📥 SSE raw 메시지 수신:", event.data); // 👈 서버가 뭐라도 보내면 무조건 찍힘! + + if (!event.data || !event.data.trim().startsWith("{")) { + return; + } + + try { + const parsedData = JSON.parse(event.data) as ApiNotificationItem; + onMessage(parsedData); + } catch (e: unknown) { + console.error("SSE 데이터 파싱 실패:", e); + } + }, + + onerror(err: unknown) { + console.error("❌ SSE 스트림 에러 발생:", err); + if (onError) onError(err); + }, + }, + ).catch((err) => { + console.error("❌ SSE 연결 래퍼 예외 발생:", err); + }); + + return () => ctrl.abort(); } function parseStoredSections(value: string | null) { diff --git a/jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx b/jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx index 4a746df..c885711 100644 --- a/jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx +++ b/jobdri/src/app/mockApply/[mockApplyId]/result/resume-analysis-loading/ResumeAnalysisLoadingPageClient.tsx @@ -23,6 +23,7 @@ interface ResumeAnalysisLoadingPageClientProps { jobPostingId?: number; applicationLabel?: string; initialSequence?: number; + isError?: boolean; } function isFailedTaskStatus(status: string) { @@ -51,6 +52,7 @@ export default function ResumeAnalysisLoadingPageClient({ jobPostingId, applicationLabel, initialSequence, + isError, }: ResumeAnalysisLoadingPageClientProps) { const router = useRouter(); @@ -59,7 +61,11 @@ export default function ResumeAnalysisLoadingPageClient({ const isValidMockApplyId = Number.isInteger(mockApplyId) && mockApplyId > 0; const [pollingRetryKey, setPollingRetryKey] = useState(0); const [errorMessage, setErrorMessage] = useState( - isValidMockApplyId ? "" : INVALID_MOCK_APPLY_MESSAGE, + isError + ? `응답 대기 시간이 길어져 작업을 멈췄어요.\n소모된 크레딧이 복구됐어요.` + : isValidMockApplyId + ? "" + : INVALID_MOCK_APPLY_MESSAGE, ); const moveToResult = useCallback( @@ -95,7 +101,7 @@ export default function ResumeAnalysisLoadingPageClient({ }, [jobPostingId, mockApplyId, router]); useEffect(() => { - if (!isValidMockApplyId) { + if (isError || !isValidMockApplyId) { return; } @@ -251,7 +257,14 @@ export default function ResumeAnalysisLoadingPageClient({ window.clearInterval(pollTimer); window.clearTimeout(timeoutTimer); }; - }, [isValidMockApplyId, mockApplyId, moveToResult, pollingRetryKey, taskId]); + }, [ + isValidMockApplyId, + mockApplyId, + moveToResult, + pollingRetryKey, + taskId, + isError, + ]); const handleRetry = () => { if (!isValidMockApplyId) { @@ -274,17 +287,13 @@ export default function ResumeAnalysisLoadingPageClient({ {errorMessage && (