"use client" import * as React from "react" import { flexRender, type Table as TanstackTable } from "@tanstack/react-table" import { ChevronRight, ChevronUp, ChevronDown } from "lucide-react" import { cn } from "@/lib/utils" import { getCommonPinningStyles } from "@/lib/data-table" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { DataTablePagination } from "@/components/data-table/data-table-pagination" import { DataTableResizer } from "@/components/data-table/data-table-resizer" import { useAutoSizeColumns } from "@/hooks/useAutoSizeColumns" interface ExpandableDataTableProps extends React.HTMLAttributes { table: TanstackTable floatingBar?: React.ReactNode | null autoSizeColumns?: boolean compact?: boolean expandedRows?: Set setExpandedRows?: React.Dispatch>> renderExpandedContent?: (row: TData) => React.ReactNode expandable?: boolean // 확장 기능 활성화 여부 maxHeight?: string | number expandedRowClassName?: string // 확장된 행의 커스텀 클래스 } /** * 확장 가능한 데이터 테이블 - 행 확장 시 바로 아래에 컨텐츠 표시 * 개선사항: * - 가로스크롤과 확장된 내용 독립성 보장 * - 동적 높이 계산으로 세로스크롤 문제 해결 * - 향상된 접근성 및 키보드 네비게이션 */ export function ExpandableDataTable({ table, floatingBar = null, autoSizeColumns = true, compact = false, expandedRows = new Set(), setExpandedRows, renderExpandedContent, expandable = false, maxHeight, expandedRowClassName, children, className, ...props }: ExpandableDataTableProps) { useAutoSizeColumns(table, autoSizeColumns) // 스크롤 컨테이너 참조 const scrollContainerRef = React.useRef(null) const [expandedHeights, setExpandedHeights] = React.useState>(new Map()) // 행 확장/축소 핸들러 (개선된 버전) const toggleRowExpansion = React.useCallback((rowId: string, event?: React.MouseEvent) => { if (!setExpandedRows) return if (event) { event.stopPropagation() } const newExpanded = new Set(expandedRows) if (newExpanded.has(rowId)) { newExpanded.delete(rowId) // 높이 정보도 제거 setExpandedHeights(prev => { const newHeights = new Map(prev) newHeights.delete(rowId) return newHeights }) } else { newExpanded.add(rowId) } setExpandedRows(newExpanded) }, [expandedRows, setExpandedRows]) // 키보드 네비게이션 핸들러 const handleKeyDown = React.useCallback((event: React.KeyboardEvent, rowId: string) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault() toggleRowExpansion(rowId) } }, [toggleRowExpansion]) // 확장된 내용의 높이 측정 및 업데이트 const updateExpandedHeight = React.useCallback((rowId: string, height: number) => { setExpandedHeights(prev => { if (prev.get(rowId) !== height) { const newHeights = new Map(prev) newHeights.set(rowId, height) return newHeights } return prev }) }, []) // 컴팩트 모드를 위한 클래스 정의 (개선된 버전) const compactStyles = compact ? { row: "h-7", cell: "py-1 px-2 text-sm", expandedCell: "py-2 px-4", groupRow: "py-1 bg-muted/20 text-sm", emptyRow: "h-16", } : { row: "", cell: "", expandedCell: "py-0 px-0", // 패딩 제거하여 확장 컨텐츠가 전체 영역 사용 groupRow: "bg-muted/20", emptyRow: "h-24", } // 확장 버튼 렌더링 함수 (접근성 개선) const renderExpandButton = (rowId: string) => { if (!expandable || !setExpandedRows) return null const isExpanded = expandedRows.has(rowId) return ( ) } // 확장된 내용 래퍼 컴포넌트 (높이 측정 기능 포함) const ExpandedContentWrapper = React.memo<{ rowId: string children: React.ReactNode }>(({ rowId, children }) => { const contentRef = React.useRef(null) React.useEffect(() => { if (contentRef.current) { const resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { updateExpandedHeight(rowId, entry.contentRect.height) } }) resizeObserver.observe(contentRef.current) return () => resizeObserver.disconnect() } }, [rowId]) return (
{children}
) }) ExpandedContentWrapper.displayName = "ExpandedContentWrapper" return (
{children} {/* 메인 테이블 컨테이너 - 가로스크롤 문제 해결 */}
{/* 가로스크롤 영역 */}
{/* 세로스크롤 영역 (확장된 내용 포함) */}
{/* 테이블 헤더 */} {table.getHeaderGroups().map((headerGroup) => ( {/* 확장 버튼 컬럼 헤더 */} {expandable && ( )} {headerGroup.headers.map((header) => { if (header.column.getIsGrouped()) { return null } return (
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} {header.column.getCanResize() && ( )}
) })}
))}
{/* 테이블 바디 */} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => { const isExpanded = expandedRows.has(row.id) const expandedHeight = expandedHeights.get(row.id) || 0 // 그룹핑 헤더 Row if (row.getIsGrouped()) { const groupingColumnId = row.groupingColumnId ?? "" const groupingColumn = table.getColumn(groupingColumnId) let columnLabel = groupingColumnId if (groupingColumn) { const headerDef = groupingColumn.columnDef.meta?.excelHeader if (typeof headerDef === "string") { columnLabel = headerDef } } return ( {row.getCanExpand() && ( )} {columnLabel}: {row.getValue(groupingColumnId)} ({row.subRows.length} rows) ) } // 일반 Row와 확장된 컨텐츠를 함께 렌더링 return ( {/* 메인 데이터 행 */} {/* 확장 버튼 셀 */} {expandable && ( {renderExpandButton(row.id)} )} {/* 데이터 셀들 */} {row.getVisibleCells().map((cell) => { if (cell.column.getIsGrouped()) { return null } return ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ) })} {/* 확장된 컨텐츠 행 - 가로스크롤 독립성 보장 */} {isExpanded && renderExpandedContent && ( {/* 확장된 내용을 위한 고정 폭 컨테이너 */}
{/* 가로스크롤과 독립적인 확장 영역 */}
{renderExpandedContent(row.original)}
{/* 높이 유지를 위한 스페이서 */}
)} ) }) ) : ( // 데이터가 없을 때 No results. )}
{/* Pagination */} {/* Floating Bar (선택된 행 있을 때) */} {table.getFilteredSelectedRowModel().rows.length > 0 && floatingBar}
) }