2025-08-21 09:41:46 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect } 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 } from "lucide-react";
|
|
|
|
|
import { LoadingSpinner } from "@/components/common/LoadingSpinner";
|
|
|
|
|
import { toast } from "sonner";
|
2025-08-29 10:09:34 +09:00
|
|
|
import { useMultiLang } from "@/hooks/useMultiLang";
|
|
|
|
|
import { TABLE_MANAGEMENT_KEYS, WEB_TYPE_OPTIONS_WITH_KEYS } from "@/constants/tableManagement";
|
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
interface TableInfo {
|
|
|
|
|
tableName: string;
|
|
|
|
|
displayName: string;
|
|
|
|
|
description: string;
|
|
|
|
|
columnCount: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ColumnTypeInfo {
|
|
|
|
|
columnName: string;
|
|
|
|
|
displayName: string;
|
|
|
|
|
dbType: string;
|
|
|
|
|
webType: string;
|
|
|
|
|
detailSettings: string;
|
|
|
|
|
description: string;
|
|
|
|
|
isNullable: string;
|
|
|
|
|
defaultValue?: string;
|
|
|
|
|
maxLength?: number;
|
|
|
|
|
numericPrecision?: number;
|
|
|
|
|
numericScale?: number;
|
|
|
|
|
codeCategory?: string;
|
|
|
|
|
codeValue?: string;
|
|
|
|
|
referenceTable?: string;
|
|
|
|
|
referenceColumn?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function TableManagementPage() {
|
2025-08-29 10:09:34 +09:00
|
|
|
const { userLang, getText } = useMultiLang({ companyCode: "*" });
|
2025-08-21 09:41:46 +09:00
|
|
|
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[]>([]); // 원본 데이터 저장
|
2025-08-29 10:09:34 +09:00
|
|
|
const [uiTexts, setUiTexts] = useState<Record<string, string>>({});
|
2025-08-21 09:41:46 +09:00
|
|
|
|
2025-08-29 10:09:34 +09:00
|
|
|
// 다국어 텍스트 로드
|
|
|
|
|
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 webTypeOptions = WEB_TYPE_OPTIONS_WITH_KEYS.map((option) => ({
|
|
|
|
|
value: option.value,
|
|
|
|
|
label: getTextFromUI(option.labelKey, option.value),
|
|
|
|
|
description: getTextFromUI(option.descriptionKey, option.value),
|
|
|
|
|
}));
|
2025-08-21 09:41:46 +09:00
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
// 웹타입 옵션 확인 (디버깅용)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
console.log("테이블 타입관리 - 웹타입 옵션 로드됨:", webTypeOptions);
|
|
|
|
|
console.log("테이블 타입관리 - 웹타입 옵션 개수:", webTypeOptions.length);
|
|
|
|
|
webTypeOptions.forEach((option, index) => {
|
|
|
|
|
console.log(`${index + 1}. ${option.value}: ${option.label}`);
|
|
|
|
|
});
|
|
|
|
|
}, [webTypeOptions]);
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
// 참조 테이블 옵션 (실제 테이블 목록에서 가져옴)
|
|
|
|
|
const referenceTableOptions = [
|
2025-08-29 10:09:34 +09:00
|
|
|
{ value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_NONE, "선택 안함") },
|
2025-08-21 09:41:46 +09:00
|
|
|
...tables.map((table) => ({ value: table.tableName, label: table.displayName || table.tableName })),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 공통 코드 옵션 (예시 - 실제로는 API에서 가져와야 함)
|
|
|
|
|
const commonCodeOptions = [
|
2025-08-29 10:09:34 +09:00
|
|
|
{ value: "none", label: getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_CODE_PLACEHOLDER, "코드 선택") },
|
2025-08-21 09:41:46 +09:00
|
|
|
{ value: "USER_STATUS", label: "사용자 상태" },
|
|
|
|
|
{ value: "DEPT_TYPE", label: "부서 유형" },
|
|
|
|
|
{ value: "PRODUCT_CATEGORY", label: "제품 카테고리" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 테이블 목록 로드
|
|
|
|
|
const loadTables = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
2025-08-29 10:09:34 +09:00
|
|
|
const response = await apiClient.get("/table-management/tables");
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// 응답 상태 확인
|
2025-08-29 10:09:34 +09:00
|
|
|
if (response.data.success) {
|
|
|
|
|
setTables(response.data.data);
|
2025-08-21 09:41:46 +09:00
|
|
|
toast.success("테이블 목록을 성공적으로 로드했습니다.");
|
|
|
|
|
} else {
|
2025-08-29 10:09:34 +09:00
|
|
|
toast.error(response.data.message || "테이블 목록 로드에 실패했습니다.");
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
|
|
|
toast.error("테이블 목록 로드 중 오류가 발생했습니다.");
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 컬럼 타입 정보 로드
|
|
|
|
|
const loadColumnTypes = async (tableName: string) => {
|
|
|
|
|
setColumnsLoading(true);
|
|
|
|
|
try {
|
2025-08-29 10:09:34 +09:00
|
|
|
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
|
2025-08-21 09:41:46 +09:00
|
|
|
|
|
|
|
|
// 응답 상태 확인
|
2025-08-29 10:09:34 +09:00
|
|
|
if (response.data.success) {
|
|
|
|
|
setColumns(response.data.data);
|
|
|
|
|
setOriginalColumns(response.data.data); // 원본 데이터 저장
|
2025-08-21 09:41:46 +09:00
|
|
|
toast.success("컬럼 정보를 성공적으로 로드했습니다.");
|
|
|
|
|
} else {
|
2025-08-29 10:09:34 +09:00
|
|
|
toast.error(response.data.message || "컬럼 정보 로드에 실패했습니다.");
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("컬럼 타입 정보 로드 실패:", error);
|
|
|
|
|
toast.error("컬럼 정보 로드 중 오류가 발생했습니다.");
|
|
|
|
|
} finally {
|
|
|
|
|
setColumnsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 테이블 선택
|
|
|
|
|
const handleTableSelect = (tableName: string) => {
|
|
|
|
|
setSelectedTable(tableName);
|
|
|
|
|
loadColumnTypes(tableName);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 웹 타입 변경
|
|
|
|
|
const handleWebTypeChange = (columnName: string, newWebType: string) => {
|
|
|
|
|
setColumns((prev) =>
|
|
|
|
|
prev.map((col) => {
|
|
|
|
|
if (col.columnName === columnName) {
|
|
|
|
|
const webTypeOption = webTypeOptions.find((option) => option.value === newWebType);
|
|
|
|
|
return {
|
|
|
|
|
...col,
|
|
|
|
|
webType: newWebType,
|
|
|
|
|
detailSettings: webTypeOption?.description || col.detailSettings,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return col;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 상세 설정 변경 (코드/엔티티 타입용)
|
|
|
|
|
const handleDetailSettingsChange = (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;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
} else {
|
|
|
|
|
const tableOption = referenceTableOptions.find((option) => option.value === value);
|
|
|
|
|
newDetailSettings = tableOption ? `참조테이블: ${tableOption.label}` : "";
|
|
|
|
|
referenceTable = value;
|
|
|
|
|
referenceColumn = "id"; // 기본값, 나중에 선택할 수 있도록 개선 가능
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...col,
|
|
|
|
|
detailSettings: newDetailSettings,
|
|
|
|
|
codeCategory,
|
|
|
|
|
codeValue,
|
|
|
|
|
referenceTable,
|
|
|
|
|
referenceColumn,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return col;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 라벨 변경 핸들러 추가
|
|
|
|
|
const handleLabelChange = (columnName: string, newLabel: string) => {
|
|
|
|
|
setColumns((prev) =>
|
|
|
|
|
prev.map((col) => {
|
|
|
|
|
if (col.columnName === columnName) {
|
|
|
|
|
return {
|
|
|
|
|
...col,
|
|
|
|
|
displayName: newLabel,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return col;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-29 10:09:34 +09:00
|
|
|
// 컬럼 변경 핸들러 (인덱스 기반)
|
|
|
|
|
const handleColumnChange = (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 = {
|
2025-09-01 11:48:12 +09:00
|
|
|
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
|
|
|
|
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
|
|
|
|
webType: column.webType || "text",
|
|
|
|
|
detailSettings: column.detailSettings || "",
|
|
|
|
|
codeCategory: column.codeCategory || "",
|
|
|
|
|
codeValue: column.codeValue || "",
|
|
|
|
|
referenceTable: column.referenceTable || "",
|
|
|
|
|
referenceColumn: column.referenceColumn || "",
|
2025-08-29 10:09:34 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
console.log("저장할 컬럼 설정:", columnSetting);
|
|
|
|
|
|
2025-08-29 10:09:34 +09:00
|
|
|
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)));
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
|
|
|
// 저장 후 데이터 확인을 위해 다시 로드
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
loadColumnTypes(selectedTable);
|
|
|
|
|
}, 1000);
|
2025-08-29 10:09:34 +09:00
|
|
|
} else {
|
|
|
|
|
toast.error(response.data.message || "컬럼 설정 저장에 실패했습니다.");
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("컬럼 설정 저장 실패:", error);
|
|
|
|
|
toast.error("컬럼 설정 저장 중 오류가 발생했습니다.");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
// 모든 컬럼 설정 저장
|
|
|
|
|
const saveAllColumnSettings = async () => {
|
|
|
|
|
if (!selectedTable || columns.length === 0) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 모든 컬럼의 설정 데이터 준비
|
|
|
|
|
const columnSettings = columns.map((column) => ({
|
2025-09-01 11:48:12 +09:00
|
|
|
columnName: column.columnName, // 실제 DB 컬럼명 (변경 불가)
|
|
|
|
|
columnLabel: column.displayName, // 사용자가 입력한 표시명
|
|
|
|
|
webType: column.webType || "text",
|
|
|
|
|
detailSettings: column.detailSettings || "",
|
|
|
|
|
codeCategory: column.codeCategory || "",
|
|
|
|
|
codeValue: column.codeValue || "",
|
|
|
|
|
referenceTable: column.referenceTable || "",
|
|
|
|
|
referenceColumn: column.referenceColumn || "",
|
2025-08-21 09:41:46 +09:00
|
|
|
}));
|
|
|
|
|
|
2025-09-01 11:48:12 +09:00
|
|
|
console.log("저장할 전체 컬럼 설정:", columnSettings);
|
|
|
|
|
|
2025-08-21 09:41:46 +09:00
|
|
|
// 전체 테이블 설정을 한 번에 저장
|
2025-08-29 10:09:34 +09:00
|
|
|
const response = await apiClient.post(
|
|
|
|
|
`/table-management/tables/${selectedTable}/columns/settings`,
|
|
|
|
|
columnSettings,
|
2025-08-21 09:41:46 +09:00
|
|
|
);
|
|
|
|
|
|
2025-08-29 10:09:34 +09:00
|
|
|
if (response.data.success) {
|
|
|
|
|
// 저장 성공 후 원본 데이터 업데이트
|
|
|
|
|
setOriginalColumns([...columns]);
|
|
|
|
|
toast.success(`${columns.length}개의 컬럼 설정이 성공적으로 저장되었습니다.`);
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
|
|
|
// 저장 후 데이터 확인을 위해 다시 로드
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
loadColumnTypes(selectedTable);
|
|
|
|
|
}, 1000);
|
2025-08-29 10:09:34 +09:00
|
|
|
} else {
|
|
|
|
|
toast.error(response.data.message || "컬럼 설정 저장에 실패했습니다.");
|
2025-08-21 09:41:46 +09:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("컬럼 설정 저장 실패:", error);
|
|
|
|
|
toast.error("컬럼 설정 저장 중 오류가 발생했습니다.");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 필터링된 테이블 목록
|
|
|
|
|
const filteredTables = tables.filter(
|
|
|
|
|
(table) =>
|
|
|
|
|
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
|
|
|
table.displayName.toLowerCase().includes(searchTerm.toLowerCase()),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 선택된 테이블 정보
|
|
|
|
|
const selectedTableInfo = tables.find((table) => table.tableName === selectedTable);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadTables();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return (
|
2025-08-29 10:09:34 +09:00
|
|
|
<div className="container mx-auto space-y-6 p-6">
|
|
|
|
|
{/* 페이지 제목 */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
2025-08-21 09:41:46 +09:00
|
|
|
<div>
|
2025-08-29 10:09:34 +09:00
|
|
|
<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>
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
2025-08-29 10:09:34 +09:00
|
|
|
<Button onClick={loadTables} disabled={loading} className="flex items-center gap-2">
|
|
|
|
|
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.BUTTON_REFRESH, "새로고침")}
|
|
|
|
|
</Button>
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-08-29 10:09:34 +09:00
|
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
|
|
|
{/* 테이블 목록 */}
|
|
|
|
|
<Card className="lg:col-span-1">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<Database className="h-5 w-5" />
|
|
|
|
|
{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">
|
2025-08-21 09:41:46 +09:00
|
|
|
{loading ? (
|
2025-08-29 10:09:34 +09:00
|
|
|
<div className="flex items-center justify-center py-8">
|
2025-08-21 09:41:46 +09:00
|
|
|
<LoadingSpinner />
|
2025-08-29 10:09:34 +09:00
|
|
|
<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, "테이블이 없습니다")}
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
|
|
|
|
) : (
|
2025-08-29 10:09:34 +09:00
|
|
|
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>
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
2025-08-29 10:09:34 +09:00
|
|
|
<Badge variant="secondary">
|
|
|
|
|
{table.columnCount} {getTextFromUI(TABLE_MANAGEMENT_KEYS.TABLE_COLUMN_COUNT, "컬럼")}
|
|
|
|
|
</Badge>
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
2025-08-29 10:09:34 +09:00
|
|
|
</div>
|
|
|
|
|
))
|
2025-08-21 09:41:46 +09:00
|
|
|
)}
|
2025-08-29 10:09:34 +09:00
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* 컬럼 타입 관리 */}
|
|
|
|
|
<Card className="lg:col-span-2">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="flex items-center gap-2">
|
|
|
|
|
<Settings className="h-5 w-5" />
|
|
|
|
|
{selectedTable ? (
|
|
|
|
|
<>
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NAME, "컬럼")} - {selectedTable}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NAME, "컬럼 타입 관리")
|
|
|
|
|
)}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{!selectedTable ? (
|
|
|
|
|
<div className="py-12 text-center text-gray-500">
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.SELECT_TABLE_PLACEHOLDER, "테이블을 선택해주세요")}
|
|
|
|
|
</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, "컬럼 정보 로딩 중...")}
|
2025-08-21 09:41:46 +09:00
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-08-29 10:09:34 +09:00
|
|
|
) : columns.length === 0 ? (
|
|
|
|
|
<div className="py-8 text-center text-gray-500">
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.MESSAGE_NO_COLUMNS, "컬럼이 없습니다")}
|
|
|
|
|
</div>
|
2025-08-21 09:41:46 +09:00
|
|
|
) : (
|
2025-08-29 10:09:34 +09:00
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NAME, "컬럼명")}</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DISPLAY_NAME, "표시명")}</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DB_TYPE, "DB 타입")}</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_WEB_TYPE, "웹 타입")}</TableHead>
|
|
|
|
|
<TableHead>
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DETAIL_SETTINGS, "상세 설정")}
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DESCRIPTION, "설명")}</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NULLABLE, "NULL 허용")}</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_DEFAULT_VALUE, "기본값")}</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_MAX_LENGTH, "최대 길이")}</TableHead>
|
|
|
|
|
<TableHead>
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NUMERIC_PRECISION, "정밀도")}
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_NUMERIC_SCALE, "소수점")}</TableHead>
|
|
|
|
|
<TableHead>
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_CODE_CATEGORY, "코드 카테고리")}
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead>{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_CODE_VALUE, "코드 값")}</TableHead>
|
|
|
|
|
<TableHead>
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_REFERENCE_TABLE, "참조 테이블")}
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead>
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.COLUMN_REFERENCE_COLUMN, "참조 컬럼")}
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead>Actions</TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{columns.map((column, index) => (
|
|
|
|
|
<TableRow key={column.columnName}>
|
|
|
|
|
<TableCell className="font-mono text-sm">{column.columnName}</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Input
|
|
|
|
|
value={column.displayName || ""}
|
|
|
|
|
onChange={(e) => handleColumnChange(index, "displayName", e.target.value)}
|
|
|
|
|
placeholder={column.columnName}
|
|
|
|
|
className="w-32"
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="font-mono text-sm">{column.dbType}</TableCell>
|
|
|
|
|
<TableCell>
|
2025-09-01 11:48:12 +09:00
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Select
|
|
|
|
|
value={column.webType || "text"}
|
|
|
|
|
onValueChange={(value) => handleWebTypeChange(column.columnName, value)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-32">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{webTypeOptions.map((option) => (
|
|
|
|
|
<SelectItem key={option.value} value={option.value}>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="font-medium">{option.label}</div>
|
|
|
|
|
<div className="text-xs text-gray-500">{option.description}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
{/* 웹타입 옵션 개수 표시 */}
|
|
|
|
|
<div className="text-xs text-gray-500">
|
|
|
|
|
사용 가능한 웹타입: {webTypeOptions.length}개
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-08-29 10:09:34 +09:00
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Input
|
|
|
|
|
value={column.detailSettings || ""}
|
|
|
|
|
onChange={(e) => handleColumnChange(index, "detailSettings", e.target.value)}
|
|
|
|
|
placeholder="상세 설정"
|
|
|
|
|
className="w-32"
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Input
|
|
|
|
|
value={column.description || ""}
|
|
|
|
|
onChange={(e) => handleColumnChange(index, "description", e.target.value)}
|
|
|
|
|
placeholder="설명"
|
|
|
|
|
className="w-32"
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Badge variant={column.isNullable === "YES" ? "default" : "secondary"}>
|
|
|
|
|
{column.isNullable === "YES"
|
|
|
|
|
? getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_YES, "예")
|
|
|
|
|
: getTextFromUI(TABLE_MANAGEMENT_KEYS.LABEL_NO, "아니오")}
|
|
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="font-mono text-sm">{column.defaultValue || "-"}</TableCell>
|
|
|
|
|
<TableCell className="text-center">{column.maxLength || "-"}</TableCell>
|
|
|
|
|
<TableCell className="text-center">{column.numericPrecision || "-"}</TableCell>
|
|
|
|
|
<TableCell className="text-center">{column.numericScale || "-"}</TableCell>
|
|
|
|
|
<TableCell>
|
2025-08-21 09:41:46 +09:00
|
|
|
<Select
|
|
|
|
|
value={column.codeCategory || "none"}
|
2025-08-29 10:09:34 +09:00
|
|
|
onValueChange={(value) => handleColumnChange(index, "codeCategory", value)}
|
2025-08-21 09:41:46 +09:00
|
|
|
>
|
2025-08-29 10:09:34 +09:00
|
|
|
<SelectTrigger className="w-32">
|
|
|
|
|
<SelectValue />
|
2025-08-21 09:41:46 +09:00
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{commonCodeOptions.map((option) => (
|
|
|
|
|
<SelectItem key={option.value} value={option.value}>
|
|
|
|
|
{option.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-08-29 10:09:34 +09:00
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Input
|
|
|
|
|
value={column.codeValue || ""}
|
|
|
|
|
onChange={(e) => handleColumnChange(index, "codeValue", e.target.value)}
|
|
|
|
|
placeholder="코드 값"
|
|
|
|
|
className="w-32"
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
2025-08-21 09:41:46 +09:00
|
|
|
<Select
|
|
|
|
|
value={column.referenceTable || "none"}
|
2025-08-29 10:09:34 +09:00
|
|
|
onValueChange={(value) => handleColumnChange(index, "referenceTable", value)}
|
2025-08-21 09:41:46 +09:00
|
|
|
>
|
2025-08-29 10:09:34 +09:00
|
|
|
<SelectTrigger className="w-32">
|
|
|
|
|
<SelectValue />
|
2025-08-21 09:41:46 +09:00
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{referenceTableOptions.map((option) => (
|
|
|
|
|
<SelectItem key={option.value} value={option.value}>
|
|
|
|
|
{option.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-08-29 10:09:34 +09:00
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Input
|
|
|
|
|
value={column.referenceColumn || ""}
|
|
|
|
|
onChange={(e) => handleColumnChange(index, "referenceColumn", e.target.value)}
|
|
|
|
|
placeholder="참조 컬럼"
|
|
|
|
|
className="w-32"
|
|
|
|
|
/>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => handleSaveColumn(column)}
|
|
|
|
|
className="flex items-center gap-1"
|
|
|
|
|
>
|
|
|
|
|
<Settings className="h-3 w-3" />
|
|
|
|
|
{getTextFromUI(TABLE_MANAGEMENT_KEYS.BUTTON_SAVE, "저장")}
|
|
|
|
|
</Button>
|
2025-08-21 09:41:46 +09:00
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
2025-08-29 10:09:34 +09:00
|
|
|
))}
|
2025-08-21 09:41:46 +09:00
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
2025-08-29 10:09:34 +09:00
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2025-08-21 09:41:46 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|