ERP-node/frontend/components/common/DataTable.tsx

281 lines
9.4 KiB
TypeScript
Raw Normal View History

2025-08-21 09:41:46 +09:00
"use client";
import * as React from "react";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
SortingState,
ColumnFiltersState,
useReactTable,
} from "@tanstack/react-table";
import { ChevronDown, ChevronUp, Search, Download, Filter } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { cn } from "@/lib/utils";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
searchable?: boolean;
searchPlaceholder?: string;
showExport?: boolean;
showFilter?: boolean;
onExport?: () => void;
onRowClick?: (row: TData) => void;
className?: string;
}
export function DataTable<TData, TValue>({
columns,
data,
searchable = true,
searchPlaceholder = "검색...",
showExport = false,
showFilter = false,
onExport,
onRowClick,
className,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = React.useState("");
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
state: {
sorting,
columnFilters,
globalFilter,
},
});
return (
<div className={cn("space-y-4", className)}>
{/* 테이블 상단 툴바 */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
{searchable && (
<div className="relative">
<Search className="absolute top-2.5 left-2.5 h-4 w-4 text-slate-500" />
<Input
placeholder={searchPlaceholder}
value={globalFilter ?? ""}
onChange={(event) => setGlobalFilter(event.target.value)}
className="w-64 pl-8"
/>
</div>
)}
{showFilter && (
<Button variant="outline" size="sm">
<Filter className="mr-2 h-4 w-4" />
</Button>
)}
</div>
<div className="flex items-center space-x-2">
{showExport && (
<Button variant="outline" size="sm" onClick={onExport}>
<Download className="mr-2 h-4 w-4" />
</Button>
)}
</div>
</div>
{/* 테이블 */}
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id} className="bg-slate-50">
{header.isPlaceholder ? null : (
<div
className={cn(
"flex items-center space-x-2",
header.column.getCanSort() ? "cursor-pointer select-none" : "",
)}
onClick={header.column.getToggleSortingHandler()}
>
<span>{flexRender(header.column.columnDef.header, header.getContext())}</span>
{header.column.getCanSort() && (
<div className="flex flex-col">
{header.column.getIsSorted() === "desc" ? (
<ChevronDown className="h-4 w-4" />
) : header.column.getIsSorted() === "asc" ? (
<ChevronUp className="h-4 w-4" />
) : (
<div className="h-4 w-4 opacity-50">
<ChevronDown className="h-3 w-3" />
</div>
)}
</div>
)}
</div>
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className={cn(onRowClick ? "cursor-pointer hover:bg-slate-50" : "")}
onClick={() => onRowClick?.(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 페이지네이션 */}
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-slate-600">
{table.getFilteredSelectedRowModel().rows.length} {table.getFilteredRowModel().rows.length}
</div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div className="flex items-center space-x-2">
<p className="text-sm font-medium"> </p>
<select
className="h-8 w-16 rounded border border-slate-300 text-sm"
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}
</option>
))}
</select>
</div>
<div className="flex w-24 items-center justify-center text-sm font-medium">
{table.getState().pagination.pageIndex + 1} / {table.getPageCount()}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only"> </span>
{"<<"}
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only"> </span>
{"<"}
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only"> </span>
{">"}
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only"> </span>
{">>"}
</Button>
</div>
</div>
</div>
</div>
);
}
// 컬럼 정의 헬퍼 함수들
export const createTextColumn = (accessorKey: string, header: string) => ({
accessorKey,
header,
cell: ({ row }: any) => <div className="text-left">{row.getValue(accessorKey)}</div>,
});
export const createDateColumn = (accessorKey: string, header: string) => ({
accessorKey,
header,
cell: ({ row }: any) => {
const date = row.getValue(accessorKey);
return <div className="text-left">{date ? new Date(date).toLocaleDateString("ko-KR") : "-"}</div>;
},
});
export const createStatusColumn = (accessorKey: string, header: string) => ({
accessorKey,
header,
cell: ({ row }: any) => {
const status = row.getValue(accessorKey);
return (
<div className="flex w-16">
<span
className={cn(
"inline-flex items-center rounded-full px-2 py-1 text-xs font-medium",
status === "active" || status === "활성"
? "bg-green-50 text-green-700"
: status === "inactive" || status === "비활성"
? "bg-gray-50 text-gray-700"
: status === "pending" || status === "대기"
? "bg-yellow-50 text-yellow-700"
: "bg-destructive/10 text-red-700",
2025-08-21 09:41:46 +09:00
)}
>
{status || "-"}
</span>
</div>
);
},
});
export const createActionColumn = (actions: React.ReactNode) => ({
id: "actions",
header: "작업",
cell: () => actions,
});