Phase 0.5: React UI 기반 계층 구축 — API 클라이언트 + TanStack Query + shadcn UI

1. 서버/UI 상태 분리 (선택지 B 채택)
   - ontologySlice: entities/relations 등 서버 상태 제거 → OntologyDraft(편집 폼)만
   - crawlSlice: progress/stats/logs 제거 → CrawlUiState(토글/확장 상태)만
   - 서버 상태는 전부 TanStack Query로 이전

2. API 클라이언트 계층 (src/lib/api/)
   - client.ts: fetch 래퍼 + Zod 스키마 검증 + ApiError
   - projects.ts: projectsApi.list/detail/create + 도메인 스키마

3. TanStack Query 훅 (src/hooks/)
   - useProjects, useProject, useCreateProject
   - queryKeys 팩토리로 키 일관성

4. shadcn 스타일 UI 프리미티브 (src/components/ui/)
   - button, card, skeleton
   - lib/utils.ts: cn() helper (clsx + tailwind-merge)

5. 레이아웃 (src/components/layout/)
   - AppShell: 축소 가능 Sidebar + Header + Outlet
   - App.tsx 라우트를 AppShell로 중첩

6. DashboardPage End-to-end 연결
   - useProjects()로 백엔드 /projects 호출
   - loading skeleton / error+retry / empty state / 카드 그리드 4가지 상태 모두 처리

7. i18n locale 파일 추가
   - public/locales/ko/common.json (한국어)
   - public/locales/en/common.json (영문 fallback)
   - 키: nav.*, dashboard.*, common.*, app.*

8. 레거시 정리
   - vanilla JS/CSS 24개 → src/legacy/로 git mv
   - tailwind content를 *.{ts,tsx}로 좁혀 legacy 제외

다음 단계: Phase 1 — OnboardingPage 파일 업로드 + useCreateProject 연결

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
lasta
2026-05-14 14:17:26 +09:00
parent 1be2c7d3a3
commit 37cad40472
41 changed files with 669 additions and 232 deletions

View File

@@ -1,58 +1,138 @@
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Plus, BarChart3, Settings } from "lucide-react";
import { Plus, FolderOpen, AlertCircle, Clock } from "lucide-react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { useProjects } from "@/hooks/useProjects";
function formatRelative(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleString();
}
export default function DashboardPage() {
const navigate = useNavigate();
const { t } = useTranslation();
const { data: projects, isLoading, isError, error, refetch } = useProjects();
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-7xl mx-auto px-4 py-12">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-gray-900">
{t("Ontology Builder")}
</h1>
<p className="text-gray-600 mt-2">
{t("Build and manage domain ontologies with AI-powered extraction")}
</p>
</div>
<button
onClick={() => navigate("/onboard")}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition"
>
<Plus className="w-5 h-5" />
{t("New Project")}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<BarChart3 className="w-8 h-8 text-blue-600 mb-4" />
<h3 className="font-semibold text-lg mb-2">{t("Quick Start")}</h3>
<p className="text-gray-600 text-sm">
{t("Get started by uploading your domain ontology")}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<Settings className="w-8 h-8 text-green-600 mb-4" />
<h3 className="font-semibold text-lg mb-2">{t("Phase 5 GraphRAG")}</h3>
<p className="text-gray-600 text-sm">
{t("Intelligent entity resolution and deduplication")}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<Settings className="w-8 h-8 text-purple-600 mb-4" />
<h3 className="font-semibold text-lg mb-2">{t("Phase 7 LLM")}</h3>
<p className="text-gray-600 text-sm">
{t("AI-powered extraction and validation")}
</p>
</div>
<div className="mx-auto max-w-7xl px-6 py-10">
<div className="mb-8 flex items-start justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">
{t("dashboard.title", "Ontology Builder")}
</h1>
<p className="mt-2 text-muted-foreground">
{t(
"dashboard.subtitle",
"Build and manage domain ontologies with AI-powered extraction",
)}
</p>
</div>
<Button onClick={() => navigate("/onboard")}>
<Plus className="h-4 w-4" />
{t("dashboard.newProject", "New Project")}
</Button>
</div>
<section>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">
{t("dashboard.projects", "Projects")}
</h2>
{projects && (
<span className="text-sm text-muted-foreground">
{t("dashboard.projectCount", "{{count}} total", {
count: projects.length,
})}
</span>
)}
</div>
{isLoading && (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32" />
))}
</div>
)}
{isError && (
<Card className="border-destructive">
<CardHeader>
<CardTitle className="flex items-center gap-2 text-destructive">
<AlertCircle className="h-5 w-5" />
{t("dashboard.loadFailed", "Failed to load projects")}
</CardTitle>
<CardDescription>{(error as Error).message}</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" onClick={() => refetch()}>
{t("common.retry", "Retry")}
</Button>
</CardContent>
</Card>
)}
{projects && projects.length === 0 && (
<Card>
<CardContent className="flex flex-col items-center gap-4 py-12 text-center">
<FolderOpen className="h-12 w-12 text-muted-foreground" />
<div>
<p className="font-medium">
{t("dashboard.empty.title", "No projects yet")}
</p>
<p className="text-sm text-muted-foreground">
{t(
"dashboard.empty.hint",
"Create your first project to start building an ontology",
)}
</p>
</div>
<Button onClick={() => navigate("/onboard")}>
<Plus className="h-4 w-4" />
{t("dashboard.newProject", "New Project")}
</Button>
</CardContent>
</Card>
)}
{projects && projects.length > 0 && (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{projects.map((p) => (
<Card
key={p.id}
role="button"
tabIndex={0}
onClick={() => navigate(`/sources/${p.name}`)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
navigate(`/sources/${p.name}`);
}
}}
className="cursor-pointer transition-colors hover:bg-accent/40"
>
<CardHeader>
<CardTitle>{p.name}</CardTitle>
<CardDescription>{p.domain}</CardDescription>
</CardHeader>
<CardContent className="flex items-center gap-2 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
{formatRelative(p.updated_at)}
</CardContent>
</Card>
))}
</div>
)}
</section>
</div>
);
}