From 71c0ba1f01b98770ec2c60cdb935ffb36c1830a9 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Mon, 8 Dec 2025 12:08:00 +0900 Subject: (김준회) 테이블 커스텀 훅 버전 생성 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client-table-v3/client-virtual-table.tsx | 309 +++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 components/client-table-v3/client-virtual-table.tsx (limited to 'components/client-table-v3/client-virtual-table.tsx') diff --git a/components/client-table-v3/client-virtual-table.tsx b/components/client-table-v3/client-virtual-table.tsx new file mode 100644 index 00000000..7a092326 --- /dev/null +++ b/components/client-table-v3/client-virtual-table.tsx @@ -0,0 +1,309 @@ +"use client"; + +import * as React from "react"; +import { Table, flexRender } from "@tanstack/react-table"; +import { useVirtualizer } from "@tanstack/react-virtual"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragEndEvent, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + horizontalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { cn } from "@/lib/utils"; +import { Loader2, ChevronRight, ChevronDown } from "lucide-react"; + +import { ClientTableToolbar } from "../client-table/client-table-toolbar"; +import { exportToExcel } from "../client-table/export-utils"; +import { ClientDataTablePagination } from "@/components/client-data-table/data-table-pagination"; +import { ClientTableViewOptions } from "../client-table/client-table-view-options"; + +import { ClientTableColumnHeader } from "./client-table-column-header"; +import { ClientTablePreset } from "./client-table-preset"; +import { ClientVirtualTableProps } from "./types"; + +export function ClientVirtualTable({ + table, + isLoading = false, + height = "100%", + estimateRowHeight = 40, + className, + actions, + customToolbar, + enableExport = true, + onExport, + enableUserPreset = false, + tableKey, + getRowClassName, + onRowClick, + renderHeaderVisualFeedback, +}: ClientVirtualTableProps) { + // --- DnD Sensors --- + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(KeyboardSensor) + ); + + // --- Drag Handler --- + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (active && over && active.id !== over.id) { + const activeId = active.id as string; + const overId = over.id as string; + + const activeColumn = table.getColumn(activeId); + const overColumn = table.getColumn(overId); + + if (activeColumn && overColumn) { + const activePinState = activeColumn.getIsPinned(); + const overPinState = overColumn.getIsPinned(); + + // If dragging between different pin states, update the pin state + if (activePinState !== overPinState) { + activeColumn.pin(overPinState); + } + + // Reorder + const currentOrder = table.getState().columnOrder; + const oldIndex = currentOrder.indexOf(activeId); + const newIndex = currentOrder.indexOf(overId); + + if (oldIndex !== -1 && newIndex !== -1) { + table.setColumnOrder(arrayMove(currentOrder, oldIndex, newIndex)); + } + } + } + }; + + // --- Virtualization --- + const tableContainerRef = React.useRef(null); + const { rows } = table.getRowModel(); + + const rowVirtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => tableContainerRef.current, + estimateSize: () => estimateRowHeight, + overscan: 10, + }); + + const virtualRows = rowVirtualizer.getVirtualItems(); + const totalSize = rowVirtualizer.getTotalSize(); + + const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0; + const paddingBottom = + virtualRows.length > 0 + ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) + : 0; + + // --- Export --- + const handleExport = async () => { + if (onExport) { + onExport(table.getFilteredRowModel().rows.map((r) => r.original)); + return; + } + const currentData = table.getFilteredRowModel().rows.map((row) => row.original); + // Note: exportToExcel needs columns definition. table.getAllColumns() or visible columns? + // Using table.getAllLeafColumns() usually. + await exportToExcel(currentData, table.getAllLeafColumns(), `export-${new Date().toISOString().slice(0, 10)}.xlsx`); + }; + + const columns = table.getVisibleLeafColumns(); + const data = table.getFilteredRowModel().rows; // or just rows which is from getRowModel + + return ( +
+ + + {enableUserPreset && tableKey && ( + + )} + + } + customToolbar={customToolbar} + actions={actions} + /> + +
+ {isLoading && ( +
+ +
+ )} + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + h.id)} + strategy={horizontalListSortingStrategy} + > + {headerGroup.headers.map((header) => ( + + ))} + + + ))} + + + {paddingTop > 0 && ( + + + )} + {virtualRows.length === 0 && !isLoading ? ( + + + + ) : ( + virtualRows.map((virtualRow) => { + const row = rows[virtualRow.index]; + + // --- Group Header Rendering --- + if (row.getIsGrouped()) { + const groupingColumnId = row.groupingColumnId ?? ""; + const groupingValue = row.getGroupingValue(groupingColumnId); + + return ( + + + + ); + } + + // --- Normal Row Rendering --- + return ( + onRowClick?.(row, e)} + > + {row.getVisibleCells().map((cell) => { + const isPinned = cell.column.getIsPinned(); + const isGrouped = cell.column.getIsGrouped(); + + const style: React.CSSProperties = { + width: cell.column.getSize(), + }; + if (isPinned === "left") { + style.position = "sticky"; + style.left = `${cell.column.getStart("left")}px`; + style.zIndex = 20; + } else if (isPinned === "right") { + style.position = "sticky"; + style.right = `${cell.column.getAfter("right")}px`; + style.zIndex = 20; + } + + return ( + + ); + })} + + ); + }) + )} + {paddingBottom > 0 && ( + + + )} + +
+
+ No results. +
+
+ {row.getIsExpanded() ? ( + + ) : ( + + )} + + + {groupingColumnId}: + + + {String(groupingValue)} + + + ({row.subRows.length}) + + +
+
+ {cell.getIsGrouped() ? null : cell.getIsAggregated() ? ( + flexRender( + cell.column.columnDef.aggregatedCell ?? + cell.column.columnDef.cell, + cell.getContext() + ) + ) : cell.getIsPlaceholder() ? null : ( + flexRender(cell.column.columnDef.cell, cell.getContext()) + )} +
+
+
+
+ + +
+ ); +} + + -- cgit v1.2.3