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>
84 lines
2.6 KiB
JavaScript
84 lines
2.6 KiB
JavaScript
import "./styles/index.css";
|
|
import { mountShell } from "./shell.js";
|
|
import { mountSidebar, renderProjects, renderSourceOptions } from "./sidebar.js";
|
|
import {
|
|
mountWorkspace,
|
|
renderOverview,
|
|
renderOntology,
|
|
loadOntologyRegistry,
|
|
loadEntities,
|
|
loadExtractionLogs,
|
|
loadResearchSessions,
|
|
loadGraphQuery,
|
|
loadTags,
|
|
} from "./workspace.js";
|
|
import { loadGraph } from "./graph.js";
|
|
import { loadWorkbench } from "./workbench.js";
|
|
import { loadPipeline } from "./pipeline.js";
|
|
import { mountGlobalSearch } from "./search.js";
|
|
import { mountInspector, refreshInspector } from "./inspector.js";
|
|
import { api } from "./api.js";
|
|
import { state } from "./state.js";
|
|
import { toast } from "./utils.js";
|
|
import { onLangChange } from "./i18n.js";
|
|
|
|
const root = document.getElementById("app");
|
|
const hosts = mountShell(root);
|
|
mountSidebar(hosts.sidebarHost, {
|
|
onProjectChanged: loadProjects,
|
|
onCrawlUpdate: () => refreshInspector(),
|
|
refreshAll,
|
|
});
|
|
mountWorkspace(hosts.workspaceHost);
|
|
mountInspector(hosts.inspectorHost);
|
|
mountGlobalSearch();
|
|
|
|
document.getElementById("refreshBtn").addEventListener("click", loadProjects);
|
|
window.addEventListener("project:select", (event) => selectProject(event.detail));
|
|
window.addEventListener("workspace:refresh", refreshAll);
|
|
onLangChange(() => refreshInspector());
|
|
|
|
loadProjects().catch((error) => toast(error.message));
|
|
|
|
async function loadProjects() {
|
|
state.projects = await api("/projects");
|
|
if (!state.selectedProject && state.projects.length) {
|
|
state.selectedProject = state.projects[0].name;
|
|
}
|
|
renderProjects();
|
|
if (state.selectedProject) {
|
|
await selectProject(state.selectedProject);
|
|
}
|
|
}
|
|
|
|
async function selectProject(projectName) {
|
|
state.selectedProject = projectName;
|
|
state.projectDetail = await api(`/projects/${encodeURIComponent(projectName)}`);
|
|
state.ontology = await api(`/ontology/${encodeURIComponent(state.projectDetail.domain)}`);
|
|
renderProjects();
|
|
renderSourceOptions(state.projectDetail);
|
|
renderOverview();
|
|
renderOntology();
|
|
refreshInspector();
|
|
await refreshAll();
|
|
}
|
|
|
|
async function refreshAll() {
|
|
if (!state.selectedProject) return;
|
|
const loaders = [
|
|
["pipeline", loadPipeline],
|
|
["ontology registry", loadOntologyRegistry],
|
|
["entities", loadEntities],
|
|
["claims workbench", loadWorkbench],
|
|
["extraction logs", loadExtractionLogs],
|
|
["research sessions", loadResearchSessions],
|
|
["graph query", loadGraphQuery],
|
|
["tags", loadTags],
|
|
["graph", loadGraph],
|
|
];
|
|
await Promise.all(loaders.map(async ([label, fn]) => {
|
|
try { await fn(); } catch (error) { console.warn(`${label} load failed`, error); }
|
|
}));
|
|
refreshInspector();
|
|
}
|