"use client" import * as React from "react" import { ColumnDef, ColumnFiltersState, SortingState, VisibilityState, flexRender, getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, Table, getGroupedRowModel, getExpandedRowModel, ColumnSizingState, ColumnPinningState } from "@tanstack/react-table" import { Table as UiTable, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { getCommonPinningStylesWithBorder } from "@/lib/data-table" import { ChevronRight, ChevronUp } from "lucide-react" import { ClientDataTableAdvancedToolbar } from "./data-table-toolbar" import { ClientDataTablePagination } from "./data-table-pagination" import { DataTableResizer } from "./data-table-resizer" import { useAutoSizeColumns } from "@/hooks/useAutoSizeColumns" interface DataTableProps { columns: ColumnDef[] data: TData[] advancedFilterFields: any[] autoSizeColumns?: boolean compact?: boolean // compact 모드 추가 onSelectedRowsChange?: (selected: TData[]) => void maxHeight?: string | number /** 추가로 표시할 버튼/컴포넌트 */ children?: React.ReactNode /** 선택 상태 초기화 트리거 */ clearSelection?: boolean initialColumnPinning?: ColumnPinningState // 추가 } export function ClientDataTable({ columns, data, advancedFilterFields, autoSizeColumns = true, compact = true, // 기본값 true children, maxHeight, onSelectedRowsChange, clearSelection, initialColumnPinning }: DataTableProps) { // (1) React Table 상태 const [rowSelection, setRowSelection] = React.useState({}) const [columnVisibility, setColumnVisibility] = React.useState({}) const [columnFilters, setColumnFilters] = React.useState([]) const [sorting, setSorting] = React.useState([]) const [grouping, setGrouping] = React.useState([]) const [columnSizing, setColumnSizing] = React.useState({}) const [columnPinning, setColumnPinning] = React.useState( initialColumnPinning || { left: ["select","TAG_NO", "TAG_DESC", "status"], right: ["update", 'actions'], } ) // 🎯 스크롤 상태 감지 추가 const [isScrolled, setIsScrolled] = React.useState(false) const table = useReactTable({ data, columns, state: { sorting, columnVisibility, rowSelection, columnFilters, grouping, columnSizing, columnPinning }, columnResizeMode: "onChange", onColumnSizingChange: setColumnSizing, enableRowSelection: true, onRowSelectionChange: setRowSelection, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, onGroupingChange: setGrouping, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), getGroupedRowModel: getGroupedRowModel(), autoResetPageIndex: false, getExpandedRowModel: getExpandedRowModel(), enableColumnPinning: true, onColumnPinningChange: setColumnPinning }) useAutoSizeColumns(table, autoSizeColumns) React.useEffect(() => { if (!onSelectedRowsChange) return const selectedRows = table .getSelectedRowModel() .flatRows.map((row) => row.original) onSelectedRowsChange(selectedRows) }, [rowSelection, table, onSelectedRowsChange]) // clearSelection prop이 변경되면 선택 상태 초기화 React.useEffect(() => { setRowSelection({}) }, [clearSelection]) // 🎯 스크롤 핸들러 추가 const handleScroll = (e: React.UIEvent) => { const scrollLeft = e.currentTarget.scrollLeft setIsScrolled(scrollLeft > 0) } // 🎯 동적 핀 스타일 함수 (width 중복 제거) const getPinnedStyle = (column: any, isHeader: boolean = false) => { const baseStyle = getCommonPinningStylesWithBorder({ column }) const pinnedSide = column.getIsPinned() // width를 제외한 나머지 스타일만 반환 const { width, ...restBaseStyle } = baseStyle return { ...restBaseStyle, // 헤더는 핀 여부와 관계없이 항상 배경 유지 (sticky로 고정되어 있기 때문) ...(isHeader && { background: "hsl(var(--background))", transition: "none", }), // 바디 셀 처리: 왼쪽과 오른쪽을 구분 ...(!isHeader && pinnedSide && { background: pinnedSide === "right" ? "hsl(var(--background))" // 오른쪽 고정은 항상 불투명 : isScrolled ? "hsl(var(--background))" // 왼쪽 고정은 스크롤 시에만 불투명 : "transparent", transition: "background-color 0.15s ease-out", }), } } // 🎯 테이블 총 너비 계산 const getTableWidth = React.useCallback(() => { const totalSize = table.getCenterTotalSize() + table.getLeftTotalSize() + table.getRightTotalSize() return Math.max(totalSize, 800) // 최소 800px 보장 }, [table]) // 컴팩트 모드를 위한 클래스 정의 const compactStyles = compact ? { row: "h-7", // 행 높이 축소 cell: "py-1 px-2 text-sm", // 셀 패딩 축소 및 폰트 크기 조정 header: "py-1 px-2 text-sm", // 헤더 패딩 축소 headerRow: "h-8", // 헤더 행 높이 축소 groupRow: "py-1 bg-muted/20 text-sm", // 그룹 행 패딩 축소 emptyRow: "h-16", // 데이터 없을 때 행 높이 조정 } : { row: "", cell: "", header: "", headerRow: "", groupRow: "bg-muted/20", emptyRow: "h-24", } // (2) 렌더 return (
{/* 툴바에 children을 넘기기 */} {children}
{/* table-fixed 제거: nested header에서 동적 width 변경을 위해 유연한 레이아웃 사용 */} {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음 if (header.column.getIsGrouped()) { return null } return (
{header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} {/* 부모 그룹 헤더는 리사이즈 불가, 자식 헤더만 리사이즈 가능 */} {header.column.getCanResize() && !('columns' in header.column.columnDef) && ( )}
) })}
))}
{table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => { // --------------------------------------------------- // 1) "그룹핑 헤더" Row인지 확인 // --------------------------------------------------- if (row.getIsGrouped()) { // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음 const groupingColumnId = row.groupingColumnId ?? "" const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체 // 컬럼 라벨 가져오기 let columnLabel = groupingColumnId if (groupingColumn) { const headerDef = groupingColumn.columnDef.meta?.excelHeader if (typeof headerDef === "string") { columnLabel = headerDef } } return ( {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */} {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */} {row.getCanExpand() && ( )} {/* Group Label + 값 */} {columnLabel}: {row.getValue(groupingColumnId)} ({row.subRows.length} rows) ) } // --------------------------------------------------- // 2) 일반 Row // → "그룹핑된 컬럼"은 숨긴다 // --------------------------------------------------- return ( {row.getVisibleCells().map((cell) => { // 이 셀의 컬럼이 grouped라면 숨긴다 if (cell.column.getIsGrouped()) { return null } return ( {flexRender( cell.column.columnDef.cell, cell.getContext() )} ) })} ) }) ) : ( // --------------------------------------------------- // 3) 데이터가 없을 때 // --------------------------------------------------- No results. )}
) }