From 37f55540833c2d5894513eca9fc8f7c6233fc2d2 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 29 May 2025 05:17:13 +0000 Subject: (대표님) 0529 14시 16분 변경사항 저장 (Vendor Data, Docu) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/data-table/data-table-pagination.tsx | 219 ++++++++++++------ components/data-table/infinite-data-table.tsx | 294 ++++++++++++++++++++++++ 2 files changed, 443 insertions(+), 70 deletions(-) create mode 100644 components/data-table/infinite-data-table.tsx (limited to 'components/data-table') diff --git a/components/data-table/data-table-pagination.tsx b/components/data-table/data-table-pagination.tsx index 4ed63a1b..922dacf1 100644 --- a/components/data-table/data-table-pagination.tsx +++ b/components/data-table/data-table-pagination.tsx @@ -7,6 +7,7 @@ import { ChevronRight, ChevronsLeft, ChevronsRight, + Infinity, } from "lucide-react" import { Button } from "@/components/ui/button" @@ -21,57 +22,99 @@ import { interface DataTablePaginationProps { table: Table pageSizeOptions?: Array + // 무한 스크롤 관련 props + infiniteScroll?: { + enabled: boolean + hasNextPage: boolean + isLoadingMore: boolean + totalCount?: number | null + onLoadMore?: () => void + } + // 페이지 크기 변경 콜백 (필수!) + onPageSizeChange?: (pageSize: number) => void } export function DataTablePagination({ table, pageSizeOptions = [10, 20, 30, 40, 50, "All"], + infiniteScroll, + onPageSizeChange, }: DataTablePaginationProps) { // 현재 테이블 pageSize const currentPageSize = table.getState().pagination.pageSize + const isInfiniteMode = infiniteScroll?.enabled || currentPageSize >= 1_000_000 - // "All"을 1,000,000으로 처리할 것이므로, - // 만약 현재 pageSize가 1,000,000이면 화면상 "All"로 표시 - const selectValue = - currentPageSize === 1_000_000 - ? "All" - : String(currentPageSize) + // "All"을 1,000,000으로 처리하고, 무한 스크롤 모드 표시 + const selectValue = isInfiniteMode ? "All" : String(currentPageSize) + + const handlePageSizeChange = (value: string) => { + if (!onPageSizeChange) { + console.warn('DataTablePagination: onPageSizeChange prop is required for page size changes to work') + return + } + + if (value === "All") { + // "All" 선택 시 무한 스크롤 모드로 전환 + onPageSizeChange(1_000_000) // URL 상태 업데이트만 수행 + } else { + const newSize = Number(value) + onPageSizeChange(newSize) // URL 상태 업데이트만 수행 + } + + // table.setPageSize()는 호출하지 않음! + // URL 상태 변경이 테이블 상태로 자동 반영됨 + } return (
+ {/* 선택된 행 및 총 개수 정보 */}
{table.getFilteredSelectedRowModel().rows.length} of{" "} - {table.getFilteredRowModel().rows.length} row(s) selected. - Total: {table.getRowCount()} records + {isInfiniteMode ? ( + // 무한 스크롤 모드일 때 + <> + {table.getRowModel().rows.length} row(s) selected. + {infiniteScroll?.totalCount !== null && ( + + Total: {infiniteScroll.totalCount?.toLocaleString()} records + + ({table.getRowModel().rows.length.toLocaleString()} loaded) + + + )} + + ) : ( + // 페이지네이션 모드일 때 + <> + {table.getFilteredRowModel().rows.length} row(s) selected. + Total: {table.getRowCount()} records + + )}
+
{/* Rows per page Select */}
-

Rows per page

- {pageSizeOptions.map((option) => { - // 화면에 표시할 라벨 const label = option === "All" ? "All" : String(option) - // value도 문자열화 const val = option === "All" ? "All" : String(option) return ( - {label} +
+ {option === "All" && ( + + )} + {label} +
) })} @@ -79,54 +122,90 @@ export function DataTablePagination({
- {/* 현재 페이지 / 전체 페이지 */} -
- Page {table.getState().pagination.pageIndex + 1} of{" "} - {table.getPageCount()} -
+ {/* 페이지네이션 모드일 때만 페이지 정보 표시 */} + {!isInfiniteMode && ( + <> + {/* 현재 페이지 / 전체 페이지 */} +
+ Page {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount()} +
- {/* 페이지 이동 버튼 */} -
- - - - -
+ {/* 페이지 이동 버튼 */} +
+ + + + +
+ + )} + + {/* 무한 스크롤 모드일 때 로드 더 버튼 */} + {isInfiniteMode && infiniteScroll && ( +
+ {infiniteScroll.hasNextPage && ( + + )} + {!infiniteScroll.hasNextPage && table.getRowModel().rows.length > 0 && ( + + All data loaded + + )} +
+ )}
) diff --git a/components/data-table/infinite-data-table.tsx b/components/data-table/infinite-data-table.tsx new file mode 100644 index 00000000..fcac56ee --- /dev/null +++ b/components/data-table/infinite-data-table.tsx @@ -0,0 +1,294 @@ +"use client" + +import * as React from "react" +import { flexRender, type Table as TanstackTable } from "@tanstack/react-table" +import { ChevronRight, ChevronUp, Loader2 } from "lucide-react" +import { useIntersection } from "@mantine/hooks" + +import { cn } from "@/lib/utils" +import { getCommonPinningStyles } from "@/lib/data-table" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Button } from "@/components/ui/button" +import { DataTableResizer } from "@/components/data-table/data-table-resizer" +import { useAutoSizeColumns } from "@/hooks/useAutoSizeColumns" + +interface InfiniteDataTableProps extends React.HTMLAttributes { + table: TanstackTable + floatingBar?: React.ReactNode | null + autoSizeColumns?: boolean + compact?: boolean + // 무한 스크롤 관련 props + hasNextPage?: boolean + isLoadingMore?: boolean + onLoadMore?: () => void + totalCount?: number | null + isEmpty?: boolean +} + +/** + * 무한 스크롤 지원 DataTable + */ +export function InfiniteDataTable({ + table, + floatingBar = null, + autoSizeColumns = true, + compact = false, + hasNextPage = false, + isLoadingMore = false, + onLoadMore, + totalCount = null, + isEmpty = false, + children, + className, + maxHeight, + ...props +}: InfiniteDataTableProps & { maxHeight?: string | number }) { + + useAutoSizeColumns(table, autoSizeColumns) + + // Intersection Observer for infinite scroll + const { ref: loadMoreRef, entry } = useIntersection({ + threshold: 0.1, + }) + + // 자동 로딩 트리거 + React.useEffect(() => { + if (entry?.isIntersecting && hasNextPage && !isLoadingMore && onLoadMore) { + onLoadMore() + } + }, [entry?.isIntersecting, hasNextPage, isLoadingMore, onLoadMore]) + + // 컴팩트 모드를 위한 클래스 정의 + const compactStyles = compact ? { + row: "h-7", + cell: "py-1 px-2 text-sm", + groupRow: "py-1 bg-muted/20 text-sm", + emptyRow: "h-16", + } : { + row: "", + cell: "", + groupRow: "bg-muted/20", + emptyRow: "h-24", + } + + return ( +
+ {children} + + {/* 총 개수 표시 */} + {totalCount !== null && ( +
+ 총 {totalCount.toLocaleString()}개 항목 + {table.getRowModel().rows.length > 0 && ( + + (현재 {table.getRowModel().rows.length.toLocaleString()}개 로드됨) + + )} +
+ )} + +
+ + {/* 테이블 헤더 */} + + {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() && ( + + )} +
+
+ ) + })} +
+ ))} +
+ + {/* 테이블 바디 */} + + {table.getRowModel().rows?.length ? ( + <> + {table.getRowModel().rows.map((row) => { + // 그룹핑 헤더 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 ( + + {row.getVisibleCells().map((cell) => { + if (cell.column.getIsGrouped()) { + return null + } + + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ) + })} + + ) + })} + + ) : isEmpty ? ( + // 데이터가 없을 때 + + + No results. + + + ) : null} + +
+
+ + {/* 무한 스크롤 로딩 영역 */} +
+ {hasNextPage && ( + <> + {/* Intersection Observer 타겟 */} +
+ + {isLoadingMore && ( +
+ + + 로딩 중... + +
+ )} + + {/* 수동 로드 버튼 (자동 로딩 실패 시 대안) */} + {!isLoadingMore && onLoadMore && ( + + )} + + )} + + {!hasNextPage && table.getRowModel().rows.length > 0 && ( +

+ 모든 데이터를 불러왔습니다. +

+ )} +
+ +
+ {/* 선택된 행 정보 */} + {table.getFilteredSelectedRowModel().rows.length > 0 && ( +
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getRowModel().rows.length} row(s) selected. +
+ )} + + {/* Floating Bar (선택된 행 있을 때) */} + {table.getFilteredSelectedRowModel().rows.length > 0 && floatingBar} +
+
+ ) +} \ No newline at end of file -- cgit v1.2.3