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:
148
frontend/src/lib/api.ts
Normal file
148
frontend/src/lib/api.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import type {
|
||||
StartInspectionResponse,
|
||||
InspectionResult,
|
||||
IssueListResponse,
|
||||
IssueFilters,
|
||||
PaginatedResponse,
|
||||
HistoryParams,
|
||||
TrendResponse,
|
||||
} from "@/types/inspection";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL || "http://localhost:8011";
|
||||
|
||||
/** API 에러 클래스 */
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
detail: string;
|
||||
|
||||
constructor(status: number, detail: string) {
|
||||
super(detail);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
/** API 클라이언트 */
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
private async request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...options,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let detail = "요청 처리 중 오류가 발생했습니다";
|
||||
try {
|
||||
const error = await response.json();
|
||||
detail = error.detail || detail;
|
||||
} catch {
|
||||
// JSON 파싱 실패 시 기본 메시지 사용
|
||||
}
|
||||
throw new ApiError(response.status, detail);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/** 검사 시작 */
|
||||
async startInspection(url: string): Promise<StartInspectionResponse> {
|
||||
return this.request("/api/inspections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
}
|
||||
|
||||
/** 검사 결과 조회 */
|
||||
async getInspection(id: string): Promise<InspectionResult> {
|
||||
return this.request(`/api/inspections/${id}`);
|
||||
}
|
||||
|
||||
/** 이슈 목록 조회 */
|
||||
async getIssues(
|
||||
id: string,
|
||||
filters?: IssueFilters
|
||||
): Promise<IssueListResponse> {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.category && filters.category !== "all") {
|
||||
params.set("category", filters.category);
|
||||
}
|
||||
if (filters?.severity && filters.severity !== "all") {
|
||||
params.set("severity", filters.severity);
|
||||
}
|
||||
const qs = params.toString();
|
||||
return this.request(`/api/inspections/${id}/issues${qs ? `?${qs}` : ""}`);
|
||||
}
|
||||
|
||||
/** 이력 목록 조회 (페이지네이션) */
|
||||
async getInspections(params: HistoryParams): Promise<PaginatedResponse> {
|
||||
const qs = new URLSearchParams();
|
||||
qs.set("page", String(params.page || 1));
|
||||
qs.set("limit", String(params.limit || 20));
|
||||
if (params.url) qs.set("url", params.url);
|
||||
if (params.sort) qs.set("sort", params.sort);
|
||||
return this.request(`/api/inspections?${qs}`);
|
||||
}
|
||||
|
||||
/** 트렌드 데이터 조회 */
|
||||
async getTrend(url: string, limit = 10): Promise<TrendResponse> {
|
||||
const qs = new URLSearchParams({ url, limit: String(limit) });
|
||||
return this.request(`/api/inspections/trend?${qs}`);
|
||||
}
|
||||
|
||||
/** PDF 다운로드 */
|
||||
async downloadPdf(id: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/inspections/${id}/report/pdf`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, "PDF 다운로드에 실패했습니다");
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download =
|
||||
response.headers.get("content-disposition")?.split("filename=")[1] ||
|
||||
`web-inspector-report.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/** JSON 다운로드 */
|
||||
async downloadJson(id: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${this.baseUrl}/api/inspections/${id}/report/json`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new ApiError(response.status, "JSON 다운로드에 실패했습니다");
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download =
|
||||
response.headers.get("content-disposition")?.split("filename=")[1] ||
|
||||
`web-inspector-report.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/** SSE 스트림 URL 반환 */
|
||||
getStreamUrl(inspectionId: string): string {
|
||||
return `${this.baseUrl}/api/inspections/${inspectionId}/stream`;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ApiClient(API_BASE_URL);
|
||||
Reference in New Issue
Block a user