diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-03-26 00:37:41 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-03-26 00:37:41 +0000 |
| commit | e0dfb55c5457aec489fc084c4567e791b4c65eb1 (patch) | |
| tree | 68543a65d88f5afb3a0202925804103daa91bc6f /components/client-data-table/data-table.tsx | |
3/25 까지의 대표님 작업사항
Diffstat (limited to 'components/client-data-table/data-table.tsx')
| -rw-r--r-- | components/client-data-table/data-table.tsx | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/components/client-data-table/data-table.tsx b/components/client-data-table/data-table.tsx new file mode 100644 index 00000000..ff10bfe4 --- /dev/null +++ b/components/client-data-table/data-table.tsx @@ -0,0 +1,336 @@ +"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 { getCommonPinningStyles } 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<TData, TValue> { + columns: ColumnDef<TData, TValue>[] + data: TData[] + advancedFilterFields: any[] + autoSizeColumns?: boolean + onSelectedRowsChange?: (selected: TData[]) => void + + /** 추가로 표시할 버튼/컴포넌트 */ + children?: React.ReactNode +} + +export function ClientDataTable<TData, TValue>({ + columns, + data, + advancedFilterFields, + autoSizeColumns = true, + children, + onSelectedRowsChange +}: DataTableProps<TData, TValue>) { + + + // (1) React Table 상태 + const [rowSelection, setRowSelection] = React.useState({}) + const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({}) + const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) + const [sorting, setSorting] = React.useState<SortingState>([]) + const [grouping, setGrouping] = React.useState<string[]>([]) + const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({}) + + // 실제 리사이징 상태만 추적 + const [isResizing, setIsResizing] = React.useState(false) + + // 리사이징 상태를 추적하기 위한 ref + const isResizingRef = React.useRef(false) + + // 리사이징 이벤트 핸들러 + const handleResizeStart = React.useCallback(() => { + isResizingRef.current = true + setIsResizing(true) + }, []) + + const handleResizeEnd = React.useCallback(() => { + isResizingRef.current = false + setIsResizing(false) + }, []) + + const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({ + left: [], + right: ["update"], + }) + + 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(() => { + // 강제로 초기 상태는 리사이징 비활성화 + setIsResizing(false) + isResizingRef.current = false + + // 전역 마우스 이벤트 핸들러 + const handleMouseUp = () => { + if (isResizingRef.current) { + handleResizeEnd() + } + } + + // 이벤트 리스너 등록 + window.addEventListener('mouseup', handleMouseUp) + window.addEventListener('touchend', handleMouseUp) + + return () => { + // 이벤트 리스너 정리 + window.removeEventListener('mouseup', handleMouseUp) + window.removeEventListener('touchend', handleMouseUp) + + // 컴포넌트 언마운트 시 정리 + setIsResizing(false) + isResizingRef.current = false + } + }, [handleResizeEnd]) + + React.useEffect(() => { + if (!onSelectedRowsChange) return + const selectedRows = table + .getSelectedRowModel() + .flatRows.map((row) => row.original) + onSelectedRowsChange(selectedRows) + }, [rowSelection, table, onSelectedRowsChange]) + + // (2) 렌더 + return ( + <div className="space-y-4"> + {/* 툴바에 children을 넘기기 */} + <ClientDataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + > + {children} + </ClientDataTableAdvancedToolbar> + + <div className="rounded-md border"> + <div className="overflow-auto" style={{maxHeight:'33.6rem'}}> + <UiTable + className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed" + > + <TableHeader> + {table.getHeaderGroups().map((headerGroup) => ( + <TableRow key={headerGroup.id}> + {headerGroup.headers.map((header) => { + // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음 + if (header.column.getIsGrouped()) { + return null + } + + return ( + <TableHead + key={header.id} + colSpan={header.colSpan} + className="relative" + style={{ + ...getCommonPinningStyles({ column: header.column }), + width: header.getSize() + }} + > + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + {/* 리사이즈 핸들 - 헤더에만 추가 */} + {header.column.getCanResize() && ( + <DataTableResizer + header={header} + onResizeStart={handleResizeStart} + onResizeEnd={handleResizeEnd} + /> + )} + </TableHead> + ) + })} + </TableRow> + ))} + </TableHeader> + <TableBody> + {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 ( + <TableRow + key={row.id} + className="bg-muted/20" + data-state={row.getIsExpanded() && "expanded"} + > + {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */} + <TableCell colSpan={table.getVisibleFlatColumns().length}> + {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */} + {row.getCanExpand() && ( + <button + onClick={row.getToggleExpandedHandler()} + className="inline-flex items-center justify-center mr-2 w-5 h-5" + style={{ + // row.depth: 0이면 top-level, 1이면 그 하위 등 + marginLeft: `${row.depth * 1.5}rem`, + }} + > + {row.getIsExpanded() ? ( + <ChevronUp size={16} /> + ) : ( + <ChevronRight size={16} /> + )} + </button> + )} + + {/* Group Label + 값 */} + <span className="font-semibold"> + {columnLabel}: {row.getValue(groupingColumnId)} + </span> + <span className="ml-2 text-xs text-muted-foreground"> + ({row.subRows.length} rows) + </span> + </TableCell> + </TableRow> + ) + } + + // --------------------------------------------------- + // 2) 일반 Row + // → "그룹핑된 컬럼"은 숨긴다 + // --------------------------------------------------- + return ( + <TableRow + key={row.id} + data-state={row.getIsSelected() && "selected"} + > + {row.getVisibleCells().map((cell) => { + // 이 셀의 컬럼이 grouped라면 숨긴다 + if (cell.column.getIsGrouped()) { + return null + } + + return ( + <TableCell + key={cell.id} + data-column-id={cell.column.id} + style={{ + ...getCommonPinningStyles({ column: cell.column }), + width: cell.column.getSize() + }} + > + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + </TableCell> + ) + })} + </TableRow> + ) + }) + ) : ( + // --------------------------------------------------- + // 3) 데이터가 없을 때 + // --------------------------------------------------- + <TableRow> + <TableCell + colSpan={table.getAllColumns().length} + className="h-24 text-center" + > + No results. + </TableCell> + </TableRow> + )} + </TableBody> + </UiTable> + + {/* 리사이징 시에만 캡처 레이어 활성화 */} + {isResizing && ( + <div className="fixed inset-0 cursor-col-resize select-none z-50" /> + )} + </div> + </div> + + <ClientDataTablePagination table={table} /> + </div> + ) +}
\ No newline at end of file |
