"use client"; /** * V2TableGrouped 설정 패널 * 토스식 단계별 UX: 데이터 소스 -> 그룹화 설정 -> 컬럼 선택 -> 표시 설정(접힘) -> 연동 설정(접힘) * 기존 TableGroupedConfigPanel의 모든 기능을 자체 UI로 완전 구현 */ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; 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, Database, Layers, Columns3, Check, ChevronsUpDown, Settings, ChevronDown, Loader2, Link2, Plus, Trash2, FoldVertical, ArrowUpDown, CheckSquare, LayoutGrid, Type, Hash, } from "lucide-react"; import { cn } from "@/lib/utils"; import { tableTypeApi } from "@/lib/api/screen"; import type { TableGroupedConfig, LinkedFilterConfig } from "@/lib/registry/components/v2-table-grouped/types"; import type { ColumnConfig } from "@/lib/registry/components/v2-table-list/types"; import { groupHeaderStyleOptions, checkboxModeOptions, sortDirectionOptions, } from "@/lib/registry/components/v2-table-grouped/config"; // ─── 섹션 헤더 컴포넌트 ─── function SectionHeader({ icon: Icon, title, description }: { icon: React.ComponentType<{ className?: string }>; title: string; description?: string; }) { return (

{title}

{description &&

{description}

}
); } // ─── 수평 Switch Row (토스 패턴) ─── function SwitchRow({ label, description, checked, onCheckedChange }: { label: string; description?: string; checked: boolean; onCheckedChange: (checked: boolean) => void; }) { return (

{label}

{description &&

{description}

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

{label}

{description &&

{description}

}
{children}
); } // ─── 그룹 헤더 스타일 카드 ─── const HEADER_STYLE_CARDS = [ { value: "default", icon: LayoutGrid, title: "기본", description: "표준 그룹 헤더" }, { value: "compact", icon: FoldVertical, title: "컴팩트", description: "간결한 헤더" }, { value: "card", icon: Layers, title: "카드", description: "카드 스타일 헤더" }, ] as const; interface V2TableGroupedConfigPanelProps { config: TableGroupedConfig; onChange: (newConfig: Partial) => void; } export const V2TableGroupedConfigPanel: 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 updateConfig = useCallback((updates: Partial) => { handleChange({ ...config, ...updates }); }, [handleChange, config]); const updateGroupConfig = useCallback((updates: Partial) => { handleChange({ ...config, groupConfig: { ...config.groupConfig, ...updates }, }); }, [handleChange, config]); // ─── 상태 ─── const [tables, setTables] = useState>([]); const [tableColumns, setTableColumns] = useState([]); const [loadingTables, setLoadingTables] = useState(false); const [loadingColumns, setLoadingColumns] = useState(false); const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // Collapsible 상태 const [displayOpen, setDisplayOpen] = useState(false); const [linkedOpen, setLinkedOpen] = useState(false); // ─── 실제 사용할 테이블 이름 ─── const targetTableName = useMemo(() => { if (config.useCustomTable && config.customTableName) { return config.customTableName; } return config.selectedTable; }, [config.useCustomTable, config.customTableName, config.selectedTable]); // ─── 테이블 목록 로드 ─── useEffect(() => { const loadTables = async () => { setLoadingTables(true); try { const tableList = await tableTypeApi.getTables(); if (tableList && Array.isArray(tableList)) { setTables( tableList.map((t: any) => ({ tableName: t.tableName || t.table_name, displayName: t.displayName || t.display_name || t.tableName || t.table_name, })) ); } } catch (err) { console.error("테이블 목록 로드 실패:", err); } finally { setLoadingTables(false); } }; loadTables(); }, []); // ─── 선택된 테이블의 컬럼 로드 ─── useEffect(() => { if (!targetTableName) { setTableColumns([]); return; } const loadColumns = async () => { setLoadingColumns(true); try { const columns = await tableTypeApi.getColumns(targetTableName); if (columns && Array.isArray(columns)) { const cols: ColumnConfig[] = columns.map((col: any, idx: number) => ({ columnName: col.column_name || col.columnName, displayName: col.display_name || col.displayName || col.column_name || col.columnName, visible: true, sortable: true, searchable: false, align: "left" as const, order: idx, })); setTableColumns(cols); if (!config.columns || config.columns.length === 0) { updateConfig({ columns: cols }); } } } catch (err) { console.error("컬럼 로드 실패:", err); } finally { setLoadingColumns(false); } }; loadColumns(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [targetTableName]); // ─── 테이블 변경 핸들러 ─── const handleTableChange = useCallback((newTableName: string) => { if (newTableName === config.selectedTable) return; updateConfig({ selectedTable: newTableName, columns: [] }); setTableComboboxOpen(false); }, [config.selectedTable, updateConfig]); // ─── 컬럼 가시성 토글 ─── const toggleColumnVisibility = useCallback((columnName: string) => { const updatedColumns = (config.columns || []).map((col) => col.columnName === columnName ? { ...col, visible: !col.visible } : col ); updateConfig({ columns: updatedColumns }); }, [config.columns, updateConfig]); // ─── 합계 컬럼 토글 ─── const toggleSumColumn = useCallback((columnName: string) => { const currentSumCols = config.groupConfig?.summary?.sumColumns || []; const newSumCols = currentSumCols.includes(columnName) ? currentSumCols.filter((c) => c !== columnName) : [...currentSumCols, columnName]; updateGroupConfig({ summary: { ...config.groupConfig?.summary, sumColumns: newSumCols, }, }); }, [config.groupConfig?.summary, updateGroupConfig]); // ─── 연결 필터 관리 ─── const addLinkedFilter = useCallback(() => { const newFilter: LinkedFilterConfig = { sourceComponentId: "", sourceField: "value", targetColumn: "", enabled: true, }; updateConfig({ linkedFilters: [...(config.linkedFilters || []), newFilter], }); }, [config.linkedFilters, updateConfig]); const removeLinkedFilter = useCallback((index: number) => { const filters = [...(config.linkedFilters || [])]; filters.splice(index, 1); updateConfig({ linkedFilters: filters }); }, [config.linkedFilters, updateConfig]); const updateLinkedFilter = useCallback((index: number, updates: Partial) => { const filters = [...(config.linkedFilters || [])]; filters[index] = { ...filters[index], ...updates }; updateConfig({ linkedFilters: filters }); }, [config.linkedFilters, updateConfig]); // ─── 렌더링 ─── return (
{/* ═══════════════════════════════════════ */} {/* 1단계: 데이터 소스 (테이블 선택) */} {/* ═══════════════════════════════════════ */}
updateConfig({ useCustomTable: checked })} /> {config.useCustomTable ? ( updateConfig({ customTableName: e.target.value })} placeholder="테이블명을 직접 입력하세요" className="h-8 text-xs" /> ) : ( { if (value.toLowerCase().includes(search.toLowerCase())) return 1; return 0; }} > 테이블을 찾을 수 없습니다. {tables.map((table) => ( handleTableChange(table.tableName)} className="text-xs" >
{table.displayName} {table.displayName !== table.tableName && ( {table.tableName} )}
))}
)}
{/* ═══════════════════════════════════════ */} {/* 2단계: 그룹화 설정 */} {/* ═══════════════════════════════════════ */} {targetTableName && (
{/* 그룹화 기준 컬럼 */} {/* 그룹 라벨 형식 */}
그룹 라벨 형식
updateGroupConfig({ groupLabelFormat: e.target.value })} placeholder="{value} ({컬럼명})" className="h-7 text-xs" />

{"{value}"} = 그룹값, {"{컬럼명}"} = 해당 컬럼 값

updateGroupConfig({ defaultExpanded: checked })} /> {/* 그룹 정렬 */} updateGroupConfig({ summary: { ...config.groupConfig?.summary, showCount: checked }, }) } /> {/* 합계 컬럼 */} {tableColumns.length > 0 && (
합계 표시 컬럼

그룹별 합계를 계산할 컬럼을 선택하세요

{tableColumns.map((col) => { const isChecked = config.groupConfig?.summary?.sumColumns?.includes(col.columnName) ?? false; return (
toggleSumColumn(col.columnName)} > toggleSumColumn(col.columnName)} className="pointer-events-none h-3.5 w-3.5" /> {col.displayName || col.columnName}
); })}
)}
)} {/* 테이블 미선택 안내 */} {!targetTableName && (

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

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

)} {/* ═══════════════════════════════════════ */} {/* 3단계: 컬럼 선택 */} {/* ═══════════════════════════════════════ */} {targetTableName && (config.columns || tableColumns).length > 0 && (
c.visible !== false).length}개 표시)`} description="표시할 컬럼을 선택하세요" />
{(config.columns || tableColumns).map((col) => { const isVisible = col.visible !== false; return (
toggleColumnVisibility(col.columnName)} > toggleColumnVisibility(col.columnName)} className="pointer-events-none h-3.5 w-3.5" /> {col.displayName || col.columnName}
); })}
)} {/* ═══════════════════════════════════════ */} {/* 4단계: 그룹 헤더 스타일 (카드 선택) */} {/* ═══════════════════════════════════════ */} {targetTableName && (
{HEADER_STYLE_CARDS.map((card) => { const Icon = card.icon; const isSelected = (config.groupHeaderStyle || "default") === card.value; return ( ); })}
)} {/* ═══════════════════════════════════════ */} {/* 5단계: 표시 설정 (기본 접힘) */} {/* ═══════════════════════════════════════ */}
{/* 체크박스 */}
체크박스
updateConfig({ showCheckbox: checked })} /> {config.showCheckbox && (
)}
{/* UI 옵션 */} updateConfig({ showExpandAllButton: checked })} /> updateConfig({ rowClickable: checked })} /> {/* 높이 및 메시지 */} updateConfig({ maxHeight: parseInt(e.target.value) || 600 })} min={200} max={2000} className="h-7 w-[100px] text-xs" />
빈 데이터 메시지 updateConfig({ emptyMessage: e.target.value })} placeholder="데이터가 없습니다." className="h-7 text-xs" />
{/* ═══════════════════════════════════════ */} {/* 6단계: 연동 설정 (기본 접힘) */} {/* ═══════════════════════════════════════ */}

다른 컴포넌트(검색필터 등)의 선택 값으로 이 테이블을 필터링합니다

{(config.linkedFilters || []).length === 0 ? (

연결된 필터가 없습니다

) : (
{(config.linkedFilters || []).map((filter, idx) => (
필터 #{idx + 1}
updateLinkedFilter(idx, { enabled: checked })} />
소스 컴포넌트 ID updateLinkedFilter(idx, { sourceComponentId: e.target.value })} placeholder="예: search-filter-1" className="h-6 text-xs" />
소스 필드 updateLinkedFilter(idx, { sourceField: e.target.value })} placeholder="value" className="h-6 text-xs" />
대상 컬럼
))}
)}
); }; V2TableGroupedConfigPanel.displayName = "V2TableGroupedConfigPanel"; export default V2TableGroupedConfigPanel;