"use client"; import React, { useCallback, useEffect, useState, useMemo } from "react"; import { Loader2, FoldVertical, UnfoldVertical } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { TableGroupedComponentProps } from "./types"; import { useGroupedData } from "./hooks/useGroupedData"; import { GroupHeader } from "./components/GroupHeader"; import { useScreenContextOptional } from "@/contexts/ScreenContext"; import { useTableOptions } from "@/contexts/TableOptionsContext"; import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer"; /** * v2-table-grouped 메인 컴포넌트 * * 데이터를 특정 컬럼 기준으로 그룹화하여 접기/펼치기 기능을 제공합니다. */ export function TableGroupedComponent({ config, isDesignMode = false, formData, onSelectionChange, onGroupToggle, onRowClick, externalData, isLoading: externalLoading, error: externalError, componentId, }: TableGroupedComponentProps) { // 화면 컨텍스트 (데이터 제공자로 등록) const screenContext = useScreenContextOptional(); // TableOptions Context (검색필터 연동) const { registerTable, unregisterTable, updateTableDataCount } = useTableOptions(); // 연결된 필터 상태 (다른 컴포넌트 값으로 필터링) const [linkedFilterValues, setLinkedFilterValues] = useState>({}); // 필터 및 그룹 설정 상태 (검색필터 연동용) const [filters, setFilters] = useState([]); const [grouping, setGrouping] = useState([]); const [columnVisibility, setColumnVisibility] = useState([]); // 그룹화 데이터 훅 (검색 필터 전달) const { groups, isLoading: hookLoading, error: hookError, toggleGroup, expandAll, collapseAll, toggleItemSelection, toggleGroupSelection, toggleAllSelection, selectedItems, isAllSelected, isIndeterminate, refresh, rawData, totalCount, groupCount, } = useGroupedData(config, externalData, linkedFilterValues); const isLoading = externalLoading ?? hookLoading; const error = externalError ?? hookError; // 필터링된 데이터 (훅에서 이미 필터 적용됨) const filteredData = rawData; // 연결된 필터 감시 useEffect(() => { const linkedFilters = config.linkedFilters; if (!linkedFilters || linkedFilters.length === 0 || !screenContext) { return; } // 연결된 소스 컴포넌트들의 값을 주기적으로 확인 const checkLinkedFilters = () => { const newFilterValues: Record = {}; let hasChanges = false; linkedFilters.forEach((filter) => { if (filter.enabled === false) return; const sourceProvider = screenContext.getDataProvider(filter.sourceComponentId); if (sourceProvider) { const selectedData = sourceProvider.getSelectedData(); if (selectedData && selectedData.length > 0) { const sourceField = filter.sourceField || "value"; const value = selectedData[0][sourceField]; if (value !== linkedFilterValues[filter.targetColumn]) { newFilterValues[filter.targetColumn] = value; hasChanges = true; } else { newFilterValues[filter.targetColumn] = linkedFilterValues[filter.targetColumn]; } } } }); if (hasChanges) { setLinkedFilterValues(newFilterValues); } }; // 초기 확인 checkLinkedFilters(); // 주기적 확인 (100ms 간격) const intervalId = setInterval(checkLinkedFilters, 100); return () => { clearInterval(intervalId); }; }, [screenContext, config.linkedFilters, linkedFilterValues]); // DataProvidable 인터페이스 구현 const dataProvider: DataProvidable = useMemo( () => ({ componentId: componentId || "", componentType: "table-grouped", getSelectedData: () => { return selectedItems; }, getAllData: () => { return filteredData; }, clearSelection: () => { toggleAllSelection(); }, }), [componentId, selectedItems, filteredData, toggleAllSelection] ); // DataReceivable 인터페이스 구현 const dataReceiver: DataReceivable = useMemo( () => ({ componentId: componentId || "", componentType: "table-grouped", receiveData: async (_receivedData: any[], _config: DataReceiverConfig) => { // 현재는 외부 데이터 수신 시 새로고침만 수행 refresh(); }, clearData: async () => { // 데이터 클리어 시 새로고침 refresh(); }, getConfig: () => { return { targetComponentId: componentId || "", mode: "replace" as const, }; }, }), [componentId, refresh] ); // 화면 컨텍스트에 데이터 제공자/수신자로 등록 useEffect(() => { if (screenContext && componentId) { screenContext.registerDataProvider(componentId, dataProvider); screenContext.registerDataReceiver(componentId, dataReceiver); return () => { screenContext.unregisterDataProvider(componentId); screenContext.unregisterDataReceiver(componentId); }; } }, [screenContext, componentId, dataProvider, dataReceiver]); // 테이블 ID (검색필터 연동용) const tableId = componentId || `table-grouped-${config.selectedTable || "default"}`; // TableOptionsContext에 테이블 등록 (검색필터가 테이블을 찾을 수 있도록) useEffect(() => { if (isDesignMode || !config.selectedTable) return; const columnsToRegister = config.columns || []; // 고유 값 조회 함수 const getColumnUniqueValues = async (columnName: string) => { const uniqueValues = new Set(); rawData.forEach((row) => { const value = row[columnName]; if (value !== null && value !== undefined && value !== "") { uniqueValues.add(String(value)); } }); return Array.from(uniqueValues) .map((value) => ({ value, label: value })) .sort((a, b) => a.label.localeCompare(b.label)); }; const registration = { tableId, label: config.selectedTable, tableName: config.selectedTable, dataCount: totalCount, columns: columnsToRegister.map((col) => ({ columnName: col.columnName, columnLabel: col.displayName || col.columnName, inputType: "text", visible: col.visible !== false, width: col.width || 150, sortable: true, filterable: true, })), onFilterChange: setFilters, onGroupChange: setGrouping, onColumnVisibilityChange: setColumnVisibility, getColumnUniqueValues, }; registerTable(registration); return () => { unregisterTable(tableId); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [tableId, config.selectedTable, config.columns, totalCount, rawData, registerTable, isDesignMode]); // 데이터 건수 변경 시 업데이트 useEffect(() => { if (!isDesignMode && config.selectedTable) { updateTableDataCount(tableId, totalCount); } }, [tableId, totalCount, updateTableDataCount, config.selectedTable, isDesignMode]); // 필터 변경 시 검색 조건 적용 useEffect(() => { if (filters.length > 0) { const newFilterValues: Record = {}; filters.forEach((filter: any) => { if (filter.value) { newFilterValues[filter.columnName] = filter.value; } }); setLinkedFilterValues((prev) => ({ ...prev, ...newFilterValues })); } }, [filters]); // 컬럼 설정 const columns = config.columns || []; const visibleColumns = columns.filter((col) => col.visible !== false); // 체크박스 컬럼 포함 시 총 컬럼 수 const totalColumnCount = visibleColumns.length + (config.showCheckbox ? 1 : 0); // 아이템 ID 추출 함수 const getItemId = useCallback( (item: any): string => { if (item.id !== undefined) return String(item.id); const firstCol = columns[0]?.columnName; if (firstCol && item[firstCol] !== undefined) return String(item[firstCol]); return JSON.stringify(item); }, [columns] ); // 선택 변경 시 콜백 useEffect(() => { if (onSelectionChange && selectedItems.length >= 0) { onSelectionChange({ selectedGroups: groups .filter((g) => g.selected) .map((g) => g.groupKey), selectedItems, isAllSelected, }); } }, [selectedItems, groups, isAllSelected, onSelectionChange]); // 그룹 토글 핸들러 const handleGroupToggle = useCallback( (groupKey: string) => { toggleGroup(groupKey); if (onGroupToggle) { const group = groups.find((g) => g.groupKey === groupKey); onGroupToggle({ groupKey, expanded: !group?.expanded, }); } }, [toggleGroup, onGroupToggle, groups] ); // 행 클릭 핸들러 const handleRowClick = useCallback( (row: any, groupKey: string, indexInGroup: number) => { if (!config.rowClickable) return; if (onRowClick) { onRowClick({ row, groupKey, indexInGroup }); } }, [config.rowClickable, onRowClick] ); // refreshTable 이벤트 구독 useEffect(() => { const handleRefresh = () => { refresh(); }; window.addEventListener("refreshTable", handleRefresh); return () => { window.removeEventListener("refreshTable", handleRefresh); }; }, [refresh]); // 디자인 모드 렌더링 if (isDesignMode) { return (
그룹화 테이블 {config.groupConfig?.groupByColumn && ( (그룹: {config.groupConfig.groupByColumn}) )}
테이블: {config.useCustomTable ? config.customTableName : config.selectedTable || "(미설정)"}
); } // 로딩 상태 if (isLoading) { return (
로딩 중...
); } // 에러 상태 if (error) { return (
{error}
); } // 데이터 없음 if (groups.length === 0) { return (
{config.emptyMessage || "데이터가 없습니다."}
); } return (
{/* 툴바 */} {config.showExpandAllButton && (
{groupCount}개 그룹 | 총 {totalCount}건
)} {/* 테이블 */}
{/* 테이블 헤더 */} {/* 전체 선택 체크박스 */} {config.showCheckbox && ( )} {/* 컬럼 헤더 */} {visibleColumns.map((col) => ( ))} {/* 테이블 바디 */} {groups.map((group) => ( {/* 그룹 헤더 */} handleGroupToggle(group.groupKey)} onSelectToggle={ config.showCheckbox ? () => toggleGroupSelection(group.groupKey) : undefined } style={config.groupHeaderStyle} columnCount={totalColumnCount} /> {/* 그룹 아이템 (펼쳐진 경우만) */} {group.expanded && group.items.map((item, idx) => { const itemId = getItemId(item); const isSelected = group.selectedItemIds?.includes(itemId); return ( handleRowClick(item, group.groupKey, idx)} > {/* 체크박스 */} {config.showCheckbox && ( )} {/* 데이터 컬럼 */} {visibleColumns.map((col) => { const value = item[col.columnName]; let displayValue: React.ReactNode = value; // 포맷 적용 if (col.format === "number" && typeof value === "number") { displayValue = value.toLocaleString(); } else if (col.format === "currency" && typeof value === "number") { displayValue = `₩${value.toLocaleString()}`; } else if (col.format === "date" && value) { displayValue = new Date(value).toLocaleDateString("ko-KR"); } else if (col.format === "boolean") { displayValue = value ? "예" : "아니오"; } return ( ); })} ); })} ))}
{col.displayName || col.columnName}
e.stopPropagation()} > toggleItemSelection(group.groupKey, itemId) } /> {displayValue ?? "-"}
); } export default TableGroupedComponent;