"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Database, Type, Hash, Calendar, CheckSquare, List, AlignLeft, Code, Building, File, Link2, ChevronDown, ChevronRight, Plus, Search, X, } from "lucide-react"; import { TableInfo, WebType } from "@/types/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { tableManagementApi } from "@/lib/api/tableManagement"; interface EntityJoinColumn { columnName: string; columnLabel: string; dataType: string; inputType?: string; description?: string; } interface EntityJoinTable { tableName: string; currentDisplayColumn: string; availableColumns: EntityJoinColumn[]; } interface TablesPanelProps { tables: TableInfo[]; searchTerm: string; onSearchChange: (term: string) => void; onDragStart: (e: React.DragEvent, table: TableInfo, column?: any) => void; selectedTableName?: string; placedColumns?: Set; // 이미 배치된 컬럼명 집합 (tableName.columnName 형식) // 테이블 선택 관련 props onTableSelect?: (tableName: string) => void; // 테이블 선택 콜백 showTableSelector?: boolean; // 테이블 선택 UI 표시 여부 } // 위젯 타입별 아이콘 const getWidgetIcon = (widgetType: WebType) => { switch (widgetType) { case "text": case "email": case "tel": return ; case "number": case "decimal": return ; case "date": case "datetime": return ; case "select": case "dropdown": return ; case "textarea": case "text_area": return ; case "boolean": case "checkbox": return ; case "code": return ; case "entity": return ; case "file": return ; default: return ; } }; export const TablesPanel: React.FC = ({ tables, searchTerm, onDragStart, placedColumns = new Set(), onTableSelect, showTableSelector = false, }) => { // 엔티티 조인 컬럼 상태 const [entityJoinTables, setEntityJoinTables] = useState([]); const [loadingEntityJoins, setLoadingEntityJoins] = useState(false); const [expandedJoinTables, setExpandedJoinTables] = useState>(new Set()); // 전체 테이블 목록 (테이블 선택용) const [allTables, setAllTables] = useState>([]); const [loadingAllTables, setLoadingAllTables] = useState(false); const [tableSearchTerm, setTableSearchTerm] = useState(""); const [showTableSelectDropdown, setShowTableSelectDropdown] = useState(false); // 시스템 컬럼 목록 (숨김 처리) const systemColumns = new Set([ "id", "created_date", "updated_date", "writer", "company_code", ]); // 전체 테이블 목록 로드 const loadAllTables = useCallback(async () => { if (allTables.length > 0) return; // 이미 로드됨 setLoadingAllTables(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.displayName || t.table_label || t.tableName || t.table_name, }))); } } catch (error) { console.error("테이블 목록 조회 오류:", error); } finally { setLoadingAllTables(false); } }, [allTables.length]); // 테이블 선택 시 호출 const handleTableSelect = (tableName: string) => { setShowTableSelectDropdown(false); setTableSearchTerm(""); onTableSelect?.(tableName); }; // 필터링된 테이블 목록 const filteredAllTables = tableSearchTerm ? allTables.filter( (t) => t.tableName.toLowerCase().includes(tableSearchTerm.toLowerCase()) || t.displayName.toLowerCase().includes(tableSearchTerm.toLowerCase()) ) : allTables; // 메인 테이블명 추출 const mainTableName = tables[0]?.tableName; // 엔티티 조인 컬럼 로드 useEffect(() => { const fetchEntityJoinColumns = async () => { if (!mainTableName) { setEntityJoinTables([]); return; } setLoadingEntityJoins(true); try { const result = await entityJoinApi.getEntityJoinColumns(mainTableName); setEntityJoinTables(result.joinTables || []); // 기본적으로 모든 조인 테이블 펼치기 setExpandedJoinTables(new Set(result.joinTables?.map((t) => t.tableName) || [])); } catch (error) { console.error("엔티티 조인 컬럼 조회 오류:", error); setEntityJoinTables([]); } finally { setLoadingEntityJoins(false); } }; fetchEntityJoinColumns(); }, [mainTableName]); // 조인 테이블 펼치기/접기 토글 const toggleJoinTable = (tableName: string) => { setExpandedJoinTables((prev) => { const newSet = new Set(prev); if (newSet.has(tableName)) { newSet.delete(tableName); } else { newSet.add(tableName); } return newSet; }); }; // 엔티티 조인 컬럼 드래그 핸들러 const handleEntityJoinDragStart = ( e: React.DragEvent, joinTable: EntityJoinTable, column: EntityJoinColumn, ) => { // "테이블명.컬럼명" 형식으로 컬럼 정보 생성 const fullColumnName = `${joinTable.tableName}.${column.columnName}`; const columnData = { columnName: fullColumnName, columnLabel: column.columnLabel || column.columnName, dataType: column.dataType, widgetType: "text" as WebType, isEntityJoin: true, entityJoinTable: joinTable.tableName, entityJoinColumn: column.columnName, }; // 기존 테이블 정보를 기반으로 가상의 테이블 정보 생성 const virtualTable: TableInfo = { tableName: mainTableName || "", tableLabel: tables[0]?.tableLabel || mainTableName || "", columns: [columnData], }; onDragStart(e, virtualTable, columnData); }; // 이미 배치된 컬럼과 시스템 컬럼을 제외한 테이블 정보 생성 const tablesWithAvailableColumns = tables.map((table) => ({ ...table, columns: table.columns.filter((col) => { const columnKey = `${table.tableName}.${col.columnName}`; // 시스템 컬럼이거나 이미 배치된 컬럼은 제외 return !systemColumns.has(col.columnName.toLowerCase()) && !placedColumns.has(columnKey); }), })); // 검색어가 있으면 컬럼 필터링 const filteredTables = tablesWithAvailableColumns .map((table) => { if (!searchTerm) { return table; } const searchLower = searchTerm.toLowerCase(); // 테이블명이 검색어와 일치하면 모든 컬럼 표시 if ( table.tableName.toLowerCase().includes(searchLower) || (table.tableLabel && table.tableLabel.toLowerCase().includes(searchLower)) ) { return table; } // 그렇지 않으면 컬럼명/라벨이 검색어와 일치하는 컬럼만 필터링 const filteredColumns = table.columns.filter( (col) => col.columnName.toLowerCase().includes(searchLower) || (col.columnLabel && col.columnLabel.toLowerCase().includes(searchLower)), ); return { ...table, columns: filteredColumns, }; }) .filter((table) => table.columns.length > 0); // 컬럼이 있는 테이블만 표시 return (
{/* 테이블 선택 버튼 (메인 테이블이 없을 때 또는 showTableSelector가 true일 때) */} {(showTableSelector || tables.length === 0) && (
{/* 드롭다운 */} {showTableSelectDropdown && (
{/* 검색 */}
setTableSearchTerm(e.target.value)} autoFocus className="w-full rounded-md border px-8 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> {tableSearchTerm && ( )}
{/* 테이블 목록 */}
{loadingAllTables ? (
로드 중...
) : filteredAllTables.length === 0 ? (
{tableSearchTerm ? "검색 결과 없음" : "테이블 없음"}
) : ( filteredAllTables.map((t) => ( )) )}
)}
{/* 현재 테이블 정보 */} {tables.length > 0 && (
현재: {tables[0]?.tableLabel || tables[0]?.tableName}
)}
)} {/* 테이블과 컬럼 평면 목록 */}
{filteredTables.map((table) => (
{/* 테이블 헤더 */}
{table.tableLabel || table.tableName} {table.columns.length}개
{/* 컬럼 목록 (항상 표시) */}
{table.columns.map((column) => (
onDragStart(e, table, column)} > {getWidgetIcon(column.widgetType)}
{column.columnLabel || column.columnName}
{column.widgetType} {column.required && ( 필수 )}
))}
))} {/* 엔티티 조인 컬럼 섹션 */} {entityJoinTables.length > 0 && (
엔티티 조인 컬럼 {entityJoinTables.length}
{entityJoinTables.map((joinTable) => { const isExpanded = expandedJoinTables.has(joinTable.tableName); // 검색어로 필터링 const filteredColumns = searchTerm ? joinTable.availableColumns.filter( (col) => col.columnName.toLowerCase().includes(searchTerm.toLowerCase()) || col.columnLabel.toLowerCase().includes(searchTerm.toLowerCase()), ) : joinTable.availableColumns; // 검색 결과가 없으면 표시하지 않음 if (searchTerm && filteredColumns.length === 0) { return null; } return (
{/* 조인 테이블 헤더 */}
toggleJoinTable(joinTable.tableName)} >
{isExpanded ? ( ) : ( )} {joinTable.tableName} {filteredColumns.length}개
{/* 조인 컬럼 목록 */} {isExpanded && (
{filteredColumns.map((column) => { const fullColumnName = `${joinTable.tableName}.${column.columnName}`; const isPlaced = placedColumns.has(fullColumnName); if (isPlaced) return null; return (
handleEntityJoinDragStart(e, joinTable, column)} title="읽기 전용 - 조인된 테이블에서 참조" >
{column.columnLabel || column.columnName}
읽기 {column.inputType || "text"}
); })}
)}
); })}
)} {/* 로딩 표시 */} {loadingEntityJoins && (
엔티티 조인 컬럼 로드 중...
)}
); }; export default TablesPanel;