ERP-node/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx

772 lines
32 KiB
TypeScript

"use client";
import React, { useState, useEffect } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { TableListConfig, ColumnConfig } from "./types";
import {
Plus,
Trash2,
ArrowUp,
ArrowDown,
Eye,
EyeOff,
Settings,
Columns,
Filter,
Palette,
MousePointer,
} from "lucide-react";
export interface TableListConfigPanelProps {
config: TableListConfig;
onChange: (config: Partial<TableListConfig>) => void;
screenTableName?: string; // 화면에 연결된 테이블명
tableColumns?: any[]; // 테이블 컬럼 정보
}
/**
* TableList 설정 패널
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
*/
export const TableListConfigPanel: React.FC<TableListConfigPanelProps> = ({
config,
onChange,
screenTableName,
tableColumns,
}) => {
console.log("🔍 TableListConfigPanel props:", { config, screenTableName, tableColumns });
const [availableTables, setAvailableTables] = useState<Array<{ tableName: string; displayName: string }>>([]);
const [loadingTables, setLoadingTables] = useState(false);
const [availableColumns, setAvailableColumns] = useState<
Array<{ columnName: string; dataType: string; label?: string }>
>([]);
// 화면 테이블명이 있으면 자동으로 설정
useEffect(() => {
if (screenTableName && (!config.selectedTable || config.selectedTable !== screenTableName)) {
console.log("🔄 화면 테이블명 자동 설정:", screenTableName);
onChange({ selectedTable: screenTableName });
}
}, [screenTableName, config.selectedTable, onChange]);
// 테이블 목록 가져오기
useEffect(() => {
const fetchTables = async () => {
setLoadingTables(true);
try {
const response = await fetch("/api/tables");
if (response.ok) {
const result = await response.json();
if (result.success && result.data) {
setAvailableTables(
result.data.map((table: any) => ({
tableName: table.tableName,
displayName: table.displayName || table.tableName,
})),
);
}
}
} catch (error) {
console.error("테이블 목록 가져오기 실패:", error);
} finally {
setLoadingTables(false);
}
};
fetchTables();
}, []);
// 선택된 테이블의 컬럼 목록 설정 (tableColumns prop 우선 사용)
useEffect(() => {
console.log("🔍 useEffect 실행됨 - tableColumns:", tableColumns, "length:", tableColumns?.length);
if (tableColumns && tableColumns.length > 0) {
// tableColumns prop이 있으면 사용
console.log("🔧 tableColumns prop 사용:", tableColumns);
console.log("🔧 첫 번째 컬럼 상세:", tableColumns[0]);
const mappedColumns = tableColumns.map((column: any) => ({
columnName: column.columnName || column.name,
dataType: column.dataType || column.type || "text",
label: column.label || column.displayName || column.columnLabel || column.columnName || column.name,
}));
console.log("🏷️ availableColumns 설정됨:", mappedColumns);
console.log("🏷️ 첫 번째 mappedColumn:", mappedColumns[0]);
setAvailableColumns(mappedColumns);
} else if (config.selectedTable || screenTableName) {
// API에서 컬럼 정보 가져오기
const fetchColumns = async () => {
const tableName = config.selectedTable || screenTableName;
if (!tableName) {
setAvailableColumns([]);
return;
}
console.log("🔧 API에서 컬럼 정보 가져오기:", tableName);
try {
const response = await fetch(`/api/tables/${tableName}/columns`);
if (response.ok) {
const result = await response.json();
if (result.success && result.data) {
console.log("🔧 API 응답 컬럼 데이터:", result.data);
setAvailableColumns(
result.data.map((col: any) => ({
columnName: col.columnName,
dataType: col.dataType,
label: col.displayName || col.columnName,
})),
);
}
}
} catch (error) {
console.error("컬럼 목록 가져오기 실패:", error);
}
};
fetchColumns();
} else {
setAvailableColumns([]);
}
}, [config.selectedTable, screenTableName, tableColumns]);
const handleChange = (key: keyof TableListConfig, value: any) => {
onChange({ [key]: value });
};
const handleNestedChange = (parentKey: keyof TableListConfig, childKey: string, value: any) => {
const parentValue = config[parentKey] as any;
onChange({
[parentKey]: {
...parentValue,
[childKey]: value,
},
});
};
// 컬럼 추가
const addColumn = (columnName: string) => {
const existingColumn = config.columns?.find((col) => col.columnName === columnName);
if (existingColumn) return;
// tableColumns에서 해당 컬럼의 라벨 정보 찾기
const columnInfo = tableColumns?.find((col: any) => (col.columnName || col.name) === columnName);
// 라벨명 우선 사용, 없으면 컬럼명 사용
const displayName = columnInfo?.label || columnInfo?.displayName || columnName;
const newColumn: ColumnConfig = {
columnName,
displayName,
visible: true,
sortable: true,
searchable: true,
align: "left",
format: "text",
order: config.columns?.length || 0,
};
handleChange("columns", [...(config.columns || []), newColumn]);
};
// 컬럼 제거
const removeColumn = (columnName: string) => {
const updatedColumns = config.columns?.filter((col) => col.columnName !== columnName) || [];
handleChange("columns", updatedColumns);
};
// 컬럼 업데이트
const updateColumn = (columnName: string, updates: Partial<ColumnConfig>) => {
const updatedColumns =
config.columns?.map((col) => (col.columnName === columnName ? { ...col, ...updates } : col)) || [];
handleChange("columns", updatedColumns);
};
// 컬럼 순서 변경
const moveColumn = (columnName: string, direction: "up" | "down") => {
const columns = [...(config.columns || [])];
const index = columns.findIndex((col) => col.columnName === columnName);
if (index === -1) return;
const targetIndex = direction === "up" ? index - 1 : index + 1;
if (targetIndex < 0 || targetIndex >= columns.length) return;
[columns[index], columns[targetIndex]] = [columns[targetIndex], columns[index]];
// order 값 재정렬
columns.forEach((col, idx) => {
col.order = idx;
});
handleChange("columns", columns);
};
return (
<div className="space-y-4">
<div className="text-sm font-medium"> </div>
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="basic" className="flex items-center gap-1">
<Settings className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="columns" className="flex items-center gap-1">
<Columns className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="filter" className="flex items-center gap-1">
<Filter className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="actions" className="flex items-center gap-1">
<MousePointer className="h-3 w-3" />
</TabsTrigger>
<TabsTrigger value="style" className="flex items-center gap-1">
<Palette className="h-3 w-3" />
</TabsTrigger>
</TabsList>
{/* 기본 설정 탭 */}
<TabsContent value="basic" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> </Label>
<div className="rounded-md bg-gray-50 p-3">
<div className="text-sm font-medium">
{screenTableName ? (
<span className="text-blue-600">{screenTableName}</span>
) : (
<span className="text-gray-500"> </span>
)}
</div>
{screenTableName && (
<div className="mt-1 text-xs text-gray-500"> </div>
)}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title"></Label>
<Input
id="title"
value={config.title || ""}
onChange={(e) => handleChange("title", e.target.value)}
placeholder="테이블 제목 (선택사항)"
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="showHeader"
checked={config.showHeader}
onCheckedChange={(checked) => handleChange("showHeader", checked)}
/>
<Label htmlFor="showHeader"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="showFooter"
checked={config.showFooter}
onCheckedChange={(checked) => handleChange("showFooter", checked)}
/>
<Label htmlFor="showFooter"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="autoLoad"
checked={config.autoLoad}
onCheckedChange={(checked) => handleChange("autoLoad", checked)}
/>
<Label htmlFor="autoLoad"> </Label>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label> </Label>
<Select
value={config.height}
onValueChange={(value: "auto" | "fixed" | "viewport") => handleChange("height", value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto"></SelectItem>
<SelectItem value="fixed"></SelectItem>
<SelectItem value="viewport"> </SelectItem>
</SelectContent>
</Select>
</div>
{config.height === "fixed" && (
<div className="space-y-2">
<Label htmlFor="fixedHeight"> (px)</Label>
<Input
id="fixedHeight"
type="number"
value={config.fixedHeight || 400}
onChange={(e) => handleChange("fixedHeight", parseInt(e.target.value) || 400)}
min={200}
max={1000}
/>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="paginationEnabled"
checked={config.pagination?.enabled}
onCheckedChange={(checked) => handleNestedChange("pagination", "enabled", checked)}
/>
<Label htmlFor="paginationEnabled"> </Label>
</div>
{config.pagination?.enabled && (
<>
<div className="space-y-2">
<Label htmlFor="pageSize"> </Label>
<Select
value={config.pagination?.pageSize?.toString() || "20"}
onValueChange={(value) => handleNestedChange("pagination", "pageSize", parseInt(value))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="showSizeSelector"
checked={config.pagination?.showSizeSelector}
onCheckedChange={(checked) => handleNestedChange("pagination", "showSizeSelector", checked)}
/>
<Label htmlFor="showSizeSelector"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="showPageInfo"
checked={config.pagination?.showPageInfo}
onCheckedChange={(checked) => handleNestedChange("pagination", "showPageInfo", checked)}
/>
<Label htmlFor="showPageInfo"> </Label>
</div>
</>
)}
</CardContent>
</Card>
</TabsContent>
{/* 컬럼 설정 탭 */}
<TabsContent value="columns" className="space-y-4">
{!screenTableName ? (
<Card>
<CardContent className="pt-6">
<div className="text-center text-gray-500">
<p> .</p>
<p className="text-sm"> .</p>
</div>
</CardContent>
</Card>
) : (
<>
<Card>
<CardHeader>
<CardTitle className="text-base"> - {screenTableName}</CardTitle>
<CardDescription>
{availableColumns.length > 0
? `${availableColumns.length}개의 사용 가능한 컬럼에서 선택하세요`
: "컬럼 정보를 불러오는 중..."}
</CardDescription>
</CardHeader>
<CardContent>
{availableColumns.length > 0 ? (
<div className="flex flex-wrap gap-2">
{availableColumns
.filter((col) => !config.columns?.find((c) => c.columnName === col.columnName))
.map((column) => (
<Button
key={column.columnName}
variant="outline"
size="sm"
onClick={() => addColumn(column.columnName)}
className="flex items-center gap-1"
>
<Plus className="h-3 w-3" />
{column.label || column.columnName}
<Badge variant="secondary" className="text-xs">
{column.dataType}
</Badge>
</Button>
))}
</div>
) : (
<div className="py-4 text-center text-gray-500">
<p> ...</p>
</div>
)}
</CardContent>
</Card>
</>
)}
{screenTableName && (
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
<CardDescription> </CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-64">
<div className="space-y-3">
{config.columns?.map((column, index) => (
<div key={column.columnName} className="space-y-3 rounded-lg border p-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
checked={column.visible}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { visible: checked as boolean })
}
/>
<span className="font-medium">
{availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName}
</span>
</div>
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "up")}
disabled={index === 0}
>
<ArrowUp className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => moveColumn(column.columnName, "down")}
disabled={index === (config.columns?.length || 0) - 1}
>
<ArrowDown className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => removeColumn(column.columnName)}
className="text-red-500 hover:text-red-600"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
</div>
{column.visible && (
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={
availableColumns.find((col) => col.columnName === column.columnName)?.label ||
column.displayName ||
column.columnName
}
onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })}
className="h-8"
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Select
value={column.align}
onValueChange={(value: "left" | "center" | "right") =>
updateColumn(column.columnName, { align: value })
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="left"></SelectItem>
<SelectItem value="center"></SelectItem>
<SelectItem value="right"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Select
value={column.format}
onValueChange={(value: "text" | "number" | "date" | "currency" | "boolean") =>
updateColumn(column.columnName, { format: value })
}
>
<SelectTrigger className="h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="text"></SelectItem>
<SelectItem value="number"></SelectItem>
<SelectItem value="date"></SelectItem>
<SelectItem value="currency"></SelectItem>
<SelectItem value="boolean"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs"> (px)</Label>
<Input
type="number"
value={column.width || ""}
onChange={(e) =>
updateColumn(column.columnName, {
width: e.target.value ? parseInt(e.target.value) : undefined,
})
}
placeholder="자동"
className="h-8"
/>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-1">
<Checkbox
checked={column.sortable}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { sortable: checked as boolean })
}
/>
<Label className="text-xs"> </Label>
</div>
<div className="flex items-center space-x-1">
<Checkbox
checked={column.searchable}
onCheckedChange={(checked) =>
updateColumn(column.columnName, { searchable: checked as boolean })
}
/>
<Label className="text-xs"> </Label>
</div>
</div>
</div>
)}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
)}
</TabsContent>
{/* 필터 설정 탭 */}
<TabsContent value="filter" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="filterEnabled"
checked={config.filter?.enabled}
onCheckedChange={(checked) => handleNestedChange("filter", "enabled", checked)}
/>
<Label htmlFor="filterEnabled"> </Label>
</div>
{config.filter?.enabled && (
<>
<div className="flex items-center space-x-2">
<Checkbox
id="quickSearch"
checked={config.filter?.quickSearch}
onCheckedChange={(checked) => handleNestedChange("filter", "quickSearch", checked)}
/>
<Label htmlFor="quickSearch"> </Label>
</div>
{config.filter?.quickSearch && (
<div className="ml-6 flex items-center space-x-2">
<Checkbox
id="showColumnSelector"
checked={config.filter?.showColumnSelector}
onCheckedChange={(checked) => handleNestedChange("filter", "showColumnSelector", checked)}
/>
<Label htmlFor="showColumnSelector"> </Label>
</div>
)}
<div className="flex items-center space-x-2">
<Checkbox
id="advancedFilter"
checked={config.filter?.advancedFilter}
onCheckedChange={(checked) => handleNestedChange("filter", "advancedFilter", checked)}
/>
<Label htmlFor="advancedFilter"> </Label>
</div>
</>
)}
</CardContent>
</Card>
</TabsContent>
{/* 액션 설정 탭 */}
<TabsContent value="actions" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="showActions"
checked={config.actions?.showActions}
onCheckedChange={(checked) => handleNestedChange("actions", "showActions", checked)}
/>
<Label htmlFor="showActions"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="bulkActions"
checked={config.actions?.bulkActions}
onCheckedChange={(checked) => handleNestedChange("actions", "bulkActions", checked)}
/>
<Label htmlFor="bulkActions"> </Label>
</div>
</CardContent>
</Card>
</TabsContent>
{/* 스타일 설정 탭 */}
<TabsContent value="style" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label></Label>
<Select
value={config.tableStyle?.theme}
onValueChange={(value: "default" | "striped" | "bordered" | "minimal") =>
handleNestedChange("tableStyle", "theme", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default"></SelectItem>
<SelectItem value="striped"></SelectItem>
<SelectItem value="bordered"></SelectItem>
<SelectItem value="minimal"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label> </Label>
<Select
value={config.tableStyle?.rowHeight}
onValueChange={(value: "compact" | "normal" | "comfortable") =>
handleNestedChange("tableStyle", "rowHeight", value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="compact"></SelectItem>
<SelectItem value="normal"></SelectItem>
<SelectItem value="comfortable"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="alternateRows"
checked={config.tableStyle?.alternateRows}
onCheckedChange={(checked) => handleNestedChange("tableStyle", "alternateRows", checked)}
/>
<Label htmlFor="alternateRows"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="hoverEffect"
checked={config.tableStyle?.hoverEffect}
onCheckedChange={(checked) => handleNestedChange("tableStyle", "hoverEffect", checked)}
/>
<Label htmlFor="hoverEffect"> </Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="stickyHeader"
checked={config.stickyHeader}
onCheckedChange={(checked) => handleChange("stickyHeader", checked)}
/>
<Label htmlFor="stickyHeader"> </Label>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};