Files
web-inspector/frontend/src/lib/api.ts
jungwoo choi 1e50b72fd8 feat: 사이트 검사 동시 검사 수 설정 추가
- 기본값 2→4로 변경, 사용자가 [1, 2, 4, 8] 중 선택 가능
- 백엔드: concurrency 파라미터 추가 (API → 서비스 → Semaphore)
- 프론트: 드롭다운에 "동시 검사 수" 옵션 UI 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:44:22 +09:00

200 lines
5.8 KiB
TypeScript

import type {
StartInspectionResponse,
InspectionResult,
IssueListResponse,
IssueFilters,
PaginatedResponse,
HistoryParams,
TrendResponse,
} from "@/types/inspection";
import type {
StartSiteInspectionResponse,
SiteInspectionResult,
InspectPageResponse,
} from "@/types/site-inspection";
const API_BASE_URL =
process.env.NEXT_PUBLIC_API_URL ?? "";
/** 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`;
}
// ─────────────────────────────────────────────────────
// 사이트 전체 검사 API
// ─────────────────────────────────────────────────────
/** 사이트 전체 검사 시작 */
async startSiteInspection(
url: string,
maxPages?: number,
maxDepth?: number,
concurrency?: number
): Promise<StartSiteInspectionResponse> {
return this.request("/api/site-inspections", {
method: "POST",
body: JSON.stringify({
url,
max_pages: maxPages,
max_depth: maxDepth,
concurrency: concurrency,
}),
});
}
/** 사이트 검사 결과 조회 */
async getSiteInspection(id: string): Promise<SiteInspectionResult> {
return this.request(`/api/site-inspections/${id}`);
}
/** 특정 페이지 수동 검사 */
async inspectPage(
siteInspectionId: string,
pageUrl: string
): Promise<InspectPageResponse> {
return this.request(
`/api/site-inspections/${siteInspectionId}/inspect-page`,
{
method: "POST",
body: JSON.stringify({ url: pageUrl }),
}
);
}
/** 사이트 검사 SSE 스트림 URL 반환 */
getSiteStreamUrl(siteInspectionId: string): string {
return `${this.baseUrl}/api/site-inspections/${siteInspectionId}/stream`;
}
}
export const api = new ApiClient(API_BASE_URL);