From 8681ac8ed319b5d626c9a82e88d67ce1f02b9cd5 Mon Sep 17 00:00:00 2001 From: lasta Date: Thu, 14 May 2026 14:46:03 +0900 Subject: [PATCH] =?UTF-8?q?Phase=201.1:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=E2=80=94=20=ED=8F=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20+=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20JSON=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 백엔드 (crawler_platform/app/api/routes.py): - POST /projects/inline 추가: 파일 시스템 의존 없이 JSON 본문으로 ProjectConfig 인라인 생성 - CreateProjectInlineRequest + InlineSourceConfig Pydantic 모델 - GET /domains 추가: 미리 정의된 도메인 목록 (perfume, tea, coffee, candle, supplement, gift) - 각 도메인의 entity_types, predicates, attribute_count 반환 프론트엔드 API 레이어 (src/lib/api/): - domains.ts: domainsApi.list + Zod 스키마 - ontology.ts: ontologyApi.get(domain) + Zod 스키마 - projects.ts: createInline() 메서드 + CreateProjectInlineRequest 타입 TanStack Query 훅 (src/hooks/): - useDomains: 도메인 목록 (30분 staleTime) - useOntology(domain): 도메인 상세 - useCreateProjectInline: 인라인 생성 mutation + projects 캐시 무효화 - queryKeys에 domains, ontology 키 팩토리 추가 UI 프리미티브 (src/components/ui/): - input.tsx, label.tsx, textarea.tsx OnboardingPage 완전 교체 (src/pages/OnboardingPage.tsx): - react-hook-form + zod 검증 (project_name 정규식, 도메인 필수) - 도메인 카드 그리드 선택 (entity/predicate 개수 미리보기) - 백엔드 /projects/inline POST → 성공 시 sonner 토스트 + /sources/{name}으로 이동 - 로딩/에러/재시도 UI 기타: - Vite proxy에 /domains prefix 추가 - i18n locale 키 onboarding.* 추가 (한/영) 다음 단계: Phase 1.2 — 온톨로지 엔티티/클레임 직접 입력 + URL 추출 Co-Authored-By: Claude Opus 4.7 --- crawler_platform/app/api/routes.py | 61 ++++- .../frontend/public/locales/en/common.json | 14 +- .../frontend/public/locales/ko/common.json | 14 +- .../web/frontend/src/components/ui/input.tsx | 21 ++ .../web/frontend/src/components/ui/label.tsx | 17 ++ .../frontend/src/components/ui/textarea.tsx | 20 ++ .../app/web/frontend/src/hooks/queryKeys.ts | 9 + .../app/web/frontend/src/hooks/useDomains.ts | 21 ++ .../app/web/frontend/src/hooks/useProjects.ts | 12 + .../app/web/frontend/src/lib/api/domains.ts | 17 ++ .../app/web/frontend/src/lib/api/index.ts | 2 + .../app/web/frontend/src/lib/api/ontology.ts | 20 ++ .../app/web/frontend/src/lib/api/projects.ts | 25 ++ .../web/frontend/src/pages/OnboardingPage.tsx | 252 ++++++++++++++++-- .../app/web/frontend/vite.config.js | 1 + 15 files changed, 474 insertions(+), 32 deletions(-) create mode 100644 crawler_platform/app/web/frontend/src/components/ui/input.tsx create mode 100644 crawler_platform/app/web/frontend/src/components/ui/label.tsx create mode 100644 crawler_platform/app/web/frontend/src/components/ui/textarea.tsx create mode 100644 crawler_platform/app/web/frontend/src/hooks/useDomains.ts create mode 100644 crawler_platform/app/web/frontend/src/lib/api/domains.ts create mode 100644 crawler_platform/app/web/frontend/src/lib/api/ontology.ts diff --git a/crawler_platform/app/api/routes.py b/crawler_platform/app/api/routes.py index 5a8fbb3..5387007 100644 --- a/crawler_platform/app/api/routes.py +++ b/crawler_platform/app/api/routes.py @@ -7,7 +7,7 @@ from fastapi import BackgroundTasks, HTTPException from pydantic import BaseModel, Field from sqlalchemy import select -from crawler_platform.app.config.loader import load_project_config +from crawler_platform.app.config.loader import ProjectConfig, SourceConfig, load_project_config from crawler_platform.app.core.crawler.discovery import discover_links from crawler_platform.app.core.crawler.fetchers import RobotsPolicy, make_fetcher from crawler_platform.app.core.crawler.pipeline import CrawlPipeline @@ -17,7 +17,7 @@ from crawler_platform.app.core.database.repository import KnowledgeRepository from crawler_platform.app.core.database.session import session_scope from crawler_platform.app.core.extractor.ai_provider import list_openai_compatible_models from crawler_platform.app.core.extractor.factory import extractor_for_domain -from crawler_platform.app.core.ontology.definitions import ontology_for_domain +from crawler_platform.app.core.ontology.definitions import DOMAIN_ONTOLOGIES, ontology_for_domain from crawler_platform.app.core.ontology.gap_detector import KnowledgeGapDetector from crawler_platform.app.core.ontology.mapper import ontology_to_dict from crawler_platform.app.core.ontology.registry import OntologyRegistry @@ -65,6 +65,43 @@ class CreateProjectRequest(BaseModel): config_path: str +class InlineSourceConfig(BaseModel): + name: str + type: str = "unknown" + trust_level: float = 0.5 + base_url: str | None = None + allowed_paths: list[str] = Field(default_factory=list) + parser: str = "generic" + fetcher: str = "requests" + rate_limit_per_minute: int = 30 + respect_robots_txt: bool = False + + +class CreateProjectInlineRequest(BaseModel): + """Create a project from inline JSON config (no filesystem dependency).""" + + project_name: str + domain: str + target_entities: list[str] = Field(default_factory=list) + fields: list[str] = Field(default_factory=list) + sources: list[InlineSourceConfig] = Field(default_factory=list) + ontology: dict[str, Any] = Field(default_factory=dict) + recommendation: dict[str, Any] = Field(default_factory=dict) + update_policy: dict[str, Any] = Field(default_factory=dict) + + def to_project_config(self) -> ProjectConfig: + return ProjectConfig( + project_name=self.project_name, + domain=self.domain, + target_entities=list(self.target_entities), + fields=list(self.fields), + sources=[SourceConfig(**s.model_dump()) for s in self.sources], + ontology=dict(self.ontology), + recommendation=dict(self.recommendation), + update_policy=dict(self.update_policy), + ) + + class ResetProjectRequest(BaseModel): config_path: str project_name: str | None = None @@ -298,6 +335,26 @@ def register_routes(app, database_url: str) -> None: project = KnowledgeRepository(session).upsert_project(config) return {"id": project.id, "name": project.name, "domain": project.domain} + @app.post("/projects/inline") + def create_project_inline(request: CreateProjectInlineRequest): + config = request.to_project_config() + with session_scope(database_url) as session: + project = KnowledgeRepository(session).upsert_project(config) + return {"id": project.id, "name": project.name, "domain": project.domain} + + @app.get("/domains") + def list_domains(): + """Available pre-defined ontology domains for project creation.""" + return [ + { + "domain": ont.domain, + "entity_types": list(ont.entity_types), + "predicates": list(ont.predicates), + "attribute_count": len(ont.attributes), + } + for ont in DOMAIN_ONTOLOGIES.values() + ] + @app.post("/projects/reset") def reset_project(request: ResetProjectRequest): config = load_project_config(request.config_path) diff --git a/crawler_platform/app/web/frontend/public/locales/en/common.json b/crawler_platform/app/web/frontend/public/locales/en/common.json index 91a7d4e..ee86693 100644 --- a/crawler_platform/app/web/frontend/public/locales/en/common.json +++ b/crawler_platform/app/web/frontend/public/locales/en/common.json @@ -5,7 +5,7 @@ }, "nav": { "dashboard": "Dashboard", - "onboard": "Upload Ontology", + "onboard": "New Project", "sources": "Sources", "crawl": "Crawl", "review": "Review", @@ -23,6 +23,18 @@ "hint": "Create your first project to start building an ontology" } }, + "onboarding": { + "title": "Create New Project", + "formTitle": "Choose Ontology Domain", + "formDesc": "Pick the domain of ontology you want to build and give your project a name.", + "projectName": "Project Name", + "projectNameHint": "Letters, digits, _ and - only (2~64 chars)", + "domain": "Domain", + "domainSummary": "{{entities}} entity types · {{predicates}} predicates", + "submit": "Create Project", + "created": "Project created: {{name}}", + "createFailed": "Create failed: {{msg}}" + }, "common": { "retry": "Retry", "cancel": "Cancel", diff --git a/crawler_platform/app/web/frontend/public/locales/ko/common.json b/crawler_platform/app/web/frontend/public/locales/ko/common.json index 274c0a1..d7009bb 100644 --- a/crawler_platform/app/web/frontend/public/locales/ko/common.json +++ b/crawler_platform/app/web/frontend/public/locales/ko/common.json @@ -5,7 +5,7 @@ }, "nav": { "dashboard": "대시보드", - "onboard": "온톨로지 업로드", + "onboard": "프로젝트 생성", "sources": "참고 소스", "crawl": "크롤 진행", "review": "결과 검토", @@ -23,6 +23,18 @@ "hint": "첫 프로젝트를 만들어 온톨로지 구축을 시작하세요" } }, + "onboarding": { + "title": "새 프로젝트 만들기", + "formTitle": "온톨로지 도메인 선택", + "formDesc": "어떤 종류의 온톨로지를 구축할지 도메인을 선택하고 프로젝트 이름을 정해주세요.", + "projectName": "프로젝트 이름", + "projectNameHint": "영문, 숫자, _ , - 만 사용 (2~64자)", + "domain": "도메인", + "domainSummary": "엔티티 {{entities}}종 · 관계 {{predicates}}개", + "submit": "프로젝트 만들기", + "created": "프로젝트가 생성되었습니다: {{name}}", + "createFailed": "생성 실패: {{msg}}" + }, "common": { "retry": "다시 시도", "cancel": "취소", diff --git a/crawler_platform/app/web/frontend/src/components/ui/input.tsx b/crawler_platform/app/web/frontend/src/components/ui/input.tsx new file mode 100644 index 0000000..98efa75 --- /dev/null +++ b/crawler_platform/app/web/frontend/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export type InputProps = React.InputHTMLAttributes; + +export const Input = React.forwardRef( + ({ className, type = "text", ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; diff --git a/crawler_platform/app/web/frontend/src/components/ui/label.tsx b/crawler_platform/app/web/frontend/src/components/ui/label.tsx new file mode 100644 index 0000000..73f6885 --- /dev/null +++ b/crawler_platform/app/web/frontend/src/components/ui/label.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export const Label = React.forwardRef< + HTMLLabelElement, + React.LabelHTMLAttributes +>(({ className, ...props }, ref) => ( +