"use client"; /** * V2CardDisplay 설정 패널 * 토스식 단계별 UX: 테이블 선택 -> 컬럼 매핑 -> 카드 스타일 -> 고급 설정(접힘) */ import React, { useState, useEffect, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; 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 { Badge } from "@/components/ui/badge"; import { Settings, ChevronDown, Database, ChevronsUpDown, Check, Plus, Trash2, Loader2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { tableManagementApi } from "@/lib/api/tableManagement"; // ─── 한 행당 카드 수 카드 정의 ─── const CARDS_PER_ROW_OPTIONS = [ { value: 1, label: "1개" }, { value: 2, label: "2개" }, { value: 3, label: "3개" }, { value: 4, label: "4개" }, { value: 5, label: "5개" }, { value: 6, label: "6개" }, ] as const; interface EntityJoinColumn { tableName: string; columnName: string; columnLabel: string; dataType: string; joinAlias: string; suggestedLabel: string; } interface JoinTable { tableName: string; currentDisplayColumn: string; joinConfig?: { sourceColumn: string }; availableColumns: Array<{ columnName: string; columnLabel: string; dataType: string; description?: string; }>; } interface V2CardDisplayConfigPanelProps { config: Record; onChange: (config: Record) => void; screenTableName?: string; tableColumns?: any[]; } export const V2CardDisplayConfigPanel: React.FC = ({ config, onChange, screenTableName, tableColumns = [], }) => { const [advancedOpen, setAdvancedOpen] = useState(false); const [tableComboboxOpen, setTableComboboxOpen] = useState(false); const [allTables, setAllTables] = useState>([]); const [loadingTables, setLoadingTables] = useState(false); const [availableColumns, setAvailableColumns] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [entityJoinColumns, setEntityJoinColumns] = useState<{ availableColumns: EntityJoinColumn[]; joinTables: JoinTable[]; }>({ availableColumns: [], joinTables: [] }); const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); const targetTableName = useMemo(() => { if (config.useCustomTable && config.customTableName) { return config.customTableName; } return config.tableName || screenTableName; }, [config.useCustomTable, config.customTableName, config.tableName, screenTableName]); const updateConfig = (field: string, value: any) => { const newConfig = { ...config, [field]: value }; onChange(newConfig); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { detail: { config: newConfig } }) ); } }; const updateNestedConfig = (path: string, value: any) => { const keys = path.split("."); const newConfig = { ...config }; let current = newConfig; for (let i = 0; i < keys.length - 1; i++) { if (!current[keys[i]]) current[keys[i]] = {}; current[keys[i]] = { ...current[keys[i]] }; current = current[keys[i]]; } current[keys[keys.length - 1]] = value; onChange(newConfig); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { detail: { config: newConfig } }) ); } }; // 테이블 목록 로드 useEffect(() => { const loadAllTables = async () => { setLoadingTables(true); try { const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setAllTables( response.data.map((t: any) => ({ tableName: t.tableName || t.table_name, displayName: t.tableLabel || t.displayName || t.tableName || t.table_name, })) ); } } catch { /* 무시 */ } finally { setLoadingTables(false); } }; loadAllTables(); }, []); // 컬럼 로드 useEffect(() => { const loadColumns = async () => { if (!targetTableName) { setAvailableColumns([]); return; } if (!config.useCustomTable && tableColumns && tableColumns.length > 0) { setAvailableColumns(tableColumns); return; } setLoadingColumns(true); try { const result = await tableManagementApi.getColumnList(targetTableName); if (result.success && result.data?.columns) { setAvailableColumns( result.data.columns.map((col: any) => ({ columnName: col.columnName, columnLabel: col.displayName || col.columnLabel || col.columnName, dataType: col.dataType, })) ); } } catch { setAvailableColumns([]); } finally { setLoadingColumns(false); } }; loadColumns(); }, [targetTableName, config.useCustomTable, tableColumns]); // 엔티티 조인 컬럼 로드 useEffect(() => { const fetchEntityJoinColumns = async () => { if (!targetTableName) { setEntityJoinColumns({ availableColumns: [], joinTables: [] }); return; } setLoadingEntityJoins(true); try { const result = await entityJoinApi.getEntityJoinColumns(targetTableName); setEntityJoinColumns({ availableColumns: result.availableColumns || [], joinTables: result.joinTables || [], }); } catch { setEntityJoinColumns({ availableColumns: [], joinTables: [] }); } finally { setLoadingEntityJoins(false); } }; fetchEntityJoinColumns(); }, [targetTableName]); const handleTableSelect = (selectedTable: string, isScreenTable: boolean) => { const newConfig = isScreenTable ? { ...config, useCustomTable: false, customTableName: undefined, tableName: selectedTable, columnMapping: { displayColumns: [] } } : { ...config, useCustomTable: true, customTableName: selectedTable, tableName: selectedTable, columnMapping: { displayColumns: [] } }; onChange(newConfig); setTableComboboxOpen(false); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { detail: { config: newConfig } }) ); } }; const getSelectedTableDisplay = () => { if (!targetTableName) return "테이블을 선택하세요"; const found = allTables.find((t) => t.tableName === targetTableName); return found?.displayName || targetTableName; }; const handleColumnSelect = (path: string, columnName: string) => { const joinColumn = entityJoinColumns.availableColumns.find( (col) => col.joinAlias === columnName ); if (joinColumn) { const joinColumnsConfig = config.joinColumns || []; const exists = joinColumnsConfig.find((jc: any) => jc.columnName === columnName); if (!exists) { const joinTableInfo = entityJoinColumns.joinTables?.find( (jt) => jt.tableName === joinColumn.tableName ); const newJoinColumnConfig = { columnName: joinColumn.joinAlias, label: joinColumn.suggestedLabel || joinColumn.columnLabel, sourceColumn: joinTableInfo?.joinConfig?.sourceColumn || "", referenceTable: joinColumn.tableName, referenceColumn: joinColumn.columnName, isJoinColumn: true, }; const newConfig = { ...config, columnMapping: { ...config.columnMapping, [path.split(".")[1]]: columnName }, joinColumns: [...joinColumnsConfig, newJoinColumnConfig], }; onChange(newConfig); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { detail: { config: newConfig } }) ); } return; } } updateNestedConfig(path, columnName); }; // 표시 컬럼 관리 const addDisplayColumn = () => { const current = config.columnMapping?.displayColumns || []; updateNestedConfig("columnMapping.displayColumns", [...current, ""]); }; const removeDisplayColumn = (index: number) => { const current = [...(config.columnMapping?.displayColumns || [])]; current.splice(index, 1); updateNestedConfig("columnMapping.displayColumns", current); }; const updateDisplayColumn = (index: number, value: string) => { const current = [...(config.columnMapping?.displayColumns || [])]; current[index] = value; const joinColumn = entityJoinColumns.availableColumns.find( (col) => col.joinAlias === value ); if (joinColumn) { const joinColumnsConfig = config.joinColumns || []; const exists = joinColumnsConfig.find((jc: any) => jc.columnName === value); if (!exists) { const joinTableInfo = entityJoinColumns.joinTables?.find( (jt) => jt.tableName === joinColumn.tableName ); const newConfig = { ...config, columnMapping: { ...config.columnMapping, displayColumns: current }, joinColumns: [ ...joinColumnsConfig, { columnName: joinColumn.joinAlias, label: joinColumn.suggestedLabel || joinColumn.columnLabel, sourceColumn: joinTableInfo?.joinConfig?.sourceColumn || "", referenceTable: joinColumn.tableName, referenceColumn: joinColumn.columnName, isJoinColumn: true, }, ], }; onChange(newConfig); if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("componentConfigChanged", { detail: { config: newConfig } }) ); } return; } } updateNestedConfig("columnMapping.displayColumns", current); }; // 테이블별 조인 컬럼 그룹화 const joinColumnsByTable: Record = {}; entityJoinColumns.availableColumns.forEach((col) => { if (!joinColumnsByTable[col.tableName]) joinColumnsByTable[col.tableName] = []; joinColumnsByTable[col.tableName].push(col); }); const currentTableColumns = config.useCustomTable ? availableColumns : tableColumns.length > 0 ? tableColumns : availableColumns; // 컬럼 선택 Select 렌더링 const renderColumnSelect = ( value: string, onChangeHandler: (value: string) => void, placeholder: string = "컬럼 선택" ) => ( ); return (
{/* ─── 1단계: 테이블 선택 ─── */}

데이터 소스

테이블을 찾을 수 없어요 {screenTableName && ( handleTableSelect(screenTableName, true)} className="text-xs" > {allTables.find((t) => t.tableName === screenTableName)?.displayName || screenTableName} )} {allTables .filter((t) => t.tableName !== screenTableName) .map((table) => ( handleTableSelect(table.tableName, false)} className="text-xs" > {table.displayName} ))} {config.useCustomTable && (

화면 기본 테이블이 아닌 다른 테이블의 데이터를 표시해요

)}
{/* ─── 2단계: 컬럼 매핑 ─── */} {(currentTableColumns.length > 0 || loadingColumns) && (

컬럼 매핑

{(loadingEntityJoins || loadingColumns) && (
{loadingColumns ? "컬럼 로딩 중..." : "조인 컬럼 로딩 중..."}
)}
타이틀
{renderColumnSelect( config.columnMapping?.titleColumn || "", (value) => handleColumnSelect("columnMapping.titleColumn", value) )}
서브타이틀
{renderColumnSelect( config.columnMapping?.subtitleColumn || "", (value) => handleColumnSelect("columnMapping.subtitleColumn", value) )}
설명
{renderColumnSelect( config.columnMapping?.descriptionColumn || "", (value) => handleColumnSelect("columnMapping.descriptionColumn", value) )}
이미지
{renderColumnSelect( config.columnMapping?.imageColumn || "", (value) => handleColumnSelect("columnMapping.imageColumn", value) )}
{/* 표시 컬럼 */}
추가 표시 컬럼
{(config.columnMapping?.displayColumns || []).length > 0 ? (
{(config.columnMapping?.displayColumns || []).map( (column: string, index: number) => (
{renderColumnSelect(column, (value) => updateDisplayColumn(index, value) )}
) )}
) : (
추가 버튼으로 표시할 컬럼을 추가해요
)}
)} {/* ─── 3단계: 카드 레이아웃 ─── */}

카드 레이아웃

한 행당 카드 수
카드 간격 (px) updateConfig("cardSpacing", parseInt(e.target.value))} className="h-7 w-[100px] text-xs" />
{/* ─── 4단계: 표시 요소 토글 ─── */}

표시 요소

타이틀

카드 상단 제목

updateNestedConfig("cardStyle.showTitle", checked)} />

서브타이틀

제목 아래 보조 텍스트

updateNestedConfig("cardStyle.showSubtitle", checked) } />

설명

카드 본문 텍스트

updateNestedConfig("cardStyle.showDescription", checked) } />

이미지

카드 이미지 영역

updateNestedConfig("cardStyle.showImage", checked) } />

액션 버튼

상세보기, 편집, 삭제 버튼

updateNestedConfig("cardStyle.showActions", checked) } />
{(config.cardStyle?.showActions ?? true) && (
상세보기 updateNestedConfig("cardStyle.showViewButton", checked) } />
편집 updateNestedConfig("cardStyle.showEditButton", checked) } />
삭제 updateNestedConfig("cardStyle.showDeleteButton", checked) } />
)}
{/* ─── 5단계: 고급 설정 (기본 접혀있음) ─── */}
설명 최대 길이 updateNestedConfig( "cardStyle.maxDescriptionLength", parseInt(e.target.value) ) } className="h-7 w-[100px] text-xs" />

비활성화

카드 상호작용을 비활성화해요

updateConfig("disabled", checked)} />

읽기 전용

데이터 수정을 막아요

updateConfig("readonly", checked)} />
); }; V2CardDisplayConfigPanel.displayName = "V2CardDisplayConfigPanel"; export default V2CardDisplayConfigPanel;