ERP-node/frontend/app/(main)/admin/tableMng/page.tsx

980 lines
42 KiB
TypeScript

"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<TableInfo[]>([]);
const [columns, setColumns] = useState<ColumnTypeInfo[]>([]);
const [selectedTable, setSelectedTable] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
const [loading, setLoading] = useState(false);
const [columnsLoading, setColumnsLoading] = useState(false);
const [originalColumns, setOriginalColumns] = useState<ColumnTypeInfo[]>([]); // 원본 데이터 저장
const [uiTexts, setUiTexts] = useState<Record<string, string>>({});
// 페이지네이션 상태
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<Record<string, ReferenceTableColumn[]>>({});
// 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<Array<{ value: string; label: string }>>([]);
// 공통 코드 옵션
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 (
<div className="w-full max-w-none px-4 py-8 space-y-8">
{/* 페이지 제목 */}
<div className="flex items-center justify-between bg-white rounded-lg shadow-sm border p-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_TITLE, "테이블 타입 관리")}
</h1>
<p className="mt-2 text-gray-600">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.PAGE_DESCRIPTION, "데이터베이스 테이블과 컬럼의 타입을 관리합니다")}
</p>
{isSuperAdmin && (
<p className="mt-1 text-sm font-medium text-blue-600">
🔧
</p>
)}
</div>
<div className="flex items-center gap-2">
{/* DDL 기능 버튼들 (최고 관리자만) */}
{isSuperAdmin && (
<>
<Button
onClick={() => setCreateTableModalOpen(true)}
className="bg-green-600 text-white hover:bg-green-700"
size="sm"
>
<Plus className="mr-2 h-4 w-4" />
</Button>
{selectedTable && (
<Button onClick={() => setAddColumnModalOpen(true)} variant="outline" size="sm">
<Plus className="mr-2 h-4 w-4" />
</Button>
)}
<Button onClick={() => setDdlLogViewerOpen(true)} variant="outline" size="sm">
<Activity className="mr-2 h-4 w-4" />
DDL
</Button>
</>
)}
<Button onClick={loadTables} disabled={loading} className="flex items-center gap-2" size="sm">
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
{getTextFromUI(TABLE_MANAGEMENT_KEYS.BUTTON_REFRESH, "새로고침")}
</Button>
</div>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5">
{/* 테이블 목록 */}
<Card className="lg:col-span-1 shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5 text-gray-600" />
{getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_NAME, "테이블 목록")}
</CardTitle>
</CardHeader>
<CardContent>
{/* 검색 */}
<div className="mb-4">
<div className="relative">
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
<Input
placeholder={getTextFromUI(TABLE_MANAGEMENT_KEYS.SEARCH_PLACEHOLDER, "테이블 검색...")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
{/* 테이블 목록 */}
<div className="max-h-96 space-y-2 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center py-8">
<LoadingSpinner />
<span className="ml-2 text-sm text-gray-500">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_TABLES, "테이블 로딩 중...")}
</span>
</div>
) : tables.length === 0 ? (
<div className="py-8 text-center text-gray-500">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_TABLES, "테이블이 없습니다")}
</div>
) : (
tables
.filter(
(table) =>
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
(table.displayName && table.displayName.toLowerCase().includes(searchTerm.toLowerCase())),
)
.map((table) => (
<div
key={table.tableName}
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
selectedTable === table.tableName
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
}`}
onClick={() => handleTableSelect(table.tableName)}
>
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-gray-900">{table.displayName || table.tableName}</h3>
<p className="text-sm text-gray-500">
{table.description || getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_DESCRIPTION, "설명 없음")}
</p>
</div>
<Badge variant="secondary">
{table.columnCount} {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_COLUMN_COUNT, "컬럼")}
</Badge>
</div>
</div>
))
)}
</div>
</CardContent>
</Card>
{/* 컬럼 타입 관리 */}
<Card className="lg:col-span-4 shadow-sm">
<CardHeader className="bg-gray-50/50">
<CardTitle className="flex items-center gap-2">
<Settings className="h-5 w-5 text-gray-600" />
{selectedTable ? <> - {selectedTable}</> : "테이블 타입 관리"}
</CardTitle>
</CardHeader>
<CardContent>
{!selectedTable ? (
<div className="py-12 text-center text-gray-500">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
</div>
) : (
<>
{/* 테이블 라벨 설정 */}
<div className="mb-6 space-y-4 rounded-lg border border-gray-200 p-4">
<h3 className="text-lg font-medium text-gray-900"> </h3>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className="mb-1 block text-sm font-medium text-gray-700"> ( )</label>
<Input value={selectedTable} disabled className="bg-gray-50" />
</div>
<div>
<label className="mb-1 block text-sm font-medium text-gray-700"></label>
<Input
value={tableLabel}
onChange={(e) => setTableLabel(e.target.value)}
placeholder="테이블 표시명을 입력하세요"
/>
</div>
<div className="md:col-span-2">
<label className="mb-1 block text-sm font-medium text-gray-700"></label>
<Input
value={tableDescription}
onChange={(e) => setTableDescription(e.target.value)}
placeholder="테이블 설명을 입력하세요"
/>
</div>
</div>
</div>
{columnsLoading ? (
<div className="flex items-center justify-center py-8">
<LoadingSpinner />
<span className="ml-2 text-sm text-gray-500">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_LOADING_COLUMNS, "컬럼 정보 로딩 중...")}
</span>
</div>
) : columns.length === 0 ? (
<div className="py-8 text-center text-gray-500">
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
</div>
) : (
<div className="space-y-4">
{/* 컬럼 헤더 */}
<div className="flex items-center border-b border-gray-200 pb-2 text-sm font-medium text-gray-700">
<div className="w-40 px-4"></div>
<div className="w-48 px-4"></div>
<div className="w-48 px-4"> </div>
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
</div>
<div className="w-80 px-4"></div>
</div>
{/* 컬럼 리스트 */}
<div
className="max-h-96 overflow-y-auto rounded-lg border border-gray-200"
onScroll={(e) => {
const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
// 스크롤이 끝에 가까워지면 더 많은 데이터 로드
if (scrollHeight - scrollTop <= clientHeight + 100) {
loadMoreColumns();
}
}}
>
{columns.map((column, index) => (
<div
key={column.columnName}
className="flex items-center border-b border-gray-200 py-2 hover:bg-gray-50"
>
<div className="w-40 px-4">
<div className="font-mono text-sm text-gray-700">{column.columnName}</div>
</div>
<div className="w-48 px-4">
<Input
value={column.displayName || ""}
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
placeholder={column.columnName}
className="h-7 text-xs"
/>
</div>
<div className="w-48 px-4">
<Select
value={column.inputType || "text"}
onValueChange={(value) => handleInputTypeChange(column.columnName, value)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="입력 타입 선택" />
</SelectTrigger>
<SelectContent>
{memoizedInputTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex-1 px-4" style={{ maxWidth: "calc(100% - 808px)" }}>
{/* 웹 타입이 'code'인 경우 공통코드 선택 */}
{column.inputType === "code" && (
<Select
value={column.codeCategory || "none"}
onValueChange={(value) => handleDetailSettingsChange(column.columnName, "code", value)}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="공통코드 선택" />
</SelectTrigger>
<SelectContent>
{commonCodeOptions.map((option, index) => (
<SelectItem key={`code-${option.value}-${index}`} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
{/* 웹 타입이 'entity'인 경우 참조 테이블 선택 */}
{column.inputType === "entity" && (
<div className="space-y-1">
{/* 🎯 Entity 타입 설정 - 가로 배치 */}
<div className="rounded-lg border border-blue-200 bg-blue-50 p-2">
<div className="mb-2 flex items-center gap-2">
<span className="text-xs font-medium text-blue-800">Entity </span>
</div>
<div className="grid grid-cols-3 gap-2">
{/* 참조 테이블 */}
<div>
<label className="mb-1 block text-xs text-gray-600"> </label>
<Select
value={column.referenceTable || "none"}
onValueChange={(value) =>
handleDetailSettingsChange(column.columnName, "entity", value)
}
>
<SelectTrigger className="h-7 bg-white text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
{referenceTableOptions.map((option, index) => (
<SelectItem key={`entity-${option.value}-${index}`} value={option.value}>
<div className="flex flex-col">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-gray-500">{option.value}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 조인 컬럼 */}
{column.referenceTable && column.referenceTable !== "none" && (
<div>
<label className="mb-1 block text-xs text-gray-600"> </label>
<Select
value={column.referenceColumn || "none"}
onValueChange={(value) =>
handleDetailSettingsChange(
column.columnName,
"entity_reference_column",
value,
)
}
>
<SelectTrigger className="h-7 bg-white text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">-- --</SelectItem>
{referenceTableColumns[column.referenceTable]?.map((refCol, index) => (
<SelectItem
key={`ref-col-${refCol.columnName}-${index}`}
value={refCol.columnName}
>
<span className="font-medium">{refCol.columnName}</span>
</SelectItem>
))}
{(!referenceTableColumns[column.referenceTable] ||
referenceTableColumns[column.referenceTable].length === 0) && (
<SelectItem value="loading" disabled>
<div className="flex items-center gap-2">
<div className="h-3 w-3 animate-spin rounded-full border border-blue-500 border-t-transparent"></div>
</div>
</SelectItem>
)}
</SelectContent>
</Select>
</div>
)}
</div>
{/* 설정 완료 표시 - 간소화 */}
{column.referenceTable &&
column.referenceTable !== "none" &&
column.referenceColumn &&
column.referenceColumn !== "none" &&
column.displayColumn &&
column.displayColumn !== "none" && (
<div className="mt-1 flex items-center gap-1 rounded bg-green-100 px-2 py-1 text-xs text-green-700">
<span className="text-green-600"></span>
<span className="truncate">
{column.columnName} {column.referenceTable}.{column.displayColumn}
</span>
</div>
)}
</div>
</div>
)}
{/* 다른 웹 타입인 경우 빈 공간 */}
{column.inputType !== "code" && column.inputType !== "entity" && (
<div className="flex h-7 items-center text-xs text-gray-400">-</div>
)}
</div>
<div className="w-80 px-4">
<Input
value={column.description || ""}
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
placeholder="설명"
className="h-7 text-xs"
/>
</div>
</div>
))}
</div>
{/* 로딩 표시 */}
{columnsLoading && (
<div className="flex items-center justify-center py-4">
<LoadingSpinner />
<span className="ml-2 text-sm text-gray-500"> ...</span>
</div>
)}
{/* 페이지 정보 */}
<div className="text-center text-sm text-gray-500">
{columns.length} / {totalColumns}
</div>
{/* 전체 저장 버튼 */}
<div className="flex justify-end pt-4">
<Button
onClick={saveAllSettings}
disabled={!selectedTable || columns.length === 0}
className="flex items-center gap-2"
>
<Settings className="h-4 w-4" />
</Button>
</div>
</div>
)}
</>
)}
</CardContent>
</Card>
</div>
{/* DDL 모달 컴포넌트들 */}
{isSuperAdmin && (
<>
<CreateTableModal
isOpen={createTableModalOpen}
onClose={() => 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);
}
}}
/>
<AddColumnModal
isOpen={addColumnModalOpen}
onClose={() => setAddColumnModalOpen(false)}
tableName={selectedTable || ""}
onSuccess={async (result) => {
toast.success("컬럼이 성공적으로 추가되었습니다!");
// 테이블 목록 새로고침 (컬럼 수 업데이트)
await loadTables();
// 선택된 테이블의 컬럼 목록 새로고침 - 페이지 리셋
if (selectedTable) {
setCurrentPage(1);
setColumns([]); // 기존 컬럼 목록 초기화
await loadColumnTypes(selectedTable, 1, pageSize);
}
}}
/>
<DDLLogViewer isOpen={ddlLogViewerOpen} onClose={() => setDdlLogViewerOpen(false)} />
</>
)}
</div>
);
}