"use client"; import React, { useState, useEffect, useMemo } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; 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 { Database, Table2, ChevronsUpDown, Check, LayoutGrid, LayoutList, Rows3, Plus, X, Type, Settings2, ChevronUp } from "lucide-react"; import { cn } from "@/lib/utils"; import { RepeatContainerConfig, SlotComponentConfig } from "./types"; import { tableTypeApi } from "@/lib/api/screen"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { DynamicComponentConfigPanel, hasComponentConfigPanel } from "@/lib/utils/getComponentConfigPanel"; interface RepeatContainerConfigPanelProps { config: RepeatContainerConfig; onChange: (config: Partial) => void; screenTableName?: string; } /** * 리피터 컨테이너 설정 패널 */ export function RepeatContainerConfigPanel({ config, onChange, screenTableName, }: RepeatContainerConfigPanelProps) { const [availableTables, setAvailableTables] = useState>([]); const [loadingTables, setLoadingTables] = useState(false); const [tableComboboxOpen, setTableComboboxOpen] = useState(false); // 컬럼 관련 상태 const [availableColumns, setAvailableColumns] = useState>([]); const [loadingColumns, setLoadingColumns] = useState(false); // 제목/설명 컬럼 콤보박스 상태 const [titleColumnOpen, setTitleColumnOpen] = useState(false); const [descriptionColumnOpen, setDescriptionColumnOpen] = useState(false); // 실제 사용할 테이블 이름 계산 const targetTableName = useMemo(() => { if (config.useCustomTable && config.customTableName) { return config.customTableName; } return config.tableName || screenTableName; }, [config.useCustomTable, config.customTableName, config.tableName, screenTableName]); // 화면 테이블명 자동 설정 (초기 한 번만) useEffect(() => { if (screenTableName && !config.tableName && !config.customTableName) { onChange({ tableName: screenTableName }); } }, [screenTableName, config.tableName, config.customTableName, onChange]); // 전체 테이블 목록 로드 useEffect(() => { const fetchTables = async () => { setLoadingTables(true); try { const response = await tableTypeApi.getTables(); setAvailableTables( response.map((table: any) => ({ tableName: table.tableName, displayName: table.displayName || table.tableName, })) ); } catch (error) { console.error("테이블 목록 가져오기 실패:", error); } finally { setLoadingTables(false); } }; fetchTables(); }, []); // 테이블 컬럼 로드 useEffect(() => { if (!targetTableName) { setAvailableColumns([]); return; } const fetchColumns = async () => { setLoadingColumns(true); try { const response = await tableManagementApi.getColumnList(targetTableName); // API 응답이 { data: { columns: [...] } } 또는 { data: [...] } 형태일 수 있음 const columnsData = response.data?.columns || response.data; if (response.success && columnsData && Array.isArray(columnsData)) { const columns = columnsData.map((col: any) => ({ columnName: col.columnName, displayName: col.displayName || col.columnLabel || col.columnName, })); setAvailableColumns(columns); } } catch (error) { console.error("컬럼 목록 가져오기 실패:", error); setAvailableColumns([]); } finally { setLoadingColumns(false); } }; fetchColumns(); }, [targetTableName, config.tableName, screenTableName, config.useCustomTable, config.customTableName]); return (
리피터 컨테이너 설정
{/* 데이터 소스 테이블 설정 */}

데이터 소스 테이블

반복 렌더링할 데이터의 테이블을 선택합니다


{/* 현재 선택된 테이블 표시 (카드 형태) */}
{config.customTableName || config.tableName || screenTableName || "테이블 미선택"}
{config.useCustomTable ? "커스텀 테이블" : "화면 기본 테이블"}
{/* 테이블 선택 Combobox */} 테이블을 찾을 수 없습니다 {/* 그룹 1: 화면 기본 테이블 */} {screenTableName && ( { onChange({ useCustomTable: false, customTableName: undefined, tableName: screenTableName, }); setTableComboboxOpen(false); }} className="text-xs cursor-pointer" > {screenTableName} )} {/* 그룹 2: 전체 테이블 */} {availableTables .filter((table) => table.tableName !== screenTableName) .map((table) => ( { onChange({ useCustomTable: true, customTableName: table.tableName, tableName: table.tableName, }); setTableComboboxOpen(false); }} className="text-xs cursor-pointer" > {table.displayName || table.tableName} ))}
{/* 데이터 소스 컴포넌트 연결 */}

데이터 소스 연결

테이블 리스트에서 선택한 데이터를 받아올 수 있습니다


{config.dataSourceType === "table-list" && (
onChange({ dataSourceComponentId: e.target.value })} placeholder="비우면 테이블명으로 자동 매칭" className="h-8 text-xs" />

비워두면 위에서 설정한 테이블명과 같은 테이블 리스트에서 데이터를 받습니다

)}
{/* 슬롯 컴포넌트 설정 */} {/* 레이아웃 설정 */}

레이아웃


{/* 레이아웃 타입 선택 */}
{/* 그리드 컬럼 수 (grid 레이아웃일 때만) */} {config.layout === "grid" && (
)} {/* 간격 */}
onChange({ gap: e.target.value })} placeholder="16px" className="h-8 text-xs" />
{/* 아이템 카드 설정 */}

아이템 카드 스타일


onChange({ backgroundColor: e.target.value })} className="h-8" />
onChange({ borderRadius: e.target.value })} placeholder="8px" className="h-8 text-xs" />
onChange({ padding: e.target.value })} placeholder="16px" className="h-8 text-xs" />
onChange({ itemHeight: e.target.value })} placeholder="auto" className="h-8 text-xs" />
onChange({ showBorder: checked as boolean })} />
onChange({ showShadow: checked as boolean })} />
{/* 아이템 제목/설명 설정 */}
onChange({ showItemTitle: checked as boolean })} />

{config.showItemTitle && (
{/* 제목 컬럼 선택 (Combobox) */}
컬럼을 찾을 수 없습니다 { onChange({ titleColumn: "" }); setTitleColumnOpen(false); }} className="text-xs" > 선택 안함 {availableColumns.map((col) => ( { onChange({ titleColumn: col.columnName }); setTitleColumnOpen(false); }} className="text-xs" >
{col.displayName || col.columnName} {col.displayName && col.displayName !== col.columnName && ( {col.columnName} )}
))}
{config.titleColumn && (

각 아이템의 "{config.titleColumn}" 값이 제목으로 표시됩니다

)}
{/* 설명 컬럼 선택 (Combobox) */}
컬럼을 찾을 수 없습니다 { onChange({ descriptionColumn: "" }); setDescriptionColumnOpen(false); }} className="text-xs" > 선택 안함 {availableColumns.map((col) => ( { onChange({ descriptionColumn: col.columnName }); setDescriptionColumnOpen(false); }} className="text-xs" >
{col.displayName || col.columnName} {col.displayName && col.displayName !== col.columnName && ( {col.columnName} )}
))}
{config.descriptionColumn && (

각 아이템의 "{config.descriptionColumn}" 값이 설명으로 표시됩니다

)}
{/* 제목 스타일 설정 */}
onChange({ titleColor: e.target.value })} className="h-7" />
{/* 설명 스타일 설정 */} {config.descriptionColumn && (
onChange({ descriptionColor: e.target.value })} className="h-7" />
)}
)}
{/* 페이징 설정 */}
onChange({ usePaging: checked as boolean })} />

{config.usePaging && (
)}
{/* 상호작용 설정 */}

상호작용


onChange({ clickable: checked as boolean })} />
{config.clickable && ( <>
onChange({ showSelectedState: checked as boolean })} />
)}
{/* 빈 상태 설정 */}

빈 상태 메시지


onChange({ emptyMessage: e.target.value })} placeholder="데이터가 없습니다" className="h-8 text-xs" />
); } // ============================================================ // 슬롯 자식 컴포넌트 관리 섹션 // ============================================================ interface SlotChildrenSectionProps { config: RepeatContainerConfig; onChange: (config: Partial) => void; availableColumns: Array<{ columnName: string; displayName?: string }>; loadingColumns: boolean; screenTableName?: string; } function SlotChildrenSection({ config, onChange, availableColumns, loadingColumns, screenTableName, }: SlotChildrenSectionProps) { const [columnComboboxOpen, setColumnComboboxOpen] = useState(false); const [expandedIds, setExpandedIds] = useState>(new Set()); const children = config.children || []; const toggleExpanded = (id: string) => { setExpandedIds((prev) => { const newSet = new Set(prev); if (newSet.has(id)) { newSet.delete(id); } else { newSet.add(id); } return newSet; }); }; const addComponent = (columnName: string, displayName: string) => { const newChild: SlotComponentConfig = { id: `slot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, componentType: "text-display", label: displayName, fieldName: columnName, position: { x: 0, y: children.length * 40 }, size: { width: 200, height: 32 }, componentConfig: {}, style: {}, }; onChange({ children: [...children, newChild], }); setColumnComboboxOpen(false); }; const removeComponent = (id: string) => { onChange({ children: children.filter((c) => c.id !== id), }); setExpandedIds((prev) => { const newSet = new Set(prev); newSet.delete(id); return newSet; }); }; const updateComponentLabel = (id: string, label: string) => { onChange({ children: children.map((c) => (c.id === id ? { ...c, label } : c)), }); }; const updateComponentConfig = (id: string, key: string, value: any) => { onChange({ children: children.map((c) => c.id === id ? { ...c, componentConfig: { ...c.componentConfig, [key]: value } } : c ), }); }; const updateComponentStyle = (id: string, key: string, value: any) => { onChange({ children: children.map((c) => c.id === id ? { ...c, style: { ...c.style, [key]: value } } : c ), }); }; const updateComponentSize = (id: string, width: number | undefined, height: number | undefined) => { onChange({ children: children.map((c) => c.id === id ? { ...c, size: { width: width ?? c.size?.width ?? 200, height: height ?? c.size?.height ?? 32 } } : c ), }); }; return (

반복 표시 필드

데이터 테이블의 컬럼을 선택하여 각 행에 표시할 필드를 추가합니다


{/* 추가된 필드 목록 */} {children.length > 0 ? (
{children.map((child, index) => { const isExpanded = expandedIds.has(child.id); return (
{/* 기본 정보 헤더 - 타입 선택 드롭다운 제거됨 */}
{index + 1}
{child.label || child.fieldName}
필드: {child.fieldName}
{/* 상세 설정 패널 */} {isExpanded && (
{hasComponentConfigPanel(child.componentType) ? ( { onChange({ children: children.map((c) => c.id === child.id ? { ...c, componentConfig: { ...c.componentConfig, ...newConfig } } : c ), }); }} onLabelChange={(label) => updateComponentLabel(child.id, label)} /> ) : ( <> {child.fieldName && (
바인딩: {child.fieldName}

각 아이템의 "{child.fieldName}" 값이 자동으로 표시됩니다

)}
updateComponentLabel(child.id, e.target.value)} placeholder="표시할 라벨" className="h-7 text-xs" />
updateComponentSize(child.id, parseInt(e.target.value) || 200, undefined) } className="h-7 text-xs" />
updateComponentSize(child.id, undefined, parseInt(e.target.value) || 32) } className="h-7 text-xs" />
updateComponentStyle(child.id, "color", e.target.value)} className="h-7" />
)}
)}
); })}
) : (
표시할 필드가 없습니다
아래 컬럼 목록에서 선택하세요
)} {/* 컬럼 선택 Combobox */}
컬럼을 찾을 수 없습니다 {availableColumns.map((col) => { const isAdded = children.some((c) => c.fieldName === col.columnName); return ( { if (!isAdded) { addComponent(col.columnName, col.displayName || col.columnName); } }} disabled={isAdded} className={cn( "text-xs cursor-pointer", isAdded && "opacity-50 cursor-not-allowed" )} >
{col.displayName || col.columnName}
{col.columnName}
{isAdded && ( )}
); })}
); } // 슬롯 컴포넌트 상세 설정 패널 interface SlotComponentDetailPanelProps { child: SlotComponentConfig; screenTableName?: string; onConfigChange: (newConfig: Record) => void; onLabelChange: (label: string) => void; } function SlotComponentDetailPanel({ child, screenTableName, onConfigChange, onLabelChange, }: SlotComponentDetailPanelProps) { return (
{child.fieldName && (
바인딩: {child.fieldName}

각 아이템의 "{child.fieldName}" 값이 자동으로 표시됩니다

)}
onLabelChange(e.target.value)} placeholder="표시할 라벨" className="h-7 text-xs" />
{child.componentType} 상세 설정
); }