feat: 3-mode inspection with tabbed UI + batch upload

- Add batch inspection backend (multipart upload, SSE streaming, MongoDB)
- Add tabbed UI (single page / site crawling / batch upload) on home and history pages
- Add batch inspection progress, result pages with 2-panel layout
- Rename "사이트 전체" to "사이트 크롤링" across codebase
- Add python-multipart dependency for file upload
- Consolidate nginx SSE location for all inspection types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2026-02-13 19:15:27 +09:00
parent 9bb844c5e1
commit 8326c84be9
32 changed files with 3700 additions and 61 deletions

View File

@ -0,0 +1,143 @@
import { create } from "zustand";
import type { AggregateScores } from "@/types/site-inspection";
import type {
BatchPage,
BatchInspectionPhase,
BatchInspectionResult,
SSEBatchPageComplete,
SSEBatchAggregateUpdate,
} from "@/types/batch-inspection";
interface BatchInspectionState {
batchInspectionId: string | null;
name: string | null;
status: BatchInspectionPhase;
discoveredPages: BatchPage[];
aggregateScores: AggregateScores | null;
errorMessage: string | null;
// Actions
setBatchInspection: (id: string, name: string) => void;
initFromApi: (data: BatchInspectionResult) => void;
updatePageStatus: (
pageUrl: string,
status: BatchPage["status"],
extra?: {
inspection_id?: string;
overall_score?: number;
grade?: string;
}
) => void;
setPageComplete: (data: SSEBatchPageComplete) => void;
updateAggregateScores: (data: SSEBatchAggregateUpdate) => void;
setCompleted: (aggregateScores: AggregateScores) => void;
setError: (message: string) => void;
reset: () => void;
}
const initialState = {
batchInspectionId: null,
name: null,
status: "idle" as BatchInspectionPhase,
discoveredPages: [] as BatchPage[],
aggregateScores: null,
errorMessage: null,
};
export const useBatchInspectionStore = create<BatchInspectionState>(
(set) => ({
...initialState,
setBatchInspection: (id, name) =>
set({
...initialState,
batchInspectionId: id,
name,
status: "inspecting",
}),
initFromApi: (data) =>
set((state) => {
// Only init if store is idle (prevents overwriting live SSE data)
if (state.status !== "idle") return state;
return {
batchInspectionId: data.batch_inspection_id,
name: data.name,
status: data.status as BatchInspectionPhase,
discoveredPages: data.discovered_pages,
aggregateScores: data.aggregate_scores,
};
}),
updatePageStatus: (pageUrl, status, extra) =>
set((state) => ({
discoveredPages: state.discoveredPages.map((page) =>
page.url === pageUrl
? {
...page,
status,
...(extra?.inspection_id && {
inspection_id: extra.inspection_id,
}),
...(extra?.overall_score !== undefined && {
overall_score: extra.overall_score,
}),
...(extra?.grade && { grade: extra.grade }),
}
: page
),
})),
setPageComplete: (data) =>
set((state) => ({
discoveredPages: state.discoveredPages.map((page) =>
page.url === data.page_url
? {
...page,
status: "completed" as const,
inspection_id: data.inspection_id,
overall_score: data.overall_score,
grade: data.grade,
}
: page
),
})),
updateAggregateScores: (data) =>
set((state) => ({
aggregateScores: state.aggregateScores
? {
...state.aggregateScores,
pages_inspected: data.pages_inspected,
pages_total: data.pages_total,
overall_score: data.overall_score,
grade: data.grade,
}
: {
overall_score: data.overall_score,
grade: data.grade,
html_css: 0,
accessibility: 0,
seo: 0,
performance_security: 0,
total_issues: 0,
pages_inspected: data.pages_inspected,
pages_total: data.pages_total,
},
})),
setCompleted: (aggregateScores) =>
set({
status: "completed",
aggregateScores,
}),
setError: (message) =>
set({
status: "error",
errorMessage: message,
}),
reset: () => set({ ...initialState }),
})
);