"use client";
/**
* V2TableGrouped 설정 패널
* 토스식 단계별 UX: 데이터 소스 -> 그룹화 설정 -> 컬럼 선택 -> 표시 설정(접힘) -> 연동 설정(접힘)
* 기존 TableGroupedConfigPanel의 모든 기능을 자체 UI로 완전 구현
*/
import React, { useState, useEffect, useCallback, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Separator } from "@/components/ui/separator";
import {
Table2,
Database,
Layers,
Columns3,
Check,
ChevronsUpDown,
Settings,
ChevronDown,
Loader2,
Link2,
Plus,
Trash2,
FoldVertical,
ArrowUpDown,
CheckSquare,
LayoutGrid,
Type,
Hash,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { tableTypeApi } from "@/lib/api/screen";
import type { TableGroupedConfig, LinkedFilterConfig } from "@/lib/registry/components/v2-table-grouped/types";
import type { ColumnConfig } from "@/lib/registry/components/v2-table-list/types";
import {
groupHeaderStyleOptions,
checkboxModeOptions,
sortDirectionOptions,
} from "@/lib/registry/components/v2-table-grouped/config";
// ─── 섹션 헤더 컴포넌트 ───
function SectionHeader({ icon: Icon, title, description }: {
icon: React.ComponentType<{ className?: string }>;
title: string;
description?: string;
}) {
return (
{title}
{description &&
{description}
}
);
}
// ─── 수평 Switch Row (토스 패턴) ───
function SwitchRow({ label, description, checked, onCheckedChange }: {
label: string;
description?: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
}) {
return (
{label}
{description &&
{description}
}
);
}
// ─── 수평 라벨 + 컨트롤 Row ───
function LabeledRow({ label, description, children }: {
label: string;
description?: string;
children: React.ReactNode;
}) {
return (
{label}
{description &&
{description}
}
{children}
);
}
// ─── 그룹 헤더 스타일 카드 ───
const HEADER_STYLE_CARDS = [
{ value: "default", icon: LayoutGrid, title: "기본", description: "표준 그룹 헤더" },
{ value: "compact", icon: FoldVertical, title: "컴팩트", description: "간결한 헤더" },
{ value: "card", icon: Layers, title: "카드", description: "카드 스타일 헤더" },
] as const;
interface V2TableGroupedConfigPanelProps {
config: TableGroupedConfig;
onChange: (newConfig: Partial) => void;
}
export const V2TableGroupedConfigPanel: React.FC = ({
config,
onChange,
}) => {
// componentConfigChanged 이벤트 발행 래퍼
const handleChange = useCallback((newConfig: Partial) => {
onChange(newConfig);
if (typeof window !== "undefined") {
window.dispatchEvent(
new CustomEvent("componentConfigChanged", {
detail: { config: { ...config, ...newConfig } },
})
);
}
}, [onChange, config]);
const updateConfig = useCallback((updates: Partial) => {
handleChange({ ...config, ...updates });
}, [handleChange, config]);
const updateGroupConfig = useCallback((updates: Partial) => {
handleChange({
...config,
groupConfig: { ...config.groupConfig, ...updates },
});
}, [handleChange, config]);
// ─── 상태 ───
const [tables, setTables] = useState>([]);
const [tableColumns, setTableColumns] = useState([]);
const [loadingTables, setLoadingTables] = useState(false);
const [loadingColumns, setLoadingColumns] = useState(false);
const [tableComboboxOpen, setTableComboboxOpen] = useState(false);
// Collapsible 상태
const [displayOpen, setDisplayOpen] = useState(false);
const [linkedOpen, setLinkedOpen] = useState(false);
// ─── 실제 사용할 테이블 이름 ───
const targetTableName = useMemo(() => {
if (config.useCustomTable && config.customTableName) {
return config.customTableName;
}
return config.selectedTable;
}, [config.useCustomTable, config.customTableName, config.selectedTable]);
// ─── 테이블 목록 로드 ───
useEffect(() => {
const loadTables = async () => {
setLoadingTables(true);
try {
const tableList = await tableTypeApi.getTables();
if (tableList && Array.isArray(tableList)) {
setTables(
tableList.map((t: any) => ({
tableName: t.tableName || t.table_name,
displayName: t.displayName || t.display_name || t.tableName || t.table_name,
}))
);
}
} catch (err) {
console.error("테이블 목록 로드 실패:", err);
} finally {
setLoadingTables(false);
}
};
loadTables();
}, []);
// ─── 선택된 테이블의 컬럼 로드 ───
useEffect(() => {
if (!targetTableName) {
setTableColumns([]);
return;
}
const loadColumns = async () => {
setLoadingColumns(true);
try {
const columns = await tableTypeApi.getColumns(targetTableName);
if (columns && Array.isArray(columns)) {
const cols: ColumnConfig[] = columns.map((col: any, idx: number) => ({
columnName: col.column_name || col.columnName,
displayName: col.display_name || col.displayName || col.column_name || col.columnName,
visible: true,
sortable: true,
searchable: false,
align: "left" as const,
order: idx,
}));
setTableColumns(cols);
if (!config.columns || config.columns.length === 0) {
updateConfig({ columns: cols });
}
}
} catch (err) {
console.error("컬럼 로드 실패:", err);
} finally {
setLoadingColumns(false);
}
};
loadColumns();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [targetTableName]);
// ─── 테이블 변경 핸들러 ───
const handleTableChange = useCallback((newTableName: string) => {
if (newTableName === config.selectedTable) return;
updateConfig({ selectedTable: newTableName, columns: [] });
setTableComboboxOpen(false);
}, [config.selectedTable, updateConfig]);
// ─── 컬럼 가시성 토글 ───
const toggleColumnVisibility = useCallback((columnName: string) => {
const updatedColumns = (config.columns || []).map((col) =>
col.columnName === columnName ? { ...col, visible: !col.visible } : col
);
updateConfig({ columns: updatedColumns });
}, [config.columns, updateConfig]);
// ─── 합계 컬럼 토글 ───
const toggleSumColumn = useCallback((columnName: string) => {
const currentSumCols = config.groupConfig?.summary?.sumColumns || [];
const newSumCols = currentSumCols.includes(columnName)
? currentSumCols.filter((c) => c !== columnName)
: [...currentSumCols, columnName];
updateGroupConfig({
summary: {
...config.groupConfig?.summary,
sumColumns: newSumCols,
},
});
}, [config.groupConfig?.summary, updateGroupConfig]);
// ─── 연결 필터 관리 ───
const addLinkedFilter = useCallback(() => {
const newFilter: LinkedFilterConfig = {
sourceComponentId: "",
sourceField: "value",
targetColumn: "",
enabled: true,
};
updateConfig({
linkedFilters: [...(config.linkedFilters || []), newFilter],
});
}, [config.linkedFilters, updateConfig]);
const removeLinkedFilter = useCallback((index: number) => {
const filters = [...(config.linkedFilters || [])];
filters.splice(index, 1);
updateConfig({ linkedFilters: filters });
}, [config.linkedFilters, updateConfig]);
const updateLinkedFilter = useCallback((index: number, updates: Partial) => {
const filters = [...(config.linkedFilters || [])];
filters[index] = { ...filters[index], ...updates };
updateConfig({ linkedFilters: filters });
}, [config.linkedFilters, updateConfig]);
// ─── 렌더링 ───
return (
{/* ═══════════════════════════════════════ */}
{/* 1단계: 데이터 소스 (테이블 선택) */}
{/* ═══════════════════════════════════════ */}
{/* ═══════════════════════════════════════ */}
{/* 2단계: 그룹화 설정 */}
{/* ═══════════════════════════════════════ */}
{targetTableName && (
{/* 그룹화 기준 컬럼 */}
{/* 그룹 라벨 형식 */}
그룹 라벨 형식
updateGroupConfig({ groupLabelFormat: e.target.value })}
placeholder="{value} ({컬럼명})"
className="h-7 text-xs"
/>
{"{value}"} = 그룹값, {"{컬럼명}"} = 해당 컬럼 값
updateGroupConfig({ defaultExpanded: checked })}
/>
{/* 그룹 정렬 */}
updateGroupConfig({
summary: { ...config.groupConfig?.summary, showCount: checked },
})
}
/>
{/* 합계 컬럼 */}
{tableColumns.length > 0 && (
합계 표시 컬럼
그룹별 합계를 계산할 컬럼을 선택하세요
{tableColumns.map((col) => {
const isChecked = config.groupConfig?.summary?.sumColumns?.includes(col.columnName) ?? false;
return (
toggleSumColumn(col.columnName)}
>
toggleSumColumn(col.columnName)}
className="pointer-events-none h-3.5 w-3.5"
/>
{col.displayName || col.columnName}
);
})}
)}
)}
{/* 테이블 미선택 안내 */}
{!targetTableName && (
테이블이 선택되지 않았습니다
위 데이터 소스에서 테이블을 선택하세요
)}
{/* ═══════════════════════════════════════ */}
{/* 3단계: 컬럼 선택 */}
{/* ═══════════════════════════════════════ */}
{targetTableName && (config.columns || tableColumns).length > 0 && (
c.visible !== false).length}개 표시)`}
description="표시할 컬럼을 선택하세요"
/>
{(config.columns || tableColumns).map((col) => {
const isVisible = col.visible !== false;
return (
toggleColumnVisibility(col.columnName)}
>
toggleColumnVisibility(col.columnName)}
className="pointer-events-none h-3.5 w-3.5"
/>
{col.displayName || col.columnName}
);
})}
)}
{/* ═══════════════════════════════════════ */}
{/* 4단계: 그룹 헤더 스타일 (카드 선택) */}
{/* ═══════════════════════════════════════ */}
{targetTableName && (
{HEADER_STYLE_CARDS.map((card) => {
const Icon = card.icon;
const isSelected = (config.groupHeaderStyle || "default") === card.value;
return (
);
})}
)}
{/* ═══════════════════════════════════════ */}
{/* 5단계: 표시 설정 (기본 접힘) */}
{/* ═══════════════════════════════════════ */}
{/* ═══════════════════════════════════════ */}
{/* 6단계: 연동 설정 (기본 접힘) */}
{/* ═══════════════════════════════════════ */}
다른 컴포넌트(검색필터 등)의 선택 값으로 이 테이블을 필터링합니다
{(config.linkedFilters || []).length === 0 ? (
) : (
{(config.linkedFilters || []).map((filter, idx) => (
))}
)}
);
};
V2TableGroupedConfigPanel.displayName = "V2TableGroupedConfigPanel";
export default V2TableGroupedConfigPanel;