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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user