diff options
| author | joonhoekim <26rote@gmail.com> | 2025-11-28 20:32:40 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-11-28 20:32:40 +0900 |
| commit | eea317cb775587d002e7a97d62220e5c8f37066d (patch) | |
| tree | 7948c6e5fc9cf8ef0a7e103879264e45af27d26d | |
| parent | 9cabe879404f1ec05dbf4e65d55162b5573aeced (diff) | |
(김준회) client table doc & dependencies
| -rw-r--r-- | components/client-table/README.md | 159 | ||||
| -rw-r--r-- | package-lock.json | 44 | ||||
| -rw-r--r-- | package.json | 3 |
3 files changed, 206 insertions, 0 deletions
diff --git a/components/client-table/README.md b/components/client-table/README.md new file mode 100644 index 00000000..053175d4 --- /dev/null +++ b/components/client-table/README.md @@ -0,0 +1,159 @@ +# Client Table Components + +A set of reusable, virtualized table components for client-side data rendering, built on top of `@tanstack/react-table` and `@tanstack/react-virtual`. + +## Features + +- **Virtualization**: Efficiently renders large datasets (50,000+ rows) by only rendering visible rows. +- **Sorting**: Built-in column sorting (Ascending/Descending). +- **Filtering**: + - Global search (all columns). + - Column-specific filters: Text (default), Select, Boolean. +- **Pagination**: Supports both client-side and server-side (manual) pagination. +- **Column Management**: + - **Reordering**: Drag and drop columns to change order. + - **Hiding**: Right-click header to hide columns. + - **Pinning**: Right-click header to pin columns (Left/Right). +- **Excel Export**: Export current table view or custom datasets to `.xlsx`. +- **Excel Import**: Utility to parse Excel files into JSON objects. +- **Template Generation**: Create Excel templates for users to fill out and import. + +## Installation / Usage + +The components are located in `@/components/client-table`. + +### 1. Basic Usage + +```tsx +import { ClientVirtualTable, ClientTableColumnDef } from "@/components/client-table" + +// 1. Define Data Type +interface User { + id: string + name: string + role: string + active: boolean +} + +// 2. Define Columns +const columns: ClientTableColumnDef<User>[] = [ + { + accessorKey: "name", + header: "Name", + // Default filter is text + }, + { + accessorKey: "role", + header: "Role", + meta: { + filterType: "select", + filterOptions: [ + { label: "Admin", value: "admin" }, + { label: "User", value: "user" }, + ] + } + }, + { + accessorKey: "active", + header: "Active", + meta: { + filterType: "boolean" + } + } +] + +// 3. Render Component +export default function UserTable({ data }: { data: User[] }) { + return ( + <ClientVirtualTable + data={data} + columns={columns} + height="600px" // Required for virtualization + enableExport={true} // Shows export button + enablePagination={true} // Shows pagination footer + /> + ) +} +``` + +### 2. Server-Side Pagination + +For very large datasets where you don't want to fetch everything at once. + +```tsx +<ClientVirtualTable + data={currentData} // Only the current page data + columns={columns} + manualPagination={true} + pageCount={totalPages} + rowCount={totalRows} + pagination={{ pageIndex, pageSize }} + onPaginationChange={setPaginationState} + enablePagination={true} +/> +``` + +### 3. Excel Utilities + +#### Exporting Data + +Automatic export is available via the `enableExport` prop. For custom export logic: + +```tsx +import { exportToExcel } from "@/components/client-table" + +await exportToExcel(data, columns, "my-data.xlsx") +``` + +#### Creating Import Templates + +Generate a blank Excel file with headers for users to fill in. + +```tsx +import { createExcelTemplate } from "@/components/client-table" + +await createExcelTemplate({ + columns, + filename: "user-import-template.xlsx", + excludeColumns: ["id", "createdAt"], // Columns to skip + includeColumns: [{ key: "notes", header: "Notes" }] // Extra columns +}) +``` + +#### Importing Data + +Parses an uploaded Excel file into a raw JSON array. Does **not** handle validation or DB insertion. + +```tsx +import { importFromExcel } from "@/components/client-table" + +const handleFileUpload = async (file: File) => { + const { data, errors } = await importFromExcel({ + file, + columnMapping: { "Name": "name", "Role": "role" } // Optional header mapping + }) + + if (errors.length > 0) { + console.error(errors) + return + } + + // Send `data` to your API/Service for validation and insertion + await saveUsers(data) +} +``` + +## Types + +We use a custom column definition type to support our extended `meta` properties. + +```typescript +import { ClientTableColumnDef } from "@/components/client-table" + +const columns: ClientTableColumnDef<MyData>[] = [ ... ] +``` + +Supported `meta` properties: + +- `filterType`: `"text" | "select" | "boolean"` +- `filterOptions`: `{ label: string; value: string }[]` (Required for `select`) diff --git a/package-lock.json b/package-lock.json index b95cecae..1423b5df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-hover-card": "^1.1.4", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-menubar": "^1.1.4", "@radix-ui/react-navigation-menu": "^1.2.3", @@ -82,6 +83,7 @@ "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", "@t3-oss/env-nextjs": "^0.11.1", + "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-table": "^8.20.6", "@tanstack/react-virtual": "^3.13.12", "@tiptap/extension-blockquote": "^2.23.1", @@ -142,6 +144,7 @@ "knex": "^3.1.0", "libphonenumber-js": "^1.12.10", "lucide-react": "^0.468.0", + "match-sorter": "^8.2.0", "next": "15.1.0", "next-auth": "^4.24.11", "next-i18n-router": "^5.5.1", @@ -4138,6 +4141,15 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", @@ -5094,6 +5106,22 @@ } } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz", + "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==", + "license": "MIT", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-table": { "version": "8.21.3", "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", @@ -12622,6 +12650,16 @@ "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/match-sorter": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.2.0.tgz", + "integrity": "sha512-qRVB7wYMJXizAWR4TKo5UYwgW7oAVzA8V9jve0wGzRvV91ou9dcqL+/2gJtD0PZ/Pm2Fq6cVT4VHXHmDFVMGRA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -15403,6 +15441,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/request": { "version": "2.16.6", "resolved": "https://registry.npmjs.org/request/-/request-2.16.6.tgz", diff --git a/package.json b/package.json index 6c9a85a5..2f435dbe 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-hover-card": "^1.1.4", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-menubar": "^1.1.4", "@radix-ui/react-navigation-menu": "^1.2.3", @@ -84,6 +85,7 @@ "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.6", "@t3-oss/env-nextjs": "^0.11.1", + "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-table": "^8.20.6", "@tanstack/react-virtual": "^3.13.12", "@tiptap/extension-blockquote": "^2.23.1", @@ -144,6 +146,7 @@ "knex": "^3.1.0", "libphonenumber-js": "^1.12.10", "lucide-react": "^0.468.0", + "match-sorter": "^8.2.0", "next": "15.1.0", "next-auth": "^4.24.11", "next-i18n-router": "^5.5.1", |
