"use client"; /** * V2StatusCount 설정 패널 * 토스식 단계별 UX: 데이터 소스 -> 컬럼 매핑 -> 상태 항목 관리 -> 표시 설정(접힘) * 기존 StatusCountConfigPanel의 모든 기능을 자체 UI로 완전 구현 */ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Separator } from "@/components/ui/separator"; import { Table2, Columns3, Check, ChevronsUpDown, Loader2, Link2, Plus, Trash2, BarChart3, Type, Maximize2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi, type EntityJoinConfig } from "@/lib/api/entityJoin"; import { apiClient } from "@/lib/api/client"; import type { StatusCountConfig, StatusCountItem } from "@/lib/registry/components/v2-status-count/types"; import { STATUS_COLOR_MAP } from "@/lib/registry/components/v2-status-count/types"; const COLOR_OPTIONS = Object.keys(STATUS_COLOR_MAP); // ─── 카드 크기 선택 카드 ─── const SIZE_CARDS = [ { value: "sm", title: "작게", description: "컴팩트" }, { value: "md", title: "보통", description: "기본 크기" }, { value: "lg", title: "크게", description: "넓은 카드" }, ] as const; // ─── 섹션 헤더 컴포넌트 ─── function SectionHeader({ icon: Icon, title, description }: { icon: React.ComponentType<{ className?: string }>; title: string; description?: string; }) { return (

{title}

{description &&

{description}

}
); } // ─── 수평 라벨 + 컨트롤 Row ─── function LabeledRow({ label, description, children }: { label: string; description?: string; children: React.ReactNode; }) { return (

{label}

{description &&

{description}

}
{children}
); } interface V2StatusCountConfigPanelProps { config: StatusCountConfig; onChange: (config: Partial) => void; } export const V2StatusCountConfigPanel: React.FC = ({ config, onChange, }) => { // componentConfigChanged 이벤트 발행 래퍼 const handleChange = useCallback((newConfig: Partial) => { onChange(newConfig); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { detail: { config: { ...config, ...newConfig } }, }) ); } }, [onChange, config]); const updateField = useCallback((key: keyof StatusCountConfig, value: any) => { handleChange({ [key]: value }); }, [handleChange]); // ─── 상태 ─── const [tables, setTables] = useState>([]); const [columns, setColumns] = useState>([]); const [entityJoins, setEntityJoins] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [loadingColumns, setLoadingColumns] = useState(false); const [loadingJoins, setLoadingJoins] = useState(false); const [statusCategoryValues, setStatusCategoryValues] = useState>([]); const [loadingCategoryValues, setLoadingCategoryValues] = useState(false); const [tableComboboxOpen, setTableComboboxOpen] = useState(false); const [statusColumnOpen, setStatusColumnOpen] = useState(false); const [relationOpen, setRelationOpen] = useState(false); const items = config.items || []; // ─── 테이블 목록 로드 ─── useEffect(() => { const loadTables = async () => { setLoadingTables(true); try { const result = await tableTypeApi.getTables(); setTables( (result || []).map((t: any) => ({ tableName: t.tableName || t.table_name, displayName: t.displayName || t.tableName || t.table_name, })) ); } catch (err) { console.error("테이블 목록 로드 실패:", err); } finally { setLoadingTables(false); } }; loadTables(); }, []); // ─── 선택된 테이블의 컬럼 + 엔티티 조인 로드 ─── useEffect(() => { if (!config.tableName) { setColumns([]); setEntityJoins([]); return; } const loadColumns = async () => { setLoadingColumns(true); try { const result = await tableTypeApi.getColumns(config.tableName); setColumns( (result || []).map((c: any) => ({ columnName: c.columnName || c.column_name, columnLabel: c.columnLabel || c.column_label || c.displayName || c.columnName || c.column_name, })) ); } catch (err) { console.error("컬럼 목록 로드 실패:", err); } finally { setLoadingColumns(false); } }; const loadEntityJoins = async () => { setLoadingJoins(true); try { const result = await entityJoinApi.getEntityJoinConfigs(config.tableName); setEntityJoins(result?.joinConfigs || []); } catch (err) { console.error("엔티티 조인 설정 로드 실패:", err); setEntityJoins([]); } finally { setLoadingJoins(false); } }; loadColumns(); loadEntityJoins(); }, [config.tableName]); // ─── 상태 컬럼의 카테고리 값 로드 ─── useEffect(() => { if (!config.tableName || !config.statusColumn) { setStatusCategoryValues([]); return; } const loadCategoryValues = async () => { setLoadingCategoryValues(true); try { const response = await apiClient.get( `/table-categories/${config.tableName}/${config.statusColumn}/values` ); if (response.data?.success && response.data?.data) { const flatValues: Array<{ value: string; label: string }> = []; const flatten = (categoryItems: any[]) => { for (const item of categoryItems) { flatValues.push({ value: item.valueCode || item.value_code, label: item.valueLabel || item.value_label, }); if (item.children?.length > 0) flatten(item.children); } }; flatten(response.data.data); setStatusCategoryValues(flatValues); } } catch { setStatusCategoryValues([]); } finally { setLoadingCategoryValues(false); } }; loadCategoryValues(); }, [config.tableName, config.statusColumn]); // ─── 엔티티 관계 Combobox 아이템 ─── const relationComboItems = useMemo(() => { return entityJoins.map((ej) => { const refTableLabel = tables.find((t) => t.tableName === ej.referenceTable)?.displayName || ej.referenceTable; return { value: `${ej.sourceColumn}::${ej.referenceTable}.${ej.referenceColumn}`, label: `${ej.sourceColumn} -> ${refTableLabel}`, sublabel: `${ej.referenceTable}.${ej.referenceColumn}`, }; }); }, [entityJoins, tables]); const currentRelationValue = useMemo(() => { if (!config.relationColumn) return ""; return relationComboItems.find((item) => { const [srcCol] = item.value.split("::"); return srcCol === config.relationColumn; })?.value || ""; }, [config.relationColumn, relationComboItems]); // ─── 상태 항목 관리 ─── const addItem = useCallback(() => { updateField("items", [...items, { value: "", label: "새 상태", color: "gray" }]); }, [items, updateField]); const removeItem = useCallback((index: number) => { updateField("items", items.filter((_: StatusCountItem, i: number) => i !== index)); }, [items, updateField]); const updateItem = useCallback((index: number, key: keyof StatusCountItem, value: string) => { const newItems = [...items]; newItems[index] = { ...newItems[index], [key]: value }; updateField("items", newItems); }, [items, updateField]); // ─── 테이블 변경 핸들러 ─── const handleTableChange = useCallback((newTableName: string) => { handleChange({ tableName: newTableName, statusColumn: "", relationColumn: "", parentColumn: "" }); setTableComboboxOpen(false); }, [handleChange]); // ─── 렌더링 ─── return (
{/* ═══════════════════════════════════════ */} {/* 1단계: 데이터 소스 (테이블 선택) */} {/* ═══════════════════════════════════════ */}
{/* 제목 */}
제목
updateField("title", e.target.value)} placeholder="예: 일련번호 현황" className="h-7 text-xs" />
{/* 테이블 선택 */} 테이블을 찾을 수 없습니다. {tables.map((table) => ( handleTableChange(table.tableName)} className="text-xs" >
{table.displayName} {table.displayName !== table.tableName && ( {table.tableName} )}
))}
{/* ═══════════════════════════════════════ */} {/* 2단계: 컬럼 매핑 */} {/* ═══════════════════════════════════════ */} {config.tableName && (
{/* 상태 컬럼 */}
상태 컬럼 * 컬럼을 찾을 수 없습니다. {columns.map((col) => ( { updateField("statusColumn", col.columnName); setStatusColumnOpen(false); }} className="text-xs" >
{col.columnLabel} {col.columnLabel !== col.columnName && ( {col.columnName} )}
))}
{/* 엔티티 관계 */}
엔티티 관계
{loadingJoins ? (
로딩중...
) : entityJoins.length > 0 ? ( 엔티티 관계가 없습니다. {relationComboItems.map((item) => ( { if (item.value === currentRelationValue) { handleChange({ relationColumn: "", parentColumn: "" }); } else { const [sourceCol, refPart] = item.value.split("::"); const [, refCol] = refPart.split("."); handleChange({ relationColumn: sourceCol, parentColumn: refCol }); } setRelationOpen(false); }} className="text-xs" >
{item.label} {item.sublabel}
))}
) : (

설정된 엔티티 관계가 없습니다

)} {config.relationColumn && config.parentColumn && (
자식 FK: {config.relationColumn} {" -> "} 부모 매칭: {config.parentColumn}
)}
)} {/* 테이블 미선택 안내 */} {!config.tableName && (

테이블이 선택되지 않았습니다

위 데이터 소스에서 테이블을 선택하세요

)} {/* ═══════════════════════════════════════ */} {/* 3단계: 카드 크기 (카드 선택 UI) */} {/* ═══════════════════════════════════════ */}
{SIZE_CARDS.map((card) => { const isSelected = (config.cardSize || "md") === card.value; return ( ); })}
{/* ═══════════════════════════════════════ */} {/* 4단계: 상태 항목 관리 */} {/* ═══════════════════════════════════════ */}
{items.length}개
{loadingCategoryValues && (
카테고리 값 로딩...
)} {items.length === 0 ? (

아직 상태 항목이 없어요

위의 추가 버튼으로 항목을 만들어보세요

) : (
{items.map((item: StatusCountItem, i: number) => (
{/* 첫 번째 줄: 상태값 + 삭제 */}
{statusCategoryValues.length > 0 ? ( ) : ( updateItem(i, "value", e.target.value)} placeholder="상태값 (예: IN_USE)" className="h-7 text-xs" /> )}
{/* 두 번째 줄: 라벨 + 색상 */}
updateItem(i, "label", e.target.value)} placeholder="표시 라벨" className="h-7 text-xs" />
))}
)} {!loadingCategoryValues && statusCategoryValues.length === 0 && config.tableName && config.statusColumn && (
카테고리 값이 없습니다. 옵션설정 > 카테고리설정에서 값을 추가하거나 직접 입력하세요.
)} {/* 미리보기 */} {items.length > 0 && (
미리보기
{items.map((item, i) => { const colors = STATUS_COLOR_MAP[item.color] || STATUS_COLOR_MAP.gray; return (
0 {item.label || "라벨"}
); })}
)}
); }; V2StatusCountConfigPanel.displayName = "V2StatusCountConfigPanel"; export default V2StatusCountConfigPanel;