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";
|
2025-09-09 14:29:04 +09:00
|
|
|
|
import { useWebTypes } from "@/hooks/admin/useWebTypes";
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
|
const DataTableConfigPanelComponent: React.FC<DataTableConfigPanelProps> = ({
|
2025-09-03 18:23:47 +09:00
|
|
|
|
component,
|
|
|
|
|
|
tables,
|
|
|
|
|
|
activeTab: externalActiveTab,
|
|
|
|
|
|
onTabChange,
|
|
|
|
|
|
onUpdateComponent,
|
|
|
|
|
|
}) => {
|
2025-09-09 14:29:04 +09:00
|
|
|
|
// 동적 웹타입 옵션 가져오기
|
|
|
|
|
|
const { webTypes } = useWebTypes({ active: "Y" });
|
|
|
|
|
|
const webTypeOptions = webTypes.map((wt) => ({
|
|
|
|
|
|
value: wt.web_type as WebType,
|
|
|
|
|
|
label: wt.type_name,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
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 || "삭제",
|
2025-10-01 17:41:30 +09:00
|
|
|
|
// 추가 모달 설정
|
2025-09-03 16:38:10 +09:00
|
|
|
|
modalTitle: component.addModalConfig?.title || "새 데이터 추가",
|
2025-09-09 14:29:04 +09:00
|
|
|
|
// 테이블명도 로컬 상태로 관리
|
|
|
|
|
|
tableName: component.tableName || "",
|
2025-09-03 16:38:10 +09:00
|
|
|
|
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-10-01 17:41:30 +09:00
|
|
|
|
// 수정 모달 설정
|
|
|
|
|
|
editModalTitle: component.editModalConfig?.title || "",
|
|
|
|
|
|
editModalDescription: component.editModalConfig?.description || "",
|
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(() => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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(),
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
// 컬럼과 필터 상세 정보 로그
|
|
|
|
|
|
if (component.columns.length > 0) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(
|
|
|
|
|
|
// "📋 현재 컬럼 목록:",
|
|
|
|
|
|
// component.columns.map((col) => ({
|
|
|
|
|
|
// id: col.id,
|
|
|
|
|
|
// columnName: col.columnName,
|
|
|
|
|
|
// label: col.label,
|
|
|
|
|
|
// visible: col.visible,
|
|
|
|
|
|
// gridColumns: col.gridColumns,
|
|
|
|
|
|
// })),
|
|
|
|
|
|
// );
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 상태 정보 로그
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔧 로컬 상태 정보:", {
|
|
|
|
|
|
// localColumnInputsCount: Object.keys(localColumnInputs).length,
|
|
|
|
|
|
// localColumnCheckboxesCount: Object.keys(localColumnCheckboxes).length,
|
|
|
|
|
|
// localColumnGridColumnsCount: Object.keys(localColumnGridColumns).length,
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
if (component.filters.length > 0) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log(
|
|
|
|
|
|
// "🔍 현재 필터 목록:",
|
|
|
|
|
|
// component.filters.map((filter) => ({
|
|
|
|
|
|
// columnName: filter.columnName,
|
|
|
|
|
|
// widgetType: filter.widgetType,
|
|
|
|
|
|
// label: filter.label,
|
|
|
|
|
|
// })),
|
|
|
|
|
|
// );
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 || "삭제",
|
2025-10-01 17:41:30 +09:00
|
|
|
|
// 추가 모달 설정
|
2025-09-03 16:38:10 +09:00
|
|
|
|
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-10-01 17:41:30 +09:00
|
|
|
|
// 수정 모달 설정
|
|
|
|
|
|
editModalTitle: component.editModalConfig?.title || "",
|
|
|
|
|
|
editModalDescription: component.editModalConfig?.description || "",
|
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,
|
2025-09-09 14:29:04 +09:00
|
|
|
|
// 테이블명 동기화
|
|
|
|
|
|
tableName: component.tableName || "",
|
2025-09-03 15:23:12 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 라벨 로컬 상태 초기화 (기존 값이 없는 경우만)
|
|
|
|
|
|
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-10-01 18:17:30 +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-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🗑️ 삭제된 필터 로컬 상태 제거:", { filterKey: key });
|
2025-09-03 16:38:10 +09:00
|
|
|
|
delete newFilterInputs[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("📝 필터 로컬 상태 동기화 완료:", {
|
|
|
|
|
|
// prevCount: Object.keys(prev).length,
|
|
|
|
|
|
// newCount: Object.keys(newFilterInputs).length,
|
|
|
|
|
|
// newKeys: Object.keys(newFilterInputs),
|
|
|
|
|
|
// });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
|
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.showSearchButton,
|
|
|
|
|
|
component.enableExport,
|
|
|
|
|
|
component.enableRefresh,
|
|
|
|
|
|
component.pagination,
|
2025-09-09 14:29:04 +09:00
|
|
|
|
component.columns.length, // 컬럼 개수만 감지
|
|
|
|
|
|
component.filters.length, // 필터 개수만 감지
|
2025-09-03 15:23:12 +09:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 테이블 정보 로드
|
|
|
|
|
|
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) => {
|
2025-09-09 14:29:04 +09:00
|
|
|
|
// 이미 같은 테이블이 선택되어 있으면 무시
|
|
|
|
|
|
if (localValues.tableName === tableName) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 상태 먼저 업데이트
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, tableName }));
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
const table = tables.find((t) => t.tableName === tableName);
|
|
|
|
|
|
if (!table) return;
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 테이블 변경:", {
|
|
|
|
|
|
// tableName,
|
|
|
|
|
|
// currentTableName: localValues.tableName,
|
|
|
|
|
|
// table,
|
|
|
|
|
|
// columnsCount: table.columns.length,
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-09-23 14:26:18 +09:00
|
|
|
|
// 테이블 변경 시 컬럼을 자동으로 추가하지 않음 (사용자가 수동으로 추가해야 함)
|
|
|
|
|
|
const defaultColumns: DataTableColumn[] = [];
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("✅ 생성된 컬럼 설정:", {
|
|
|
|
|
|
// defaultColumnsCount: defaultColumns.length,
|
|
|
|
|
|
// visibleColumns: defaultColumns.filter((col) => col.visible).length,
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
|
// 상태 업데이트를 한 번에 처리
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
columns: defaultColumns,
|
|
|
|
|
|
filters: [], // 빈 필터 배열
|
|
|
|
|
|
});
|
|
|
|
|
|
setSelectedTable(table);
|
|
|
|
|
|
}, 0);
|
2025-09-03 15:23:12 +09:00
|
|
|
|
},
|
2025-09-09 14:29:04 +09:00
|
|
|
|
[tables, onUpdateComponent, localValues.tableName],
|
2025-09-03 15:23:12 +09:00
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-19 18:43:55 +09:00
|
|
|
|
// 컬럼 타입 추론 (통합 매핑 시스템 사용)
|
2025-09-03 15:23:12 +09:00
|
|
|
|
const getWidgetTypeFromColumn = (column: ColumnInfo): WebType => {
|
2025-09-19 18:43:55 +09:00
|
|
|
|
// 통합 자동 매핑 유틸리티 사용
|
|
|
|
|
|
const { inferWebTypeFromColumn } = require("@/lib/utils/dbTypeMapping");
|
2025-09-05 12:04:13 +09:00
|
|
|
|
|
2025-09-19 18:43:55 +09:00
|
|
|
|
return inferWebTypeFromColumn(column.dataType || "text", column.columnName);
|
2025-09-03 15:23:12 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 업데이트
|
|
|
|
|
|
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));
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컬럼 상세 설정 업데이트:", { columnId, webTypeConfig, updatedColumns });
|
2025-09-03 17:12:27 +09:00
|
|
|
|
onUpdateComponent({ columns: updatedColumns });
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 테이블 타입 관리에도 반영 (라디오 타입인 경우)
|
|
|
|
|
|
const targetColumn = component.columns.find((col) => col.id === columnId);
|
|
|
|
|
|
if (targetColumn && targetColumn.widgetType === "radio" && selectedTable) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// TODO: 테이블 타입 관리 API 호출하여 웹 타입과 상세 설정 업데이트
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("📡 테이블 타입 관리 업데이트 필요:", {
|
|
|
|
|
|
// tableName: component.tableName,
|
|
|
|
|
|
// columnName: targetColumn.columnName,
|
|
|
|
|
|
// webType: "radio",
|
|
|
|
|
|
// detailSettings: JSON.stringify(webTypeConfig),
|
|
|
|
|
|
// });
|
2025-09-03 17:12:27 +09:00
|
|
|
|
} catch (error) {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.error("테이블 타입 관리 업데이트 실패:", error);
|
2025-09-03 17:12:27 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[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>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<select
|
2025-09-03 17:12:27 +09:00
|
|
|
|
value={localSettings.defaultValue || "__NONE__"}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const value = e.target.value;
|
|
|
|
|
|
updateSettings({ defaultValue: value === "__NONE__" ? "" : value });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-7 w-full items-center justify-between rounded-md border px-3 py-1 text-xs focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
2025-09-03 17:12:27 +09:00
|
|
|
|
>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<option value="__NONE__">선택 안함</option>
|
|
|
|
|
|
{(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 (
|
|
|
|
|
|
<option key={index} value={optionValue}>
|
|
|
|
|
|
{optionLabel}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</select>
|
2025-09-03 17:12:27 +09:00
|
|
|
|
</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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🗑️ 컬럼 삭제:", {
|
|
|
|
|
|
// columnId,
|
|
|
|
|
|
// columnName: columnToRemove?.columnName,
|
|
|
|
|
|
// remainingColumns: updatedColumns.length,
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
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-10-01 18:17:30 +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,
|
2025-09-23 14:26:18 +09:00
|
|
|
|
// 웹타입별 추가 정보 설정
|
|
|
|
|
|
codeCategory: targetColumn.codeCategory,
|
|
|
|
|
|
referenceTable: targetColumn.referenceTable,
|
|
|
|
|
|
referenceColumn: targetColumn.referenceColumn,
|
|
|
|
|
|
displayColumn: targetColumn.displayColumn,
|
2025-09-03 15:23:12 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("➕ 필터 추가 시작:", {
|
|
|
|
|
|
// targetColumnName: targetColumn.columnName,
|
|
|
|
|
|
// targetColumnLabel: targetColumn.columnLabel,
|
|
|
|
|
|
// inferredWidgetType: widgetType,
|
|
|
|
|
|
// currentFiltersCount: component.filters.length,
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("➕ 생성된 새 필터:", {
|
|
|
|
|
|
// columnName: newFilter.columnName,
|
|
|
|
|
|
// widgetType: newFilter.widgetType,
|
|
|
|
|
|
// label: newFilter.label,
|
|
|
|
|
|
// gridColumns: newFilter.gridColumns,
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
const updatedFilters = [...component.filters, newFilter];
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 필터 업데이트 호출:", {
|
|
|
|
|
|
// filtersToAdd: 1,
|
|
|
|
|
|
// totalFiltersAfter: updatedFilters.length,
|
|
|
|
|
|
// updatedFilters: updatedFilters.map((filter) => ({
|
|
|
|
|
|
// columnName: filter.columnName,
|
|
|
|
|
|
// widgetType: filter.widgetType,
|
|
|
|
|
|
// label: filter.label,
|
|
|
|
|
|
// })),
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
// 먼저 로컬 상태를 업데이트하고
|
|
|
|
|
|
const filterKey = `${newFilter.columnName}-${component.filters.length}`;
|
|
|
|
|
|
setLocalFilterInputs((prev) => {
|
|
|
|
|
|
const newState = {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[filterKey]: newFilter.label,
|
|
|
|
|
|
};
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("📝 필터 로컬 상태 업데이트:", {
|
|
|
|
|
|
// filterKey,
|
|
|
|
|
|
// newLabel: newFilter.label,
|
|
|
|
|
|
// prevState: prev,
|
|
|
|
|
|
// newState,
|
|
|
|
|
|
// });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
return newState;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 그 다음 컴포넌트 상태를 업데이트
|
2025-09-03 15:23:12 +09:00
|
|
|
|
onUpdateComponent({ filters: updatedFilters });
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
// 필터 추가 후 필터 탭으로 자동 이동
|
|
|
|
|
|
setActiveTab("filters");
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔍 필터 추가 후 탭 이동:", {
|
|
|
|
|
|
// activeTab: "filters",
|
|
|
|
|
|
// isExternalControl: !!onTabChange,
|
|
|
|
|
|
// });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
|
|
|
|
|
|
// 강제로 리렌더링을 트리거하기 위해 여러 방법 사용
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setLocalFilterInputs((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[filterKey]: newFilter.label,
|
|
|
|
|
|
}));
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 setTimeout에서 강제 로컬 상태 업데이트:", { filterKey, label: newFilter.label });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 추가적인 강제 업데이트
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setLocalFilterInputs((prev) => {
|
|
|
|
|
|
const updated = { ...prev, [filterKey]: newFilter.label };
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 두 번째 강제 업데이트:", { updated });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
return updated;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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 => {
|
2025-09-09 14:29:04 +09:00
|
|
|
|
// 대부분의 웹타입은 필터링 가능 (파일, 버튼 등만 제외)
|
|
|
|
|
|
const nonFilterableTypes = ["file", "button", "image"];
|
|
|
|
|
|
return !nonFilterableTypes.includes(webType);
|
2025-09-03 15:23:12 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 추가 (테이블에서 선택)
|
|
|
|
|
|
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),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 필터는 자동으로 추가하지 않음 (사용자가 수동으로 추가)
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// 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,
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
// 필터는 수동으로만 추가
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 상태에 새 컬럼 입력값 추가
|
|
|
|
|
|
setLocalColumnInputs((prev) => {
|
|
|
|
|
|
const newInputs = {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[newColumn.id]: newColumn.label,
|
|
|
|
|
|
};
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 로컬 컬럼 상태 업데이트:", {
|
|
|
|
|
|
// newColumnId: newColumn.id,
|
|
|
|
|
|
// newLabel: newColumn.label,
|
|
|
|
|
|
// totalLocalInputs: Object.keys(newInputs).length,
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
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],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컴포넌트 업데이트 호출:", {
|
|
|
|
|
|
// columnsToAdd: 1,
|
|
|
|
|
|
// totalColumnsAfter: updates.columns?.length,
|
|
|
|
|
|
// hasColumns: !!updates.columns,
|
|
|
|
|
|
// updateKeys: Object.keys(updates),
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 업데이트 상세 내용:", {
|
|
|
|
|
|
// columns: updates.columns?.map((col) => ({ id: col.id, columnName: col.columnName, label: col.label })),
|
|
|
|
|
|
// });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
|
|
onUpdateComponent(updates);
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
// 컬럼 추가 후 컬럼 탭으로 자동 이동
|
|
|
|
|
|
setActiveTab("columns");
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("📋 컬럼 추가 후 탭 이동:", {
|
|
|
|
|
|
// activeTab: "columns",
|
|
|
|
|
|
// isExternalControl: !!onTabChange,
|
|
|
|
|
|
// });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("✅ 컬럼 추가 완료 - onUpdateComponent 호출됨");
|
2025-09-03 15:23:12 +09:00
|
|
|
|
},
|
|
|
|
|
|
[selectedTable, component.columns, component.filters, onUpdateComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-06 00:16:27 +09:00
|
|
|
|
// 가상 파일 컬럼 추가
|
|
|
|
|
|
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: ["*/*"],
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("📁 가상 파일 컬럼 추가:", {
|
|
|
|
|
|
// columnName: newColumn.columnName,
|
|
|
|
|
|
// label: newColumn.label,
|
|
|
|
|
|
// isVirtualFileColumn: newColumn.isVirtualFileColumn,
|
|
|
|
|
|
// });
|
2025-09-06 00:16:27 +09:00
|
|
|
|
|
|
|
|
|
|
// 로컬 상태에 새 컬럼 입력값 추가
|
|
|
|
|
|
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");
|
|
|
|
|
|
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("✅ 가상 파일 컬럼 추가 완료");
|
2025-09-06 00:16:27 +09:00
|
|
|
|
}, [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>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<select
|
2025-10-02 14:34:15 +09:00
|
|
|
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
2025-09-09 14:29:04 +09:00
|
|
|
|
value={localValues.tableName}
|
|
|
|
|
|
onChange={(e) => handleTableChange(e.target.value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">테이블을 선택하세요</option>
|
|
|
|
|
|
{tables.map((table) => (
|
|
|
|
|
|
<option key={table.tableName} value={table.tableName}>
|
|
|
|
|
|
{table.tableLabel || table.tableName}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</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>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<select
|
2025-09-03 18:23:47 +09:00
|
|
|
|
value={localValues.modalWidth}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const value = e.target.value;
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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-09 14:29:04 +09:00
|
|
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-8 w-full items-center justify-between rounded-md border px-3 py-1 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
2025-09-03 17:12:27 +09:00
|
|
|
|
>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<option value="sm">작음 (384px)</option>
|
|
|
|
|
|
<option value="md">보통 (448px)</option>
|
|
|
|
|
|
<option value="lg">큼 (512px)</option>
|
|
|
|
|
|
<option value="xl">매우 큼 (576px)</option>
|
|
|
|
|
|
<option value="2xl">특대 (672px)</option>
|
|
|
|
|
|
<option value="full">전체 너비</option>
|
|
|
|
|
|
</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-09 14:29:04 +09:00
|
|
|
|
<select
|
2025-09-03 18:23:47 +09:00
|
|
|
|
value={localValues.modalLayout}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const value = e.target.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-09 14:29:04 +09:00
|
|
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-8 w-full items-center justify-between rounded-md border px-3 py-1 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
2025-09-03 15:23:12 +09:00
|
|
|
|
>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<option value="single">단일 컬럼</option>
|
|
|
|
|
|
<option value="two-column">2컬럼</option>
|
|
|
|
|
|
<option value="grid">그리드</option>
|
|
|
|
|
|
</select>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</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>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<select
|
2025-09-03 18:23:47 +09:00
|
|
|
|
value={localValues.modalGridColumns.toString()}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const gridColumns = parseInt(e.target.value);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
setLocalValues((prev) => ({ ...prev, modalGridColumns: gridColumns }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: { ...component.addModalConfig, gridColumns },
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-8 w-full items-center justify-between rounded-md border px-3 py-1 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
2025-09-03 18:23:47 +09:00
|
|
|
|
>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<option value="2">2컬럼</option>
|
|
|
|
|
|
<option value="3">3컬럼</option>
|
|
|
|
|
|
<option value="4">4컬럼</option>
|
|
|
|
|
|
</select>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</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-10-01 17:41:30 +09:00
|
|
|
|
{/* 수정 모달 설정 */}
|
|
|
|
|
|
{localValues.enableEdit && (
|
|
|
|
|
|
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
|
|
|
|
|
<h4 className="mb-3 text-sm font-medium text-gray-900">수정 모달 설정</h4>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="edit-modal-title" className="text-sm">
|
|
|
|
|
|
모달 제목
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="edit-modal-title"
|
|
|
|
|
|
value={localValues.editModalTitle}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, editModalTitle: newValue }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
editModalConfig: { ...component.editModalConfig, title: newValue },
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="데이터 수정"
|
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-500">비워두면 기본 제목이 표시됩니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="edit-modal-description" className="text-sm">
|
|
|
|
|
|
모달 설명
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="edit-modal-description"
|
|
|
|
|
|
value={localValues.editModalDescription}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, editModalDescription: newValue }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
editModalConfig: { ...component.editModalConfig, description: newValue },
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="선택한 데이터를 수정합니다"
|
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-500">비워두면 설명이 표시되지 않습니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="grid-columns">그리드 컬럼 수</Label>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<select
|
2025-10-02 14:34:15 +09:00
|
|
|
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
2025-09-03 18:23:47 +09:00
|
|
|
|
value={localValues.gridColumns.toString()}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const gridColumns = parseInt(e.target.value, 10);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 테이블 그리드 컬럼 수 변경:", gridColumns);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
setLocalValues((prev) => ({ ...prev, gridColumns }));
|
|
|
|
|
|
onUpdateComponent({ gridColumns });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<option value="">그리드 컬럼 수 선택</option>
|
|
|
|
|
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((num) => (
|
|
|
|
|
|
<option key={num} value={num.toString()}>
|
|
|
|
|
|
{num}컬럼 ({Math.round((num / 12) * 100)}%)
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
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="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="show-search-button"
|
|
|
|
|
|
checked={localValues.showSearchButton}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 검색 버튼 표시 변경:", checked);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 내보내기 기능 변경:", checked);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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">
|
2025-09-08 13:10:09 +09:00
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<h3 className="text-sm font-medium">테이블 컬럼 설정</h3>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<Badge variant="secondary">{component.columns.length}개</Badge>
|
2025-09-08 13:10:09 +09:00
|
|
|
|
</div>
|
2025-09-06 00:16:27 +09:00
|
|
|
|
|
2025-09-08 13:10:09 +09:00
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
2025-09-06 00:16:27 +09:00
|
|
|
|
{/* 파일 컬럼 추가 버튼 */}
|
|
|
|
|
|
<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 ? (
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<select
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
if (e.target.value) {
|
|
|
|
|
|
addColumn(e.target.value);
|
|
|
|
|
|
e.target.value = ""; // 선택 후 초기화
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-8 w-32 items-center justify-between rounded-md border px-3 py-1 text-xs focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">DB 컬럼</option>
|
|
|
|
|
|
{availableColumns.map((col) => (
|
|
|
|
|
|
<option key={col.columnName} value={col.columnName}>
|
|
|
|
|
|
{col.columnLabel || col.columnName}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
) : (
|
|
|
|
|
|
<Button size="sm" disabled>
|
|
|
|
|
|
<Plus className="h-4 w-4" />
|
2025-09-06 00:16:27 +09:00
|
|
|
|
<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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컬럼 표시 변경:", { columnId: column.id, checked });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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);
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컬럼 그리드 컬럼 변경:", { columnId: column.id, newGridColumns });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컬럼 정렬 가능 변경:", { columnId: column.id, checked });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 컬럼 검색 가능 변경:", { columnId: column.id, checked });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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;
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🎯 필터 입력 값 결정:", {
|
|
|
|
|
|
// filterKey,
|
|
|
|
|
|
// localValue,
|
|
|
|
|
|
// filterLabel: filter.label,
|
|
|
|
|
|
// finalValue,
|
|
|
|
|
|
// allLocalInputs: Object.keys(localFilterInputs),
|
|
|
|
|
|
// });
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 페이지네이션 사용 변경:", checked);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
<select
|
2025-10-02 14:34:15 +09:00
|
|
|
|
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-primary focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
2025-09-03 18:23:47 +09:00
|
|
|
|
value={component.pagination.pageSize.toString()}
|
2025-09-09 14:29:04 +09:00
|
|
|
|
onChange={(e) =>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
pagination: {
|
|
|
|
|
|
...component.pagination,
|
2025-09-09 14:29:04 +09:00
|
|
|
|
pageSize: parseInt(e.target.value),
|
2025-09-03 18:23:47 +09:00
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
2025-09-09 14:29:04 +09:00
|
|
|
|
{[5, 10, 20, 50, 100].map((size) => (
|
|
|
|
|
|
<option key={size} value={size.toString()}>
|
|
|
|
|
|
{size}개
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 페이지 크기 선택기 표시 변경:", checked);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 페이지 정보 표시 변경:", checked);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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) => {
|
2025-10-01 18:17:30 +09:00
|
|
|
|
// console.log("🔄 처음/마지막 버튼 표시 변경:", checked);
|
2025-09-03 18:23:47 +09:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-09 14:29:04 +09:00
|
|
|
|
// 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;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
export default DataTableConfigPanel;
|