Phase 1.2: 참고 소스 관리 — 소스 CRUD + UI

백엔드 (crawler_platform/app/api/routes.py):
- POST /projects/{name}/sources: 프로젝트에 소스 추가/업데이트
  - InlineSourceConfig 재사용, KnowledgeRepository.upsert_source 호출
- DELETE /projects/{name}/sources/{source_name}: 소스 삭제
  - 404 처리 + 외래 키 정리
- GET /projects/{name} 응답에 base_url 필드 추가

프론트엔드 API 레이어 (src/lib/api/):
- sources.ts: sourcesApi.create/delete + Zod 스키마 (Source, deleteSourceResponseSchema)

TanStack Query 훅 (src/hooks/):
- useSources.ts: useCreateSource, useDeleteSource (mutation + 캐시 무효화)
- queryKeys에 sources.byProject 키 팩토리

ConfigureSourcesPage 완전 교체 (src/pages/):
- 2열 레이아웃: 등록된 소스 목록 + 소스 추가 폼
- react-hook-form + zod 검증 (name 정규식, type enum, trust 0~1, rate_limit 1~600)
- 소스 카드 표시: type 배지, 신뢰도, base_url 링크, 삭제 버튼
- 빈 상태/로딩 스켈레톤/에러+재시도 모두 처리
- "크롤 진행" 버튼은 소스 1개 이상일 때만 활성
- sonner 토스트 알림

i18n locale 키 sources.* 추가 (한/영)

다음 단계: Phase 1.3 — URL 시드 크롤 (CrawlPage 실제 구현)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
lasta
2026-05-14 14:53:39 +09:00
parent 8681ac8ed3
commit 461ebc062b
8 changed files with 558 additions and 26 deletions

View File

@@ -407,6 +407,7 @@ def register_routes(app, database_url: str) -> None:
"id": source.id,
"name": source.name,
"type": source.type,
"base_url": source.base_url,
"trust_level": source.trust_level,
"respect_robots_txt": source.respect_robots_txt,
"rate_limit_per_minute": source.rate_limit_per_minute,
@@ -415,6 +416,42 @@ def register_routes(app, database_url: str) -> None:
],
}
@app.post("/projects/{project_name}/sources")
def add_project_source(project_name: str, request: InlineSourceConfig):
"""Add or update a source on an existing project."""
with session_scope(database_url) as session:
repo = KnowledgeRepository(session)
project = repo.get_project(project_name)
source = repo.upsert_source(project, SourceConfig(**request.model_dump()))
return {
"id": source.id,
"name": source.name,
"type": source.type,
"base_url": source.base_url,
"trust_level": source.trust_level,
"respect_robots_txt": source.respect_robots_txt,
"rate_limit_per_minute": source.rate_limit_per_minute,
}
@app.delete("/projects/{project_name}/sources/{source_name}")
def delete_project_source(project_name: str, source_name: str):
with session_scope(database_url) as session:
repo = KnowledgeRepository(session)
project = repo.get_project(project_name)
source = session.scalar(
select(models.Source).where(
models.Source.project_id == project.id,
models.Source.name == source_name,
)
)
if source is None:
raise HTTPException(
status_code=404,
detail=f"Source '{source_name}' not found in project '{project_name}'",
)
session.delete(source)
return {"ok": True, "deleted": source_name}
@app.get("/ontology/{domain}")
def ontology(domain: str):
return ontology_to_dict(ontology_for_domain(domain))