feat: data table 페이지네이션 ui 추가

This commit is contained in:
2025-04-07 13:11:22 +09:00
parent 632042b19c
commit 35d9595ea2
3 changed files with 136 additions and 31 deletions
+1
View File
@@ -33,6 +33,7 @@
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
@@ -1,10 +1,18 @@
import {
flexRender,
type RowData,
type Table as TableType,
type Table as TableInstance,
} from "@tanstack/react-table";
import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
} from "@radix-ui/react-icons";
import { cn } from "../../lib";
import { Button } from "../button";
import { Table, TableCell, TableHead, TableHeader, TableRow } from "../table";
import type { HTMLAttributes } from "react";
declare module "@tanstack/react-table" {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -13,7 +21,50 @@ declare module "@tanstack/react-table" {
}
}
function THead<T>({ table }: { table: TableType<T> }) {
type DataTableProps<T> = {
table: TableInstance<T>;
variant?: "default" | "secondary" | "line";
className?: HTMLAttributes<HTMLTableElement>["className"];
viewPagination?: boolean;
onRowClick?: (row: T) => void;
};
function DataTable<T>({
table,
variant = "default",
className,
viewPagination,
onRowClick,
}: DataTableProps<T>) {
return (
<>
<Table className={cn("w-full", className)} variant={variant}>
<THead table={table} />
<tbody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
onClick={() => onRowClick?.(row.original)}
className={cn(onRowClick && "cursor-pointer hover:bg-accent")}
>
{row.getVisibleCells().map((cell) => {
const { meta } = cell.column.columnDef;
return (
<TableCell key={cell.id} align="center" {...meta}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
})}
</TableRow>
))}
</tbody>
</Table>
{viewPagination && <Pagination table={table} />}
</>
);
}
function THead<T>({ table }: { table: TableInstance<T> }) {
return (
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@@ -37,39 +88,81 @@ function THead<T>({ table }: { table: TableType<T> }) {
);
}
type TableProps<T> = {
table: TableType<T>;
variant?: "default" | "secondary" | "line";
onRowClick?: (row: T) => void;
};
function Pagination<T>({ table }: { table: TableInstance<T> }) {
const pageCount = table.getPageCount();
const currentPage = table.getState().pagination.pageIndex;
const maxVisiblePages = 10; // 한 번에 보여줄 최대 페이지 수
let startPage = Math.max(0, currentPage - maxVisiblePages / 2); // 시작 페이지
const endPage = Math.min(pageCount, startPage + maxVisiblePages); // 끝 페이지
if (endPage - startPage < maxVisiblePages) {
startPage = Math.max(0, endPage - maxVisiblePages);
}
const pageNumbers = Array(endPage - startPage)
.fill(0)
.map((_, i) => startPage + i);
function DataTable<T>({
table,
variant = "default",
onRowClick,
}: TableProps<T>) {
return (
<Table className="w-full" variant={variant}>
<THead table={table} />
<tbody>
{table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
onClick={() => onRowClick?.(row.original)}
className={cn(onRowClick && "cursor-pointer hover:bg-accent")}
<div className="mt-[24px] flex items-center justify-center space-x-2">
<Button
variant="ghost"
color="white"
className="hidden size-10 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">첫 페이지로</span>
<DoubleArrowLeftIcon className="size-4" />
</Button>
<Button
variant="ghost"
color="white"
className="size-10 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">이전 페이지로</span>
<ChevronLeftIcon className="size-4" />
</Button>
<div className="flex items-center justify-center text-sm font-medium">
{pageNumbers.map((pageNumber) => (
<Button
key={pageNumber}
variant="ghost"
className={cn(
"inline-block size-10 !bg-transparent p-0 text-foreground hover:!bg-accent",
currentPage === pageNumber &&
"!bg-primary !font-bold !text-primary-foreground hover:!bg-primary",
)}
onClick={() => table.setPageIndex(pageNumber)}
>
{row.getVisibleCells().map((cell) => {
const { meta } = cell.column.columnDef;
return (
<TableCell key={cell.id} align="center" {...meta}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
})}
</TableRow>
{pageNumber + 1}
</Button>
))}
</tbody>
</Table>
</div>
<Button
variant="ghost"
color="white"
className="size-10 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">다음 페이지로</span>
<ChevronRightIcon className="size-4" />
</Button>
<Button
variant="ghost"
color="white"
className="hidden size-10 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">마지막 페이지로</span>
<DoubleArrowRightIcon className="size-4" />
</Button>
</div>
);
}
+11
View File
@@ -384,6 +384,9 @@ importers:
'@radix-ui/react-dropdown-menu':
specifier: ^2.1.6
version: 2.1.6(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-icons':
specifier: ^1.3.2
version: 1.3.2(react@18.3.1)
'@radix-ui/react-label':
specifier: ^2.1.2
version: 2.1.2(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1)
@@ -2102,6 +2105,14 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-icons@1.3.2(react@18.3.1):
resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==}
peerDependencies:
react: ^18.3.1
dependencies:
react: 18.3.1
dev: false
/@radix-ui/react-id@1.1.0(@types/react@18.3.18)(react@18.3.1):
resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==}
peerDependencies: