"use client"; import { useEffect, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Check, Lock, ChevronDown, ChevronUp, Monitor, Sliders, CalendarClock, Workflow, Shield, Database, } from "lucide-react"; import { getTableGroups } from "@/lib/api/provisioning"; /** * Step 2 · 템플릿 그룹 선택 (시안 v2 포팅). * 왼쪽: 카드 리스트 (필수 / 선택 subheader 로 분리, 각 카드에 아이콘·체크·확장) * 오른쪽: SUMMARY 패널 (선택된 그룹 · 복제될 테이블 · 예상 레코드 · 예상 소요 시간) * * 로직 (유지): * - /table-groups 로드 * - g.required 는 서버가 강제하므로 UI 도 disabled + 항상 체크 * - selected_groups 에는 선택 그룹(required=false) 만 저장 * - valid = true 항상 */ export default function Step2Template({ state, setState, onValidChange, }: { state: Record; setState: (patch: Record) => void; onValidChange: (valid: boolean) => void; }) { const { data: groups = [], isLoading } = useQuery({ queryKey: ["provisioning-table-groups"], queryFn: getTableGroups, staleTime: 60_000, }); const [expanded, setExpanded] = useState(null); const selected: string[] = state.selected_groups || []; useEffect(() => { if (!state.selected_groups) setState({ selected_groups: [] }); }, []); // eslint-disable-line useEffect(() => { onValidChange(true); }, [onValidChange]); function toggle(id: string, required: boolean) { if (required) return; const next = selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id]; setState({ selected_groups: next }); } const required = groups.filter((g: any) => g.required); const optional = groups.filter((g: any) => !g.required); const selectedGroups = groups.filter((g: any) => g.required || selected.includes(g.id)); const totalTables = selectedGroups.reduce( (a: number, g: any) => a + (Array.isArray(g.tables) ? g.tables.length : 0), 0, ); const totalRows = selectedGroups.reduce((a: number, g: any) => a + (Number(g.count) || 0), 0); const estimatedSec = 10 + selectedGroups.length * 3; return (
02 · 템플릿 선택

복사할 기준 데이터

기본 회사(INVION_DEFAULT) 의 기준 데이터 중 새 회사에 복제할 항목을 선택합니다. 필수 그룹은 해제할 수 없습니다.
{isLoading && (
로딩 중...
)}
{required.length > 0 && ( <> 필수 (해제 불가) {required.map((g: any) => ( {}} expanded={expanded === g.id} onExpand={() => setExpanded(expanded === g.id ? null : g.id)} /> ))} )} {optional.length > 0 && ( <> 선택 {optional.map((g: any) => ( toggle(g.id, false)} expanded={expanded === g.id} onExpand={() => setExpanded(expanded === g.id ? null : g.id)} /> ))} )}
{/* 요약 패널 */}
요약
); } /* ─── 카드 ─────────────────────────────────────────────── */ const ICON_MAP: Record> = { screen: Monitor, control: Sliders, batch: CalendarClock, dataflow: Workflow, authmenu: Shield, menu: Shield, auth: Shield, }; function TemplateCard({ g, checked, onToggle, expanded, onExpand, }: { g: Record; checked: boolean; onToggle: () => void; expanded: boolean; onExpand: () => void; }) { const locked = !!g.required; const Ic = ICON_MAP[g.id] || ICON_MAP[g.key] || Database; const tables: string[] = Array.isArray(g.tables) ? g.tables : []; const meta: string[] = []; meta.push(`${tables.length}개 테이블`); if (g.count != null) meta.push(`${Number(g.count).toLocaleString()}건`); if (g.size) meta.push(String(g.size)); return (
!locked && onToggle()} style={{ padding: "1rem 1.15rem", display: "flex", alignItems: "center", gap: "0.85rem", cursor: locked ? "not-allowed" : "pointer", }} > {/* checkbox */}
{checked && }
{/* icon tile */}
{/* label */}
{g.label || g.id} {locked && ( 필수 )}
{meta.join(" · ")}
{tables.length > 0 && ( )}
0 ? "1fr" : "0fr", transition: "grid-template-rows 0.3s cubic-bezier(0.4,0,0.2,1)", }} >
{tables.length > 0 && (
{tables.map((t) => ( {t} ))}
)}
); } function SubHead({ children, style }: { children: React.ReactNode; style?: React.CSSProperties }) { return (
{children}
); } function SummaryRow({ label, value, mono, accent, isLast, }: { label: string; value: React.ReactNode; mono?: boolean; accent?: "cyan"; isLast: boolean; }) { return (
{label} {value}
); }