Files
frontend/packages/common/components/src/ui/data-table/index.tsx
T

200 lines
6.0 KiB
TypeScript

import {
flexRender,
type RowData,
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
interface ColumnMeta<TData extends RowData, TValue> {
width?: number;
align?: "left" | "center" | "right";
rowSpan?: number;
}
}
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) => {
const isSelectedRow = row.getIsSelected();
return (
<TableRow
key={row.id}
onClick={() => onRowClick?.(row.original)}
className={cn(
onRowClick && "cursor-pointer hover:bg-accent",
isSelectedRow && "!bg-primary-transparent",
)}
>
{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) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const columnRelativeDepth = header.depth - header.column.depth;
if (
!header.isPlaceholder &&
columnRelativeDepth > 1 &&
header.id === header.column.id
) {
return null;
}
let rowSpan = 1;
if (header.isPlaceholder) {
const leafs = header.getLeafHeaders();
rowSpan = (leafs?.[leafs.length - 1]?.depth ?? 0) - header.depth;
}
const { meta } = header.column.columnDef;
return (
<TableHead
key={header.id}
className="sticky top-0 z-10"
colSpan={header.colSpan}
rowSpan={rowSpan}
{...meta}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
);
}
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);
return (
<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 h-10 min-w-10 !bg-transparent p-1 text-foreground hover:!bg-accent",
currentPage === pageNumber &&
"!bg-primary !font-bold !text-primary-foreground hover:!bg-primary",
)}
onClick={() => table.setPageIndex(pageNumber)}
>
{pageNumber + 1}
</Button>
))}
</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>
);
}
export { DataTable };