feat: 웹사이트 표준화 검사 도구 구현
- 4개 검사 엔진: HTML/CSS, 접근성(WCAG), SEO, 성능/보안 (총 50개 항목) - FastAPI 백엔드 (9개 API, SSE 실시간 진행, PDF/JSON 리포트) - Next.js 15 프론트엔드 (6개 페이지, 29개 컴포넌트, 반원 게이지 차트) - Docker Compose 배포 (Backend:8011, Frontend:3011, MongoDB:27022, Redis:6392) - 전체 테스트 32/32 PASS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
103
frontend/src/hooks/useInspectionSSE.ts
Normal file
103
frontend/src/hooks/useInspectionSSE.ts
Normal file
@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useInspectionStore } from "@/stores/useInspectionStore";
|
||||
import { api } from "@/lib/api";
|
||||
import type {
|
||||
SSEProgressEvent,
|
||||
SSECategoryCompleteEvent,
|
||||
SSECompleteEvent,
|
||||
} from "@/types/inspection";
|
||||
|
||||
/**
|
||||
* SSE를 통해 검사 진행 상태를 수신하는 커스텀 훅.
|
||||
* EventSource로 실시간 진행 상태를 수신하고 Zustand 스토어를 업데이트한다.
|
||||
*/
|
||||
export function useInspectionSSE(inspectionId: string | null) {
|
||||
const {
|
||||
updateProgress,
|
||||
setCategoryComplete,
|
||||
setCompleted,
|
||||
setError,
|
||||
setConnecting,
|
||||
} = useInspectionStore();
|
||||
const router = useRouter();
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inspectionId) return;
|
||||
|
||||
setConnecting();
|
||||
|
||||
const streamUrl = api.getStreamUrl(inspectionId);
|
||||
const eventSource = new EventSource(streamUrl);
|
||||
eventSourceRef.current = eventSource;
|
||||
|
||||
eventSource.addEventListener("progress", (e: MessageEvent) => {
|
||||
try {
|
||||
const data: SSEProgressEvent = JSON.parse(e.data);
|
||||
updateProgress(data);
|
||||
} catch {
|
||||
// JSON 파싱 실패 무시
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("category_complete", (e: MessageEvent) => {
|
||||
try {
|
||||
const data: SSECategoryCompleteEvent = JSON.parse(e.data);
|
||||
setCategoryComplete(data);
|
||||
} catch {
|
||||
// JSON 파싱 실패 무시
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("complete", (e: MessageEvent) => {
|
||||
try {
|
||||
const data: SSECompleteEvent = JSON.parse(e.data);
|
||||
setCompleted(data);
|
||||
eventSource.close();
|
||||
// 결과 페이지로 자동 이동
|
||||
router.push(`/inspections/${inspectionId}`);
|
||||
} catch {
|
||||
// JSON 파싱 실패 무시
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.addEventListener("error", (e: Event) => {
|
||||
// SSE 프로토콜 에러 vs 검사 에러 구분
|
||||
if (e instanceof MessageEvent) {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
setError(data.message || "검사 중 오류가 발생했습니다");
|
||||
} catch {
|
||||
setError("검사 중 오류가 발생했습니다");
|
||||
}
|
||||
}
|
||||
// 네트워크 에러인 경우 재연결하지 않고 에러 표시
|
||||
if (eventSource.readyState === EventSource.CLOSED) {
|
||||
setError("서버와의 연결이 끊어졌습니다");
|
||||
}
|
||||
});
|
||||
|
||||
// SSE 연결 타임아웃 (120초)
|
||||
const timeout = setTimeout(() => {
|
||||
eventSource.close();
|
||||
setError("검사 시간이 초과되었습니다 (120초)");
|
||||
}, 120000);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
eventSource.close();
|
||||
eventSourceRef.current = null;
|
||||
};
|
||||
}, [
|
||||
inspectionId,
|
||||
updateProgress,
|
||||
setCategoryComplete,
|
||||
setCompleted,
|
||||
setError,
|
||||
setConnecting,
|
||||
router,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user