"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { Database, Link2, Columns3, Key, Save, Plus, Pencil, Trash2, RefreshCw, Loader2, Check, ChevronsUpDown, Table2, ArrowRight, Eye, Settings2, Settings, Monitor, ExternalLink, Type, Hash, Calendar, ToggleLeft, FileText, Search, List, X, } from "lucide-react"; import { getFieldJoins, createFieldJoin, updateFieldJoin, deleteFieldJoin, FieldJoin, } from "@/lib/api/screenGroup"; import { tableManagementApi, ColumnTypeInfo, TableInfo, ColumnSettings } from "@/lib/api/tableManagement"; import { screenApi } from "@/lib/api/screen"; import { INPUT_TYPE_OPTIONS } from "@/types/input-types"; import TableManagementPage from "@/app/(main)/admin/systemMng/tableMngList/page"; // ============================================================ // 타입 정의 // ============================================================ interface JoinColumnRef { column: string; refTable: string; refTableLabel?: string; refColumn: string; } interface ReferencedBy { fromTable: string; fromTableLabel?: string; fromColumn: string; toColumn: string; } interface ColumnInfo { column: string; label?: string; type?: string; isPK?: boolean; isFK?: boolean; refTable?: string; refColumn?: string; } interface ScreenUsingTable { screenId: number; screenName: string; screenCode?: string; tableRole: string; // main, filter, join } interface TableSettingModalProps { isOpen: boolean; onClose: () => void; tableName: string; tableLabel?: string; screenId?: number; joinColumnRefs?: JoinColumnRef[]; referencedBy?: ReferencedBy[]; columns?: ColumnInfo[]; filterColumns?: string[]; onSaveSuccess?: () => void; } // 검색 가능한 Select 컴포넌트 interface SearchableSelectProps { value: string; onValueChange: (value: string) => void; options: Array<{ value: string; label: string; description?: string }>; placeholder?: string; disabled?: boolean; className?: string; } function SearchableSelect({ value, onValueChange, options, placeholder = "선택...", disabled = false, className, }: SearchableSelectProps) { const [open, setOpen] = useState(false); const selectedOption = options.find((opt) => opt.value === value); return ( 결과 없음 {options.map((option) => ( { onValueChange(option.value); setOpen(false); }} className="text-xs" >
{option.label} {option.description && ( {option.description} )}
))}
); } // 입력 타입별 아이콘 function getInputTypeIcon(inputType: string) { switch (inputType) { case "text": return ; case "number": return ; case "date": case "datetime": return ; case "boolean": case "checkbox": return ; case "textarea": return ; case "entity": return ; case "code": return ; default: return ; } } // ============================================================ // 메인 모달 컴포넌트 // ============================================================ export function TableSettingModal({ isOpen, onClose, tableName, tableLabel, screenId, joinColumnRefs = [], referencedBy = [], columns = [], filterColumns = [], onSaveSuccess, }: TableSettingModalProps) { const [activeTab, setActiveTab] = useState("columns"); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [tableColumns, setTableColumns] = useState([]); const [tables, setTables] = useState([]); const [fieldJoins, setFieldJoins] = useState([]); const [screensUsingTable, setScreensUsingTable] = useState([]); // 선택된 컬럼 const [selectedColumn, setSelectedColumn] = useState(null); // 컬럼 설정 편집 상태 const [editedColumns, setEditedColumns] = useState>>({}); // 테이블 라벨/설명 const [editedTableLabel, setEditedTableLabel] = useState(tableLabel || ""); const [editedTableDescription, setEditedTableDescription] = useState(""); // 참조 테이블 컬럼 캐시 const [refTableColumns, setRefTableColumns] = useState>({}); const [loadingRefColumns, setLoadingRefColumns] = useState(false); // 테이블 타입 관리 모달 상태 const [showTableManagementModal, setShowTableManagementModal] = useState(false); // 테이블 컬럼 정보 로드 const loadTableData = useCallback(async () => { if (!tableName) return; setLoading(true); try { // 테이블 목록 로드 const tablesResponse = await tableManagementApi.getTableList(); if (tablesResponse.success && tablesResponse.data) { setTables(tablesResponse.data); } // 테이블 컬럼 로드 (column_labels 정보 포함) const columnsResponse = await tableManagementApi.getColumnList(tableName); if (columnsResponse.success && columnsResponse.data?.columns) { // 백엔드 응답은 camelCase로 옴 const columnsData = columnsResponse.data.columns; setTableColumns(columnsData); // 초기 편집 상태 설정 const initialEdits: Record> = {}; columnsData.forEach((col) => { initialEdits[col.columnName] = { displayName: col.displayName, inputType: col.inputType || "direct", referenceTable: col.referenceTable, referenceColumn: col.referenceColumn, displayColumn: col.displayColumn, }; }); setEditedColumns(initialEdits); } // 필드 조인 로드 (screenId가 있는 경우) if (screenId) { const joinsResponse = await getFieldJoins(screenId); if (joinsResponse.success && joinsResponse.data) { const relevantJoins = joinsResponse.data.filter( (j) => j.save_table === tableName || j.join_table === tableName ); setFieldJoins(relevantJoins); } } // 이 테이블을 사용하는 화면 목록 로드 await loadScreensUsingTable(); } catch (error) { console.error("테이블 정보 로드 실패:", error); toast.error("테이블 정보를 불러오는데 실패했습니다."); } finally { setLoading(false); } }, [tableName, screenId]); // 이 테이블을 사용하는 화면 목록 로드 const loadScreensUsingTable = useCallback(async () => { if (!tableName) return; try { // 모든 화면 조회 const screensResponse = await screenApi.getScreens({ size: 1000 }); if (screensResponse.items) { const usingScreens: ScreenUsingTable[] = []; screensResponse.items.forEach((screen: any) => { // 메인 테이블로 사용하는 경우 if (screen.tableName === tableName) { usingScreens.push({ screenId: screen.screenId, screenName: screen.screenName, screenCode: screen.screenCode, tableRole: "main", }); } // TODO: 필터 테이블, 조인 테이블로 사용하는 경우도 추가 }); setScreensUsingTable(usingScreens); } } catch (error) { console.error("화면 목록 로드 실패:", error); } }, [tableName]); // 참조 테이블 컬럼 로드 const loadRefTableColumns = useCallback(async (refTableName: string) => { if (!refTableName || refTableName === "none" || refTableColumns[refTableName]) return; setLoadingRefColumns(true); try { const response = await tableManagementApi.getColumnList(refTableName); if (response.success && response.data?.columns) { setRefTableColumns((prev) => ({ ...prev, [refTableName]: response.data!.columns, })); } } catch (error) { console.error("참조 테이블 컬럼 로드 실패:", error); } finally { setLoadingRefColumns(false); } }, [refTableColumns]); useEffect(() => { if (isOpen && tableName) { loadTableData(); setEditedTableLabel(tableLabel || tableName); } }, [isOpen, tableName, tableLabel, loadTableData]); // 참조 테이블 변경 시 컬럼 로드 useEffect(() => { Object.values(editedColumns).forEach((col) => { if (col.referenceTable && col.referenceTable !== "none") { loadRefTableColumns(col.referenceTable); } }); }, [editedColumns, loadRefTableColumns]); // 새로고침 const handleRefresh = () => { loadTableData(); toast.success("새로고침 완료"); }; // 컬럼 설정 변경 핸들러 const handleColumnChange = (columnName: string, field: string, value: any) => { setEditedColumns((prev) => ({ ...prev, [columnName]: { ...prev[columnName], [field]: value, }, })); // 참조 테이블 변경 시 참조 컬럼 초기화 if (field === "referenceTable") { setEditedColumns((prev) => ({ ...prev, [columnName]: { ...prev[columnName], referenceColumn: "", displayColumn: "", }, })); if (value && value !== "none") { loadRefTableColumns(value); } } }; // 전체 저장 (테이블타입관리 페이지와 동일한 로직) const handleSaveAll = async () => { setSaving(true); try { // 변경된 컬럼들만 저장 for (const [columnName, editedSettings] of Object.entries(editedColumns)) { // 기존 컬럼 정보 찾기 const originalColumn = tableColumns.find((c) => c.columnName === columnName); if (!originalColumn) continue; // 기존 값과 편집된 값 병합 const mergedColumn = { ...originalColumn, ...editedSettings, }; // detailSettings 처리 (Entity 타입인 경우) let finalDetailSettings = mergedColumn.detailSettings || ""; if (mergedColumn.inputType === "entity" && mergedColumn.referenceTable) { // 기존 detailSettings를 파싱하거나 새로 생성 let existingSettings: Record = {}; if (typeof mergedColumn.detailSettings === "string" && mergedColumn.detailSettings.trim().startsWith("{")) { try { existingSettings = JSON.parse(mergedColumn.detailSettings); } catch { existingSettings = {}; } } // 엔티티 설정 추가 const entitySettings = { ...existingSettings, entityTable: mergedColumn.referenceTable, entityCodeColumn: mergedColumn.referenceColumn || "id", entityLabelColumn: mergedColumn.displayColumn || "name", placeholder: (existingSettings.placeholder as string) || "항목을 선택하세요", searchable: existingSettings.searchable ?? true, }; finalDetailSettings = JSON.stringify(entitySettings); console.log("Entity 설정 JSON 생성:", entitySettings); } // Code 타입인 경우 hierarchyRole을 detailSettings에 포함 if (mergedColumn.inputType === "code" && (mergedColumn as any).hierarchyRole) { let existingSettings: Record = {}; if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) { try { existingSettings = JSON.parse(finalDetailSettings); } catch { existingSettings = {}; } } const codeSettings = { ...existingSettings, hierarchyRole: (mergedColumn as any).hierarchyRole, }; finalDetailSettings = JSON.stringify(codeSettings); console.log("Code 계층 역할 설정 JSON 생성:", codeSettings); } // ColumnSettings 인터페이스에 맞게 데이터 구성 const columnSetting: ColumnSettings = { columnName: columnName, columnLabel: mergedColumn.displayName || originalColumn.displayName || "", webType: mergedColumn.inputType || originalColumn.inputType || "text", detailSettings: finalDetailSettings, codeCategory: mergedColumn.codeCategory || originalColumn.codeCategory || "", codeValue: mergedColumn.codeValue || originalColumn.codeValue || "", referenceTable: mergedColumn.referenceTable || "", referenceColumn: mergedColumn.referenceColumn || "", displayColumn: mergedColumn.displayColumn || "", }; console.log("저장할 컬럼 설정:", columnSetting); // API 호출 const response = await tableManagementApi.updateColumnSettings(tableName, columnName, columnSetting); if (!response.success) { console.error(`컬럼 '${columnName}' 저장 실패:`, response.message); toast.error(`컬럼 '${columnName}' 저장 실패: ${response.message}`); return; } } toast.success("테이블 설정이 저장되었습니다."); setEditedColumns({}); // 편집 상태 초기화 onSaveSuccess?.(); await loadTableData(); } catch (error) { console.error("저장 실패:", error); toast.error("저장에 실패했습니다."); } finally { setSaving(false); } }; // 컬럼 정보 통합 const mergedColumns = useMemo(() => { const columnsMap = new Map(); // API에서 가져온 컬럼 정보 (camelCase) tableColumns.forEach((tcol) => { columnsMap.set(tcol.columnName, { ...tcol, isPK: tcol.isPrimaryKey, isFK: false, // 백엔드에서 isForeignKey를 제공하지 않으므로 false 기본값 }); }); return Array.from(columnsMap.values()); }, [tableColumns]); // 선택된 컬럼 정보 const selectedColumnInfo = useMemo(() => { if (!selectedColumn) return null; return mergedColumns.find((c) => c.columnName === selectedColumn); }, [selectedColumn, mergedColumns]); // 테이블 옵션 const tableOptions = useMemo( () => [ { value: "none", label: "-- 선택 안함 --" }, ...tables.map((t) => ({ value: t.tableName, label: t.displayName || t.tableName, description: t.tableName, })), ], [tables] ); // 입력 타입 옵션 const inputTypeOptions = useMemo( () => INPUT_TYPE_OPTIONS.map((opt) => ({ value: opt.value, label: opt.label, })), [] ); // 참조 테이블 컬럼 옵션 const getRefColumnOptions = (refTable: string) => { const cols = refTableColumns[refTable] || []; return [ { value: "", label: "-- 선택 안함 --" }, ...cols.map((c) => ({ value: c.columnName, label: c.displayName || c.columnName, description: c.dataType, })), ]; }; return ( <>
테이블 설정: {tableLabel || tableName} {tableName !== tableLabel && tableName !== (tableLabel || tableName) && ( ({tableName}) )}
테이블의 컬럼 설정, 조인 관계, 화면 연동 현황을 확인하고 설정합니다.
{/* 좌측: 탭 (40%) */}
컬럼 설정 화면 연동 참조 관계
{/* 탭 1: 컬럼 설정 */} {/* 탭 2: 화면 연동 */} {/* 탭 3: 참조 관계 */}
{/* 우측: 컬럼 상세 설정 (60%) */}
{selectedColumn && selectedColumnInfo ? ( handleColumnChange(selectedColumn, field, value)} /> ) : (

왼쪽에서 컬럼을 선택하면

상세 설정을 할 수 있습니다.

)}
{/* 테이블 타입 관리 전체 화면 모달 */}
{/* 헤더 */}
테이블 타입 관리

데이터베이스 테이블과 컬럼의 타입을 관리합니다

{/* TableManagementPage */}
); } // ============================================================ // 탭 1: 컬럼 목록 // ============================================================ interface ColumnListTabProps { columns: (ColumnTypeInfo & { isPK?: boolean; isFK?: boolean })[]; editedColumns: Record>; selectedColumn: string | null; onSelectColumn: (columnName: string) => void; loading: boolean; } function ColumnListTab({ columns, editedColumns, selectedColumn, onSelectColumn, loading, }: ColumnListTabProps) { const [searchTerm, setSearchTerm] = useState(""); const filteredColumns = useMemo(() => { if (!searchTerm) return columns; const term = searchTerm.toLowerCase(); return columns.filter( (col) => col.columnName.toLowerCase().includes(term) || (col.displayName || "").toLowerCase().includes(term) ); }, [columns, searchTerm]); if (loading) { return (
); } return (
{/* 검색 */}
setSearchTerm(e.target.value)} className="h-9 pl-9 text-sm" />
{/* 통계 */}
전체 {columns.length}개 PK {columns.filter((c) => c.isPK).length}개 FK {columns.filter((c) => c.isFK).length}개
{/* 컬럼 목록 */}
{filteredColumns.length === 0 ? (
{searchTerm ? "검색 결과가 없습니다." : "컬럼이 없습니다."}
) : (
{filteredColumns.map((col) => { const edited = editedColumns[col.columnName] || {}; const inputType = (edited.inputType || col.inputType || "text") as string; const isSelected = selectedColumn === col.columnName; return (
onSelectColumn(col.columnName)} className={cn( "cursor-pointer rounded-lg border p-3 transition-colors", isSelected ? "border-green-300 bg-green-50" : "border-transparent bg-background hover:bg-muted/50" )} >
{getInputTypeIcon(inputType)} {edited.displayName || col.displayName || col.columnName}
{col.isPK && ( PK )} {col.isFK && ( FK )} {(edited.referenceTable || col.referenceTable) && ( 조인 )}
{col.columnName} {col.dataType}
); })}
)}
); } // ============================================================ // 컬럼 상세 설정 패널 // ============================================================ interface ColumnDetailPanelProps { columnInfo: ColumnTypeInfo & { isPK?: boolean; isFK?: boolean }; editedColumn: Partial; tableOptions: Array<{ value: string; label: string; description?: string }>; inputTypeOptions: Array<{ value: string; label: string }>; getRefColumnOptions: (refTable: string) => Array<{ value: string; label: string; description?: string }>; loadingRefColumns: boolean; onColumnChange: (field: string, value: any) => void; } function ColumnDetailPanel({ columnInfo, editedColumn, tableOptions, inputTypeOptions, getRefColumnOptions, loadingRefColumns, onColumnChange, }: ColumnDetailPanelProps) { const currentLabel = editedColumn.displayName ?? columnInfo.displayName ?? ""; const currentInputType = (editedColumn.inputType ?? columnInfo.inputType ?? "text") as string; const currentRefTable = editedColumn.referenceTable ?? columnInfo.referenceTable ?? ""; const currentRefColumn = editedColumn.referenceColumn ?? columnInfo.referenceColumn ?? ""; const currentDisplayColumn = editedColumn.displayColumn ?? columnInfo.displayColumn ?? ""; return (
{/* 헤더 */}

컬럼 설정

{columnInfo.columnName} {columnInfo.dataType} {columnInfo.isPK && ( Primary Key )} {columnInfo.isFK && ( Foreign Key )}
{/* 설정 폼 */}
{/* 기본 정보 */}

기본 정보

onColumnChange("displayName", e.target.value)} placeholder={columnInfo.columnName} className="h-9 text-sm" />

화면에 표시될 컬럼 이름입니다.

데이터 입력 시 사용할 컴포넌트 유형입니다.

{/* 조인 설정 (Entity 타입일 때만) */} {currentInputType === "entity" && (

Entity 조인 설정

onColumnChange("referenceTable", v === "none" ? "" : v)} options={tableOptions} placeholder="테이블 선택" />
{currentRefTable && currentRefTable !== "none" && ( <>
{loadingRefColumns ? (
컬럼 로딩 중...
) : ( onColumnChange("referenceColumn", v)} options={getRefColumnOptions(currentRefTable)} placeholder="컬럼 선택" /> )}

이 컬럼과 연결될 참조 테이블의 컬럼입니다.

{loadingRefColumns ? (
컬럼 로딩 중...
) : ( onColumnChange("displayColumn", v)} options={getRefColumnOptions(currentRefTable)} placeholder="표시 컬럼 선택" /> )}

화면에 실제로 표시될 참조 테이블의 컬럼입니다.

)}
)} {/* 컬럼 정보 (읽기 전용) */}

컬럼 정보

데이터 타입

{columnInfo.dataType}

NULL 허용

{columnInfo.isNullable === "YES" ? "예" : "아니오"}

{columnInfo.maxLength && (
최대 길이

{columnInfo.maxLength}

)} {columnInfo.defaultValue && (
기본값

{columnInfo.defaultValue}

)}
); } // ============================================================ // 탭 2: 화면 연동 현황 // ============================================================ interface ScreensTabProps { screensUsingTable: ScreenUsingTable[]; loading: boolean; } function ScreensTab({ screensUsingTable, loading }: ScreensTabProps) { if (loading) { return (
); } return (

이 테이블을 사용하는 화면 ({screensUsingTable.length}개)

{screensUsingTable.length === 0 ? (

이 테이블을 사용하는 화면이 없습니다.

) : (
{screensUsingTable.map((screen) => (

{screen.screenName}

{screen.screenCode && (

{screen.screenCode}

)}
{screen.tableRole === "main" ? "메인 테이블" : screen.tableRole === "filter" ? "필터 테이블" : "조인 테이블"}
))}
)}
); } // ============================================================ // 탭 3: 참조 관계 // ============================================================ interface ReferenceTabProps { tableName: string; tableLabel?: string; referencedBy: ReferencedBy[]; joinColumnRefs: JoinColumnRef[]; loading: boolean; } function ReferenceTab({ tableName, tableLabel, referencedBy, joinColumnRefs, loading, }: ReferenceTabProps) { if (loading) { return (
); } return (
{/* 이 테이블이 참조하는 테이블 */}

이 테이블이 참조하는 테이블 ({joinColumnRefs.length}개)

{joinColumnRefs.length > 0 ? (
{joinColumnRefs.map((ref, idx) => (
{ref.column}
{ref.refTableLabel || ref.refTable} .{ref.refColumn}
))}
) : (
참조하는 테이블이 없습니다.
)}
{/* 이 테이블을 참조하는 테이블 */}

이 테이블을 참조하는 테이블 ({referencedBy.length}개)

{referencedBy.length > 0 ? (
{referencedBy.map((ref, idx) => (
{ref.fromTableLabel || ref.fromTable} .{ref.fromColumn}
{ref.toColumn}
))}
) : (
이 테이블을 참조하는 테이블이 없습니다.
)}
); } export default TableSettingModal;