ERP-node/frontend/components/screen/panels/DataTableConfigPanel.tsx

1697 lines
71 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
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 { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Table, Plus, Trash2, Settings, Filter, Columns, ChevronDown } from "lucide-react";
import { DataTableComponent, DataTableColumn, DataTableFilter, TableInfo, ColumnInfo, WebType } from "@/types/screen";
import { generateComponentId } from "@/lib/utils/generateId";
interface DataTableConfigPanelProps {
component: DataTableComponent;
tables: TableInfo[];
onUpdateComponent: (updates: Partial<DataTableComponent>) => void;
}
const webTypeOptions: { value: WebType; label: string }[] = [
{ value: "text", label: "텍스트" },
{ value: "number", label: "숫자" },
{ value: "decimal", label: "소수" },
{ value: "date", label: "날짜" },
{ value: "datetime", label: "날짜시간" },
{ value: "select", label: "선택박스" },
{ value: "checkbox", label: "체크박스" },
{ value: "email", label: "이메일" },
{ value: "tel", label: "전화번호" },
];
export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({ component, tables, onUpdateComponent }) => {
const [selectedTable, setSelectedTable] = useState<TableInfo | null>(null);
// 로컬 입력 상태 (실시간 타이핑용)
const [localValues, setLocalValues] = useState({
title: component.title || "",
searchButtonText: component.searchButtonText || "검색",
showSearchButton: component.showSearchButton ?? true,
enableExport: component.enableExport ?? true,
enableRefresh: component.enableRefresh ?? true,
enableAdd: component.enableAdd ?? true,
enableEdit: component.enableEdit ?? true,
enableDelete: component.enableDelete ?? true,
addButtonText: component.addButtonText || "추가",
editButtonText: component.editButtonText || "수정",
deleteButtonText: component.deleteButtonText || "삭제",
// 모달 설정
modalTitle: component.addModalConfig?.title || "새 데이터 추가",
modalDescription: component.addModalConfig?.description || "",
modalWidth: component.addModalConfig?.width || "lg",
modalLayout: component.addModalConfig?.layout || "two-column",
modalGridColumns: component.addModalConfig?.gridColumns || 2,
modalSubmitButtonText: component.addModalConfig?.submitButtonText || "추가",
modalCancelButtonText: component.addModalConfig?.cancelButtonText || "취소",
paginationEnabled: component.pagination?.enabled ?? true,
showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true,
showPageInfo: component.pagination?.showPageInfo ?? true,
showFirstLast: component.pagination?.showFirstLast ?? true,
gridColumns: component.gridColumns || 6,
});
// 컬럼별 로컬 입력 상태
const [localColumnInputs, setLocalColumnInputs] = useState<Record<string, string>>({});
// 컬럼별 체크박스 및 설정 상태
const [localColumnCheckboxes, setLocalColumnCheckboxes] = useState<
Record<
string,
{
visible: boolean;
sortable: boolean;
searchable: boolean;
}
>
>({});
// 컬럼별 그리드 컬럼 설정 상태
const [localColumnGridColumns, setLocalColumnGridColumns] = useState<Record<string, number>>({});
// 필터별 로컬 입력 상태
const [localFilterInputs, setLocalFilterInputs] = useState<Record<string, string>>({});
// 모달 설정 확장/축소 상태
const [isModalConfigOpen, setIsModalConfigOpen] = useState<Record<string, boolean>>({});
// 컴포넌트 변경 시 로컬 값 동기화
useEffect(() => {
console.log("🔄 DataTableConfig: 컴포넌트 변경 감지", {
componentId: component.id,
title: component.title,
searchButtonText: component.searchButtonText,
columnsCount: component.columns.length,
filtersCount: component.filters.length,
columnIds: component.columns.map((col) => col.id),
filterColumnNames: component.filters.map((filter) => filter.columnName),
timestamp: new Date().toISOString(),
});
// 컬럼과 필터 상세 정보 로그
if (component.columns.length > 0) {
console.log(
"📋 현재 컬럼 목록:",
component.columns.map((col) => ({
id: col.id,
columnName: col.columnName,
label: col.label,
visible: col.visible,
gridColumns: col.gridColumns,
})),
);
}
// 로컬 상태 정보 로그
console.log("🔧 로컬 상태 정보:", {
localColumnInputsCount: Object.keys(localColumnInputs).length,
localColumnCheckboxesCount: Object.keys(localColumnCheckboxes).length,
localColumnGridColumnsCount: Object.keys(localColumnGridColumns).length,
});
if (component.filters.length > 0) {
console.log(
"🔍 현재 필터 목록:",
component.filters.map((filter) => ({
columnName: filter.columnName,
widgetType: filter.widgetType,
label: filter.label,
})),
);
}
setLocalValues({
title: component.title || "",
searchButtonText: component.searchButtonText || "검색",
showSearchButton: component.showSearchButton ?? true,
enableExport: component.enableExport ?? true,
enableRefresh: component.enableRefresh ?? true,
enableAdd: component.enableAdd ?? true,
enableEdit: component.enableEdit ?? true,
enableDelete: component.enableDelete ?? true,
addButtonText: component.addButtonText || "추가",
editButtonText: component.editButtonText || "수정",
deleteButtonText: component.deleteButtonText || "삭제",
// 모달 설정
modalTitle: component.addModalConfig?.title || "새 데이터 추가",
modalDescription: component.addModalConfig?.description || "",
modalWidth: component.addModalConfig?.width || "lg",
modalLayout: component.addModalConfig?.layout || "two-column",
modalGridColumns: component.addModalConfig?.gridColumns || 2,
modalSubmitButtonText: component.addModalConfig?.submitButtonText || "추가",
modalCancelButtonText: component.addModalConfig?.cancelButtonText || "취소",
paginationEnabled: component.pagination?.enabled ?? true,
showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true,
showPageInfo: component.pagination?.showPageInfo ?? true,
showFirstLast: component.pagination?.showFirstLast ?? true,
gridColumns: component.gridColumns || 6,
});
// 컬럼 라벨 로컬 상태 초기화 (기존 값이 없는 경우만)
setLocalColumnInputs((prev) => {
const newInputs = { ...prev };
component.columns.forEach((col) => {
// 기존에 로컬 입력값이 없는 경우만 초기화
if (!(col.id in newInputs)) {
newInputs[col.id] = col.label;
}
});
// 삭제된 컬럼의 로컬 상태 제거
const currentColumnIds = new Set(component.columns.map((col) => col.id));
Object.keys(newInputs).forEach((id) => {
if (!currentColumnIds.has(id)) {
delete newInputs[id];
}
});
return newInputs;
});
// 컬럼별 체크박스 상태 초기화
setLocalColumnCheckboxes((prev) => {
const newCheckboxes = { ...prev };
component.columns.forEach((col) => {
// 기존에 로컬 체크박스 상태가 없는 경우만 초기화
if (!(col.id in newCheckboxes)) {
newCheckboxes[col.id] = {
visible: col.visible,
sortable: col.sortable,
searchable: col.searchable,
};
}
});
// 삭제된 컬럼의 로컬 상태 제거
const currentColumnIds = new Set(component.columns.map((col) => col.id));
Object.keys(newCheckboxes).forEach((id) => {
if (!currentColumnIds.has(id)) {
delete newCheckboxes[id];
}
});
return newCheckboxes;
});
// 컬럼별 그리드 컬럼 설정 상태 초기화
setLocalColumnGridColumns((prev) => {
const newGridColumns = { ...prev };
component.columns.forEach((col) => {
// 기존에 로컬 그리드 컬럼 설정이 없는 경우만 초기화
if (!(col.id in newGridColumns)) {
newGridColumns[col.id] = col.gridColumns;
}
});
// 삭제된 컬럼의 로컬 상태 제거
const currentColumnIds = new Set(component.columns.map((col) => col.id));
Object.keys(newGridColumns).forEach((id) => {
if (!currentColumnIds.has(id)) {
delete newGridColumns[id];
}
});
return newGridColumns;
});
// 필터별 로컬 입력 상태 동기화 (기존 값 보존하면서 새 필터만 추가)
setLocalFilterInputs((prev) => {
const newFilterInputs = { ...prev };
component.filters?.forEach((filter, index) => {
const filterKey = `${filter.columnName}-${index}`;
if (!(filterKey in newFilterInputs)) {
newFilterInputs[filterKey] = filter.label || filter.columnName;
}
});
// 삭제된 필터의 로컬 상태 제거
const currentFilterKeys = new Set(
component.filters?.map((filter, index) => `${filter.columnName}-${index}`) || [],
);
Object.keys(newFilterInputs).forEach((key) => {
if (!currentFilterKeys.has(key)) {
delete newFilterInputs[key];
}
});
return newFilterInputs;
});
}, [
component.id,
component.title,
component.searchButtonText,
component.columns,
component.filters,
component.showSearchButton,
component.enableExport,
component.enableRefresh,
component.pagination,
component.columns.length, // 컬럼 개수 변경 감지
component.filters.length, // 필터 개수 변경 감지
]);
// 선택된 테이블 정보 로드
useEffect(() => {
if (component.tableName && tables.length > 0) {
const table = tables.find((t) => t.tableName === component.tableName);
setSelectedTable(table || null);
}
}, [component.tableName, tables]);
// 테이블 변경 시 컬럼 자동 설정
const handleTableChange = useCallback(
(tableName: string) => {
const table = tables.find((t) => t.tableName === tableName);
if (!table) return;
console.log("🔄 테이블 변경:", {
tableName,
table,
columnsCount: table.columns.length,
columns: table.columns.map((col) => ({
name: col.columnName,
label: col.columnLabel,
type: col.dataType,
})),
});
// 테이블의 모든 컬럼을 기본 설정으로 추가
const defaultColumns: DataTableColumn[] = table.columns.map((col, index) => ({
id: generateComponentId(),
columnName: col.columnName,
label: col.columnLabel || col.columnName,
widgetType: getWidgetTypeFromColumn(col),
gridColumns: 2, // 기본 2칸
visible: index < 6, // 처음 6개만 기본으로 표시
filterable: isFilterableWebType(getWidgetTypeFromColumn(col)),
sortable: true,
searchable: ["text", "email", "tel"].includes(getWidgetTypeFromColumn(col)),
}));
// 필터는 사용자가 수동으로 추가
console.log("✅ 생성된 컬럼 설정:", {
defaultColumnsCount: defaultColumns.length,
visibleColumns: defaultColumns.filter((col) => col.visible).length,
});
onUpdateComponent({
tableName,
columns: defaultColumns,
filters: [], // 빈 필터 배열
});
setSelectedTable(table);
},
[tables, onUpdateComponent],
);
// 컬럼 타입 추론
const getWidgetTypeFromColumn = (column: ColumnInfo): WebType => {
const type = column.dataType?.toLowerCase() || "";
const name = column.columnName.toLowerCase();
console.log("🔍 웹타입 추론:", {
columnName: column.columnName,
dataType: column.dataType,
type,
name,
});
// 숫자 타입
if (type.includes("int") || type.includes("integer") || type.includes("bigint") || type.includes("smallint")) {
return "number";
}
if (
type.includes("decimal") ||
type.includes("numeric") ||
type.includes("float") ||
type.includes("double") ||
type.includes("real")
) {
return "decimal";
}
// 날짜/시간 타입
if (type.includes("timestamp") || type.includes("datetime")) {
return "datetime";
}
if (type.includes("date")) {
return "date";
}
if (type.includes("time")) {
return "datetime";
}
// 불린 타입
if (type.includes("bool") || type.includes("boolean")) {
return "checkbox";
}
// 컬럼명 기반 추론
if (name.includes("email") || name.includes("mail")) return "email";
if (name.includes("phone") || name.includes("tel") || name.includes("mobile")) return "tel";
if (name.includes("url") || name.includes("link")) return "text";
if (name.includes("password") || name.includes("pwd")) return "text";
// 텍스트 타입 (기본값)
return "text";
};
// 컬럼 업데이트
const updateColumn = useCallback(
(columnId: string, updates: Partial<DataTableColumn>) => {
const updatedColumns = component.columns.map((col) => (col.id === columnId ? { ...col, ...updates } : col));
onUpdateComponent({ columns: updatedColumns });
},
[component.columns, onUpdateComponent],
);
// 컬럼 삭제
const removeColumn = useCallback(
(columnId: string) => {
const columnToRemove = component.columns.find((col) => col.id === columnId);
const updatedColumns = component.columns.filter((col) => col.id !== columnId);
// 로컬 상태에서도 해당 컬럼 제거
setLocalColumnInputs((prev) => {
const newInputs = { ...prev };
delete newInputs[columnId];
return newInputs;
});
// 로컬 체크박스 상태에서도 해당 컬럼 제거
setLocalColumnCheckboxes((prev) => {
const newCheckboxes = { ...prev };
delete newCheckboxes[columnId];
return newCheckboxes;
});
// 로컬 그리드 컬럼 상태에서도 해당 컬럼 제거
setLocalColumnGridColumns((prev) => {
const newGridColumns = { ...prev };
delete newGridColumns[columnId];
return newGridColumns;
});
console.log("🗑️ 컬럼 삭제:", {
columnId,
columnName: columnToRemove?.columnName,
remainingColumns: updatedColumns.length,
});
onUpdateComponent({
columns: updatedColumns,
});
},
[component.columns, onUpdateComponent],
);
// 필터 업데이트
const updateFilter = useCallback(
(index: number, updates: Partial<DataTableFilter>) => {
const updatedFilters = component.filters.map((filter, i) => (i === index ? { ...filter, ...updates } : filter));
console.log("🔄 필터 업데이트:", { index, updates, updatedFilters });
onUpdateComponent({ filters: updatedFilters });
},
[component.filters, onUpdateComponent],
);
// 필터 추가
const addFilter = useCallback(() => {
if (!selectedTable) return;
// 필터 가능한 컬럼들 중에서 아직 필터가 없는 컬럼들만 선택
const availableColumns = selectedTable.columns.filter(
(col) =>
isFilterableWebType(getWidgetTypeFromColumn(col)) &&
!component.filters.some((filter) => filter.columnName === col.columnName),
);
if (availableColumns.length === 0) return;
const targetColumn = availableColumns[0];
const widgetType = getWidgetTypeFromColumn(targetColumn);
const newFilter: DataTableFilter = {
columnName: targetColumn.columnName,
widgetType,
label: targetColumn.columnLabel || targetColumn.columnName,
gridColumns: 3,
};
console.log(" 필터 추가 시작:", {
targetColumnName: targetColumn.columnName,
targetColumnLabel: targetColumn.columnLabel,
inferredWidgetType: widgetType,
currentFiltersCount: component.filters.length,
});
console.log(" 생성된 새 필터:", {
columnName: newFilter.columnName,
widgetType: newFilter.widgetType,
label: newFilter.label,
gridColumns: newFilter.gridColumns,
});
const updatedFilters = [...component.filters, newFilter];
console.log("🔄 필터 업데이트 호출:", {
filtersToAdd: 1,
totalFiltersAfter: updatedFilters.length,
updatedFilters: updatedFilters.map((filter) => ({
columnName: filter.columnName,
widgetType: filter.widgetType,
label: filter.label,
})),
});
onUpdateComponent({ filters: updatedFilters });
console.log("✅ 필터 추가 완료 - onUpdateComponent 호출됨");
}, [selectedTable, component.filters, onUpdateComponent]);
// 필터 삭제
const removeFilter = useCallback(
(index: number) => {
const updatedFilters = component.filters.filter((_, i) => i !== index);
// 로컬 필터 입력 상태에서도 해당 필터 제거
setLocalFilterInputs((prev) => {
const newFilterInputs = { ...prev };
const filterKey = `${component.filters?.[index]?.columnName}-${index}`;
delete newFilterInputs[filterKey];
return newFilterInputs;
});
onUpdateComponent({ filters: updatedFilters });
},
[component.filters, onUpdateComponent],
);
// 웹 타입별 필터 가능 여부 확인
const isFilterableWebType = (webType: WebType): boolean => {
const filterableTypes: WebType[] = ["text", "number", "decimal", "date", "datetime", "select", "email", "tel"];
return filterableTypes.includes(webType);
};
// 컬럼 추가 (테이블에서 선택)
const addColumn = useCallback(
(columnName?: string) => {
if (!selectedTable) return;
const availableColumns = selectedTable.columns.filter(
(col) => !component.columns.some((column) => column.columnName === col.columnName),
);
if (availableColumns.length === 0) return;
// 특정 컬럼이 지정되었으면 해당 컬럼을, 아니면 첫 번째 사용 가능한 컬럼을 사용
const targetColumn = columnName
? availableColumns.find((col) => col.columnName === columnName) || availableColumns[0]
: availableColumns[0];
const widgetType = getWidgetTypeFromColumn(targetColumn);
const newColumn: DataTableColumn = {
id: generateComponentId(),
columnName: targetColumn.columnName,
label: targetColumn.columnLabel || targetColumn.columnName,
widgetType,
gridColumns: 2,
visible: true,
filterable: isFilterableWebType(widgetType),
sortable: true,
searchable: ["text", "email", "tel"].includes(widgetType),
};
// 필터는 자동으로 추가하지 않음 (사용자가 수동으로 추가)
console.log(" 컬럼 추가 시작:", {
targetColumnName: targetColumn.columnName,
targetColumnLabel: targetColumn.columnLabel,
inferredWidgetType: widgetType,
currentColumnsCount: component.columns.length,
currentFiltersCount: component.filters.length,
});
console.log(" 생성된 새 컬럼:", {
id: newColumn.id,
columnName: newColumn.columnName,
label: newColumn.label,
widgetType: newColumn.widgetType,
filterable: newColumn.filterable,
visible: newColumn.visible,
sortable: newColumn.sortable,
searchable: newColumn.searchable,
});
// 필터는 수동으로만 추가
// 로컬 상태에 새 컬럼 입력값 추가
setLocalColumnInputs((prev) => {
const newInputs = {
...prev,
[newColumn.id]: newColumn.label,
};
console.log("🔄 로컬 컬럼 상태 업데이트:", {
newColumnId: newColumn.id,
newLabel: newColumn.label,
totalLocalInputs: Object.keys(newInputs).length,
});
return newInputs;
});
// 로컬 체크박스 상태에 새 컬럼 추가
setLocalColumnCheckboxes((prev) => ({
...prev,
[newColumn.id]: {
visible: newColumn.visible,
sortable: newColumn.sortable,
searchable: newColumn.searchable,
},
}));
// 로컬 그리드 컬럼 상태에 새 컬럼 추가
setLocalColumnGridColumns((prev) => ({
...prev,
[newColumn.id]: newColumn.gridColumns,
}));
// 컬럼만 업데이트
const updates: Partial<DataTableComponent> = {
columns: [...component.columns, newColumn],
};
console.log("🔄 컴포넌트 업데이트 호출:", {
columnsToAdd: 1,
totalColumnsAfter: updates.columns?.length,
hasColumns: !!updates.columns,
updateKeys: Object.keys(updates),
});
console.log("🔄 업데이트 상세 내용:", {
columns: updates.columns?.map((col) => ({ id: col.id, columnName: col.columnName, label: col.label })),
});
onUpdateComponent(updates);
console.log("✅ 컬럼 추가 완료 - onUpdateComponent 호출됨");
},
[selectedTable, component.columns, component.filters, onUpdateComponent],
);
return (
<div className="max-h-[80vh] space-y-4 overflow-y-auto p-4">
{/* 기본 설정 */}
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-sm">
<Settings className="h-4 w-4" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="table-select"> </Label>
<Select value={component.tableName} onValueChange={handleTableChange}>
<SelectTrigger>
<SelectValue placeholder="테이블을 선택하세요" />
</SelectTrigger>
<SelectContent>
{tables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.tableLabel || table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="title"> </Label>
<Input
id="title"
value={localValues.title}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, title: newValue }));
onUpdateComponent({ title: newValue });
}}
placeholder="테이블 제목을 입력하세요"
/>
</div>
{/* CRUD 기능 설정 */}
<div className="space-y-4">
<div className="flex items-center space-x-2">
<h4 className="text-sm font-medium">CRUD </h4>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="enable-add"
checked={localValues.enableAdd}
onCheckedChange={(checked) => {
setLocalValues((prev) => ({ ...prev, enableAdd: checked as boolean }));
onUpdateComponent({ enableAdd: checked as boolean });
}}
/>
<Label htmlFor="enable-add" className="text-sm">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enable-edit"
checked={localValues.enableEdit}
onCheckedChange={(checked) => {
setLocalValues((prev) => ({ ...prev, enableEdit: checked as boolean }));
onUpdateComponent({ enableEdit: checked as boolean });
}}
/>
<Label htmlFor="enable-edit" className="text-sm">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enable-delete"
checked={localValues.enableDelete}
onCheckedChange={(checked) => {
setLocalValues((prev) => ({ ...prev, enableDelete: checked as boolean }));
onUpdateComponent({ enableDelete: checked as boolean });
}}
/>
<Label htmlFor="enable-delete" className="text-sm">
</Label>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="add-button-text" className="text-sm">
</Label>
<Input
id="add-button-text"
value={localValues.addButtonText}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, addButtonText: newValue }));
onUpdateComponent({ addButtonText: newValue });
}}
placeholder="추가"
disabled={!localValues.enableAdd}
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-button-text" className="text-sm">
</Label>
<Input
id="edit-button-text"
value={localValues.editButtonText}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, editButtonText: newValue }));
onUpdateComponent({ editButtonText: newValue });
}}
placeholder="수정"
disabled={!localValues.enableEdit}
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label htmlFor="delete-button-text" className="text-sm">
</Label>
<Input
id="delete-button-text"
value={localValues.deleteButtonText}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, deleteButtonText: newValue }));
onUpdateComponent({ deleteButtonText: newValue });
}}
placeholder="삭제"
disabled={!localValues.enableDelete}
className="h-8 text-sm"
/>
</div>
</div>
</div>
{/* 추가 모달 커스터마이징 설정 */}
{localValues.enableAdd && (
<div className="space-y-4 border-t pt-4">
<div className="flex items-center space-x-2">
<h4 className="text-sm font-medium"> </h4>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-title" className="text-sm">
</Label>
<Input
id="modal-title"
value={localValues.modalTitle}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, modalTitle: newValue }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, title: newValue },
});
}}
placeholder="새 데이터 추가"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label htmlFor="modal-width" className="text-sm">
</Label>
<Select
value={localValues.modalWidth}
onValueChange={(value) => {
setLocalValues((prev) => ({ ...prev, modalWidth: value as any }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, width: value as any },
});
}}
>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="sm"> (384px)</SelectItem>
<SelectItem value="md"> (448px)</SelectItem>
<SelectItem value="lg"> (512px)</SelectItem>
<SelectItem value="xl"> (576px)</SelectItem>
<SelectItem value="2xl"> (672px)</SelectItem>
<SelectItem value="full"> </SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="modal-description" className="text-sm">
</Label>
<Input
id="modal-description"
value={localValues.modalDescription}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, modalDescription: newValue }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, description: newValue },
});
}}
placeholder="모달에 표시될 설명을 입력하세요"
className="h-8 text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-layout" className="text-sm">
</Label>
<Select
value={localValues.modalLayout}
onValueChange={(value) => {
setLocalValues((prev) => ({ ...prev, modalLayout: value as any }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, layout: value as any },
});
}}
>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="single"> </SelectItem>
<SelectItem value="two-column">2</SelectItem>
<SelectItem value="grid"></SelectItem>
</SelectContent>
</Select>
</div>
{localValues.modalLayout === "grid" && (
<div className="space-y-2">
<Label htmlFor="modal-grid-columns" className="text-sm">
</Label>
<Select
value={localValues.modalGridColumns.toString()}
onValueChange={(value) => {
const gridColumns = parseInt(value);
setLocalValues((prev) => ({ ...prev, modalGridColumns: gridColumns }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, gridColumns },
});
}}
>
<SelectTrigger className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2</SelectItem>
<SelectItem value="3">3</SelectItem>
<SelectItem value="4">4</SelectItem>
</SelectContent>
</Select>
</div>
)}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-submit-text" className="text-sm">
</Label>
<Input
id="modal-submit-text"
value={localValues.modalSubmitButtonText}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, modalSubmitButtonText: newValue }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, submitButtonText: newValue },
});
}}
placeholder="추가"
className="h-8 text-sm"
/>
</div>
<div className="space-y-2">
<Label htmlFor="modal-cancel-text" className="text-sm">
</Label>
<Input
id="modal-cancel-text"
value={localValues.modalCancelButtonText}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, modalCancelButtonText: newValue }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, cancelButtonText: newValue },
});
}}
placeholder="취소"
className="h-8 text-sm"
/>
</div>
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="grid-columns"> </Label>
<Select
value={localValues.gridColumns.toString()}
onValueChange={(value) => {
const gridColumns = parseInt(value, 10);
console.log("🔄 테이블 그리드 컬럼 수 변경:", gridColumns);
setLocalValues((prev) => ({ ...prev, gridColumns }));
onUpdateComponent({ gridColumns });
}}
>
<SelectTrigger>
<SelectValue placeholder="그리드 컬럼 수 선택" />
</SelectTrigger>
<SelectContent>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((num) => (
<SelectItem key={num} value={num.toString()}>
{num} ({Math.round((num / 12) * 100)}%)
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="show-search-button"
checked={localValues.showSearchButton}
onCheckedChange={(checked) => {
console.log("🔄 검색 버튼 표시 변경:", checked);
setLocalValues((prev) => ({ ...prev, showSearchButton: checked as boolean }));
onUpdateComponent({ showSearchButton: checked as boolean });
}}
/>
<Label htmlFor="show-search-button" className="text-sm">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enable-export"
checked={localValues.enableExport}
onCheckedChange={(checked) => {
console.log("🔄 내보내기 기능 변경:", checked);
setLocalValues((prev) => ({ ...prev, enableExport: checked as boolean }));
onUpdateComponent({ enableExport: checked as boolean });
}}
/>
<Label htmlFor="enable-export" className="text-sm">
</Label>
</div>
</div>
</CardContent>
</Card>
{/* 탭 설정 */}
<Tabs defaultValue="columns" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="columns" className="flex items-center space-x-1">
<Columns className="h-4 w-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="filters" className="flex items-center space-x-1">
<Filter className="h-4 w-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="pagination" className="flex items-center space-x-1">
<Table className="h-4 w-4" />
<span></span>
</TabsTrigger>
</TabsList>
{/* 컬럼 설정 */}
<TabsContent value="columns" className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium"> </h3>
<div className="flex items-center space-x-2">
<Badge variant="secondary">{component.columns.length}</Badge>
{selectedTable &&
(() => {
const availableColumns = selectedTable.columns.filter(
(col) => !component.columns.some((column) => column.columnName === col.columnName),
);
return availableColumns.length > 0 ? (
<Select onValueChange={(value) => addColumn(value)}>
<SelectTrigger className="h-8 w-32 text-xs">
<SelectValue placeholder="컬럼 추가" />
</SelectTrigger>
<SelectContent>
{availableColumns.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Button size="sm" disabled>
<Plus className="h-4 w-4" />
<span className="ml-1 text-xs"> </span>
</Button>
);
})()}
</div>
</div>
<div className="max-h-96 space-y-3 overflow-y-auto">
{component.columns.map((column, index) => (
<Card key={`${column.id}-${column.columnName}-${index}`} className="p-2">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
checked={localColumnCheckboxes[column.id]?.visible ?? column.visible}
onCheckedChange={(checked) => {
console.log("🔄 컬럼 표시 변경:", { columnId: column.id, checked });
setLocalColumnCheckboxes((prev) => ({
...prev,
[column.id]: { ...prev[column.id], visible: checked as boolean },
}));
updateColumn(column.id, { visible: checked as boolean });
}}
/>
<span className="text-sm font-medium">{column.label}</span>
<Badge variant="outline" className="text-xs">
{column.columnName}
</Badge>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
removeColumn(column.id);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={localColumnInputs[column.id] !== undefined ? localColumnInputs[column.id] : column.label}
onChange={(e) => {
const newValue = e.target.value;
setLocalColumnInputs((prev) => ({ ...prev, [column.id]: newValue }));
updateColumn(column.id, { label: newValue });
}}
onBlur={(e) => {
// 포커스 잃을 때 빈 값이면 원본 라벨로 복원하지 않음 (사용자가 의도적으로 지운 것)
const newValue = e.target.value;
if (newValue !== localColumnInputs[column.id]) {
setLocalColumnInputs((prev) => ({ ...prev, [column.id]: newValue }));
updateColumn(column.id, { label: newValue });
}
}}
placeholder="표시명을 입력하세요"
className="h-8 text-xs"
/>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Select
value={(localColumnGridColumns[column.id] ?? column.gridColumns).toString()}
onValueChange={(value) => {
const newGridColumns = parseInt(value);
console.log("🔄 컬럼 그리드 컬럼 변경:", { columnId: column.id, newGridColumns });
setLocalColumnGridColumns((prev) => ({
...prev,
[column.id]: newGridColumns,
}));
updateColumn(column.id, { gridColumns: newGridColumns });
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[1, 2, 3, 4, 6, 12].map((num) => (
<SelectItem key={num} value={num.toString()}>
{num}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center space-x-1">
<Checkbox
id={`sortable-${column.id}`}
checked={localColumnCheckboxes[column.id]?.sortable ?? column.sortable}
onCheckedChange={(checked) => {
console.log("🔄 컬럼 정렬 가능 변경:", { columnId: column.id, checked });
setLocalColumnCheckboxes((prev) => ({
...prev,
[column.id]: { ...prev[column.id], sortable: checked as boolean },
}));
updateColumn(column.id, { sortable: checked as boolean });
}}
/>
<Label htmlFor={`sortable-${column.id}`} className="text-xs">
</Label>
</div>
<div className="flex items-center space-x-1">
<Checkbox
id={`searchable-${column.id}`}
checked={localColumnCheckboxes[column.id]?.searchable ?? column.searchable}
onCheckedChange={(checked) => {
console.log("🔄 컬럼 검색 가능 변경:", { columnId: column.id, checked });
setLocalColumnCheckboxes((prev) => ({
...prev,
[column.id]: { ...prev[column.id], searchable: checked as boolean },
}));
updateColumn(column.id, { searchable: checked as boolean });
}}
/>
<Label htmlFor={`searchable-${column.id}`} className="text-xs">
</Label>
</div>
</div>
{/* 모달 전용 설정 */}
{component.enableAdd && (
<div className="border-t pt-2">
<Collapsible
open={isModalConfigOpen[column.id] || false}
onOpenChange={(open) => setIsModalConfigOpen((prev) => ({ ...prev, [column.id]: open }))}
>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
size="sm"
className="flex h-auto w-full items-center justify-between p-1"
>
<Label className="text-xs font-medium"> </Label>
<ChevronDown
className={`h-3 w-3 transition-transform ${
isModalConfigOpen[column.id] ? "rotate-180" : ""
}`}
/>
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-2">
<div className="space-y-2">
<div className="grid grid-cols-2 gap-3">
<div className="flex items-center space-x-1">
<Checkbox
id={`required-${column.id}`}
checked={
component.addModalConfig?.requiredFields?.includes(column.columnName) || false
}
onCheckedChange={(checked) => {
const requiredFields = component.addModalConfig?.requiredFields || [];
let newRequiredFields;
if (checked) {
newRequiredFields = [...requiredFields, column.columnName];
} else {
newRequiredFields = requiredFields.filter((field) => field !== column.columnName);
}
onUpdateComponent({
addModalConfig: {
...component.addModalConfig,
requiredFields: newRequiredFields,
},
});
}}
/>
<Label htmlFor={`required-${column.id}`} className="text-xs">
</Label>
</div>
<div className="flex items-center space-x-1">
<Checkbox
id={`hidden-${column.id}`}
checked={component.addModalConfig?.hiddenFields?.includes(column.columnName) || false}
onCheckedChange={(checked) => {
const hiddenFields = component.addModalConfig?.hiddenFields || [];
let newHiddenFields;
if (checked) {
newHiddenFields = [...hiddenFields, column.columnName];
} else {
newHiddenFields = hiddenFields.filter((field) => field !== column.columnName);
}
onUpdateComponent({
addModalConfig: {
...component.addModalConfig,
hiddenFields: newHiddenFields,
},
});
}}
/>
<Label htmlFor={`hidden-${column.id}`} className="text-xs">
</Label>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={
component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.inputType ||
"normal"
}
onValueChange={(value) => {
const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {};
const currentConfig = advancedConfigs[column.columnName] || {
columnName: column.columnName,
};
const newConfig = {
...currentConfig,
inputType: value as any,
autoValueType:
value === "auto" ? "current_datetime" : currentConfig.autoValueType || "none",
};
onUpdateComponent({
addModalConfig: {
...component.addModalConfig,
advancedFieldConfigs: {
...advancedConfigs,
[column.columnName]: newConfig,
},
},
});
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="normal"> </SelectItem>
<SelectItem value="readonly"> </SelectItem>
<SelectItem value="auto"> </SelectItem>
<SelectItem value="hidden"></SelectItem>
</SelectContent>
</Select>
</div>
{component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.inputType ===
"auto" && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Select
value={
component.addModalConfig?.advancedFieldConfigs?.[column.columnName]
?.autoValueType || "current_datetime"
}
onValueChange={(value) => {
const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {};
const currentConfig = advancedConfigs[column.columnName] || {
columnName: column.columnName,
};
onUpdateComponent({
addModalConfig: {
...component.addModalConfig,
advancedFieldConfigs: {
...advancedConfigs,
[column.columnName]: {
...currentConfig,
autoValueType: value as any,
},
},
},
});
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="current_datetime"> </SelectItem>
<SelectItem value="current_date"> </SelectItem>
<SelectItem value="current_time"> </SelectItem>
<SelectItem value="current_user"> </SelectItem>
<SelectItem value="uuid">UUID</SelectItem>
<SelectItem value="sequence">퀀</SelectItem>
<SelectItem value="custom"> </SelectItem>
</SelectContent>
</Select>
</div>
)}
{component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.autoValueType ===
"custom" && (
<div className="space-y-2">
<Label className="text-xs"> </Label>
<Input
value={
component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.customValue ||
""
}
onChange={(e) => {
const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {};
const currentConfig = advancedConfigs[column.columnName] || {
columnName: column.columnName,
};
onUpdateComponent({
addModalConfig: {
...component.addModalConfig,
advancedFieldConfigs: {
...advancedConfigs,
[column.columnName]: {
...currentConfig,
customValue: e.target.value,
},
},
},
});
}}
placeholder="고정값 입력..."
className="h-8 text-xs"
/>
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
</div>
)}
</div>
</Card>
))}
</div>
</TabsContent>
{/* 필터 설정 */}
<TabsContent value="filters" className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium"> </h3>
<div className="flex items-center space-x-2">
<Badge variant="secondary">{component.filters.length}</Badge>
<Button
type="button"
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
addFilter();
}}
disabled={!selectedTable}
>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
{component.filters.length === 0 ? (
<div className="text-muted-foreground py-8 text-center">
<Filter className="mx-auto mb-2 h-8 w-8 opacity-50" />
<p className="text-sm"> </p>
<p className="text-xs"> </p>
</div>
) : (
<div className="max-h-96 space-y-3 overflow-y-auto">
{component.filters.map((filter, index) => {
const getWebTypeIcon = (webType: WebType) => {
switch (webType) {
case "text":
case "email":
case "tel":
return "📝";
case "number":
case "decimal":
return "🔢";
case "date":
case "datetime":
return "📅";
case "select":
return "📋";
default:
return "🔍";
}
};
const getWebTypeDescription = (webType: WebType) => {
switch (webType) {
case "text":
return "텍스트 검색 (부분 일치)";
case "email":
return "이메일 형식 검색";
case "tel":
return "전화번호 검색";
case "number":
return "숫자 범위 검색";
case "decimal":
return "소수점 범위 검색";
case "date":
return "날짜 범위 검색";
case "datetime":
return "날짜시간 범위 검색";
case "select":
return "선택 옵션 필터";
default:
return "기본 검색";
}
};
return (
<Card key={`filter-${filter.columnName}-${filter.widgetType}-${index}`} className="p-2">
<div className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex flex-1 items-center space-x-2">
<span className="text-lg">{getWebTypeIcon(filter.widgetType)}</span>
<div className="flex-1">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
value={
localFilterInputs[`${filter.columnName}-${index}`] !== undefined
? localFilterInputs[`${filter.columnName}-${index}`]
: filter.label
}
onChange={(e) => {
const newValue = e.target.value;
const filterKey = `${filter.columnName}-${index}`;
setLocalFilterInputs((prev) => ({ ...prev, [filterKey]: newValue }));
updateFilter(index, { label: newValue });
}}
placeholder="필터 이름 입력..."
className="h-8 text-xs"
/>
</div>
<p className="text-muted-foreground mt-1 text-xs">
{getWebTypeDescription(filter.widgetType)}
</p>
</div>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
removeFilter(index);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="grid grid-cols-3 gap-3">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Select
value={filter.columnName}
onValueChange={(value) => {
const column = selectedTable?.columns.find((col) => col.columnName === value);
if (column) {
const newWidgetType = getWidgetTypeFromColumn(column);
updateFilter(index, {
columnName: value,
label: column.columnLabel || column.columnName,
widgetType: newWidgetType,
});
}
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{selectedTable?.columns
.filter((col) => isFilterableWebType(getWidgetTypeFromColumn(col)))
.map((col) => (
<SelectItem key={col.columnName} value={col.columnName}>
{getWebTypeIcon(getWidgetTypeFromColumn(col))} {col.columnLabel || col.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label className="text-xs"> </Label>
<div className="bg-muted flex h-8 items-center rounded-md px-2 text-xs">
<Badge variant="outline" className="text-xs">
{webTypeOptions.find((opt) => opt.value === filter.widgetType)?.label ||
filter.widgetType}
</Badge>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Select
value={filter.gridColumns.toString()}
onValueChange={(value) => updateFilter(index, { gridColumns: parseInt(value) })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[1, 2, 3, 4, 6, 12].map((num) => (
<SelectItem key={num} value={num.toString()}>
{num}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 웹 타입별 추가 설정 미리보기 */}
<div className="border-t pt-2">
<div className="text-muted-foreground text-xs">
{filter.widgetType === "date" || filter.widgetType === "datetime" ? (
<span>📅 ( ~ )</span>
) : filter.widgetType === "number" || filter.widgetType === "decimal" ? (
<span>🔢 ( ~ )</span>
) : filter.widgetType === "select" ? (
<span>📋 </span>
) : (
<span>🔍 </span>
)}
</div>
</div>
</div>
</Card>
);
})}
</div>
)}
</TabsContent>
{/* 페이지네이션 설정 */}
<TabsContent value="pagination" className="space-y-4">
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Checkbox
id="pagination-enabled"
checked={localValues.paginationEnabled}
onCheckedChange={(checked) => {
console.log("🔄 페이지네이션 사용 변경:", checked);
setLocalValues((prev) => ({ ...prev, paginationEnabled: checked as boolean }));
onUpdateComponent({
pagination: { ...component.pagination, enabled: checked as boolean },
});
}}
/>
<Label htmlFor="pagination-enabled"> </Label>
</div>
{component.pagination.enabled && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label> </Label>
<Select
value={component.pagination.pageSize.toString()}
onValueChange={(value) =>
onUpdateComponent({
pagination: {
...component.pagination,
pageSize: parseInt(value),
},
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{[5, 10, 20, 50, 100].map((size) => (
<SelectItem key={size} value={size.toString()}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="show-page-size-selector"
checked={localValues.showPageSizeSelector}
onCheckedChange={(checked) => {
console.log("🔄 페이지 크기 선택기 표시 변경:", checked);
setLocalValues((prev) => ({ ...prev, showPageSizeSelector: checked as boolean }));
onUpdateComponent({
pagination: {
...component.pagination,
showPageSizeSelector: checked as boolean,
},
});
}}
/>
<Label htmlFor="show-page-size-selector" className="text-sm">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="show-page-info"
checked={localValues.showPageInfo}
onCheckedChange={(checked) => {
console.log("🔄 페이지 정보 표시 변경:", checked);
setLocalValues((prev) => ({ ...prev, showPageInfo: checked as boolean }));
onUpdateComponent({
pagination: {
...component.pagination,
showPageInfo: checked as boolean,
},
});
}}
/>
<Label htmlFor="show-page-info" className="text-sm">
</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="show-first-last"
checked={localValues.showFirstLast}
onCheckedChange={(checked) => {
console.log("🔄 처음/마지막 버튼 표시 변경:", checked);
setLocalValues((prev) => ({ ...prev, showFirstLast: checked as boolean }));
onUpdateComponent({
pagination: {
...component.pagination,
showFirstLast: checked as boolean,
},
});
}}
/>
<Label htmlFor="show-first-last" className="text-sm">
/
</Label>
</div>
</div>
</div>
)}
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default DataTableConfigPanel;