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,95 @@
"use client";
import Link from "next/link";
import { Card, CardContent } from "@/components/ui/card";
import { ScoreBadge } from "@/components/common/ScoreBadge";
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
import { EmptyState } from "@/components/common/EmptyState";
import { useRecentSiteInspections } from "@/lib/queries";
import { formatDate, getScoreTailwindColor } from "@/lib/constants";
import { Globe } from "lucide-react";
import type { Grade } from "@/types/inspection";
import { cn } from "@/lib/utils";
/** 최근 사이트 크롤링 이력 (메인 페이지용) */
export function RecentSiteInspections() {
const { data, isLoading, isError } = useRecentSiteInspections();
if (isLoading) {
return <LoadingSpinner message="최근 사이트 크롤링 이력을 불러오는 중..." />;
}
if (isError || !data) {
return null;
}
if (data.items.length === 0) {
return (
<EmptyState
message="사이트 크롤링 이력이 없습니다"
description="사이트 URL을 입력하여 첫 번째 크롤링을 시작해보세요"
/>
);
}
return (
<div className="w-full max-w-2xl mx-auto mt-8">
<h2 className="text-lg font-semibold mb-4"> </h2>
<div className="space-y-3">
{data.items.map((item) => (
<Link
key={item.site_inspection_id}
href={`/site-inspections/${item.site_inspection_id}`}
>
<Card className="hover:shadow-md transition-shadow cursor-pointer">
<CardContent className="flex items-center justify-between py-4 px-5">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-sm font-medium truncate">
{item.domain || item.root_url}
</span>
</div>
<div className="flex items-center gap-2 mt-1">
<p className="text-xs text-muted-foreground">
{formatDate(item.created_at)}
</p>
<span className="text-xs text-muted-foreground">
{item.pages_total}
</span>
</div>
</div>
<div className="flex items-center gap-3 ml-4">
{item.overall_score !== null && (
<>
<span
className={cn(
"text-lg font-bold",
getScoreTailwindColor(item.overall_score)
)}
>
{item.overall_score}
</span>
{item.grade && <ScoreBadge grade={item.grade as Grade} />}
</>
)}
{item.overall_score === null && (
<span className="text-sm text-muted-foreground">
{item.status === "crawling"
? "크롤링 중"
: item.status === "inspecting"
? "검사 중"
: item.status === "error"
? "오류"
: "대기 중"}
</span>
)}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
);
}