- 기본값 2→4로 변경, 사용자가 [1, 2, 4, 8] 중 선택 가능 - 백엔드: concurrency 파라미터 추가 (API → 서비스 → Semaphore) - 프론트: 드롭다운에 "동시 검사 수" 옵션 UI 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
5.8 KiB
TypeScript
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);
|