2025-09-03 15:23:12 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
2025-09-03 16:38:10 +09:00
|
|
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
|
|
|
|
|
import { Table, Plus, Trash2, Settings, Filter, Columns, ChevronDown } from "lucide-react";
|
2025-09-03 15:23:12 +09:00
|
|
|
|
import { DataTableComponent, DataTableColumn, DataTableFilter, TableInfo, ColumnInfo, WebType } from "@/types/screen";
|
|
|
|
|
|
import { generateComponentId } from "@/lib/utils/generateId";
|
|
|
|
|
|
|
|
|
|
|
|
interface DataTableConfigPanelProps {
|
|
|
|
|
|
component: DataTableComponent;
|
|
|
|
|
|
tables: TableInfo[];
|
2025-09-03 18:23:47 +09:00
|
|
|
|
activeTab?: string;
|
|
|
|
|
|
onTabChange?: (tab: string) => void;
|
2025-09-03 15:23:12 +09:00
|
|
|
|
onUpdateComponent: (updates: Partial<DataTableComponent>) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const webTypeOptions: { value: WebType; label: string }[] = [
|
|
|
|
|
|
{ value: "text", label: "텍스트" },
|
|
|
|
|
|
{ value: "number", label: "숫자" },
|
|
|
|
|
|
{ value: "decimal", label: "소수" },
|
|
|
|
|
|
{ value: "date", label: "날짜" },
|
|
|
|
|
|
{ value: "datetime", label: "날짜시간" },
|
|
|
|
|
|
{ value: "select", label: "선택박스" },
|
|
|
|
|
|
{ value: "checkbox", label: "체크박스" },
|
|
|
|
|
|
{ value: "email", label: "이메일" },
|
|
|
|
|
|
{ value: "tel", label: "전화번호" },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
export const DataTableConfigPanel: React.FC<DataTableConfigPanelProps> = ({
|
|
|
|
|
|
component,
|
|
|
|
|
|
tables,
|
|
|
|
|
|
activeTab: externalActiveTab,
|
|
|
|
|
|
onTabChange,
|
|
|
|
|
|
onUpdateComponent,
|
|
|
|
|
|
}) => {
|
2025-09-03 15:23:12 +09:00
|
|
|
|
const [selectedTable, setSelectedTable] = useState<TableInfo | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 입력 상태 (실시간 타이핑용)
|
|
|
|
|
|
const [localValues, setLocalValues] = useState({
|
|
|
|
|
|
title: component.title || "",
|
|
|
|
|
|
searchButtonText: component.searchButtonText || "검색",
|
|
|
|
|
|
showSearchButton: component.showSearchButton ?? true,
|
|
|
|
|
|
enableExport: component.enableExport ?? true,
|
|
|
|
|
|
enableRefresh: component.enableRefresh ?? true,
|
2025-09-03 16:38:10 +09:00
|
|
|
|
enableAdd: component.enableAdd ?? true,
|
|
|
|
|
|
enableEdit: component.enableEdit ?? true,
|
|
|
|
|
|
enableDelete: component.enableDelete ?? true,
|
|
|
|
|
|
addButtonText: component.addButtonText || "추가",
|
|
|
|
|
|
editButtonText: component.editButtonText || "수정",
|
|
|
|
|
|
deleteButtonText: component.deleteButtonText || "삭제",
|
|
|
|
|
|
// 모달 설정
|
|
|
|
|
|
modalTitle: component.addModalConfig?.title || "새 데이터 추가",
|
|
|
|
|
|
modalDescription: component.addModalConfig?.description || "",
|
|
|
|
|
|
modalWidth: component.addModalConfig?.width || "lg",
|
|
|
|
|
|
modalLayout: component.addModalConfig?.layout || "two-column",
|
|
|
|
|
|
modalGridColumns: component.addModalConfig?.gridColumns || 2,
|
|
|
|
|
|
modalSubmitButtonText: component.addModalConfig?.submitButtonText || "추가",
|
|
|
|
|
|
modalCancelButtonText: component.addModalConfig?.cancelButtonText || "취소",
|
2025-09-03 15:23:12 +09:00
|
|
|
|
paginationEnabled: component.pagination?.enabled ?? true,
|
|
|
|
|
|
showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true,
|
|
|
|
|
|
showPageInfo: component.pagination?.showPageInfo ?? true,
|
|
|
|
|
|
showFirstLast: component.pagination?.showFirstLast ?? true,
|
|
|
|
|
|
gridColumns: component.gridColumns || 6,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼별 로컬 입력 상태
|
|
|
|
|
|
const [localColumnInputs, setLocalColumnInputs] = useState<Record<string, string>>({});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼별 체크박스 및 설정 상태
|
|
|
|
|
|
const [localColumnCheckboxes, setLocalColumnCheckboxes] = useState<
|
|
|
|
|
|
Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
{
|
|
|
|
|
|
visible: boolean;
|
|
|
|
|
|
sortable: boolean;
|
|
|
|
|
|
searchable: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
>({});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼별 그리드 컬럼 설정 상태
|
|
|
|
|
|
const [localColumnGridColumns, setLocalColumnGridColumns] = useState<Record<string, number>>({});
|
|
|
|
|
|
|
2025-09-03 16:38:10 +09:00
|
|
|
|
// 필터별 로컬 입력 상태
|
|
|
|
|
|
const [localFilterInputs, setLocalFilterInputs] = useState<Record<string, string>>({});
|
|
|
|
|
|
|
2025-09-03 17:12:27 +09:00
|
|
|
|
// 컬럼별 상세 설정 상태
|
|
|
|
|
|
const [localColumnDetailSettings, setLocalColumnDetailSettings] = useState<Record<string, any>>({});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼별 상세 설정 확장/축소 상태
|
|
|
|
|
|
const [isColumnDetailOpen, setIsColumnDetailOpen] = useState<Record<string, boolean>>({});
|
|
|
|
|
|
|
2025-09-03 16:38:10 +09:00
|
|
|
|
// 모달 설정 확장/축소 상태
|
|
|
|
|
|
const [isModalConfigOpen, setIsModalConfigOpen] = useState<Record<string, boolean>>({});
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
// 탭 상태 관리 (외부에서 받거나 로컬 상태 사용)
|
|
|
|
|
|
const [internalActiveTab, setInternalActiveTab] = useState("basic");
|
|
|
|
|
|
const activeTab = externalActiveTab || internalActiveTab;
|
|
|
|
|
|
const setActiveTab = onTabChange || setInternalActiveTab;
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
// 컴포넌트 변경 시 로컬 값 동기화
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
console.log("🔄 DataTableConfig: 컴포넌트 변경 감지", {
|
|
|
|
|
|
componentId: component.id,
|
|
|
|
|
|
title: component.title,
|
|
|
|
|
|
searchButtonText: component.searchButtonText,
|
|
|
|
|
|
columnsCount: component.columns.length,
|
|
|
|
|
|
filtersCount: component.filters.length,
|
|
|
|
|
|
columnIds: component.columns.map((col) => col.id),
|
|
|
|
|
|
filterColumnNames: component.filters.map((filter) => filter.columnName),
|
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼과 필터 상세 정보 로그
|
|
|
|
|
|
if (component.columns.length > 0) {
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
"📋 현재 컬럼 목록:",
|
|
|
|
|
|
component.columns.map((col) => ({
|
|
|
|
|
|
id: col.id,
|
|
|
|
|
|
columnName: col.columnName,
|
|
|
|
|
|
label: col.label,
|
|
|
|
|
|
visible: col.visible,
|
|
|
|
|
|
gridColumns: col.gridColumns,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 상태 정보 로그
|
|
|
|
|
|
console.log("🔧 로컬 상태 정보:", {
|
|
|
|
|
|
localColumnInputsCount: Object.keys(localColumnInputs).length,
|
|
|
|
|
|
localColumnCheckboxesCount: Object.keys(localColumnCheckboxes).length,
|
|
|
|
|
|
localColumnGridColumnsCount: Object.keys(localColumnGridColumns).length,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (component.filters.length > 0) {
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
"🔍 현재 필터 목록:",
|
|
|
|
|
|
component.filters.map((filter) => ({
|
|
|
|
|
|
columnName: filter.columnName,
|
|
|
|
|
|
widgetType: filter.widgetType,
|
|
|
|
|
|
label: filter.label,
|
|
|
|
|
|
})),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setLocalValues({
|
|
|
|
|
|
title: component.title || "",
|
|
|
|
|
|
searchButtonText: component.searchButtonText || "검색",
|
|
|
|
|
|
showSearchButton: component.showSearchButton ?? true,
|
|
|
|
|
|
enableExport: component.enableExport ?? true,
|
|
|
|
|
|
enableRefresh: component.enableRefresh ?? true,
|
2025-09-03 16:38:10 +09:00
|
|
|
|
enableAdd: component.enableAdd ?? true,
|
|
|
|
|
|
enableEdit: component.enableEdit ?? true,
|
|
|
|
|
|
enableDelete: component.enableDelete ?? true,
|
|
|
|
|
|
addButtonText: component.addButtonText || "추가",
|
|
|
|
|
|
editButtonText: component.editButtonText || "수정",
|
|
|
|
|
|
deleteButtonText: component.deleteButtonText || "삭제",
|
|
|
|
|
|
// 모달 설정
|
|
|
|
|
|
modalTitle: component.addModalConfig?.title || "새 데이터 추가",
|
|
|
|
|
|
modalDescription: component.addModalConfig?.description || "",
|
|
|
|
|
|
modalWidth: component.addModalConfig?.width || "lg",
|
|
|
|
|
|
modalLayout: component.addModalConfig?.layout || "two-column",
|
|
|
|
|
|
modalGridColumns: component.addModalConfig?.gridColumns || 2,
|
|
|
|
|
|
modalSubmitButtonText: component.addModalConfig?.submitButtonText || "추가",
|
|
|
|
|
|
modalCancelButtonText: component.addModalConfig?.cancelButtonText || "취소",
|
2025-09-03 15:23:12 +09:00
|
|
|
|
paginationEnabled: component.pagination?.enabled ?? true,
|
|
|
|
|
|
showPageSizeSelector: component.pagination?.showPageSizeSelector ?? true,
|
|
|
|
|
|
showPageInfo: component.pagination?.showPageInfo ?? true,
|
|
|
|
|
|
showFirstLast: component.pagination?.showFirstLast ?? true,
|
|
|
|
|
|
gridColumns: component.gridColumns || 6,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 라벨 로컬 상태 초기화 (기존 값이 없는 경우만)
|
|
|
|
|
|
setLocalColumnInputs((prev) => {
|
|
|
|
|
|
const newInputs = { ...prev };
|
|
|
|
|
|
component.columns.forEach((col) => {
|
|
|
|
|
|
// 기존에 로컬 입력값이 없는 경우만 초기화
|
|
|
|
|
|
if (!(col.id in newInputs)) {
|
|
|
|
|
|
newInputs[col.id] = col.label;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 삭제된 컬럼의 로컬 상태 제거
|
|
|
|
|
|
const currentColumnIds = new Set(component.columns.map((col) => col.id));
|
|
|
|
|
|
Object.keys(newInputs).forEach((id) => {
|
|
|
|
|
|
if (!currentColumnIds.has(id)) {
|
|
|
|
|
|
delete newInputs[id];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return newInputs;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼별 체크박스 상태 초기화
|
|
|
|
|
|
setLocalColumnCheckboxes((prev) => {
|
|
|
|
|
|
const newCheckboxes = { ...prev };
|
|
|
|
|
|
component.columns.forEach((col) => {
|
|
|
|
|
|
// 기존에 로컬 체크박스 상태가 없는 경우만 초기화
|
|
|
|
|
|
if (!(col.id in newCheckboxes)) {
|
|
|
|
|
|
newCheckboxes[col.id] = {
|
|
|
|
|
|
visible: col.visible,
|
|
|
|
|
|
sortable: col.sortable,
|
|
|
|
|
|
searchable: col.searchable,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 삭제된 컬럼의 로컬 상태 제거
|
|
|
|
|
|
const currentColumnIds = new Set(component.columns.map((col) => col.id));
|
|
|
|
|
|
Object.keys(newCheckboxes).forEach((id) => {
|
|
|
|
|
|
if (!currentColumnIds.has(id)) {
|
|
|
|
|
|
delete newCheckboxes[id];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return newCheckboxes;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼별 그리드 컬럼 설정 상태 초기화
|
|
|
|
|
|
setLocalColumnGridColumns((prev) => {
|
|
|
|
|
|
const newGridColumns = { ...prev };
|
|
|
|
|
|
component.columns.forEach((col) => {
|
|
|
|
|
|
// 기존에 로컬 그리드 컬럼 설정이 없는 경우만 초기화
|
|
|
|
|
|
if (!(col.id in newGridColumns)) {
|
|
|
|
|
|
newGridColumns[col.id] = col.gridColumns;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 삭제된 컬럼의 로컬 상태 제거
|
|
|
|
|
|
const currentColumnIds = new Set(component.columns.map((col) => col.id));
|
|
|
|
|
|
Object.keys(newGridColumns).forEach((id) => {
|
|
|
|
|
|
if (!currentColumnIds.has(id)) {
|
|
|
|
|
|
delete newGridColumns[id];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return newGridColumns;
|
|
|
|
|
|
});
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
|
|
|
|
|
// 필터별 로컬 입력 상태 동기화 (기존 값 보존하면서 새 필터만 추가)
|
|
|
|
|
|
setLocalFilterInputs((prev) => {
|
|
|
|
|
|
const newFilterInputs = { ...prev };
|
|
|
|
|
|
component.filters?.forEach((filter, index) => {
|
|
|
|
|
|
const filterKey = `${filter.columnName}-${index}`;
|
|
|
|
|
|
if (!(filterKey in newFilterInputs)) {
|
|
|
|
|
|
newFilterInputs[filterKey] = filter.label || filter.columnName;
|
2025-09-03 18:23:47 +09:00
|
|
|
|
console.log("🆕 새 필터 로컬 상태 추가:", {
|
|
|
|
|
|
filterKey,
|
|
|
|
|
|
label: filter.label,
|
|
|
|
|
|
columnName: filter.columnName,
|
|
|
|
|
|
});
|
2025-09-03 16:38:10 +09:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 삭제된 필터의 로컬 상태 제거
|
|
|
|
|
|
const currentFilterKeys = new Set(
|
|
|
|
|
|
component.filters?.map((filter, index) => `${filter.columnName}-${index}`) || [],
|
|
|
|
|
|
);
|
|
|
|
|
|
Object.keys(newFilterInputs).forEach((key) => {
|
|
|
|
|
|
if (!currentFilterKeys.has(key)) {
|
2025-09-03 18:23:47 +09:00
|
|
|
|
console.log("🗑️ 삭제된 필터 로컬 상태 제거:", { filterKey: key });
|
2025-09-03 16:38:10 +09:00
|
|
|
|
delete newFilterInputs[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
console.log("📝 필터 로컬 상태 동기화 완료:", {
|
|
|
|
|
|
prevCount: Object.keys(prev).length,
|
|
|
|
|
|
newCount: Object.keys(newFilterInputs).length,
|
|
|
|
|
|
newKeys: Object.keys(newFilterInputs),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-03 16:38:10 +09:00
|
|
|
|
return newFilterInputs;
|
|
|
|
|
|
});
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}, [
|
|
|
|
|
|
component.id,
|
|
|
|
|
|
component.title,
|
|
|
|
|
|
component.searchButtonText,
|
|
|
|
|
|
component.columns,
|
|
|
|
|
|
component.filters,
|
|
|
|
|
|
component.showSearchButton,
|
|
|
|
|
|
component.enableExport,
|
|
|
|
|
|
component.enableRefresh,
|
|
|
|
|
|
component.pagination,
|
|
|
|
|
|
component.columns.length, // 컬럼 개수 변경 감지
|
|
|
|
|
|
component.filters.length, // 필터 개수 변경 감지
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 테이블 정보 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (component.tableName && tables.length > 0) {
|
|
|
|
|
|
const table = tables.find((t) => t.tableName === component.tableName);
|
|
|
|
|
|
setSelectedTable(table || null);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [component.tableName, tables]);
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블 변경 시 컬럼 자동 설정
|
|
|
|
|
|
const handleTableChange = useCallback(
|
|
|
|
|
|
(tableName: string) => {
|
|
|
|
|
|
const table = tables.find((t) => t.tableName === tableName);
|
|
|
|
|
|
if (!table) return;
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔄 테이블 변경:", {
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
table,
|
|
|
|
|
|
columnsCount: table.columns.length,
|
|
|
|
|
|
columns: table.columns.map((col) => ({
|
|
|
|
|
|
name: col.columnName,
|
|
|
|
|
|
label: col.columnLabel,
|
|
|
|
|
|
type: col.dataType,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블의 모든 컬럼을 기본 설정으로 추가
|
|
|
|
|
|
const defaultColumns: DataTableColumn[] = table.columns.map((col, index) => ({
|
|
|
|
|
|
id: generateComponentId(),
|
|
|
|
|
|
columnName: col.columnName,
|
|
|
|
|
|
label: col.columnLabel || col.columnName,
|
|
|
|
|
|
widgetType: getWidgetTypeFromColumn(col),
|
|
|
|
|
|
gridColumns: 2, // 기본 2칸
|
|
|
|
|
|
visible: index < 6, // 처음 6개만 기본으로 표시
|
|
|
|
|
|
filterable: isFilterableWebType(getWidgetTypeFromColumn(col)),
|
|
|
|
|
|
sortable: true,
|
|
|
|
|
|
searchable: ["text", "email", "tel"].includes(getWidgetTypeFromColumn(col)),
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 필터는 사용자가 수동으로 추가
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 생성된 컬럼 설정:", {
|
|
|
|
|
|
defaultColumnsCount: defaultColumns.length,
|
|
|
|
|
|
visibleColumns: defaultColumns.filter((col) => col.visible).length,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
tableName,
|
|
|
|
|
|
columns: defaultColumns,
|
|
|
|
|
|
filters: [], // 빈 필터 배열
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setSelectedTable(table);
|
|
|
|
|
|
},
|
|
|
|
|
|
[tables, onUpdateComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 타입 추론
|
|
|
|
|
|
const getWidgetTypeFromColumn = (column: ColumnInfo): WebType => {
|
|
|
|
|
|
const type = column.dataType?.toLowerCase() || "";
|
|
|
|
|
|
const name = column.columnName.toLowerCase();
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 웹타입 추론:", {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
dataType: column.dataType,
|
|
|
|
|
|
type,
|
|
|
|
|
|
name,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 숫자 타입
|
|
|
|
|
|
if (type.includes("int") || type.includes("integer") || type.includes("bigint") || type.includes("smallint")) {
|
|
|
|
|
|
return "number";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
type.includes("decimal") ||
|
|
|
|
|
|
type.includes("numeric") ||
|
|
|
|
|
|
type.includes("float") ||
|
|
|
|
|
|
type.includes("double") ||
|
|
|
|
|
|
type.includes("real")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return "decimal";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 날짜/시간 타입
|
|
|
|
|
|
if (type.includes("timestamp") || type.includes("datetime")) {
|
|
|
|
|
|
return "datetime";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (type.includes("date")) {
|
|
|
|
|
|
return "date";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (type.includes("time")) {
|
|
|
|
|
|
return "datetime";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 불린 타입
|
|
|
|
|
|
if (type.includes("bool") || type.includes("boolean")) {
|
|
|
|
|
|
return "checkbox";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼명 기반 추론
|
|
|
|
|
|
if (name.includes("email") || name.includes("mail")) return "email";
|
|
|
|
|
|
if (name.includes("phone") || name.includes("tel") || name.includes("mobile")) return "tel";
|
|
|
|
|
|
if (name.includes("url") || name.includes("link")) return "text";
|
|
|
|
|
|
if (name.includes("password") || name.includes("pwd")) return "text";
|
|
|
|
|
|
|
2025-09-05 12:04:13 +09:00
|
|
|
|
// 파일 타입 추론
|
|
|
|
|
|
if (
|
|
|
|
|
|
name.includes("file") ||
|
|
|
|
|
|
name.includes("attach") ||
|
|
|
|
|
|
name.includes("upload") ||
|
|
|
|
|
|
name.includes("document") ||
|
|
|
|
|
|
name.includes("docs") ||
|
|
|
|
|
|
name.includes("image") ||
|
|
|
|
|
|
name.includes("photo") ||
|
|
|
|
|
|
name.includes("picture") ||
|
|
|
|
|
|
name.includes("media")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return "file";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
// 텍스트 타입 (기본값)
|
|
|
|
|
|
return "text";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 업데이트
|
|
|
|
|
|
const updateColumn = useCallback(
|
|
|
|
|
|
(columnId: string, updates: Partial<DataTableColumn>) => {
|
|
|
|
|
|
const updatedColumns = component.columns.map((col) => (col.id === columnId ? { ...col, ...updates } : col));
|
|
|
|
|
|
onUpdateComponent({ columns: updatedColumns });
|
|
|
|
|
|
},
|
|
|
|
|
|
[component.columns, onUpdateComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-03 17:12:27 +09:00
|
|
|
|
// 컬럼 상세 설정 업데이트 (테이블 타입 관리에도 반영)
|
|
|
|
|
|
const updateColumnDetailSettings = useCallback(
|
|
|
|
|
|
async (columnId: string, webTypeConfig: any) => {
|
|
|
|
|
|
// 1. 먼저 화면 컴포넌트의 컬럼 설정 업데이트
|
|
|
|
|
|
const updatedColumns = component.columns.map((col) => (col.id === columnId ? { ...col, webTypeConfig } : col));
|
|
|
|
|
|
console.log("🔄 컬럼 상세 설정 업데이트:", { columnId, webTypeConfig, updatedColumns });
|
|
|
|
|
|
onUpdateComponent({ columns: updatedColumns });
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 테이블 타입 관리에도 반영 (라디오 타입인 경우)
|
|
|
|
|
|
const targetColumn = component.columns.find((col) => col.id === columnId);
|
|
|
|
|
|
if (targetColumn && targetColumn.widgetType === "radio" && selectedTable) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// TODO: 테이블 타입 관리 API 호출하여 웹 타입과 상세 설정 업데이트
|
|
|
|
|
|
console.log("📡 테이블 타입 관리 업데이트 필요:", {
|
|
|
|
|
|
tableName: component.tableName,
|
|
|
|
|
|
columnName: targetColumn.columnName,
|
|
|
|
|
|
webType: "radio",
|
|
|
|
|
|
detailSettings: JSON.stringify(webTypeConfig),
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("테이블 타입 관리 업데이트 실패:", error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[component.columns, component.tableName, selectedTable, onUpdateComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼의 현재 웹 타입 가져오기 (테이블 타입 관리에서 설정된 값)
|
|
|
|
|
|
const getColumnWebType = useCallback(
|
|
|
|
|
|
(column: DataTableColumn) => {
|
|
|
|
|
|
// 테이블 타입 관리에서 설정된 웹 타입 찾기
|
|
|
|
|
|
if (!selectedTable) return "text";
|
|
|
|
|
|
|
|
|
|
|
|
const tableColumn = selectedTable.columns.find((col) => col.columnName === column.columnName);
|
|
|
|
|
|
return (
|
|
|
|
|
|
tableColumn?.webType ||
|
2025-09-03 18:23:47 +09:00
|
|
|
|
getWidgetTypeFromColumn(
|
|
|
|
|
|
tableColumn || {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
dataType: "text",
|
|
|
|
|
|
tableName: "",
|
|
|
|
|
|
isNullable: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2025-09-03 17:12:27 +09:00
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
[selectedTable],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼의 현재 상세 설정 가져오기
|
|
|
|
|
|
const getColumnCurrentDetailSettings = useCallback((column: DataTableColumn) => {
|
|
|
|
|
|
return column.webTypeConfig || {};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
// 웹 타입별 상세 설정 렌더링
|
|
|
|
|
|
const renderColumnDetailSettings = useCallback(
|
|
|
|
|
|
(column: DataTableColumn) => {
|
|
|
|
|
|
const webType = getColumnWebType(column);
|
|
|
|
|
|
const currentSettings = getColumnCurrentDetailSettings(column);
|
|
|
|
|
|
const localSettings = localColumnDetailSettings[column.id] || currentSettings;
|
|
|
|
|
|
|
|
|
|
|
|
const updateSettings = (newSettings: any) => {
|
|
|
|
|
|
const merged = { ...localSettings, ...newSettings };
|
|
|
|
|
|
setLocalColumnDetailSettings((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[column.id]: merged,
|
|
|
|
|
|
}));
|
|
|
|
|
|
updateColumnDetailSettings(column.id, merged);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
switch (webType) {
|
|
|
|
|
|
case "select":
|
|
|
|
|
|
case "dropdown":
|
|
|
|
|
|
case "radio":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium">옵션 목록</Label>
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{(localSettings.options || []).map((option: any, index: number) => {
|
|
|
|
|
|
// 안전한 값 추출
|
|
|
|
|
|
const currentLabel =
|
|
|
|
|
|
typeof option === "object" && option !== null
|
|
|
|
|
|
? option.label || option.value || ""
|
|
|
|
|
|
: String(option || "");
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={index} className="flex items-center space-x-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={currentLabel}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newOptions = [...(localSettings.options || [])];
|
|
|
|
|
|
newOptions[index] = { label: e.target.value, value: e.target.value };
|
|
|
|
|
|
updateSettings({ options: newOptions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="옵션명"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newOptions = (localSettings.options || []).filter((_: any, i: number) => i !== index);
|
|
|
|
|
|
updateSettings({ options: newOptions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-7 w-7 p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newOption = { label: "", value: "" };
|
|
|
|
|
|
updateSettings({ options: [...(localSettings.options || []), newOption] });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
|
|
|
|
옵션 추가
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{webType === "radio" ? (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs font-medium">기본값 설정</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={localSettings.defaultValue || "__NONE__"}
|
|
|
|
|
|
onValueChange={(value) => updateSettings({ defaultValue: value === "__NONE__" ? "" : value })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="기본값 선택..." />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="__NONE__">선택 안함</SelectItem>
|
|
|
|
|
|
{(localSettings.options || []).map((option: any, index: number) => {
|
|
|
|
|
|
// 안전한 문자열 변환
|
|
|
|
|
|
const getStringValue = (val: any): string => {
|
|
|
|
|
|
if (typeof val === "string") return val;
|
|
|
|
|
|
if (typeof val === "number") return String(val);
|
|
|
|
|
|
if (typeof val === "object" && val !== null) {
|
|
|
|
|
|
return val.label || val.value || val.name || JSON.stringify(val);
|
|
|
|
|
|
}
|
|
|
|
|
|
return String(val || "");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const optionValue = getStringValue(option.value || option.label || option) || `option-${index}`;
|
|
|
|
|
|
const optionLabel =
|
|
|
|
|
|
getStringValue(option.label || option.value || option) || `옵션 ${index + 1}`;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<SelectItem key={index} value={optionValue}>
|
|
|
|
|
|
{optionLabel}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={localSettings.multiple || false}
|
|
|
|
|
|
onCheckedChange={(checked) => updateSettings({ multiple: checked })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label className="text-xs">다중 선택 허용</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
|
case "decimal":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">최소값</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={localSettings.min || ""}
|
|
|
|
|
|
onChange={(e) => updateSettings({ min: e.target.value ? Number(e.target.value) : undefined })}
|
|
|
|
|
|
placeholder="최소값"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">최대값</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={localSettings.max || ""}
|
|
|
|
|
|
onChange={(e) => updateSettings({ max: e.target.value ? Number(e.target.value) : undefined })}
|
|
|
|
|
|
placeholder="최대값"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{webType === "decimal" && (
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">단계 (step)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
step="0.01"
|
|
|
|
|
|
value={localSettings.step || "0.01"}
|
|
|
|
|
|
onChange={(e) => updateSettings({ step: e.target.value })}
|
|
|
|
|
|
placeholder="0.01"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case "date":
|
|
|
|
|
|
case "datetime":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">최소 날짜</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={localSettings.minDate || ""}
|
|
|
|
|
|
onChange={(e) => updateSettings({ minDate: e.target.value })}
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">최대 날짜</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={localSettings.maxDate || ""}
|
|
|
|
|
|
onChange={(e) => updateSettings({ maxDate: e.target.value })}
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{webType === "datetime" && (
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={localSettings.showSeconds || false}
|
|
|
|
|
|
onCheckedChange={(checked) => updateSettings({ showSeconds: checked })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label className="text-xs">초 단위까지 표시</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case "text":
|
|
|
|
|
|
case "email":
|
|
|
|
|
|
case "tel":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">최대 길이</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={localSettings.maxLength || ""}
|
|
|
|
|
|
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
|
|
|
|
|
|
placeholder="최대 문자 수"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">플레이스홀더</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={localSettings.placeholder || ""}
|
|
|
|
|
|
onChange={(e) => updateSettings({ placeholder: e.target.value })}
|
|
|
|
|
|
placeholder="입력 안내 텍스트"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">행 수</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={localSettings.rows || "3"}
|
|
|
|
|
|
onChange={(e) => updateSettings({ rows: Number(e.target.value) })}
|
|
|
|
|
|
placeholder="3"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">최대 길이</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={localSettings.maxLength || ""}
|
|
|
|
|
|
onChange={(e) => updateSettings({ maxLength: e.target.value ? Number(e.target.value) : undefined })}
|
|
|
|
|
|
placeholder="최대 문자 수"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case "file":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">허용 파일 형식</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={localSettings.accept || ""}
|
|
|
|
|
|
onChange={(e) => updateSettings({ accept: e.target.value })}
|
|
|
|
|
|
placeholder=".jpg,.png,.pdf"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">최대 파일 크기 (MB)</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={localSettings.maxSize ? localSettings.maxSize / 1024 / 1024 : "10"}
|
|
|
|
|
|
onChange={(e) => updateSettings({ maxSize: Number(e.target.value) * 1024 * 1024 })}
|
|
|
|
|
|
placeholder="10"
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={localSettings.multiple || false}
|
|
|
|
|
|
onCheckedChange={(checked) => updateSettings({ multiple: checked })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label className="text-xs">다중 파일 허용</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
return <div className="py-2 text-xs text-gray-500">이 웹 타입({webType})에 대한 상세 설정이 없습니다.</div>;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[getColumnWebType, getColumnCurrentDetailSettings, localColumnDetailSettings, updateColumnDetailSettings],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
// 컬럼 삭제
|
|
|
|
|
|
const removeColumn = useCallback(
|
|
|
|
|
|
(columnId: string) => {
|
|
|
|
|
|
const columnToRemove = component.columns.find((col) => col.id === columnId);
|
|
|
|
|
|
const updatedColumns = component.columns.filter((col) => col.id !== columnId);
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 상태에서도 해당 컬럼 제거
|
|
|
|
|
|
setLocalColumnInputs((prev) => {
|
|
|
|
|
|
const newInputs = { ...prev };
|
|
|
|
|
|
delete newInputs[columnId];
|
|
|
|
|
|
return newInputs;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 체크박스 상태에서도 해당 컬럼 제거
|
|
|
|
|
|
setLocalColumnCheckboxes((prev) => {
|
|
|
|
|
|
const newCheckboxes = { ...prev };
|
|
|
|
|
|
delete newCheckboxes[columnId];
|
|
|
|
|
|
return newCheckboxes;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 그리드 컬럼 상태에서도 해당 컬럼 제거
|
|
|
|
|
|
setLocalColumnGridColumns((prev) => {
|
|
|
|
|
|
const newGridColumns = { ...prev };
|
|
|
|
|
|
delete newGridColumns[columnId];
|
|
|
|
|
|
return newGridColumns;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🗑️ 컬럼 삭제:", {
|
|
|
|
|
|
columnId,
|
|
|
|
|
|
columnName: columnToRemove?.columnName,
|
|
|
|
|
|
remainingColumns: updatedColumns.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
columns: updatedColumns,
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
[component.columns, onUpdateComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 필터 업데이트
|
|
|
|
|
|
const updateFilter = useCallback(
|
|
|
|
|
|
(index: number, updates: Partial<DataTableFilter>) => {
|
|
|
|
|
|
const updatedFilters = component.filters.map((filter, i) => (i === index ? { ...filter, ...updates } : filter));
|
2025-09-03 16:38:10 +09:00
|
|
|
|
console.log("🔄 필터 업데이트:", { index, updates, updatedFilters });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
onUpdateComponent({ filters: updatedFilters });
|
|
|
|
|
|
},
|
|
|
|
|
|
[component.filters, onUpdateComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 필터 추가
|
|
|
|
|
|
const addFilter = useCallback(() => {
|
|
|
|
|
|
if (!selectedTable) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 필터 가능한 컬럼들 중에서 아직 필터가 없는 컬럼들만 선택
|
|
|
|
|
|
const availableColumns = selectedTable.columns.filter(
|
|
|
|
|
|
(col) =>
|
|
|
|
|
|
isFilterableWebType(getWidgetTypeFromColumn(col)) &&
|
|
|
|
|
|
!component.filters.some((filter) => filter.columnName === col.columnName),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (availableColumns.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const targetColumn = availableColumns[0];
|
|
|
|
|
|
const widgetType = getWidgetTypeFromColumn(targetColumn);
|
|
|
|
|
|
|
|
|
|
|
|
const newFilter: DataTableFilter = {
|
|
|
|
|
|
columnName: targetColumn.columnName,
|
|
|
|
|
|
widgetType,
|
2025-09-03 16:38:10 +09:00
|
|
|
|
label: targetColumn.columnLabel || targetColumn.columnName,
|
2025-09-03 15:23:12 +09:00
|
|
|
|
gridColumns: 3,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log("➕ 필터 추가 시작:", {
|
|
|
|
|
|
targetColumnName: targetColumn.columnName,
|
|
|
|
|
|
targetColumnLabel: targetColumn.columnLabel,
|
|
|
|
|
|
inferredWidgetType: widgetType,
|
|
|
|
|
|
currentFiltersCount: component.filters.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("➕ 생성된 새 필터:", {
|
|
|
|
|
|
columnName: newFilter.columnName,
|
|
|
|
|
|
widgetType: newFilter.widgetType,
|
|
|
|
|
|
label: newFilter.label,
|
|
|
|
|
|
gridColumns: newFilter.gridColumns,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const updatedFilters = [...component.filters, newFilter];
|
|
|
|
|
|
console.log("🔄 필터 업데이트 호출:", {
|
|
|
|
|
|
filtersToAdd: 1,
|
|
|
|
|
|
totalFiltersAfter: updatedFilters.length,
|
|
|
|
|
|
updatedFilters: updatedFilters.map((filter) => ({
|
|
|
|
|
|
columnName: filter.columnName,
|
|
|
|
|
|
widgetType: filter.widgetType,
|
|
|
|
|
|
label: filter.label,
|
|
|
|
|
|
})),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
// 먼저 로컬 상태를 업데이트하고
|
|
|
|
|
|
const filterKey = `${newFilter.columnName}-${component.filters.length}`;
|
|
|
|
|
|
setLocalFilterInputs((prev) => {
|
|
|
|
|
|
const newState = {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[filterKey]: newFilter.label,
|
|
|
|
|
|
};
|
|
|
|
|
|
console.log("📝 필터 로컬 상태 업데이트:", {
|
|
|
|
|
|
filterKey,
|
|
|
|
|
|
newLabel: newFilter.label,
|
|
|
|
|
|
prevState: prev,
|
|
|
|
|
|
newState,
|
|
|
|
|
|
});
|
|
|
|
|
|
return newState;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 그 다음 컴포넌트 상태를 업데이트
|
2025-09-03 15:23:12 +09:00
|
|
|
|
onUpdateComponent({ filters: updatedFilters });
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
// 필터 추가 후 필터 탭으로 자동 이동
|
|
|
|
|
|
setActiveTab("filters");
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔍 필터 추가 후 탭 이동:", {
|
|
|
|
|
|
activeTab: "filters",
|
|
|
|
|
|
isExternalControl: !!onTabChange,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 강제로 리렌더링을 트리거하기 위해 여러 방법 사용
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setLocalFilterInputs((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[filterKey]: newFilter.label,
|
|
|
|
|
|
}));
|
|
|
|
|
|
console.log("🔄 setTimeout에서 강제 로컬 상태 업데이트:", { filterKey, label: newFilter.label });
|
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 추가적인 강제 업데이트
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setLocalFilterInputs((prev) => {
|
|
|
|
|
|
const updated = { ...prev, [filterKey]: newFilter.label };
|
|
|
|
|
|
console.log("🔄 두 번째 강제 업데이트:", { updated });
|
|
|
|
|
|
return updated;
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 필터 추가 완료 - 로컬 상태와 컴포넌트 모두 업데이트됨", {
|
|
|
|
|
|
filterKey,
|
|
|
|
|
|
newFilterLabel: newFilter.label,
|
|
|
|
|
|
switchedToTab: "filters",
|
|
|
|
|
|
});
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}, [selectedTable, component.filters, onUpdateComponent]);
|
|
|
|
|
|
|
|
|
|
|
|
// 필터 삭제
|
|
|
|
|
|
const removeFilter = useCallback(
|
|
|
|
|
|
(index: number) => {
|
|
|
|
|
|
const updatedFilters = component.filters.filter((_, i) => i !== index);
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
|
|
|
|
|
// 로컬 필터 입력 상태에서도 해당 필터 제거
|
|
|
|
|
|
setLocalFilterInputs((prev) => {
|
|
|
|
|
|
const newFilterInputs = { ...prev };
|
|
|
|
|
|
const filterKey = `${component.filters?.[index]?.columnName}-${index}`;
|
|
|
|
|
|
delete newFilterInputs[filterKey];
|
|
|
|
|
|
return newFilterInputs;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
onUpdateComponent({ filters: updatedFilters });
|
|
|
|
|
|
},
|
|
|
|
|
|
[component.filters, onUpdateComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 웹 타입별 필터 가능 여부 확인
|
|
|
|
|
|
const isFilterableWebType = (webType: WebType): boolean => {
|
|
|
|
|
|
const filterableTypes: WebType[] = ["text", "number", "decimal", "date", "datetime", "select", "email", "tel"];
|
|
|
|
|
|
return filterableTypes.includes(webType);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 추가 (테이블에서 선택)
|
|
|
|
|
|
const addColumn = useCallback(
|
|
|
|
|
|
(columnName?: string) => {
|
|
|
|
|
|
if (!selectedTable) return;
|
|
|
|
|
|
|
|
|
|
|
|
const availableColumns = selectedTable.columns.filter(
|
|
|
|
|
|
(col) => !component.columns.some((column) => column.columnName === col.columnName),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (availableColumns.length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 특정 컬럼이 지정되었으면 해당 컬럼을, 아니면 첫 번째 사용 가능한 컬럼을 사용
|
|
|
|
|
|
const targetColumn = columnName
|
|
|
|
|
|
? availableColumns.find((col) => col.columnName === columnName) || availableColumns[0]
|
|
|
|
|
|
: availableColumns[0];
|
|
|
|
|
|
|
|
|
|
|
|
const widgetType = getWidgetTypeFromColumn(targetColumn);
|
|
|
|
|
|
|
|
|
|
|
|
const newColumn: DataTableColumn = {
|
|
|
|
|
|
id: generateComponentId(),
|
|
|
|
|
|
columnName: targetColumn.columnName,
|
|
|
|
|
|
label: targetColumn.columnLabel || targetColumn.columnName,
|
|
|
|
|
|
widgetType,
|
|
|
|
|
|
gridColumns: 2,
|
|
|
|
|
|
visible: true,
|
|
|
|
|
|
filterable: isFilterableWebType(widgetType),
|
|
|
|
|
|
sortable: true,
|
|
|
|
|
|
searchable: ["text", "email", "tel"].includes(widgetType),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 필터는 자동으로 추가하지 않음 (사용자가 수동으로 추가)
|
|
|
|
|
|
|
|
|
|
|
|
console.log("➕ 컬럼 추가 시작:", {
|
|
|
|
|
|
targetColumnName: targetColumn.columnName,
|
|
|
|
|
|
targetColumnLabel: targetColumn.columnLabel,
|
|
|
|
|
|
inferredWidgetType: widgetType,
|
|
|
|
|
|
currentColumnsCount: component.columns.length,
|
|
|
|
|
|
currentFiltersCount: component.filters.length,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("➕ 생성된 새 컬럼:", {
|
|
|
|
|
|
id: newColumn.id,
|
|
|
|
|
|
columnName: newColumn.columnName,
|
|
|
|
|
|
label: newColumn.label,
|
|
|
|
|
|
widgetType: newColumn.widgetType,
|
|
|
|
|
|
filterable: newColumn.filterable,
|
|
|
|
|
|
visible: newColumn.visible,
|
|
|
|
|
|
sortable: newColumn.sortable,
|
|
|
|
|
|
searchable: newColumn.searchable,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 필터는 수동으로만 추가
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 상태에 새 컬럼 입력값 추가
|
|
|
|
|
|
setLocalColumnInputs((prev) => {
|
|
|
|
|
|
const newInputs = {
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[newColumn.id]: newColumn.label,
|
|
|
|
|
|
};
|
|
|
|
|
|
console.log("🔄 로컬 컬럼 상태 업데이트:", {
|
|
|
|
|
|
newColumnId: newColumn.id,
|
|
|
|
|
|
newLabel: newColumn.label,
|
|
|
|
|
|
totalLocalInputs: Object.keys(newInputs).length,
|
|
|
|
|
|
});
|
|
|
|
|
|
return newInputs;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 체크박스 상태에 새 컬럼 추가
|
|
|
|
|
|
setLocalColumnCheckboxes((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[newColumn.id]: {
|
|
|
|
|
|
visible: newColumn.visible,
|
|
|
|
|
|
sortable: newColumn.sortable,
|
|
|
|
|
|
searchable: newColumn.searchable,
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 그리드 컬럼 상태에 새 컬럼 추가
|
|
|
|
|
|
setLocalColumnGridColumns((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[newColumn.id]: newColumn.gridColumns,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼만 업데이트
|
|
|
|
|
|
const updates: Partial<DataTableComponent> = {
|
|
|
|
|
|
columns: [...component.columns, newColumn],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔄 컴포넌트 업데이트 호출:", {
|
|
|
|
|
|
columnsToAdd: 1,
|
|
|
|
|
|
totalColumnsAfter: updates.columns?.length,
|
|
|
|
|
|
hasColumns: !!updates.columns,
|
|
|
|
|
|
updateKeys: Object.keys(updates),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log("🔄 업데이트 상세 내용:", {
|
|
|
|
|
|
columns: updates.columns?.map((col) => ({ id: col.id, columnName: col.columnName, label: col.label })),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent(updates);
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
// 컬럼 추가 후 컬럼 탭으로 자동 이동
|
|
|
|
|
|
setActiveTab("columns");
|
|
|
|
|
|
|
|
|
|
|
|
console.log("📋 컬럼 추가 후 탭 이동:", {
|
|
|
|
|
|
activeTab: "columns",
|
|
|
|
|
|
isExternalControl: !!onTabChange,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
console.log("✅ 컬럼 추가 완료 - onUpdateComponent 호출됨");
|
|
|
|
|
|
},
|
|
|
|
|
|
[selectedTable, component.columns, component.filters, onUpdateComponent],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
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: ["*/*"],
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
console.log("📁 가상 파일 컬럼 추가:", {
|
|
|
|
|
|
columnName: newColumn.columnName,
|
|
|
|
|
|
label: newColumn.label,
|
|
|
|
|
|
isVirtualFileColumn: newColumn.isVirtualFileColumn,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 상태에 새 컬럼 입력값 추가
|
|
|
|
|
|
setLocalColumnInputs((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[newColumn.id]: newColumn.label,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 체크박스 상태에 새 컬럼 추가
|
|
|
|
|
|
setLocalColumnCheckboxes((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[newColumn.id]: {
|
|
|
|
|
|
visible: newColumn.visible,
|
|
|
|
|
|
sortable: newColumn.sortable,
|
|
|
|
|
|
searchable: newColumn.searchable,
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 로컬 그리드 컬럼 상태에 새 컬럼 추가
|
|
|
|
|
|
setLocalColumnGridColumns((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[newColumn.id]: newColumn.gridColumns,
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 업데이트
|
|
|
|
|
|
const updates: Partial<DataTableComponent> = {
|
|
|
|
|
|
columns: [...component.columns, newColumn],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent(updates);
|
|
|
|
|
|
|
|
|
|
|
|
// 컬럼 추가 후 컬럼 탭으로 자동 이동
|
|
|
|
|
|
setActiveTab("columns");
|
|
|
|
|
|
|
|
|
|
|
|
console.log("✅ 가상 파일 컬럼 추가 완료");
|
|
|
|
|
|
}, [component.columns, onUpdateComponent]);
|
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
|
return (
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="max-h-[80vh] p-4">
|
|
|
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
|
|
|
|
|
<TabsList className="grid w-full grid-cols-4">
|
|
|
|
|
|
<TabsTrigger value="basic">기본 설정</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="columns">컬럼 설정</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="filters">필터 설정</TabsTrigger>
|
|
|
|
|
|
<TabsTrigger value="modal">모달 설정</TabsTrigger>
|
|
|
|
|
|
</TabsList>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<TabsContent value="basic" className="mt-4 max-h-[70vh] overflow-y-auto">
|
|
|
|
|
|
{/* 기본 설정 */}
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="flex items-center space-x-2 text-sm">
|
|
|
|
|
|
<Settings className="h-4 w-4" />
|
|
|
|
|
|
<span>기본 설정</span>
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
2025-09-03 16:38:10 +09:00
|
|
|
|
<div className="space-y-2">
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<Label htmlFor="table-select">연결 테이블</Label>
|
|
|
|
|
|
<Select value={component.tableName} onValueChange={handleTableChange}>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="테이블을 선택하세요" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{tables.map((table) => (
|
|
|
|
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
|
|
|
|
{table.tableLabel || table.tableName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<Label htmlFor="title">테이블 제목</Label>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
<Input
|
2025-09-03 18:23:47 +09:00
|
|
|
|
id="title"
|
|
|
|
|
|
value={localValues.title}
|
2025-09-03 16:38:10 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
2025-09-03 18:23:47 +09:00
|
|
|
|
setLocalValues((prev) => ({ ...prev, title: newValue }));
|
|
|
|
|
|
onUpdateComponent({ title: newValue });
|
2025-09-03 16:38:10 +09:00
|
|
|
|
}}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
placeholder="테이블 제목을 입력하세요"
|
2025-09-03 16:38:10 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
{/* CRUD 기능 설정 */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<h4 className="text-sm font-medium">CRUD 기능</h4>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="enable-add"
|
|
|
|
|
|
checked={localValues.enableAdd}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, enableAdd: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({ enableAdd: checked as boolean });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="enable-add" className="text-sm">
|
|
|
|
|
|
데이터 추가 기능
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="enable-edit"
|
|
|
|
|
|
checked={localValues.enableEdit}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, enableEdit: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({ enableEdit: checked as boolean });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="enable-edit" className="text-sm">
|
|
|
|
|
|
데이터 수정 기능
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="enable-delete"
|
|
|
|
|
|
checked={localValues.enableDelete}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, enableDelete: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({ enableDelete: checked as boolean });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="enable-delete" className="text-sm">
|
|
|
|
|
|
데이터 삭제 기능
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="grid grid-cols-3 gap-4">
|
2025-09-03 16:38:10 +09:00
|
|
|
|
<div className="space-y-2">
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<Label htmlFor="add-button-text" className="text-sm">
|
|
|
|
|
|
추가 버튼 텍스트
|
2025-09-03 16:38:10 +09:00
|
|
|
|
</Label>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<Input
|
|
|
|
|
|
id="add-button-text"
|
|
|
|
|
|
value={localValues.addButtonText}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, addButtonText: newValue }));
|
|
|
|
|
|
onUpdateComponent({ addButtonText: newValue });
|
2025-09-03 16:38:10 +09:00
|
|
|
|
}}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
placeholder="추가"
|
|
|
|
|
|
disabled={!localValues.enableAdd}
|
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
|
/>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="edit-button-text" className="text-sm">
|
|
|
|
|
|
수정 버튼 텍스트
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="edit-button-text"
|
|
|
|
|
|
value={localValues.editButtonText}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, editButtonText: newValue }));
|
|
|
|
|
|
onUpdateComponent({ editButtonText: newValue });
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="수정"
|
|
|
|
|
|
disabled={!localValues.enableEdit}
|
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="delete-button-text" className="text-sm">
|
|
|
|
|
|
삭제 버튼 텍스트
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="delete-button-text"
|
|
|
|
|
|
value={localValues.deleteButtonText}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, deleteButtonText: newValue }));
|
|
|
|
|
|
onUpdateComponent({ deleteButtonText: newValue });
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="삭제"
|
|
|
|
|
|
disabled={!localValues.enableDelete}
|
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
{/* 추가 모달 커스터마이징 설정 */}
|
|
|
|
|
|
{localValues.enableAdd && (
|
|
|
|
|
|
<div className="space-y-4 border-t pt-4">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<h4 className="text-sm font-medium">추가 모달 설정</h4>
|
|
|
|
|
|
</div>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="modal-title" className="text-sm">
|
|
|
|
|
|
모달 제목
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="modal-title"
|
|
|
|
|
|
value={localValues.modalTitle}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, modalTitle: newValue }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: { ...component.addModalConfig, title: newValue },
|
|
|
|
|
|
});
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
placeholder="새 데이터 추가"
|
|
|
|
|
|
className="h-8 text-sm"
|
2025-09-03 15:23:12 +09:00
|
|
|
|
/>
|
2025-09-03 17:12:27 +09:00
|
|
|
|
</div>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="modal-width" className="text-sm">
|
|
|
|
|
|
모달 크기
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={localValues.modalWidth}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, modalWidth: value as any }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: { ...component.addModalConfig, width: value as any },
|
|
|
|
|
|
});
|
2025-09-03 17:12:27 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<SelectTrigger className="h-8 text-sm">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="sm">작음 (384px)</SelectItem>
|
|
|
|
|
|
<SelectItem value="md">보통 (448px)</SelectItem>
|
|
|
|
|
|
<SelectItem value="lg">큼 (512px)</SelectItem>
|
|
|
|
|
|
<SelectItem value="xl">매우 큼 (576px)</SelectItem>
|
|
|
|
|
|
<SelectItem value="2xl">특대 (672px)</SelectItem>
|
|
|
|
|
|
<SelectItem value="full">전체 너비</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="modal-description" className="text-sm">
|
|
|
|
|
|
모달 설명
|
|
|
|
|
|
</Label>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
<Input
|
2025-09-03 18:23:47 +09:00
|
|
|
|
id="modal-description"
|
|
|
|
|
|
value={localValues.modalDescription}
|
2025-09-03 15:23:12 +09:00
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
2025-09-03 18:23:47 +09:00
|
|
|
|
setLocalValues((prev) => ({ ...prev, modalDescription: newValue }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: { ...component.addModalConfig, description: newValue },
|
|
|
|
|
|
});
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
placeholder="모달에 표시될 설명을 입력하세요"
|
|
|
|
|
|
className="h-8 text-sm"
|
2025-09-03 15:23:12 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="modal-layout" className="text-sm">
|
|
|
|
|
|
레이아웃
|
|
|
|
|
|
</Label>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
<Select
|
2025-09-03 18:23:47 +09:00
|
|
|
|
value={localValues.modalLayout}
|
2025-09-03 15:23:12 +09:00
|
|
|
|
onValueChange={(value) => {
|
2025-09-03 18:23:47 +09:00
|
|
|
|
setLocalValues((prev) => ({ ...prev, modalLayout: value as any }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: { ...component.addModalConfig, layout: value as any },
|
|
|
|
|
|
});
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<SelectTrigger className="h-8 text-sm">
|
2025-09-03 15:23:12 +09:00
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<SelectItem value="single">단일 컬럼</SelectItem>
|
|
|
|
|
|
<SelectItem value="two-column">2컬럼</SelectItem>
|
|
|
|
|
|
<SelectItem value="grid">그리드</SelectItem>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
{localValues.modalLayout === "grid" && (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="modal-grid-columns" className="text-sm">
|
|
|
|
|
|
그리드 컬럼 수
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={localValues.modalGridColumns.toString()}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const gridColumns = parseInt(value);
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, modalGridColumns: gridColumns }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: { ...component.addModalConfig, gridColumns },
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 text-sm">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="2">2컬럼</SelectItem>
|
|
|
|
|
|
<SelectItem value="3">3컬럼</SelectItem>
|
|
|
|
|
|
<SelectItem value="4">4컬럼</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="modal-submit-text" className="text-sm">
|
|
|
|
|
|
제출 버튼 텍스트
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="modal-submit-text"
|
|
|
|
|
|
value={localValues.modalSubmitButtonText}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, modalSubmitButtonText: newValue }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: { ...component.addModalConfig, submitButtonText: newValue },
|
|
|
|
|
|
});
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
placeholder="추가"
|
|
|
|
|
|
className="h-8 text-sm"
|
2025-09-03 15:23:12 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="modal-cancel-text" className="text-sm">
|
|
|
|
|
|
취소 버튼 텍스트
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="modal-cancel-text"
|
|
|
|
|
|
value={localValues.modalCancelButtonText}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, modalCancelButtonText: newValue }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: { ...component.addModalConfig, cancelButtonText: newValue },
|
|
|
|
|
|
});
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
placeholder="취소"
|
|
|
|
|
|
className="h-8 text-sm"
|
2025-09-03 15:23:12 +09:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label htmlFor="grid-columns">그리드 컬럼 수</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={localValues.gridColumns.toString()}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const gridColumns = parseInt(value, 10);
|
|
|
|
|
|
console.log("🔄 테이블 그리드 컬럼 수 변경:", gridColumns);
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, gridColumns }));
|
|
|
|
|
|
onUpdateComponent({ gridColumns });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue placeholder="그리드 컬럼 수 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((num) => (
|
|
|
|
|
|
<SelectItem key={num} value={num.toString()}>
|
|
|
|
|
|
{num}컬럼 ({Math.round((num / 12) * 100)}%)
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="show-search-button"
|
|
|
|
|
|
checked={localValues.showSearchButton}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
console.log("🔄 검색 버튼 표시 변경:", checked);
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, showSearchButton: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({ showSearchButton: checked as boolean });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="show-search-button" className="text-sm">
|
|
|
|
|
|
검색 버튼 표시
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="enable-export"
|
|
|
|
|
|
checked={localValues.enableExport}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
console.log("🔄 내보내기 기능 변경:", checked);
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, enableExport: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({ enableExport: checked as boolean });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="enable-export" className="text-sm">
|
|
|
|
|
|
내보내기 기능
|
|
|
|
|
|
</Label>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<TabsContent value="columns" className="mt-4 max-h-[70vh] overflow-y-auto">
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="flex items-center space-x-2 text-sm">
|
|
|
|
|
|
<Columns className="h-4 w-4" />
|
|
|
|
|
|
<span>컬럼 설정</span>
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<h3 className="text-sm font-medium">테이블 컬럼 설정</h3>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Badge variant="secondary">{component.columns.length}개</Badge>
|
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 ? (
|
|
|
|
|
|
<Select onValueChange={(value) => addColumn(value)}>
|
|
|
|
|
|
<SelectTrigger className="h-8 w-32 text-xs">
|
2025-09-06 00:16:27 +09:00
|
|
|
|
<SelectValue placeholder="DB 컬럼" />
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{availableColumns.map((col) => (
|
|
|
|
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
|
|
|
|
{col.columnLabel || col.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Button size="sm" disabled>
|
|
|
|
|
|
<Plus className="h-4 w-4" />
|
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) => {
|
|
|
|
|
|
console.log("🔄 컬럼 표시 변경:", { columnId: column.id, checked });
|
|
|
|
|
|
setLocalColumnCheckboxes((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[column.id]: { ...prev[column.id], visible: checked as boolean },
|
|
|
|
|
|
}));
|
|
|
|
|
|
updateColumn(column.id, { visible: checked as boolean });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="truncate text-sm font-medium">{column.label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
setIsColumnDetailOpen((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[column.id]: !prev[column.id],
|
|
|
|
|
|
}));
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Settings className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
removeColumn(column.id);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
|
</Button>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
|
<Badge variant="outline" className="max-w-[120px] truncate text-xs">
|
|
|
|
|
|
{column.columnName}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
<Badge variant="secondary" className="max-w-[80px] truncate text-xs">
|
|
|
|
|
|
{getColumnWebType(column)}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">표시명</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={
|
|
|
|
|
|
localColumnInputs[column.id] !== undefined ? localColumnInputs[column.id] : column.label
|
|
|
|
|
|
}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
setLocalColumnInputs((prev) => ({ ...prev, [column.id]: newValue }));
|
|
|
|
|
|
updateColumn(column.id, { label: newValue });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
onBlur={(e) => {
|
|
|
|
|
|
// 포커스 잃을 때 빈 값이면 원본 라벨로 복원하지 않음 (사용자가 의도적으로 지운 것)
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
if (newValue !== localColumnInputs[column.id]) {
|
|
|
|
|
|
setLocalColumnInputs((prev) => ({ ...prev, [column.id]: newValue }));
|
|
|
|
|
|
updateColumn(column.id, { label: newValue });
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="표시명을 입력하세요"
|
|
|
|
|
|
className="h-8 text-xs"
|
|
|
|
|
|
/>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="space-y-2">
|
2025-09-03 15:23:12 +09:00
|
|
|
|
<div className="space-y-1">
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<Label className="text-xs">그리드 컬럼</Label>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
<Select
|
2025-09-03 18:23:47 +09:00
|
|
|
|
value={(localColumnGridColumns[column.id] ?? column.gridColumns).toString()}
|
2025-09-03 15:23:12 +09:00
|
|
|
|
onValueChange={(value) => {
|
2025-09-03 18:23:47 +09:00
|
|
|
|
const newGridColumns = parseInt(value);
|
|
|
|
|
|
console.log("🔄 컬럼 그리드 컬럼 변경:", { columnId: column.id, newGridColumns });
|
|
|
|
|
|
setLocalColumnGridColumns((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[column.id]: newGridColumns,
|
|
|
|
|
|
}));
|
|
|
|
|
|
updateColumn(column.id, { gridColumns: newGridColumns });
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{[1, 2, 3, 4, 6, 12].map((num) => (
|
|
|
|
|
|
<SelectItem key={num} value={num.toString()}>
|
|
|
|
|
|
{num}칸
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={`sortable-${column.id}`}
|
|
|
|
|
|
checked={localColumnCheckboxes[column.id]?.sortable ?? column.sortable}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
console.log("🔄 컬럼 정렬 가능 변경:", { columnId: column.id, checked });
|
|
|
|
|
|
setLocalColumnCheckboxes((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[column.id]: { ...prev[column.id], sortable: checked as boolean },
|
|
|
|
|
|
}));
|
|
|
|
|
|
updateColumn(column.id, { sortable: checked as boolean });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor={`sortable-${column.id}`} className="text-xs">
|
|
|
|
|
|
정렬
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={`searchable-${column.id}`}
|
|
|
|
|
|
checked={localColumnCheckboxes[column.id]?.searchable ?? column.searchable}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
console.log("🔄 컬럼 검색 가능 변경:", { columnId: column.id, checked });
|
|
|
|
|
|
setLocalColumnCheckboxes((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[column.id]: { ...prev[column.id], searchable: checked as boolean },
|
|
|
|
|
|
}));
|
|
|
|
|
|
updateColumn(column.id, { searchable: checked as boolean });
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor={`searchable-${column.id}`} className="text-xs">
|
|
|
|
|
|
검색
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 웹 타입 상세 설정 */}
|
|
|
|
|
|
{isColumnDetailOpen[column.id] && (
|
|
|
|
|
|
<div className="border-t pt-2">
|
|
|
|
|
|
<div className="mb-2 flex items-center justify-between">
|
|
|
|
|
|
<Label className="text-xs font-medium">웹 타입 상세 설정</Label>
|
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
|
{getColumnWebType(column)}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="pl-2">{renderColumnDetailSettings(column)}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 모달 전용 설정 */}
|
|
|
|
|
|
{component.enableAdd && (
|
|
|
|
|
|
<div className="border-t pt-2">
|
|
|
|
|
|
<Collapsible
|
|
|
|
|
|
open={isModalConfigOpen[column.id] || false}
|
|
|
|
|
|
onOpenChange={(open) => setIsModalConfigOpen((prev) => ({ ...prev, [column.id]: open }))}
|
|
|
|
|
|
>
|
|
|
|
|
|
<CollapsibleTrigger asChild>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
className="flex h-auto w-full items-center justify-between p-1"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Label className="text-xs font-medium">모달 설정</Label>
|
|
|
|
|
|
<ChevronDown
|
|
|
|
|
|
className={`h-3 w-3 transition-transform ${
|
|
|
|
|
|
isModalConfigOpen[column.id] ? "rotate-180" : ""
|
|
|
|
|
|
}`}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
|
<CollapsibleContent className="mt-2">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={`required-${column.id}`}
|
|
|
|
|
|
checked={
|
|
|
|
|
|
component.addModalConfig?.requiredFields?.includes(column.columnName) || false
|
|
|
|
|
|
}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
const requiredFields = component.addModalConfig?.requiredFields || [];
|
|
|
|
|
|
let newRequiredFields;
|
|
|
|
|
|
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
|
newRequiredFields = [...requiredFields, column.columnName];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newRequiredFields = requiredFields.filter(
|
|
|
|
|
|
(field) => field !== column.columnName,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: {
|
|
|
|
|
|
...component.addModalConfig,
|
|
|
|
|
|
requiredFields: newRequiredFields,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor={`required-${column.id}`} className="text-xs">
|
|
|
|
|
|
필수
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id={`hidden-${column.id}`}
|
|
|
|
|
|
checked={
|
|
|
|
|
|
component.addModalConfig?.hiddenFields?.includes(column.columnName) || false
|
|
|
|
|
|
}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
const hiddenFields = component.addModalConfig?.hiddenFields || [];
|
|
|
|
|
|
let newHiddenFields;
|
|
|
|
|
|
|
|
|
|
|
|
if (checked) {
|
|
|
|
|
|
newHiddenFields = [...hiddenFields, column.columnName];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newHiddenFields = hiddenFields.filter((field) => field !== column.columnName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: {
|
|
|
|
|
|
...component.addModalConfig,
|
|
|
|
|
|
hiddenFields: newHiddenFields,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor={`hidden-${column.id}`} className="text-xs">
|
|
|
|
|
|
숨김
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs">입력 타입</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={
|
|
|
|
|
|
component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.inputType ||
|
|
|
|
|
|
"normal"
|
|
|
|
|
|
}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {};
|
|
|
|
|
|
const currentConfig = advancedConfigs[column.columnName] || {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const newConfig = {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
inputType: value as any,
|
|
|
|
|
|
autoValueType:
|
|
|
|
|
|
value === "auto" ? "current_datetime" : currentConfig.autoValueType || "none",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: {
|
|
|
|
|
|
...component.addModalConfig,
|
|
|
|
|
|
advancedFieldConfigs: {
|
|
|
|
|
|
...advancedConfigs,
|
|
|
|
|
|
[column.columnName]: newConfig,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="normal">일반 입력</SelectItem>
|
|
|
|
|
|
<SelectItem value="readonly">읽기 전용</SelectItem>
|
|
|
|
|
|
<SelectItem value="auto">자동 생성</SelectItem>
|
|
|
|
|
|
<SelectItem value="hidden">숨김</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.inputType ===
|
|
|
|
|
|
"auto" && (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs">자동 값 타입</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={
|
|
|
|
|
|
component.addModalConfig?.advancedFieldConfigs?.[column.columnName]
|
|
|
|
|
|
?.autoValueType || "current_datetime"
|
|
|
|
|
|
}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {};
|
|
|
|
|
|
const currentConfig = advancedConfigs[column.columnName] || {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: {
|
|
|
|
|
|
...component.addModalConfig,
|
|
|
|
|
|
advancedFieldConfigs: {
|
|
|
|
|
|
...advancedConfigs,
|
|
|
|
|
|
[column.columnName]: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
autoValueType: value as any,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="current_datetime">현재 날짜시간</SelectItem>
|
|
|
|
|
|
<SelectItem value="current_date">현재 날짜</SelectItem>
|
|
|
|
|
|
<SelectItem value="current_time">현재 시간</SelectItem>
|
|
|
|
|
|
<SelectItem value="current_user">현재 사용자</SelectItem>
|
|
|
|
|
|
<SelectItem value="uuid">UUID</SelectItem>
|
|
|
|
|
|
<SelectItem value="sequence">시퀀스</SelectItem>
|
|
|
|
|
|
<SelectItem value="custom">사용자 정의</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{component.addModalConfig?.advancedFieldConfigs?.[column.columnName]?.autoValueType ===
|
|
|
|
|
|
"custom" && (
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label className="text-xs">사용자 정의 값</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={
|
|
|
|
|
|
component.addModalConfig?.advancedFieldConfigs?.[column.columnName]
|
|
|
|
|
|
?.customValue || ""
|
|
|
|
|
|
}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {};
|
|
|
|
|
|
const currentConfig = advancedConfigs[column.columnName] || {
|
|
|
|
|
|
columnName: column.columnName,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
addModalConfig: {
|
|
|
|
|
|
...component.addModalConfig,
|
|
|
|
|
|
advancedFieldConfigs: {
|
|
|
|
|
|
...advancedConfigs,
|
|
|
|
|
|
[column.columnName]: {
|
|
|
|
|
|
...currentConfig,
|
|
|
|
|
|
customValue: e.target.value,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="고정값 입력..."
|
|
|
|
|
|
className="h-8 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
|
</Collapsible>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<TabsContent value="filters" className="mt-4 max-h-[70vh] overflow-y-auto">
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="flex items-center space-x-2 text-sm">
|
|
|
|
|
|
<Filter className="h-4 w-4" />
|
|
|
|
|
|
<span>필터 설정</span>
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<h3 className="text-sm font-medium">검색 필터 설정</h3>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Badge variant="secondary">{component.filters.length}개</Badge>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
addFilter();
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={!selectedTable}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="h-4 w-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
{component.filters.length === 0 ? (
|
|
|
|
|
|
<div className="text-muted-foreground py-8 text-center">
|
|
|
|
|
|
<Filter className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
|
|
|
|
|
<p className="text-sm">필터가 없습니다</p>
|
|
|
|
|
|
<p className="text-xs">컬럼을 추가하면 자동으로 필터가 생성됩니다</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="max-h-96 space-y-3 overflow-y-auto">
|
|
|
|
|
|
{component.filters.map((filter, index) => {
|
|
|
|
|
|
const getWebTypeIcon = (webType: WebType) => {
|
|
|
|
|
|
switch (webType) {
|
|
|
|
|
|
case "text":
|
|
|
|
|
|
case "email":
|
|
|
|
|
|
case "tel":
|
|
|
|
|
|
return "📝";
|
|
|
|
|
|
case "number":
|
|
|
|
|
|
case "decimal":
|
|
|
|
|
|
return "🔢";
|
|
|
|
|
|
case "date":
|
|
|
|
|
|
case "datetime":
|
|
|
|
|
|
return "📅";
|
|
|
|
|
|
case "select":
|
|
|
|
|
|
return "📋";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "🔍";
|
2025-09-03 15:23:12 +09:00
|
|
|
|
}
|
2025-09-03 18:23:47 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getWebTypeDescription = (webType: WebType) => {
|
|
|
|
|
|
switch (webType) {
|
|
|
|
|
|
case "text":
|
|
|
|
|
|
return "텍스트 검색 (부분 일치)";
|
|
|
|
|
|
case "email":
|
|
|
|
|
|
return "이메일 형식 검색";
|
|
|
|
|
|
case "tel":
|
|
|
|
|
|
return "전화번호 검색";
|
|
|
|
|
|
case "number":
|
|
|
|
|
|
return "숫자 범위 검색";
|
|
|
|
|
|
case "decimal":
|
|
|
|
|
|
return "소수점 범위 검색";
|
|
|
|
|
|
case "date":
|
|
|
|
|
|
return "날짜 범위 검색";
|
|
|
|
|
|
case "datetime":
|
|
|
|
|
|
return "날짜시간 범위 검색";
|
|
|
|
|
|
case "select":
|
|
|
|
|
|
return "선택 옵션 필터";
|
|
|
|
|
|
default:
|
|
|
|
|
|
return "기본 검색";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card key={`filter-${filter.columnName}-${filter.widgetType}-${index}`} className="p-2">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex flex-1 items-center space-x-2">
|
|
|
|
|
|
<span className="text-lg">{getWebTypeIcon(filter.widgetType)}</span>
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">필터 이름</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={(() => {
|
|
|
|
|
|
const filterKey = `${filter.columnName}-${index}`;
|
|
|
|
|
|
const localValue = localFilterInputs[filterKey];
|
|
|
|
|
|
const finalValue = localValue !== undefined ? localValue : filter.label;
|
|
|
|
|
|
console.log("🎯 필터 입력 값 결정:", {
|
|
|
|
|
|
filterKey,
|
|
|
|
|
|
localValue,
|
|
|
|
|
|
filterLabel: filter.label,
|
|
|
|
|
|
finalValue,
|
|
|
|
|
|
allLocalInputs: Object.keys(localFilterInputs),
|
|
|
|
|
|
});
|
|
|
|
|
|
return finalValue;
|
|
|
|
|
|
})()}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newValue = e.target.value;
|
|
|
|
|
|
const filterKey = `${filter.columnName}-${index}`;
|
|
|
|
|
|
setLocalFilterInputs((prev) => ({ ...prev, [filterKey]: newValue }));
|
|
|
|
|
|
updateFilter(index, { label: newValue });
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="필터 이름 입력..."
|
|
|
|
|
|
className="h-8 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-muted-foreground mt-1 text-xs">
|
|
|
|
|
|
{getWebTypeDescription(filter.widgetType)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
removeFilter(index);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-3 gap-3">
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">연결 컬럼</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={filter.columnName}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const column = selectedTable?.columns.find((col) => col.columnName === value);
|
|
|
|
|
|
if (column) {
|
|
|
|
|
|
const newWidgetType = getWidgetTypeFromColumn(column);
|
|
|
|
|
|
updateFilter(index, {
|
|
|
|
|
|
columnName: value,
|
|
|
|
|
|
label: column.columnLabel || column.columnName,
|
|
|
|
|
|
widgetType: newWidgetType,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{selectedTable?.columns
|
|
|
|
|
|
.filter((col) => isFilterableWebType(getWidgetTypeFromColumn(col)))
|
|
|
|
|
|
.map((col) => (
|
|
|
|
|
|
<SelectItem key={col.columnName} value={col.columnName}>
|
|
|
|
|
|
{getWebTypeIcon(getWidgetTypeFromColumn(col))}{" "}
|
|
|
|
|
|
{col.columnLabel || col.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">필터 타입</Label>
|
|
|
|
|
|
<div className="bg-muted flex h-8 items-center rounded-md px-2 text-xs">
|
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
|
{webTypeOptions.find((opt) => opt.value === filter.widgetType)?.label ||
|
|
|
|
|
|
filter.widgetType}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
|
<Label className="text-xs">그리드 컬럼</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={filter.gridColumns.toString()}
|
|
|
|
|
|
onValueChange={(value) => updateFilter(index, { gridColumns: parseInt(value) })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{[1, 2, 3, 4, 6, 12].map((num) => (
|
|
|
|
|
|
<SelectItem key={num} value={num.toString()}>
|
|
|
|
|
|
{num}칸
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 웹 타입별 추가 설정 미리보기 */}
|
|
|
|
|
|
<div className="border-t pt-2">
|
|
|
|
|
|
<div className="text-muted-foreground text-xs">
|
|
|
|
|
|
{filter.widgetType === "date" || filter.widgetType === "datetime" ? (
|
|
|
|
|
|
<span>📅 날짜 범위 선택 (시작일 ~ 종료일)</span>
|
|
|
|
|
|
) : filter.widgetType === "number" || filter.widgetType === "decimal" ? (
|
|
|
|
|
|
<span>🔢 숫자 범위 입력 (최소값 ~ 최대값)</span>
|
|
|
|
|
|
) : filter.widgetType === "select" ? (
|
|
|
|
|
|
<span>📋 다중 선택 옵션</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span>🔍 텍스트 입력 검색</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
|
|
<TabsContent value="modal" className="mt-4 max-h-[70vh] overflow-y-auto">
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<CardHeader>
|
|
|
|
|
|
<CardTitle className="flex items-center space-x-2 text-sm">
|
|
|
|
|
|
<Settings className="h-4 w-4" />
|
|
|
|
|
|
<span>모달 및 페이징 설정</span>
|
|
|
|
|
|
</CardTitle>
|
|
|
|
|
|
</CardHeader>
|
|
|
|
|
|
<CardContent className="space-y-6">
|
|
|
|
|
|
{/* 페이지네이션 설정 */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<h4 className="text-sm font-medium">페이지네이션 설정</h4>
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="pagination-enabled"
|
|
|
|
|
|
checked={localValues.paginationEnabled}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
console.log("🔄 페이지네이션 사용 변경:", checked);
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, paginationEnabled: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
pagination: { ...component.pagination, enabled: checked as boolean },
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="pagination-enabled">페이지네이션 사용</Label>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
{component.pagination.enabled && (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<Label>페이지당 행 수</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={component.pagination.pageSize.toString()}
|
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
pagination: {
|
|
|
|
|
|
...component.pagination,
|
|
|
|
|
|
pageSize: parseInt(value),
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{[5, 10, 20, 50, 100].map((size) => (
|
|
|
|
|
|
<SelectItem key={size} value={size.toString()}>
|
|
|
|
|
|
{size}개
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="show-page-size-selector"
|
|
|
|
|
|
checked={localValues.showPageSizeSelector}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
console.log("🔄 페이지 크기 선택기 표시 변경:", checked);
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, showPageSizeSelector: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
pagination: {
|
|
|
|
|
|
...component.pagination,
|
|
|
|
|
|
showPageSizeSelector: checked as boolean,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="show-page-size-selector" className="text-sm">
|
|
|
|
|
|
페이지 크기 선택기 표시
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
2025-09-03 18:23:47 +09:00
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="show-page-info"
|
|
|
|
|
|
checked={localValues.showPageInfo}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
console.log("🔄 페이지 정보 표시 변경:", checked);
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, showPageInfo: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
pagination: {
|
|
|
|
|
|
...component.pagination,
|
|
|
|
|
|
showPageInfo: checked as boolean,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="show-page-info" className="text-sm">
|
|
|
|
|
|
페이지 정보 표시
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
id="show-first-last"
|
|
|
|
|
|
checked={localValues.showFirstLast}
|
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
|
console.log("🔄 처음/마지막 버튼 표시 변경:", checked);
|
|
|
|
|
|
setLocalValues((prev) => ({ ...prev, showFirstLast: checked as boolean }));
|
|
|
|
|
|
onUpdateComponent({
|
|
|
|
|
|
pagination: {
|
|
|
|
|
|
...component.pagination,
|
|
|
|
|
|
showFirstLast: checked as boolean,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Label htmlFor="show-first-last" className="text-sm">
|
|
|
|
|
|
처음/마지막 버튼 표시
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 모달 설정은 여기에 추가 가능 */}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<h4 className="text-sm font-medium">모달 설정</h4>
|
|
|
|
|
|
<p className="text-xs text-gray-500">추가/수정 모달 관련 설정들이 여기에 표시됩니다.</p>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</div>
|
2025-09-03 18:23:47 +09:00
|
|
|
|
</CardContent>
|
|
|
|
|
|
</Card>
|
2025-09-03 15:23:12 +09:00
|
|
|
|
</TabsContent>
|
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export default DataTableConfigPanel;
|