๐Ÿžํ”„๋กœ๊ทธ๋ž˜๋ฐ/Next.js

[Next.js] ๋ฆฌ์—‘ํŠธ ํ…Œ์ด๋ธ” CSV ์ €์žฅ ๊ตฌํ˜„(feat. Shadcn)

TwoIceFish 2024. 6. 21. 15:03

๊ฐœ์š”

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

 

How to export React Tanstack Table to CSV file

It’s easy. We can achieve this with just a few lines of code.

medium.com