import { useNavigate, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { toast } from "sonner"; import { AlertCircle, ArrowLeft, ArrowRight, ExternalLink, Loader2, Plus, Trash2, } from "lucide-react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { useProject } from "@/hooks/useProjects"; import { useCreateSource, useDeleteSource } from "@/hooks/useSources"; const SOURCE_TYPES = [ "official", "review", "blog", "news", "community", "unknown", ] as const; const sourceSchema = z.object({ name: z .string() .min(2, "최소 2자 이상") .max(64, "최대 64자") .regex(/^[a-zA-Z0-9_-]+$/, "영문/숫자/_/-만 허용"), type: z.enum(SOURCE_TYPES), base_url: z .string() .url("올바른 URL 형식이 아닙니다") .or(z.literal("")) .optional(), trust_level: z .number({ invalid_type_error: "0~1 사이의 숫자" }) .min(0) .max(1), rate_limit_per_minute: z .number({ invalid_type_error: "양의 정수" }) .int() .min(1) .max(600), respect_robots_txt: z.boolean(), }); type SourceFormValues = z.infer; export default function ConfigureSourcesPage() { const navigate = useNavigate(); const { projectId } = useParams<{ projectId: string }>(); const { t } = useTranslation(); const projectName = projectId ?? ""; const { data: project, isLoading, isError, error, refetch, } = useProject(projectName); const createSource = useCreateSource(projectName); const deleteSource = useDeleteSource(projectName); const { register, handleSubmit, reset, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(sourceSchema), defaultValues: { name: "", type: "official", base_url: "", trust_level: 0.7, rate_limit_per_minute: 30, respect_robots_txt: true, }, }); const onAddSource = async (values: SourceFormValues) => { try { await createSource.mutateAsync({ ...values, base_url: values.base_url || null, }); toast.success( t("sources.added", "소스가 추가되었습니다: {{name}}", { name: values.name, }), ); reset(); } catch (e) { toast.error( t("sources.addFailed", "추가 실패: {{msg}}", { msg: (e as Error).message, }), ); } }; const onDelete = async (sourceName: string) => { if ( !confirm( t("sources.confirmDelete", "정말 '{{name}}' 소스를 삭제하시겠습니까?", { name: sourceName, }), ) ) { return; } try { await deleteSource.mutateAsync(sourceName); toast.success( t("sources.deleted", "소스가 삭제되었습니다: {{name}}", { name: sourceName, }), ); } catch (e) { toast.error( t("sources.deleteFailed", "삭제 실패: {{msg}}", { msg: (e as Error).message, }), ); } }; return (

{t("sources.title", "참고 소스 설정")}

{project && (

{project.name}{" "} · {project.domain}

)}
{isError && (
{(error as Error).message}
)}
{t("sources.listTitle", "등록된 소스")} {t( "sources.listDesc", "프로젝트 온톨로지 구축에 사용할 참고 사이트 목록", )} {isLoading && (
{Array.from({ length: 3 }).map((_, i) => ( ))}
)} {project && project.sources.length === 0 && (

{t( "sources.empty", "아직 등록된 소스가 없습니다. 오른쪽 폼에서 추가하세요.", )}

)} {project && project.sources.length > 0 && (
    {project.sources.map((s) => (
  • {s.name} {s.type} {typeof s.trust_level === "number" && ( {t("sources.trust", "신뢰도")}{" "} {s.trust_level.toFixed(2)} )}
    {s.base_url && ( {s.base_url} )}
  • ))}
)}
); }