๊ฐ์
Shadcn์ Data Table์์ ๋ถ๋ฌ์จ ๋ฐ์ดํฐ๋ฅผ CSV๋ก ์ ์ฅํ๋ ๊ธฐ๋ฅ์ด ํ์ํ์ฌ ์ ์ฉํด๋ณด์๋ค. export-to-csv ๋ชจ๋์ ์ค์นํ๊ณ Shadcn Data Table ๋ชจ๋์ ๋ฒํผ์ ์ถ๊ฐํ์ฌ ์ปค์คํ ํ ํ์ด์ง๋ฅผ ์์ฑํด ๋ณด์๋ค.
์ค์น
๋ค์์ ๋ชจ๋์ ์ค์นํ๊ณ ๋ค์์ ํจ์๋ฅผ ์ฌ์ฉํ๋ค.
npm install export-to-csv --save
import { mkConfig, generateCsv, download } from 'export-to-csv'
๋ณธ๋ฌธ(Shadcn ์ปค์คํ )
Shadcn Data Table์ CSV ๋ค์ด ๋ฒํผ์ ์ ์ฉํ ํ์ผ์ด๋ค.
ProjectPage.tsx
"use client";
import * as React from "react";
import { useEffect, useState } from "react";
import { ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown, Delete, MoreHorizontal, Pencil } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import Status from "@/app/components/Status";
import Priority from "@/app/components/Priority";
import Link from "next/link";
import UserCard from "@/app/aira/components/UserCard";
import MyDataTable from "@/app/aira/components/MyDataTable";
interface Project {
id: string;
title: string;
description?: string;
status: string;
priority: string;
username: string;
}
const columns: ColumnDef<Project>[] = [
{
accessorKey: "id",
enableHiding: true,
},
{
accessorKey: "username",
header: "User",
cell: ({ row }) => (
<UserCard
username={row.getValue("username")}
usermail={""}
reverse={false}
/>
),
},
{
accessorKey: "description",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Title
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => (
<Link
href={"/aira/projects/" + row.getValue("id")}
className="flex flex-col gap-2"
>
<div>{row.original.title}</div>
<div className="overflow-hidden truncate whitespace-nowrap text-muted-foreground">
{row.getValue("description")}
</div>
</Link>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
return <Status status={row.getValue("status")} />;
},
},
{
accessorKey: "priority",
header: "Priority",
cell: ({ row }) => {
return <Priority Priority={row.getValue("priority")} />;
},
},
{
accessorKey: "actions",
cell: ({ row }) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>
<Link
href={"/menu/project/edit/" + row.getValue("id")}
className={"flex w-full items-center justify-between gap-2"}
>
Edit
<Pencil className={"h-4 w-4"} />
</Link>
</DropdownMenuItem>
<DropdownMenuItem
className={"flex w-full cursor-pointer justify-between"}
onClick={() => {
if (confirm("Are you sure you want to delete this project?")) {
fetch(`/api/v1/projects/${row.getValue("id")}/delete`).then(
(r) => {
if (r.ok) {
} else {
alert("Failed to delete project");
}
location.reload();
},
);
}
}}
>
<div>Delete</div>
<Delete className={"h-4 w-4"} />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
export default function ProjectsPage() {
const [data, setData] = useState<Project[]>([]);
const getData = async () => {
const response = await fetch("/api/v1/projects");
const data = await response.json();
if (data.result) {
return data.data;
}
};
useEffect(() => {
getData().then((r) => {
setData(r);
});
}, []);
return (
<MyDataTable
columnDef={columns}
myData={data}
fileName={"dddd"}
cardTitle={"Projects"}
cardDescription={"Manage AIRA user projects"}
searchValue={"username"}
search={true}
/>
);
}
MyDataTable.tsx
"use client";
import { download, generateCsv, mkConfig } from "export-to-csv";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table";
import { ChevronDown, DownloadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
interface MyDataTableProps<T> {
columnDef: ColumnDef<T>[];
myData: T[];
fileName?: string;
cardTitle: string;
cardDescription: string;
searchValue?: string;
search: boolean;
}
export default function MyDataTable<T>({
myData,
columnDef,
fileName = "my-data",
cardTitle,
cardDescription,
searchValue,
search = false,
}: MyDataTableProps<T>) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data: myData,
columns: columnDef,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
const csvConfig = mkConfig({
fieldSeparator: ",",
filename: fileName, // export file name (without .csv)
decimalSeparator: ".",
useKeysAsHeaders: true,
});
// export function
// Note: change _ in Row<_>[] with your Typescript type.
const exportExcel = (rows: Row<T>[]) => {
const rowData = rows.map((row) => {
// Convert Project to { [k: string]: AcceptedData; [k: number]: AcceptedData; }
const project = row.original;
return Object.fromEntries(
Object.entries(project).map(([key, value]) => [key, String(value)]),
);
});
const csv = generateCsv(csvConfig)(rowData);
download(csvConfig)(csv);
};
return (
<div className="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
<Card className="md:col-span-2 lg:col-span-4">
<CardHeader>
<CardTitle>{cardTitle}</CardTitle>
<CardDescription>{cardDescription}</CardDescription>
</CardHeader>
<CardContent>
<div className="w-full">
<div className="flex items-center justify-between py-1">
{search && (
<Input
placeholder={`Filter ${searchValue}...`}
value={
(table
.getColumn(searchValue)
?.getFilterValue() as string) ?? ""
}
onChange={(event) =>
table
.getColumn(searchValue)
?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
)}
<div className={"flex items-center gap-2"}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto">
Columns <ChevronDown className="ml-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(
(column) => column.getCanHide() && column.id !== "id",
)
.map((column) => {
// Check if the column ID is 'a' and set its initial visibility to false
// Ensure column visibility is set correctly on initial render
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
<Button
size={"icon"}
onClick={() => exportExcel(table.getFilteredRowModel().rows)}
>
<DownloadIcon />
</Button>
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers
.filter((column) => column.column && column.id !== "id")
.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row
.getVisibleCells()
.filter((cell) => cell.column.id !== "id") // Filter out the column with ID 'id'
.map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columnDef.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
์ฐธ๊ณ
https://medium.com/@j.lilian/how-to-export-react-tanstack-table-to-csv-file-722a22ccd9c5
'๐ํ๋ก๊ทธ๋๋ฐ > Next.js' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Next.js] CNN ๊ณตํฌ ํ์ ์ง์ ๊ณต์ api (feat. TypeScript) (0) | 2024.08.03 |
---|---|
[Next.js] npm run start prisma findMany ๊ฐฑ์ ์๋ ๋ (0) | 2024.08.02 |
[์ ๋ณด] NextAuth.js Type ์๋ฌ ์ก๊ธฐ - CredentialsProvider / prisma (0) | 2024.02.07 |
[Nextjs] api return ๊ฐ ๊ฐฑ์ ์๋๋ ์ด์ (0) | 2023.12.04 |
[nextjs] ์์ ํ์ผ ๋ค์ด๋ก๋ (1) | 2023.11.27 |