"use client"; import React, { useState, useEffect, useCallback } from "react"; import { DndContext, closestCenter, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { Plus, Settings2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { Checkbox } from "@/components/ui/checkbox"; import type { ColumnConfig, SearchColumnConfig, GroupingConfig, ColumnDisplayConfig, EntityReferenceConfig } from "./types"; import { SortableColumnItem } from "./components/SortableColumnItem"; import { SearchableColumnSelect } from "./components/SearchableColumnSelect"; interface ColumnInfo { column_name: string; data_type: string; column_comment?: string; input_type?: string; web_type?: string; reference_table?: string; reference_column?: string; } // 참조 테이블 컬럼 정보 interface ReferenceColumnInfo { columnName: string; displayName: string; dataType: string; } interface ColumnConfigModalProps { open: boolean; onOpenChange: (open: boolean) => void; tableName: string; displayColumns: ColumnConfig[]; searchColumns?: SearchColumnConfig[]; grouping?: GroupingConfig; showSearch?: boolean; onSave: (config: { displayColumns: ColumnConfig[]; searchColumns: SearchColumnConfig[]; grouping: GroupingConfig; showSearch: boolean; }) => void; side: "left" | "right"; // 좌측/우측 패널 구분 } export const ColumnConfigModal: React.FC = ({ open, onOpenChange, tableName, displayColumns: initialDisplayColumns, searchColumns: initialSearchColumns, grouping: initialGrouping, showSearch: initialShowSearch, onSave, side, }) => { // 로컬 상태 (모달 내에서만 사용, 저장 시 부모로 전달) const [displayColumns, setDisplayColumns] = useState([]); const [searchColumns, setSearchColumns] = useState([]); const [grouping, setGrouping] = useState({ enabled: false, groupByColumn: "" }); const [showSearch, setShowSearch] = useState(false); // 컬럼 세부설정 모달 const [detailModalOpen, setDetailModalOpen] = useState(false); const [editingColumnIndex, setEditingColumnIndex] = useState(null); const [editingColumn, setEditingColumn] = useState(null); // 테이블 컬럼 목록 const [columns, setColumns] = useState([]); const [columnsLoading, setColumnsLoading] = useState(false); // 엔티티 참조 관련 상태 const [entityReferenceColumns, setEntityReferenceColumns] = useState>(new Map()); const [loadingEntityColumns, setLoadingEntityColumns] = useState>(new Set()); // 드래그 센서 const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) ); // 초기값 설정 useEffect(() => { if (open) { setDisplayColumns(initialDisplayColumns || []); setSearchColumns(initialSearchColumns || []); setGrouping(initialGrouping || { enabled: false, groupByColumn: "" }); setShowSearch(initialShowSearch || false); } }, [open, initialDisplayColumns, initialSearchColumns, initialGrouping, initialShowSearch]); // 테이블 컬럼 로드 (entity 타입 정보 포함) const loadColumns = useCallback(async () => { if (!tableName) { setColumns([]); return; } setColumnsLoading(true); try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns?size=200`); let columnList: any[] = []; if (response.data?.success && response.data?.data?.columns) { columnList = response.data.data.columns; } else if (Array.isArray(response.data?.data?.columns)) { columnList = response.data.data.columns; } else if (Array.isArray(response.data?.data)) { columnList = response.data.data; } // entity 타입 정보를 포함하여 변환 const transformedColumns = columnList.map((c: any) => ({ column_name: c.columnName ?? c.column_name ?? c.name ?? "", data_type: c.dataType ?? c.data_type ?? c.type ?? "", column_comment: c.displayName ?? c.column_comment ?? c.label ?? "", input_type: c.inputType ?? c.input_type ?? "", web_type: c.webType ?? c.web_type ?? "", reference_table: c.referenceTable ?? c.reference_table ?? "", reference_column: c.referenceColumn ?? c.reference_column ?? "", })); setColumns(transformedColumns); } catch (error) { console.error("컬럼 목록 로드 실패:", error); setColumns([]); } finally { setColumnsLoading(false); } }, [tableName]); // 엔티티 참조 테이블의 컬럼 목록 로드 const loadEntityReferenceColumns = useCallback(async (columnName: string, referenceTable: string) => { if (!referenceTable || entityReferenceColumns.has(columnName)) { return; } setLoadingEntityColumns(prev => new Set(prev).add(columnName)); try { const result = await entityJoinApi.getReferenceTableColumns(referenceTable); if (result?.columns) { setEntityReferenceColumns(prev => { const newMap = new Map(prev); newMap.set(columnName, result.columns); return newMap; }); } } catch (error) { console.error(`엔티티 참조 컬럼 로드 실패 (${referenceTable}):`, error); } finally { setLoadingEntityColumns(prev => { const newSet = new Set(prev); newSet.delete(columnName); return newSet; }); } }, [entityReferenceColumns]); useEffect(() => { if (open && tableName) { loadColumns(); } }, [open, tableName, loadColumns]); // 드래그 종료 핸들러 const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = displayColumns.findIndex((col, idx) => `col-${idx}` === active.id); const newIndex = displayColumns.findIndex((col, idx) => `col-${idx}` === over.id); if (oldIndex !== -1 && newIndex !== -1) { setDisplayColumns(arrayMove(displayColumns, oldIndex, newIndex)); } } }; // 컬럼 추가 const handleAddColumn = () => { setDisplayColumns([ ...displayColumns, { name: "", label: "", displayRow: side === "left" ? "name" : "info", sourceTable: tableName, }, ]); }; // 컬럼 삭제 const handleRemoveColumn = (index: number) => { setDisplayColumns(displayColumns.filter((_, i) => i !== index)); }; // 컬럼 업데이트 (entity 타입이면 참조 테이블 컬럼도 로드) const handleUpdateColumn = (index: number, updates: Partial) => { const newColumns = [...displayColumns]; newColumns[index] = { ...newColumns[index], ...updates }; setDisplayColumns(newColumns); // 컬럼명이 변경된 경우 entity 타입인지 확인하고 참조 테이블 컬럼 로드 if (updates.name) { const columnInfo = columns.find(c => c.column_name === updates.name); if (columnInfo && (columnInfo.input_type === 'entity' || columnInfo.web_type === 'entity')) { if (columnInfo.reference_table) { loadEntityReferenceColumns(updates.name, columnInfo.reference_table); } } } }; // 컬럼 세부설정 열기 (entity 타입이면 참조 테이블 컬럼도 로드) const handleOpenDetailSettings = (index: number) => { const column = displayColumns[index]; setEditingColumnIndex(index); setEditingColumn({ ...column }); setDetailModalOpen(true); // entity 타입인지 확인하고 참조 테이블 컬럼 로드 if (column.name) { const columnInfo = columns.find(c => c.column_name === column.name); if (columnInfo && (columnInfo.input_type === 'entity' || columnInfo.web_type === 'entity')) { if (columnInfo.reference_table) { loadEntityReferenceColumns(column.name, columnInfo.reference_table); } } } }; // 컬럼 세부설정 저장 const handleSaveDetailSettings = () => { if (editingColumnIndex !== null && editingColumn) { handleUpdateColumn(editingColumnIndex, editingColumn); } setDetailModalOpen(false); setEditingColumnIndex(null); setEditingColumn(null); }; // 검색 컬럼 추가 const handleAddSearchColumn = () => { setSearchColumns([...searchColumns, { columnName: "", label: "" }]); }; // 검색 컬럼 삭제 const handleRemoveSearchColumn = (index: number) => { setSearchColumns(searchColumns.filter((_, i) => i !== index)); }; // 검색 컬럼 업데이트 const handleUpdateSearchColumn = (index: number, columnName: string) => { const newColumns = [...searchColumns]; newColumns[index] = { ...newColumns[index], columnName }; setSearchColumns(newColumns); }; // 저장 const handleSave = () => { onSave({ displayColumns, searchColumns, grouping, showSearch, }); onOpenChange(false); }; // 엔티티 표시 컬럼 토글 const toggleEntityDisplayColumn = (selectedColumn: string) => { if (!editingColumn) return; const currentDisplayColumns = editingColumn.entityReference?.displayColumns || []; const newDisplayColumns = currentDisplayColumns.includes(selectedColumn) ? currentDisplayColumns.filter(col => col !== selectedColumn) : [...currentDisplayColumns, selectedColumn]; setEditingColumn({ ...editingColumn, entityReference: { ...editingColumn.entityReference, displayColumns: newDisplayColumns, } as EntityReferenceConfig, }); }; // 현재 편집 중인 컬럼이 entity 타입인지 확인 const getEditingColumnEntityInfo = useCallback(() => { if (!editingColumn?.name) return null; const columnInfo = columns.find(c => c.column_name === editingColumn.name); if (!columnInfo) return null; if (columnInfo.input_type !== 'entity' && columnInfo.web_type !== 'entity') return null; return { referenceTable: columnInfo.reference_table || '', referenceColumns: entityReferenceColumns.get(editingColumn.name) || [], isLoading: loadingEntityColumns.has(editingColumn.name), }; }, [editingColumn, columns, entityReferenceColumns, loadingEntityColumns]); // 이미 선택된 컬럼명 목록 (중복 선택 방지용) const selectedColumnNames = displayColumns.map((col) => col.name).filter(Boolean); return ( <> {side === "left" ? "좌측" : "우측"} 패널 컬럼 설정 표시할 컬럼을 추가하고 순서를 드래그로 변경할 수 있습니다. 표시 컬럼 그룹핑 검색 {/* 표시 컬럼 탭 */}
{displayColumns.length === 0 ? (

표시할 컬럼이 없습니다

) : ( `col-${idx}`)} strategy={verticalListSortingStrategy} >
{displayColumns.map((col, index) => (
handleOpenDetailSettings(index)} onRemove={() => handleRemoveColumn(index)} showGroupingSettings={grouping.enabled} /> {/* 컬럼 빠른 선택 (인라인) */} {!col.name && (
{ const colInfo = columns.find((c) => c.column_name === value); handleUpdateColumn(index, { name: value, label: colInfo?.column_comment || "", }); }} excludeColumns={selectedColumnNames} placeholder="컬럼을 선택하세요" />
)}
))}
)}
{/* 그룹핑 탭 (좌측 패널만) */}

동일한 값을 가진 행들을 하나로 그룹화합니다

setGrouping({ ...grouping, enabled: checked }) } />
{grouping.enabled && (
setGrouping({ ...grouping, groupByColumn: value }) } placeholder="그룹 기준 컬럼 선택" className="mt-1" />

예: item_id로 그룹핑하면 같은 품목의 데이터를 하나로 표시합니다

)}
{/* 검색 탭 */}

검색 입력창을 표시합니다

{showSearch && (
{searchColumns.length === 0 ? (
검색할 컬럼을 추가하세요
) : (
{searchColumns.map((searchCol, index) => (
handleUpdateSearchColumn(index, value)} placeholder="검색 컬럼 선택" className="flex-1" />
))}
)}
)}
{/* 컬럼 세부설정 모달 */} 컬럼 세부설정 {editingColumn?.label || editingColumn?.name || "컬럼"}의 표시 설정을 변경합니다. {editingColumn && (
{/* 기본 설정 */}

기본 설정

{ const colInfo = columns.find((c) => c.column_name === value); setEditingColumn({ ...editingColumn, name: value, label: colInfo?.column_comment || editingColumn.label, }); }} className="mt-1" />
setEditingColumn({ ...editingColumn, label: e.target.value }) } placeholder="라벨명 (미입력 시 컬럼명 사용)" className="mt-1 h-9" />
setEditingColumn({ ...editingColumn, width: e.target.value ? parseInt(e.target.value) : undefined, }) } placeholder="자동" className="mt-1 h-9" />
{/* 그룹핑/집계 설정 (그룹핑 활성화 시만) */} {grouping.enabled && (

그룹핑/집계 설정

배지는 여러 값을 태그 형태로 나란히 표시합니다

그룹핑 시 값을 집계합니다

setEditingColumn({ ...editingColumn, displayConfig: { displayType: editingColumn.displayConfig?.displayType || "text", aggregate: { enabled: checked, function: editingColumn.displayConfig?.aggregate?.function || "DISTINCT", }, }, }) } />
{editingColumn.displayConfig?.aggregate?.enabled && (
)}
)} {/* 엔티티 참조 설정 (entity 타입 컬럼일 때만 표시) */} {(() => { const entityInfo = getEditingColumnEntityInfo(); if (!entityInfo) return null; return (

엔티티 표시 컬럼

참조 테이블: {entityInfo.referenceTable}

{entityInfo.isLoading ? (
컬럼 정보 로딩 중...
) : entityInfo.referenceColumns.length === 0 ? (
참조 테이블의 컬럼 정보를 불러올 수 없습니다
) : (
{entityInfo.referenceColumns.map((col) => { const isSelected = (editingColumn.entityReference?.displayColumns || []).includes(col.columnName); return (
toggleEntityDisplayColumn(col.columnName)} > toggleEntityDisplayColumn(col.columnName)} />
{col.displayName || col.columnName} {col.columnName} ({col.dataType})
); })}
)} {(editingColumn.entityReference?.displayColumns || []).length > 0 && (

선택됨: {(editingColumn.entityReference?.displayColumns || []).length}개

{(editingColumn.entityReference?.displayColumns || []).map((colName) => { const colInfo = entityInfo.referenceColumns.find(c => c.columnName === colName); return ( {colInfo?.displayName || colName} ); })}
)}
); })()}
)}
); }; export default ColumnConfigModal;