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:
jungwoo choi
2026-02-13 13:57:27 +09:00
parent c37cda5b13
commit b5fa5d96b9
93 changed files with 18735 additions and 22 deletions

View 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,
]);
}