"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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; 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, } from "lucide-react"; import { getFieldJoins, createFieldJoin, updateFieldJoin, deleteFieldJoin, FieldJoin, } from "@/lib/api/screenGroup"; import { tableManagementApi, ColumnTypeInfo, TableInfo } from "@/lib/api/tableManagement"; // ============================================================ // 타입 정의 // ============================================================ 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 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} )}
))}
); } // ============================================================ // 메인 모달 컴포넌트 // ============================================================ export function TableSettingModal({ isOpen, onClose, tableName, tableLabel, screenId, joinColumnRefs = [], referencedBy = [], columns = [], filterColumns = [], onSaveSuccess, }: TableSettingModalProps) { const [activeTab, setActiveTab] = useState("info"); const [loading, setLoading] = useState(false); const [tableColumns, setTableColumns] = useState([]); const [tables, setTables] = useState([]); const [fieldJoins, setFieldJoins] = useState([]); // 테이블 컬럼 정보 로드 const loadTableColumns = useCallback(async () => { if (!tableName) return; setLoading(true); try { // 테이블 목록 로드 const tablesResponse = await tableManagementApi.getTables(); if (tablesResponse.success && tablesResponse.data) { setTables(tablesResponse.data); } // 테이블 컬럼 로드 const columnsResponse = await tableManagementApi.getTableColumns(tableName); if (columnsResponse.success && columnsResponse.data) { setTableColumns(columnsResponse.data); } // 필드 조인 로드 (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); } } } catch (error) { console.error("테이블 정보 로드 실패:", error); toast.error("테이블 정보를 불러오는데 실패했습니다."); } finally { setLoading(false); } }, [tableName, screenId]); useEffect(() => { if (isOpen && tableName) { loadTableColumns(); } }, [isOpen, tableName, loadTableColumns]); // 새로고침 const handleRefresh = () => { loadTableColumns(); toast.success("새로고침 완료"); }; return ( 테이블 설정: {tableLabel || tableName} 테이블의 컬럼 정보, 조인 설정, 참조 관계를 확인하고 설정합니다. {tableName !== tableLabel && ( ({tableName}) )}
테이블 정보 조인 설정 참조 관계
{/* 탭 1: 테이블 정보 */} {/* 탭 2: 조인 설정 */} {/* 탭 3: 참조 관계 */}
); } // ============================================================ // 탭 1: 테이블 정보 // ============================================================ interface TableInfoTabProps { tableName: string; tableLabel?: string; columns: ColumnInfo[]; tableColumns: ColumnTypeInfo[]; filterColumns: string[]; loading: boolean; } function TableInfoTab({ tableName, tableLabel, columns, tableColumns, filterColumns, loading, }: TableInfoTabProps) { // 컬럼 정보 통합 (기존 columns + API에서 가져온 tableColumns) const mergedColumns = useMemo(() => { const columnsMap = new Map(); // 먼저 기존 columns 추가 columns.forEach((col) => { columnsMap.set(col.column, col); }); // API에서 가져온 컬럼 정보로 보강 tableColumns.forEach((tcol) => { const existing = columnsMap.get(tcol.column_name); if (existing) { columnsMap.set(tcol.column_name, { ...existing, type: tcol.data_type, isPK: tcol.is_primary_key, isFK: tcol.is_foreign_key, refTable: tcol.references?.table, refColumn: tcol.references?.column, label: existing.label || tcol.column_name, }); } else { columnsMap.set(tcol.column_name, { column: tcol.column_name, label: tcol.column_name, type: tcol.data_type, isPK: tcol.is_primary_key, isFK: tcol.is_foreign_key, refTable: tcol.references?.table, refColumn: tcol.references?.column, }); } }); return Array.from(columnsMap.values()); }, [columns, tableColumns]); // PK, FK 분류 const pkColumns = mergedColumns.filter((c) => c.isPK); const fkColumns = mergedColumns.filter((c) => c.isFK); if (loading) { return (
); } return (
{/* 기본 정보 */}
{mergedColumns.length}
전체 컬럼
{pkColumns.length}
Primary Key
{fkColumns.length}
Foreign Key
{filterColumns.length}
필터 컬럼
{/* 필터 컬럼 */} {filterColumns.length > 0 && (

필터 컬럼

{filterColumns.map((col, idx) => ( {col} ))}
)} {/* 컬럼 목록 */}

컬럼 목록

# 컬럼명 데이터 타입 참조 {mergedColumns.length === 0 ? ( 컬럼 정보를 불러올 수 없습니다. ) : ( mergedColumns.map((col, idx) => ( {idx + 1} {col.label || col.column} {col.label && col.column !== col.label && ( ({col.column}) )} {col.type || "-"}
{col.isPK && ( PK )} {col.isFK && ( FK )}
{col.refTable && col.refColumn ? ( {col.refTable}.{col.refColumn} ) : ( "-" )}
)) )}
); } // ============================================================ // 탭 2: 조인 설정 // ============================================================ interface JoinSettingTabProps { tableName: string; tableLabel?: string; screenId?: number; joinColumnRefs: JoinColumnRef[]; fieldJoins: FieldJoin[]; tables: TableInfo[]; tableColumns: ColumnTypeInfo[]; loading: boolean; onReload: () => void; onSaveSuccess?: () => void; } function JoinSettingTab({ tableName, tableLabel, screenId, joinColumnRefs, fieldJoins, tables, tableColumns, loading, onReload, onSaveSuccess, }: JoinSettingTabProps) { const [isEditing, setIsEditing] = useState(false); const [editItem, setEditItem] = useState(null); const [formData, setFormData] = useState({ save_column: "", join_table: "", join_column: "", display_column: "", join_type: "LEFT", }); const [targetColumns, setTargetColumns] = useState([]); // 조인 테이블 변경 시 컬럼 로드 const loadTargetColumns = useCallback(async (targetTable: string) => { if (!targetTable) { setTargetColumns([]); return; } try { const response = await tableManagementApi.getTableColumns(targetTable); if (response.success && response.data) { setTargetColumns(response.data); } } catch (error) { console.error("대상 테이블 컬럼 로드 실패:", error); } }, []); useEffect(() => { if (formData.join_table) { loadTargetColumns(formData.join_table); } }, [formData.join_table, loadTargetColumns]); // 폼 초기화 const resetForm = () => { setFormData({ save_column: "", join_table: "", join_column: "", display_column: "", join_type: "LEFT", }); setEditItem(null); setIsEditing(false); setTargetColumns([]); }; // 수정 모드 const handleEdit = (item: FieldJoin) => { setEditItem(item); setFormData({ save_column: item.save_column, join_table: item.join_table, join_column: item.join_column, display_column: item.display_column || "", join_type: item.join_type, }); setIsEditing(true); loadTargetColumns(item.join_table); }; // 디자이너 설정을 DB로 저장 const handleSaveDesignerJoin = async (join: JoinColumnRef) => { if (!screenId) { toast.error("화면 ID가 필요합니다."); return; } try { const payload = { screen_id: screenId, save_table: tableName, save_column: join.column, join_table: join.refTable, join_column: join.refColumn, display_column: "", join_type: "LEFT", is_active: "Y", }; const response = await createFieldJoin(payload); if (response.success) { toast.success("조인 설정이 DB에 저장되었습니다."); onReload(); onSaveSuccess?.(); } else { toast.error(response.message || "저장에 실패했습니다."); } } catch (error) { console.error("저장 오류:", error); toast.error("저장 중 오류가 발생했습니다."); } }; // 저장 const handleSave = async () => { if (!screenId) { toast.error("화면 ID가 필요합니다."); return; } if (!formData.save_column || !formData.join_table || !formData.join_column) { toast.error("필수 항목을 모두 입력해주세요."); return; } try { const payload = { screen_id: screenId, save_table: tableName, save_column: formData.save_column, join_table: formData.join_table, join_column: formData.join_column, display_column: formData.display_column || null, join_type: formData.join_type, is_active: "Y", }; let response; if (editItem) { response = await updateFieldJoin(editItem.id, payload); } else { response = await createFieldJoin(payload); } if (response.success) { toast.success(editItem ? "조인 설정이 수정되었습니다." : "조인 설정이 추가되었습니다."); resetForm(); onReload(); onSaveSuccess?.(); } else { toast.error(response.message || "저장에 실패했습니다."); } } catch (error) { console.error("저장 오류:", error); toast.error("저장 중 오류가 발생했습니다."); } }; // 삭제 const handleDelete = async (id: number) => { if (!confirm("정말로 삭제하시겠습니까?")) return; try { const response = await deleteFieldJoin(id); if (response.success) { toast.success("조인 설정이 삭제되었습니다."); onReload(); onSaveSuccess?.(); } else { toast.error(response.message || "삭제에 실패했습니다."); } } catch (error) { console.error("삭제 오류:", error); toast.error("삭제 중 오류가 발생했습니다."); } }; // 통합 조인 목록 (디자이너 + DB) const unifiedJoins = useMemo(() => { // DB에서 가져온 조인 const dbJoins = fieldJoins.map((j) => ({ ...j, id: j.id, source: "db" as const, })); // 디자이너 조인 (DB에 없는 것만) const dbJoinKeys = new Set( fieldJoins.map((j) => `${j.save_column}:${j.join_table}:${j.join_column}`) ); const designerJoins = joinColumnRefs .filter( (j) => !dbJoinKeys.has(`${j.column}:${j.refTable}:${j.refColumn}`) ) .map((j, idx) => ({ id: `designer-${idx}`, source: "designer" as const, save_table: tableName, save_column: j.column, join_table: j.refTable, join_table_label: j.refTableLabel, join_column: j.refColumn, display_column: "", join_type: "LEFT", })); return [...designerJoins, ...dbJoins]; }, [fieldJoins, joinColumnRefs, tableName]); // 테이블 옵션 const tableOptions = useMemo( () => tables.map((t) => ({ value: t.table_name, label: t.table_name, })), [tables] ); // 현재 테이블 컬럼 옵션 const columnOptions = useMemo( () => tableColumns.map((c) => ({ value: c.column_name, label: c.column_name, description: c.data_type, })), [tableColumns] ); // 대상 테이블 컬럼 옵션 const targetColumnOptions = useMemo( () => targetColumns.map((c) => ({ value: c.column_name, label: c.column_name, description: c.data_type, })), [targetColumns] ); if (loading) { return (
); } return (
{/* 입력 폼 */} {screenId && (
{isEditing ? "조인 설정 수정" : "새 조인 설정 추가"}
setFormData({ ...formData, save_column: v })} options={columnOptions} placeholder="컬럼 선택" />
setFormData({ ...formData, join_table: v, join_column: "", display_column: "" }) } options={tableOptions} placeholder="테이블 선택" />
setFormData({ ...formData, join_column: v })} options={targetColumnOptions} placeholder="컬럼 선택" disabled={!formData.join_table} />
setFormData({ ...formData, display_column: v })} options={targetColumnOptions} placeholder="선택 (옵션)" disabled={!formData.join_table} />
setFormData({ ...formData, join_type: v })} options={[ { value: "LEFT", label: "LEFT JOIN" }, { value: "INNER", label: "INNER JOIN" }, { value: "RIGHT", label: "RIGHT JOIN" }, ]} placeholder="타입 선택" />
{isEditing && ( )}
)} {/* 목록 */}
출처 현재 컬럼 조인 테이블 조인 컬럼 타입 작업 {unifiedJoins.length === 0 ? ( 등록된 조인 설정이 없습니다. ) : ( unifiedJoins.map((item) => ( {item.source === "designer" ? "화면" : "DB"} {item.save_column} {"join_table_label" in item && item.join_table_label ? item.join_table_label : item.join_table} {item.join_column} {item.join_type}
{item.source === "designer" ? ( ) : ( <> )}
)) )}
{/* 안내 */}
* 화면: 화면 디자이너에서 설정됨 (DB 저장으로 변환 가능) | * DB: 데이터베이스에 저장됨 (수정/삭제 가능)
); } // ============================================================ // 탭 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;