"use client"; import { useState, useEffect, useMemo, useCallback } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Search, Database, RefreshCw, Settings, Menu, X, Plus, Activity } from "lucide-react"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { toast } from "sonner"; import { useMultiLang } from "@/hooks/useMultiLang"; import { useAuth } from "@/hooks/useAuth"; import { TABLE_MANAGEMENT_KEYS } from "@/constants/tableManagement"; import { INPUT_TYPE_OPTIONS } from "@/types/input-types"; import { apiClient } from "@/lib/api/client"; import { commonCodeApi } from "@/lib/api/commonCode"; import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin"; import { CreateTableModal } from "@/components/admin/CreateTableModal"; import { AddColumnModal } from "@/components/admin/AddColumnModal"; import { DDLLogViewer } from "@/components/admin/DDLLogViewer"; // 가상화 스크롤링을 위한 간단한 구현 interface TableInfo { tableName: string; displayName: string; description: string; columnCount: number; } interface ColumnTypeInfo { columnName: string; displayName: string; inputType: string; // webType → inputType 변경 detailSettings: string; description: string; isNullable: string; defaultValue?: string; maxLength?: number; numericPrecision?: number; numericScale?: number; codeCategory?: string; codeValue?: string; referenceTable?: string; referenceColumn?: string; displayColumn?: string; // 🎯 Entity 조인에서 표시할 컬럼명 } export default function TableManagementPage() { const { userLang, getText } = useMultiLang({ companyCode: "*" }); const { user } = useAuth(); const [tables, setTables] = useState([]); const [columns, setColumns] = useState([]); const [selectedTable, setSelectedTable] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [loading, setLoading] = useState(false); const [columnsLoading, setColumnsLoading] = useState(false); const [originalColumns, setOriginalColumns] = useState([]); // 원본 데이터 저장 const [uiTexts, setUiTexts] = useState>({}); // 페이지네이션 상태 const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [totalColumns, setTotalColumns] = useState(0); // 테이블 라벨 상태 const [tableLabel, setTableLabel] = useState(""); const [tableDescription, setTableDescription] = useState(""); // 🎯 Entity 조인 관련 상태 const [referenceTableColumns, setReferenceTableColumns] = useState>({}); // DDL 기능 관련 상태 const [createTableModalOpen, setCreateTableModalOpen] = useState(false); const [addColumnModalOpen, setAddColumnModalOpen] = useState(false); const [ddlLogViewerOpen, setDdlLogViewerOpen] = useState(false); // 최고 관리자 여부 확인 const isSuperAdmin = user?.companyCode === "*" && user?.userId === "plm_admin"; // 다국어 텍스트 로드 useEffect(() => { const loadTexts = async () => { if (!userLang) return; try { const response = await apiClient.post( "/multilang/batch", { langKeys: Object.values(TABLE_MANAGEMENT_KEYS), }, { params: { companyCode: "*", menuCode: "TABLE_MANAGEMENT", userLang: userLang, }, }, ); if (response.data.success) { setUiTexts(response.data.data); } } catch (error) { console.error("다국어 텍스트 로드 실패:", error); } }; loadTexts(); }, [userLang]); // 텍스트 가져오기 함수 const getTextFromUI = (key: string, fallback?: string) => { return uiTexts[key] || fallback || key; }; // 🎯 참조 테이블 컬럼 정보 로드 const loadReferenceTableColumns = useCallback( async (tableName: string) => { if (!tableName) { return; } // 이미 로드된 경우이지만 빈 배열이 아닌 경우만 스킵 const existingColumns = referenceTableColumns[tableName]; if (existingColumns && existingColumns.length > 0) { console.log(`🎯 참조 테이블 컬럼 이미 로드됨: ${tableName}`, existingColumns); return; } console.log(`🎯 참조 테이블 컬럼 로드 시작: ${tableName}`); try { const result = await entityJoinApi.getReferenceTableColumns(tableName); console.log(`🎯 참조 테이블 컬럼 로드 성공: ${tableName}`, result.columns); setReferenceTableColumns((prev) => ({ ...prev, [tableName]: result.columns, })); } catch (error) { console.error(`참조 테이블 컬럼 로드 실패: ${tableName}`, error); setReferenceTableColumns((prev) => ({ ...prev, [tableName]: [], })); } }, [], // 의존성 배열에서 referenceTableColumns 제거 ); // 입력 타입 옵션 (8개 핵심 타입) const inputTypeOptions = INPUT_TYPE_OPTIONS.map((option) => ({ value: option.value, label: option.label, description: option.description, })); // 메모이제이션된 입력타입 옵션 const memoizedInputTypeOptions = useMemo(() => inputTypeOptions, []); // 참조 테이블 옵션 (실제 테이블 목록에서 가져옴) const referenceTableOptions = [ { value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_NONE, "선택 안함") }, ...tables.map((table) => ({ value: table.tableName, label: table.displayName || table.tableName })), ]; // 공통 코드 카테고리 목록 상태 const [commonCodeCategories, setCommonCodeCategories] = useState>([]); // 공통 코드 옵션 const commonCodeOptions = [ { value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_CODE_PLACEHOLDER, "코드 선택") }, ...commonCodeCategories, ]; // 공통코드 카테고리 목록 로드 const loadCommonCodeCategories = async () => { try { const response = await commonCodeApi.categories.getList({ isActive: true }); console.log("🔍 공통코드 카테고리 API 응답:", response); if (response.success && response.data) { console.log("📋 공통코드 카테고리 데이터:", response.data); const categories = response.data.map((category) => { console.log("🏷️ 카테고리 항목:", category); return { value: category.category_code, label: category.category_name || category.category_code, }; }); console.log("✅ 매핑된 카테고리 옵션:", categories); setCommonCodeCategories(categories); } } catch (error) { console.error("공통코드 카테고리 로드 실패:", error); // 에러는 로그만 남기고 사용자에게는 알리지 않음 (선택적 기능) } }; // 테이블 목록 로드 const loadTables = async () => { setLoading(true); try { const response = await apiClient.get("/table-management/tables"); // 응답 상태 확인 if (response.data.success) { setTables(response.data.data); toast.success("테이블 목록을 성공적으로 로드했습니다."); } else { toast.error(response.data.message || "테이블 목록 로드에 실패했습니다."); } } catch (error) { console.error("테이블 목록 로드 실패:", error); toast.error("테이블 목록 로드 중 오류가 발생했습니다."); } finally { setLoading(false); } }; // 컬럼 타입 정보 로드 (페이지네이션 적용) const loadColumnTypes = useCallback(async (tableName: string, page: number = 1, size: number = 50) => { setColumnsLoading(true); try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`, { params: { page, size }, }); // 응답 상태 확인 if (response.data.success) { const data = response.data.data; // 컬럼 데이터에 기본값 설정 const processedColumns = (data.columns || data).map((col: any) => ({ ...col, inputType: col.inputType || "text", // 기본값: text })); if (page === 1) { setColumns(processedColumns); setOriginalColumns(processedColumns); } else { // 페이지 추가 로드 시 기존 데이터에 추가 setColumns((prev) => [...prev, ...processedColumns]); } setTotalColumns(data.total || processedColumns.length); toast.success("컬럼 정보를 성공적으로 로드했습니다."); } else { toast.error(response.data.message || "컬럼 정보 로드에 실패했습니다."); } } catch (error) { console.error("컬럼 타입 정보 로드 실패:", error); toast.error("컬럼 정보 로드 중 오류가 발생했습니다."); } finally { setColumnsLoading(false); } }, []); // 테이블 선택 const handleTableSelect = useCallback( (tableName: string) => { setSelectedTable(tableName); setCurrentPage(1); setColumns([]); // 선택된 테이블 정보에서 라벨 설정 const tableInfo = tables.find((table) => table.tableName === tableName); setTableLabel(tableInfo?.displayName || tableName); setTableDescription(tableInfo?.description || ""); loadColumnTypes(tableName, 1, pageSize); }, [loadColumnTypes, pageSize, tables], ); // 입력 타입 변경 const handleInputTypeChange = useCallback( (columnName: string, newInputType: string) => { setColumns((prev) => prev.map((col) => { if (col.columnName === columnName) { const inputTypeOption = memoizedInputTypeOptions.find((option) => option.value === newInputType); return { ...col, inputType: newInputType, detailSettings: inputTypeOption?.description || col.detailSettings, }; } return col; }), ); }, [memoizedInputTypeOptions], ); // 상세 설정 변경 (코드/엔티티 타입용) const handleDetailSettingsChange = useCallback( (columnName: string, settingType: string, value: string) => { setColumns((prev) => prev.map((col) => { if (col.columnName === columnName) { let newDetailSettings = col.detailSettings; let codeCategory = col.codeCategory; let codeValue = col.codeValue; let referenceTable = col.referenceTable; let referenceColumn = col.referenceColumn; let displayColumn = col.displayColumn; if (settingType === "code") { if (value === "none") { newDetailSettings = ""; codeCategory = undefined; codeValue = undefined; } else { const codeOption = commonCodeOptions.find((option) => option.value === value); newDetailSettings = codeOption ? `공통코드: ${codeOption.label}` : ""; codeCategory = value; codeValue = value; } } else if (settingType === "entity") { if (value === "none") { newDetailSettings = ""; referenceTable = undefined; referenceColumn = undefined; displayColumn = undefined; } else { const tableOption = referenceTableOptions.find((option) => option.value === value); newDetailSettings = tableOption ? `참조테이블: ${tableOption.label}` : ""; referenceTable = value; // 🎯 참조 컬럼을 소스 컬럼명과 동일하게 설정 (일반적인 경우) // 예: user_info.dept_code -> dept_info.dept_code referenceColumn = col.columnName; // 참조 테이블의 컬럼 정보 로드 loadReferenceTableColumns(value); } } else if (settingType === "entity_reference_column") { // 🎯 Entity 참조 컬럼 변경 (조인할 컬럼) referenceColumn = value; const tableOption = referenceTableOptions.find((option) => option.value === col.referenceTable); newDetailSettings = tableOption ? `참조테이블: ${tableOption.label}` : ""; } else if (settingType === "entity_display_column") { // 🎯 Entity 표시 컬럼 변경 displayColumn = value; const tableOption = referenceTableOptions.find((option) => option.value === col.referenceTable); newDetailSettings = tableOption ? `참조테이블: ${tableOption.label} (${value})` : ""; } return { ...col, detailSettings: newDetailSettings, codeCategory, codeValue, referenceTable, referenceColumn, displayColumn, }; } return col; }), ); }, [commonCodeOptions, referenceTableOptions, loadReferenceTableColumns], ); // 라벨 변경 핸들러 추가 const handleLabelChange = useCallback((columnName: string, newLabel: string) => { setColumns((prev) => prev.map((col) => { if (col.columnName === columnName) { return { ...col, displayName: newLabel, }; } return col; }), ); }, []); // 컬럼 변경 핸들러 (인덱스 기반) const handleColumnChange = useCallback((index: number, field: keyof ColumnTypeInfo, value: any) => { setColumns((prev) => prev.map((col, i) => { if (i === index) { return { ...col, [field]: value, }; } return col; }), ); }, []); // 개별 컬럼 저장 const handleSaveColumn = async (column: ColumnTypeInfo) => { if (!selectedTable) return; try { const columnSetting = { columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가) columnLabel: column.displayName, // 사용자가 입력한 표시명 inputType: column.inputType || "text", detailSettings: column.detailSettings || "", codeCategory: column.codeCategory || "", codeValue: column.codeValue || "", referenceTable: column.referenceTable || "", referenceColumn: column.referenceColumn || "", displayColumn: column.displayColumn || "", // 🎯 Entity 조인에서 표시할 컬럼명 }; console.log("저장할 컬럼 설정:", columnSetting); const response = await apiClient.post(`/table-management/tables/${selectedTable}/columns/settings`, [ columnSetting, ]); if (response.data.success) { toast.success("컬럼 설정이 성공적으로 저장되었습니다."); // 원본 데이터 업데이트 setOriginalColumns((prev) => prev.map((col) => (col.columnName === column.columnName ? column : col))); // 저장 후 데이터 확인을 위해 다시 로드 setTimeout(() => { loadColumnTypes(selectedTable); }, 1000); } else { toast.error(response.data.message || "컬럼 설정 저장에 실패했습니다."); } } catch (error) { console.error("컬럼 설정 저장 실패:", error); toast.error("컬럼 설정 저장 중 오류가 발생했습니다."); } }; // 전체 저장 (테이블 라벨 + 모든 컬럼 설정) const saveAllSettings = async () => { if (!selectedTable) return; try { // 1. 테이블 라벨 저장 (변경된 경우에만) if (tableLabel !== selectedTable || tableDescription) { try { await apiClient.put(`/table-management/tables/${selectedTable}/label`, { displayName: tableLabel, description: tableDescription, }); } catch (error) { console.warn("테이블 라벨 저장 실패 (API 미구현 가능):", error); } } // 2. 모든 컬럼 설정 저장 if (columns.length > 0) { const columnSettings = columns.map((column) => ({ columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가) columnLabel: column.displayName, // 사용자가 입력한 표시명 inputType: column.inputType || "text", detailSettings: column.detailSettings || "", description: column.description || "", codeCategory: column.codeCategory || "", codeValue: column.codeValue || "", referenceTable: column.referenceTable || "", referenceColumn: column.referenceColumn || "", displayColumn: column.displayColumn || "", // 🎯 Entity 조인에서 표시할 컬럼명 })); console.log("저장할 전체 설정:", { tableLabel, tableDescription, columnSettings }); // 전체 테이블 설정을 한 번에 저장 const response = await apiClient.post( `/table-management/tables/${selectedTable}/columns/settings`, columnSettings, ); if (response.data.success) { // 저장 성공 후 원본 데이터 업데이트 setOriginalColumns([...columns]); toast.success(`테이블 '${selectedTable}' 설정이 모두 저장되었습니다.`); // 테이블 목록 새로고침 (라벨 변경 반영) loadTables(); // 저장 후 데이터 확인을 위해 다시 로드 setTimeout(() => { loadColumnTypes(selectedTable, 1, pageSize); }, 1000); } else { toast.error(response.data.message || "설정 저장에 실패했습니다."); } } } catch (error) { console.error("설정 저장 실패:", error); toast.error("설정 저장 중 오류가 발생했습니다."); } }; // 필터링된 테이블 목록 (메모이제이션) const filteredTables = useMemo( () => tables.filter( (table) => table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) || table.displayName.toLowerCase().includes(searchTerm.toLowerCase()), ), [tables, searchTerm], ); // 선택된 테이블 정보 const selectedTableInfo = tables.find((table) => table.tableName === selectedTable); useEffect(() => { loadTables(); loadCommonCodeCategories(); }, []); // 🎯 컬럼 로드 후 이미 설정된 참조 테이블들의 컬럼 정보 로드 useEffect(() => { if (columns.length > 0) { const entityColumns = columns.filter( (col) => col.webType === "entity" && col.referenceTable && col.referenceTable !== "none", ); entityColumns.forEach((col) => { if (col.referenceTable) { console.log(`🎯 기존 Entity 컬럼 발견, 참조 테이블 컬럼 로드: ${col.columnName} -> ${col.referenceTable}`); loadReferenceTableColumns(col.referenceTable); } }); } }, [columns, loadReferenceTableColumns]); // 더 많은 데이터 로드 const loadMoreColumns = useCallback(() => { if (selectedTable && columns.length < totalColumns && !columnsLoading) { const nextPage = Math.floor(columns.length / pageSize) + 1; loadColumnTypes(selectedTable, nextPage, pageSize); } }, [selectedTable, columns.length, totalColumns, columnsLoading, pageSize, loadColumnTypes]); return (
{/* 페이지 제목 */}

{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}

{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_DESCRIPTION, "데이터베이스 테이블과 컬럼의 타입을 관리합니다")}

{isSuperAdmin && (

🔧 최고 관리자 권한으로 새 테이블 생성 및 컬럼 추가가 가능합니다

)}
{/* DDL 기능 버튼들 (최고 관리자만) */} {isSuperAdmin && ( <> {selectedTable && ( )} )}
{/* 테이블 목록 */} {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")} {/* 검색 */}
setSearchTerm(e.target.value)} className="pl-10" />
{/* 테이블 목록 */}
{loading ? (
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_TABLES, "테이블 로딩 중...")}
) : tables.length === 0 ? (
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")}
) : ( tables .filter( (table) => table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) || (table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())), ) .map((table) => (
handleTableSelect(table.tableName)} >

{table.displayName || table.tableName}

{table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}

{table.columnCount} {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_COLUMN_COUNT, "컬럼")}
)) )}
{/* 컬럼 타입 관리 */} {selectedTable ? <>테이블 설정 - {selectedTable} : "테이블 타입 관리"} {!selectedTable ? (
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
) : ( <> {/* 테이블 라벨 설정 */}

테이블 정보

setTableLabel(e.target.value)} placeholder="테이블 표시명을 입력하세요" />
setTableDescription(e.target.value)} placeholder="테이블 설명을 입력하세요" />
{columnsLoading ? (
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
) : columns.length === 0 ? (
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
) : (
{/* 컬럼 헤더 */}
컬럼명
라벨
입력 타입
상세 설정
설명
{/* 컬럼 리스트 */}
{ const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; // 스크롤이 끝에 가까워지면 더 많은 데이터 로드 if (scrollHeight - scrollTop <= clientHeight + 100) { loadMoreColumns(); } }} > {columns.map((column, index) => (
{column.columnName}
handleLabelChange(column.columnName, e.target.value)} placeholder={column.columnName} className="h-7 text-xs" />
{/* 웹 타입이 'code'인 경우 공통코드 선택 */} {column.inputType === "code" && ( )} {/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */} {column.inputType === "entity" && (
{/* 🎯 Entity 타입 설정 - 가로 배치 */}
Entity 설정
{/* 참조 테이블 */}
{/* 조인 컬럼 */} {column.referenceTable && column.referenceTable !== "none" && (
)} {/* 표시 컬럼 */} {column.referenceTable && column.referenceTable !== "none" && (
)}
{/* 설정 완료 표시 - 간소화 */} {column.referenceTable && column.referenceTable !== "none" && column.referenceColumn && column.referenceColumn !== "none" && column.displayColumn && column.displayColumn !== "none" && (
{column.columnName} → {column.referenceTable}.{column.displayColumn}
)}
)} {/* 다른 웹 타입인 경우 빈 공간 */} {column.inputType !== "code" && column.inputType !== "entity" && (
-
)}
handleColumnChange(index, "description", e.target.value)} placeholder="설명" className="h-7 text-xs" />
))}
{/* 로딩 표시 */} {columnsLoading && (
더 많은 컬럼 로딩 중...
)} {/* 페이지 정보 */}
{columns.length} / {totalColumns} 컬럼 표시됨
{/* 전체 저장 버튼 */}
)} )}
{/* DDL 모달 컴포넌트들 */} {isSuperAdmin && ( <> setCreateTableModalOpen(false)} onSuccess={async (result) => { toast.success("테이블이 성공적으로 생성되었습니다!"); // 테이블 목록 새로고침 await loadTables(); // 새로 생성된 테이블 자동 선택 및 컬럼 로드 if (result.data?.tableName) { setSelectedTable(result.data.tableName); setCurrentPage(1); setColumns([]); await loadColumnTypes(result.data.tableName, 1, pageSize); } }} /> setAddColumnModalOpen(false)} tableName={selectedTable || ""} onSuccess={async (result) => { toast.success("컬럼이 성공적으로 추가되었습니다!"); // 테이블 목록 새로고침 (컬럼 수 업데이트) await loadTables(); // 선택된 테이블의 컬럼 목록 새로고침 - 페이지 리셋 if (selectedTable) { setCurrentPage(1); setColumns([]); // 기존 컬럼 목록 초기화 await loadColumnTypes(selectedTable, 1, pageSize); } }} /> setDdlLogViewerOpen(false)} /> )}
); }