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

614 lines
26 KiB
TypeScript

"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";
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() {
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 webTypeOptions = [
{ value: "text", label: "text", description: "일반 텍스트 입력" },
{ value: "number", label: "number", description: "숫자 입력" },
{ value: "date", label: "date", description: "날짜 선택기" },
{ value: "code", label: "code", description: "코드 선택 (공통코드 지정)" },
{ value: "entity", label: "entity", description: "엔티티 참조 (참조테이블 지정)" },
];
// 참조 테이블 옵션 (실제 테이블 목록에서 가져옴)
const referenceTableOptions = [
{ value: "none", label: "테이블 선택" },
...tables.map((table) => ({ value: table.tableName, label: table.displayName || table.tableName })),
];
// 공통 코드 옵션 (예시 - 실제로는 API에서 가져와야 함)
const commonCodeOptions = [
{ value: "none", label: "코드 선택" },
{ value: "USER_STATUS", label: "사용자 상태" },
{ value: "DEPT_TYPE", label: "부서 유형" },
{ value: "PRODUCT_CATEGORY", label: "제품 카테고리" },
];
// 테이블 목록 로드
const loadTables = async () => {
setLoading(true);
try {
const response = await fetch("http://localhost:8080/api/table-management/tables");
// 응답 상태 확인
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 응답 텍스트를 먼저 확인
const responseText = await response.text();
console.log("Raw response:", responseText);
// JSON 파싱 시도
let result;
try {
result = JSON.parse(responseText);
} catch (parseError) {
console.error("JSON 파싱 오류:", parseError);
console.error("응답 텍스트:", responseText);
throw new Error("JSON 파싱에 실패했습니다.");
}
if (result.success) {
setTables(result.data);
toast.success("테이블 목록을 성공적으로 로드했습니다.");
} else {
toast.error(result.message || "테이블 목록 로드에 실패했습니다.");
}
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
toast.error("테이블 목록 로드 중 오류가 발생했습니다.");
} finally {
setLoading(false);
}
};
// 컬럼 타입 정보 로드
const loadColumnTypes = async (tableName: string) => {
setColumnsLoading(true);
try {
const response = await fetch(`http://localhost:8080/api/table-management/tables/${tableName}/columns`);
// 응답 상태 확인
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 응답 텍스트를 먼저 확인
const responseText = await response.text();
console.log("Raw column response:", responseText);
// JSON 파싱 시도
let result;
try {
result = JSON.parse(responseText);
} catch (parseError) {
console.error("JSON 파싱 오류:", parseError);
console.error("응답 텍스트:", responseText);
throw new Error("JSON 파싱에 실패했습니다.");
}
if (result.success) {
setColumns(result.data);
setOriginalColumns(result.data); // 원본 데이터 저장
toast.success("컬럼 정보를 성공적으로 로드했습니다.");
} else {
toast.error(result.message || "컬럼 정보 로드에 실패했습니다.");
}
} 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;
}),
);
};
// 모든 컬럼 설정 저장
const saveAllColumnSettings = async () => {
if (!selectedTable || columns.length === 0) return;
try {
// 모든 컬럼의 설정 데이터 준비
const columnSettings = columns.map((column) => ({
columnName: column.columnName,
columnLabel: column.displayName, // 라벨 추가
webType: column.webType,
detailSettings: column.detailSettings,
codeCategory: column.codeCategory,
codeValue: column.codeValue,
referenceTable: column.referenceTable,
referenceColumn: column.referenceColumn,
}));
// 전체 테이블 설정을 한 번에 저장
const response = await fetch(
`http://localhost:8080/api/table-management/tables/${selectedTable}/columns/settings`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(columnSettings),
},
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.message || "컬럼 설정 저장에 실패했습니다.");
}
// 저장 성공 후 원본 데이터 업데이트
setOriginalColumns([...columns]);
toast.success(`${columns.length}개의 컬럼 설정이 성공적으로 저장되었습니다.`);
} 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 (
<div className="container mx-auto space-y-4 p-4 md:space-y-6 md:p-6">
{/* 헤더 영역 */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="text-2xl font-bold md:text-3xl"> </h1>
<p className="text-muted-foreground text-sm md:text-base"> .</p>
</div>
<div className="flex items-center gap-2 md:gap-4">
<Button onClick={loadTables} disabled={loading} size="sm" className="md:text-base">
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? "animate-spin" : ""}`} />
<span className="hidden sm:inline"></span>
<span className="sm:hidden"></span>
</Button>
</div>
</div>
{/* 검색 필터 */}
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform" />
<Input
placeholder="테이블 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* 메인 컨텐츠 영역 */}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:grid-cols-4">
{/* 좌측: 테이블 목록 */}
<div className="lg:col-span-1 xl:col-span-1">
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
<Database className="h-4 w-4 md:h-5 md:w-5" />
<span className="hidden sm:inline"> </span>
<span className="sm:hidden"> </span>
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
{loading ? (
<div className="flex justify-center py-8">
<LoadingSpinner />
</div>
) : (
<div className="max-h-96 space-y-2 overflow-y-auto">
{filteredTables.length === 0 ? (
<div className="text-muted-foreground py-8 text-center text-sm">
{searchTerm ? "검색 결과가 없습니다." : "테이블이 없습니다."}
</div>
) : (
filteredTables.map((table) => (
<div
key={table.tableName}
onClick={() => handleTableSelect(table.tableName)}
className={`cursor-pointer rounded-lg border p-3 transition-colors ${
selectedTable === table.tableName
? "bg-primary/10 border-primary"
: "hover:bg-muted/50 border-border"
}`}
>
<div className="flex items-center justify-between">
<div className="min-w-0 flex-1">
<h3 className="truncate text-sm font-medium md:text-base">{table.displayName}</h3>
<p className="text-muted-foreground truncate text-xs md:text-sm">{table.tableName}</p>
<p className="text-muted-foreground mt-1 truncate text-xs">{table.description}</p>
</div>
<Badge variant="secondary" className="ml-2 text-xs md:text-sm">
{table.columnCount}
</Badge>
</div>
</div>
))
)}
</div>
)}
</CardContent>
</Card>
</div>
{/* 우측: 컬럼 타입 설정 */}
<div className="lg:col-span-2 xl:col-span-3">
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base md:text-lg">
{selectedTable ? (
<div>
<span className="hidden sm:inline"> - </span>
{selectedTableInfo?.displayName}
<span className="text-muted-foreground ml-2 text-xs font-normal md:text-sm">
({selectedTableInfo?.tableName})
</span>
</div>
) : (
"컬럼 타입 설정"
)}
</CardTitle>
{selectedTable && (
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p className="text-muted-foreground text-xs md:text-sm">{selectedTableInfo?.description}</p>
<Button onClick={saveAllColumnSettings} disabled={columnsLoading} size="sm" className="md:text-base">
</Button>
</div>
)}
</CardHeader>
<CardContent className="pt-0">
{!selectedTable ? (
<div className="text-muted-foreground py-12 text-center text-sm md:text-base">
</div>
) : columnsLoading ? (
<div className="flex justify-center py-8">
<LoadingSpinner />
</div>
) : (
<div className="overflow-x-auto">
{/* 모바일: 카드 형태 */}
<div className="space-y-4 lg:hidden">
{columns.map((column) => (
<div key={column.columnName} className="space-y-3 rounded-lg border p-4">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium">{column.columnName}</h4>
<Badge variant="outline" className="text-xs">
{column.dbType}
</Badge>
</div>
<div className="space-y-2">
<div>
<label className="text-muted-foreground text-xs"></label>
<Input
value={column.displayName}
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
placeholder="라벨 입력"
className="text-sm"
/>
</div>
<div>
<label className="text-muted-foreground text-xs"> </label>
<Select
value={column.webType}
onValueChange={(value) => handleWebTypeChange(column.columnName, value)}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{webTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{column.webType === "code" && (
<div>
<label className="text-muted-foreground text-xs"> </label>
<Select
value={column.codeCategory || "none"}
onValueChange={(value) => handleDetailSettingsChange(column.columnName, "code", value)}
>
<SelectTrigger className="text-sm">
<SelectValue placeholder="코드 선택" />
</SelectTrigger>
<SelectContent>
{commonCodeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{column.webType === "entity" && (
<div>
<label className="text-muted-foreground text-xs"> </label>
<Select
value={column.referenceTable || "none"}
onValueChange={(value) =>
handleDetailSettingsChange(column.columnName, "entity", value)
}
>
<SelectTrigger className="text-sm">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{referenceTableOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
</div>
</div>
))}
</div>
{/* 태블릿/PC: 테이블 형태 */}
<div className="hidden lg:block">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-xs md:text-sm"></TableHead>
<TableHead className="text-xs md:text-sm"></TableHead>
<TableHead className="text-xs md:text-sm">DB </TableHead>
<TableHead className="text-xs md:text-sm"> </TableHead>
<TableHead className="text-xs md:text-sm xl:table-cell"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{columns.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-muted-foreground py-8 text-center text-sm">
.
</TableCell>
</TableRow>
) : (
columns.map((column) => (
<TableRow key={column.columnName}>
<TableCell className="text-xs font-medium md:text-sm">{column.columnName}</TableCell>
<TableCell>
<Input
value={column.displayName}
onChange={(e) => handleLabelChange(column.columnName, e.target.value)}
placeholder="라벨 입력"
className="w-24 text-xs md:w-32 md:text-sm"
/>
</TableCell>
<TableCell>
<Badge variant="outline" className="text-xs">
{column.dbType}
</Badge>
</TableCell>
<TableCell>
<Select
value={column.webType}
onValueChange={(value) => handleWebTypeChange(column.columnName, value)}
>
<SelectTrigger className="w-20 text-xs md:w-32 md:text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{webTypeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
<TableCell className="text-muted-foreground text-xs md:text-sm xl:table-cell">
{column.webType === "code" ? (
<Select
value={column.codeCategory || "none"}
onValueChange={(value) =>
handleDetailSettingsChange(column.columnName, "code", value)
}
>
<SelectTrigger className="w-32 text-xs md:w-40 md:text-sm">
<SelectValue placeholder="코드 선택" />
</SelectTrigger>
<SelectContent>
{commonCodeOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : column.webType === "entity" ? (
<Select
value={column.referenceTable || "none"}
onValueChange={(value) =>
handleDetailSettingsChange(column.columnName, "entity", value)
}
>
<SelectTrigger className="w-32 text-xs md:w-40 md:text-sm">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{referenceTableOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<span className="text-xs md:text-sm">{column.detailSettings}</span>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</div>
</div>
);
}