"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"; import { useWebTypes } from "@/hooks/admin/useWebTypes"; interface DataTableConfigPanelProps { component: DataTableComponent; tables: TableInfo[]; activeTab?: string; onTabChange?: (tab: string) => void; onUpdateComponent: (updates: Partial) => void; } const DataTableConfigPanelComponent: React.FC = ({ component, tables, activeTab: externalActiveTab, onTabChange, onUpdateComponent, }) => { // 동적 웹타입 옵션 가져오기 const { webTypes } = useWebTypes({ active: "Y" }); const webTypeOptions = webTypes.map((wt) => ({ value: wt.web_type as WebType, label: wt.type_name, })); const [selectedTable, setSelectedTable] = useState(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 || "새 데이터 추가", // 테이블명도 로컬 상태로 관리 tableName: component.tableName || "", 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>({}); // 컬럼별 체크박스 및 설정 상태 const [localColumnCheckboxes, setLocalColumnCheckboxes] = useState< Record< string, { visible: boolean; sortable: boolean; searchable: boolean; } > >({}); // 컬럼별 그리드 컬럼 설정 상태 const [localColumnGridColumns, setLocalColumnGridColumns] = useState>({}); // 필터별 로컬 입력 상태 const [localFilterInputs, setLocalFilterInputs] = useState>({}); // 컬럼별 상세 설정 상태 const [localColumnDetailSettings, setLocalColumnDetailSettings] = useState>({}); // 컬럼별 상세 설정 확장/축소 상태 const [isColumnDetailOpen, setIsColumnDetailOpen] = useState>({}); // 모달 설정 확장/축소 상태 const [isModalConfigOpen, setIsModalConfigOpen] = useState>({}); // 탭 상태 관리 (외부에서 받거나 로컬 상태 사용) const [internalActiveTab, setInternalActiveTab] = useState("basic"); const activeTab = externalActiveTab || internalActiveTab; const setActiveTab = onTabChange || setInternalActiveTab; // 컴포넌트 변경 시 로컬 값 동기화 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, // 테이블명 동기화 tableName: component.tableName || "", }); // 컬럼 라벨 로컬 상태 초기화 (기존 값이 없는 경우만) 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; console.log("🆕 새 필터 로컬 상태 추가:", { filterKey, label: filter.label, columnName: filter.columnName, }); } }); // 삭제된 필터의 로컬 상태 제거 const currentFilterKeys = new Set( component.filters?.map((filter, index) => `${filter.columnName}-${index}`) || [], ); Object.keys(newFilterInputs).forEach((key) => { if (!currentFilterKeys.has(key)) { console.log("🗑️ 삭제된 필터 로컬 상태 제거:", { filterKey: key }); delete newFilterInputs[key]; } }); console.log("📝 필터 로컬 상태 동기화 완료:", { prevCount: Object.keys(prev).length, newCount: Object.keys(newFilterInputs).length, newKeys: Object.keys(newFilterInputs), }); return newFilterInputs; }); }, [ component.id, component.title, component.searchButtonText, 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) => { // 이미 같은 테이블이 선택되어 있으면 무시 if (localValues.tableName === tableName) { return; } // 로컬 상태 먼저 업데이트 setLocalValues((prev) => ({ ...prev, tableName })); const table = tables.find((t) => t.tableName === tableName); if (!table) return; console.log("🔄 테이블 변경:", { tableName, currentTableName: localValues.tableName, table, columnsCount: table.columns.length, }); // 테이블의 모든 컬럼을 기본 설정으로 추가 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, }); // 상태 업데이트를 한 번에 처리 setTimeout(() => { onUpdateComponent({ tableName, columns: defaultColumns, filters: [], // 빈 필터 배열 }); setSelectedTable(table); }, 0); }, [tables, onUpdateComponent, localValues.tableName], ); // 컬럼 타입 추론 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"; // 파일 타입 추론 if ( name.includes("file") || name.includes("attach") || name.includes("upload") || name.includes("document") || name.includes("docs") || name.includes("image") || name.includes("photo") || name.includes("picture") || name.includes("media") ) { return "file"; } // 텍스트 타입 (기본값) return "text"; }; // 컬럼 업데이트 const updateColumn = useCallback( (columnId: string, updates: Partial) => { const updatedColumns = component.columns.map((col) => (col.id === columnId ? { ...col, ...updates } : col)); onUpdateComponent({ columns: updatedColumns }); }, [component.columns, onUpdateComponent], ); // 컬럼 상세 설정 업데이트 (테이블 타입 관리에도 반영) const updateColumnDetailSettings = useCallback( async (columnId: string, webTypeConfig: any) => { // 1. 먼저 화면 컴포넌트의 컬럼 설정 업데이트 const updatedColumns = component.columns.map((col) => (col.id === columnId ? { ...col, webTypeConfig } : col)); console.log("🔄 컬럼 상세 설정 업데이트:", { columnId, webTypeConfig, updatedColumns }); onUpdateComponent({ columns: updatedColumns }); // 2. 테이블 타입 관리에도 반영 (라디오 타입인 경우) const targetColumn = component.columns.find((col) => col.id === columnId); if (targetColumn && targetColumn.widgetType === "radio" && selectedTable) { try { // TODO: 테이블 타입 관리 API 호출하여 웹 타입과 상세 설정 업데이트 console.log("📡 테이블 타입 관리 업데이트 필요:", { tableName: component.tableName, columnName: targetColumn.columnName, webType: "radio", detailSettings: JSON.stringify(webTypeConfig), }); } catch (error) { console.error("테이블 타입 관리 업데이트 실패:", error); } } }, [component.columns, component.tableName, selectedTable, onUpdateComponent], ); // 컬럼의 현재 웹 타입 가져오기 (테이블 타입 관리에서 설정된 값) const getColumnWebType = useCallback( (column: DataTableColumn) => { // 테이블 타입 관리에서 설정된 웹 타입 찾기 if (!selectedTable) return "text"; const tableColumn = selectedTable.columns.find((col) => col.columnName === column.columnName); return ( tableColumn?.webType || getWidgetTypeFromColumn( tableColumn || { columnName: column.columnName, dataType: "text", tableName: "", isNullable: true, }, ) ); }, [selectedTable], ); // 컬럼의 현재 상세 설정 가져오기 const getColumnCurrentDetailSettings = useCallback((column: DataTableColumn) => { return column.webTypeConfig || {}; }, []); // 웹 타입별 상세 설정 렌더링 const renderColumnDetailSettings = useCallback( (column: DataTableColumn) => { const webType = getColumnWebType(column); const currentSettings = getColumnCurrentDetailSettings(column); const localSettings = localColumnDetailSettings[column.id] || currentSettings; const updateSettings = (newSettings: any) => { const merged = { ...localSettings, ...newSettings }; setLocalColumnDetailSettings((prev) => ({ ...prev, [column.id]: merged, })); updateColumnDetailSettings(column.id, merged); }; switch (webType) { case "select": case "dropdown": case "radio": return (
{(localSettings.options || []).map((option: any, index: number) => { // 안전한 값 추출 const currentLabel = typeof option === "object" && option !== null ? option.label || option.value || "" : String(option || ""); return (
{ const newOptions = [...(localSettings.options || [])]; newOptions[index] = { label: e.target.value, value: e.target.value }; updateSettings({ options: newOptions }); }} placeholder="옵션명" className="h-7 text-xs" />
); })}
{webType === "radio" ? (
) : (
updateSettings({ multiple: checked })} />
)}
); case "number": case "decimal": return (
updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })} placeholder="최소값" className="h-7 text-xs" />
updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })} placeholder="최대값" className="h-7 text-xs" />
{webType === "decimal" && (
updateSettings({ step: e.target.value })} placeholder="0.01" className="h-7 text-xs" />
)}
); case "date": case "datetime": return (
updateSettings({ minDate: e.target.value })} className="h-7 text-xs" />
updateSettings({ maxDate: e.target.value })} className="h-7 text-xs" />
{webType === "datetime" && (
updateSettings({ showSeconds: checked })} />
)}
); case "text": case "email": case "tel": return (
updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })} placeholder="최대 문자 수" className="h-7 text-xs" />
updateSettings({ placeholder: e.target.value })} placeholder="입력 안내 텍스트" className="h-7 text-xs" />
); case "textarea": return (
updateSettings({ rows: Number(e.target.value) })} placeholder="3" className="h-7 text-xs" />
updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })} placeholder="최대 문자 수" className="h-7 text-xs" />
); case "file": return (
updateSettings({ accept: e.target.value })} placeholder=".jpg,.png,.pdf" className="h-7 text-xs" />
updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })} placeholder="10" className="h-7 text-xs" />
updateSettings({ multiple: checked })} />
); default: return
이 웹 타입({webType})에 대한 상세 설정이 없습니다.
; } }, [getColumnWebType, getColumnCurrentDetailSettings, localColumnDetailSettings, updateColumnDetailSettings], ); // 컬럼 삭제 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) => { 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, })), }); // 먼저 로컬 상태를 업데이트하고 const filterKey = `${newFilter.columnName}-${component.filters.length}`; setLocalFilterInputs((prev) => { const newState = { ...prev, [filterKey]: newFilter.label, }; console.log("📝 필터 로컬 상태 업데이트:", { filterKey, newLabel: newFilter.label, prevState: prev, newState, }); return newState; }); // 그 다음 컴포넌트 상태를 업데이트 onUpdateComponent({ filters: updatedFilters }); // 필터 추가 후 필터 탭으로 자동 이동 setActiveTab("filters"); console.log("🔍 필터 추가 후 탭 이동:", { activeTab: "filters", isExternalControl: !!onTabChange, }); // 강제로 리렌더링을 트리거하기 위해 여러 방법 사용 setTimeout(() => { setLocalFilterInputs((prev) => ({ ...prev, [filterKey]: newFilter.label, })); console.log("🔄 setTimeout에서 강제 로컬 상태 업데이트:", { filterKey, label: newFilter.label }); }, 0); // 추가적인 강제 업데이트 setTimeout(() => { setLocalFilterInputs((prev) => { const updated = { ...prev, [filterKey]: newFilter.label }; console.log("🔄 두 번째 강제 업데이트:", { updated }); return updated; }); }, 100); console.log("✅ 필터 추가 완료 - 로컬 상태와 컴포넌트 모두 업데이트됨", { filterKey, newFilterLabel: newFilter.label, switchedToTab: "filters", }); }, [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 nonFilterableTypes = ["file", "button", "image"]; return !nonFilterableTypes.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 = { 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); // 컬럼 추가 후 컬럼 탭으로 자동 이동 setActiveTab("columns"); console.log("📋 컬럼 추가 후 탭 이동:", { activeTab: "columns", isExternalControl: !!onTabChange, }); console.log("✅ 컬럼 추가 완료 - onUpdateComponent 호출됨"); }, [selectedTable, component.columns, component.filters, onUpdateComponent], ); // 가상 파일 컬럼 추가 const addVirtualFileColumn = useCallback(() => { const fileColumnCount = component.columns.filter((col) => col.isVirtualFileColumn).length; const newColumnName = `file_column_${fileColumnCount + 1}`; // 순차적 번호 사용 const newColumn: DataTableColumn = { id: generateComponentId(), columnName: newColumnName, label: `파일 컬럼 ${fileColumnCount + 1}`, widgetType: "file", gridColumns: 2, visible: true, filterable: false, // 파일 컬럼은 필터링 불가 sortable: false, // 파일 컬럼은 정렬 불가 searchable: false, // 파일 컬럼은 검색 불가 isVirtualFileColumn: true, // 가상 파일 컬럼 표시 fileColumnConfig: { docType: "DOCUMENT", docTypeName: "일반 문서", maxFiles: 5, accept: ["*/*"], }, }; console.log("📁 가상 파일 컬럼 추가:", { columnName: newColumn.columnName, label: newColumn.label, isVirtualFileColumn: newColumn.isVirtualFileColumn, }); // 로컬 상태에 새 컬럼 입력값 추가 setLocalColumnInputs((prev) => ({ ...prev, [newColumn.id]: newColumn.label, })); // 로컬 체크박스 상태에 새 컬럼 추가 setLocalColumnCheckboxes((prev) => ({ ...prev, [newColumn.id]: { visible: newColumn.visible, sortable: newColumn.sortable, searchable: newColumn.searchable, }, })); // 로컬 그리드 컬럼 상태에 새 컬럼 추가 setLocalColumnGridColumns((prev) => ({ ...prev, [newColumn.id]: newColumn.gridColumns, })); // 컬럼 업데이트 const updates: Partial = { columns: [...component.columns, newColumn], }; onUpdateComponent(updates); // 컬럼 추가 후 컬럼 탭으로 자동 이동 setActiveTab("columns"); console.log("✅ 가상 파일 컬럼 추가 완료"); }, [component.columns, onUpdateComponent]); return (
기본 설정 컬럼 설정 필터 설정 모달 설정 {/* 기본 설정 */} 기본 설정
{ const newValue = e.target.value; setLocalValues((prev) => ({ ...prev, title: newValue })); onUpdateComponent({ title: newValue }); }} placeholder="테이블 제목을 입력하세요" />
{/* CRUD 기능 설정 */}

CRUD 기능

{ setLocalValues((prev) => ({ ...prev, enableAdd: checked as boolean })); onUpdateComponent({ enableAdd: checked as boolean }); }} />
{ setLocalValues((prev) => ({ ...prev, enableEdit: checked as boolean })); onUpdateComponent({ enableEdit: checked as boolean }); }} />
{ setLocalValues((prev) => ({ ...prev, enableDelete: checked as boolean })); onUpdateComponent({ enableDelete: checked as boolean }); }} />
{ const newValue = e.target.value; setLocalValues((prev) => ({ ...prev, addButtonText: newValue })); onUpdateComponent({ addButtonText: newValue }); }} placeholder="추가" disabled={!localValues.enableAdd} className="h-8 text-sm" />
{ const newValue = e.target.value; setLocalValues((prev) => ({ ...prev, editButtonText: newValue })); onUpdateComponent({ editButtonText: newValue }); }} placeholder="수정" disabled={!localValues.enableEdit} className="h-8 text-sm" />
{ const newValue = e.target.value; setLocalValues((prev) => ({ ...prev, deleteButtonText: newValue })); onUpdateComponent({ deleteButtonText: newValue }); }} placeholder="삭제" disabled={!localValues.enableDelete} className="h-8 text-sm" />
{/* 추가 모달 커스터마이징 설정 */} {localValues.enableAdd && (

추가 모달 설정

{ const newValue = e.target.value; setLocalValues((prev) => ({ ...prev, modalTitle: newValue })); onUpdateComponent({ addModalConfig: { ...component.addModalConfig, title: newValue }, }); }} placeholder="새 데이터 추가" className="h-8 text-sm" />
{ const newValue = e.target.value; setLocalValues((prev) => ({ ...prev, modalDescription: newValue })); onUpdateComponent({ addModalConfig: { ...component.addModalConfig, description: newValue }, }); }} placeholder="모달에 표시될 설명을 입력하세요" className="h-8 text-sm" />
{localValues.modalLayout === "grid" && (
)}
{ const newValue = e.target.value; setLocalValues((prev) => ({ ...prev, modalSubmitButtonText: newValue })); onUpdateComponent({ addModalConfig: { ...component.addModalConfig, submitButtonText: newValue }, }); }} placeholder="추가" className="h-8 text-sm" />
{ const newValue = e.target.value; setLocalValues((prev) => ({ ...prev, modalCancelButtonText: newValue })); onUpdateComponent({ addModalConfig: { ...component.addModalConfig, cancelButtonText: newValue }, }); }} placeholder="취소" className="h-8 text-sm" />
)}
{ console.log("🔄 검색 버튼 표시 변경:", checked); setLocalValues((prev) => ({ ...prev, showSearchButton: checked as boolean })); onUpdateComponent({ showSearchButton: checked as boolean }); }} />
{ console.log("🔄 내보내기 기능 변경:", checked); setLocalValues((prev) => ({ ...prev, enableExport: checked as boolean })); onUpdateComponent({ enableExport: checked as boolean }); }} />
컬럼 설정

테이블 컬럼 설정

{component.columns.length}개
{/* 파일 컬럼 추가 버튼 */} {/* 기존 DB 컬럼 추가 */} {selectedTable && (() => { const availableColumns = selectedTable.columns.filter( (col) => !component.columns.some((column) => column.columnName === col.columnName), ); return availableColumns.length > 0 ? ( ) : ( ); })()}
{component.columns.map((column, index) => (
{ 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 }); }} /> {column.label}
{column.columnName} {getColumnWebType(column)}
{ 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" />
{ 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 }); }} />
{ 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 }); }} />
{/* 웹 타입 상세 설정 */} {isColumnDetailOpen[column.id] && (
{getColumnWebType(column)}
{renderColumnDetailSettings(column)}
)} {/* 모달 전용 설정 */} {component.enableAdd && (
setIsModalConfigOpen((prev) => ({ ...prev, [column.id]: open }))} >
{ 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, }, }); }} />
{ 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, }, }); }} />
{component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.inputType === "auto" && (
)} {component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.autoValueType === "custom" && (
{ 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" />
)}
)}
))}
필터 설정

검색 필터 설정

{component.filters.length}개
{component.filters.length === 0 ? (

필터가 없습니다

컬럼을 추가하면 자동으로 필터가 생성됩니다

) : (
{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 (
{getWebTypeIcon(filter.widgetType)}
{ const filterKey = `${filter.columnName}-${index}`; const localValue = localFilterInputs[filterKey]; const finalValue = localValue !== undefined ? localValue : filter.label; console.log("🎯 필터 입력 값 결정:", { filterKey, localValue, filterLabel: filter.label, finalValue, allLocalInputs: Object.keys(localFilterInputs), }); return finalValue; })()} 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" />

{getWebTypeDescription(filter.widgetType)}

{webTypeOptions.find((opt) => opt.value === filter.widgetType)?.label || filter.widgetType}
{/* 웹 타입별 추가 설정 미리보기 */}
{filter.widgetType === "date" || filter.widgetType === "datetime" ? ( 📅 날짜 범위 선택 (시작일 ~ 종료일) ) : filter.widgetType === "number" || filter.widgetType === "decimal" ? ( 🔢 숫자 범위 입력 (최소값 ~ 최대값) ) : filter.widgetType === "select" ? ( 📋 다중 선택 옵션 ) : ( 🔍 텍스트 입력 검색 )}
); })}
)}
모달 및 페이징 설정 {/* 페이지네이션 설정 */}

페이지네이션 설정

{ console.log("🔄 페이지네이션 사용 변경:", checked); setLocalValues((prev) => ({ ...prev, paginationEnabled: checked as boolean })); onUpdateComponent({ pagination: { ...component.pagination, enabled: checked as boolean }, }); }} />
{component.pagination.enabled && (
{ console.log("🔄 페이지 크기 선택기 표시 변경:", checked); setLocalValues((prev) => ({ ...prev, showPageSizeSelector: checked as boolean })); onUpdateComponent({ pagination: { ...component.pagination, showPageSizeSelector: checked as boolean, }, }); }} />
{ console.log("🔄 페이지 정보 표시 변경:", checked); setLocalValues((prev) => ({ ...prev, showPageInfo: checked as boolean })); onUpdateComponent({ pagination: { ...component.pagination, showPageInfo: checked as boolean, }, }); }} />
{ console.log("🔄 처음/마지막 버튼 표시 변경:", checked); setLocalValues((prev) => ({ ...prev, showFirstLast: checked as boolean })); onUpdateComponent({ pagination: { ...component.pagination, showFirstLast: checked as boolean, }, }); }} />
)}
{/* 모달 설정은 여기에 추가 가능 */}

모달 설정

추가/수정 모달 관련 설정들이 여기에 표시됩니다.

); }; // React.memo로 감싸서 불필요한 리렌더링 방지 export const DataTableConfigPanel = React.memo(DataTableConfigPanelComponent, (prevProps, nextProps) => { // 컴포넌트 ID가 다르면 리렌더링 if (prevProps.component.id !== nextProps.component.id) { return false; } // 테이블 목록이 변경되면 리렌더링 if (prevProps.tables.length !== nextProps.tables.length) { return false; } // 활성 탭이 변경되면 리렌더링 if (prevProps.activeTab !== nextProps.activeTab) { return false; } // 컬럼 개수나 필터 개수가 변경되면 리렌더링 if ( prevProps.component.columns?.length !== nextProps.component.columns?.length || prevProps.component.filters?.length !== nextProps.component.filters?.length ) { return false; } // 기본 속성들이 변경되면 리렌더링 if ( prevProps.component.title !== nextProps.component.title || prevProps.component.tableName !== nextProps.component.tableName || prevProps.component.searchButtonText !== nextProps.component.searchButtonText ) { return false; } // 그 외의 경우는 리렌더링하지 않음 return true; }); export default DataTableConfigPanel;