1697 lines
71 KiB
TypeScript
1697 lines
71 KiB
TypeScript
"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;
|