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

2301 lines
99 KiB
TypeScript
Raw Normal View History

2025-09-03 15:23:12 +09:00
"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";
2025-09-03 16:38:10 +09:00
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Table, Plus, Trash2, Settings, Filter, Columns, ChevronDown } from "lucide-react";
2025-09-03 15:23:12 +09:00
import { DataTableComponent, DataTableColumn, DataTableFilter, TableInfo, ColumnInfo, WebType } from "@/types/screen";
import { generateComponentId } from "@/lib/utils/generateId";
interface DataTableConfigPanelProps {
component: DataTableComponent;
tables: TableInfo[];
2025-09-03 18:23:47 +09:00
activeTab?: string;
onTabChange?: (tab: string) => void;
2025-09-03 15:23:12 +09:00
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: "전화번호" },
];
2025-09-03 18:23:47 +09:00
export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({
component,
tables,
activeTab: externalActiveTab,
onTabChange,
onUpdateComponent,
}) => {
2025-09-03 15:23:12 +09:00
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,
2025-09-03 16:38:10 +09:00
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 || "취소",
2025-09-03 15:23:12 +09:00
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>>({});
2025-09-03 16:38:10 +09:00
// 필터별 로컬 입력 상태
const [localFilterInputs, setLocalFilterInputs] = useState<Record<string, string>>({});
2025-09-03 17:12:27 +09:00
// 컬럼별 상세 설정 상태
const [localColumnDetailSettings, setLocalColumnDetailSettings] = useState<Record<string, any>>({});
// 컬럼별 상세 설정 확장/축소 상태
const [isColumnDetailOpen, setIsColumnDetailOpen] = useState<Record<string, boolean>>({});
2025-09-03 16:38:10 +09:00
// 모달 설정 확장/축소 상태
const [isModalConfigOpen, setIsModalConfigOpen] = useState<Record<string, boolean>>({});
2025-09-03 18:23:47 +09:00
// 탭 상태 관리 (외부에서 받거나 로컬 상태 사용)
const [internalActiveTab, setInternalActiveTab] = useState("basic");
const activeTab = externalActiveTab || internalActiveTab;
const setActiveTab = onTabChange || setInternalActiveTab;
2025-09-03 15:23:12 +09:00
// 컴포넌트 변경 시 로컬 값 동기화
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,
2025-09-03 16:38:10 +09:00
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 || "취소",
2025-09-03 15:23:12 +09:00
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;
});
2025-09-03 16:38:10 +09:00
// 필터별 로컬 입력 상태 동기화 (기존 값 보존하면서 새 필터만 추가)
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;
2025-09-03 18:23:47 +09:00
console.log("🆕 새 필터 로컬 상태 추가:", {
filterKey,
label: filter.label,
columnName: filter.columnName,
});
2025-09-03 16:38:10 +09:00
}
});
// 삭제된 필터의 로컬 상태 제거
const currentFilterKeys = new Set(
component.filters?.map((filter, index) => `${filter.columnName}-${index}`) || [],
);
Object.keys(newFilterInputs).forEach((key) => {
if (!currentFilterKeys.has(key)) {
2025-09-03 18:23:47 +09:00
console.log("🗑️ 삭제된 필터 로컬 상태 제거:", { filterKey: key });
2025-09-03 16:38:10 +09:00
delete newFilterInputs[key];
}
});
2025-09-03 18:23:47 +09:00
console.log("📝 필터 로컬 상태 동기화 완료:", {
prevCount: Object.keys(prev).length,
newCount: Object.keys(newFilterInputs).length,
newKeys: Object.keys(newFilterInputs),
});
2025-09-03 16:38:10 +09:00
return newFilterInputs;
});
2025-09-03 15:23:12 +09:00
}, [
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";
2025-09-05 12:04:13 +09:00
// 파일 타입 추론
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";
}
2025-09-03 15:23:12 +09:00
// 텍스트 타입 (기본값)
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],
);
2025-09-03 17:12:27 +09:00
// 컬럼 상세 설정 업데이트 (테이블 타입 관리에도 반영)
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 ||
2025-09-03 18:23:47 +09:00
getWidgetTypeFromColumn(
tableColumn || {
columnName: column.columnName,
dataType: "text",
tableName: "",
isNullable: true,
},
)
2025-09-03 17:12:27 +09:00
);
},
[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 (
<div className="space-y-3">
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<div className="space-y-2">
{(localSettings.options || []).map((option: any, index: number) => {
// 안전한 값 추출
const currentLabel =
typeof option === "object" && option !== null
? option.label || option.value || ""
: String(option || "");
return (
<div key={index} className="flex items-center space-x-2">
<Input
value={currentLabel}
onChange={(e) => {
const newOptions = [...(localSettings.options || [])];
newOptions[index] = { label: e.target.value, value: e.target.value };
updateSettings({ options: newOptions });
}}
placeholder="옵션명"
className="h-7 text-xs"
/>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => {
const newOptions = (localSettings.options || []).filter((_: any, i: number) => i !== index);
updateSettings({ options: newOptions });
}}
className="h-7 w-7 p-0"
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
);
})}
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const newOption = { label: "", value: "" };
updateSettings({ options: [...(localSettings.options || []), newOption] });
}}
className="h-7 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
</div>
{webType === "radio" ? (
<div className="space-y-2">
<Label className="text-xs font-medium"> </Label>
<Select
value={localSettings.defaultValue || "__NONE__"}
onValueChange={(value) => updateSettings({ defaultValue: value === "__NONE__" ? "" : value })}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue placeholder="기본값 선택..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="__NONE__"> </SelectItem>
{(localSettings.options || []).map((option: any, index: number) => {
// 안전한 문자열 변환
const getStringValue = (val: any): string => {
if (typeof val === "string") return val;
if (typeof val === "number") return String(val);
if (typeof val === "object" && val !== null) {
return val.label || val.value || val.name || JSON.stringify(val);
}
return String(val || "");
};
const optionValue = getStringValue(option.value || option.label || option) || `option-${index}`;
const optionLabel =
getStringValue(option.label || option.value || option) || `옵션 ${index + 1}`;
return (
<SelectItem key={index} value={optionValue}>
{optionLabel}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
) : (
<div className="flex items-center space-x-2">
<Checkbox
checked={localSettings.multiple || false}
onCheckedChange={(checked) => updateSettings({ multiple: checked })}
/>
<Label className="text-xs"> </Label>
</div>
)}
</div>
);
case "number":
case "decimal":
return (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
type="number"
value={localSettings.min || ""}
onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최소값"
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
type="number"
value={localSettings.max || ""}
onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대값"
className="h-7 text-xs"
/>
</div>
</div>
{webType === "decimal" && (
<div className="space-y-1">
<Label className="text-xs"> (step)</Label>
<Input
type="number"
step="0.01"
value={localSettings.step || "0.01"}
onChange={(e) => updateSettings({ step: e.target.value })}
placeholder="0.01"
className="h-7 text-xs"
/>
</div>
)}
</div>
);
case "date":
case "datetime":
return (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
type="date"
value={localSettings.minDate || ""}
onChange={(e) => updateSettings({ minDate: e.target.value })}
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
type="date"
value={localSettings.maxDate || ""}
onChange={(e) => updateSettings({ maxDate: e.target.value })}
className="h-7 text-xs"
/>
</div>
</div>
{webType === "datetime" && (
<div className="flex items-center space-x-2">
<Checkbox
checked={localSettings.showSeconds || false}
onCheckedChange={(checked) => updateSettings({ showSeconds: checked })}
/>
<Label className="text-xs"> </Label>
</div>
)}
</div>
);
case "text":
case "email":
case "tel":
return (
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
type="number"
value={localSettings.maxLength || ""}
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대 문자 수"
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-xs"></Label>
<Input
value={localSettings.placeholder || ""}
onChange={(e) => updateSettings({ placeholder: e.target.value })}
placeholder="입력 안내 텍스트"
className="h-7 text-xs"
/>
</div>
</div>
);
case "textarea":
return (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
type="number"
value={localSettings.rows || "3"}
onChange={(e) => updateSettings({ rows: Number(e.target.value) })}
placeholder="3"
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
type="number"
value={localSettings.maxLength || ""}
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
placeholder="최대 문자 수"
className="h-7 text-xs"
/>
</div>
</div>
</div>
);
case "file":
return (
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-xs"> </Label>
<Input
value={localSettings.accept || ""}
onChange={(e) => updateSettings({ accept: e.target.value })}
placeholder=".jpg,.png,.pdf"
className="h-7 text-xs"
/>
</div>
<div className="space-y-1">
<Label className="text-xs"> (MB)</Label>
<Input
type="number"
value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"}
onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })}
placeholder="10"
className="h-7 text-xs"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
checked={localSettings.multiple || false}
onCheckedChange={(checked) => updateSettings({ multiple: checked })}
/>
<Label className="text-xs"> </Label>
</div>
</div>
);
default:
return <div className="py-2 text-xs text-gray-500"> ({webType}) .</div>;
}
},
[getColumnWebType, getColumnCurrentDetailSettings, localColumnDetailSettings, updateColumnDetailSettings],
);
2025-09-03 15:23:12 +09:00
// 컬럼 삭제
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));
2025-09-03 16:38:10 +09:00
console.log("🔄 필터 업데이트:", { index, updates, updatedFilters });
2025-09-03 15:23:12 +09:00
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,
2025-09-03 16:38:10 +09:00
label: targetColumn.columnLabel || targetColumn.columnName,
2025-09-03 15:23:12 +09:00
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,
})),
});
2025-09-03 18:23:47 +09:00
// 먼저 로컬 상태를 업데이트하고
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;
});
// 그 다음 컴포넌트 상태를 업데이트
2025-09-03 15:23:12 +09:00
onUpdateComponent({ filters: updatedFilters });
2025-09-03 18:23:47 +09:00
// 필터 추가 후 필터 탭으로 자동 이동
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",
});
2025-09-03 15:23:12 +09:00
}, [selectedTable, component.filters, onUpdateComponent]);
// 필터 삭제
const removeFilter = useCallback(
(index: number) => {
const updatedFilters = component.filters.filter((_, i) => i !== index);
2025-09-03 16:38:10 +09:00
// 로컬 필터 입력 상태에서도 해당 필터 제거
setLocalFilterInputs((prev) => {
const newFilterInputs = { ...prev };
const filterKey = `${component.filters?.[index]?.columnName}-${index}`;
delete newFilterInputs[filterKey];
return newFilterInputs;
});
2025-09-03 15:23:12 +09:00
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);
2025-09-03 18:23:47 +09:00
// 컬럼 추가 후 컬럼 탭으로 자동 이동
setActiveTab("columns");
console.log("📋 컬럼 추가 후 탭 이동:", {
activeTab: "columns",
isExternalControl: !!onTabChange,
});
2025-09-03 15:23:12 +09:00
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<DataTableComponent> = {
columns: [...component.columns, newColumn],
};
onUpdateComponent(updates);
// 컬럼 추가 후 컬럼 탭으로 자동 이동
setActiveTab("columns");
console.log("✅ 가상 파일 컬럼 추가 완료");
}, [component.columns, onUpdateComponent]);
2025-09-03 15:23:12 +09:00
return (
2025-09-03 18:23:47 +09:00
<div className="max-h-[80vh] p-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="basic"> </TabsTrigger>
<TabsTrigger value="columns"> </TabsTrigger>
<TabsTrigger value="filters"> </TabsTrigger>
<TabsTrigger value="modal"> </TabsTrigger>
</TabsList>
2025-09-03 16:38:10 +09:00
2025-09-03 18:23:47 +09:00
<TabsContent value="basic" className="mt-4 max-h-[70vh] overflow-y-auto">
{/* 기본 설정 */}
<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">
2025-09-03 16:38:10 +09:00
<div className="space-y-2">
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 16:38:10 +09:00
</div>
<div className="space-y-2">
2025-09-03 18:23:47 +09:00
<Label htmlFor="title"> </Label>
2025-09-03 16:38:10 +09:00
<Input
2025-09-03 18:23:47 +09:00
id="title"
value={localValues.title}
2025-09-03 16:38:10 +09:00
onChange={(e) => {
const newValue = e.target.value;
2025-09-03 18:23:47 +09:00
setLocalValues((prev) => ({ ...prev, title: newValue }));
onUpdateComponent({ title: newValue });
2025-09-03 16:38:10 +09:00
}}
2025-09-03 18:23:47 +09:00
placeholder="테이블 제목을 입력하세요"
2025-09-03 16:38:10 +09:00
/>
</div>
2025-09-03 18:23:47 +09:00
{/* CRUD 기능 설정 */}
<div className="space-y-4">
<div className="flex items-center space-x-2">
<h4 className="text-sm font-medium">CRUD </h4>
2025-09-03 16:38:10 +09:00
</div>
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 16:38:10 +09:00
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 16:38:10 +09:00
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 16:38:10 +09:00
</div>
2025-09-03 18:23:47 +09:00
<div className="grid grid-cols-3 gap-4">
2025-09-03 16:38:10 +09:00
<div className="space-y-2">
2025-09-03 18:23:47 +09:00
<Label htmlFor="add-button-text" className="text-sm">
2025-09-03 16:38:10 +09:00
</Label>
2025-09-03 18:23:47 +09:00
<Input
id="add-button-text"
value={localValues.addButtonText}
onChange={(e) => {
const newValue = e.target.value;
setLocalValues((prev) => ({ ...prev, addButtonText: newValue }));
onUpdateComponent({ addButtonText: newValue });
2025-09-03 16:38:10 +09:00
}}
2025-09-03 18:23:47 +09:00
placeholder="추가"
disabled={!localValues.enableAdd}
className="h-8 text-sm"
/>
2025-09-03 16:38:10 +09:00
</div>
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 16:38:10 +09:00
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 16:38:10 +09:00
</div>
</div>
2025-09-03 15:23:12 +09:00
2025-09-03 18:23:47 +09:00
{/* 추가 모달 커스터마이징 설정 */}
{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>
2025-09-03 15:23:12 +09:00
2025-09-03 18:23:47 +09:00
<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 },
});
2025-09-03 15:23:12 +09:00
}}
2025-09-03 18:23:47 +09:00
placeholder="새 데이터 추가"
className="h-8 text-sm"
2025-09-03 15:23:12 +09:00
/>
2025-09-03 17:12:27 +09:00
</div>
2025-09-03 18:23:47 +09:00
<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 },
});
2025-09-03 17:12:27 +09:00
}}
>
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 15:23:12 +09:00
</div>
</div>
2025-09-03 18:23:47 +09:00
<div className="space-y-2">
<Label htmlFor="modal-description" className="text-sm">
</Label>
2025-09-03 15:23:12 +09:00
<Input
2025-09-03 18:23:47 +09:00
id="modal-description"
value={localValues.modalDescription}
2025-09-03 15:23:12 +09:00
onChange={(e) => {
const newValue = e.target.value;
2025-09-03 18:23:47 +09:00
setLocalValues((prev) => ({ ...prev, modalDescription: newValue }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, description: newValue },
});
2025-09-03 15:23:12 +09:00
}}
2025-09-03 18:23:47 +09:00
placeholder="모달에 표시될 설명을 입력하세요"
className="h-8 text-sm"
2025-09-03 15:23:12 +09:00
/>
</div>
2025-09-03 18:23:47 +09:00
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="modal-layout" className="text-sm">
</Label>
2025-09-03 15:23:12 +09:00
<Select
2025-09-03 18:23:47 +09:00
value={localValues.modalLayout}
2025-09-03 15:23:12 +09:00
onValueChange={(value) => {
2025-09-03 18:23:47 +09:00
setLocalValues((prev) => ({ ...prev, modalLayout: value as any }));
onUpdateComponent({
addModalConfig: { ...component.addModalConfig, layout: value as any },
});
2025-09-03 15:23:12 +09:00
}}
>
2025-09-03 18:23:47 +09:00
<SelectTrigger className="h-8 text-sm">
2025-09-03 15:23:12 +09:00
<SelectValue />
</SelectTrigger>
<SelectContent>
2025-09-03 18:23:47 +09:00
<SelectItem value="single"> </SelectItem>
<SelectItem value="two-column">2</SelectItem>
<SelectItem value="grid"></SelectItem>
2025-09-03 15:23:12 +09:00
</SelectContent>
</Select>
</div>
2025-09-03 18:23:47 +09:00
{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 },
});
2025-09-03 15:23:12 +09:00
}}
2025-09-03 18:23:47 +09:00
placeholder="추가"
className="h-8 text-sm"
2025-09-03 15:23:12 +09:00
/>
</div>
2025-09-03 18:23:47 +09:00
<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 },
});
2025-09-03 15:23:12 +09:00
}}
2025-09-03 18:23:47 +09:00
placeholder="취소"
className="h-8 text-sm"
2025-09-03 15:23:12 +09:00
/>
</div>
</div>
2025-09-03 18:23:47 +09:00
</div>
)}
2025-09-03 16:38:10 +09:00
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 16:38:10 +09:00
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 16:38:10 +09:00
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 15:23:12 +09:00
</div>
2025-09-03 18:23:47 +09:00
</div>
</CardContent>
</Card>
2025-09-03 15:23:12 +09:00
</TabsContent>
2025-09-03 18:23:47 +09:00
<TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-sm">
<Columns className="h-4 w-4" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent 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>
{/* 파일 컬럼 추가 버튼 */}
<Button size="sm" variant="outline" onClick={addVirtualFileColumn} className="h-8 text-xs">
<Plus className="h-4 w-4" />
<span className="ml-1"> </span>
</Button>
{/* 기존 DB 컬럼 추가 */}
2025-09-03 18:23:47 +09:00
{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="DB 컬럼" />
2025-09-03 18:23:47 +09:00
</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"> DB </span>
2025-09-03 18:23:47 +09:00
</Button>
);
})()}
</div>
</div>
2025-09-03 15:23:12 +09:00
2025-09-03 18:23:47 +09:00
<div className="max-h-96 space-y-3 overflow-x-hidden overflow-y-auto">
{component.columns.map((column, index) => (
<Card key={`${column.id}-${column.columnName}-${index}`} className="w-full p-2">
2025-09-03 16:38:10 +09:00
<div className="space-y-2">
2025-09-03 18:23:47 +09:00
<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="truncate text-sm font-medium">{column.label}</span>
</div>
<div className="flex items-center space-x-1">
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsColumnDetailOpen((prev) => ({
...prev,
[column.id]: !prev[column.id],
}));
}}
>
<Settings className="h-3 w-3" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
removeColumn(column.id);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
2025-09-03 15:23:12 +09:00
</div>
</div>
2025-09-03 18:23:47 +09:00
<div className="flex flex-wrap gap-1">
<Badge variant="outline" className="max-w-[120px] truncate text-xs">
{column.columnName}
</Badge>
<Badge variant="secondary" className="max-w-[80px] truncate text-xs">
{getColumnWebType(column)}
</Badge>
</div>
</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 });
2025-09-03 15:23:12 +09:00
}}
2025-09-03 18:23:47 +09:00
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"
/>
2025-09-03 15:23:12 +09:00
</div>
2025-09-03 18:23:47 +09:00
<div className="space-y-2">
2025-09-03 15:23:12 +09:00
<div className="space-y-1">
2025-09-03 18:23:47 +09:00
<Label className="text-xs"> </Label>
2025-09-03 15:23:12 +09:00
<Select
2025-09-03 18:23:47 +09:00
value={(localColumnGridColumns[column.id] ?? column.gridColumns).toString()}
2025-09-03 15:23:12 +09:00
onValueChange={(value) => {
2025-09-03 18:23:47 +09:00
const newGridColumns = parseInt(value);
console.log("🔄 컬럼 그리드 컬럼 변경:", { columnId: column.id, newGridColumns });
setLocalColumnGridColumns((prev) => ({
...prev,
[column.id]: newGridColumns,
}));
updateColumn(column.id, { gridColumns: newGridColumns });
2025-09-03 15:23:12 +09:00
}}
>
<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>
2025-09-03 18:23:47 +09:00
<div className="flex items-center gap-4">
<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>
2025-09-03 15:23:12 +09:00
</div>
</div>
2025-09-03 18:23:47 +09:00
{/* 웹 타입 상세 설정 */}
{isColumnDetailOpen[column.id] && (
<div className="border-t pt-2">
<div className="mb-2 flex items-center justify-between">
<Label className="text-xs font-medium"> </Label>
<Badge variant="outline" className="text-xs">
{getColumnWebType(column)}
</Badge>
</div>
<div className="pl-2">{renderColumnDetailSettings(column)}</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>
)}
2025-09-03 15:23:12 +09:00
</div>
</Card>
2025-09-03 18:23:47 +09:00
))}
</div>
</CardContent>
</Card>
2025-09-03 15:23:12 +09:00
</TabsContent>
2025-09-03 18:23:47 +09:00
<TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto">
<Card>
<CardHeader>
<CardTitle className="flex items-center space-x-2 text-sm">
<Filter className="h-4 w-4" />
<span> </span>
</CardTitle>
</CardHeader>
<CardContent 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>
2025-09-03 15:23:12 +09:00
2025-09-03 18:23:47 +09:00
{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 "🔍";
2025-09-03 15:23:12 +09:00
}
2025-09-03 18:23:47 +09:00
};
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={(() => {
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"
/>
</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>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="modal" className="mt-4 max-h-[70vh] overflow-y-auto">
<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-6">
{/* 페이지네이션 설정 */}
<div className="space-y-4">
<h4 className="text-sm font-medium"> </h4>
<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>
2025-09-03 15:23:12 +09:00
</div>
2025-09-03 18:23:47 +09:00
{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>
2025-09-03 15:23:12 +09:00
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 15:23:12 +09:00
2025-09-03 18:23:47 +09:00
<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>
2025-09-03 15:23:12 +09:00
</div>
2025-09-03 18:23:47 +09:00
)}
</div>
{/* 모달 설정은 여기에 추가 가능 */}
<div className="space-y-4">
<h4 className="text-sm font-medium"> </h4>
<p className="text-xs text-gray-500">/ .</p>
2025-09-03 15:23:12 +09:00
</div>
2025-09-03 18:23:47 +09:00
</CardContent>
</Card>
2025-09-03 15:23:12 +09:00
</TabsContent>
</Tabs>
</div>
);
};
export default DataTableConfigPanel;