2025-12-19 15:44:38 +09:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-28 17:36:19 +09:00
|
|
|
* V2List 설정 패널
|
2026-03-11 21:29:28 +09:00
|
|
|
* 토스식 단계별 UX: 테이블 정보 표시 -> 기본 옵션(Switch) -> 상세 설정(Collapsible)
|
|
|
|
|
* 컬럼/필터 등 복잡한 설정은 TableListConfigPanel에 위임하여 기능 누락 방지
|
2025-12-19 15:44:38 +09:00
|
|
|
*/
|
|
|
|
|
|
2026-03-11 21:29:28 +09:00
|
|
|
import React, { useState, useMemo } from "react";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
|
|
|
|
import { Switch } from "@/components/ui/switch";
|
|
|
|
|
import {
|
|
|
|
|
Collapsible,
|
|
|
|
|
CollapsibleContent,
|
|
|
|
|
CollapsibleTrigger,
|
|
|
|
|
} from "@/components/ui/collapsible";
|
|
|
|
|
import { Table2, Settings, ChevronDown } from "lucide-react";
|
|
|
|
|
import { cn } from "@/lib/utils";
|
2026-01-15 17:00:21 +09:00
|
|
|
import { TableListConfigPanel } from "@/lib/registry/components/table-list/TableListConfigPanel";
|
|
|
|
|
import { TableListConfig } from "@/lib/registry/components/table-list/types";
|
2025-12-19 15:44:38 +09:00
|
|
|
|
2026-01-28 17:36:19 +09:00
|
|
|
interface V2ListConfigPanelProps {
|
2025-12-19 15:44:38 +09:00
|
|
|
config: Record<string, any>;
|
|
|
|
|
onChange: (config: Record<string, any>) => void;
|
2025-12-23 13:53:22 +09:00
|
|
|
currentTableName?: string;
|
2025-12-19 16:40:40 +09:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 17:36:19 +09:00
|
|
|
export const V2ListConfigPanel: React.FC<V2ListConfigPanelProps> = ({
|
2025-12-19 15:44:38 +09:00
|
|
|
config,
|
|
|
|
|
onChange,
|
2025-12-23 13:53:22 +09:00
|
|
|
currentTableName,
|
2025-12-19 15:44:38 +09:00
|
|
|
}) => {
|
2026-03-11 21:29:28 +09:00
|
|
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
const updateConfig = (field: string, value: any) => {
|
|
|
|
|
onChange({ ...config, [field]: value });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const tableName = config.tableName || config.dataSource?.table || currentTableName || "";
|
|
|
|
|
const columnCount = (config.columns || []).length;
|
|
|
|
|
|
|
|
|
|
// ─── V2List config → TableListConfig 변환 (기존 로직 100% 유지) ───
|
2026-01-15 17:00:21 +09:00
|
|
|
const tableListConfig: TableListConfig = useMemo(() => {
|
|
|
|
|
const columns = (config.columns || []).map((col: any, index: number) => ({
|
|
|
|
|
columnName: col.key || col.columnName || col.field || "",
|
|
|
|
|
displayName: col.title || col.header || col.displayName || col.key || col.columnName || col.field || "",
|
|
|
|
|
width: col.width ? parseInt(col.width, 10) : undefined,
|
|
|
|
|
visible: col.visible !== false,
|
|
|
|
|
sortable: col.sortable !== false,
|
|
|
|
|
searchable: col.searchable !== false,
|
|
|
|
|
align: col.align || "left",
|
|
|
|
|
order: index,
|
|
|
|
|
isEntityJoin: col.isJoinColumn || col.isEntityJoin || false,
|
|
|
|
|
thousandSeparator: col.thousandSeparator,
|
|
|
|
|
editable: col.editable,
|
|
|
|
|
entityDisplayConfig: col.entityDisplayConfig,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
selectedTable: config.tableName || config.dataSource?.table || currentTableName,
|
|
|
|
|
tableName: config.tableName || config.dataSource?.table || currentTableName,
|
|
|
|
|
columns,
|
|
|
|
|
useCustomTable: config.useCustomTable,
|
|
|
|
|
customTableName: config.customTableName,
|
2026-03-11 21:29:28 +09:00
|
|
|
isReadOnly: config.isReadOnly !== false,
|
|
|
|
|
displayMode: "table",
|
2026-03-12 04:25:34 +09:00
|
|
|
showHeader: true,
|
|
|
|
|
showFooter: false,
|
2026-01-15 17:00:21 +09:00
|
|
|
pagination: config.pagination !== false ? {
|
|
|
|
|
enabled: true,
|
|
|
|
|
pageSize: config.pageSize || 10,
|
2026-03-12 04:25:34 +09:00
|
|
|
showSizeSelector: true,
|
|
|
|
|
showPageInfo: true,
|
2026-01-15 17:00:21 +09:00
|
|
|
pageSizeOptions: [5, 10, 20, 50, 100],
|
|
|
|
|
} : {
|
|
|
|
|
enabled: false,
|
|
|
|
|
pageSize: 10,
|
2026-03-12 04:25:34 +09:00
|
|
|
showSizeSelector: false,
|
|
|
|
|
showPageInfo: false,
|
2026-01-15 17:00:21 +09:00
|
|
|
pageSizeOptions: [10],
|
|
|
|
|
},
|
2026-03-12 04:25:34 +09:00
|
|
|
filter: config.filter || { enabled: false, filters: [] },
|
2026-01-15 17:00:21 +09:00
|
|
|
dataFilter: config.dataFilter,
|
2026-03-12 04:25:34 +09:00
|
|
|
actions: config.actions || {
|
|
|
|
|
showActions: false,
|
|
|
|
|
actions: [],
|
|
|
|
|
bulkActions: false,
|
|
|
|
|
bulkActionList: [],
|
|
|
|
|
},
|
|
|
|
|
tableStyle: config.tableStyle || {
|
|
|
|
|
theme: "default",
|
|
|
|
|
headerStyle: "default",
|
|
|
|
|
rowHeight: "normal",
|
|
|
|
|
alternateRows: false,
|
|
|
|
|
hoverEffect: true,
|
|
|
|
|
borderStyle: "light",
|
|
|
|
|
},
|
2026-01-15 17:00:21 +09:00
|
|
|
checkbox: {
|
|
|
|
|
enabled: true,
|
2026-03-12 04:25:34 +09:00
|
|
|
multiple: true,
|
2026-01-15 17:00:21 +09:00
|
|
|
position: "left",
|
2026-03-12 04:25:34 +09:00
|
|
|
selectAll: true,
|
2026-01-15 17:00:21 +09:00
|
|
|
},
|
|
|
|
|
height: "auto",
|
|
|
|
|
autoWidth: true,
|
|
|
|
|
stickyHeader: true,
|
|
|
|
|
autoLoad: true,
|
|
|
|
|
horizontalScroll: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
minColumnWidth: 100,
|
|
|
|
|
maxColumnWidth: 300,
|
|
|
|
|
},
|
2026-03-12 04:25:34 +09:00
|
|
|
toolbar: config.toolbar,
|
|
|
|
|
linkedFilters: config.linkedFilters,
|
|
|
|
|
excludeFilter: config.excludeFilter,
|
|
|
|
|
defaultSort: config.defaultSort,
|
2026-01-15 17:00:21 +09:00
|
|
|
};
|
|
|
|
|
}, [config, currentTableName]);
|
2025-12-19 16:40:40 +09:00
|
|
|
|
2026-03-11 21:29:28 +09:00
|
|
|
// ─── TableListConfig 변경 → V2List config 변환 (기존 로직 100% 유지) ───
|
2026-01-15 17:00:21 +09:00
|
|
|
const handleConfigChange = (partialConfig: Partial<TableListConfig>) => {
|
|
|
|
|
const newConfig: Record<string, any> = { ...config };
|
2025-12-24 13:54:24 +09:00
|
|
|
|
2026-01-15 17:00:21 +09:00
|
|
|
if (partialConfig.selectedTable !== undefined) {
|
|
|
|
|
newConfig.tableName = partialConfig.selectedTable;
|
2026-03-11 21:29:28 +09:00
|
|
|
if (!newConfig.dataSource) newConfig.dataSource = {};
|
2026-01-15 17:00:21 +09:00
|
|
|
newConfig.dataSource.table = partialConfig.selectedTable;
|
|
|
|
|
}
|
|
|
|
|
if (partialConfig.tableName !== undefined) {
|
|
|
|
|
newConfig.tableName = partialConfig.tableName;
|
2026-03-11 21:29:28 +09:00
|
|
|
if (!newConfig.dataSource) newConfig.dataSource = {};
|
2026-01-15 17:00:21 +09:00
|
|
|
newConfig.dataSource.table = partialConfig.tableName;
|
|
|
|
|
}
|
|
|
|
|
if (partialConfig.useCustomTable !== undefined) {
|
|
|
|
|
newConfig.useCustomTable = partialConfig.useCustomTable;
|
2026-03-11 21:29:28 +09:00
|
|
|
}
|
2026-01-15 17:00:21 +09:00
|
|
|
if (partialConfig.customTableName !== undefined) {
|
|
|
|
|
newConfig.customTableName = partialConfig.customTableName;
|
|
|
|
|
}
|
|
|
|
|
if (partialConfig.isReadOnly !== undefined) {
|
|
|
|
|
newConfig.isReadOnly = partialConfig.isReadOnly;
|
2025-12-23 13:53:22 +09:00
|
|
|
}
|
2026-01-05 15:35:19 +09:00
|
|
|
|
2026-01-15 17:00:21 +09:00
|
|
|
if (partialConfig.columns !== undefined) {
|
|
|
|
|
newConfig.columns = partialConfig.columns.map((col: any) => ({
|
|
|
|
|
key: col.columnName,
|
|
|
|
|
field: col.columnName,
|
|
|
|
|
title: col.displayName,
|
|
|
|
|
header: col.displayName,
|
|
|
|
|
width: col.width ? String(col.width) : undefined,
|
|
|
|
|
visible: col.visible,
|
|
|
|
|
sortable: col.sortable,
|
|
|
|
|
searchable: col.searchable,
|
|
|
|
|
align: col.align,
|
|
|
|
|
isJoinColumn: col.isEntityJoin,
|
|
|
|
|
isEntityJoin: col.isEntityJoin,
|
|
|
|
|
thousandSeparator: col.thousandSeparator,
|
|
|
|
|
editable: col.editable,
|
|
|
|
|
entityDisplayConfig: col.entityDisplayConfig,
|
|
|
|
|
}));
|
|
|
|
|
}
|
2026-01-05 15:35:19 +09:00
|
|
|
|
2026-01-15 17:00:21 +09:00
|
|
|
if (partialConfig.pagination !== undefined) {
|
|
|
|
|
newConfig.pagination = partialConfig.pagination?.enabled;
|
|
|
|
|
newConfig.pageSize = partialConfig.pagination?.pageSize || 10;
|
|
|
|
|
}
|
2025-12-19 15:44:38 +09:00
|
|
|
|
2026-01-15 17:00:21 +09:00
|
|
|
if (partialConfig.filter !== undefined) {
|
|
|
|
|
newConfig.filter = partialConfig.filter;
|
|
|
|
|
}
|
2025-12-23 13:53:22 +09:00
|
|
|
|
2026-01-15 17:00:21 +09:00
|
|
|
if (partialConfig.dataFilter !== undefined) {
|
|
|
|
|
newConfig.dataFilter = partialConfig.dataFilter;
|
|
|
|
|
}
|
2025-12-23 13:53:22 +09:00
|
|
|
|
2026-03-12 04:25:34 +09:00
|
|
|
if (partialConfig.actions !== undefined) {
|
|
|
|
|
newConfig.actions = partialConfig.actions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (partialConfig.tableStyle !== undefined) {
|
|
|
|
|
newConfig.tableStyle = partialConfig.tableStyle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (partialConfig.toolbar !== undefined) {
|
|
|
|
|
newConfig.toolbar = partialConfig.toolbar;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (partialConfig.linkedFilters !== undefined) {
|
|
|
|
|
newConfig.linkedFilters = partialConfig.linkedFilters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (partialConfig.excludeFilter !== undefined) {
|
|
|
|
|
newConfig.excludeFilter = partialConfig.excludeFilter;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (partialConfig.defaultSort !== undefined) {
|
|
|
|
|
newConfig.defaultSort = partialConfig.defaultSort;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 17:00:21 +09:00
|
|
|
onChange(newConfig);
|
2025-12-23 13:53:22 +09:00
|
|
|
};
|
|
|
|
|
|
2025-12-19 15:44:38 +09:00
|
|
|
return (
|
2026-03-11 21:29:28 +09:00
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* ─── 1단계: 테이블 정보 ─── */}
|
|
|
|
|
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Table2 className="h-4 w-4 text-primary" />
|
|
|
|
|
<span className="text-sm font-medium">데이터 소스</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{tableName ? (
|
|
|
|
|
<div className="rounded-md border bg-background p-3">
|
|
|
|
|
<p className="text-xs text-muted-foreground">연결된 테이블</p>
|
|
|
|
|
<p className="mt-0.5 text-sm font-medium">{tableName}</p>
|
|
|
|
|
{columnCount > 0 && (
|
|
|
|
|
<p className="mt-1 text-[11px] text-muted-foreground">
|
|
|
|
|
{columnCount}개의 컬럼이 설정되어 있어요
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="rounded-md border-2 border-dashed p-4 text-center">
|
|
|
|
|
<Table2 className="mx-auto mb-2 h-8 w-8 opacity-30 text-muted-foreground" />
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
아직 테이블이 연결되지 않았어요
|
|
|
|
|
</p>
|
|
|
|
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
|
|
|
아래 상세 설정에서 테이블을 선택해주세요
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ─── 2단계: 기본 옵션 (Switch + 설명) ─── */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm">읽기 전용</p>
|
|
|
|
|
<p className="text-[11px] text-muted-foreground">
|
|
|
|
|
데이터 조회만 가능하고 수정할 수 없어요
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={config.isReadOnly !== false}
|
|
|
|
|
onCheckedChange={(checked) => updateConfig("isReadOnly", checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm">페이지네이션</p>
|
|
|
|
|
<p className="text-[11px] text-muted-foreground">
|
|
|
|
|
데이터를 페이지 단위로 나눠서 보여줘요
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={config.pagination !== false}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
updateConfig("pagination", checked);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{config.pagination !== false && (
|
|
|
|
|
<div className="ml-4 border-l-2 border-primary/20 pl-3">
|
|
|
|
|
<div className="flex items-center justify-between py-1">
|
|
|
|
|
<span className="text-xs text-muted-foreground">페이지당 행 수</span>
|
|
|
|
|
<Select
|
|
|
|
|
value={String(config.pageSize || 10)}
|
|
|
|
|
onValueChange={(v) => updateConfig("pageSize", Number(v))}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-8 w-[180px] text-sm">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="5">5개</SelectItem>
|
|
|
|
|
<SelectItem value="10">10개</SelectItem>
|
|
|
|
|
<SelectItem value="20">20개</SelectItem>
|
|
|
|
|
<SelectItem value="50">50개</SelectItem>
|
|
|
|
|
<SelectItem value="100">100개</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ─── 3단계: 상세 설정 (컬럼, 필터, 테이블 선택 등) ─── */}
|
|
|
|
|
<Collapsible open={detailOpen} onOpenChange={setDetailOpen}>
|
|
|
|
|
<CollapsibleTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="flex w-full items-center justify-between rounded-lg border bg-muted/30 px-4 py-2.5 text-left transition-colors hover:bg-muted/50"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Settings className="h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<span className="text-sm font-medium">컬럼 및 상세 설정</span>
|
|
|
|
|
</div>
|
|
|
|
|
<ChevronDown
|
|
|
|
|
className={cn(
|
|
|
|
|
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
|
|
|
|
detailOpen && "rotate-180",
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</button>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<CollapsibleContent>
|
|
|
|
|
<div className="rounded-b-lg border border-t-0 p-2">
|
|
|
|
|
<p className="text-xs text-muted-foreground px-2 pb-2">
|
|
|
|
|
테이블 선택, 컬럼 구성, 필터 조건 등을 설정할 수 있어요
|
|
|
|
|
</p>
|
|
|
|
|
<TableListConfigPanel
|
|
|
|
|
config={tableListConfig}
|
|
|
|
|
onChange={handleConfigChange}
|
|
|
|
|
screenTableName={currentTableName}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
</div>
|
2025-12-19 15:44:38 +09:00
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-28 17:36:19 +09:00
|
|
|
V2ListConfigPanel.displayName = "V2ListConfigPanel";
|
2025-12-19 15:44:38 +09:00
|
|
|
|
2026-01-28 17:36:19 +09:00
|
|
|
export default V2ListConfigPanel;
|