"use client"; import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Search, Database, RefreshCw, Save, Plus, Activity, Trash2, Copy, Check, ChevronsUpDown, Loader2, Pencil, Columns3, Link2, } from "lucide-react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "@/lib/utils"; import { LoadingSpinner } from "@/components/common/LoadingSpinner"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/utils/toastUtils"; import { useMultiLang } from "@/hooks/useMultiLang"; import { useAuth } from "@/hooks/useAuth"; import { TABLE_MANAGEMENT_KEYS } from "@/constants/tableManagement"; import { INPUT_TYPE_OPTIONS, USER_SELECTABLE_INPUT_TYPE_ORDER } from "@/types/input-types"; import { apiClient } from "@/lib/api/client"; import { getCodeInfoList } from "@/lib/api/commonCode"; import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin"; import { ddlApi } from "@/lib/api/ddl"; // getSecondLevelMenus / createColumnMapping / deleteColumnMappingsByColumn (카테고리 모듈 폐기) const getSecondLevelMenus = async (): Promise<{ success: boolean; data?: any[] }> => ({ success: true, data: [] }); const createColumnMapping = async (_params: Record): Promise<{ success: boolean }> => ({ success: true }); const deleteColumnMappingsByColumn = async (_table: string, _column: string): Promise<{ success: boolean }> => ({ success: true }); import { saveNumberingRuleToTest } from "@/lib/api/numberingRule"; import { CreateTableModal } from "@/components/admin/CreateTableModal"; import { AddColumnModal } from "@/components/admin/AddColumnModal"; import { DDLLogViewer } from "@/components/admin/DDLLogViewer"; import { TableLogViewer } from "@/components/admin/TableLogViewer"; import { ScrollToTop } from "@/components/common/ScrollToTop"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import type { TableInfo, ColumnTypeInfo, SecondLevelMenu } from "@/components/admin/table-type/types"; import { TypeOverviewStrip } from "@/components/admin/table-type/TypeOverviewStrip"; import { ColumnGrid } from "@/components/admin/table-type/ColumnGrid"; import { ColumnDetailPanel } from "@/components/admin/table-type/ColumnDetailPanel"; import { ReferenceListView } from "@/components/admin/table-type/ReferenceListView"; 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(9999); // 전체 컬럼 표시 const [totalColumns, setTotalColumns] = useState(0); // 테이블 라벨 상태 const [tableLabel, setTableLabel] = useState(""); const [tableDescription, setTableDescription] = useState(""); // 헤더 인라인 편집 상태 (Google Docs / Notion 패턴) const [editingHeaderField, setEditingHeaderField] = useState<"label" | "description" | null>(null); const [editingHeaderValue, setEditingHeaderValue] = useState(""); // 🎯 Entity 조인 관련 상태 const [referenceTableColumns, setReferenceTableColumns] = useState>({}); // 🆕 Entity 타입 Combobox 열림/닫힘 상태 (컬럼별 관리) const [entityComboboxOpen, setEntityComboboxOpen] = useState< Record< string, { table: boolean; joinColumn: boolean; displayColumn: boolean; } > >({}); // DDL 기능 관련 상태 const [createTableModalOpen, setCreateTableModalOpen] = useState(false); const [addColumnModalOpen, setAddColumnModalOpen] = useState(false); const [ddlLogViewerOpen, setDdlLogViewerOpen] = useState(false); // 테이블 복제 관련 상태 const [duplicateModalMode, setDuplicateModalMode] = useState<"create" | "duplicate">("create"); const [duplicateSourceTable, setDuplicateSourceTable] = useState(null); // 🆕 Category 타입용: 2레벨 메뉴 목록 const [secondLevelMenus, setSecondLevelMenus] = useState([]); // 채번 타입은 옵션설정 > 채번설정에서 관리 (별도 선택 불필요) // 로그 뷰어 상태 const [logViewerOpen, setLogViewerOpen] = useState(false); const [logViewerTableName, setLogViewerTableName] = useState(""); // 저장 중 상태 (중복 실행 방지) const [isSaving, setIsSaving] = useState(false); // 테이블 삭제 확인 다이얼로그 상태 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [tableToDelete, setTableToDelete] = useState(""); const [deleteColumnDialogOpen, setDeleteColumnDialogOpen] = useState(false); const [columnToDelete, setColumnToDelete] = useState(""); const [isDeletingColumn, setIsDeletingColumn] = useState(false); const [isDeleting, setIsDeleting] = useState(false); // PK/인덱스 관리 상태 const [constraints, setConstraints] = useState<{ primaryKey: { name: string; columns: string[] }; indexes: Array<{ name: string; columns: string[]; is_unique: boolean }>; }>({ primaryKey: { name: "", columns: [] }, indexes: [] }); const [pkDialogOpen, setPkDialogOpen] = useState(false); const [pendingPkColumns, setPendingPkColumns] = useState([]); // 선택된 테이블 목록 (체크박스) const [selectedTableIds, setSelectedTableIds] = useState>(new Set()); // 컬럼 그리드: 선택된 컬럼(우측 상세 패널 표시) const [selectedColumn, setSelectedColumn] = useState(null); // 타입 오버뷰 스트립: 타입 필터 (null = 전체) const [typeFilter, setTypeFilter] = useState(null); // 최고 관리자 여부 확인 (회사코드가 "*" AND userType이 "SUPER_ADMIN") const isSuperAdmin = user?.company_code === "*" && user?.user_type === "SUPER_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개 사용자 선택 가능 타입 — Layer 2) const inputTypeOptions = INPUT_TYPE_OPTIONS .filter((o) => USER_SELECTABLE_INPUT_TYPE_ORDER.includes(o.value as any)) .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.table_name, label: table.display_name && table.display_name !== table.table_name ? `${table.display_name} (${table.table_name})` : table.table_name, })), ]; // 공통 코드 카테고리 목록 상태 const [commonCodeCategories, setCommonCodeCategories] = useState>([]); // 공통 코드 옵션 const commonCodeOptions = [ { value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_CODE_PLACEHOLDER, "코드 선택") }, ...commonCodeCategories, ]; // 공통코드 카테고리 목록 로드 const loadCommonCodeCategories = async () => { try { const response = await getCodeInfoList({ is_active: true }); if (response.success && response.data) { const categories = response.data.map((row: Record) => ({ value: row.code_info, label: row.code_name || row.code_info, })); // console.log("✅ 매핑된 카테고리 옵션:", categories); setCommonCodeCategories(categories); } } catch (error) { // console.error("공통코드 카테고리 로드 실패:", error); // 에러는 로그만 남기고 사용자에게는 알리지 않음 (선택적 기능) } }; // 🆕 2레벨 메뉴 목록 로드 const loadSecondLevelMenus = async () => { try { const response = await getSecondLevelMenus(); if (response.success && response.data) { setSecondLevelMenus(response.data); } else { console.warn("⚠️ 2레벨 메뉴 로드 실패:", response); setSecondLevelMenus([]); // 빈 배열로 설정하여 로딩 상태 해제 } } catch (error) { console.error("❌ 2레벨 메뉴 로드 에러:", error); setSecondLevelMenus([]); // 에러 발생 시에도 빈 배열로 설정 } }; // 🆕 채번규칙 목록 로드 // 테이블 목록 로드 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 { showErrorToast("테이블 목록을 불러오는 데 실패했습니다", response.data.message, { guidance: "네트워크 연결을 확인해 주세요.", }); } } catch (error) { // console.error("테이블 목록 로드 실패:", error); showErrorToast("테이블 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요.", }); } 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; console.log("📥 원본 API 응답:", { hasColumns: !!(data.columns || data), firstColumn: (data.columns || data)[0], statusColumn: (data.columns || data).find((col: any) => col.column_name === "status"), }); // 컬럼 데이터에 기본값 설정 const processedColumns = (data.columns || data).map((col: any) => { let hierarchyRole: "large" | "medium" | "small" | undefined = undefined; if (col.detail_settings && typeof col.detail_settings === "string") { try { const parsed = JSON.parse(col.detail_settings); if ( parsed.hierarchy_role === "large" || parsed.hierarchy_role === "medium" || parsed.hierarchy_role === "small" ) { hierarchyRole = parsed.hierarchy_role; } } catch { // JSON 파싱 실패 시 무시 } } return { ...col, input_type: col.input_type || "text", is_unique: col.is_unique || "NO", category_menus: col.category_menus || [], hierarchy_role: hierarchyRole, category_ref: col.category_ref || null, }; }); if (page === 1) { setColumns(processedColumns); setOriginalColumns(processedColumns); } else { // 페이지 추가 로드 시 기존 데이터에 추가 setColumns((prev) => [...prev, ...processedColumns]); } setTotalColumns(data.total || processedColumns.length); toast.success("컬럼 정보를 성공적으로 로드했습니다."); } else { showErrorToast("컬럼 정보를 불러오는 데 실패했습니다", response.data.message, { guidance: "네트워크 연결을 확인해 주세요.", }); } } catch (error) { // console.error("컬럼 타입 정보 로드 실패:", error); showErrorToast("컬럼 정보를 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요.", }); } finally { setColumnsLoading(false); } }, []); // PK/인덱스 제약조건 로드 const loadConstraints = useCallback(async (tableName: string) => { try { const response = await apiClient.get(`/table-management/tables/${tableName}/constraints`); if (response.data.success) { const data = response.data.data; setConstraints({ primaryKey: data.primary_key ?? { name: "", columns: [] }, indexes: data.indexes ?? [], }); } } catch (error) { console.error("제약조건 로드 실패:", error); setConstraints({ primaryKey: { name: "", columns: [] }, indexes: [] }); } }, []); // 테이블 선택 const handleTableSelect = useCallback( (tableName: string) => { setSelectedTable(tableName); setCurrentPage(1); setColumns([]); setSelectedColumn(null); setTypeFilter(null); // 선택된 테이블 정보에서 라벨 설정 const tableInfo = tables.find((table) => table.table_name === tableName); setTableLabel(tableInfo?.display_name || tableName); setTableDescription(tableInfo?.description || ""); loadColumnTypes(tableName, 1, pageSize); loadConstraints(tableName); }, [loadColumnTypes, loadConstraints, pageSize, tables], ); // 입력 타입 변경 - 이전 타입의 설정값 초기화 포함 const handleInputTypeChange = useCallback( (columnName: string, newInputType: string) => { setColumns((prev) => prev.map((col) => { if (col.column_name === columnName) { const inputTypeOption = memoizedInputTypeOptions.find((option) => option.value === newInputType); const updated: typeof col = { ...col, input_type: newInputType, detail_settings: inputTypeOption?.description || col.detail_settings, }; // 엔티티가 아닌 타입으로 변경 시 참조 설정 초기화 if (newInputType !== "entity") { updated.reference_table = undefined; updated.reference_column = undefined; updated.display_column = undefined; } // 코드가 아닌 타입으로 변경 시 코드 설정 초기화 if (newInputType !== "code") { updated.code_info = undefined; updated.code_value = undefined; updated.hierarchy_role = undefined; } // 카테고리가 아닌 타입으로 변경 시 카테고리 참조 초기화 if (newInputType !== "category") { updated.category_ref = undefined; } return updated; } return col; }), ); }, [memoizedInputTypeOptions], ); // 상세 설정 변경 (코드/엔티티 타입용) const handleDetailSettingsChange = useCallback( (columnName: string, settingType: string, value: string) => { setColumns((prev) => prev.map((col) => { if (col.column_name === columnName) { let newDetailSettings = col.detail_settings; let codeInfo = col.code_info; let codeValue = col.code_value; let referenceTable = col.reference_table; let referenceColumn = col.reference_column; let displayColumn = col.display_column; let hierarchyRole = col.hierarchy_role; if (settingType === "code") { if (value === "none") { newDetailSettings = ""; codeInfo = undefined; codeValue = undefined; hierarchyRole = undefined; // 코드 선택 해제 시 계층 역할도 초기화 } else { // 기존 hierarchyRole 유지하면서 JSON 형식으로 저장 const existingHierarchyRole = hierarchyRole; newDetailSettings = JSON.stringify({ code_info: value, hierarchy_role: existingHierarchyRole, }); codeInfo = value; codeValue = value; } } else if (settingType === "hierarchy_role") { // 계층구조 역할 변경 - JSON 형식으로 저장 hierarchyRole = value === "none" ? undefined : (value as "large" | "medium" | "small"); // detailSettings를 JSON으로 업데이트 let existingSettings: Record = {}; if (typeof col.detail_settings === "string" && col.detail_settings.trim().startsWith("{")) { try { existingSettings = JSON.parse(col.detail_settings); } catch { existingSettings = {}; } } newDetailSettings = JSON.stringify({ ...existingSettings, hierarchy_role: hierarchyRole, }); } 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.column_name; // 참조 테이블의 컬럼 정보 로드 loadReferenceTableColumns(value); } } else if (settingType === "entity_reference_column") { // 🎯 Entity 참조 컬럼 변경 (조인할 컬럼) referenceColumn = value; const tableOption = referenceTableOptions.find((option) => option.value === col.reference_table); newDetailSettings = tableOption ? `참조테이블: ${tableOption.label}` : ""; } else if (settingType === "entity_display_column") { // 🎯 Entity 표시 컬럼 변경 displayColumn = value; const tableOption = referenceTableOptions.find((option) => option.value === col.reference_table); newDetailSettings = tableOption ? `참조테이블: ${tableOption.label} (${value})` : ""; } return { ...col, detail_settings: newDetailSettings, code_info: codeInfo, code_value: codeValue, reference_table: referenceTable, reference_column: referenceColumn, display_column: displayColumn, hierarchy_role: hierarchyRole, }; } return col; }), ); }, [commonCodeOptions, referenceTableOptions, loadReferenceTableColumns], ); // 라벨 변경 핸들러 추가 const handleLabelChange = useCallback((columnName: string, newLabel: string) => { setColumns((prev) => prev.map((col) => { if (col.column_name === columnName) { return { ...col, display_name: 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 { // 🎯 Entity 타입인 경우 detailSettings에 엔티티 설정을 JSON으로 포함 let finalDetailSettings = column.detail_settings || ""; if (column.input_type === "entity" && column.reference_table) { // 기존 detailSettings를 파싱하거나 새로 생성 let existingSettings: Record = {}; if (typeof column.detail_settings === "string" && column.detail_settings.trim().startsWith("{")) { try { existingSettings = JSON.parse(column.detail_settings); } catch { existingSettings = {}; } } // 엔티티 설정 추가 const entitySettings = { ...existingSettings, entityTable: column.reference_table, entityCodeColumn: column.reference_column || "id", entityLabelColumn: column.display_column || "name", placeholder: (existingSettings.placeholder as string) || "항목을 선택하세요", searchable: existingSettings.searchable ?? true, }; finalDetailSettings = JSON.stringify(entitySettings); console.log("🔧 Entity 설정 JSON 생성:", entitySettings); } // 🎯 Code 타입인 경우 hierarchyRole을 detailSettings에 포함 if (column.input_type === "code" && column.hierarchy_role) { let existingSettings: Record = {}; if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) { try { existingSettings = JSON.parse(finalDetailSettings); } catch { existingSettings = {}; } } const codeSettings = { ...existingSettings, hierarchy_role: column.hierarchy_role, }; finalDetailSettings = JSON.stringify(codeSettings); console.log("🔧 Code 계층 역할 설정 JSON 생성:", codeSettings); } const columnSetting = { column_name: column.column_name, column_label: column.display_name, input_type: column.input_type || "text", detail_settings: finalDetailSettings, code_info: column.code_info || "", code_value: column.code_value || "", reference_table: column.reference_table || "", reference_column: column.reference_column || "", display_column: column.display_column || "", category_ref: column.category_ref || null, }; // console.log("저장할 컬럼 설정:", columnSetting); console.log("💾 저장할 컬럼 정보:", { columnName: column.column_name, inputType: column.input_type, categoryMenus: column.category_menus, hasCategoryMenus: !!column.category_menus, categoryMenusLength: column.category_menus?.length || 0, }); const response = await apiClient.post(`/table-management/tables/${selectedTable}/columns/settings`, [ columnSetting, ]); if (response.data.success) { console.log("✅ 컬럼 설정 저장 성공"); // 🆕 Category 타입인 경우 컬럼 매핑 처리 console.log("🔍 카테고리 조건 체크:", { isCategory: column.input_type === "category", hasCategoryMenus: !!column.category_menus, length: column.category_menus?.length || 0, }); if (column.input_type === "category" && !column.category_ref) { // 참조가 아닌 자체 카테고리만 메뉴 매핑 처리 console.log("기존 카테고리 메뉴 매핑 삭제 시작:", { tableName: selectedTable, columnName: column.column_name, }); try { const deleteResponse = await deleteColumnMappingsByColumn(selectedTable, column.column_name); console.log("🗑️ 기존 매핑 삭제 결과:", deleteResponse); } catch (error) { console.error("❌ 기존 매핑 삭제 실패:", error); } // 2. 새로운 매핑 추가 (선택된 메뉴가 있는 경우만) if (column.category_menus && column.category_menus.length > 0) { console.log("📥 카테고리 메뉴 매핑 시작:", { columnName: column.column_name, categoryMenus: column.category_menus, count: column.category_menus.length, }); let successCount = 0; let failCount = 0; for (const menuObjid of column.category_menus) { try { const mappingResponse = await createColumnMapping({ tableName: selectedTable, logicalColumnName: column.column_name, physicalColumnName: column.column_name, menuObjid, description: `${column.display_name} (메뉴별 카테고리)`, }); if (mappingResponse.success) { successCount++; } else { console.error("❌ 매핑 생성 실패:", mappingResponse); failCount++; } } catch (error) { console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error); failCount++; } } if (successCount > 0 && failCount === 0) { toast.success(`컬럼 설정 및 ${successCount}개 메뉴 매핑이 저장되었습니다.`); } else if (successCount > 0 && failCount > 0) { toast.warning(`컬럼 설정 저장 성공. ${successCount}개 메뉴 매핑 성공, ${failCount}개 실패.`); } else if (failCount > 0) { toast.error("컬럼 설정 저장 성공. 메뉴 매핑 생성 실패."); } } else { toast.success("컬럼 설정이 저장되었습니다. (메뉴 매핑 없음)"); } } else { toast.success("컬럼 설정이 성공적으로 저장되었습니다."); } // 원본 데이터 업데이트 setOriginalColumns((prev) => prev.map((col) => (col.column_name === column.column_name ? column : col))); // 저장 후 데이터 확인을 위해 다시 로드 setTimeout(() => { loadColumnTypes(selectedTable); }, 1000); } else { showErrorToast("컬럼 설정 저장에 실패했습니다", response.data.message, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요.", }); } } catch (error) { // console.error("컬럼 설정 저장 실패:", error); showErrorToast("컬럼 설정 저장에 실패했습니다", error, { guidance: "입력 데이터를 확인하고 다시 시도해 주세요.", }); } }; // 헤더 표시명/설명 인라인 저장 (PUT /label) — Google Docs 식 blur/Enter 커밋 const commitHeaderEdit = async () => { if (!editingHeaderField || !selectedTable) { setEditingHeaderField(null); return; } const next = editingHeaderValue.trim(); const current = editingHeaderField === "label" ? tableLabel : tableDescription; if (next === current) { setEditingHeaderField(null); return; } const newLabel = editingHeaderField === "label" ? next : tableLabel; const newDescription = editingHeaderField === "description" ? next : tableDescription; if (editingHeaderField === "label" && !newLabel) { toast.error("표시명은 비울 수 없습니다."); setEditingHeaderField(null); return; } if (editingHeaderField === "label") setTableLabel(newLabel); else setTableDescription(newDescription); setEditingHeaderField(null); try { await apiClient.put(`/table-management/tables/${selectedTable}/label`, { display_name: newLabel, description: newDescription, }); toast.success(editingHeaderField === "label" ? "표시명이 저장되었습니다." : "설명이 저장되었습니다."); } catch (error: any) { showErrorToast("저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요.", }); } }; // 컬럼 설정만 일괄 저장 (헤더 라벨/설명은 inline 편집으로 즉시 저장됨) const saveAllSettings = async () => { if (!selectedTable) return; if (isSaving) return; // 저장 중 중복 실행 방지 setIsSaving(true); try { // 모든 컬럼 설정 저장 if (columns.length > 0) { const columnSettings = columns.map((column) => { // detailSettings 계산 let finalDetailSettings = column.detail_settings || ""; // 🆕 Entity 타입인 경우 detailSettings에 엔티티 설정 포함 if (column.input_type === "entity" && column.reference_table) { let existingSettings: Record = {}; if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) { try { existingSettings = JSON.parse(finalDetailSettings); } catch { existingSettings = {}; } } const entitySettings = { ...existingSettings, entityTable: column.reference_table, entityCodeColumn: column.reference_column || "id", entityLabelColumn: column.display_column || "name", }; finalDetailSettings = JSON.stringify(entitySettings); } // 🆕 Code 타입인 경우 hierarchyRole을 detailSettings에 포함 if (column.input_type === "code" && column.hierarchy_role) { let existingSettings: Record = {}; if (typeof finalDetailSettings === "string" && finalDetailSettings.trim().startsWith("{")) { try { existingSettings = JSON.parse(finalDetailSettings); } catch { existingSettings = {}; } } const codeSettings = { ...existingSettings, hierarchy_role: column.hierarchy_role, }; finalDetailSettings = JSON.stringify(codeSettings); } return { column_name: column.column_name, column_label: column.display_name, input_type: column.input_type || "text", detail_settings: finalDetailSettings, description: column.description || "", code_info: column.code_info || "", code_value: column.code_value || "", reference_table: column.reference_table || "", reference_column: column.reference_column || "", display_column: column.display_column || "", category_ref: column.category_ref || null, }; }); // console.log("저장할 전체 설정:", { tableLabel, tableDescription, columnSettings }); // 전체 테이블 설정을 한 번에 저장 const response = await apiClient.post( `/table-management/tables/${selectedTable}/columns/settings`, columnSettings, ); if (response.data.success) { // 자체 카테고리 컬럼만 메뉴 매핑 처리 (참조 컬럼 제외) const categoryColumns = columns.filter((col) => col.input_type === "category" && !col.category_ref); console.log("📥 전체 저장: 카테고리 컬럼 확인", { totalColumns: columns.length, categoryColumns: categoryColumns.length, categoryColumnsData: categoryColumns.map((col) => ({ columnName: col.column_name, categoryMenus: col.category_menus, })), }); if (categoryColumns.length > 0) { let totalSuccessCount = 0; let totalFailCount = 0; for (const column of categoryColumns) { // 1. 먼저 기존 매핑 모두 삭제 console.log("🗑️ 기존 카테고리 메뉴 매핑 삭제:", { tableName: selectedTable, columnName: column.column_name, }); try { const deleteResponse = await deleteColumnMappingsByColumn(selectedTable, column.column_name); console.log("🗑️ 기존 매핑 삭제 결과:", deleteResponse); } catch (error) { console.error("❌ 기존 매핑 삭제 실패:", error); } // 2. 새로운 매핑 추가 (선택된 메뉴가 있는 경우만) if (column.category_menus && column.category_menus.length > 0) { for (const menuObjid of column.category_menus) { try { console.log("🔄 매핑 API 호출:", { tableName: selectedTable, columnName: column.column_name, menuObjid, }); const mappingResponse = await createColumnMapping({ tableName: selectedTable, logicalColumnName: column.column_name, physicalColumnName: column.column_name, menuObjid, description: `${column.display_name} (메뉴별 카테고리)`, }); console.log("✅ 매핑 API 응답:", mappingResponse); if (mappingResponse.success) { totalSuccessCount++; } else { console.error("❌ 매핑 생성 실패:", mappingResponse); totalFailCount++; } } catch (error) { console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error); totalFailCount++; } } } } console.log("📊 전체 매핑 결과:", { totalSuccessCount, totalFailCount }); if (totalSuccessCount > 0) { toast.success(`테이블 설정 및 ${totalSuccessCount}개 카테고리 메뉴 매핑이 저장되었습니다.`); } else if (totalFailCount > 0) { toast.warning(`테이블 설정은 저장되었으나 ${totalFailCount}개 메뉴 매핑 생성 실패.`); } else { toast.success(`테이블 '${selectedTable}' 설정이 모두 저장되었습니다.`); } } else { toast.success(`테이블 '${selectedTable}' 설정이 모두 저장되었습니다.`); } // 저장 성공 후 원본 데이터 업데이트 setOriginalColumns([...columns]); // 테이블 목록 새로고침 (라벨 변경 반영) loadTables(); // 저장 후 데이터 다시 로드 setTimeout(() => { loadColumnTypes(selectedTable, 1, pageSize); }, 1000); } else { showErrorToast("설정 저장에 실패했습니다", response.data.message, { guidance: "잠시 후 다시 시도해 주세요.", }); } } } catch (error) { // console.error("설정 저장 실패:", error); showErrorToast("설정 저장에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요.", }); } finally { setIsSaving(false); } }; // Ctrl+S 단축키: 테이블 설정 전체 저장 // saveAllSettings를 ref로 참조하여 useEffect 의존성 문제 방지 const saveAllSettingsRef = useRef(saveAllSettings); saveAllSettingsRef.current = saveAllSettings; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); // 브라우저 기본 저장 동작 방지 if (selectedTable && columns.length > 0) { saveAllSettingsRef.current(); } } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [selectedTable, columns.length]); // 필터링 + 한글 우선 정렬 (ㄱ~ㅎ → a~z) const filteredTables = useMemo(() => { const filtered = tables.filter( (table) => (table.table_name ?? '').toLowerCase().includes(searchTerm.toLowerCase()) || (table.display_name ?? '').toLowerCase().includes(searchTerm.toLowerCase()), ); const isKorean = (str: string) => /^[가-힣ㄱ-ㅎ]/.test(str); const q = searchTerm.trim().toLowerCase(); // 검색 매치 강도: 0=정확, 1=시작, 2=포함 — 낮을수록 위 const matchScore = (t: typeof tables[number]) => { if (!q) return 0; const tn = (t.table_name ?? "").toLowerCase(); const dn = (t.display_name ?? "").toLowerCase(); if (tn === q || dn === q) return 0; if (tn.startsWith(q) || dn.startsWith(q)) return 1; return 2; }; return filtered.sort((a, b) => { const sa = matchScore(a); const sb = matchScore(b); if (sa !== sb) return sa - sb; const nameA = a.display_name || a.table_name; const nameB = b.display_name || b.table_name; const aKo = isKorean(nameA); const bKo = isKorean(nameB); if (aKo && !bKo) return -1; if (!aKo && bKo) return 1; return nameA.localeCompare(nameB, aKo ? "ko" : "en"); }); }, [tables, searchTerm]); // 선택된 테이블 정보 const selectedTableInfo = tables.find((table) => table.table_name === selectedTable); useEffect(() => { loadTables(); loadCommonCodeCategories(); loadSecondLevelMenus(); }, []); // 🎯 컬럼 로드 후 이미 설정된 참조 테이블들의 컬럼 정보 로드 useEffect(() => { if (columns.length > 0) { const entityColumns = columns.filter( (col) => col.input_type === "entity" && col.reference_table && col.reference_table !== "none", ); entityColumns.forEach((col) => { if (col.reference_table) { // console.log(`🎯 기존 Entity 컬럼 발견, 참조 테이블 컬럼 로드: ${col.column_name} -> ${col.reference_table}`); loadReferenceTableColumns(col.reference_table); } }); } }, [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]); // PK 체크박스 변경 핸들러 const handlePkToggle = useCallback( (columnName: string, checked: boolean) => { const currentPkCols = [...(constraints.primaryKey?.columns ?? [])]; let newPkCols: string[]; if (checked) { newPkCols = [...currentPkCols, columnName]; } else { newPkCols = currentPkCols.filter((c) => c !== columnName); } // PK 변경은 확인 다이얼로그 표시 setPendingPkColumns(newPkCols); setPkDialogOpen(true); }, [constraints.primaryKey?.columns], ); // PK 변경 확인 const handlePkConfirm = async () => { if (!selectedTable) return; try { if (pendingPkColumns.length === 0) { toast.error("PK 컬럼을 최소 1개 이상 선택해야 합니다."); setPkDialogOpen(false); return; } const response = await apiClient.put(`/table-management/tables/${selectedTable}/primary-key`, { columns: pendingPkColumns, }); if (response.data.success) { toast.success(response.data.message); await loadConstraints(selectedTable); } else { toast.error(response.data.message || "PK 설정 실패"); } } catch (error: any) { showErrorToast("PK 설정에 실패했습니다", error, { guidance: "컬럼 정보를 확인하고 다시 시도해 주세요.", }); } finally { setPkDialogOpen(false); } }; // 인덱스 토글 핸들러 (일반 인덱스만 DB 레벨 - 유니크는 앱 레벨 소프트 제약조건으로 분리됨) const handleIndexToggle = useCallback( async (columnName: string, indexType: "index", checked: boolean) => { if (!selectedTable) return; const action = checked ? "create" : "drop"; try { const response = await apiClient.post(`/table-management/tables/${selectedTable}/indexes`, { column_name: columnName, index_type: indexType, action, }); if (response.data.success) { toast.success(response.data.message); await loadConstraints(selectedTable); } else { toast.error(response.data.message || "인덱스 설정 실패"); } } catch (error: any) { showErrorToast("인덱스 설정에 실패했습니다", error, { guidance: "컬럼 정보를 확인하고 다시 시도해 주세요.", }); } }, [selectedTable, loadConstraints], ); // 컬럼별 인덱스 상태 헬퍼 const getColumnIndexState = useCallback( (columnName: string) => { const isPk = (constraints.primaryKey?.columns ?? []).includes(columnName); const hasIndex = constraints.indexes.some( (idx) => !idx.is_unique && idx.columns.length === 1 && idx.columns[0] === columnName, ); return { isPk, hasIndex }; }, [constraints], ); // UNIQUE 토글 핸들러 (앱 레벨 소프트 제약조건 - NOT NULL과 동일 패턴) const handleUniqueToggle = useCallback( async (columnName: string, currentIsUnique: string) => { if (!selectedTable) return; const isCurrentlyUnique = currentIsUnique === "YES"; const newUnique = !isCurrentlyUnique; try { const response = await apiClient.put( `/table-management/tables/${selectedTable}/columns/${columnName}/unique`, { unique: newUnique }, ); if (response.data.success) { toast.success(response.data.message); setColumns((prev) => prev.map((col) => col.column_name === columnName ? { ...col, is_unique: newUnique ? "YES" : "NO" } : col, ), ); } else { showErrorToast("UNIQUE 제약 조건 설정에 실패했습니다", response.data.message, { guidance: "해당 컬럼에 중복 데이터가 없는지 확인해 주세요.", }); } } catch (error: any) { showErrorToast("UNIQUE 제약 조건 설정에 실패했습니다", error, { guidance: "해당 컬럼에 중복 데이터가 없는지 확인해 주세요.", }); } }, [selectedTable], ); // NOT NULL 토글 핸들러 const handleNullableToggle = useCallback( async (columnName: string, currentIsNullable: string) => { if (!selectedTable) return; // isNullable이 "YES"면 nullable, "NO"면 NOT NULL // 체크박스 체크 = NOT NULL 설정 (nullable: false) // 체크박스 해제 = NOT NULL 해제 (nullable: true) const isCurrentlyNotNull = currentIsNullable === "NO"; const newNullable = isCurrentlyNotNull; // NOT NULL이면 해제, NULL이면 설정 try { const response = await apiClient.put( `/table-management/tables/${selectedTable}/columns/${columnName}/nullable`, { nullable: newNullable }, ); if (response.data.success) { toast.success(response.data.message); // 컬럼 상태 로컬 업데이트 setColumns((prev) => prev.map((col) => col.column_name === columnName ? { ...col, is_nullable: newNullable ? "YES" : "NO" } : col, ), ); } else { showErrorToast("NOT NULL 제약 조건 설정에 실패했습니다", response.data.message, { guidance: "해당 컬럼에 NULL 값이 없는지 확인해 주세요.", }); } } catch (error: any) { showErrorToast("NOT NULL 제약 조건 설정에 실패했습니다", error, { guidance: "해당 컬럼에 NULL 값이 없는지 확인해 주세요.", }); } }, [selectedTable], ); // 테이블 삭제 확인 const handleDeleteTableClick = (tableName: string) => { setTableToDelete(tableName); setDeleteDialogOpen(true); }; // 컬럼 삭제 (DBeaver 방식 — FK 참조 있으면 Postgres 가 거부) const handleDeleteColumnClick = (columnName: string) => { setColumnToDelete(columnName); setDeleteColumnDialogOpen(true); }; const handleDeleteColumn = async () => { if (!selectedTable || !columnToDelete) return; setIsDeletingColumn(true); try { const result = await ddlApi.dropColumn(selectedTable, columnToDelete); if (result.success) { toast.success(`컬럼 '${columnToDelete}'이 삭제되었습니다.`); if (selectedColumn === columnToDelete) setSelectedColumn(null); await loadColumnTypes(selectedTable); } else { showErrorToast("컬럼 삭제에 실패했습니다", result.message, { guidance: "다른 테이블에서 외래키로 참조 중이거나 종속 객체가 있는지 확인해 주세요.", }); } } catch (error) { showErrorToast("컬럼 삭제에 실패했습니다", error, { guidance: "다른 테이블에서 외래키로 참조 중이거나 종속 객체가 있는지 확인해 주세요.", }); } finally { setIsDeletingColumn(false); setDeleteColumnDialogOpen(false); setColumnToDelete(""); } }; // 테이블 삭제 실행 const handleDeleteTable = async () => { if (!tableToDelete) return; setIsDeleting(true); try { const result = await ddlApi.dropTable(tableToDelete); if (result.success) { toast.success(`테이블 '${tableToDelete}'이 성공적으로 삭제되었습니다.`); // 삭제된 테이블이 선택된 테이블이었다면 선택 해제 if (selectedTable === tableToDelete) { setSelectedTable(null); setColumns([]); } // 테이블 목록 새로고침 await loadTables(); } else { showErrorToast("테이블 삭제에 실패했습니다", result.message, { guidance: "테이블에 종속된 데이터가 있는지 확인해 주세요.", }); } } catch (error: any) { showErrorToast("테이블 삭제에 실패했습니다", error, { guidance: "테이블에 종속된 데이터가 있는지 확인해 주세요.", }); } finally { setIsDeleting(false); setDeleteDialogOpen(false); setTableToDelete(""); } }; // 체크박스 선택 핸들러 const handleTableCheck = (tableName: string, checked: boolean) => { setSelectedTableIds((prev) => { const newSet = new Set(prev); if (checked) { newSet.add(tableName); } else { newSet.delete(tableName); } return newSet; }); }; // 전체 선택/해제 const handleSelectAll = (checked: boolean) => { if (checked) { const filteredTables = tables.filter( (table) => table.table_name.toLowerCase().includes(searchTerm.toLowerCase()) || (table.display_name && table.display_name.toLowerCase().includes(searchTerm.toLowerCase())), ); setSelectedTableIds(new Set(filteredTables.map((table) => table.table_name))); } else { setSelectedTableIds(new Set()); } }; // 일괄 삭제 확인 const handleBulkDeleteClick = () => { if (selectedTableIds.size === 0) return; setDeleteDialogOpen(true); }; // 일괄 삭제 실행 const handleBulkDelete = async () => { if (selectedTableIds.size === 0) return; setIsDeleting(true); try { const tablesToDelete = Array.from(selectedTableIds); let successCount = 0; let failCount = 0; for (const tableName of tablesToDelete) { try { const result = await ddlApi.dropTable(tableName); if (result.success) { successCount++; // 삭제된 테이블이 선택된 테이블이었다면 선택 해제 if (selectedTable === tableName) { setSelectedTable(null); setColumns([]); } } else { failCount++; } } catch (error) { failCount++; } } if (successCount > 0) { toast.success(`${successCount}개의 테이블이 성공적으로 삭제되었습니다.`); } if (failCount > 0) { toast.error(`${failCount}개의 테이블 삭제에 실패했습니다.`); } // 선택 초기화 및 테이블 목록 새로고침 setSelectedTableIds(new Set()); await loadTables(); } catch (error: any) { showErrorToast("테이블 삭제에 실패했습니다", error, { guidance: "테이블에 종속된 데이터가 있는지 확인해 주세요.", }); } finally { setIsDeleting(false); setDeleteDialogOpen(false); setTableToDelete(""); } }; return (
{/* 컴팩트 탑바 (52px) */}

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

{tables.length} 테이블
{isSuperAdmin && ( <> {selectedTable && ( )} )}
{/* 메인 (우측 패널은 overlay 라 2패널 layout) */}
{/* 좌측: 테이블 목록 (240px) */}
{/* 검색 */}
setSearchTerm(e.target.value)} className="bg-background h-[34px] pl-8 text-xs" />
{isSuperAdmin && (
0 && filteredTables.every((table) => selectedTableIds.has(table.table_name)) } onCheckedChange={handleSelectAll} aria-label="전체 선택" className="h-3.5 w-3.5" /> {selectedTableIds.size > 0 ? `${selectedTableIds.size}개` : "전체"}
{selectedTableIds.size > 0 && ( )}
)}
{/* 테이블 리스트 */}
{loading ? (
) : filteredTables.length === 0 ? (
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")}
) : ( filteredTables.map((table, idx) => { const isActive = selectedTable === table.table_name; const prevTable = idx > 0 ? filteredTables[idx - 1] : null; const isKo = /^[가-힣ㄱ-ㅎ]/.test(table.display_name || table.table_name); const prevIsKo = prevTable ? /^[가-힣ㄱ-ㅎ]/.test(prevTable.display_name || prevTable.table_name) : null; const showDivider = idx === 0 || (prevIsKo !== null && isKo !== prevIsKo); return (
{showDivider && (
{isKo ? "한글" : "ENGLISH"}
)}
handleTableSelect(table.table_name)} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleTableSelect(table.table_name); } }} > {isActive && (
)} {isSuperAdmin && ( handleTableCheck(table.table_name, checked as boolean)} aria-label={`${table.display_name || table.table_name} 선택`} className="h-3.5 w-3.5 flex-shrink-0" onClick={(e) => e.stopPropagation()} /> )}
{table.display_name || table.table_name}
{table.table_name}
{table.column_count}
); }) )}
{/* 하단 정보 */}
{filteredTables.length} / {tables.length} 테이블
{/* 중앙: 컬럼 그리드 */}
{!selectedTable ? (

{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}

) : ( <> {/* 중앙 헤더: inline click-to-edit (Google Docs / Notion 패턴) */}
{/* 표시명 (display_name) — 클릭하면 그 자리에서 편집 */} {editingHeaderField === "label" ? ( setEditingHeaderValue(e.target.value)} onBlur={commitHeaderEdit} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); e.currentTarget.blur(); } else if (e.key === "Escape") { setEditingHeaderField(null); } }} className="h-7 -mx-2 px-2 text-[15px] font-bold tracking-tight" /> ) : (
{tableLabel || ( {selectedTable} )}
)} {/* table_name (코드, 편집 불가) */}
{selectedTable}
{/* 설명 (description) — 클릭하면 그 자리에서 편집 */} {editingHeaderField === "description" ? ( setEditingHeaderValue(e.target.value)} onBlur={commitHeaderEdit} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); e.currentTarget.blur(); } else if (e.key === "Escape") { setEditingHeaderField(null); } }} placeholder="이 테이블에 대한 짧은 설명" className="mt-1 h-7 -mx-2 px-2 text-xs" /> ) : (
{tableDescription || ( + 설명 추가 )}
)}
{columnsLoading ? (
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
) : columns.length === 0 ? (
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
) : ( 컬럼 참조 {(() => { const refCount = columns.filter((c) => ["entity", "code", "category", "numbering"].includes(c.input_type), ).length; return refCount > 0 ? ( {refCount} ) : null; })()} setSelectedColumn((prev) => (prev === c ? null : c))} onColumnChange={(columnName, field, value) => { if (field === "is_unique") { const currentColumn = columns.find((c) => c.column_name === columnName); if (currentColumn) { handleUniqueToggle(columnName, currentColumn.is_unique || "NO"); } return; } if (field === "is_nullable") { const currentColumn = columns.find((c) => c.column_name === columnName); if (currentColumn) { handleNullableToggle(columnName, currentColumn.is_nullable || "YES"); } return; } const idx = columns.findIndex((c) => c.column_name === columnName); if (idx >= 0) handleColumnChange(idx, field, value); }} constraints={constraints} typeFilter={typeFilter} getColumnIndexState={getColumnIndexState} onPkToggle={handlePkToggle} onIndexToggle={(columnName, checked) => handleIndexToggle(columnName, "index", checked) } onDeleteColumn={handleDeleteColumnClick} tables={tables} referenceTableColumns={referenceTableColumns} /> setSelectedColumn((prev) => (prev === c ? null : c))} /> )} )}
{/* 우측: 상세 패널 - 와이드 모니터 (xl 이상): 항상 보이는 고정 3-pane - 좁은 화면: 기존처럼 슬라이드 in 오버레이 */}
c.column_name === selectedColumn) ?? null} tables={tables} referenceTableColumns={referenceTableColumns} secondLevelMenus={secondLevelMenus} numberingRules={[]} onColumnChange={(field, value) => { if (!selectedColumn) return; if (field === "input_type") { handleInputTypeChange(selectedColumn, value as string); return; } if (field === "reference_table" && value) { loadReferenceTableColumns(value as string); } setColumns((prev) => prev.map((c) => c.column_name === selectedColumn ? { ...c, [field]: value } : c, ), ); }} onClose={() => setSelectedColumn(null)} onLoadReferenceColumns={loadReferenceTableColumns} codeInfoOptions={commonCodeOptions} referenceTableOptions={referenceTableOptions} />
{/* DDL 모달 컴포넌트들 */} {isSuperAdmin && ( <> { setCreateTableModalOpen(false); setDuplicateModalMode("create"); setDuplicateSourceTable(null); }} onSuccess={async (result) => { const message = duplicateModalMode === "duplicate" ? "테이블이 성공적으로 복제되었습니다!" : "테이블이 성공적으로 생성되었습니다!"; toast.success(message); // 테이블 목록 새로고침 await loadTables(); // 새로 생성된 테이블 자동 선택 및 컬럼 로드 if (result.data?.table_name) { setSelectedTable(result.data.table_name); setCurrentPage(1); setColumns([]); await loadColumnTypes(result.data.table_name, 1, pageSize); } // 선택 초기화 setSelectedTableIds(new Set()); // 상태 초기화 setDuplicateModalMode("create"); setDuplicateSourceTable(null); }} mode={duplicateModalMode} sourceTableName={duplicateSourceTable || undefined} /> setAddColumnModalOpen(false)} tableName={selectedTable || ""} onSuccess={async (result) => { toast.success("컬럼이 성공적으로 추가되었습니다!"); // 테이블 목록 새로고침 (컬럼 수 업데이트) await loadTables(); // 선택된 테이블의 컬럼 목록 새로고침 - 페이지 리셋 if (selectedTable) { setCurrentPage(1); setColumns([]); // 기존 컬럼 목록 초기화 await loadColumnTypes(selectedTable, 1, pageSize); } }} /> setDdlLogViewerOpen(false)} /> {/* 테이블 로그 뷰어 */} {/* 테이블 삭제 확인 다이얼로그 */} {selectedTableIds.size > 0 ? "테이블 일괄 삭제 확인" : "테이블 삭제 확인"} {selectedTableIds.size > 0 ? ( <> 선택된 {selectedTableIds.size}개의 테이블을 삭제하시겠습니까?
이 작업은 되돌릴 수 없습니다. ) : ( <>정말로 테이블을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. )}
{selectedTableIds.size === 0 && tableToDelete && (

경고

테이블 {tableToDelete}과 모든 데이터가 영구적으로 삭제됩니다.

)} {selectedTableIds.size > 0 && (

경고

다음 테이블들과 모든 데이터가 영구적으로 삭제됩니다:

    {Array.from(selectedTableIds).map((tableName) => (
  • {tableName}
  • ))}
)}
{/* 컬럼 삭제 확인 다이얼로그 */} 컬럼 삭제 확인 정말 삭제할까요? 이 작업은 되돌릴 수 없습니다. {columnToDelete && (

경고

{selectedTable}.{columnToDelete} 컬럼과 해당 컬럼의 모든 데이터가 영구적으로 삭제됩니다.

)}
)} {/* PK 변경 확인 다이얼로그 */} PK 변경 확인 PK를 변경하면 기존 제약조건이 삭제되고 새로 생성됩니다.
데이터 무결성에 영향을 줄 수 있습니다.

변경될 PK 컬럼:

{pendingPkColumns.length > 0 ? (
{pendingPkColumns.map((col) => { const colInfo = columns.find((c) => c.column_name === col); return ( {colInfo?.display_name && colInfo.display_name !== col ? `${colInfo.display_name} (${col})` : col} ); })}
) : (

PK가 모두 제거됩니다

)}
{/* Scroll to Top 버튼 */}
); }