summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-12-08 15:58:20 +0900
committerjoonhoekim <26rote@gmail.com>2025-12-08 15:58:20 +0900
commit137dc8abffcac7721890f320f183ab13eb30b790 (patch)
tree78a2362cfadbfb1297ce0f86608256dc932e95cc
parentad29c8d9dc5ce3f57d1e994e84603edcdb961c12 (diff)
parentd853ddc380bc03d968872e9ce53d7ea13a5304f8 (diff)
Merge branch 'table-v2' into dujinkim
-rw-r--r--.cursor/rules/evcp-project-rules.mdc22
-rw-r--r--.cursor/rules/table-guide.mdc4
-rw-r--r--README.md4
-rw-r--r--app/[lng]/test/table-v2/actions.ts326
-rw-r--r--app/[lng]/test/table-v2/column-defs.ts60
-rw-r--r--app/[lng]/test/table-v2/columns.tsx212
-rw-r--r--app/[lng]/test/table-v2/page.tsx637
-rw-r--r--app/[lng]/test/table-v3/page.tsx188
-rw-r--r--components/client-table-v2/GUIDE-v2.md93
-rw-r--r--components/client-table-v2/GUIDE-v3-ko.md92
-rw-r--r--components/client-table-v2/GUIDE-v3.md93
-rw-r--r--components/client-table-v2/GUIDE.md178
-rw-r--r--components/client-table-v2/adapter/create-table-service.ts101
-rw-r--r--components/client-table-v2/adapter/drizzle-table-adapter.ts173
-rw-r--r--components/client-table-v2/client-table-preset.tsx3
-rw-r--r--components/client-table-v3/GUIDE.md99
-rw-r--r--components/client-table-v3/client-table-column-header.tsx237
-rw-r--r--components/client-table-v3/client-table-filter.tsx103
-rw-r--r--components/client-table-v3/client-table-preset.tsx189
-rw-r--r--components/client-table-v3/client-virtual-table.tsx309
-rw-r--r--components/client-table-v3/index.ts9
-rw-r--r--components/client-table-v3/preset-actions.ts84
-rw-r--r--components/client-table-v3/preset-types.ts15
-rw-r--r--components/client-table-v3/types.ts84
-rw-r--r--components/client-table-v3/use-client-table.ts283
-rw-r--r--db/db.ts4
-rw-r--r--db/schema/index.ts5
-rw-r--r--db/schema/test-table-v2.ts139
-rw-r--r--db/seeds/test-table-v2.ts187
-rw-r--r--lib/table/server-query-builder.ts129
-rw-r--r--lib/vendors/items-table/item-action-dialog.tsx477
-rw-r--r--next.config.ts51
-rw-r--r--package-lock.json1888
-rw-r--r--package.json275
-rw-r--r--types/table.d.ts8
35 files changed, 4425 insertions, 2336 deletions
diff --git a/.cursor/rules/evcp-project-rules.mdc b/.cursor/rules/evcp-project-rules.mdc
new file mode 100644
index 00000000..cbd3e44d
--- /dev/null
+++ b/.cursor/rules/evcp-project-rules.mdc
@@ -0,0 +1,22 @@
+---
+alwaysApply: true
+---
+1. tech stacks: nextjs 15, postgres 17 with drizzle-orm, shadcn-ui, react 18.3.1 full stack
+2. user info: Intermediate English. Hardcoded text and comment can be written in English.
+
+specific:
+- Above Nextjs 15, exported server action functions should be async function.
+- Do not use shadcn ui ScrollArea Function in Dialog component. (It has error now.)
+- Most packages are already installed. If package installation required, check package.json file first.
+
+design:
+- If you can suppose some design patterns for solve the problem in prompt, notify it.
+- Check component/common/* for shared components.
+For these tasks, instruct the user rather than doing
+
+limit:
+- About CLI task, just notify. User will operate CLI. For example, 'npx drizzle-kit generate & migrate', 'npm run dev'.
+- You can't read and edit .env.* files.
+
+limit-solution:
+- For limited tasks, instruct the user rather than doing them yourself. \ No newline at end of file
diff --git a/.cursor/rules/table-guide.mdc b/.cursor/rules/table-guide.mdc
new file mode 100644
index 00000000..7b240413
--- /dev/null
+++ b/.cursor/rules/table-guide.mdc
@@ -0,0 +1,4 @@
+---
+alwaysApply: false
+---
+If table management is required, see @/components/client-table-v2/GUIDE-v3.md \ No newline at end of file
diff --git a/README.md b/README.md
index 060e57e1..fbf7b0e9 100644
--- a/README.md
+++ b/README.md
@@ -5,9 +5,9 @@
1. 프로젝트 압축
```bash
-zip -r public/archive-$(date +%Y%m%d-%H%M%S).zip . -x "./public/*" "./.git/*" "./.next/*" "./tmp/*" "./temp/*" "./.cursor/*" "./_docker/*" "./db/migrations/*"
+zip -r public/archive-$(date +%Y%m%d-%H%M%S).zip . -x "./public/*" "./.git/*" "./.next/*" "./tmp/*" "./temp/*" "./.cursor/*" "./_docker/*" "./db/migrations/*" "./_info/*"
-zip -r public/archive-$(date +%Y%m%d-%H%M%S).zip . -x "./public/*" "./.git/*" "./.next/*" "./tmp/*" "./temp/*" "./.cursor/*" "./node_modules/*" "./_docker/*" "./db/migrations/*"
+zip -r public/archive-$(date +%Y%m%d-%H%M%S).zip . -x "./public/*" "./.git/*" "./.next/*" "./tmp/*" "./temp/*" "./.cursor/*" "./node_modules/*" "./_docker/*" "./db/migrations/*" "./_info/*"
```
2. 내부망으로 이동해서 경로 생성 후 압축 풀기
diff --git a/app/[lng]/test/table-v2/actions.ts b/app/[lng]/test/table-v2/actions.ts
new file mode 100644
index 00000000..f5fd5f66
--- /dev/null
+++ b/app/[lng]/test/table-v2/actions.ts
@@ -0,0 +1,326 @@
+"use server";
+
+import db from "@/db/db";
+import { testProducts, testOrders, testCustomers } from "@/db/schema/test-table-v2";
+import { createTableService } from "@/components/client-table-v2/adapter/create-table-service";
+import { DrizzleTableState } from "@/components/client-table-v2/adapter/drizzle-table-adapter";
+import { productColumnDefs, OrderWithDetails, ServerColumnMeta } from "./column-defs";
+import { SQL, count, eq, desc, sql, asc } from "drizzle-orm";
+import { TestProduct } from "@/db/schema/test-table-v2";
+
+// ============================================================
+// Pattern 1: Client-Side - 전체 데이터 로드
+// ============================================================
+
+export async function getAllProducts() {
+ return await db.select().from(testProducts).orderBy(testProducts.id);
+}
+
+// ============================================================
+// Pattern 2: Factory Service - 자동 생성된 서버 액션
+// ============================================================
+
+// Server-side용 컬럼 정의 사용 (React 컴포넌트 없음)
+export const getProductTableData = createTableService({
+ db,
+ schema: testProducts,
+ columns: productColumnDefs,
+});
+
+// ============================================================
+// Pattern 2-B: Factory Service with Grouping Support
+// ============================================================
+
+/**
+ * 그룹 정보 타입
+ */
+export interface GroupInfo {
+ groupKey: string;
+ groupValue: string | number | boolean | null;
+ count: number;
+ // 확장 시 로드된 하위 행들
+ rows?: TestProduct[];
+}
+
+/**
+ * 그룹핑 응답 타입
+ */
+export interface GroupedResponse {
+ groups: GroupInfo[];
+ totalGroups: number;
+}
+
+/**
+ * 일반 응답 타입
+ */
+export interface NormalResponse {
+ data: TestProduct[];
+ totalRows: number;
+ pageCount: number;
+}
+
+/**
+ * 서버 사이드 그룹핑을 지원하는 상품 테이블 데이터 조회
+ *
+ * @param tableState - 테이블 상태 (pagination, sorting, filters, grouping)
+ * @param expandedGroups - 확장된 그룹 키 목록 (예: ["category:Electronics", "status:active"])
+ */
+export async function getProductTableDataWithGrouping(
+ tableState: DrizzleTableState,
+ expandedGroups: string[] = []
+): Promise<GroupedResponse | NormalResponse> {
+ const { grouping, pagination } = tableState;
+
+ // 그룹핑이 없으면 일반 조회
+ if (!grouping || grouping.length === 0) {
+ const result = await getProductTableData(tableState);
+ return result as NormalResponse;
+ }
+
+ // 첫 번째 그룹핑 컬럼만 처리 (다중 그룹핑은 복잡도가 높음)
+ const groupColumnId = grouping[0];
+
+ // 서버 그룹핑 가능 여부 확인
+ const columnDef = productColumnDefs.find(
+ col => 'accessorKey' in col && col.accessorKey === groupColumnId
+ );
+ const meta = columnDef?.meta as ServerColumnMeta | undefined;
+
+ if (!meta?.serverGroupable) {
+ // 서버 그룹핑 불가 - 전체 데이터 반환하여 클라이언트에서 처리
+ console.warn(`Column "${groupColumnId}" does not support server grouping. Falling back to client-side.`);
+ const allData = await db.select().from(testProducts);
+ return {
+ data: allData,
+ totalRows: allData.length,
+ pageCount: 1,
+ };
+ }
+
+ // 그룹별 카운트 조회
+ const groupColumn = getProductColumn(groupColumnId);
+ if (!groupColumn) {
+ throw new Error(`Unknown column: ${groupColumnId}`);
+ }
+
+ const groupsResult = await db
+ .select({
+ groupValue: groupColumn,
+ count: count(),
+ })
+ .from(testProducts)
+ .groupBy(groupColumn)
+ .orderBy(asc(groupColumn));
+
+ // 그룹 정보 구성
+ const groups: GroupInfo[] = await Promise.all(
+ groupsResult.map(async (g) => {
+ const groupKey = `${groupColumnId}:${g.groupValue}`;
+ const isExpanded = expandedGroups.includes(groupKey);
+
+ let rows: TestProduct[] | undefined;
+
+ // 확장된 그룹의 하위 행 로드
+ if (isExpanded) {
+ rows = await db
+ .select()
+ .from(testProducts)
+ .where(eq(groupColumn, g.groupValue))
+ .orderBy(testProducts.id)
+ .limit(pagination?.pageSize ?? 100); // 그룹 내 행 제한
+ }
+
+ return {
+ groupKey,
+ groupValue: g.groupValue,
+ count: Number(g.count),
+ rows,
+ };
+ })
+ );
+
+ return {
+ groups,
+ totalGroups: groups.length,
+ };
+}
+
+/**
+ * 컬럼 ID로 Drizzle 컬럼 객체 반환
+ */
+function getProductColumn(columnId: string) {
+ const columnMap: Record<string, any> = {
+ id: testProducts.id,
+ sku: testProducts.sku,
+ name: testProducts.name,
+ category: testProducts.category,
+ price: testProducts.price,
+ stock: testProducts.stock,
+ status: testProducts.status,
+ isNew: testProducts.isNew,
+ createdAt: testProducts.createdAt,
+ updatedAt: testProducts.updatedAt,
+ };
+ return columnMap[columnId];
+}
+
+// ============================================================
+// Pattern 3: Custom Service - 복잡한 조인 쿼리
+// ============================================================
+
+export async function getOrderTableData(tableState: DrizzleTableState): Promise<{
+ data: OrderWithDetails[];
+ totalRows: number;
+ pageCount: number;
+}> {
+ // Pattern 3에서는 DrizzleTableAdapter를 사용하지 않습니다.
+ // 조인된 결과의 컬럼들은 단일 테이블에 매핑되지 않기 때문입니다.
+ // 대신, 페이지네이션 값만 직접 계산합니다.
+
+ const pageSize = tableState.pagination?.pageSize ?? 10;
+ const pageIndex = tableState.pagination?.pageIndex ?? 0;
+ const limit = pageSize;
+ const offset = pageIndex * pageSize;
+
+ // Build ORDER BY clause based on sorting state
+ const orderByClauses =
+ tableState.sorting?.reduce<SQL<unknown>[]>((clauses, sort) => {
+ const columnMap: Record<string, any> = {
+ id: testOrders.id,
+ orderNumber: testOrders.orderNumber,
+ quantity: testOrders.quantity,
+ unitPrice: testOrders.unitPrice,
+ totalAmount: testOrders.totalAmount,
+ status: testOrders.status,
+ orderedAt: testOrders.orderedAt,
+ customerName: testCustomers.name,
+ customerEmail: testCustomers.email,
+ customerTier: testCustomers.tier,
+ productName: testProducts.name,
+ productSku: testProducts.sku,
+ };
+
+ const column = columnMap[sort.id];
+ if (!column) return clauses;
+
+ clauses.push(sort.desc ? desc(column) : asc(column));
+ return clauses;
+ }, []) ?? [];
+
+ // 커스텀 조인 쿼리 작성
+ const data = await db
+ .select({
+ id: testOrders.id,
+ orderNumber: testOrders.orderNumber,
+ quantity: testOrders.quantity,
+ unitPrice: testOrders.unitPrice,
+ totalAmount: testOrders.totalAmount,
+ status: testOrders.status,
+ orderedAt: testOrders.orderedAt,
+ // 고객 정보 조인
+ customerName: testCustomers.name,
+ customerEmail: testCustomers.email,
+ customerTier: testCustomers.tier,
+ // 상품 정보 조인
+ productName: testProducts.name,
+ productSku: testProducts.sku,
+ })
+ .from(testOrders)
+ .leftJoin(testCustomers, eq(testOrders.customerId, testCustomers.id))
+ .leftJoin(testProducts, eq(testOrders.productId, testProducts.id))
+ .orderBy(...(orderByClauses.length > 0 ? orderByClauses : [desc(testOrders.orderedAt)]))
+ .limit(limit)
+ .offset(offset);
+
+ // 총 개수 쿼리
+ const totalResult = await db
+ .select({ count: count() })
+ .from(testOrders);
+
+ const totalRows = Number(totalResult[0]?.count ?? 0);
+
+ return {
+ data: data as OrderWithDetails[],
+ totalRows,
+ pageCount: Math.ceil(totalRows / pageSize),
+ };
+}
+
+// ============================================================
+// Pattern 3-B: Custom Service with Grouping (Orders by Status)
+// ============================================================
+
+export interface OrderGroupInfo {
+ groupKey: string;
+ groupValue: string;
+ count: number;
+ totalAmount: number;
+ rows?: OrderWithDetails[];
+}
+
+/**
+ * 주문 데이터를 상태별로 그룹핑하여 조회
+ */
+export async function getOrderTableDataGroupedByStatus(
+ expandedGroups: string[] = []
+): Promise<{ groups: OrderGroupInfo[]; totalGroups: number }> {
+ // 상태별 그룹 집계
+ const groupsResult = await db
+ .select({
+ status: testOrders.status,
+ count: count(),
+ totalAmount: sql<number>`SUM(${testOrders.totalAmount}::numeric)`,
+ })
+ .from(testOrders)
+ .groupBy(testOrders.status)
+ .orderBy(testOrders.status);
+
+ const groups: OrderGroupInfo[] = await Promise.all(
+ groupsResult.map(async (g) => {
+ const groupKey = `status:${g.status}`;
+ const isExpanded = expandedGroups.includes(groupKey);
+
+ let rows: OrderWithDetails[] | undefined;
+
+ if (isExpanded) {
+ // 확장된 그룹의 상세 주문 조회 (조인 포함)
+ const orderRows = await db
+ .select({
+ id: testOrders.id,
+ orderNumber: testOrders.orderNumber,
+ quantity: testOrders.quantity,
+ unitPrice: testOrders.unitPrice,
+ totalAmount: testOrders.totalAmount,
+ status: testOrders.status,
+ orderedAt: testOrders.orderedAt,
+ customerName: testCustomers.name,
+ customerEmail: testCustomers.email,
+ customerTier: testCustomers.tier,
+ productName: testProducts.name,
+ productSku: testProducts.sku,
+ })
+ .from(testOrders)
+ .leftJoin(testCustomers, eq(testOrders.customerId, testCustomers.id))
+ .leftJoin(testProducts, eq(testOrders.productId, testProducts.id))
+ .where(eq(testOrders.status, g.status))
+ .orderBy(desc(testOrders.orderedAt))
+ .limit(50);
+
+ rows = orderRows as OrderWithDetails[];
+ }
+
+ return {
+ groupKey,
+ groupValue: g.status,
+ count: Number(g.count),
+ totalAmount: Number(g.totalAmount) || 0,
+ rows,
+ };
+ })
+ );
+
+ return {
+ groups,
+ totalGroups: groups.length,
+ };
+}
diff --git a/app/[lng]/test/table-v2/column-defs.ts b/app/[lng]/test/table-v2/column-defs.ts
new file mode 100644
index 00000000..3ece4287
--- /dev/null
+++ b/app/[lng]/test/table-v2/column-defs.ts
@@ -0,0 +1,60 @@
+/**
+ * Column Definitions for Server Actions
+ *
+ * 서버 액션에서 DrizzleTableAdapter가 사용할 컬럼 정의입니다.
+ * React 컴포넌트 없이 accessorKey만 정의합니다.
+ */
+
+import { ColumnDef } from "@tanstack/react-table";
+import { TestProduct } from "@/db/schema/test-table-v2";
+
+/**
+ * 서버 사이드 기능을 위한 컬럼 메타 정보
+ */
+export interface ServerColumnMeta {
+ /** 서버에서 GROUP BY 가능 여부 (DB 컬럼에 직접 매핑되어야 함) */
+ serverGroupable?: boolean;
+ /** 서버에서 정렬 가능 여부 */
+ serverSortable?: boolean;
+ /** 서버에서 필터 가능 여부 */
+ serverFilterable?: boolean;
+}
+
+// === Product Columns (Server-side compatible) ===
+// DrizzleTableAdapter는 accessorKey만 사용하므로 cell renderer가 필요 없습니다.
+// meta.serverGroupable로 서버 GROUP BY 지원 여부를 표시합니다.
+
+type ProductColumnDef = ColumnDef<TestProduct, any> & { meta?: ServerColumnMeta };
+
+export const productColumnDefs: ProductColumnDef[] = [
+ { accessorKey: "id", meta: { serverGroupable: false } }, // PK는 그룹핑 의미 없음
+ { accessorKey: "sku", meta: { serverGroupable: false } }, // Unique 값
+ { accessorKey: "name", meta: { serverGroupable: false } }, // 이름은 그룹핑 비효율
+ { accessorKey: "category", meta: { serverGroupable: true } }, // ✅ 그룹핑 적합
+ { accessorKey: "price", meta: { serverGroupable: false } },
+ { accessorKey: "stock", meta: { serverGroupable: false } },
+ { accessorKey: "status", meta: { serverGroupable: true } }, // ✅ 그룹핑 적합
+ { accessorKey: "isNew", meta: { serverGroupable: true } }, // ✅ 그룹핑 적합
+ { accessorKey: "createdAt", meta: { serverGroupable: false } },
+ { accessorKey: "updatedAt", meta: { serverGroupable: false } },
+];
+
+// === Order Columns for joined data (Pattern 3) ===
+// Custom Service에서는 DrizzleTableAdapter를 사용하지 않고 직접 쿼리합니다.
+// 조인된 데이터의 컬럼은 단일 테이블에 매핑되지 않기 때문입니다.
+
+export type OrderWithDetails = {
+ id: number;
+ orderNumber: string;
+ quantity: number;
+ unitPrice: string;
+ totalAmount: string;
+ status: string;
+ orderedAt: Date;
+ customerName: string | null;
+ customerEmail: string | null;
+ customerTier: string | null;
+ productName: string | null;
+ productSku: string | null;
+};
+
diff --git a/app/[lng]/test/table-v2/columns.tsx b/app/[lng]/test/table-v2/columns.tsx
new file mode 100644
index 00000000..703e9fd8
--- /dev/null
+++ b/app/[lng]/test/table-v2/columns.tsx
@@ -0,0 +1,212 @@
+"use client";
+
+import { ColumnDef } from "@tanstack/react-table";
+import { Badge } from "@/components/ui/badge";
+import { TestProduct } from "@/db/schema/test-table-v2";
+import { OrderWithDetails } from "./column-defs";
+
+// === Product Columns (Pattern 1, 2) ===
+// meta.serverGroupable: 서버 사이드 GROUP BY 지원 여부
+
+export const productColumns: ColumnDef<TestProduct>[] = [
+ {
+ accessorKey: "id",
+ header: "ID",
+ size: 60,
+ enableGrouping: false, // 클라이언트 그룹핑도 비활성화
+ meta: { serverGroupable: false },
+ },
+ {
+ accessorKey: "sku",
+ header: "SKU",
+ size: 100,
+ enableGrouping: false,
+ meta: { serverGroupable: false },
+ },
+ {
+ accessorKey: "name",
+ header: "Product Name",
+ size: 200,
+ enableGrouping: false,
+ meta: { serverGroupable: false },
+ },
+ {
+ accessorKey: "category",
+ header: "Category",
+ size: 120,
+ enableGrouping: true, // ✅ 그룹핑 가능
+ meta: { serverGroupable: true },
+ cell: ({ getValue }) => {
+ const category = getValue() as string;
+ return <Badge variant="outline">{category}</Badge>;
+ },
+ },
+ {
+ accessorKey: "price",
+ header: "Price",
+ size: 100,
+ enableGrouping: false,
+ meta: { serverGroupable: false },
+ cell: ({ getValue }) => {
+ const price = parseFloat(getValue() as string);
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(price);
+ },
+ },
+ {
+ accessorKey: "stock",
+ header: "Stock",
+ size: 80,
+ enableGrouping: false,
+ meta: { serverGroupable: false },
+ cell: ({ getValue }) => {
+ const stock = getValue() as number;
+ return (
+ <span className={stock < 10 ? "text-red-500 font-medium" : ""}>
+ {stock}
+ </span>
+ );
+ },
+ },
+ {
+ accessorKey: "status",
+ header: "Status",
+ size: 110,
+ enableGrouping: true, // ✅ 그룹핑 가능
+ meta: { serverGroupable: true },
+ cell: ({ getValue }) => {
+ const status = getValue() as string;
+ const variants: Record<string, "default" | "secondary" | "destructive"> = {
+ active: "default",
+ inactive: "secondary",
+ discontinued: "destructive",
+ };
+ return <Badge variant={variants[status] || "secondary"}>{status}</Badge>;
+ },
+ },
+ {
+ accessorKey: "isNew",
+ header: "New",
+ size: 60,
+ enableGrouping: true, // ✅ 그룹핑 가능
+ meta: { serverGroupable: true },
+ cell: ({ getValue }) => {
+ const isNew = getValue() as boolean;
+ return isNew ? <Badge className="bg-emerald-500">NEW</Badge> : null;
+ },
+ },
+ {
+ accessorKey: "createdAt",
+ header: "Created",
+ size: 110,
+ enableGrouping: false,
+ meta: { serverGroupable: false },
+ cell: ({ getValue }) => {
+ const date = getValue() as Date;
+ return date ? new Date(date).toLocaleDateString() : "-";
+ },
+ },
+];
+
+// === Order Columns with joined data (Pattern 3 - Custom Service) ===
+
+export const orderColumns: ColumnDef<OrderWithDetails>[] = [
+ {
+ accessorKey: "id",
+ header: "ID",
+ size: 60,
+ },
+ {
+ accessorKey: "orderNumber",
+ header: "Order #",
+ size: 140,
+ cell: ({ getValue }) => (
+ <span className="font-mono text-xs">{getValue() as string}</span>
+ ),
+ },
+ {
+ accessorKey: "customerName",
+ header: "Customer",
+ size: 150,
+ },
+ {
+ accessorKey: "customerTier",
+ header: "Tier",
+ size: 90,
+ cell: ({ getValue }) => {
+ const tier = getValue() as string;
+ if (!tier) return "-";
+ const colors: Record<string, string> = {
+ standard: "bg-gray-500",
+ premium: "bg-blue-500",
+ vip: "bg-amber-500",
+ };
+ return (
+ <Badge className={colors[tier] || "bg-gray-500"}>
+ {tier.toUpperCase()}
+ </Badge>
+ );
+ },
+ },
+ {
+ accessorKey: "productName",
+ header: "Product",
+ size: 180,
+ },
+ {
+ accessorKey: "quantity",
+ header: "Qty",
+ size: 60,
+ },
+ {
+ accessorKey: "totalAmount",
+ header: "Total",
+ size: 100,
+ cell: ({ getValue }) => {
+ const amount = parseFloat(getValue() as string);
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(amount);
+ },
+ },
+ {
+ accessorKey: "status",
+ header: "Status",
+ size: 110,
+ cell: ({ getValue }) => {
+ const status = getValue() as string;
+ const variants: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
+ pending: "outline",
+ processing: "secondary",
+ shipped: "default",
+ delivered: "default",
+ cancelled: "destructive",
+ };
+ const colors: Record<string, string> = {
+ delivered: "bg-emerald-500",
+ shipped: "bg-blue-500",
+ };
+ return (
+ <Badge
+ variant={variants[status] || "secondary"}
+ className={colors[status] || ""}
+ >
+ {status}
+ </Badge>
+ );
+ },
+ },
+ {
+ accessorKey: "orderedAt",
+ header: "Order Date",
+ size: 110,
+ cell: ({ getValue }) => {
+ const date = getValue() as Date;
+ return date ? new Date(date).toLocaleDateString() : "-";
+ },
+ },
+];
+
diff --git a/app/[lng]/test/table-v2/page.tsx b/app/[lng]/test/table-v2/page.tsx
new file mode 100644
index 00000000..65c0ee1d
--- /dev/null
+++ b/app/[lng]/test/table-v2/page.tsx
@@ -0,0 +1,637 @@
+"use client";
+
+import * as React from "react";
+import { PaginationState, SortingState, ColumnFiltersState, GroupingState } from "@tanstack/react-table";
+import { ClientVirtualTable } from "@/components/client-table-v2/client-virtual-table";
+import { TestProduct } from "@/db/schema/test-table-v2";
+import { productColumns, orderColumns } from "./columns";
+import { OrderWithDetails } from "./column-defs";
+import {
+ getAllProducts,
+ getProductTableData,
+ getOrderTableData,
+ getProductTableDataWithGrouping,
+ GroupInfo,
+} from "./actions";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { ChevronDown, ChevronRight, Loader2 } from "lucide-react";
+import { cn } from "@/lib/utils";
+
+// ============================================================
+// Reusable Loading Overlay Component
+// ============================================================
+
+function LoadingOverlay({
+ isLoading,
+ children
+}: {
+ isLoading: boolean;
+ children: React.ReactNode
+}) {
+ return (
+ <div className="relative">
+ {children}
+ {isLoading && (
+ <div className="absolute inset-0 z-50 flex items-center justify-center bg-background/60 backdrop-blur-[2px] transition-all duration-200">
+ <div className="flex items-center gap-2 px-4 py-2 bg-background rounded-lg shadow-lg border">
+ <Loader2 className="h-5 w-5 animate-spin text-primary" />
+ <span className="text-sm text-muted-foreground">Loading...</span>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
+
+// ============================================================
+// Pattern 1: Client-Side Table
+// ============================================================
+
+function ClientSideTable() {
+ const [data, setData] = React.useState<TestProduct[]>([]);
+ const [isLoading, setIsLoading] = React.useState(true);
+
+ React.useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const products = await getAllProducts();
+ setData(products);
+ } catch (error) {
+ console.error("Failed to fetch products:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ return (
+ <Card>
+ <CardHeader>
+ <div className="flex items-center gap-2">
+ <CardTitle>Pattern 1: Client-Side</CardTitle>
+ <Badge variant="outline">fetchMode=&quot;client&quot;</Badge>
+ </div>
+ <CardDescription>
+ 모든 데이터를 한 번에 받아와 클라이언트에서 필터링/정렬/페이지네이션/그룹핑 처리합니다.
+ <br />
+ <span className="text-muted-foreground">
+ 적합: 데이터 1000건 이하, 빠른 인터랙션 필요 시
+ </span>
+ <br />
+ <span className="text-emerald-600 text-sm">
+ ✅ 그룹핑: 헤더 우클릭 → Group by [Column]
+ </span>
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <LoadingOverlay isLoading={isLoading}>
+ <div className="h-[500px]">
+ <ClientVirtualTable
+ fetchMode="client"
+ data={data}
+ columns={productColumns}
+ isLoading={false} // LoadingOverlay로 처리
+ enablePagination
+ enableGrouping
+ height="100%"
+ enableUserPreset={true}
+ tableKey="test-table-v2-pattern1"
+ />
+ </div>
+ </LoadingOverlay>
+ </CardContent>
+ </Card>
+ );
+}
+
+// ============================================================
+// Pattern 2: Factory Service (Server-Side)
+// ============================================================
+
+function FactoryServiceTable() {
+ const [data, setData] = React.useState<TestProduct[]>([]);
+ const [totalRows, setTotalRows] = React.useState(0);
+ const [isLoading, setIsLoading] = React.useState(true);
+
+ // Table state
+ const [pagination, setPagination] = React.useState<PaginationState>({
+ pageIndex: 0,
+ pageSize: 10,
+ });
+ const [sorting, setSorting] = React.useState<SortingState>([]);
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
+ const [globalFilter, setGlobalFilter] = React.useState("");
+
+ // Fetch data on state change
+ React.useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const result = await getProductTableData({
+ pagination,
+ sorting,
+ columnFilters,
+ globalFilter,
+ });
+ setData(result.data);
+ setTotalRows(result.totalRows);
+ } catch (error) {
+ console.error("Failed to fetch products:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [pagination, sorting, columnFilters, globalFilter]);
+
+ return (
+ <Card>
+ <CardHeader>
+ <div className="flex items-center gap-2">
+ <CardTitle>Pattern 2: Factory Service</CardTitle>
+ <Badge variant="outline">fetchMode=&quot;server&quot;</Badge>
+ <Badge variant="secondary">createTableService</Badge>
+ </div>
+ <CardDescription>
+ <code>createTableService</code>로 서버 액션을 자동 생성합니다.
+ <br />
+ <span className="text-muted-foreground">
+ 적합: 단순 CRUD, 마스터 테이블 조회
+ </span>
+ <br />
+ <span className="text-amber-600 text-sm">
+ ⚠️ 그룹핑: 서버 모드에서는 별도 구현 필요 (Pattern 2-B 참고)
+ </span>
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <LoadingOverlay isLoading={isLoading}>
+ <div className="h-[500px]">
+ <ClientVirtualTable
+ fetchMode="server"
+ data={data}
+ rowCount={totalRows}
+ columns={productColumns}
+ isLoading={false}
+ enablePagination
+ enableGrouping={false}
+ height="100%"
+ pagination={pagination}
+ onPaginationChange={setPagination}
+ sorting={sorting}
+ onSortingChange={setSorting}
+ columnFilters={columnFilters}
+ onColumnFiltersChange={setColumnFilters}
+ globalFilter={globalFilter}
+ onGlobalFilterChange={setGlobalFilter}
+ enableUserPreset={true}
+ tableKey="test-table-v2-pattern-2-A"
+ />
+ </div>
+ </LoadingOverlay>
+ </CardContent>
+ </Card>
+ );
+}
+
+// ============================================================
+// Pattern 2-B: Server-Side Grouping (Context Menu 방식)
+// ============================================================
+
+function ServerGroupingTable() {
+ const [grouping, setGrouping] = React.useState<GroupingState>([]);
+ const [expandedGroups, setExpandedGroups] = React.useState<string[]>([]);
+ const [groups, setGroups] = React.useState<GroupInfo[]>([]);
+ const [flatData, setFlatData] = React.useState<TestProduct[]>([]);
+ const [isGrouped, setIsGrouped] = React.useState(false);
+ const [isLoading, setIsLoading] = React.useState(true);
+ const [totalRows, setTotalRows] = React.useState(0);
+ const [sorting, setSorting] = React.useState<SortingState>([]);
+
+ const [pagination, setPagination] = React.useState<PaginationState>({
+ pageIndex: 0,
+ pageSize: 10,
+ });
+
+ // 데이터 페칭
+ React.useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const result = await getProductTableDataWithGrouping(
+ { pagination, grouping, sorting },
+ expandedGroups
+ );
+
+ if ('groups' in result) {
+ setGroups(result.groups);
+ setIsGrouped(true);
+ setFlatData([]);
+ } else {
+ setFlatData(result.data);
+ setTotalRows(result.totalRows);
+ setIsGrouped(false);
+ setGroups([]);
+ }
+ } catch (error) {
+ console.error("Failed to fetch:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [pagination, grouping, sorting, expandedGroups]);
+
+ // 그룹 토글
+ const toggleGroup = (groupKey: string) => {
+ setExpandedGroups(prev =>
+ prev.includes(groupKey)
+ ? prev.filter(k => k !== groupKey)
+ : [...prev, groupKey]
+ );
+ };
+
+ // 그룹핑 상태 변경 핸들러 (Context Menu에서 호출됨)
+ const handleGroupingChange = React.useCallback((updater: GroupingState | ((old: GroupingState) => GroupingState)) => {
+ const newGrouping = typeof updater === 'function' ? updater(grouping) : updater;
+ setGrouping(newGrouping);
+ setExpandedGroups([]); // 그룹핑 변경 시 확장 상태 초기화
+ }, [grouping]);
+
+ return (
+ <Card>
+ <CardHeader>
+ <div className="flex items-center gap-2">
+ <CardTitle>Pattern 2-B: Server-Side Grouping</CardTitle>
+ <Badge variant="outline">fetchMode=&quot;server&quot;</Badge>
+ <Badge className="bg-emerald-500">GROUP BY</Badge>
+ </div>
+ <CardDescription>
+ 서버에서 GROUP BY + 집계 쿼리로 그룹 정보를 조회합니다.
+ <br />
+ <span className="text-emerald-600 text-sm">
+ ✅ 그룹핑: 헤더 우클릭 → Group by [Column] (category, status, isNew만 지원)
+ </span>
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {/* 현재 그룹핑 상태 표시 */}
+ {grouping.length > 0 && (
+ <div className="flex items-center gap-2 text-sm">
+ <span className="text-muted-foreground">Grouped by:</span>
+ {grouping.map((col) => (
+ <Badge key={col} variant="secondary">
+ {col}
+ <button
+ className="ml-1 hover:text-destructive"
+ onClick={() => setGrouping([])}
+ >
+ ×
+ </button>
+ </Badge>
+ ))}
+ </div>
+ )}
+
+ {/* Content with Loading Overlay */}
+ <LoadingOverlay isLoading={isLoading}>
+ <div className="border rounded-md min-h-[400px] max-h-[500px] overflow-auto">
+ {isGrouped ? (
+ // Grouped View - Custom Rendering
+ <div className="divide-y">
+ {groups.length === 0 ? (
+ <div className="flex items-center justify-center h-[400px] text-muted-foreground">
+ No data
+ </div>
+ ) : (
+ groups.map((group) => (
+ <div key={group.groupKey}>
+ {/* Group Header */}
+ <button
+ className="w-full px-4 py-3 flex items-center gap-3 hover:bg-muted/50 transition-colors text-left"
+ onClick={() => toggleGroup(group.groupKey)}
+ >
+ {expandedGroups.includes(group.groupKey) ? (
+ <ChevronDown className="w-4 h-4" />
+ ) : (
+ <ChevronRight className="w-4 h-4" />
+ )}
+ <span className="font-medium">
+ {grouping[0]}: <Badge variant="outline">{String(group.groupValue)}</Badge>
+ </span>
+ <span className="text-muted-foreground text-sm">
+ ({group.count} items)
+ </span>
+ </button>
+
+ {/* Expanded Rows */}
+ {expandedGroups.includes(group.groupKey) && group.rows && (
+ <div className="bg-muted/20 border-t">
+ <table className="w-full text-sm">
+ <thead>
+ <tr className="border-b bg-muted/30">
+ <th className="px-4 py-2 text-left">ID</th>
+ <th className="px-4 py-2 text-left">SKU</th>
+ <th className="px-4 py-2 text-left">Name</th>
+ <th className="px-4 py-2 text-left">Price</th>
+ <th className="px-4 py-2 text-left">Stock</th>
+ </tr>
+ </thead>
+ <tbody>
+ {group.rows.map((row) => (
+ <tr key={row.id} className="border-b hover:bg-muted/30">
+ <td className="px-4 py-2">{row.id}</td>
+ <td className="px-4 py-2 font-mono text-xs">{row.sku}</td>
+ <td className="px-4 py-2">{row.name}</td>
+ <td className="px-4 py-2">
+ {new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ }).format(parseFloat(row.price))}
+ </td>
+ <td className="px-4 py-2">{row.stock}</td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ )}
+ </div>
+ ))
+ )}
+ </div>
+ ) : (
+ // Normal Table View with Context Menu Grouping
+ <ClientVirtualTable
+ fetchMode="server"
+ data={flatData}
+ rowCount={totalRows}
+ columns={productColumns}
+ enablePagination
+ enableGrouping // Context Menu에서 Group By 옵션 활성화
+ height="400px"
+ pagination={pagination}
+ onPaginationChange={setPagination}
+ sorting={sorting}
+ onSortingChange={setSorting}
+ // 그룹핑 상태 연결
+ grouping={grouping}
+ onGroupingChange={handleGroupingChange}
+ />
+ )}
+ </div>
+ </LoadingOverlay>
+ </CardContent>
+ </Card>
+ );
+}
+
+// ============================================================
+// Pattern 3: Custom Service (Server-Side with Joins)
+// ============================================================
+
+function CustomServiceTable() {
+ const [data, setData] = React.useState<OrderWithDetails[]>([]);
+ const [totalRows, setTotalRows] = React.useState(0);
+ const [isLoading, setIsLoading] = React.useState(true);
+
+ // Table state
+ const [pagination, setPagination] = React.useState<PaginationState>({
+ pageIndex: 0,
+ pageSize: 10,
+ });
+ const [sorting, setSorting] = React.useState<SortingState>([]);
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
+ const [globalFilter, setGlobalFilter] = React.useState("");
+
+ // Fetch data on state change
+ React.useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const result = await getOrderTableData({
+ pagination,
+ sorting,
+ columnFilters,
+ globalFilter,
+ });
+ setData(result.data);
+ setTotalRows(result.totalRows);
+ } catch (error) {
+ console.error("Failed to fetch orders:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [pagination, sorting, columnFilters, globalFilter]);
+
+ return (
+ <Card>
+ <CardHeader>
+ <div className="flex items-center gap-2">
+ <CardTitle>Pattern 3: Custom Service</CardTitle>
+ <Badge variant="outline">fetchMode=&quot;server&quot;</Badge>
+ <Badge variant="secondary">DrizzleTableAdapter</Badge>
+ </div>
+ <CardDescription>
+ <code>DrizzleTableAdapter</code>를 도구로 사용하여 복잡한 조인 쿼리를 직접 작성합니다.
+ <br />
+ <span className="text-muted-foreground">
+ 적합: 여러 테이블 조인, 복잡한 비즈니스 로직
+ </span>
+ <br />
+ <span className="text-amber-600 text-sm">
+ ⚠️ 그룹핑: 가상 컬럼(조인 결과)은 서버 GROUP BY 불가
+ </span>
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <LoadingOverlay isLoading={isLoading}>
+ <div className="h-[500px]">
+ <ClientVirtualTable
+ fetchMode="server"
+ data={data}
+ rowCount={totalRows}
+ columns={orderColumns}
+ isLoading={false}
+ enablePagination
+ enableGrouping={false}
+ height="100%"
+ pagination={pagination}
+ onPaginationChange={setPagination}
+ sorting={sorting}
+ onSortingChange={setSorting}
+ columnFilters={columnFilters}
+ onColumnFiltersChange={setColumnFilters}
+ globalFilter={globalFilter}
+ onGlobalFilterChange={setGlobalFilter}
+ />
+ </div>
+ </LoadingOverlay>
+ </CardContent>
+ </Card>
+ );
+}
+
+// ============================================================
+// Main Page
+// ============================================================
+
+export default function TableV2TestPage() {
+ return (
+ <div className="container py-6 space-y-6">
+ <div>
+ <h1 className="text-3xl font-bold tracking-tight">
+ ClientVirtualTable V2 - 데이터 페칭 패턴 테스트
+ </h1>
+ <p className="text-muted-foreground mt-2">
+ GUIDE.md에 정의된 데이터 페칭 패턴과 그룹핑 처리 방법을 테스트합니다.
+ <br />
+ 테스트 전 시딩이 필요합니다: <code className="bg-muted px-1 rounded">npx tsx db/seeds/test-table-v2.ts</code>
+ </p>
+ </div>
+
+ <Tabs defaultValue="pattern1" className="space-y-4">
+ <TabsList className="grid w-full grid-cols-4">
+ <TabsTrigger value="pattern1">
+ 1. Client-Side
+ </TabsTrigger>
+ <TabsTrigger value="pattern2">
+ 2. Factory Service
+ </TabsTrigger>
+ <TabsTrigger value="pattern2b">
+ 2-B. Server Grouping
+ </TabsTrigger>
+ <TabsTrigger value="pattern3">
+ 3. Custom Service
+ </TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="pattern1">
+ <ClientSideTable />
+ </TabsContent>
+
+ <TabsContent value="pattern2">
+ <FactoryServiceTable />
+ </TabsContent>
+
+ <TabsContent value="pattern2b">
+ <ServerGroupingTable />
+ </TabsContent>
+
+ <TabsContent value="pattern3">
+ <CustomServiceTable />
+ </TabsContent>
+ </Tabs>
+
+ {/* Summary Table */}
+ <Card>
+ <CardHeader>
+ <CardTitle>패턴별 그룹핑 지원 현황</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="overflow-x-auto">
+ <table className="w-full text-sm">
+ <thead>
+ <tr className="border-b">
+ <th className="text-left py-2 px-4">패턴</th>
+ <th className="text-left py-2 px-4">그룹핑 방식</th>
+ <th className="text-left py-2 px-4">가상 컬럼 지원</th>
+ <th className="text-left py-2 px-4">비고</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr className="border-b">
+ <td className="py-2 px-4 font-medium">1. Client-Side</td>
+ <td className="py-2 px-4">
+ <Badge className="bg-emerald-500">TanStack Grouping</Badge>
+ </td>
+ <td className="py-2 px-4">
+ <Badge className="bg-emerald-500">✓ 지원</Badge>
+ </td>
+ <td className="py-2 px-4 text-muted-foreground">
+ 메모리에서 처리, 전체 데이터 필요
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2 px-4 font-medium">2. Factory Service</td>
+ <td className="py-2 px-4">
+ <Badge variant="outline">미지원</Badge>
+ </td>
+ <td className="py-2 px-4">-</td>
+ <td className="py-2 px-4 text-muted-foreground">
+ 별도 구현 필요 (2-B 참고)
+ </td>
+ </tr>
+ <tr className="border-b">
+ <td className="py-2 px-4 font-medium">2-B. Server Grouping</td>
+ <td className="py-2 px-4">
+ <Badge className="bg-blue-500">DB GROUP BY</Badge>
+ </td>
+ <td className="py-2 px-4">
+ <Badge variant="destructive">✗ 불가</Badge>
+ </td>
+ <td className="py-2 px-4 text-muted-foreground">
+ serverGroupable 컬럼만 가능
+ </td>
+ </tr>
+ <tr>
+ <td className="py-2 px-4 font-medium">3. Custom Service</td>
+ <td className="py-2 px-4">
+ <Badge variant="secondary">커스텀 구현</Badge>
+ </td>
+ <td className="py-2 px-4">
+ <Badge variant="secondary">선택적</Badge>
+ </td>
+ <td className="py-2 px-4 text-muted-foreground">
+ 쿼리 설계에 따라 다름
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* Column Groupability Info */}
+ <Card>
+ <CardHeader>
+ <CardTitle>컬럼별 서버 그룹핑 지원 여부</CardTitle>
+ <CardDescription>
+ <code>meta.serverGroupable</code> 플래그로 DB GROUP BY 가능 여부를 표시합니다.
+ <br />
+ 헤더 우클릭 시 &quot;Group by [Column]&quot; 메뉴가 표시됩니다.
+ </CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div className="flex flex-wrap gap-2">
+ {productColumns.map((col) => {
+ if (!('accessorKey' in col)) return null;
+ const meta = col.meta as { serverGroupable?: boolean } | undefined;
+ const isGroupable = meta?.serverGroupable;
+ return (
+ <Badge
+ key={col.accessorKey as string}
+ variant={isGroupable ? "default" : "outline"}
+ className={isGroupable ? "bg-emerald-500" : ""}
+ >
+ {col.accessorKey as string}
+ {isGroupable && " ✓"}
+ </Badge>
+ );
+ })}
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+ );
+}
diff --git a/app/[lng]/test/table-v3/page.tsx b/app/[lng]/test/table-v3/page.tsx
new file mode 100644
index 00000000..eccf7cff
--- /dev/null
+++ b/app/[lng]/test/table-v3/page.tsx
@@ -0,0 +1,188 @@
+"use client";
+
+import * as React from "react";
+import { useClientTable, ClientVirtualTable } from "@/components/client-table-v3";
+import { productColumns } from "../table-v2/columns";
+import {
+ getProductTableData,
+ getAllProducts,
+ getProductTableDataWithGrouping,
+ GroupInfo
+} from "../table-v2/actions";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { ChevronDown, ChevronRight } from "lucide-react";
+
+// --- Components for Examples ---
+
+function ClientSideExample() {
+ const [products, setProducts] = React.useState<any[]>([]);
+
+ // Load initial data once
+ React.useEffect(() => {
+ getAllProducts().then(setProducts);
+ }, []);
+
+ // Hook handles table state
+ const { table, isLoading } = useClientTable({
+ fetchMode: "client",
+ data: products,
+ columns: productColumns,
+ enablePagination: true,
+ enableGrouping: true,
+ });
+
+ return (
+ <Card>
+ <CardHeader>
+ <CardTitle>Pattern 1: Client-Side (V3)</CardTitle>
+ <CardDescription>
+ Uses `useClientTable` hook for simplified state management.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="h-[500px]">
+ <ClientVirtualTable
+ table={table}
+ isLoading={isLoading}
+ enableUserPreset
+ tableKey="v3-client-pattern"
+ />
+ </CardContent>
+ </Card>
+ );
+}
+
+function ServerFactoryExample() {
+ // Hook handles everything: state, fetching, debouncing
+ const { table, isLoading } = useClientTable({
+ fetchMode: "server",
+ fetcher: getProductTableData,
+ columns: productColumns,
+ enablePagination: true,
+ enableUserPreset: true, // We can enable this in options too? No, hook doesn't care. Component cares.
+ });
+
+ return (
+ <Card>
+ <CardHeader>
+ <CardTitle>Pattern 2: Factory Service (V3)</CardTitle>
+ <CardDescription>
+ Zero boilerplate state management in the component.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="h-[500px]">
+ <ClientVirtualTable
+ table={table}
+ isLoading={isLoading}
+ enableUserPreset
+ tableKey="v3-server-pattern"
+ />
+ </CardContent>
+ </Card>
+ );
+}
+
+function ServerGroupingExample() {
+ // Adapter for V2 fetcher signature to work with V3 hook
+ // The V2 action expects (state, expandedGroups), but V3 hook passes (state).
+ // We wrap it to extract expandedGroups from state.expanded.
+ const fetcher = React.useCallback((state: any) => {
+ const expanded = state.expanded || {};
+ // Convert TanStack ExpandedState { [key]: true } to string[]
+ const expandedKeys = Object.keys(expanded).filter(k => expanded[k]);
+ return getProductTableDataWithGrouping(state, expandedKeys);
+ }, []);
+
+ // Pattern 2-B support
+ const {
+ table,
+ isLoading,
+ isServerGrouped,
+ serverGroups,
+ refresh,
+ } = useClientTable({
+ fetchMode: "server",
+ fetcher,
+ columns: productColumns,
+ enablePagination: true,
+ enableGrouping: true,
+ });
+
+ // When serverGroups change (new grouping), reset expansion
+ // (In a real app, you might want to persist expansion logic in the fetcher wrapper or hook)
+
+ return (
+ <Card>
+ <CardHeader>
+ <CardTitle>Pattern 2-B: Server Grouping (V3)</CardTitle>
+ <CardDescription>
+ Hook manages state, Component manages Custom Rendering for Groups.
+ </CardDescription>
+ </CardHeader>
+ <CardContent className="h-[500px] flex flex-col">
+ {/* We need the toolbar even in grouped mode */}
+ <div className="mb-4 p-2 border rounded bg-muted/20">
+ <p className="text-sm text-muted-foreground">
+ Group by a column (Category, Status, IsNew) to see server grouping.
+ </p>
+ </div>
+
+ {isServerGrouped ? (
+ <div className="overflow-auto border rounded-md p-4 space-y-2">
+ {serverGroups.map((group: GroupInfo) => (
+ <div key={group.groupKey} className="border rounded p-2">
+ <div className="font-bold flex items-center gap-2">
+ <Badge variant="outline">{String(group.groupValue)}</Badge>
+ <span>({group.count})</span>
+ </div>
+ {/* Rows would go here */}
+ </div>
+ ))}
+ </div>
+ ) : (
+ <ClientVirtualTable
+ table={table}
+ isLoading={isLoading}
+ enableUserPreset
+ tableKey="v3-server-grouping"
+ />
+ )}
+ </CardContent>
+ </Card>
+ );
+}
+
+export default function TableV3Page() {
+ return (
+ <div className="container py-8 space-y-8">
+ <div>
+ <h1 className="text-3xl font-bold">ClientVirtualTable V3 DX Demo</h1>
+ <p className="text-muted-foreground">
+ Demonstrating the new `useClientTable` hook for improved Developer Experience.
+ </p>
+ </div>
+
+ <Tabs defaultValue="client">
+ <TabsList>
+ <TabsTrigger value="client">Client-Side</TabsTrigger>
+ <TabsTrigger value="server">Server Factory</TabsTrigger>
+ <TabsTrigger value="grouping">Server Grouping</TabsTrigger>
+ </TabsList>
+
+ <TabsContent value="client">
+ <ClientSideExample />
+ </TabsContent>
+
+ <TabsContent value="server">
+ <ServerFactoryExample />
+ </TabsContent>
+
+ <TabsContent value="grouping">
+ <ServerGroupingExample />
+ </TabsContent>
+ </Tabs>
+ </div>
+ );
+}
+
diff --git a/components/client-table-v2/GUIDE-v2.md b/components/client-table-v2/GUIDE-v2.md
new file mode 100644
index 00000000..930123fb
--- /dev/null
+++ b/components/client-table-v2/GUIDE-v2.md
@@ -0,0 +1,93 @@
+# ClientVirtualTable V2 — Server Fetching Guide
+
+This guide focuses on `fetchMode="server"` usage (Tabs 2, 2-B, 3 in `/[lng]/test/table-v2`). Client mode is unchanged from `GUIDE.md`.
+
+## Core Concepts
+- `fetchMode="server"` sets `manualPagination|manualSorting|manualFiltering|manualGrouping` to true. The table **renders what the server returns**; no client-side sorting/filtering/pagination is applied.
+- You must control table state (pagination, sorting, filters, grouping, globalFilter) in the parent and refetch on change.
+- Provide `rowCount` (and optionally `pageCount`) so the pagination footer is accurate.
+- Export uses the current row model; in server mode it only exports the loaded page unless you fetch everything yourself.
+
+## Minimal Wiring (Factory Service)
+```tsx
+const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
+const [sorting, setSorting] = useState([]);
+const [columnFilters, setColumnFilters] = useState([]);
+const [globalFilter, setGlobalFilter] = useState("");
+const [data, setData] = useState([]);
+const [rowCount, setRowCount] = useState(0);
+const [loading, setLoading] = useState(false);
+
+useEffect(() => {
+ const run = async () => {
+ setLoading(true);
+ const res = await getTableData({
+ pagination,
+ sorting,
+ columnFilters,
+ globalFilter,
+ });
+ setData(res.data);
+ setRowCount(res.totalRows);
+ setLoading(false);
+ };
+ run();
+}, [pagination, sorting, columnFilters, globalFilter]);
+
+<ClientVirtualTable
+ fetchMode="server"
+ data={data}
+ rowCount={rowCount}
+ columns={columns}
+ isLoading={loading}
+ enablePagination
+ pagination={pagination}
+ onPaginationChange={setPagination}
+ sorting={sorting}
+ onSortingChange={setSorting}
+ columnFilters={columnFilters}
+ onColumnFiltersChange={setColumnFilters}
+ globalFilter={globalFilter}
+ onGlobalFilterChange={setGlobalFilter}
+/>
+```
+
+## Using `createTableService` (Pattern 2)
+- Import `createTableService` in a server action and pass `columns` (accessorKey-based) plus schema/db.
+- The adapter maps `sorting`, `columnFilters`, `globalFilter`, `pagination` to Drizzle query parts.
+- Returned shape: `{ data, totalRows, pageCount }`. Always forward `totalRows` to the client.
+
+## Custom Service (Pattern 3)
+- Build custom joins manually; still read `tableState` for pagination/sorting/filtering if you need them.
+- For sorting: map `tableState.sorting` IDs to your joined columns; provide a default order if none is set.
+- Grouping in custom services requires manual implementation (see `getOrderTableDataGroupedByStatus` pattern).
+
+## Server Grouping (Pattern 2-B)
+- Only columns marked `meta.serverGroupable` in server column defs should be used.
+- Group headers are fetched via DB `GROUP BY`; expanded rows are fetched per group.
+- When grouping is active, the table may render a custom grouped view instead of the virtual table; ensure your fetcher returns either `{ groups }` or `{ data, totalRows }`.
+
+## Presets in Server Mode
+- Presets store: sorting, columnFilters, globalFilter, columnVisibility, columnPinning, columnOrder, grouping, pageSize.
+- Loading a preset triggers the table’s `set*` APIs; parent `on*Change` handlers refetch with the restored state.
+- The component resets pageIndex to 0 when applying a preset to avoid out-of-range requests after pageSize changes.
+- Use unique `tableKey` per screen to avoid clashing presets across pages.
+
+## Common Pitfalls
+- Forgetting `rowCount` → pagination shows wrong totals.
+- Not reacting to `sorting`/`filters`/`grouping` changes in your effect → UI toggles with no data change.
+- Mapping `sorting` IDs to columns incorrectly in custom services → server ignores the sort.
+- Mixing client-side models with server mode: do not enable client `getSortedRowModel`/`getFilteredRowModel` for server fetches (the component already skips them when `fetchMode="server"`).
+
+## Feature Matrix (Server Mode)
+- Sorting: Supported; must be implemented in the server fetcher.
+- Filtering: Supported; column filters/global filter forwarded; implement in server.
+- Pagination: Supported; manual; provide `rowCount`.
+- Grouping: Client grouping is off in server mode; implement via server `GROUP BY` or custom grouped view.
+- Column show/hide, pinning, reorder: Client-side only; state is preserved and sent to presets but does not affect server queries unless you opt to read it.
+- Export: Exports the currently loaded rows; fetch all data yourself for full exports.
+
+## Debug Checklist
+- Confirm `fetchMode="server"` and `rowCount` are set.
+- Verify the parent effect depends on `pagination`, `sorting`, `columnFilters`, `globalFilter`, and (if used) `grouping`.
+- In custom services, console/log the incoming `tableState` to confirm the UI is sending the intended state.
diff --git a/components/client-table-v2/GUIDE-v3-ko.md b/components/client-table-v2/GUIDE-v3-ko.md
new file mode 100644
index 00000000..9ec71065
--- /dev/null
+++ b/components/client-table-v2/GUIDE-v3-ko.md
@@ -0,0 +1,92 @@
+# ClientVirtualTable V3 가이드 (한국어)
+
+`components/client-table-v2` 테이블 컴포넌트와 `fetchMode="server"` 사용 시 주의점을 정리했습니다.
+
+## 모듈 맵
+- `client-virtual-table.tsx`: 코어 테이블(가상 스크롤, 컬럼 DnD, 핀/숨김, 프리셋, 툴바, 페이지네이션).
+- `client-table-column-header.tsx`: 헤더 셀(정렬 토글, 필터 UI, 컨텍스트 메뉴: 핀/숨김/그룹/재정렬).
+- `client-table-toolbar.tsx` (client-table): 검색, 내보내기, 뷰 옵션, 프리셋 엔트리.
+- `client-table-view-options.tsx` (client-table): 컬럼 표시/숨김 토글.
+- `client-table-filter.tsx`: 컬럼 필터 UI(text/select/boolean).
+- `client-table-preset.tsx`: `tableKey`+사용자별 프리셋 저장/불러오기/삭제.
+- 기타: `export-utils`, `import-utils`, `ClientDataTablePagination`(client-data-table).
+- 서버 헬퍼: `adapter/create-table-service.ts`, `adapter/drizzle-table-adapter.ts`.
+- 타입: `types.ts`, `preset-types.ts`.
+
+## 핵심 동작 (ClientVirtualTable)
+- 가상 스크롤: `height` 필수, `estimateRowHeight` 기본 40.
+- DnD: 컬럼 재배치, 핀 섹션 간 이동 시 핀 상태 동기화.
+- 핀/숨김/순서: 클라이언트 상태(`columnVisibility`, `columnPinning`, `columnOrder`).
+- 정렬/필터/페이지네이션/그룹핑
+ - `fetchMode="client"`: TanStack 모델 사용.
+ - `fetchMode="server"`: manual 플래그 on, 클라이언트 모델 skip → **서버가 정렬/필터/페이징된 결과를 반환해야 함**.
+- 내보내기: 현재 렌더된 행 기준. 서버 모드에서 전체 내보내기는 직접 `onExport`로 구현 필요.
+- 프리셋: `enableUserPreset`+`tableKey` 설정 시 표시. 불러올 때 pageIndex를 0으로 리셋해 서버 모드에서 범위 오류 방지.
+
+## 주요 Props
+- `fetchMode`: `"client"` | `"server"` (기본 `"client"`).
+- 데이터: `data`, `rowCount?`, `pageCount?`.
+- 상태/핸들러:
+ - 페이지: `pagination`, `onPaginationChange`, `enablePagination`, `manualPagination?`.
+ - 정렬: `sorting`, `onSortingChange`.
+ - 필터: `columnFilters`, `onColumnFiltersChange`, `globalFilter`, `onGlobalFilterChange`.
+ - 그룹핑: `grouping`, `onGroupingChange`, `expanded`, `onExpandedChange`, `enableGrouping`.
+ - 표시/핀/순서: `columnVisibility`, `columnPinning`, `columnOrder` 및 각 onChange.
+ - 선택: `enableRowSelection`, `enableMultiRowSelection`, `rowSelection`, `onRowSelectionChange`.
+- UX: `actions`, `customToolbar`, `enableExport`, `onExport`, `renderHeaderVisualFeedback`, `getRowClassName`, `onRowClick`.
+- 프리셋: `enableUserPreset`, `tableKey`.
+- 메타: `meta`, `getRowId`.
+
+## 서버 페칭 패턴
+### 패턴 1: 클라이언트 모드
+- `fetchMode="client"`, 전체 데이터 전달. 정렬/필터/그룹핑은 브라우저에서 처리.
+
+### 패턴 2: Factory Service (`createTableService`)
+- 서버 액션: `createTableService({ db, schema, columns, defaultWhere?, customQuery? })`.
+- 어댑터가 `sorting`, `columnFilters`, `globalFilter`, `pagination`, `grouping`을 Drizzle `where/orderBy/limit/offset/groupBy`로 변환.
+- 반환 `{ data, totalRows, pageCount }` → 클라이언트에서 `rowCount` 설정 필수.
+- 클라이언트: `pagination/sorting/columnFilters/globalFilter` 제어 후 deps로 `useEffect` 재호출.
+
+### 패턴 2-B: 서버 그룹핑
+- `getProductTableDataWithGrouping` 예시: `grouping` 없으면 일반 페칭, 있으면 DB `GROUP BY` 후 `{ groups }` 반환.
+- 서버 그룹핑 가능한 컬럼(`meta.serverGroupable`)만 사용.
+- 그룹 확장 시 그룹 키별 하위 행을 추가 조회, 그룹 변경 시 확장 상태 초기화.
+- 그룹뷰 렌더 시 가상 테이블 대신 커스텀 블록을 사용할 수 있음.
+
+### 패턴 3: 커스텀 서비스
+- 조인/파생 컬럼용. `tableState`를 읽어 정렬 ID를 조인 컬럼에 매핑, 정렬 없을 때 기본 정렬 제공.
+- 필터/글로벌 필터는 직접 구현해야 함.
+- 그룹핑도 수동 구현(`getOrderTableDataGroupedByStatus` 참고).
+
+## 상태 → 쿼리 매핑 (서버)
+- 정렬: `tableState.sorting`(id, desc) → DB 컬럼 매핑, 모르는 id는 무시.
+- 필터: 텍스트(ilike), 불리언, 숫자, 범위[min,max], 다중선택(IN) 지원.
+- 글로벌 필터: 매핑된 컬럼 OR ilike.
+- 페이지: pageIndex/pageSize → limit/offset, `rowCount` 반환.
+- 그룹핑: 지원 컬럼만 `GROUP BY`.
+
+## 프리셋 (서버 호환)
+- 저장: sorting, columnFilters, globalFilter, columnVisibility, columnPinning, columnOrder, grouping, pageSize.
+- 불러오기: `table.set*` 호출 + pageIndex 0 리셋 → 상위 `on*Change` 핸들러에서 재페칭.
+- 화면별 고유 `tableKey` 사용 권장. 세션 필요.
+
+## 기능 매트릭스 (서버 모드)
+- 정렬: 지원 (서버 구현 필요)
+- 필터: 지원 (서버 구현 필요)
+- 페이지네이션: 지원 (manual, `rowCount` 필요)
+- 그룹핑: 자동 미지원, 서버 그룹핑 또는 커스텀 뷰로 구현
+- 컬럼 숨김/핀/순서: 클라이언트 전용(시각용), 서버 쿼리에 자동 반영 안 함
+- 내보내기: 로드된 행만; 전체 내보내기는 커스텀 `onExport` 필요
+
+## 구현 팁
+- `fetchMode="server"`일 때 `rowCount` 꼭 설정.
+- `pagination/sorting/columnFilters/globalFilter/(grouping)` 변경 시마다 재페칭.
+- 정렬 없을 때 서버 기본 정렬을 지정.
+- 그룹 변경 시 확장 상태 초기화.
+- `height`를 항상 지정(가상 스크롤 컨테이너 필요).
+
+## 빠른 예시
+- 클라이언트: `fetchMode="client"`, 전체 데이터 전달, 그룹핑 옵션 사용 가능.
+- Factory 서버: `fetchMode="server"`, `createTableService`, 제어형 상태 + `rowCount`.
+- 서버 그룹핑: `grouping`에 따라 `{ groups }` vs `{ data }` 반환, `serverGroupable` 컬럼만 허용.
+- 커스텀 조인: 정렬 ID 직접 매핑, 필터/글로벌 직접 적용, `rowCount` 반환.
diff --git a/components/client-table-v2/GUIDE-v3.md b/components/client-table-v2/GUIDE-v3.md
new file mode 100644
index 00000000..21a1217d
--- /dev/null
+++ b/components/client-table-v2/GUIDE-v3.md
@@ -0,0 +1,93 @@
+# ClientVirtualTable V3 Guide
+
+This guide documents the table components in `components/client-table-v2`, with an emphasis on server fetching (`fetchMode="server"`) and how supporting components fit together.
+
+## Module Map
+- `client-virtual-table.tsx`: Core table (virtualized, DnD columns, pin/hide, presets hook point, toolbar, pagination).
+- `client-table-column-header.tsx`: Header cell with sort toggle, filter UI, context menu (hide/pin/group/reorder hook).
+- `client-table-toolbar.tsx` (from `components/client-table`): Search box, export button, view options, preset entry point.
+- `client-table-view-options.tsx` (from `components/client-table`): Column visibility toggles.
+- `client-table-filter.tsx`: Column filter UI (text/select/boolean).
+- `client-table-preset.tsx`: Save/load/delete presets per `tableKey` + user.
+- `client-table-save-view.tsx`, `client-table-preset.tsx`, `client-table-toolbar.tsx`: Preset and view controls.
+- `client-virtual-table` dependencies: `ClientDataTablePagination` (`components/client-data-table`), `export-utils`, `import-utils`.
+- Server helpers: `adapter/create-table-service.ts`, `adapter/drizzle-table-adapter.ts`.
+- Types: `types.ts`, `preset-types.ts`.
+
+## Core Behaviors (ClientVirtualTable)
+- Virtualization: `height` is required; `estimateRowHeight` defaults to 40.
+- Drag & Drop: Columns reorder across pin sections; drag between pin states updates pinning.
+- Pin/Hide/Reorder: Managed client-side; state is exposed via `columnVisibility`, `columnPinning`, `columnOrder`.
+- Sorting/Filtering/Pagination/Grouping:
+ - `fetchMode="client"`: uses TanStack models (`getSortedRowModel`, `getFilteredRowModel`, `getPaginationRowModel`, etc.).
+ - `fetchMode="server"`: sets manual flags true and skips client models; **server must return already-sorted/filtered/paged data**.
+- Export: Uses current row model; in server mode it exports only the loaded rows unless you supply all data yourself via `onExport`.
+- Presets: When `enableUserPreset` and `tableKey` are set, toolbar shows the preset control; loading a preset resets pageIndex to 0 to avoid invalid pages on server mode.
+
+## Key Props (ClientVirtualTable)
+- `fetchMode`: `"client"` | `"server"` (default `"client"`).
+- Data: `data`, `rowCount?`, `pageCount?`.
+- State + handlers (controlled or uncontrolled):
+ - Pagination: `pagination`, `onPaginationChange`, `enablePagination`, `manualPagination?`.
+ - Sorting: `sorting`, `onSortingChange`.
+ - Filters: `columnFilters`, `onColumnFiltersChange`, `globalFilter`, `onGlobalFilterChange`.
+ - Grouping: `grouping`, `onGroupingChange`, `expanded`, `onExpandedChange`, `enableGrouping`.
+ - Visibility/Pinning/Order: `columnVisibility`, `onColumnVisibilityChange`, `columnPinning`, `onColumnPinningChange`, `columnOrder`, `onColumnOrderChange`.
+ - Selection: `enableRowSelection`, `enableMultiRowSelection`, `rowSelection`, `onRowSelectionChange`.
+- UX: `actions`, `customToolbar`, `enableExport`, `onExport`, `renderHeaderVisualFeedback`, `getRowClassName`, `onRowClick`.
+- Presets: `enableUserPreset`, `tableKey`.
+- Meta: `meta`, `getRowId`.
+
+## Server Fetching Patterns
+### Pattern 1: Client-Side (baseline)
+- `fetchMode="client"`, pass full dataset; TanStack handles sorting/filtering/grouping locally.
+
+### Pattern 2: Factory Service (`createTableService`)
+- Server action: `createTableService({ db, schema, columns, defaultWhere?, customQuery? })`.
+- The adapter maps `sorting`, `columnFilters`, `globalFilter`, `pagination`, `grouping` → Drizzle `where`, `orderBy`, `limit`, `offset`, `groupBy`.
+- Returns `{ data, totalRows, pageCount }`; always forward `totalRows` to the client and wire `rowCount`.
+- Client wiring: control `pagination`, `sorting`, `columnFilters`, `globalFilter`; refetch in `useEffect` on those deps.
+
+### Pattern 2-B: Server Grouping
+- Uses `getProductTableDataWithGrouping` sample: if `grouping` is empty → normal server fetch; else returns `{ groups }` built from DB `GROUP BY`.
+- Columns must be marked `meta.serverGroupable` in server column defs.
+- Expanded groups fetch child rows per group key; grouping change clears expanded state.
+- UI may render a custom grouped view (not the virtual table) when grouped.
+
+### Pattern 3: Custom Service
+- For joins/derived columns: read `tableState` and manually map `sorting` IDs to joined columns; supply a default order when no sort is present.
+- Filtering/global filter are not automatic—implement them if needed.
+- Grouping is manual; see `getOrderTableDataGroupedByStatus` pattern for a grouped response shape.
+
+## State → Query Mapping (Server)
+- Sorting: `tableState.sorting` (id, desc) → map to DB columns; ignore unknown ids.
+- Filters: `columnFilters` supports text (ilike), boolean, number, range `[min,max]`, multi-select (IN).
+- Global filter: ilike OR across mapped columns.
+- Pagination: pageIndex/pageSize → limit/offset; return `rowCount`.
+- Grouping: `grouping` → `GROUP BY` for supported columns only.
+
+## Presets (Server-Friendly)
+- Saved keys: sorting, columnFilters, globalFilter, columnVisibility, columnPinning, columnOrder, grouping, pageSize.
+- On load: applies `table.set*` and resets pageIndex to 0; parent `on*Change` handlers should trigger refetch.
+- Use unique `tableKey` per screen to avoid collisions; requires authenticated session.
+
+## Feature Matrix (Server Mode)
+- Sorting: Yes—server implemented.
+- Filtering: Yes—server implemented.
+- Pagination: Yes—manual; provide `rowCount`.
+- Grouping: Not automatic; implement via server grouping or custom grouped view.
+- Column hide/pin/reorder: Client-only (visual); does not change server query unless you opt to read it.
+- Export: Only current rows unless you provide `onExport` with full data.
+
+## Implementation Tips
+- Always set `rowCount` when `fetchMode="server"`.
+- Refetch on `pagination`, `sorting`, `columnFilters`, `globalFilter`, and `grouping` (if used).
+- Provide a default sort on the server when `sorting` is empty.
+- Reset `expanded` or group expand state when grouping changes in server grouping flows.
+- Ensure `height` is set; virtualization needs a scroll container.
+
+## Quick Examples
+- Client: `fetchMode="client"` with `data` = full list; optional grouping enabled.
+- Factory server: `fetchMode="server"`, `createTableService` action, controlled state with `rowCount`.
+- Server grouping: `grouping` drives `{ groups }` vs `{ data }` response; only `serverGroupable` columns allowed.
+- Custom join: Manually map `sorting` ids; apply filters/global; return `rowCount`.
diff --git a/components/client-table-v2/GUIDE.md b/components/client-table-v2/GUIDE.md
new file mode 100644
index 00000000..4ccadfc7
--- /dev/null
+++ b/components/client-table-v2/GUIDE.md
@@ -0,0 +1,178 @@
+# Table Component & Data Fetching Guide
+
+이 가이드는 `ClientVirtualTable`을 사용하여 테이블을 구현하고, 데이터를 페칭하는 3가지 주요 패턴을 설명합니다.
+
+## 개요
+
+프로젝트의 복잡도와 요구사항에 따라 아래 3가지 패턴 중 하나를 선택하여 사용할 수 있습니다.
+
+| 모드 | 패턴 | 적합한 상황 | 특징 |
+|---|---|---|---|
+| **Client** | 1. Client-Side | 데이터가 적을 때 (< 1000건), 빠른 인터랙션 필요 | 전체 데이터 로드 후 브라우저에서 처리 |
+| **Server** | 2. Factory Service | 단순 CRUD, 마스터 테이블 | 코드 1줄로 서버 액션 생성, 빠른 개발 |
+| **Server** | 3. Custom Service | 복잡한 조인, 비즈니스 로직 | 완전한 쿼리 제어, Adapter를 도구로 사용 |
+
+---
+
+## 1. Client-Side (기본 모드)
+
+데이터가 많지 않을 때 가장 간단한 방법입니다. 모든 데이터를 한 번에 받아와 `data` prop으로 넘깁니다.
+
+### 사용법
+
+```tsx
+// page.tsx
+import { getAllUsers } from "@/lib/api/users";
+import { ClientVirtualTable } from "@/components/client-table-v2/client-virtual-table";
+import { columns } from "./columns";
+
+export default async function UsersPage() {
+ const users = await getAllUsers(); // 전체 목록 조회
+
+ return (
+ <ClientVirtualTable
+ fetchMode="client"
+ data={users}
+ columns={columns}
+ enablePagination
+ enableSorting
+ enableFiltering
+ />
+ );
+}
+```
+
+---
+
+## 2. Factory Service (추천 - 단순 조회용)
+
+`createTableService`를 사용하여 서버 사이드 페칭을 위한 액션을 자동으로 생성합니다.
+
+### 1) Server Action 생성
+
+```typescript
+// app/actions/user-table.ts
+"use server"
+
+import { db } from "@/lib/db";
+import { users } from "@/lib/db/schema";
+import { columns } from "@/components/users/columns";
+import { createTableService } from "@/components/client-table-v2/adapter/create-table-service";
+
+// 팩토리 함수로 액션 생성 (한 줄로 끝!)
+export const getUserTableData = createTableService({
+ db,
+ schema: users,
+ columns: columns
+});
+```
+
+### 2) 클라이언트 컴포넌트 연결
+
+```tsx
+// components/users/user-table.tsx
+"use client"
+
+import { getUserTableData } from "@/app/actions/user-table";
+import { ClientVirtualTable } from "@/components/client-table-v2/client-virtual-table";
+import { columns } from "./columns";
+import { useState, useEffect } from "react";
+
+export function UserTable() {
+ const [data, setData] = useState([]);
+ const [totalRows, setTotalRows] = useState(0);
+ const [loading, setLoading] = useState(false);
+
+ // 테이블 상태 관리
+ const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 10 });
+ const [sorting, setSorting] = useState([]);
+ const [columnFilters, setColumnFilters] = useState([]);
+ const [globalFilter, setGlobalFilter] = useState("");
+
+ // 데이터 페칭
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true);
+ const result = await getUserTableData({
+ pagination,
+ sorting,
+ columnFilters,
+ globalFilter
+ });
+ setData(result.data);
+ setTotalRows(result.totalRows);
+ setLoading(false);
+ };
+
+ fetchData();
+ }, [pagination, sorting, columnFilters, globalFilter]);
+
+ return (
+ <ClientVirtualTable
+ fetchMode="server"
+ data={data}
+ rowCount={totalRows}
+ columns={columns}
+ isLoading={loading}
+ // 상태 연결
+ pagination={pagination} onPaginationChange={setPagination}
+ sorting={sorting} onSortingChange={setSorting}
+ columnFilters={columnFilters} onColumnFiltersChange={setColumnFilters}
+ globalFilter={globalFilter} onGlobalFilterChange={setGlobalFilter}
+ />
+ );
+}
+```
+
+---
+
+## 3. Custom Service (복잡한 로직용)
+
+여러 테이블을 조인하거나, 특정 권한 체크 등 복잡한 로직이 필요할 때는 `DrizzleTableAdapter`를 직접 사용합니다.
+
+### 1) Custom Server Action 작성
+
+```typescript
+// app/actions/order-table.ts
+"use server"
+
+import { db } from "@/lib/db";
+import { orders, users } from "@/lib/db/schema";
+import { DrizzleTableAdapter } from "@/components/client-table-v2/adapter/drizzle-table-adapter";
+import { count, eq } from "drizzle-orm";
+
+export async function getOrderTableData(tableState) {
+ // 1. 어댑터로 조건절 생성
+ const adapter = new DrizzleTableAdapter(orders, columns);
+ const { where, orderBy, limit, offset } = adapter.getQueryParts(tableState);
+
+ // 2. 커스텀 쿼리 작성 (예: 유저 조인)
+ const data = await db
+ .select({
+ orderId: orders.id,
+ amount: orders.amount,
+ userName: users.name // 조인된 컬럼
+ })
+ .from(orders)
+ .leftJoin(users, eq(orders.userId, users.id))
+ .where(where)
+ .orderBy(...orderBy)
+ .limit(limit)
+ .offset(offset);
+
+ // 3. 카운트 쿼리
+ const total = await db
+ .select({ count: count() })
+ .from(orders)
+ .where(where);
+
+ return {
+ data,
+ totalRows: total[0]?.count ?? 0
+ };
+}
+```
+
+### 2) 클라이언트 연결
+
+Factory Service 방식과 동일하게 `useEffect`에서 `getOrderTableData`를 호출하면 됩니다.
diff --git a/components/client-table-v2/adapter/create-table-service.ts b/components/client-table-v2/adapter/create-table-service.ts
new file mode 100644
index 00000000..41c38906
--- /dev/null
+++ b/components/client-table-v2/adapter/create-table-service.ts
@@ -0,0 +1,101 @@
+import { PgTable } from "drizzle-orm/pg-core";
+import { DrizzleTableAdapter, DrizzleTableState } from "./drizzle-table-adapter";
+import { ColumnDef } from "@tanstack/react-table";
+import { SQL, and, count } from "drizzle-orm";
+
+// Define a minimal DB interface that we need
+// Adjust this to match your actual db instance type
+interface DbInstance {
+ select: (args?: any) => any;
+}
+
+export interface CreateTableServiceConfig<TData> {
+ /**
+ * Drizzle Database Instance
+ */
+ db: DbInstance;
+
+ /**
+ * Drizzle Table Schema (e.g. users, orders)
+ */
+ schema: PgTable; // Using PgTable as base, works for most Drizzle tables
+
+ /**
+ * React Table Columns Definition
+ * Used to map accessorKeys to DB columns
+ */
+ columns: ColumnDef<TData, any>[];
+
+ /**
+ * Optional: Custom WHERE clause to always apply (e.g. deleted_at IS NULL)
+ */
+ defaultWhere?: SQL;
+
+ /**
+ * Optional: Custom query modifier
+ * Allows joining other tables or selecting specific fields
+ */
+ customQuery?: (queryBuilder: any) => any;
+}
+
+/**
+ * Factory function to create a standardized server action for a table.
+ *
+ * @example
+ * export const getUsers = createTableService({
+ * db,
+ * schema: users,
+ * columns: userColumns
+ * });
+ */
+export function createTableService<TData>(config: CreateTableServiceConfig<TData>) {
+ const { db, schema, columns, defaultWhere, customQuery } = config;
+
+ // Return the actual Server Action function
+ return async function getTableData(tableState: DrizzleTableState) {
+ const adapter = new DrizzleTableAdapter(schema, columns);
+ const { where, orderBy, limit, offset, groupBy } = adapter.getQueryParts(tableState);
+
+ // Merge defaultWhere with dynamic where
+ const finalWhere = defaultWhere
+ ? (where ? and(defaultWhere, where) : defaultWhere)
+ : where;
+
+ // 1. Build Data Query
+ let dataQuery = db.select()
+ .from(schema)
+ .where(finalWhere)
+ .orderBy(...orderBy)
+ .limit(limit)
+ .offset(offset);
+
+ if (groupBy && groupBy.length > 0) {
+ dataQuery = dataQuery.groupBy(...groupBy);
+ }
+
+ // Apply custom query modifications (joins, etc)
+ if (customQuery) {
+ dataQuery = customQuery(dataQuery);
+ }
+
+ // 2. Build Count Query
+ const countQuery = db.select({ count: count() })
+ .from(schema)
+ .where(finalWhere);
+
+ // Execute queries
+ // We use Promise.all to run them in parallel
+ const [data, countResult] = await Promise.all([
+ dataQuery,
+ countQuery
+ ]);
+
+ const totalRows = Number(countResult[0]?.count ?? 0);
+
+ return {
+ data: data as TData[],
+ totalRows,
+ pageCount: Math.ceil(totalRows / (tableState.pagination?.pageSize ?? 10))
+ };
+ };
+}
diff --git a/components/client-table-v2/adapter/drizzle-table-adapter.ts b/components/client-table-v2/adapter/drizzle-table-adapter.ts
new file mode 100644
index 00000000..05bf4c5f
--- /dev/null
+++ b/components/client-table-v2/adapter/drizzle-table-adapter.ts
@@ -0,0 +1,173 @@
+import {
+ ColumnFiltersState,
+ SortingState,
+ PaginationState,
+ GroupingState,
+ ColumnDef
+} from "@tanstack/react-table";
+import {
+ SQL,
+ and,
+ or,
+ eq,
+ ilike,
+ gt,
+ lt,
+ gte,
+ lte,
+ inArray,
+ asc,
+ desc,
+ getTableColumns,
+} from "drizzle-orm";
+import { PgTable, PgView, PgColumn } from "drizzle-orm/pg-core";
+
+// Helper to detect if value is empty or undefined
+const isEmpty = (value: any) => value === undefined || value === null || value === "";
+
+export interface DrizzleTableState {
+ sorting?: SortingState;
+ columnFilters?: ColumnFiltersState;
+ globalFilter?: string;
+ pagination?: PaginationState;
+ grouping?: GroupingState;
+}
+
+export class DrizzleTableAdapter<TData> {
+ private columnMap: Map<string, PgColumn>;
+
+ constructor(
+ private table: PgTable | PgView,
+ private columns: ColumnDef<TData, any>[]
+ ) {
+ // Create a map of accessorKey -> Drizzle Column for fast lookup
+ this.columnMap = new Map();
+ // @ts-ignore - getTableColumns works on views in newer drizzle versions or we can cast
+ const drizzleColumns = getTableColumns(table as any);
+
+ columns.forEach(col => {
+ // We currently only support accessorKey which maps directly to a DB column
+ if ('accessorKey' in col && typeof col.accessorKey === 'string') {
+ const dbCol = drizzleColumns[col.accessorKey];
+ if (dbCol) {
+ this.columnMap.set(col.accessorKey, dbCol);
+ }
+ }
+ });
+ }
+
+ private getColumn(columnId: string): PgColumn | undefined {
+ return this.columnMap.get(columnId);
+ }
+
+ /**
+ * Build the WHERE clause based on column filters and global filter
+ */
+ getWhere(columnFilters?: ColumnFiltersState, globalFilter?: string): SQL | undefined {
+ const conditions: SQL[] = [];
+
+ // 1. Column Filters
+ if (columnFilters) {
+ for (const filter of columnFilters) {
+ const column = this.getColumn(filter.id);
+ if (!column) continue;
+
+ const value = filter.value;
+ if (isEmpty(value)) continue;
+
+ // Handle Array (range or multiple select)
+ if (Array.isArray(value)) {
+ // Range filter (e.g. [min, max])
+ if (value.length === 2 && (typeof value[0] === 'number' || typeof value[0] === 'string')) {
+ const [min, max] = value;
+ if (!isEmpty(min) && !isEmpty(max)) {
+ conditions.push(and(gte(column, min), lte(column, max))!);
+ } else if (!isEmpty(min)) {
+ conditions.push(gte(column, min)!);
+ } else if (!isEmpty(max)) {
+ conditions.push(lte(column, max)!);
+ }
+ }
+ // Multi-select (IN)
+ else if (value.length > 0) {
+ conditions.push(inArray(column, value)!);
+ }
+ }
+ // Boolean
+ else if (typeof value === 'boolean') {
+ conditions.push(eq(column, value)!);
+ }
+ // Number
+ else if (typeof value === 'number') {
+ conditions.push(eq(column, value)!);
+ }
+ // String (Search)
+ else if (typeof value === 'string') {
+ conditions.push(ilike(column, `%${value}%`)!);
+ }
+ }
+ }
+
+ // 2. Global Filter
+ if (globalFilter) {
+ const searchConditions: SQL[] = [];
+ this.columnMap.forEach((column) => {
+ // Implicitly supports only text-compatible columns for ilike
+ // Drizzle might throw if type mismatch, so user should be aware
+ searchConditions.push(ilike(column, `%${globalFilter}%`));
+ });
+
+ if (searchConditions.length > 0) {
+ conditions.push(or(...searchConditions)!);
+ }
+ }
+
+ return conditions.length > 0 ? and(...conditions) : undefined;
+ }
+
+ /**
+ * Build the ORDER BY clause
+ */
+ getOrderBy(sorting?: SortingState): SQL[] {
+ if (!sorting || !sorting.length) return [];
+
+ return sorting.map((sort) => {
+ const column = this.getColumn(sort.id);
+ if (!column) return null;
+ return sort.desc ? desc(column) : asc(column);
+ }).filter(Boolean) as SQL[];
+ }
+
+ /**
+ * Build the GROUP BY clause
+ */
+ getGroupBy(grouping?: GroupingState): SQL[] {
+ if (!grouping || !grouping.length) return [];
+
+ return grouping.map(g => this.getColumn(g)).filter(Boolean) as SQL[];
+ }
+
+ /**
+ * Get Limit and Offset
+ */
+ getPagination(pagination?: PaginationState) {
+ if (!pagination) return { limit: 10, offset: 0 };
+ return {
+ limit: pagination.pageSize,
+ offset: pagination.pageIndex * pagination.pageSize,
+ };
+ }
+
+ /**
+ * Helper to apply all state to a query builder.
+ * Returns the modifier objects that can be passed to drizzle query builder.
+ */
+ getQueryParts(state: DrizzleTableState) {
+ return {
+ where: this.getWhere(state.columnFilters, state.globalFilter),
+ orderBy: this.getOrderBy(state.sorting),
+ groupBy: this.getGroupBy(state.grouping),
+ ...this.getPagination(state.pagination)
+ };
+ }
+}
diff --git a/components/client-table-v2/client-table-preset.tsx b/components/client-table-v2/client-table-preset.tsx
index 64930e7a..21486c9b 100644
--- a/components/client-table-v2/client-table-preset.tsx
+++ b/components/client-table-v2/client-table-preset.tsx
@@ -108,6 +108,9 @@ export function ClientTablePreset<TData>({
if (s.columnOrder) table.setColumnOrder(s.columnOrder);
if (s.grouping) table.setGrouping(s.grouping);
if (s.pagination?.pageSize) table.setPageSize(s.pagination.pageSize);
+ // Reset page index to avoid loading an out-of-range page after applying a preset,
+ // which is especially important in server-mode pagination.
+ table.setPageIndex(0);
toast.success(`Preset "${preset.name}" loaded`);
};
diff --git a/components/client-table-v3/GUIDE.md b/components/client-table-v3/GUIDE.md
new file mode 100644
index 00000000..05d7455e
--- /dev/null
+++ b/components/client-table-v3/GUIDE.md
@@ -0,0 +1,99 @@
+# ClientVirtualTable V3 Guide
+
+This version introduces the `useClientTable` hook to drastically reduce boilerplate code and improve Developer Experience (DX).
+
+## Key Changes from V2
+- **`useClientTable` Hook**: Manages all state (sorting, filtering, pagination, grouping) and data fetching (Client or Server).
+- **Cleaner Component**: `ClientVirtualTable` now accepts a `table` instance prop, making it a pure renderer.
+- **Better Separation**: Logic is in the hook; UI is in the component.
+
+## Usage
+
+### 1. Client-Side Mode
+Load all data once, let the hook handle the rest.
+
+```tsx
+import { useClientTable, ClientVirtualTable } from "@/components/client-table-v3";
+
+function MyTable() {
+ const [data, setData] = useState([]);
+
+ // Load data...
+
+ const { table, isLoading } = useClientTable({
+ fetchMode: "client",
+ data,
+ columns,
+ enablePagination: true, // Auto-enabled
+ });
+
+ return <ClientVirtualTable table={table} isLoading={isLoading} />;
+}
+```
+
+### 2. Server-Side Mode (Factory Service)
+Pass your server action as the `fetcher`. The hook handles debouncing and refetching.
+
+```tsx
+import { useClientTable, ClientVirtualTable } from "@/components/client-table-v3";
+import { myServerAction } from "./actions";
+
+function MyServerTable() {
+ const { table, isLoading } = useClientTable({
+ fetchMode: "server",
+ fetcher: myServerAction, // Must accept TableState
+ columns,
+ enablePagination: true,
+ });
+
+ return <ClientVirtualTable table={table} isLoading={isLoading} />;
+}
+```
+
+### 3. Server Grouping (Pattern 2-B)
+The hook detects server-side grouping responses and provides them separately.
+
+```tsx
+const { table, isLoading, isServerGrouped, serverGroups } = useClientTable({
+ fetchMode: "server",
+ fetcher: myGroupFetcher,
+ columns,
+ enableGrouping: true,
+});
+
+if (isServerGrouped) {
+ return <MyGroupRenderer groups={serverGroups} />;
+}
+
+return <ClientVirtualTable table={table} ... />;
+```
+
+## Hook Options (`useClientTable`)
+
+| Option | Type | Description |
+|--------|------|-------------|
+| `fetchMode` | `'client' \| 'server'` | Default `'client'`. |
+| `data` | `TData[]` | Data for client mode. |
+| `fetcher` | `(state) => Promise` | Server action for server mode. |
+| `columns` | `ColumnDef[]` | Column definitions. |
+| `initialState` | `object` | Initial sorting, filters, etc. |
+| `enablePagination` | `boolean` | Enable pagination logic. |
+| `enableGrouping` | `boolean` | Enable grouping logic. |
+
+## Component Props (`ClientVirtualTable`)
+
+| Prop | Type | Description |
+|------|------|-------------|
+| `table` | `Table<TData>` | The table instance from the hook. |
+| `isLoading` | `boolean` | Shows loading overlay. |
+| `height` | `string` | Table height (required for virtualization). |
+| `enableUserPreset` | `boolean` | Enable saving/loading view presets. |
+| `tableKey` | `string` | Unique key for presets. |
+
+## Migration from V2
+
+1. Replace `<ClientVirtualTable ...props />` with `const { table } = useClientTable({...}); <ClientVirtualTable table={table} />`.
+2. Remove local state (`sorting`, `pagination`, `useEffect` for fetching) from your page component.
+3. Pass `fetcher` directly to the hook.
+
+
diff --git a/components/client-table-v3/client-table-column-header.tsx b/components/client-table-v3/client-table-column-header.tsx
new file mode 100644
index 00000000..3be20565
--- /dev/null
+++ b/components/client-table-v3/client-table-column-header.tsx
@@ -0,0 +1,237 @@
+"use client"
+
+import * as React from "react"
+import { Header, Column } from "@tanstack/react-table"
+import { useSortable } from "@dnd-kit/sortable"
+import { CSS } from "@dnd-kit/utilities"
+import { flexRender } from "@tanstack/react-table"
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuSeparator,
+ ContextMenuTrigger,
+} from "@/components/ui/context-menu"
+import {
+ ArrowDown,
+ ArrowUp,
+ ChevronsUpDown,
+ EyeOff,
+ PinOff,
+ MoveLeft,
+ MoveRight,
+ Group,
+ Ungroup,
+} from "lucide-react"
+import { cn } from "@/lib/utils"
+import { ClientTableFilter } from "./client-table-filter"
+
+interface ClientTableColumnHeaderProps<TData, TValue>
+ extends React.HTMLAttributes<HTMLTableHeaderCellElement> {
+ header: Header<TData, TValue>
+ enableReordering?: boolean
+ renderHeaderVisualFeedback?: (props: {
+ column: Column<TData, TValue>
+ isPinned: boolean | string
+ isSorted: boolean | string
+ isFiltered: boolean
+ isGrouped: boolean
+ }) => React.ReactNode
+}
+
+export function ClientTableColumnHeader<TData, TValue>({
+ header,
+ enableReordering = true,
+ renderHeaderVisualFeedback,
+ className,
+ ...props
+}: ClientTableColumnHeaderProps<TData, TValue>) {
+ const column = header.column
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ } = useSortable({
+ id: header.id,
+ disabled: !enableReordering || column.getIsResizing(),
+ })
+
+ // -- Styles --
+ const style: React.CSSProperties = {
+ // Apply transform only if reordering is enabled and active
+ transform: enableReordering ? CSS.Translate.toString(transform) : undefined,
+ transition: enableReordering ? transition : undefined,
+ width: header.getSize(),
+ zIndex: isDragging ? 100 : 0,
+ position: "relative",
+ ...props.style,
+ }
+
+ // Pinning Styles
+ const isPinned = column.getIsPinned()
+ const isSorted = column.getIsSorted()
+ const isFiltered = column.getFilterValue() !== undefined
+ const isGrouped = column.getIsGrouped()
+
+ if (isPinned === "left") {
+ style.left = `${column.getStart("left")}px`
+ style.position = "sticky"
+ style.zIndex = 30 // Pinned columns needs to be higher than normal headers
+ } else if (isPinned === "right") {
+ style.right = `${column.getAfter("right")}px`
+ style.position = "sticky"
+ style.zIndex = 30 // Pinned columns needs to be higher than normal headers
+ }
+
+ // -- Handlers --
+ const handleHide = () => column.toggleVisibility(false)
+ const handlePinLeft = () => column.pin("left")
+ const handlePinRight = () => column.pin("right")
+ const handleUnpin = () => column.pin(false)
+ const handleToggleGrouping = () => column.toggleGrouping()
+
+ // -- Content --
+ const content = (
+ <>
+ <div
+ className={cn(
+ "flex items-center gap-2",
+ column.getCanSort() ? "cursor-pointer select-none" : ""
+ )}
+ onClick={column.getToggleSortingHandler()}
+ >
+ {flexRender(column.columnDef.header, header.getContext())}
+ {column.getCanSort() && (
+ <span className="flex items-center">
+ {column.getIsSorted() === "desc" ? (
+ <ArrowDown className="h-4 w-4" />
+ ) : column.getIsSorted() === "asc" ? (
+ <ArrowUp className="h-4 w-4" />
+ ) : (
+ <ChevronsUpDown className="h-4 w-4 opacity-50" />
+ )}
+ </span>
+ )}
+ {isGrouped && <Group className="h-4 w-4 text-blue-500" />}
+ </div>
+
+ {/* Resize Handle */}
+ <div
+ onMouseDown={header.getResizeHandler()}
+ onTouchStart={header.getResizeHandler()}
+ onPointerDown={(e) => e.stopPropagation()}
+ onClick={(e) => e.stopPropagation()} // Prevent sort trigger
+ className={cn(
+ "absolute right-0 top-0 h-full w-2 cursor-col-resize select-none touch-none z-10",
+ "after:absolute after:right-0 after:top-0 after:h-full after:w-[1px] after:bg-border", // 시각적 구분선
+ "hover:bg-primary/20 hover:w-4 hover:-right-2", // 호버 시 클릭 영역 확장
+ header.column.getIsResizing() ? "bg-primary/50 w-1" : "bg-transparent"
+ )}
+ />
+
+ {/* Filter */}
+ {column.getCanFilter() && <ClientTableFilter column={column} />}
+
+ {/* Visual Feedback Indicators */}
+ {renderHeaderVisualFeedback ? (
+ renderHeaderVisualFeedback({
+ column,
+ isPinned,
+ isSorted,
+ isFiltered,
+ isGrouped,
+ })
+ ) : (
+ (isPinned || isFiltered || isGrouped) && (
+ <div className="absolute top-0.5 right-1 flex gap-1 z-10 pointer-events-none">
+ {isPinned && <div className="h-1.5 w-1.5 rounded-full bg-blue-500" />}
+ {isFiltered && <div className="h-1.5 w-1.5 rounded-full bg-yellow-500" />}
+ {isGrouped && <div className="h-1.5 w-1.5 rounded-full bg-green-500" />}
+ </div>
+ )
+ )}
+ </>
+ )
+
+ if (header.isPlaceholder) {
+ return (
+ <th
+ colSpan={header.colSpan}
+ style={style}
+ className={cn("border-b px-4 py-2 text-left text-sm font-medium bg-muted", className)}
+ {...props}
+ >
+ {null}
+ </th>
+ )
+ }
+
+ return (
+ <ContextMenu>
+ <ContextMenuTrigger asChild>
+ <th
+ ref={setNodeRef}
+ colSpan={header.colSpan}
+ style={style}
+ className={cn(
+ "border-b px-4 py-2 text-left text-sm font-medium bg-muted group transition-colors",
+ isDragging ? "opacity-50 bg-accent" : "",
+ isPinned ? "shadow-[0_0_10px_rgba(0,0,0,0.1)]" : "",
+ className
+ )}
+ {...attributes}
+ {...listeners}
+ {...props}
+ >
+ {content}
+ </th>
+ </ContextMenuTrigger>
+ <ContextMenuContent className="w-48">
+ <ContextMenuItem onClick={handleHide}>
+ <EyeOff className="mr-2 h-4 w-4" />
+ Hide Column
+ </ContextMenuItem>
+
+ {column.getCanGroup() && (
+ <>
+ <ContextMenuSeparator />
+ <ContextMenuItem onClick={handleToggleGrouping}>
+ {isGrouped ? (
+ <>
+ <Ungroup className="mr-2 h-4 w-4" />
+ Ungroup
+ </>
+ ) : (
+ <>
+ <Group className="mr-2 h-4 w-4" />
+ Group by {column.id}
+ </>
+ )}
+ </ContextMenuItem>
+ </>
+ )}
+
+ <ContextMenuSeparator />
+ <ContextMenuItem onClick={handlePinLeft}>
+ <MoveLeft className="mr-2 h-4 w-4" />
+ Pin Left
+ </ContextMenuItem>
+ <ContextMenuItem onClick={handlePinRight}>
+ <MoveRight className="mr-2 h-4 w-4" />
+ Pin Right
+ </ContextMenuItem>
+ {isPinned && (
+ <ContextMenuItem onClick={handleUnpin}>
+ <PinOff className="mr-2 h-4 w-4" />
+ Unpin
+ </ContextMenuItem>
+ )}
+ </ContextMenuContent>
+ </ContextMenu>
+ )
+}
+
+
diff --git a/components/client-table-v3/client-table-filter.tsx b/components/client-table-v3/client-table-filter.tsx
new file mode 100644
index 00000000..eaf6b31e
--- /dev/null
+++ b/components/client-table-v3/client-table-filter.tsx
@@ -0,0 +1,103 @@
+"use client"
+
+import * as React from "react"
+import { Column } from "@tanstack/react-table"
+import { Input } from "@/components/ui/input"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { ClientTableColumnMeta } from "./types"
+
+interface ClientTableFilterProps<TData, TValue> {
+ column: Column<TData, TValue>
+}
+
+export function ClientTableFilter<TData, TValue>({
+ column,
+}: ClientTableFilterProps<TData, TValue>) {
+ const columnFilterValue = column.getFilterValue()
+ // Cast meta to our local type
+ const meta = column.columnDef.meta as ClientTableColumnMeta | undefined
+
+ // Handle Boolean Filter
+ if (meta?.filterType === "boolean") {
+ return (
+ <div onClick={(e) => e.stopPropagation()} className="mt-2">
+ <Select
+ value={(columnFilterValue as string) ?? "all"}
+ onValueChange={(value) =>
+ column.setFilterValue(value === "all" ? undefined : value === "true")
+ }
+ >
+ <SelectTrigger className="h-8 w-full">
+ <SelectValue placeholder="All" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">All</SelectItem>
+ <SelectItem value="true">Yes</SelectItem>
+ <SelectItem value="false">No</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+ )
+ }
+
+ // Handle Select Filter (for specific options)
+ if (meta?.filterType === "select" && meta.filterOptions) {
+ return (
+ <div onClick={(e) => e.stopPropagation()} className="mt-2">
+ <Select
+ value={(columnFilterValue as string) ?? "all"}
+ onValueChange={(value) =>
+ column.setFilterValue(value === "all" ? undefined : value)
+ }
+ >
+ <SelectTrigger className="h-8 w-full">
+ <SelectValue placeholder="All" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="all">All</SelectItem>
+ {meta.filterOptions.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ )
+ }
+
+ // Default Text Filter
+ const [value, setValue] = React.useState(columnFilterValue)
+
+ React.useEffect(() => {
+ setValue(columnFilterValue)
+ }, [columnFilterValue])
+
+ React.useEffect(() => {
+ const timeout = setTimeout(() => {
+ column.setFilterValue(value)
+ }, 500)
+
+ return () => clearTimeout(timeout)
+ }, [value, column])
+
+ return (
+ <div onClick={(e) => e.stopPropagation()} className="mt-2">
+ <Input
+ type="text"
+ value={(value ?? "") as string}
+ onChange={(e) => setValue(e.target.value)}
+ placeholder="Search..."
+ className="h-8 w-full font-normal bg-background"
+ />
+ </div>
+ )
+}
+
+
diff --git a/components/client-table-v3/client-table-preset.tsx b/components/client-table-v3/client-table-preset.tsx
new file mode 100644
index 00000000..557e8493
--- /dev/null
+++ b/components/client-table-v3/client-table-preset.tsx
@@ -0,0 +1,189 @@
+"use client";
+
+import * as React from "react";
+import { Table } from "@tanstack/react-table";
+import { useSession } from "next-auth/react";
+import { Button } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Bookmark, Save, Trash2 } from "lucide-react";
+import {
+ getPresets,
+ savePreset,
+ deletePreset,
+} from "./preset-actions";
+import { Preset } from "./preset-types";
+import { toast } from "sonner";
+
+interface ClientTablePresetProps<TData> {
+ table: Table<TData>;
+ tableKey: string;
+}
+
+export function ClientTablePreset<TData>({
+ table,
+ tableKey,
+}: ClientTablePresetProps<TData>) {
+ const { data: session } = useSession();
+ const [savedPresets, setSavedPresets] = React.useState<Preset[]>([]);
+ const [isPresetDialogOpen, setIsPresetDialogOpen] = React.useState(false);
+ const [newPresetName, setNewPresetName] = React.useState("");
+ const [isLoading, setIsLoading] = React.useState(false);
+
+ const fetchSettings = React.useCallback(async () => {
+ const userIdVal = session?.user?.id;
+ if (!userIdVal) return;
+
+ const userId = Number(userIdVal);
+ if (isNaN(userId)) return;
+
+ const res = await getPresets(tableKey, userId);
+ if (res.success && res.data) {
+ setSavedPresets(res.data);
+ }
+ }, [session, tableKey]);
+
+ React.useEffect(() => {
+ if (session) {
+ fetchSettings();
+ }
+ }, [fetchSettings, session]);
+
+ const handleSavePreset = async () => {
+ const userIdVal = session?.user?.id;
+ if (!newPresetName.trim() || !userIdVal) return;
+ const userId = Number(userIdVal);
+ if (isNaN(userId)) return;
+
+ setIsLoading(true);
+ const state = table.getState();
+ const settingToSave = {
+ sorting: state.sorting,
+ columnFilters: state.columnFilters,
+ globalFilter: state.globalFilter,
+ columnVisibility: state.columnVisibility,
+ columnPinning: state.columnPinning,
+ columnOrder: state.columnOrder,
+ grouping: state.grouping,
+ pagination: { pageSize: state.pagination.pageSize },
+ };
+
+ const res = await savePreset(userId, tableKey, newPresetName, settingToSave);
+ setIsLoading(false);
+
+ if (res.success) {
+ toast.success("Preset saved successfully");
+ setIsPresetDialogOpen(false);
+ setNewPresetName("");
+ fetchSettings();
+ } else {
+ toast.error("Failed to save preset");
+ }
+ };
+
+ const handleLoadPreset = (preset: Preset) => {
+ const s = preset.setting as Record<string, any>;
+ if (!s) return;
+
+ if (s.sorting) table.setSorting(s.sorting);
+ if (s.columnFilters) table.setColumnFilters(s.columnFilters);
+ if (s.globalFilter !== undefined) table.setGlobalFilter(s.globalFilter);
+ if (s.columnVisibility) table.setColumnVisibility(s.columnVisibility);
+ if (s.columnPinning) table.setColumnPinning(s.columnPinning);
+ if (s.columnOrder) table.setColumnOrder(s.columnOrder);
+ if (s.grouping) table.setGrouping(s.grouping);
+ if (s.pagination?.pageSize) table.setPageSize(s.pagination.pageSize);
+ // Reset page index to avoid loading an out-of-range page after applying a preset
+ table.setPageIndex(0);
+
+ toast.success(`Preset "${preset.name}" loaded`);
+ };
+
+ const handleDeletePreset = async (e: React.MouseEvent, id: string) => {
+ e.stopPropagation();
+ if (!confirm("Are you sure you want to delete this preset?")) return;
+
+ const res = await deletePreset(id);
+ if (res.success) {
+ toast.success("Preset deleted");
+ fetchSettings();
+ } else {
+ toast.error("Failed to delete preset");
+ }
+ };
+
+ if (!session) return null;
+
+ return (
+ <>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button variant="outline" size="sm" className="ml-2 hidden h-8 lg:flex">
+ <Bookmark className="mr-2 h-4 w-4" />
+ Presets
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-[200px]">
+ <DropdownMenuLabel>Saved Presets</DropdownMenuLabel>
+ <DropdownMenuSeparator />
+ {savedPresets.length === 0 ? (
+ <div className="p-2 text-sm text-muted-foreground text-center">No saved presets</div>
+ ) : (
+ savedPresets.map((preset) => (
+ <DropdownMenuItem key={preset.id} onClick={() => handleLoadPreset(preset)} className="flex justify-between cursor-pointer">
+ <span className="truncate flex-1">{preset.name}</span>
+ <Button variant="ghost" size="icon" className="h-4 w-4" onClick={(e) => handleDeletePreset(e, preset.id)}>
+ <Trash2 className="h-3 w-3 text-destructive" />
+ </Button>
+ </DropdownMenuItem>
+ ))
+ )}
+ <DropdownMenuSeparator />
+ <DropdownMenuItem onClick={() => setIsPresetDialogOpen(true)} className="cursor-pointer">
+ <Save className="mr-2 h-4 w-4" />
+ Save Current Preset
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+
+ <Dialog open={isPresetDialogOpen} onOpenChange={setIsPresetDialogOpen}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Save Preset</DialogTitle>
+ <DialogDescription>
+ Save the current table configuration as a preset.
+ </DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-4 py-4">
+ <Input
+ placeholder="Preset Name"
+ value={newPresetName}
+ onChange={(e) => setNewPresetName(e.target.value)}
+ />
+ </div>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => setIsPresetDialogOpen(false)}>Cancel</Button>
+ <Button onClick={handleSavePreset} disabled={isLoading}>Save</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ </>
+ );
+}
+
+
diff --git a/components/client-table-v3/client-virtual-table.tsx b/components/client-table-v3/client-virtual-table.tsx
new file mode 100644
index 00000000..7a092326
--- /dev/null
+++ b/components/client-table-v3/client-virtual-table.tsx
@@ -0,0 +1,309 @@
+"use client";
+
+import * as React from "react";
+import { Table, flexRender } from "@tanstack/react-table";
+import { useVirtualizer } from "@tanstack/react-virtual";
+import {
+ DndContext,
+ closestCenter,
+ KeyboardSensor,
+ PointerSensor,
+ useSensor,
+ useSensors,
+ DragEndEvent,
+} from "@dnd-kit/core";
+import {
+ arrayMove,
+ SortableContext,
+ horizontalListSortingStrategy,
+} from "@dnd-kit/sortable";
+import { cn } from "@/lib/utils";
+import { Loader2, ChevronRight, ChevronDown } from "lucide-react";
+
+import { ClientTableToolbar } from "../client-table/client-table-toolbar";
+import { exportToExcel } from "../client-table/export-utils";
+import { ClientDataTablePagination } from "@/components/client-data-table/data-table-pagination";
+import { ClientTableViewOptions } from "../client-table/client-table-view-options";
+
+import { ClientTableColumnHeader } from "./client-table-column-header";
+import { ClientTablePreset } from "./client-table-preset";
+import { ClientVirtualTableProps } from "./types";
+
+export function ClientVirtualTable<TData>({
+ table,
+ isLoading = false,
+ height = "100%",
+ estimateRowHeight = 40,
+ className,
+ actions,
+ customToolbar,
+ enableExport = true,
+ onExport,
+ enableUserPreset = false,
+ tableKey,
+ getRowClassName,
+ onRowClick,
+ renderHeaderVisualFeedback,
+}: ClientVirtualTableProps<TData>) {
+ // --- DnD Sensors ---
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 8,
+ },
+ }),
+ useSensor(KeyboardSensor)
+ );
+
+ // --- Drag Handler ---
+ const handleDragEnd = (event: DragEndEvent) => {
+ const { active, over } = event;
+ if (active && over && active.id !== over.id) {
+ const activeId = active.id as string;
+ const overId = over.id as string;
+
+ const activeColumn = table.getColumn(activeId);
+ const overColumn = table.getColumn(overId);
+
+ if (activeColumn && overColumn) {
+ const activePinState = activeColumn.getIsPinned();
+ const overPinState = overColumn.getIsPinned();
+
+ // If dragging between different pin states, update the pin state
+ if (activePinState !== overPinState) {
+ activeColumn.pin(overPinState);
+ }
+
+ // Reorder
+ const currentOrder = table.getState().columnOrder;
+ const oldIndex = currentOrder.indexOf(activeId);
+ const newIndex = currentOrder.indexOf(overId);
+
+ if (oldIndex !== -1 && newIndex !== -1) {
+ table.setColumnOrder(arrayMove(currentOrder, oldIndex, newIndex));
+ }
+ }
+ }
+ };
+
+ // --- Virtualization ---
+ const tableContainerRef = React.useRef<HTMLDivElement>(null);
+ const { rows } = table.getRowModel();
+
+ const rowVirtualizer = useVirtualizer({
+ count: rows.length,
+ getScrollElement: () => tableContainerRef.current,
+ estimateSize: () => estimateRowHeight,
+ overscan: 10,
+ });
+
+ const virtualRows = rowVirtualizer.getVirtualItems();
+ const totalSize = rowVirtualizer.getTotalSize();
+
+ const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0;
+ const paddingBottom =
+ virtualRows.length > 0
+ ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0)
+ : 0;
+
+ // --- Export ---
+ const handleExport = async () => {
+ if (onExport) {
+ onExport(table.getFilteredRowModel().rows.map((r) => r.original));
+ return;
+ }
+ const currentData = table.getFilteredRowModel().rows.map((row) => row.original);
+ // Note: exportToExcel needs columns definition. table.getAllColumns() or visible columns?
+ // Using table.getAllLeafColumns() usually.
+ await exportToExcel(currentData, table.getAllLeafColumns(), `export-${new Date().toISOString().slice(0, 10)}.xlsx`);
+ };
+
+ const columns = table.getVisibleLeafColumns();
+ const data = table.getFilteredRowModel().rows; // or just rows which is from getRowModel
+
+ return (
+ <div
+ className={cn("flex flex-col gap-4", className)}
+ style={{ height }}
+ >
+ <ClientTableToolbar
+ globalFilter={table.getState().globalFilter ?? ""}
+ setGlobalFilter={table.setGlobalFilter}
+ totalRows={table.getRowCount()}
+ visibleRows={table.getRowModel().rows.length}
+ onExport={enableExport ? handleExport : undefined}
+ viewOptions={
+ <>
+ <ClientTableViewOptions table={table} />
+ {enableUserPreset && tableKey && (
+ <ClientTablePreset table={table} tableKey={tableKey} />
+ )}
+ </>
+ }
+ customToolbar={customToolbar}
+ actions={actions}
+ />
+
+ <div
+ ref={tableContainerRef}
+ className="relative border rounded-md overflow-auto bg-background flex-1 min-h-0"
+ >
+ {isLoading && (
+ <div className="absolute inset-0 z-50 flex items-center justify-center bg-background/50 backdrop-blur-sm">
+ <Loader2 className="h-8 w-8 animate-spin text-primary" />
+ </div>
+ )}
+
+ <DndContext
+ sensors={sensors}
+ collisionDetection={closestCenter}
+ onDragEnd={handleDragEnd}
+ >
+ <table
+ className="table-fixed border-collapse w-full min-w-full"
+ style={{ width: table.getTotalSize() }}
+ >
+ <thead className="sticky top-0 z-40 bg-muted">
+ {table.getHeaderGroups().map((headerGroup) => (
+ <tr key={headerGroup.id}>
+ <SortableContext
+ items={headerGroup.headers.map((h) => h.id)}
+ strategy={horizontalListSortingStrategy}
+ >
+ {headerGroup.headers.map((header) => (
+ <ClientTableColumnHeader
+ key={header.id}
+ header={header}
+ enableReordering={true}
+ renderHeaderVisualFeedback={renderHeaderVisualFeedback}
+ />
+ ))}
+ </SortableContext>
+ </tr>
+ ))}
+ </thead>
+ <tbody>
+ {paddingTop > 0 && (
+ <tr>
+ <td style={{ height: `${paddingTop}px` }} />
+ </tr>
+ )}
+ {virtualRows.length === 0 && !isLoading ? (
+ <tr>
+ <td colSpan={columns.length} className="h-24 text-center">
+ No results.
+ </td>
+ </tr>
+ ) : (
+ virtualRows.map((virtualRow) => {
+ const row = rows[virtualRow.index];
+
+ // --- Group Header Rendering ---
+ if (row.getIsGrouped()) {
+ const groupingColumnId = row.groupingColumnId ?? "";
+ const groupingValue = row.getGroupingValue(groupingColumnId);
+
+ return (
+ <tr
+ key={row.id}
+ className="hover:bg-muted/50 border-b bg-muted/30"
+ style={{ height: `${virtualRow.size}px` }}
+ >
+ <td
+ colSpan={columns.length}
+ className="px-4 py-2 text-left font-medium cursor-pointer"
+ onClick={row.getToggleExpandedHandler()}
+ >
+ <div className="flex items-center gap-2">
+ {row.getIsExpanded() ? (
+ <ChevronDown className="h-4 w-4" />
+ ) : (
+ <ChevronRight className="h-4 w-4" />
+ )}
+ <span className="flex items-center gap-2">
+ <span className="font-bold capitalize">
+ {groupingColumnId}:
+ </span>
+ <span>
+ {String(groupingValue)}
+ </span>
+ <span className="text-muted-foreground text-sm font-normal">
+ ({row.subRows.length})
+ </span>
+ </span>
+ </div>
+ </td>
+ </tr>
+ );
+ }
+
+ // --- Normal Row Rendering ---
+ return (
+ <tr
+ key={row.id}
+ className={cn(
+ "hover:bg-muted/50 border-b last:border-0",
+ getRowClassName ? getRowClassName(row.original, row.index) : "",
+ onRowClick ? "cursor-pointer" : ""
+ )}
+ style={{ height: `${virtualRow.size}px` }}
+ onClick={(e) => onRowClick?.(row, e)}
+ >
+ {row.getVisibleCells().map((cell) => {
+ const isPinned = cell.column.getIsPinned();
+ const isGrouped = cell.column.getIsGrouped();
+
+ const style: React.CSSProperties = {
+ width: cell.column.getSize(),
+ };
+ if (isPinned === "left") {
+ style.position = "sticky";
+ style.left = `${cell.column.getStart("left")}px`;
+ style.zIndex = 20;
+ } else if (isPinned === "right") {
+ style.position = "sticky";
+ style.right = `${cell.column.getAfter("right")}px`;
+ style.zIndex = 20;
+ }
+
+ return (
+ <td
+ key={cell.id}
+ className={cn(
+ "px-2 py-0 text-sm truncate border-b bg-background",
+ isGrouped ? "bg-muted/20" : ""
+ )}
+ style={style}
+ >
+ {cell.getIsGrouped() ? null : cell.getIsAggregated() ? (
+ flexRender(
+ cell.column.columnDef.aggregatedCell ??
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )
+ ) : cell.getIsPlaceholder() ? null : (
+ flexRender(cell.column.columnDef.cell, cell.getContext())
+ )}
+ </td>
+ );
+ })}
+ </tr>
+ );
+ })
+ )}
+ {paddingBottom > 0 && (
+ <tr>
+ <td style={{ height: `${paddingBottom}px` }} />
+ </tr>
+ )}
+ </tbody>
+ </table>
+ </DndContext>
+ </div>
+
+ <ClientDataTablePagination table={table} />
+ </div>
+ );
+}
+
+
diff --git a/components/client-table-v3/index.ts b/components/client-table-v3/index.ts
new file mode 100644
index 00000000..678a4757
--- /dev/null
+++ b/components/client-table-v3/index.ts
@@ -0,0 +1,9 @@
+export * from "./client-virtual-table";
+export * from "./use-client-table";
+export * from "./types";
+export * from "./client-table-column-header";
+export * from "./client-table-filter";
+export * from "./client-table-preset";
+export * from "./preset-types";
+
+
diff --git a/components/client-table-v3/preset-actions.ts b/components/client-table-v3/preset-actions.ts
new file mode 100644
index 00000000..3ef4d239
--- /dev/null
+++ b/components/client-table-v3/preset-actions.ts
@@ -0,0 +1,84 @@
+"use server";
+
+import db from "@/db/db";
+import { userCustomData } from "@/db/schema/user-custom-data/userCustomData";
+import { eq, and } from "drizzle-orm";
+import { Preset } from "./preset-types";
+
+export async function getPresets(tableKey: string, userId: number): Promise<{ success: boolean; data?: Preset[]; error?: string }> {
+ try {
+ const settings = await db
+ .select()
+ .from(userCustomData)
+ .where(
+ and(
+ eq(userCustomData.tableKey, tableKey),
+ eq(userCustomData.userId, userId)
+ )
+ )
+ .orderBy(userCustomData.createdDate);
+
+ const data: Preset[] = settings.map(s => ({
+ id: s.id,
+ name: s.customSettingName,
+ setting: s.customSetting,
+ createdAt: s.createdDate,
+ updatedAt: s.updatedDate,
+ }));
+
+ return { success: true, data };
+ } catch (error) {
+ console.error("Failed to fetch presets:", error);
+ return { success: false, error: "Failed to fetch presets" };
+ }
+}
+
+export async function savePreset(
+ userId: number,
+ tableKey: string,
+ name: string,
+ setting: any
+): Promise<{ success: boolean; error?: string }> {
+ try {
+ const existing = await db.query.userCustomData.findFirst({
+ where: and(
+ eq(userCustomData.userId, userId),
+ eq(userCustomData.tableKey, tableKey),
+ eq(userCustomData.customSettingName, name)
+ )
+ });
+
+ if (existing) {
+ await db.update(userCustomData)
+ .set({
+ customSetting: setting,
+ updatedDate: new Date()
+ })
+ .where(eq(userCustomData.id, existing.id));
+ } else {
+ await db.insert(userCustomData).values({
+ userId,
+ tableKey,
+ customSettingName: name,
+ customSetting: setting,
+ });
+ }
+
+ return { success: true };
+ } catch (error) {
+ console.error("Failed to save preset:", error);
+ return { success: false, error: "Failed to save preset" };
+ }
+}
+
+export async function deletePreset(id: string): Promise<{ success: boolean; error?: string }> {
+ try {
+ await db.delete(userCustomData).where(eq(userCustomData.id, id));
+ return { success: true };
+ } catch (error) {
+ console.error("Failed to delete preset:", error);
+ return { success: false, error: "Failed to delete preset" };
+ }
+}
+
+
diff --git a/components/client-table-v3/preset-types.ts b/components/client-table-v3/preset-types.ts
new file mode 100644
index 00000000..37177cff
--- /dev/null
+++ b/components/client-table-v3/preset-types.ts
@@ -0,0 +1,15 @@
+export interface Preset {
+ id: string;
+ name: string;
+ setting: any; // JSON object for table state
+ createdAt: Date;
+ updatedAt: Date;
+}
+
+export interface PresetRepository {
+ getPresets(tableKey: string, userId: number): Promise<{ success: boolean; data?: Preset[]; error?: string }>;
+ savePreset(userId: number, tableKey: string, name: string, setting: any): Promise<{ success: boolean; error?: string }>;
+ deletePreset(id: string): Promise<{ success: boolean; error?: string }>;
+}
+
+
diff --git a/components/client-table-v3/types.ts b/components/client-table-v3/types.ts
new file mode 100644
index 00000000..4f2d8c82
--- /dev/null
+++ b/components/client-table-v3/types.ts
@@ -0,0 +1,84 @@
+import {
+ ColumnDef,
+ RowData,
+ Table,
+ PaginationState,
+ SortingState,
+ ColumnFiltersState,
+ VisibilityState,
+ ColumnPinningState,
+ ColumnOrderState,
+ GroupingState,
+ ExpandedState,
+ RowSelectionState,
+ OnChangeFn,
+ Row,
+ Column,
+} from "@tanstack/react-table";
+
+// --- Column Meta ---
+export interface ClientTableColumnMeta {
+ filterType?: "text" | "select" | "boolean" | "date-range"; // Added date-range
+ filterOptions?: { label: string; value: string }[];
+ serverGroupable?: boolean; // For Pattern 2-B
+}
+
+export type ClientTableColumnDef<TData extends RowData, TValue = unknown> = ColumnDef<TData, TValue> & {
+ meta?: ClientTableColumnMeta;
+};
+
+// --- Fetcher Types ---
+export interface TableState {
+ pagination: PaginationState;
+ sorting: SortingState;
+ columnFilters: ColumnFiltersState;
+ globalFilter: string;
+ grouping: GroupingState;
+ expanded: ExpandedState;
+}
+
+export interface FetcherResult<TData> {
+ data: TData[];
+ totalRows: number;
+ pageCount?: number;
+ groups?: any[]; // For grouping response
+}
+
+export type TableFetcher<TData> = (
+ state: TableState,
+ additionalArgs?: any
+) => Promise<FetcherResult<TData>>;
+
+// --- Component Props ---
+export interface ClientVirtualTableProps<TData> {
+ table: Table<TData>;
+ isLoading?: boolean;
+ height?: string | number;
+ estimateRowHeight?: number;
+ className?: string;
+
+ // UI Features
+ actions?: React.ReactNode;
+ customToolbar?: React.ReactNode;
+ enableExport?: boolean;
+ onExport?: (data: TData[]) => void;
+
+ // Preset
+ enableUserPreset?: boolean;
+ tableKey?: string;
+
+ // Styling
+ getRowClassName?: (originalRow: TData, index: number) => string;
+ onRowClick?: (row: Row<TData>, event: React.MouseEvent) => void;
+
+ // Visuals
+ renderHeaderVisualFeedback?: (props: {
+ column: Column<TData, unknown>;
+ isPinned: boolean | string;
+ isSorted: boolean | string;
+ isFiltered: boolean;
+ isGrouped: boolean;
+ }) => React.ReactNode;
+}
+
+
diff --git a/components/client-table-v3/use-client-table.ts b/components/client-table-v3/use-client-table.ts
new file mode 100644
index 00000000..87ce8a78
--- /dev/null
+++ b/components/client-table-v3/use-client-table.ts
@@ -0,0 +1,283 @@
+import * as React from "react";
+import {
+ useReactTable,
+ getCoreRowModel,
+ getSortedRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getGroupedRowModel,
+ getExpandedRowModel,
+ getFacetedRowModel,
+ getFacetedUniqueValues,
+ getFacetedMinMaxValues,
+ Table,
+ ColumnDef,
+ SortingState,
+ ColumnFiltersState,
+ PaginationState,
+ VisibilityState,
+ ColumnPinningState,
+ ColumnOrderState,
+ GroupingState,
+ ExpandedState,
+ RowSelectionState,
+ OnChangeFn,
+ FilterFn,
+} from "@tanstack/react-table";
+import { rankItem } from "@tanstack/match-sorter-utils";
+import { TableFetcher, FetcherResult } from "./types";
+
+// --- Utils ---
+const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
+ const itemRank = rankItem(row.getValue(columnId), value);
+ addMeta({ itemRank });
+ return itemRank.passed;
+};
+
+// Simple debounce hook
+function useDebounce<T>(value: T, delay: number): T {
+ const [debouncedValue, setDebouncedValue] = React.useState(value);
+ React.useEffect(() => {
+ const handler = setTimeout(() => {
+ setDebouncedValue(value);
+ }, delay);
+ return () => clearTimeout(handler);
+ }, [value, delay]);
+ return debouncedValue;
+}
+
+// --- Props ---
+export interface UseClientTableProps<TData, TValue> {
+ // Data Source
+ data?: TData[]; // For client mode
+ fetcher?: TableFetcher<TData>; // For server mode
+ fetchMode?: "client" | "server";
+
+ // Columns
+ columns: ColumnDef<TData, TValue>[];
+
+ // Options
+ enableGrouping?: boolean;
+ enablePagination?: boolean;
+ enableRowSelection?: boolean | ((row: any) => boolean);
+ enableMultiRowSelection?: boolean | ((row: any) => boolean);
+
+ // Initial State (Optional overrides)
+ initialState?: {
+ pagination?: PaginationState;
+ sorting?: SortingState;
+ columnFilters?: ColumnFiltersState;
+ globalFilter?: string;
+ columnVisibility?: VisibilityState;
+ columnPinning?: ColumnPinningState;
+ columnOrder?: ColumnOrderState;
+ grouping?: GroupingState;
+ expanded?: ExpandedState;
+ rowSelection?: RowSelectionState;
+ };
+
+ // Callbacks
+ onDataChange?: (data: TData[]) => void;
+ onError?: (error: any) => void;
+
+ // Custom Row ID
+ getRowId?: (originalRow: TData, index: number, parent?: any) => string;
+}
+
+// --- Hook ---
+export function useClientTable<TData, TValue = unknown>({
+ data: initialData = [],
+ fetcher,
+ fetchMode = "client",
+ columns,
+ enableGrouping = false,
+ enablePagination = true,
+ enableRowSelection,
+ enableMultiRowSelection,
+ initialState,
+ onDataChange,
+ onError,
+ getRowId,
+}: UseClientTableProps<TData, TValue>) {
+ // 1. State Definitions
+ const [sorting, setSorting] = React.useState<SortingState>(initialState?.sorting ?? []);
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(initialState?.columnFilters ?? []);
+ const [globalFilter, setGlobalFilter] = React.useState<string>(initialState?.globalFilter ?? "");
+ const [pagination, setPagination] = React.useState<PaginationState>(
+ initialState?.pagination ?? { pageIndex: 0, pageSize: 10 }
+ );
+ const [grouping, setGrouping] = React.useState<GroupingState>(initialState?.grouping ?? []);
+ const [expanded, setExpanded] = React.useState<ExpandedState>(initialState?.expanded ?? {});
+ const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(initialState?.rowSelection ?? {});
+ const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(initialState?.columnVisibility ?? {});
+ const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>(
+ initialState?.columnPinning ?? { left: [], right: [] }
+ );
+ const [columnOrder, setColumnOrder] = React.useState<ColumnOrderState>(
+ initialState?.columnOrder ?? columns.map((c) => c.id || (c as any).accessorKey) as string[]
+ );
+
+ // 2. Data State
+ const [data, setData] = React.useState<TData[]>(initialData);
+ const [totalRows, setTotalRows] = React.useState<number>(initialData.length);
+ const [pageCount, setPageCount] = React.useState<number>(-1);
+ const [isLoading, setIsLoading] = React.useState<boolean>(false);
+
+ // Grouping specific data
+ // In Pattern 2-B, the server returns "groups" instead of flat data.
+ // We might need to store that separately or handle it within data if using TanStack's mix.
+ // For now, let's assume the fetcher returns a flat array or we handle groups manually in the component.
+ // But wait, client-virtual-table V2 handles groups by checking row.getIsGrouped().
+ // If fetchMode is server, and grouping is active, the server might return "groups".
+ // The V2 implementation handled this by setting `groups` state in the consumer and switching rendering.
+ // We want to encapsulate this.
+ const [serverGroups, setServerGroups] = React.useState<any[]>([]);
+ const [isServerGrouped, setIsServerGrouped] = React.useState(false);
+
+ const isServer = fetchMode === "server";
+
+ // Debounced states for fetching to avoid rapid-fire requests
+ const debouncedGlobalFilter = useDebounce(globalFilter, 300);
+ const debouncedColumnFilters = useDebounce(columnFilters, 300);
+ // Pagination and Sorting don't need debounce usually, but grouping might.
+
+ // 3. Data Fetching (Server Mode)
+ const refresh = React.useCallback(async () => {
+ if (!isServer || !fetcher) return;
+
+ setIsLoading(true);
+ try {
+ const result = await fetcher({
+ pagination,
+ sorting,
+ columnFilters: debouncedColumnFilters,
+ globalFilter: debouncedGlobalFilter,
+ grouping,
+ expanded,
+ });
+
+ if (result.groups) {
+ setServerGroups(result.groups);
+ setIsServerGrouped(true);
+ setData([]); // Clear flat data
+ } else {
+ setData(result.data);
+ setTotalRows(result.totalRows);
+ setPageCount(result.pageCount ?? -1);
+ setServerGroups([]);
+ setIsServerGrouped(false);
+ if (onDataChange) onDataChange(result.data);
+ }
+ } catch (err) {
+ console.error("Failed to fetch table data:", err);
+ if (onError) onError(err);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [
+ isServer,
+ fetcher,
+ pagination,
+ sorting,
+ debouncedColumnFilters,
+ debouncedGlobalFilter,
+ grouping,
+ expanded,
+ onDataChange,
+ onError,
+ ]);
+
+ // Initial fetch and refetch on state change
+ React.useEffect(() => {
+ if (isServer) {
+ refresh();
+ }
+ }, [refresh, isServer]);
+
+ // Update data when props change in Client Mode
+ React.useEffect(() => {
+ if (!isServer) {
+ setData(initialData);
+ setTotalRows(initialData.length);
+ }
+ }, [initialData, isServer]);
+
+ // 4. TanStack Table Instance
+ const table = useReactTable({
+ data,
+ columns,
+ state: {
+ sorting,
+ columnFilters,
+ globalFilter,
+ pagination,
+ columnVisibility,
+ columnPinning,
+ columnOrder,
+ rowSelection,
+ grouping,
+ expanded,
+ },
+ // Server-side Flags
+ manualPagination: isServer,
+ manualSorting: isServer,
+ manualFiltering: isServer,
+ manualGrouping: isServer,
+
+ // Counts
+ pageCount: isServer ? pageCount : undefined,
+ rowCount: isServer ? totalRows : undefined,
+
+ // Handlers
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onGlobalFilterChange: setGlobalFilter,
+ onPaginationChange: setPagination,
+ onColumnVisibilityChange: setColumnVisibility,
+ onColumnPinningChange: setColumnPinning,
+ onColumnOrderChange: setColumnOrder,
+ onRowSelectionChange: setRowSelection,
+ onGroupingChange: setGrouping,
+ onExpandedChange: setExpanded,
+
+ // Configs
+ enableRowSelection,
+ enableMultiRowSelection,
+ enableGrouping,
+ getCoreRowModel: getCoreRowModel(),
+
+ // Conditional Models (Client vs Server)
+ getFilteredRowModel: !isServer ? getFilteredRowModel() : undefined,
+ getFacetedRowModel: !isServer ? getFacetedRowModel() : undefined,
+ getFacetedUniqueValues: !isServer ? getFacetedUniqueValues() : undefined,
+ getFacetedMinMaxValues: !isServer ? getFacetedMinMaxValues() : undefined,
+ getSortedRowModel: !isServer ? getSortedRowModel() : undefined,
+ getGroupedRowModel: (!isServer && enableGrouping) ? getGroupedRowModel() : undefined,
+ getExpandedRowModel: (!isServer && enableGrouping) ? getExpandedRowModel() : undefined,
+ getPaginationRowModel: (!isServer && enablePagination) ? getPaginationRowModel() : undefined,
+
+ columnResizeMode: "onChange",
+ filterFns: {
+ fuzzy: fuzzyFilter,
+ },
+ globalFilterFn: fuzzyFilter,
+ getRowId,
+ });
+
+ return {
+ table,
+ data,
+ totalRows,
+ isLoading,
+ isServerGrouped,
+ serverGroups,
+ refresh,
+ // State setters if needed manually
+ setSorting,
+ setColumnFilters,
+ setPagination,
+ setGlobalFilter,
+ };
+}
+
+
diff --git a/db/db.ts b/db/db.ts
index 4a51d870..95e3d89a 100644
--- a/db/db.ts
+++ b/db/db.ts
@@ -3,8 +3,8 @@ import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({
- connectionString: process.env.DATABASE_URL as string,
- // connectionString: "postgresql://dts:dujinDTS2@localhost:5432/evcp",
+ // connectionString: process.env.DATABASE_URL as string,
+ connectionString: "postgresql://dts:dujinDTS2@localhost:5432/evcp",
max: Number(process.env.DB_POOL_MAX) || 4,
});
diff --git a/db/schema/index.ts b/db/schema/index.ts
index 022431cc..da17b069 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -95,4 +95,7 @@ export * from './avl/avl';
export * from './avl/vendor-pool';
// === Email Logs 스키마 ===
export * from './emailLogs';
-export * from './emailWhitelist'; \ No newline at end of file
+export * from './emailWhitelist';
+
+// === Test Table V2 (테스트용 스키마) ===
+export * from './test-table-v2'; \ No newline at end of file
diff --git a/db/schema/test-table-v2.ts b/db/schema/test-table-v2.ts
new file mode 100644
index 00000000..37ccccbd
--- /dev/null
+++ b/db/schema/test-table-v2.ts
@@ -0,0 +1,139 @@
+/**
+ * Test Table Schema for client-table-v2 테스트용 스키마
+ *
+ * 3가지 패턴 테스트를 위한 테이블:
+ * 1. testProducts - 기본 상품 테이블 (Client-Side, Factory Service 패턴용)
+ * 2. testOrders - 주문 테이블 (Custom Service 패턴용 - 조인 테스트)
+ * 3. testCustomers - 고객 테이블 (Custom Service 패턴용 - 조인 테스트)
+ */
+
+import {
+ integer,
+ serial,
+ pgTable,
+ varchar,
+ timestamp,
+ pgEnum,
+ text,
+ numeric,
+ boolean,
+ index,
+} from "drizzle-orm/pg-core";
+
+// === Enums ===
+
+export const testProductStatusEnum = pgEnum("test_product_status", [
+ "active",
+ "inactive",
+ "discontinued",
+]);
+
+export const testOrderStatusEnum = pgEnum("test_order_status", [
+ "pending",
+ "processing",
+ "shipped",
+ "delivered",
+ "cancelled",
+]);
+
+// === Tables ===
+
+/**
+ * 상품 테이블 - Client-Side 및 Factory Service 패턴 테스트용
+ */
+export const testProducts = pgTable(
+ "test_products",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ name: varchar("name", { length: 255 }).notNull(),
+ sku: varchar("sku", { length: 50 }).notNull().unique(),
+ description: text("description"),
+ category: varchar("category", { length: 100 }).notNull(),
+ price: numeric("price", { precision: 10, scale: 2 }).notNull(),
+ stock: integer("stock").notNull().default(0),
+ status: testProductStatusEnum("status").notNull().default("active"),
+ isNew: boolean("is_new").default(false),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ },
+ (table) => {
+ return {
+ categoryIdx: index("test_products_category_idx").on(table.category),
+ statusIdx: index("test_products_status_idx").on(table.status),
+ };
+ }
+);
+
+/**
+ * 고객 테이블 - Custom Service 패턴의 조인 테스트용
+ */
+export const testCustomers = pgTable(
+ "test_customers",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ name: varchar("name", { length: 255 }).notNull(),
+ email: varchar("email", { length: 255 }).notNull().unique(),
+ phone: varchar("phone", { length: 40 }),
+ country: varchar("country", { length: 100 }),
+ tier: varchar("tier", { length: 40 }).notNull().default("standard"), // standard, premium, vip
+ totalOrders: integer("total_orders").default(0),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ },
+ (table) => {
+ return {
+ tierIdx: index("test_customers_tier_idx").on(table.tier),
+ };
+ }
+);
+
+/**
+ * 주문 테이블 - Custom Service 패턴용 (고객/상품 조인)
+ */
+export const testOrders = pgTable(
+ "test_orders",
+ {
+ id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
+ orderNumber: varchar("order_number", { length: 50 }).notNull().unique(),
+ customerId: integer("customer_id")
+ .references(() => testCustomers.id, { onDelete: "cascade" })
+ .notNull(),
+ productId: integer("product_id")
+ .references(() => testProducts.id, { onDelete: "set null" }),
+ quantity: integer("quantity").notNull().default(1),
+ unitPrice: numeric("unit_price", { precision: 10, scale: 2 }).notNull(),
+ totalAmount: numeric("total_amount", { precision: 10, scale: 2 }).notNull(),
+ status: testOrderStatusEnum("status").notNull().default("pending"),
+ notes: text("notes"),
+ orderedAt: timestamp("ordered_at", { withTimezone: true })
+ .defaultNow()
+ .notNull(),
+ shippedAt: timestamp("shipped_at", { withTimezone: true }),
+ deliveredAt: timestamp("delivered_at", { withTimezone: true }),
+ },
+ (table) => {
+ return {
+ customerIdx: index("test_orders_customer_idx").on(table.customerId),
+ productIdx: index("test_orders_product_idx").on(table.productId),
+ statusIdx: index("test_orders_status_idx").on(table.status),
+ orderedAtIdx: index("test_orders_ordered_at_idx").on(table.orderedAt),
+ };
+ }
+);
+
+// === Types ===
+
+export type TestProduct = typeof testProducts.$inferSelect;
+export type NewTestProduct = typeof testProducts.$inferInsert;
+
+export type TestCustomer = typeof testCustomers.$inferSelect;
+export type NewTestCustomer = typeof testCustomers.$inferInsert;
+
+export type TestOrder = typeof testOrders.$inferSelect;
+export type NewTestOrder = typeof testOrders.$inferInsert;
+
diff --git a/db/seeds/test-table-v2.ts b/db/seeds/test-table-v2.ts
new file mode 100644
index 00000000..07bf8914
--- /dev/null
+++ b/db/seeds/test-table-v2.ts
@@ -0,0 +1,187 @@
+/**
+ * Test Table V2 Seeding Script
+ *
+ * 사용법:
+ * npx tsx db/seeds/test-table-v2.ts
+ */
+
+import db from "@/db/db";
+import { faker } from "@faker-js/faker";
+import {
+ testProducts,
+ testCustomers,
+ testOrders,
+ NewTestProduct,
+ NewTestCustomer,
+ NewTestOrder,
+} from "../schema/test-table-v2";
+
+// === Generators ===
+
+const CATEGORIES = [
+ "Electronics",
+ "Clothing",
+ "Home & Garden",
+ "Sports",
+ "Books",
+ "Toys",
+ "Food",
+ "Health",
+];
+
+const PRODUCT_STATUSES = ["active", "inactive", "discontinued"] as const;
+const ORDER_STATUSES = ["pending", "processing", "shipped", "delivered", "cancelled"] as const;
+const CUSTOMER_TIERS = ["standard", "premium", "vip"] as const;
+const COUNTRIES = ["Korea", "USA", "Japan", "Germany", "UK", "France", "China", "Singapore"];
+
+function generateProduct(): NewTestProduct {
+ const category = faker.helpers.arrayElement(CATEGORIES);
+ const price = faker.number.float({ min: 10, max: 1000, fractionDigits: 2 });
+
+ return {
+ name: faker.commerce.productName(),
+ sku: faker.string.alphanumeric({ length: 8, casing: "upper" }),
+ description: faker.commerce.productDescription(),
+ category,
+ price: price.toString(),
+ stock: faker.number.int({ min: 0, max: 500 }),
+ status: faker.helpers.arrayElement(PRODUCT_STATUSES),
+ isNew: faker.datatype.boolean({ probability: 0.2 }),
+ createdAt: faker.date.past({ years: 2 }),
+ updatedAt: faker.date.recent({ days: 30 }),
+ };
+}
+
+function generateCustomer(): NewTestCustomer {
+ return {
+ name: faker.person.fullName(),
+ email: faker.internet.email(),
+ phone: faker.phone.number(),
+ country: faker.helpers.arrayElement(COUNTRIES),
+ tier: faker.helpers.arrayElement(CUSTOMER_TIERS),
+ totalOrders: faker.number.int({ min: 0, max: 100 }),
+ createdAt: faker.date.past({ years: 3 }),
+ };
+}
+
+function generateOrder(
+ customerId: number,
+ productId: number | null,
+ productPrice: number
+): NewTestOrder {
+ const quantity = faker.number.int({ min: 1, max: 10 });
+ const unitPrice = productPrice || faker.number.float({ min: 10, max: 500, fractionDigits: 2 });
+ const totalAmount = quantity * unitPrice;
+ const status = faker.helpers.arrayElement(ORDER_STATUSES);
+ const orderedAt = faker.date.past({ years: 1 });
+
+ let shippedAt: Date | undefined;
+ let deliveredAt: Date | undefined;
+
+ if (status === "shipped" || status === "delivered") {
+ shippedAt = faker.date.between({ from: orderedAt, to: new Date() });
+ }
+ if (status === "delivered") {
+ deliveredAt = faker.date.between({ from: shippedAt || orderedAt, to: new Date() });
+ }
+
+ return {
+ orderNumber: `ORD-${faker.string.alphanumeric({ length: 8, casing: "upper" })}`,
+ customerId,
+ productId,
+ quantity,
+ unitPrice: unitPrice.toString(),
+ totalAmount: totalAmount.toString(),
+ status,
+ notes: faker.datatype.boolean({ probability: 0.3 }) ? faker.lorem.sentence() : null,
+ orderedAt,
+ shippedAt,
+ deliveredAt,
+ };
+}
+
+// === Main Seeding Function ===
+
+export async function seedTestTableV2(options: {
+ productCount?: number;
+ customerCount?: number;
+ orderCount?: number;
+} = {}) {
+ const {
+ productCount = 100,
+ customerCount = 50,
+ orderCount = 200,
+ } = options;
+
+ console.log("🗑️ Clearing existing test data...");
+
+ // Delete in order (orders first due to FK)
+ await db.delete(testOrders);
+ await db.delete(testCustomers);
+ await db.delete(testProducts);
+
+ console.log(`📦 Generating ${productCount} products...`);
+ const products: NewTestProduct[] = [];
+ for (let i = 0; i < productCount; i++) {
+ products.push(generateProduct());
+ }
+ const insertedProducts = await db.insert(testProducts).values(products).returning();
+ console.log(`✅ Inserted ${insertedProducts.length} products`);
+
+ console.log(`👥 Generating ${customerCount} customers...`);
+ const customers: NewTestCustomer[] = [];
+ for (let i = 0; i < customerCount; i++) {
+ customers.push(generateCustomer());
+ }
+ const insertedCustomers = await db.insert(testCustomers).values(customers).returning();
+ console.log(`✅ Inserted ${insertedCustomers.length} customers`);
+
+ console.log(`🛒 Generating ${orderCount} orders...`);
+ const orders: NewTestOrder[] = [];
+ for (let i = 0; i < orderCount; i++) {
+ const customer = faker.helpers.arrayElement(insertedCustomers);
+ const product = faker.helpers.arrayElement(insertedProducts);
+ orders.push(generateOrder(customer.id, product.id, parseFloat(product.price)));
+ }
+ const insertedOrders = await db.insert(testOrders).values(orders).returning();
+ console.log(`✅ Inserted ${insertedOrders.length} orders`);
+
+ console.log("🎉 Test table v2 seeding completed!");
+
+ return {
+ products: insertedProducts.length,
+ customers: insertedCustomers.length,
+ orders: insertedOrders.length,
+ };
+}
+
+// === CLI Runner ===
+
+async function main() {
+ console.log("⏳ Starting test-table-v2 seed...");
+ const start = Date.now();
+
+ try {
+ const result = await seedTestTableV2({
+ productCount: 100,
+ customerCount: 50,
+ orderCount: 200,
+ });
+
+ const end = Date.now();
+ console.log(`\n📊 Summary:`);
+ console.log(` Products: ${result.products}`);
+ console.log(` Customers: ${result.customers}`);
+ console.log(` Orders: ${result.orders}`);
+ console.log(`\n✅ Seed completed in ${end - start}ms`);
+ } catch (err) {
+ console.error("❌ Seed failed:", err);
+ process.exit(1);
+ }
+
+ process.exit(0);
+}
+
+// Run if called directly
+main();
+
diff --git a/lib/table/server-query-builder.ts b/lib/table/server-query-builder.ts
deleted file mode 100644
index 7ea25313..00000000
--- a/lib/table/server-query-builder.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import {
- ColumnFiltersState,
- SortingState,
- PaginationState,
- GroupingState
-} from "@tanstack/react-table";
-import {
- SQL,
- and,
- or,
- eq,
- ilike,
- like,
- gt,
- lt,
- gte,
- lte,
- inArray,
- asc,
- desc,
- not,
- sql
-} from "drizzle-orm";
-import { PgTable } from "drizzle-orm/pg-core";
-
-/**
- * Table State를 Drizzle Query 조건으로 변환하는 유틸리티
- */
-export class TableQueryBuilder {
- private table: PgTable;
- private searchableColumns: string[];
-
- constructor(table: PgTable, searchableColumns: string[] = []) {
- this.table = table;
- this.searchableColumns = searchableColumns;
- }
-
- /**
- * Pagination State -> Limit/Offset
- */
- getPagination(pagination: PaginationState) {
- return {
- limit: pagination.pageSize,
- offset: pagination.pageIndex * pagination.pageSize,
- };
- }
-
- /**
- * Sorting State -> Order By
- */
- getOrderBy(sorting: SortingState) {
- if (!sorting.length) return [];
-
- return sorting.map((sort) => {
- // 컬럼 이름이 테이블에 존재하는지 확인
- const column = this.table[sort.id as keyof typeof this.table];
- if (!column) return null;
-
- return sort.desc ? desc(column) : asc(column);
- }).filter(Boolean) as SQL[];
- }
-
- /**
- * Column Filters -> Where Clause
- */
- getWhere(columnFilters: ColumnFiltersState, globalFilter?: string) {
- const conditions: SQL[] = [];
-
- // 1. Column Filters
- for (const filter of columnFilters) {
- const column = this.table[filter.id as keyof typeof this.table];
- if (!column) continue;
-
- const value = filter.value;
-
- // 값의 타입에 따라 적절한 연산자 선택 (기본적인 예시)
- if (Array.isArray(value)) {
- // 범위 필터 (예: 날짜, 숫자 범위)
- if (value.length === 2) {
- const [min, max] = value;
- if (min !== null && max !== null) {
- conditions.push(and(gte(column, min), lte(column, max))!);
- } else if (min !== null) {
- conditions.push(gte(column, min)!);
- } else if (max !== null) {
- conditions.push(lte(column, max)!);
- }
- }
- // 다중 선택 (Select)
- else {
- conditions.push(inArray(column, value)!);
- }
- } else if (typeof value === 'string') {
- // 텍스트 검색 (Partial Match)
- conditions.push(ilike(column, `%${value}%`)!);
- } else if (typeof value === 'boolean') {
- conditions.push(eq(column, value)!);
- } else if (typeof value === 'number') {
- conditions.push(eq(column, value)!);
- }
- }
-
- // 2. Global Filter (검색창)
- if (globalFilter && this.searchableColumns.length > 0) {
- const searchConditions = this.searchableColumns.map(colName => {
- const column = this.table[colName as keyof typeof this.table];
- if (!column) return null;
- return ilike(column, `%${globalFilter}%`);
- }).filter(Boolean) as SQL[];
-
- if (searchConditions.length > 0) {
- conditions.push(or(...searchConditions)!);
- }
- }
-
- return conditions.length > 0 ? and(...conditions) : undefined;
- }
-
- /**
- * Grouping State -> Group By & Select
- * 주의: Group By 사용 시 집계 함수가 필요할 수 있음
- */
- getGroupBy(grouping: GroupingState) {
- return grouping.map(g => {
- const column = this.table[g as keyof typeof this.table];
- return column;
- }).filter(Boolean);
- }
-}
diff --git a/lib/vendors/items-table/item-action-dialog.tsx b/lib/vendors/items-table/item-action-dialog.tsx
index 19df27f8..6bbcc436 100644
--- a/lib/vendors/items-table/item-action-dialog.tsx
+++ b/lib/vendors/items-table/item-action-dialog.tsx
@@ -1,248 +1,289 @@
-// components/vendor-items/item-actions-dialogs.tsx
"use client"
import * as React from "react"
-import type { DataTableRowAction } from "@/types/table"
-import { VendorItemsView } from "@/db/schema/vendors"
-import { toast } from "sonner"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { Check, ChevronsUpDown } from "lucide-react"
+import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
-} from "@/components/ui/alert-dialog"
-import { Button } from "@/components/ui/button"
-import { Label } from "@/components/ui/label"
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command"
+import { cn } from "@/lib/utils"
-import { updateVendorItem, deleteVendorItem, getItemsForVendor } from "../service"
+import {
+ createVendorItemSchema,
+ type CreateVendorItemSchema,
+} from "../validations"
-interface ItemActionsDialogsProps {
+import { createVendorItem, getItemsForVendor, ItemDropdownOption } from "../service"
+
+interface AddItemDialogProps {
vendorId: number
- rowAction: DataTableRowAction<VendorItemsView> | null
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<VendorItemsView> | null>>
}
-export function ItemActionsDialogs({
- vendorId,
- rowAction,
- setRowAction,
-}: ItemActionsDialogsProps) {
- const [isUpdatePending, startUpdateTransition] = React.useTransition()
- const [isDeletePending, startDeleteTransition] = React.useTransition()
- const [availableMaterials, setAvailableMaterials] = React.useState<any[]>([])
- const [selectedItemCode, setSelectedItemCode] = React.useState<string>("")
-
- // 사용 가능한 재료 목록 로드
- React.useEffect(() => {
- if (rowAction?.type === "update") {
- getItemsForVendor(vendorId).then((result) => {
- if (result.data) {
- setAvailableMaterials(result.data)
- }
- })
- }
- }, [rowAction, vendorId])
-
- // Edit Dialog
- const EditDialog = () => {
- if (!rowAction || rowAction.type !== "update") return null
+export function AddItemDialog({ vendorId }: AddItemDialogProps) {
+ const [open, setOpen] = React.useState(false)
+ const [commandOpen, setCommandOpen] = React.useState(false)
+ const [items, setItems] = React.useState<ItemDropdownOption[]>([])
+ const [filteredItems, setFilteredItems] = React.useState<ItemDropdownOption[]>([])
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [searchTerm, setSearchTerm] = React.useState("")
+
+ // 선택된 아이템의 정보를 보여주기 위한 상태
+ const [selectedItem, setSelectedItem] = React.useState<{
+ itemName: string;
+ description: string;
+ } | null>(null)
- const item = rowAction.row.original
+ // react-hook-form 세팅 - 서버로 보낼 값은 vendorId와 itemCode만
+ const form = useForm<CreateVendorItemSchema>({
+ resolver: zodResolver(createVendorItemSchema),
+ defaultValues: {
+ vendorId,
+ itemCode: "",
+ },
+ })
- const handleSubmit = () => {
- if (!selectedItemCode) {
- toast.error("Please select a new item")
- return
- }
+ console.log(vendorId)
- if (!item.itemCode) {
- toast.error("Invalid item code")
- return
+ // 아이템 목록 가져오기 (한 번만 호출)
+ const fetchItems = React.useCallback(async () => {
+ if (items.length > 0) return // 이미 로드된 경우 스킵
+
+ setIsLoading(true)
+ try {
+ const result = await getItemsForVendor(vendorId)
+ if (result.data) {
+ setItems(result.data)
+ setFilteredItems(result.data)
}
+ } catch (error) {
+ console.error("Failed to fetch items:", error)
+ } finally {
+ setIsLoading(false)
+ }
+ }, [items.length])
- startUpdateTransition(async () => {
- const result = await updateVendorItem(vendorId, item.itemCode, selectedItemCode)
-
- if (result.error) {
- toast.error(result.error)
- } else {
- toast.success("Item updated successfully")
- setRowAction(null)
- }
- })
+ // 팝오버 열릴 때 아이템 목록 로드
+ React.useEffect(() => {
+ if (commandOpen) {
+ fetchItems()
}
+ }, [commandOpen, fetchItems])
- return (
- <Dialog
- open={true}
- onOpenChange={(open) => !open && setRowAction(null)}
- >
- <DialogContent className="sm:max-w-[425px]">
- <DialogHeader>
- <DialogTitle>Change Item</DialogTitle>
- <DialogDescription>
- Select a new item to replace "{item.itemName}" (Code: {item.itemCode || 'N/A'}).
- </DialogDescription>
- </DialogHeader>
-
- <div className="space-y-4">
- <div className="space-y-2">
- <Label>Current Item</Label>
- <div className="p-2 bg-muted rounded-md">
- <div className="font-medium">{item.itemName}</div>
- <div className="text-sm text-muted-foreground">Code: {item.itemCode || 'N/A'}</div>
- </div>
- </div>
-
- <div className="space-y-2">
- <Label htmlFor="newItem">New Item</Label>
- <Select value={selectedItemCode} onValueChange={setSelectedItemCode}>
- <SelectTrigger>
- <SelectValue placeholder="Select a new item" />
- </SelectTrigger>
- <SelectContent>
- {availableMaterials.map((material) => (
- <SelectItem key={material.itemCode} value={material.itemCode}>
- <div>
- <div className="font-medium">{material.itemName}</div>
- <div className="text-sm text-muted-foreground">Code: {material.itemCode}</div>
- </div>
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
- </div>
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => setRowAction(null)}
- disabled={isUpdatePending}
- >
- Cancel
- </Button>
- <Button
- onClick={handleSubmit}
- disabled={isUpdatePending || !selectedItemCode}
- >
- {isUpdatePending ? "Updating..." : "Update Item"}
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
+ // 클라이언트 사이드 필터링
+ React.useEffect(() => {
+ if (!items.length) return
+
+ if (!searchTerm.trim()) {
+ setFilteredItems(items)
+ return
+ }
+
+ const lowerSearch = searchTerm.toLowerCase()
+ const filtered = items.filter(item =>
+ item.itemCode.toLowerCase().includes(lowerSearch) ||
+ item.itemName.toLowerCase().includes(lowerSearch) ||
+ (item.description && item.description.toLowerCase().includes(lowerSearch))
)
- }
-
- // Delete Dialog
- const DeleteDialog = () => {
- if (!rowAction || rowAction.type !== "delete") return null
-
- const item = rowAction.row.original
+
+ setFilteredItems(filtered)
+ }, [searchTerm, items])
- const handleDelete = () => {
- if (!item.itemCode) {
- toast.error("Invalid item code")
- return
- }
+ // 선택된 아이템 데이터로 폼 업데이트
+ const handleSelectItem = (item: ItemDropdownOption) => {
+ // 폼에는 itemCode만 설정
+ form.setValue("itemCode", item.itemCode)
+
+ // 나머지 정보는 표시용 상태에 저장
+ setSelectedItem({
+ itemName: item.itemName,
+ description: item.description || "",
+ })
+
+ setCommandOpen(false)
+ }
- startDeleteTransition(async () => {
- const result = await deleteVendorItem(vendorId, item.itemCode)
-
- if (result.error) {
- toast.error(result.error)
- } else {
- toast.success("Item deleted successfully")
- setRowAction(null)
- }
- })
+ // 폼 제출 - itemCode만 서버로 전송
+ async function onSubmit(data: CreateVendorItemSchema) {
+ // 서버에는 vendorId와 itemCode만 전송됨
+ const result = await createVendorItem(data)
+ console.log(result)
+ if (result.error) {
+ alert(`에러: ${result.error}`)
+ return
}
-
- return (
- <AlertDialog
- open={true}
- onOpenChange={(open) => !open && setRowAction(null)}
- >
- <AlertDialogContent>
- return (
- <AlertDialog
- open={true}
- onOpenChange={(open) => !open && setRowAction(null)}
- >
- <AlertDialogContent>
- <AlertDialogHeader>
- <AlertDialogTitle>Are you sure?</AlertDialogTitle>
- <AlertDialogDescription>
- This will permanently delete the item "{item.itemName}" (Code: {item.itemCode || 'N/A'}).
- This action cannot be undone.
- </AlertDialogDescription>
- </AlertDialogHeader>
- <AlertDialogFooter>
- <AlertDialogCancel disabled={isDeletePending}>
- Cancel
- </AlertDialogCancel>
- <AlertDialogAction
- onClick={handleDelete}
- disabled={isDeletePending}
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
- >
- {isDeletePending ? "Deleting..." : "Delete"}
- </AlertDialogAction>
- </AlertDialogFooter>
- </AlertDialogContent>
- </AlertDialog>
- )
+ // 성공 시 모달 닫고 폼 리셋
+ form.reset()
+ setSelectedItem(null)
+ setOpen(false)
}
- return (
- <>
- <EditDialog />
- <DeleteDialog />
- </>
- )
-}
- <AlertDialogFooter>
- <AlertDialogCancel disabled={isDeletePending}>
- Cancel
- </AlertDialogCancel>
- <AlertDialogAction
- onClick={handleDelete}
- disabled={isDeletePending}
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
- >
- {isDeletePending ? "Deleting..." : "Delete"}
- </AlertDialogAction>
- </AlertDialogFooter>
- </AlertDialogContent>
- </AlertDialog>
- )
+ // 모달 열림/닫힘 핸들
+ function handleDialogOpenChange(nextOpen: boolean) {
+ if (!nextOpen) {
+ // 닫힐 때 폼 리셋
+ form.reset()
+ setSelectedItem(null)
+ }
+ setOpen(nextOpen)
}
+ // 현재 선택된 아이템 코드
+ const selectedItemCode = form.watch("itemCode")
+
+ // 선택된 아이템 코드가 있으면 상세 정보 표시를 위한 아이템 찾기
+ const displayItemCode = selectedItemCode || "아이템 선택..."
+ const displayItemName = selectedItem?.itemName || ""
+
return (
- <>
- <EditDialog />
- <DeleteDialog />
- </>
+ <Dialog open={open} onOpenChange={handleDialogOpenChange}>
+ {/* 모달 열기 버튼 */}
+ <DialogTrigger asChild>
+ <Button variant="default" size="sm">
+ Add Item
+ </Button>
+ </DialogTrigger>
+
+ <DialogContent className="max-h-[90vh] overflow-hidden flex flex-col">
+ <DialogHeader>
+ <DialogTitle>Create New Item</DialogTitle>
+ <DialogDescription>
+ 아이템을 선택한 후 <b>Create</b> 버튼을 누르세요.
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* shadcn/ui Form + react-hook-form */}
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 overflow-hidden">
+ <div className="space-y-4 py-4 flex-1 overflow-y-auto">
+
+ {/* 아이템 선택 */}
+ <div>
+ <FormLabel className="text-sm font-medium">아이템 선택</FormLabel>
+ <Popover open={commandOpen} onOpenChange={setCommandOpen}>
+ <PopoverTrigger asChild>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={commandOpen}
+ className="w-full justify-between mt-1"
+ >
+ {selectedItemCode
+ ? `${selectedItemCode} - ${displayItemName}`
+ : "아이템 선택..."}
+ <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ </Button>
+ </PopoverTrigger>
+ <PopoverContent className="w-[400px] p-0">
+ <Command>
+ <CommandInput
+ placeholder="아이템 코드/이름 검색..."
+ onValueChange={setSearchTerm}
+ />
+ <CommandList className="max-h-[200px]">
+ <CommandEmpty>검색 결과가 없습니다</CommandEmpty>
+ {isLoading ? (
+ <div className="py-6 text-center text-sm">로딩 중...</div>
+ ) : (
+ <CommandGroup>
+ {filteredItems.map((item) => (
+ <CommandItem
+ key={item.itemCode}
+ value={`${item.itemCode} ${item.itemName}`}
+ onSelect={() => handleSelectItem(item)}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ selectedItemCode === item.itemCode
+ ? "opacity-100"
+ : "opacity-0"
+ )}
+ />
+ <span className="font-medium">{item.itemCode}</span>
+ <span className="ml-2 text-gray-500 truncate">- {item.itemName}</span>
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ )}
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ </div>
+
+ {/* 아이템 정보 영역 - 선택된 경우에만 표시 */}
+ {selectedItem && (
+ <div className="rounded-md border p-3 mt-4 overflow-hidden">
+ <h3 className="font-medium text-sm mb-2">선택된 아이템 정보</h3>
+
+ {/* Item Code - readonly (hidden field) */}
+ <FormField
+ control={form.control}
+ name="itemCode"
+ render={({ field }) => (
+ <FormItem className="hidden">
+ <FormControl>
+ <Input {...field} />
+ </FormControl>
+ </FormItem>
+ )}
+ />
+
+ {/* Item Name (표시용) */}
+ <div className="mb-2">
+ <p className="text-xs font-medium text-gray-500">Item Name</p>
+ <p className="text-sm mt-0.5 break-words">{selectedItem.itemName}</p>
+ </div>
+
+ {/* Description (표시용) */}
+ {selectedItem.description && (
+ <div>
+ <p className="text-xs font-medium text-gray-500">Description</p>
+ <p className="text-sm mt-0.5 break-words max-h-20 overflow-y-auto">{selectedItem.description}</p>
+ </div>
+ )}
+ </div>
+ )}
+
+ </div>
+
+ <DialogFooter className="flex-shrink-0 pt-2">
+ <Button type="button" variant="outline" onClick={() => setOpen(false)}>
+ Cancel
+ </Button>
+ <Button
+ type="submit"
+ disabled={form.formState.isSubmitting || !selectedItemCode}
+ >
+ Create
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
)
} \ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
index 98c58d2e..9202d625 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -2,14 +2,16 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ['pino', 'pino-pretty', 'node-cron', 'oracledb','sharp', '@pdftron/pdfnet-node'],
-
reactStrictMode: false,
+
eslint: {
ignoreDuringBuilds: true,
},
+
typescript: {
ignoreBuildErrors: true,
},
+
webpack: (config) => {
// [김준회] turbo의 resolveAlias와 동일한 설정을 webpack에 적용
config.resolve.alias = {
@@ -48,34 +50,39 @@ const nextConfig: NextConfig = {
return config;
},
+
experimental: {
serverActions: {
// [김준회] DRM 복호화/암호화 백엔드로 보낼 때 사이즈 제한 변경(기본값: 1MB)
// DDoS 공격을 방지하기 위해 기본값이 1MB로 설정되어 있음. 암호화된 파일 중 큰 파일(도면 등)도 1GB 이하로 가정하여 설정 (파일별로 서버액션 개별 호출)
bodySizeLimit: '1024mb',
},
- turbo: {
- treeShaking: false,
- minify: false,
- unstablePersistentCaching: false,
- // [김준회 프로] 오라클 DB 사용을 위한 라이브러리/nextjs 번들러 호환 문제 해결
- resolveAlias: {
- '@azure/app-configuration': 'data:text/javascript,export default {};',
- '@azure/identity': 'data:text/javascript,export default {};',
- '@azure/keyvault-secrets': 'data:text/javascript,export default {};',
- 'oci-common': 'data:text/javascript,export default {};',
- 'oci-objectstorage': 'data:text/javascript,export default {};',
- 'oci-secrets': 'data:text/javascript,export default {};',
- // knex 관련 데이터베이스 드라이버들
- 'better-sqlite3': 'data:text/javascript,export default {};',
- 'mysql': 'data:text/javascript,export default {};',
- 'mysql2': 'data:text/javascript,export default {};',
- 'pg-query-stream': 'data:text/javascript,export default {};',
- 'sqlite3': 'data:text/javascript,export default {};',
- 'tedious': 'data:text/javascript,export default {};',
- },
- }
+
+ // deprecated options
+ // turbopackTreeShaking: false,
+ // turbopackMinify: false
},
+
+ turbopack: {
+ // unstablePersistentCaching: false,
+
+ // [김준회 프로] 오라클 DB 사용을 위한 라이브러리/nextjs 번들러 호환 문제 해결
+ resolveAlias: {
+ '@azure/app-configuration': 'data:text/javascript,export default {};',
+ '@azure/identity': 'data:text/javascript,export default {};',
+ '@azure/keyvault-secrets': 'data:text/javascript,export default {};',
+ 'oci-common': 'data:text/javascript,export default {};',
+ 'oci-objectstorage': 'data:text/javascript,export default {};',
+ 'oci-secrets': 'data:text/javascript,export default {};',
+ // knex 관련 데이터베이스 드라이버들
+ 'better-sqlite3': 'data:text/javascript,export default {};',
+ 'mysql': 'data:text/javascript,export default {};',
+ 'mysql2': 'data:text/javascript,export default {};',
+ 'pg-query-stream': 'data:text/javascript,export default {};',
+ 'sqlite3': 'data:text/javascript,export default {};',
+ 'tedious': 'data:text/javascript,export default {};',
+ }
+ }
};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 1423b5df..8cd2160e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -106,8 +106,6 @@
"@tiptap/extension-underline": "^2.23.1",
"@tiptap/react": "^2.23.1",
"@tiptap/starter-kit": "^2.23.1",
- "@toast-ui/editor": "^3.2.2",
- "@toast-ui/react-editor": "^3.2.3",
"@types/docusign-esign": "^5.19.8",
"@types/formidable": "^3.4.5",
"accept-language": "^3.0.20",
@@ -119,7 +117,6 @@
"codemirror": "^6.0.2",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
- "dns": "^0.2.2",
"docusign-esign": "^8.0.1",
"docx": "^9.5.1",
"drizzle-orm": "^0.38.2",
@@ -145,13 +142,13 @@
"libphonenumber-js": "^1.12.10",
"lucide-react": "^0.468.0",
"match-sorter": "^8.2.0",
- "next": "15.1.0",
+ "next": "^15.1.9",
"next-auth": "^4.24.11",
"next-i18n-router": "^5.5.1",
"next-i18next": "^15.4.1",
"next-themes": "^0.4.4",
"node-cron": "^4.1.1",
- "nodemailer": "^6.9.16",
+ "nodemailer": "^7.0.7",
"nuqs": "^2.2.3",
"oracledb": "^6.8.0",
"pg": "^8.13.1",
@@ -3387,9 +3384,9 @@
}
},
"node_modules/@next/env": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.0.tgz",
- "integrity": "sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz",
+ "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -3403,9 +3400,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.0.tgz",
- "integrity": "sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
+ "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
"cpu": [
"arm64"
],
@@ -3419,9 +3416,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.0.tgz",
- "integrity": "sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
+ "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
"cpu": [
"x64"
],
@@ -3435,9 +3432,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.0.tgz",
- "integrity": "sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
+ "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
"cpu": [
"arm64"
],
@@ -3451,9 +3448,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.0.tgz",
- "integrity": "sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
+ "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
"cpu": [
"arm64"
],
@@ -3467,9 +3464,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.0.tgz",
- "integrity": "sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz",
+ "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==",
"cpu": [
"x64"
],
@@ -3483,9 +3480,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.0.tgz",
- "integrity": "sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
+ "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
"cpu": [
"x64"
],
@@ -3499,9 +3496,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.0.tgz",
- "integrity": "sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
+ "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
"cpu": [
"arm64"
],
@@ -3515,9 +3512,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.0.tgz",
- "integrity": "sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
+ "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
"cpu": [
"x64"
],
@@ -5058,12 +5055,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@swc/counter": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
- "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
- "license": "Apache-2.0"
- },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -5756,34 +5747,6 @@
"url": "https://github.com/sponsors/ueberdosis"
}
},
- "node_modules/@toast-ui/editor": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/@toast-ui/editor/-/editor-3.2.2.tgz",
- "integrity": "sha512-ASX7LFjN2ZYQJrwmkUajPs7DRr9FsM1+RQ82CfTO0Y5ZXorBk1VZS4C2Dpxinx9kl55V4F8/A2h2QF4QMDtRbA==",
- "license": "MIT",
- "dependencies": {
- "dompurify": "^2.3.3",
- "prosemirror-commands": "^1.1.9",
- "prosemirror-history": "^1.1.3",
- "prosemirror-inputrules": "^1.1.3",
- "prosemirror-keymap": "^1.1.4",
- "prosemirror-model": "^1.14.1",
- "prosemirror-state": "^1.3.4",
- "prosemirror-view": "^1.18.7"
- }
- },
- "node_modules/@toast-ui/react-editor": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/@toast-ui/react-editor/-/react-editor-3.2.3.tgz",
- "integrity": "sha512-86QdgiOkBeSwRBEUWRKsTpnm6yu5j9HNJ3EfQN8EGcd7kI8k8AhExXyUJ3NNgNTzN7FfSKMw+1VaCDDC+aZ3dw==",
- "license": "MIT",
- "dependencies": {
- "@toast-ui/editor": "^3.2.2"
- },
- "peerDependencies": {
- "react": "^17.0.1"
- }
- },
"node_modules/@tsconfig/node10": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
@@ -6748,37 +6711,6 @@
"bcp47": "^1.1.2"
}
},
- "node_modules/accepts": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.0.7.tgz",
- "integrity": "sha512-iq8ew2zitUlNcUca0wye3fYwQ6sSPItDo38oC0R+XA5KTzeXRN+GF7NjOXs3dVItj4J+gQVdpq4/qbnMb1hMHw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~1.0.0",
- "negotiator": "0.4.7"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/accepts/node_modules/mime-types": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz",
- "integrity": "sha512-echfutj/t5SoTL4WZpqjA1DCud1XO0WQF3/GJ48YBmc4ZMhCK77QA6Z/w6VTQERLKuJ4drze3kw2TUT8xZXVNw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/accepts/node_modules/negotiator": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.4.7.tgz",
- "integrity": "sha512-ujxWwyRfZ6udAgHGECQC3JDO9e6UAsuItfUMcqA0Xf2OLNQTveFVFx+fHGIJ5p0MJaJrZyGQqPwzuN0NxJzEKA==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -6815,11 +6747,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/after": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/after/-/after-0.8.1.tgz",
- "integrity": "sha512-SuI3vWhCFeSmkmmJ3efyuOkrhGyp/AuHthh3F5DinGYh2kR9t/0xUlm3/Vn2qMScfgg+cKho5fW7TUEYUhYeiA=="
- },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -6944,9 +6871,9 @@
}
},
"node_modules/archiver-utils/node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -7227,26 +7154,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/arraybuffer.slice": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
- "integrity": "sha512-6ZjfQaBSy6CuIH0+B0NrxMfDE5VIOCP/5gOqSpEIsaAZx9/giszzrXg6PZ7G51U/n88UmlAgYLNQ9wAnII7PJA=="
- },
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
- "node_modules/assert-plus": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
- "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8"
- }
- },
"node_modules/ast-types-flow": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
@@ -7310,14 +7223,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/aws-sign": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/aws-sign/-/aws-sign-0.2.0.tgz",
- "integrity": "sha512-6P7/Ls5F6++DsKu7iacris7qq/AZSWaX+gT4dtSyUxM82ePxWxaP7Slo82ZO3ZTx6GSKxQHAQlmFvM8e+Dd8ZA==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/axe-core": {
"version": "4.10.3",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz",
@@ -7329,9 +7234,9 @@
}
},
"node_modules/axios": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
- "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -7388,14 +7293,6 @@
"integrity": "sha512-b3N5eTW1g7vXkw+0CXh/HazGTcO5KYuu/RCNaJbDMPI6LHDi+7qe8EmxKUVe1sUbY2KZOVZFyj62x0OEz9qyAA==",
"license": "Apache-2.0"
},
- "node_modules/base64-arraybuffer": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.2.tgz",
- "integrity": "sha512-ewBKKVVPIl78B26mYQHYlaxR7NydMiD/GxwLNIwTAfLIE4xhN2Gxcy30//azq5UrejXjzGpWjcBu3NUJxzMMzg==",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -7416,14 +7313,6 @@
],
"license": "MIT"
},
- "node_modules/base64id": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/base64id/-/base64id-0.1.0.tgz",
- "integrity": "sha512-DSjtfjhAsHl9J4OJj7e4+toV2zqxJrGwVd3CLlsCp8QmicvOn7irG0Mb8brOc/nur3SdO8lIbNlY1s1ZDJdUKQ==",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
@@ -7433,12 +7322,6 @@
"node": ">=6.0.0"
}
},
- "node_modules/basic-auth": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.0.0.tgz",
- "integrity": "sha512-qzxS7/bW/LSiKZzdZw3isPjiVmzXbJLM3ImZZ62WMR3oJQAyqy094Nnb0TA2ZZm65xB7nu0acfTQ99z7wwCDCw==",
- "license": "MIT"
- },
"node_modules/bcp47": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/bcp47/-/bcp47-1.1.2.tgz",
@@ -7457,17 +7340,6 @@
"bcrypt": "bin/bcrypt"
}
},
- "node_modules/better-assert": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
- "integrity": "sha512-bYeph2DFlpK1XmGs6fvlLRUN29QISM3GBuUwSFsMY2XRx4AvC0WNCS57j4c/xGrK2RS24C1w3YoBOsw9fT46tQ==",
- "dependencies": {
- "callsite": "1.0.0"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
@@ -7502,14 +7374,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/binaryheap": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/binaryheap/-/binaryheap-0.0.3.tgz",
- "integrity": "sha512-9JFb4Yt5R9FZwbJaxOayF+T5sxn5eiU2NA9/LOeI1g2FUFRTdxpdmWppikO4O5AbNze8s0sL6ZuFxB1y4Ay8GA==",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@@ -7545,56 +7409,12 @@
"ieee754": "^1.1.13"
}
},
- "node_modules/blob": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.2.tgz",
- "integrity": "sha512-BoCcDt8zBGShn6DawAGQw37s9SSs+fEjiZWDzyB+841PbOogcR2X7LGlM4sR3Zsiq/zoyl8MFWDfN6oDSlveBQ=="
- },
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
- "node_modules/body-parser": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.5.0.tgz",
- "integrity": "sha512-UJfZike68QN1mdo0mA+Z0y+0qi10oxOrCPw2CZpP73O/LIfEWHDy9SHhwsME1mdk1WmnltBLddUkfBpuKPH4Ng==",
- "license": "MIT",
- "dependencies": {
- "bytes": "1.0.0",
- "depd": "0.4.2",
- "iconv-lite": "0.4.4",
- "media-typer": "0.2.0",
- "qs": "0.6.6",
- "raw-body": "1.3.0",
- "type-is": "~1.3.2"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/boom": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/boom/-/boom-0.3.1.tgz",
- "integrity": "sha512-xWrlXnkK46TjEW7HU5G39AXWuG5aiHz3++zk3bBzF4mfnVCkpcYbwsnLUqMmfZNgPEYS/AI8MH+vmJxH5Kz0PA==",
- "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).",
- "dependencies": {
- "hoek": "0.4.x"
- },
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/boom/node_modules/hoek": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.4.2.tgz",
- "integrity": "sha512-Yj/N2TCrS0d8jvZgUpq9sDNt8/ABwTxPJW4+8QT0KXCMxOtRfUCUTEZEYyvMSgfDT3MGvwgO+NHfWPobagAIug==",
- "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).",
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -7672,17 +7492,6 @@
"node": ">=0.10"
}
},
- "node_modules/buffercursor": {
- "version": "0.0.12",
- "resolved": "https://registry.npmjs.org/buffercursor/-/buffercursor-0.0.12.tgz",
- "integrity": "sha512-Z+6Jm/eW6ITeqcFQKVXX7LYIGk7rENqCKHJ4CbWfJMeLpQZJj1v70WehkLmp+1kFN/QyCgpQ3Z0dKUHAwSbf9w==",
- "dependencies": {
- "verror": "^1.4.0"
- },
- "engines": {
- "node": ">= 0.5.0"
- }
- },
"node_modules/buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
@@ -7691,22 +7500,6 @@
"node": ">=0.2.0"
}
},
- "node_modules/busboy": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
- "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
- "dependencies": {
- "streamsearch": "^1.1.0"
- },
- "engines": {
- "node": ">=10.16.0"
- }
- },
- "node_modules/bytes": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz",
- "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ=="
- },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -7756,14 +7549,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/callsite": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
- "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -7990,14 +7775,6 @@
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
"license": "MIT"
},
- "node_modules/colors": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
- "integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==",
- "engines": {
- "node": ">=0.1.90"
- }
- },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -8019,21 +7796,6 @@
"node": ">=14"
}
},
- "node_modules/component-bind": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
- "integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw=="
- },
- "node_modules/component-emitter": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz",
- "integrity": "sha512-YhIbp3PJiznERfjlIkK0ue4obZxt2S60+0W8z24ZymOHT8sHloOqWOqZRU2eN5OlY8U08VFsP02letcu26FilA=="
- },
- "node_modules/component-inherit": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
- "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA=="
- },
"node_modules/compress-commons": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
@@ -8072,42 +7834,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
- "node_modules/connect": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/connect/-/connect-3.0.2.tgz",
- "integrity": "sha512-k3kqw6T2Fc5ihvh5VVjwuJHA++qvh0/rPfe2GkJ5QNSQ9tRigQXMjtX2CYf73KZFe4+IV3HfQ3VR3W+nkt0eWQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "1.0.3",
- "finalhandler": "0.0.2",
- "parseurl": "~1.1.3",
- "utils-merge": "1.0.0"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/connect/node_modules/debug": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.3.tgz",
- "integrity": "sha512-MltK7Ykj/udtD728gD/RrONStwVnDpBNIP1h+CBcnwnJdHqHxfWHI1E8XLootUl7NOPAYTCCXlb8/Qmy7WyB1w==",
- "dependencies": {
- "ms": "0.6.2"
- }
- },
- "node_modules/connect/node_modules/ms": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz",
- "integrity": "sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A=="
- },
- "node_modules/connect/node_modules/utils-merge": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
- "integrity": "sha512-HwU9SLQEtyo+0uoKXd1nkLqigUWLB+QuNQR4OcmB73eWqksM5ovuqcycks2x043W8XVb75rG1HQ0h93TMXkzQQ==",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -8129,19 +7855,6 @@
"node": ">= 0.6"
}
},
- "node_modules/cookie-jar": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/cookie-jar/-/cookie-jar-0.2.0.tgz",
- "integrity": "sha512-yImk9AY90xjoUsN2fWHoIhVgveXqiZv7LDqUTZEzVBHyzfay8AjcJITUZpz2fTYLh6rnP+7GogiuRCo/5j2epg==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/cookie-signature": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.4.tgz",
- "integrity": "sha512-k+lrG38ZC/S7zN6l1/HcF6xF4jMwkIUjnr5afDU7tzFxIfDmKzdqJdXo8HNYaXOuBJ3tPKxSiwCOTA0b3qQfaA=="
- },
"node_modules/core-js": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
@@ -8252,18 +7965,6 @@
"node": ">= 8"
}
},
- "node_modules/cryptiles": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-0.1.0.tgz",
- "integrity": "sha512-WiOGszxSaVHd8T4hlu5Xcqs2uUYxbvotBP171ag2pLPKSwSKeTJYnzd98/YWV3jQYk/rpMHa3r01cQfN8SZrHQ==",
- "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).",
- "dependencies": {
- "boom": "0.3.x"
- },
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
@@ -8306,14 +8007,6 @@
"lodash.get": "~4.4.2"
}
},
- "node_modules/cycle": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz",
- "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@@ -8551,14 +8244,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/defaultable": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/defaultable/-/defaultable-0.7.2.tgz",
- "integrity": "sha512-UEaHGfefWfbnANtSlCtuAelo7HZhCbdLAQAttRDVJpQplbA1G21t/J70VGznRA4z9py2k70tTW+3ogGs5VgrcQ==",
- "engines": [
- "node"
- ]
- },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -8610,15 +8295,6 @@
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
- "node_modules/depd": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/depd/-/depd-0.4.2.tgz",
- "integrity": "sha512-tG4S/hTtpA6stvb9Li65vWHrCblQ/oSN/UI90RKIA3wMk3N9lR1k/dCs8NKKNAy7UXD0+1/dUqhiaBuMatVNAQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -8717,25 +8393,6 @@
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"license": "MIT"
},
- "node_modules/dns": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/dns/-/dns-0.2.2.tgz",
- "integrity": "sha512-dhCgBk0QglzySl2BVlIkRuk7aTqxlCe+5KhHEX5ULuco7RcB6d1zDnP5iGSs2rLdJaTc+82MxegtJtjFuueWiQ==",
- "dependencies": {
- "hbo-dnsd": "0.9.8",
- "native-dns": "0.6.1",
- "node-options": "0.0.6",
- "tomahawk": "0.1.6",
- "tomahawk-plugin-kv-memory-store": "0.0.3",
- "winston": "0.7.3"
- },
- "bin": {
- "dns": "bin/dns"
- },
- "engines": {
- "node": ">= 0.10.0 < 0.11.0"
- }
- },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -8808,12 +8465,6 @@
"csstype": "^3.0.2"
}
},
- "node_modules/dompurify": {
- "version": "2.5.8",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
- "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
- "license": "(MPL-2.0 OR Apache-2.0)"
- },
"node_modules/drizzle-kit": {
"version": "0.30.6",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.6.tgz",
@@ -9057,12 +8708,6 @@
"safe-buffer": "^5.0.1"
}
},
- "node_modules/ee-first": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.0.3.tgz",
- "integrity": "sha512-1q/3kz+ZwmrrWpJcCCrBZ3JnBzB1BMA5EVW9nxnIP1LxDZ16Cqs9VdolqLWlExet1vU+bar3WSkAa4/YrA9bIw==",
- "license": "MIT"
- },
"node_modules/embla-carousel": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
@@ -9091,14 +8736,6 @@
"embla-carousel": "8.6.0"
}
},
- "node_modules/emitter": {
- "version": "1.0.1",
- "resolved": "http://github.com/component/emitter/archive/1.0.1.tar.gz",
- "integrity": "sha512-r/UcFj7JS3lRjv9cgYjgpDNbAsGUdqU64n6ZUOgSF7s1UFBbGu7pUDwKEjHu9NBCy6j2AmmjNW4rijR4De65eA==",
- "dependencies": {
- "indexof": "0.0.1"
- }
- },
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -9114,63 +8751,6 @@
"once": "^1.4.0"
}
},
- "node_modules/engine.io": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-1.3.1.tgz",
- "integrity": "sha512-fjnHWC9SLPoygMp6pqwoxmNkDDdYme4eCRTBTZLmEtGZETCpUEgSwoQjSgyj7IyIjqninKRF+2VeEV2kOniUFQ==",
- "dependencies": {
- "base64id": "0.1.0",
- "debug": "0.6.0",
- "engine.io-parser": "1.0.6",
- "ws": "0.4.31"
- }
- },
- "node_modules/engine.io-client": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-1.3.1.tgz",
- "integrity": "sha512-bTOZMqAe7HXhyA/2T7Fve04b/ZZruTHSOqa+yn8U4RFSyRAVPePjopOgJOUNciEfuXo1gx850P5LzaQU28/p3w==",
- "dependencies": {
- "component-emitter": "1.1.2",
- "component-inherit": "0.0.3",
- "debug": "0.7.4",
- "engine.io-parser": "1.0.6",
- "has-cors": "1.0.3",
- "indexof": "0.0.1",
- "parsejson": "0.0.1",
- "parseqs": "0.0.2",
- "parseuri": "0.0.2",
- "ws": "0.4.31",
- "xmlhttprequest": "https://github.com/LearnBoost/node-XMLHttpRequest/archive/0f36d0b5ebc03d85f860d42a64ae9791e1daa433.tar.gz"
- }
- },
- "node_modules/engine.io-client/node_modules/debug": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
- "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/engine.io-parser": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-1.0.6.tgz",
- "integrity": "sha512-ipbmiNj4OfAL9csof0FlI9L2jkU/lgcUphHjnTDo1KABsA21WtsVy/1OjhCj8xxhNIHtxEZ3/t7uB45gEMhD4g==",
- "dependencies": {
- "after": "0.8.1",
- "arraybuffer.slice": "0.0.6",
- "base64-arraybuffer": "0.1.2",
- "blob": "0.0.2",
- "utf8": "2.0.0"
- }
- },
- "node_modules/engine.io/node_modules/debug": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-0.6.0.tgz",
- "integrity": "sha512-2vIZf67+gMicLu8McscD1NNhMWbiTSJkhlByoTA1Gw54zOb/9IlxylYG+Kr9z1X2wZTHh1AMSp+YiMjYtLkVUA==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@@ -9205,19 +8785,6 @@
"is-arrayish": "^0.2.1"
}
},
- "node_modules/errorhandler": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/errorhandler/-/errorhandler-1.1.1.tgz",
- "integrity": "sha512-nqVAii3wDkiowAVKDmcuwKOQ/5vsg9GfCcJxSMHgy8yiZUA3mMDpBcHnCVolDYgQ7wsC2yZQVOavR5fGHhFMkg==",
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.0.4",
- "escape-html": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/es-abstract": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
@@ -9452,11 +9019,6 @@
"node": ">=6"
}
},
- "node_modules/escape-html": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz",
- "integrity": "sha512-z6kAnok8fqVTra7Yu77dZF2Y6ETJlxH58wN38wNyuNQLm8xXdKnfNrlSmfXsTePWP03rRVUKHubtUwanwUi7+g=="
- },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -10129,115 +9691,6 @@
"node": ">= 10"
}
},
- "node_modules/express": {
- "version": "4.6.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.6.1.tgz",
- "integrity": "sha512-nG9Y8xfzgrW/9XCr5sv+KDbtY8mZPN9HO3GziltaubpvleI+1RyHxAKvYjmFih3HkQIaPXW9ozxMHBDNf3UXng==",
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.0.7",
- "buffer-crc32": "0.2.3",
- "cookie": "0.1.2",
- "cookie-signature": "1.0.4",
- "debug": "1.0.3",
- "depd": "0.3.0",
- "escape-html": "1.0.1",
- "finalhandler": "0.0.3",
- "fresh": "0.2.2",
- "media-typer": "0.2.0",
- "merge-descriptors": "0.0.2",
- "methods": "1.1.0",
- "parseurl": "~1.1.3",
- "path-to-regexp": "0.1.3",
- "proxy-addr": "1.0.1",
- "qs": "0.6.6",
- "range-parser": "1.0.0",
- "send": "0.6.0",
- "serve-static": "~1.3.2",
- "type-is": "~1.3.2",
- "utils-merge": "1.0.0",
- "vary": "0.1.0"
- },
- "engines": {
- "node": ">= 0.10.0"
- }
- },
- "node_modules/express/node_modules/buffer-crc32": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.3.tgz",
- "integrity": "sha512-HLvoSqq1z8fJEcT1lUlJZ4OJaXJZ1wsWm0+fBxkz9Bdf/WphA4Da7FtGUguNNyEXL4WB0hNMTaWmdFRFPy8YOQ==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/express/node_modules/cookie": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz",
- "integrity": "sha512-+mHmWbhevLwkiBf7QcbZXHr0v4ZQQ/OgHk3fsQHrsMMiGzuvAmU/YMUR+ZfrO/BLAGIWFfx2Z7Oyso0tZR/wiA==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/express/node_modules/debug": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.3.tgz",
- "integrity": "sha512-MltK7Ykj/udtD728gD/RrONStwVnDpBNIP1h+CBcnwnJdHqHxfWHI1E8XLootUl7NOPAYTCCXlb8/Qmy7WyB1w==",
- "dependencies": {
- "ms": "0.6.2"
- }
- },
- "node_modules/express/node_modules/depd": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-0.3.0.tgz",
- "integrity": "sha512-Uyx3FgdvEYlpA3W4lf37Ide++2qOsjLlJ7dap0tbM63j/BxTCcxmyIOO6PXbKbOuNSko+fsDHzzx1DUeo1+3fA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/express/node_modules/finalhandler": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.0.3.tgz",
- "integrity": "sha512-/fqgssseNfnD8Y77HWyJKQ+1xbKu7bZl2LXfhFjkgeGg91WRMMO9GN1KKL53NnIG9g1H2Xq3iKrZkuIcAmjd0A==",
- "license": "MIT",
- "dependencies": {
- "debug": "1.0.3",
- "escape-html": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/express/node_modules/ms": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz",
- "integrity": "sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A=="
- },
- "node_modules/express/node_modules/utils-merge": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
- "integrity": "sha512-HwU9SLQEtyo+0uoKXd1nkLqigUWLB+QuNQR4OcmB73eWqksM5ovuqcycks2x043W8XVb75rG1HQ0h93TMXkzQQ==",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
- "node_modules/extsprintf": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
- "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
- "engines": [
- "node >=0.6.0"
- ],
- "license": "MIT"
- },
- "node_modules/eyes": {
- "version": "0.1.8",
- "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
- "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
- "engines": {
- "node": "> 0.1.90"
- }
- },
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
@@ -10407,32 +9860,6 @@
"node": ">=8"
}
},
- "node_modules/finalhandler": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.0.2.tgz",
- "integrity": "sha512-SbpQfvWVwWEBlPTQyaM9gs0D5404ENTC0x2jzbb7t+P+EOD/cBlWjAAvfozIQYtOepUuNkxoLNLCK9/kS29f4w==",
- "license": "MIT",
- "dependencies": {
- "debug": "1.0.2",
- "escape-html": "1.0.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/finalhandler/node_modules/debug": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.2.tgz",
- "integrity": "sha512-T9bufXIzQvCa4VrTIpLvvwdLhH+wuBtvIJJA3xgzVcaVETGmTIWMfEXQEd1K4p8BaRmQJPn6MPut38H7YQ+iIA==",
- "dependencies": {
- "ms": "0.6.2"
- }
- },
- "node_modules/finalhandler/node_modules/ms": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz",
- "integrity": "sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A=="
- },
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
@@ -10456,15 +9883,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/finished": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/finished/-/finished-1.2.2.tgz",
- "integrity": "sha512-HPJ8x7Gn1pmTS1zWyMoXmQ1yxHkYHRoFsBI66ONq4PS9iWBJy1iHYXOSqMWNp3ksMXfrBpenkSwBhl9WG4zr4Q==",
- "license": "MIT",
- "dependencies": {
- "ee-first": "1.0.3"
- }
- },
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
@@ -10550,14 +9968,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/forever-agent": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.2.0.tgz",
- "integrity": "sha512-IasWSRIlfPnBZY1K9jEUK3PwsScR4mrcK+aNBJzGoPnW+S9b6f8I8ScyH4cehEOFNqnjGpP2gCaA22gqSV1xQA==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
@@ -10591,11 +10001,6 @@
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
- "node_modules/fresh": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.2.tgz",
- "integrity": "sha512-ZGGi8GROK//ijm2gB33sUuN9TjN1tC/dvG4Bt4j6IWrVGpMmudUBCxx+Ir7qePsdREfkpQC4FL8W0jeSOsgv1w=="
- },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -10919,12 +10324,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/global": {
- "version": "2.0.1",
- "resolved": "https://github.com/component/global/archive/v2.0.1.tar.gz",
- "integrity": "sha512-O91OcV/NbdmQJPHaRu2ekSP7bqFRLWgqSwaJvqHPZHUwmHBagQYTOra29+LnzzG3lZkXH1ANzHzfCxtAPM9HMA==",
- "license": "MIT"
- },
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -11023,30 +10422,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-binary-data": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/has-binary-data/-/has-binary-data-0.1.1.tgz",
- "integrity": "sha512-XqIrqIgPlA2gxvHKudDsLJt8Xu8B4DvkHyUWGmLWYOAO0rFOL94Ds4NSveSZ1fCjWX22tQgIiRpDKAETex8GCQ==",
- "license": "ISC",
- "dependencies": {
- "isarray": "0.0.1"
- }
- },
- "node_modules/has-binary-data/node_modules/isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
- "license": "MIT"
- },
- "node_modules/has-cors": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.0.3.tgz",
- "integrity": "sha512-Mxk1ba23PNtB3zPigreijApS3uuH9bhgZkqQtLQj7ydWHsGeb9uOtk4gsK6mZj4rYG6VNS/CT9G1XkYfgItpKg==",
- "license": "MIT",
- "dependencies": {
- "global": "https://github.com/component/global/archive/v2.0.1.tar.gz"
- }
- },
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -11141,48 +10516,12 @@
"node": ">= 0.4"
}
},
- "node_modules/hawk": {
- "version": "0.10.2",
- "resolved": "https://registry.npmjs.org/hawk/-/hawk-0.10.2.tgz",
- "integrity": "sha512-BjpmnZ95odv7KOIsydfNTAxfGOGaVc6xbYL4fozWl45PWjDqskix0LHAekmGkpnrCAI6+AZRvJIXNTAllj+e6w==",
- "deprecated": "This module moved to @hapi/hawk. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.",
- "dependencies": {
- "boom": "0.3.x",
- "cryptiles": "0.1.x",
- "hoek": "0.7.x",
- "sntp": "0.1.x"
- },
- "engines": {
- "node": "0.8.x"
- }
- },
- "node_modules/hbo-dnsd": {
- "version": "0.9.8",
- "resolved": "https://registry.npmjs.org/hbo-dnsd/-/hbo-dnsd-0.9.8.tgz",
- "integrity": "sha512-mIj4V7OicuAlnSfvTXopd401Ba7eFFSL2L3EmM1NqlIWe1pJ/x9dyHqfnKXnJr5qSbNFLnadXvwNd3kURNy+ug==",
- "dependencies": {
- "defaultable": "~0.7.2",
- "optimist": "~0.3.4"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"license": "MIT"
},
- "node_modules/hoek": {
- "version": "0.7.6",
- "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.7.6.tgz",
- "integrity": "sha512-z75muWk69yyjWn6nNzJP0pnfgcewtSTs7uBolGUA7kWNdCYZukzHn3sYqUirhXul7qp9WBUwNT/7ieJZNveJqg==",
- "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).",
- "engines": {
- "node": "0.8.x"
- }
- },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -11296,15 +10635,6 @@
"@babel/runtime": "^7.23.2"
}
},
- "node_modules/iconv-lite": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.4.tgz",
- "integrity": "sha512-BnjNp13aZpK4WBGbmjaNHN2MCp3P850n8zd/JLinQJ8Lsnq2Br4o2467C2waMsY5kr7Z41SL1gEqh8Vbfzg15A==",
- "license": "MIT",
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -11367,11 +10697,6 @@
"node": ">=0.8.19"
}
},
- "node_modules/indexof": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
- "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg=="
- },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -11432,15 +10757,6 @@
"node": ">= 0.10"
}
},
- "node_modules/ipaddr.js": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
- "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
- "license": "MIT",
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -12012,9 +11328,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12063,18 +11379,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/json-stringify-safe": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-3.0.0.tgz",
- "integrity": "sha512-VSSuxEAawKLYlCabQOR7YDijQ69zPqQBOriUuCgNhlAqtU7RPr41gPpaSs6WkEu+ZOtUequpXWbI51CS+Z/gMQ==",
- "license": "BSD"
- },
- "node_modules/json3": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/json3/-/json3-3.2.6.tgz",
- "integrity": "sha512-KA+GHhYTLTo7Ri4DyjwUgW8kn98AYtVZtBC94qL5yD0ZSYct8/eF8qBmTNyk+gPE578bKeIL4WBq+MUyd1I26g==",
- "deprecated": "Please use the native JSON object instead of JSON 3"
- },
"node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
@@ -12186,12 +11490,12 @@
}
},
"node_modules/jws": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
- "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
+ "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
"license": "MIT",
"dependencies": {
- "jwa": "^1.4.1",
+ "jwa": "^1.4.2",
"safe-buffer": "^5.0.1"
}
},
@@ -12675,21 +11979,6 @@
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
- "node_modules/media-typer": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.2.0.tgz",
- "integrity": "sha512-TSggxYk75oP4tae7JkT8InpcFGUP4340zg1dOWjcu9qcphaDKtXEuNUv3OD4vJ+gVTvIDK797W0uYeNm8qqsDg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/merge-descriptors": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-0.0.2.tgz",
- "integrity": "sha512-dYBT4Ep+t/qnPeJcnMymmhTdd4g8/hn48ciaDqLAkfRf8abzLPS6Rb6EBdz5CZCL8tzZuI5ps9MhGQGxk+EuKg==",
- "license": "MIT"
- },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -12699,12 +11988,6 @@
"node": ">= 8"
}
},
- "node_modules/methods": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.0.tgz",
- "integrity": "sha512-Th88HxNePtsAmz0WjEhVVyRGv9AQFLv4z6zOj4Dt15PjsKLWB8JXSmxzP+Q27139+AXao0AlCWvonFuJhu4GuA==",
- "license": "MIT"
- },
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -12718,11 +12001,6 @@
"node": ">=8.6"
}
},
- "node_modules/mime": {
- "version": "1.2.11",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz",
- "integrity": "sha512-Ysa2F/nqTNGHhhm9MV8ure4+Hc+Y8AWiqUdHxsO7xu8zc92ND9f3kpALHjaP026Ft17UfxrMt95c50PLUeynBw=="
- },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -12823,21 +12101,6 @@
"node": ">=10"
}
},
- "node_modules/morgan": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.2.0.tgz",
- "integrity": "sha512-VrasIzA69dsxJm1+MVWTLTiij3kiG33XPfGiexqstHpcSvSu/Z51W+FGQyIlbc3jZZuF2PFujsjw+YQvpXz3UA==",
- "license": "MIT",
- "dependencies": {
- "basic-auth": "1.0.0",
- "bytes": "1.0.0",
- "depd": "0.4.2",
- "finished": "~1.2.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -12855,12 +12118,6 @@
"thenify-all": "^1.0.0"
}
},
- "node_modules/nan": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/nan/-/nan-0.3.2.tgz",
- "integrity": "sha512-V9/Pyy5Oelv6vVJP9X+dAzU3IO19j6YXrJnODHxP2h54hTvfFQGahdsQV6Ule/UukiEJk1SkQ/aUyWUm61RBQw==",
- "license": "MIT"
- },
"node_modules/nanoid": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
@@ -12895,44 +12152,6 @@
"url": "https://opencollective.com/napi-postinstall"
}
},
- "node_modules/native-dns": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/native-dns/-/native-dns-0.6.1.tgz",
- "integrity": "sha512-svX0dstdoFeEO1sD1Kkrrj/Ad7QfHuczp2YpRnBpjJHqh0dpYLZhLERbf76S6LMkLAT5eZ8tJrPwZciIX5pj6Q==",
- "dependencies": {
- "ipaddr.js": ">= 0.1.1",
- "native-dns-cache": ">= 0.0.1",
- "native-dns-packet": ">= 0.0.4"
- },
- "engines": {
- "node": ">= 0.5.0"
- }
- },
- "node_modules/native-dns-cache": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/native-dns-cache/-/native-dns-cache-0.0.2.tgz",
- "integrity": "sha512-09HXHdb/updxfigaFbR53F8nCKqxM8WuHfTWBsusVlwSSZZ3qwWRdD6Kx2x8HBI1Q5IaycwcJOvBoXZWJNfVEg==",
- "dependencies": {
- "binaryheap": ">= 0.0.3",
- "native-dns-packet": ">= 0.0.1"
- },
- "engines": {
- "node": ">= 0.5.0"
- }
- },
- "node_modules/native-dns-packet": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/native-dns-packet/-/native-dns-packet-0.1.1.tgz",
- "integrity": "sha512-j1XxnFFTUB7mujma468WyAOmyVtkuuLTelxJF13tSTIPO56X7bHALrG0G4jFQnvyTPCt4VnFiZezWpfKbaHc+g==",
- "license": "MIT",
- "dependencies": {
- "buffercursor": ">= 0.0.12",
- "ipaddr.js": ">= 0.1.1"
- },
- "engines": {
- "node": ">= 0.5.0"
- }
- },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -12956,15 +12175,13 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "15.1.0",
- "resolved": "https://registry.npmjs.org/next/-/next-15.1.0.tgz",
- "integrity": "sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==",
+ "version": "15.5.7",
+ "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz",
+ "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==",
"license": "MIT",
"dependencies": {
- "@next/env": "15.1.0",
- "@swc/counter": "0.1.3",
+ "@next/env": "15.5.7",
"@swc/helpers": "0.5.15",
- "busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -12976,19 +12193,19 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "15.1.0",
- "@next/swc-darwin-x64": "15.1.0",
- "@next/swc-linux-arm64-gnu": "15.1.0",
- "@next/swc-linux-arm64-musl": "15.1.0",
- "@next/swc-linux-x64-gnu": "15.1.0",
- "@next/swc-linux-x64-musl": "15.1.0",
- "@next/swc-win32-arm64-msvc": "15.1.0",
- "@next/swc-win32-x64-msvc": "15.1.0",
- "sharp": "^0.33.5"
+ "@next/swc-darwin-arm64": "15.5.7",
+ "@next/swc-darwin-x64": "15.5.7",
+ "@next/swc-linux-arm64-gnu": "15.5.7",
+ "@next/swc-linux-arm64-musl": "15.5.7",
+ "@next/swc-linux-x64-gnu": "15.5.7",
+ "@next/swc-linux-x64-musl": "15.5.7",
+ "@next/swc-win32-arm64-msvc": "15.5.7",
+ "@next/swc-win32-x64-msvc": "15.5.7",
+ "sharp": "^0.34.3"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
- "@playwright/test": "^1.41.2",
+ "@playwright/test": "^1.51.1",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
@@ -13010,9 +12227,9 @@
}
},
"node_modules/next-auth": {
- "version": "4.24.11",
- "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz",
- "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==",
+ "version": "4.24.13",
+ "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz",
+ "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==",
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.20.13",
@@ -13026,9 +12243,9 @@
"uuid": "^8.3.2"
},
"peerDependencies": {
- "@auth/core": "0.34.2",
- "next": "^12.2.5 || ^13 || ^14 || ^15",
- "nodemailer": "^6.6.5",
+ "@auth/core": "0.34.3",
+ "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16",
+ "nodemailer": "^7.0.7",
"react": "^17.0.2 || ^18 || ^19",
"react-dom": "^17.0.2 || ^18 || ^19"
},
@@ -13106,367 +12323,6 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
- "node_modules/next/node_modules/@img/sharp-darwin-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
- "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.0.4"
- }
- },
- "node_modules/next/node_modules/@img/sharp-darwin-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
- "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.0.4"
- }
- },
- "node_modules/next/node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
- "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
- "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
- "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
- "cpu": [
- "arm"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
- "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
- "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
- "cpu": [
- "s390x"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
- "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
- "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
- "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-linux-arm": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
- "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
- "cpu": [
- "arm"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.0.5"
- }
- },
- "node_modules/next/node_modules/@img/sharp-linux-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
- "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.0.4"
- }
- },
- "node_modules/next/node_modules/@img/sharp-linux-s390x": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
- "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
- "cpu": [
- "s390x"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.0.4"
- }
- },
- "node_modules/next/node_modules/@img/sharp-linux-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
- "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.0.4"
- }
- },
- "node_modules/next/node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
- "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
- }
- },
- "node_modules/next/node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
- "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
- }
- },
- "node_modules/next/node_modules/@img/sharp-wasm32": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
- "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
- "cpu": [
- "wasm32"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/runtime": "^1.2.0"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-win32-ia32": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
- "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
- "cpu": [
- "ia32"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/next/node_modules/@img/sharp-win32-x64": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
- "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
"node_modules/next/node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -13513,46 +12369,6 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/next/node_modules/sharp": {
- "version": "0.33.5",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
- "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
- "hasInstallScript": true,
- "license": "Apache-2.0",
- "optional": true,
- "dependencies": {
- "color": "^4.2.3",
- "detect-libc": "^2.0.3",
- "semver": "^7.6.3"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.33.5",
- "@img/sharp-darwin-x64": "0.33.5",
- "@img/sharp-libvips-darwin-arm64": "1.0.4",
- "@img/sharp-libvips-darwin-x64": "1.0.4",
- "@img/sharp-libvips-linux-arm": "1.0.5",
- "@img/sharp-libvips-linux-arm64": "1.0.4",
- "@img/sharp-libvips-linux-s390x": "1.0.4",
- "@img/sharp-libvips-linux-x64": "1.0.4",
- "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
- "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
- "@img/sharp-linux-arm": "0.33.5",
- "@img/sharp-linux-arm64": "0.33.5",
- "@img/sharp-linux-s390x": "0.33.5",
- "@img/sharp-linux-x64": "0.33.5",
- "@img/sharp-linuxmusl-arm64": "0.33.5",
- "@img/sharp-linuxmusl-x64": "0.33.5",
- "@img/sharp-wasm32": "0.33.5",
- "@img/sharp-win32-ia32": "0.33.5",
- "@img/sharp-win32-x64": "0.33.5"
- }
- },
"node_modules/node-cron": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
@@ -13582,27 +12398,10 @@
}
}
},
- "node_modules/node-options": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/node-options/-/node-options-0.0.6.tgz",
- "integrity": "sha512-OrfY9+LgcLjoo2oyqxjP3gZLBuNDV1IblF69HGLdbE8JUJxSnl2kB561r41KOMc1GWLspjMSfa9L6+iW4fvYrw==",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
- "node_modules/node-uuid": {
- "version": "1.4.8",
- "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
- "integrity": "sha512-TkCET/3rr9mUuRp+CpO7qfgT++aAxfDRaalQhwPFzI9BY/2rCDn6OfpZOVggi1AXfTPpfkTrg5f5WQx5G1uLxA==",
- "deprecated": "Use uuid module instead",
- "bin": {
- "uuid": "bin/uuid"
- }
- },
"node_modules/nodemailer": {
- "version": "6.10.1",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
- "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.11.tgz",
+ "integrity": "sha512-gnXhNRE0FNhD7wPSCGhdNh46Hs6nm+uTyg+Kq0cZukNQiYdnCsoQjodNP9BQVG9XrcK/v6/MgpAPBUFyzh9pvw==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
@@ -13684,14 +12483,6 @@
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
"license": "MIT"
},
- "node_modules/oauth-sign": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.2.0.tgz",
- "integrity": "sha512-4DtiD64CwPJ5vZ636j/KtM7DxWbX1KlkqwbqbEAxI3BCpBrQdrKOv8vC/36U6gfm1CVapy6QmcVxPnXPPQApTA==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -13701,11 +12492,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/object-component": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
- "integrity": "sha512-S0sN3agnVh2SZNEIGc0N1X4Z5K0JeFbGBrnuZpsxuUh5XLF0BnvWkMjRXo/zGKLd/eghvNIKcx1pQkmUjXIyrA=="
- },
"node_modules/object-hash": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
@@ -13870,24 +12656,6 @@
"url": "https://github.com/sponsors/panva"
}
},
- "node_modules/optimist": {
- "version": "0.3.7",
- "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz",
- "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==",
- "license": "MIT/X11",
- "dependencies": {
- "wordwrap": "~0.0.2"
- }
- },
- "node_modules/optimist/node_modules/wordwrap": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
- "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -13906,14 +12674,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/options": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz",
- "integrity": "sha512-bOj3L1ypm++N+n7CEbbe473A414AB7z+amKYshRb//iuL3MpdDCLhPnw6aVTdKB9g5ZRVHIEp8eUln6L2NUStg==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/oracledb": {
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/oracledb/-/oracledb-6.9.0.tgz",
@@ -14022,39 +12782,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/parsejson": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.1.tgz",
- "integrity": "sha512-W9CRvTfYQY/kbRc5Q6YTWarb/QDxdEGbd6RCP8CLUQDJV89RVHoS2A0dZYNtAcq31fulGNN4ZhAhiQQazwlKJg==",
- "license": "MIT",
- "dependencies": {
- "better-assert": "~1.0.0"
- }
- },
- "node_modules/parseqs": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.2.tgz",
- "integrity": "sha512-vyyyfQGUFZnDhgrrdn+hh1JuOfvbXU5oRr6dijfkSIbaFuxGgTSCA/RNVcsADmo0k2NX6wERVTMKkXokjuObJA==",
- "license": "MIT",
- "dependencies": {
- "better-assert": "~1.0.0"
- }
- },
- "node_modules/parseuri": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.2.tgz",
- "integrity": "sha512-m0H+R0u5LXOx8sbxufnvgKrRLpkVpvtMf0AyWXYSqLwo2MWrVEgCIbgpaSVa398xl6wTLe0A7CGhiC4hBdEzHQ==",
- "license": "MIT",
- "dependencies": {
- "better-assert": "~1.0.0"
- }
- },
- "node_modules/parseurl": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.1.3.tgz",
- "integrity": "sha512-7y9IL/9x2suvr1uIvoAc3yv3f28hZ55g2OM+ybEtnZqV6Ykeg36sy1PCsTN9rQUZYzb9lTKLzzmJM11jaXSloA==",
- "license": "MIT"
- },
"node_modules/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
@@ -14145,11 +12872,6 @@
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
- "node_modules/path-to-regexp": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz",
- "integrity": "sha512-sd4vSOW+DCM6A5aRICI1CWaC7nufnzVpZfuh5T0VXshxxzFWuaFcvqKovAFLNGReOc+uZRptpcpPmn7CDvzLuA=="
- },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -14363,15 +13085,6 @@
"node": ">= 6"
}
},
- "node_modules/pkginfo": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.3.1.tgz",
- "integrity": "sha512-yO5feByMzAp96LtP58wvPKSbaKAi/1C4kV9XpTctr6EepnP6F33RBNOiVrdz9BrPA98U2BMFsTNHo44TWcbQ2A==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4.0"
- }
- },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@@ -14887,26 +13600,6 @@
"prosemirror-transform": "^1.1.0"
}
},
- "node_modules/proxy-addr": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.1.tgz",
- "integrity": "sha512-rIUGzBlSfkJMWWCgsd4N5wvVSNAcJZg//UwPZumDIbScHRUzuSOjBmIdyICiKkB9yArv+er9qC6RA/NL3AWc6A==",
- "license": "MIT",
- "dependencies": {
- "ipaddr.js": "0.1.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/proxy-addr/node_modules/ipaddr.js": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-0.1.2.tgz",
- "integrity": "sha512-MGrEjHz4Hk5UVpJXZQ2tHB+bp6xgdRKCAEWdrgFsoAmXCgKAPtj8LqMxgvlWEAj9aN+PpTcvE051uZU3K3kLSQ==",
- "engines": {
- "node": ">= 0.2.5"
- }
- },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -14958,14 +13651,6 @@
],
"license": "MIT"
},
- "node_modules/qs": {
- "version": "0.6.6",
- "resolved": "https://registry.npmjs.org/qs/-/qs-0.6.6.tgz",
- "integrity": "sha512-kN+yNdAf29Jgp+AYHUmC7X4QdJPR8czuMWLNLc0aRxkQ7tB3vJQEONKKT9ou/rW7EbqVec11srC9q9BiVbcnHA==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -14998,24 +13683,6 @@
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
"license": "MIT"
},
- "node_modules/range-parser": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.0.tgz",
- "integrity": "sha512-wOH5LIH2ZHo0P7/bwkR+aNbJ+kv3CHVX4B8qs9GqbtY29fi1bGPV5xczrutN20G+Z4XhRqRMTW3q0S4iyJJPfw=="
- },
- "node_modules/raw-body": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.3.0.tgz",
- "integrity": "sha512-iuI1bOSi9tEmVCrXq02ZysXatTrhAu+fSo7XOQHhMo4g87dSy9YB2W/9Udwhz0bPpFk4UcoLhjrHgpPbRD3ktA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "1",
- "iconv-lite": "0.4.4"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -15447,73 +14114,6 @@
"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",
- "integrity": "sha512-TfD4kMo40kwuOpO7GYfAZpb2wYdw7yvTIglPNgPPSmp2Fz6MKNvPLla40FQ/ypdhy6B2jRNz3VlCjPD6mnzsmA==",
- "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
- "engines": [
- "node >= 0.8.0"
- ],
- "dependencies": {
- "aws-sign": "~0.2.0",
- "cookie-jar": "~0.2.0",
- "forever-agent": "~0.2.0",
- "form-data": "~0.0.3",
- "hawk": "~0.10.2",
- "json-stringify-safe": "~3.0.0",
- "mime": "~1.2.7",
- "node-uuid": "~1.4.0",
- "oauth-sign": "~0.2.0",
- "qs": "~0.5.4",
- "tunnel-agent": "~0.2.0"
- }
- },
- "node_modules/request/node_modules/async": {
- "version": "0.2.10",
- "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
- "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
- },
- "node_modules/request/node_modules/combined-stream": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
- "integrity": "sha512-qfexlmLp9MyrkajQVyjEDb0Vj+KhRgR/rxLiVhaihlT+ZkX0lReqtH6Ack40CvMDERR4b5eFp3CreskpBs1Pig==",
- "dependencies": {
- "delayed-stream": "0.0.5"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/request/node_modules/delayed-stream": {
- "version": "0.0.5",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz",
- "integrity": "sha512-v+7uBd1pqe5YtgPacIIbZ8HuHeLFVNe4mUEyFDXL6KiqzEykjbw+5mXZXpGFgNVasdL4jWKgaKIXrEHiynN1LA==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/request/node_modules/form-data": {
- "version": "0.0.10",
- "resolved": "https://registry.npmjs.org/form-data/-/form-data-0.0.10.tgz",
- "integrity": "sha512-Z9/PpT/agxXi80nMpOH6GFD7XOr6mwk5aWMxDt/KMY+Nm7e4FnRMjddM4/mLPJhpmp6alY1F/1JQpRE6z07xng==",
- "dependencies": {
- "async": "~0.2.7",
- "combined-stream": "~0.0.4",
- "mime": "~1.2.2"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/request/node_modules/qs": {
- "version": "0.5.6",
- "resolved": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz",
- "integrity": "sha512-KbOrQrP5Ye+0gmq+hwxoJwAFRwExACWqwxj1IDFFgqOw9Poxy3wwSbafd9ZqP6T6ykMfnxM573kt/a4i9ybatQ==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
@@ -15753,61 +14353,6 @@
"node": ">=10"
}
},
- "node_modules/send": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.6.0.tgz",
- "integrity": "sha512-A3EwHmDwcPcmLxIRNjr2YbXiYWq6M9JyUq4303pLKVFs4m5oeME0a9Cpcu9N22fED5XVepldjPYGo9eJifb7Yg==",
- "license": "MIT",
- "dependencies": {
- "debug": "1.0.3",
- "depd": "0.3.0",
- "escape-html": "1.0.1",
- "finished": "1.2.2",
- "fresh": "0.2.2",
- "mime": "1.2.11",
- "ms": "0.6.2",
- "range-parser": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/node_modules/debug": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-1.0.3.tgz",
- "integrity": "sha512-MltK7Ykj/udtD728gD/RrONStwVnDpBNIP1h+CBcnwnJdHqHxfWHI1E8XLootUl7NOPAYTCCXlb8/Qmy7WyB1w==",
- "dependencies": {
- "ms": "0.6.2"
- }
- },
- "node_modules/send/node_modules/depd": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/depd/-/depd-0.3.0.tgz",
- "integrity": "sha512-Uyx3FgdvEYlpA3W4lf37Ide++2qOsjLlJ7dap0tbM63j/BxTCcxmyIOO6PXbKbOuNSko+fsDHzzx1DUeo1+3fA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/send/node_modules/ms": {
- "version": "0.6.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-0.6.2.tgz",
- "integrity": "sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A=="
- },
- "node_modules/serve-static": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.3.2.tgz",
- "integrity": "sha512-KwjCeYUx7IM1neg8/P0+O1DZsl76XcOSuV0ZxrI0r60vwGlcjMjKOYCK/OFLJy/a2CFuIyAa/x0PuQ0yuG+IgQ==",
- "license": "MIT",
- "dependencies": {
- "escape-html": "1.0.1",
- "parseurl": "~1.1.3",
- "send": "0.6.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -16042,27 +14587,6 @@
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
- "node_modules/sntp": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/sntp/-/sntp-0.1.2.tgz",
- "integrity": "sha512-6fsOpJYQAQcO/UeW7T9mJwEenJymdU77o+gNiompGAammlSa+C49Oyt79ta/kgVbT13l4JAuKlo8FNvUnVjvEQ==",
- "deprecated": "This module moved to @hapi/sntp. Please make sure to switch over as this distribution is no longer supported and may contain bugs and critical security issues.",
- "dependencies": {
- "hoek": "0.4.x"
- },
- "engines": {
- "node": ">=0.8.0"
- }
- },
- "node_modules/sntp/node_modules/hoek": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/hoek/-/hoek-0.4.2.tgz",
- "integrity": "sha512-Yj/N2TCrS0d8jvZgUpq9sDNt8/ABwTxPJW4+8QT0KXCMxOtRfUCUTEZEYyvMSgfDT3MGvwgO+NHfWPobagAIug==",
- "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).",
- "engines": {
- "node": ">=0.8.0"
- }
- },
"node_modules/soap": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/soap/-/soap-1.2.1.tgz",
@@ -16084,112 +14608,6 @@
"node": ">=14.17.0"
}
},
- "node_modules/socket.io": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-1.0.6.tgz",
- "integrity": "sha512-1x7TkMh8aKfLoXuXe5rXnDnv3xfcOFrDM6hR9z15dpZ83tTxt2NUxnpuGL2zMIAJQ4DitKiadEBvBVju5cxcHw==",
- "dependencies": {
- "debug": "0.7.4",
- "engine.io": "1.3.1",
- "has-binary-data": "0.1.1",
- "socket.io-adapter": "0.2.0",
- "socket.io-client": "1.0.6",
- "socket.io-parser": "2.2.0"
- }
- },
- "node_modules/socket.io-adapter": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-0.2.0.tgz",
- "integrity": "sha512-3PlX+MOlpHiY+ZTbKhpE4i+M4u8hFUlVyqFP4K/mH+t+D9bMKATFqUUY3zWQMEo2g/1ckosURXviQw6M8R/y8A==",
- "dependencies": {
- "debug": "0.7.4",
- "socket.io-parser": "2.1.2"
- }
- },
- "node_modules/socket.io-adapter/node_modules/debug": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
- "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/socket.io-adapter/node_modules/isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
- "license": "MIT"
- },
- "node_modules/socket.io-adapter/node_modules/socket.io-parser": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.1.2.tgz",
- "integrity": "sha512-eVkt8prgw20H+4P8Iw6tis/w7leiN5EW/93Vq+KL8w+yNJu+QNgaej2Cgt8FhVCVuN3AHyLU50vXvM8cpUR1JQ==",
- "dependencies": {
- "debug": "0.7.4",
- "emitter": "http://github.com/component/emitter/archive/1.0.1.tar.gz",
- "isarray": "0.0.1",
- "json3": "3.2.6"
- }
- },
- "node_modules/socket.io-client": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-1.0.6.tgz",
- "integrity": "sha512-itdtz6fQBTFIDBP4+hJox0OlT+SbCVdENjPgjMup3ehu7OsiG6t0FYBXCx+k/upt9lbeyp9BmUNNi5EfnGa5Vw==",
- "license": "MIT",
- "dependencies": {
- "component-bind": "1.0.0",
- "component-emitter": "1.1.2",
- "debug": "0.7.4",
- "engine.io-client": "1.3.1",
- "has-binary-data": "0.1.1",
- "indexof": "0.0.1",
- "object-component": "0.0.3",
- "parseuri": "0.0.2",
- "socket.io-parser": "2.2.0",
- "to-array": "0.1.3"
- }
- },
- "node_modules/socket.io-client/node_modules/debug": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
- "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/socket.io-parser": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-2.2.0.tgz",
- "integrity": "sha512-uW3UiLVibAyleKq8r/yZe1oPO51olhY18T6HtnN0iI6RLqJfYC0YiyAFlsPw1+8I0Z1qFd8jFLTRZo2vr6ISxA==",
- "dependencies": {
- "debug": "0.7.4",
- "emitter": "http://github.com/component/emitter/archive/1.0.1.tar.gz",
- "isarray": "0.0.1",
- "json3": "3.2.6"
- }
- },
- "node_modules/socket.io-parser/node_modules/debug": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
- "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/socket.io-parser/node_modules/isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
- "license": "MIT"
- },
- "node_modules/socket.io/node_modules/debug": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz",
- "integrity": "sha512-EohAb3+DSHSGx8carOSKJe8G0ayV5/i609OD0J2orCkuyae7SyZSz2aoLmQF2s0Pj5gITDebwPH7GFBlqOUQ1Q==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
@@ -16264,15 +14682,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/stack-trace": {
- "version": "0.0.10",
- "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
- "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -16287,14 +14696,6 @@
"node": ">= 0.4"
}
},
- "node_modules/streamsearch": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
- "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
- "engines": {
- "node": ">=10.0.0"
- }
- },
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
@@ -16604,9 +15005,9 @@
}
},
"node_modules/sucrase/node_modules/glob": {
- "version": "10.4.5",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
- "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -16897,14 +15298,6 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
- "node_modules/tinycolor": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/tinycolor/-/tinycolor-0.0.1.tgz",
- "integrity": "sha512-+CorETse1kl98xg0WAzii8DTT4ABF4R3nquhrkIbVGcw1T8JYs5Gfx9xEfGINPUZGDj9C4BmOtuKeaTtuuRolg==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
@@ -16988,11 +15381,6 @@
"tmp": "^0.2.0"
}
},
- "node_modules/to-array": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.3.tgz",
- "integrity": "sha512-JQk/QMS4oHyU2VufVeyjN25dcnZnr1PV1pa1oKSj7l5tVO9WrU62og3fYzB3mrgJZZgBxdrrA/v6iZzMDuyFYw=="
- },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -17005,32 +15393,6 @@
"node": ">=8.0"
}
},
- "node_modules/tomahawk": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/tomahawk/-/tomahawk-0.1.6.tgz",
- "integrity": "sha512-HFLoewTx2gHD0o2t0tR+EIcDXhqdtakfZCDiYsGjOO93nYQ1i7nbhj3UL7iQdtoBbPAcEbrxeJ0KlfPOvhxFyg==",
- "dependencies": {
- "body-parser": "1.5.0",
- "connect": "3.0.2",
- "errorhandler": "1.1.1",
- "express": "4.6.1",
- "morgan": "1.2.0",
- "node-options": "0.0.6",
- "socket.io": "1.0.6",
- "winston": "0.7.3"
- },
- "bin": {
- "tomahawk": "bin/tomahawk"
- },
- "engines": {
- "node": ">= 0.8.0 < 0.11.0"
- }
- },
- "node_modules/tomahawk-plugin-kv-memory-store": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tomahawk-plugin-kv-memory-store/-/tomahawk-plugin-kv-memory-store-0.0.3.tgz",
- "integrity": "sha512-opt82r6s+775jmrREiWruMVTQaGQYgPd6/zYTDRwwHhDGSqpFaZZgCSnI/BAIs8nC88puTK4PyodkSRpUDp/2Q=="
- },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -17588,14 +15950,6 @@
"@esbuild/win32-x64": "0.25.8"
}
},
- "node_modules/tunnel-agent": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.2.0.tgz",
- "integrity": "sha512-PXy4q1PH88BK0pcGOEMXFAslyBuRWz1wxLfPXTlYFd41eyUgjOALaVGbWJN1ymjbnBzjWunVSKmrrMMh8oLaZA==",
- "engines": {
- "node": "*"
- }
- },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -17609,28 +15963,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/type-is": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.3.2.tgz",
- "integrity": "sha512-sdIhnvhWEyIP2DKjj1o9tL31m8vFxDfLPD56KXz2absqY5AF2QYkJC7Wrw2fkzsZA9mv+PCtgyB7EqYOgR+r3Q==",
- "license": "MIT",
- "dependencies": {
- "media-typer": "0.2.0",
- "mime-types": "~1.0.1"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "node_modules/type-is/node_modules/mime-types": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-1.0.2.tgz",
- "integrity": "sha512-echfutj/t5SoTL4WZpqjA1DCud1XO0WQF3/GJ48YBmc4ZMhCK77QA6Z/w6VTQERLKuJ4drze3kw2TUT8xZXVNw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -17993,11 +16325,6 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
- "node_modules/utf8": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/utf8/-/utf8-2.0.0.tgz",
- "integrity": "sha512-jWXHr+bQ8RsWazLzVY3V7XACPTbBHYSg/VoDVok+DBQk5ULm0AuBCNb9tGmjq2H+znnkBFwjhzzCbn9G3xlYcA=="
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -18033,15 +16360,6 @@
"devOptional": true,
"license": "MIT"
},
- "node_modules/vary": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/vary/-/vary-0.1.0.tgz",
- "integrity": "sha512-tyyeG46NQdwyVP/RsWLSrT78ouwEuvwk9gK8vQK4jdXmqoXtTXW+vsCfNcnqRhigF8olV34QVZarmAi6wBV2Mw==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
@@ -18055,20 +16373,6 @@
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
- "node_modules/verror": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
- "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
- "license": "MIT",
- "dependencies": {
- "assert-plus": "^1.0.0",
- "core-util-is": "1.0.2",
- "extsprintf": "^1.2.0"
- },
- "engines": {
- "node": ">=0.6.0"
- }
- },
"node_modules/victory-vendor": {
"version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
@@ -18244,28 +16548,6 @@
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
- "node_modules/winston": {
- "version": "0.7.3",
- "resolved": "https://registry.npmjs.org/winston/-/winston-0.7.3.tgz",
- "integrity": "sha512-iVTT8tf9YnTyfZX+aEUj2fl6WBRet7za6vdjMeyF8SA80Vii2rreM5XH+5qmpBV9uJGj8jz8BozvTDcroVq/eA==",
- "dependencies": {
- "async": "0.2.x",
- "colors": "0.6.x",
- "cycle": "1.0.x",
- "eyes": "0.1.x",
- "pkginfo": "0.3.x",
- "request": "2.16.x",
- "stack-trace": "0.0.x"
- },
- "engines": {
- "node": ">= 0.6.0"
- }
- },
- "node_modules/winston/node_modules/async": {
- "version": "0.2.10",
- "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
- "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
- },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -18379,32 +16661,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
- "node_modules/ws": {
- "version": "0.4.31",
- "resolved": "https://registry.npmjs.org/ws/-/ws-0.4.31.tgz",
- "integrity": "sha512-mWiVQ9qZGPXvLxQ4xGy58Ix5Bw0L99SB+hDT8L59bty4fbnQczaGl4YEWR7AzLQGbvPn/30r9/o41dPiSuUmYw==",
- "hasInstallScript": true,
- "dependencies": {
- "commander": "~0.6.1",
- "nan": "~0.3.0",
- "options": ">=0.0.5",
- "tinycolor": "0.x"
- },
- "bin": {
- "wscat": "bin/wscat"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/ws/node_modules/commander": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz",
- "integrity": "sha512-0fLycpl1UMTGX257hRsu/arL/cUbcvQM4zMKwvLvzXtfdezIV4yotPS2dYtknF+NmEfWSoCEF6+hj9XLm/6hEw==",
- "engines": {
- "node": ">= 0.4.x"
- }
- },
"node_modules/xhr2": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz",
@@ -18518,14 +16774,6 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
},
- "node_modules/xmlhttprequest": {
- "version": "1.5.0",
- "resolved": "https://github.com/LearnBoost/node-XMLHttpRequest/archive/0f36d0b5ebc03d85f860d42a64ae9791e1daa433.tar.gz",
- "integrity": "sha512-TVSZwoeUQ7OKhb8jnQdSxGFz+lm4MGWmhG0deeYg85VQT74x5LcSrKeXHE0ZIzEycgqQ5mF8r8e1AykA7TpNAQ==",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/xpath": {
"version": "0.0.34",
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz",
diff --git a/package.json b/package.json
index 2f435dbe..75dc54db 100644
--- a/package.json
+++ b/package.json
@@ -10,206 +10,197 @@
"db:seed_2": "tsx db/seeds_2/seed.ts"
},
"dependencies": {
- "@codemirror/commands": "^6.8.1",
- "@codemirror/lang-html": "^6.4.9",
- "@codemirror/language": "^6.11.2",
+ "@codemirror/commands": "^6.10.0",
+ "@codemirror/lang-html": "^6.4.11",
+ "@codemirror/language": "^6.11.3",
"@codemirror/search": "^6.5.11",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
- "@codemirror/view": "^6.38.1",
+ "@codemirror/view": "^6.38.8",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.14.0",
- "@emotion/styled": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
"@hello-pangea/dnd": "^18.0.1",
- "@hookform/resolvers": "^3.9.1",
- "@mantine/hooks": "^8.0.2",
+ "@hookform/resolvers": "^3.10.0",
+ "@mantine/hooks": "^8.3.9",
"@mescius/spread-sheets": "^18.1.3",
- "@mescius/spread-sheets-barcode": "^18.1.3",
- "@mescius/spread-sheets-charts": "^18.1.3",
- "@mescius/spread-sheets-datacharts-addon": "^18.1.3",
- "@mescius/spread-sheets-designer": "^18.2.0",
- "@mescius/spread-sheets-designer-react": "^18.1.3",
- "@mescius/spread-sheets-designer-resources-en": "^18.2.0",
- "@mescius/spread-sheets-designer-resources-ko": "^18.2.0",
- "@mescius/spread-sheets-formula-panel": "^18.1.3",
- "@mescius/spread-sheets-ganttsheet": "^18.1.3",
- "@mescius/spread-sheets-io": "^18.1.3",
- "@mescius/spread-sheets-languagepackages": "^18.1.3",
- "@mescius/spread-sheets-pdf": "^18.1.3",
- "@mescius/spread-sheets-pivot-addon": "^18.1.3",
+ "@mescius/spread-sheets-barcode": "^18.2.5",
+ "@mescius/spread-sheets-charts": "^18.2.5",
+ "@mescius/spread-sheets-datacharts-addon": "^18.2.5",
+ "@mescius/spread-sheets-designer": "^18.2.5",
+ "@mescius/spread-sheets-designer-react": "^18.2.5",
+ "@mescius/spread-sheets-designer-resources-en": "^18.2.5",
+ "@mescius/spread-sheets-designer-resources-ko": "^18.2.5",
+ "@mescius/spread-sheets-formula-panel": "^18.2.5",
+ "@mescius/spread-sheets-ganttsheet": "^18.2.5",
+ "@mescius/spread-sheets-io": "^18.2.5",
+ "@mescius/spread-sheets-languagepackages": "^18.2.5",
+ "@mescius/spread-sheets-pdf": "^18.2.5",
+ "@mescius/spread-sheets-pivot-addon": "^18.2.5",
"@mescius/spread-sheets-print": "^18.1.3",
- "@mescius/spread-sheets-react": "^18.1.3",
- "@mescius/spread-sheets-reportsheet-addon": "^18.1.3",
- "@mescius/spread-sheets-resources-ko": "^18.1.3",
+ "@mescius/spread-sheets-react": "^18.2.5",
+ "@mescius/spread-sheets-reportsheet-addon": "^18.2.5",
+ "@mescius/spread-sheets-resources-ko": "^18.2.5",
"@mescius/spread-sheets-shapes": "^18.1.3",
- "@mescius/spread-sheets-slicers": "^18.1.3",
+ "@mescius/spread-sheets-slicers": "^18.2.5",
"@mescius/spread-sheets-tablesheet": "^18.1.3",
- "@mui/material": "^6.2.1",
- "@mui/x-data-grid-premium": "^7.23.3",
- "@mui/x-tree-view": "^7.23.6",
- "@node-saml/node-saml": "^5.0.1",
- "@pdftron/pdfnet-node": "^11.5.0",
- "@pdftron/webviewer": "^11.3.0",
+ "@mui/material": "^6.5.0",
+ "@mui/x-data-grid-premium": "^7.29.12",
+ "@mui/x-tree-view": "^7.29.10",
+ "@node-saml/node-saml": "^5.1.0",
+ "@pdftron/pdfnet-node": "^11.9.0",
+ "@pdftron/webviewer": "^11.9.0",
"@radix-ui/primitive": "^1.1.1",
- "@radix-ui/react-accordion": "^1.2.2",
- "@radix-ui/react-alert-dialog": "^1.1.4",
- "@radix-ui/react-aspect-ratio": "^1.1.1",
- "@radix-ui/react-avatar": "^1.1.2",
- "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-accordion": "^1.2.12",
+ "@radix-ui/react-alert-dialog": "^1.1.15",
+ "@radix-ui/react-aspect-ratio": "^1.1.8",
+ "@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.2",
- "@radix-ui/react-context-menu": "^2.2.4",
+ "@radix-ui/react-context-menu": "^2.2.16",
"@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-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-hover-card": "^1.1.15",
"@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",
- "@radix-ui/react-popover": "^1.1.4",
- "@radix-ui/react-portal": "^1.1.3",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-menubar": "^1.1.16",
+ "@radix-ui/react-navigation-menu": "^1.2.14",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-portal": "^1.1.10",
"@radix-ui/react-primitive": "^2.0.1",
- "@radix-ui/react-progress": "^1.1.1",
- "@radix-ui/react-radio-group": "^1.2.2",
- "@radix-ui/react-scroll-area": "^1.2.2",
- "@radix-ui/react-select": "^2.1.4",
- "@radix-ui/react-separator": "^1.1.1",
- "@radix-ui/react-slider": "^1.2.2",
+ "@radix-ui/react-progress": "^1.1.8",
+ "@radix-ui/react-radio-group": "^1.3.8",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.1.1",
- "@radix-ui/react-switch": "^1.1.2",
- "@radix-ui/react-tabs": "^1.1.2",
- "@radix-ui/react-toast": "^1.2.4",
+ "@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.1",
- "@radix-ui/react-toggle-group": "^1.1.1",
- "@radix-ui/react-tooltip": "^1.1.6",
+ "@radix-ui/react-toggle-group": "^1.1.11",
+ "@radix-ui/react-tooltip": "^1.2.8",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/match-sorter-utils": "^8.19.4",
- "@tanstack/react-table": "^8.20.6",
+ "@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
- "@tiptap/extension-blockquote": "^2.23.1",
- "@tiptap/extension-bullet-list": "^2.23.1",
- "@tiptap/extension-highlight": "^2.23.1",
- "@tiptap/extension-image": "^2.23.1",
- "@tiptap/extension-link": "^2.23.1",
- "@tiptap/extension-list-item": "^2.23.1",
- "@tiptap/extension-ordered-list": "^2.23.1",
- "@tiptap/extension-placeholder": "^2.23.1",
- "@tiptap/extension-subscript": "^2.23.1",
- "@tiptap/extension-superscript": "^2.23.1",
- "@tiptap/extension-table": "^2.23.1",
- "@tiptap/extension-table-cell": "^2.23.1",
- "@tiptap/extension-table-header": "^2.23.1",
- "@tiptap/extension-table-row": "^2.23.1",
- "@tiptap/extension-task-item": "^2.23.1",
- "@tiptap/extension-task-list": "^2.23.1",
- "@tiptap/extension-text-align": "^2.23.1",
- "@tiptap/extension-underline": "^2.23.1",
- "@tiptap/react": "^2.23.1",
- "@tiptap/starter-kit": "^2.23.1",
- "@toast-ui/editor": "^3.2.2",
- "@toast-ui/react-editor": "^3.2.3",
- "@types/docusign-esign": "^5.19.8",
- "@types/formidable": "^3.4.5",
+ "@tiptap/extension-blockquote": "^2.27.1",
+ "@tiptap/extension-bullet-list": "^2.27.1",
+ "@tiptap/extension-highlight": "^2.27.1",
+ "@tiptap/extension-image": "^2.27.1",
+ "@tiptap/extension-link": "^2.27.1",
+ "@tiptap/extension-list-item": "^2.27.1",
+ "@tiptap/extension-ordered-list": "^2.27.1",
+ "@tiptap/extension-placeholder": "^2.27.1",
+ "@tiptap/extension-subscript": "^2.27.1",
+ "@tiptap/extension-superscript": "^2.27.1",
+ "@tiptap/extension-table": "^2.27.1",
+ "@tiptap/extension-table-cell": "^2.27.1",
+ "@tiptap/extension-table-header": "^2.27.1",
+ "@tiptap/extension-table-row": "^2.27.1",
+ "@tiptap/extension-task-item": "^2.27.1",
+ "@tiptap/extension-task-list": "^2.27.1",
+ "@tiptap/extension-text-align": "^2.27.1",
+ "@tiptap/extension-underline": "^2.27.1",
+ "@tiptap/react": "^2.27.1",
+ "@tiptap/starter-kit": "^2.27.1",
+ "@types/docusign-esign": "^5.19.9",
+ "@types/formidable": "^3.4.6",
"accept-language": "^3.0.20",
"archiver": "^7.0.1",
- "bcryptjs": "^3.0.2",
+ "bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
- "cmdk": "^1.0.4",
+ "cmdk": "^1.1.1",
"codemirror": "^6.0.2",
"crypto-js": "^4.2.0",
"date-fns": "^3.6.0",
- "dns": "^0.2.2",
- "docusign-esign": "^8.0.1",
+ "docusign-esign": "^8.5.0",
"docx": "^9.5.1",
- "drizzle-orm": "^0.38.2",
+ "drizzle-orm": "^0.38.4",
"drizzle-seed": "^0.1.3",
- "drizzle-zod": "^0.8.2",
- "embla-carousel-react": "^8.5.1",
+ "drizzle-zod": "^0.8.3",
+ "embla-carousel-react": "^8.6.0",
"exceljs": "^4.4.0",
"fast-deep-equal": "^3.1.3",
- "fast-xml-parser": "^5.2.2",
+ "fast-xml-parser": "^5.3.2",
"file-saver": "^2.0.5",
- "formidable": "^3.5.2",
+ "formidable": "^3.5.4",
"handlebars": "^4.7.8",
- "i18n-iso-countries": "^7.13.0",
- "i18next": "^24.1.2",
- "i18next-browser-languagedetector": "^8.0.2",
- "i18next-http-backend": "^3.0.1",
+ "i18n-iso-countries": "^7.14.0",
+ "i18next": "^24.2.3",
+ "i18next-browser-languagedetector": "^8.2.0",
+ "i18next-http-backend": "^3.0.2",
"i18next-resources-to-backend": "^1.2.1",
- "input-otp": "^1.4.1",
- "jotai": "^2.10.4",
- "jsonwebtoken": "^9.0.2",
+ "input-otp": "^1.4.2",
+ "jotai": "^2.15.2",
+ "jsonwebtoken": "^9.0.3",
"jszip": "^3.10.1",
"knex": "^3.1.0",
- "libphonenumber-js": "^1.12.10",
+ "libphonenumber-js": "^1.12.31",
"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",
- "next-i18next": "^15.4.1",
- "next-themes": "^0.4.4",
- "node-cron": "^4.1.1",
- "nodemailer": "^6.9.16",
- "nuqs": "^2.2.3",
- "oracledb": "^6.8.0",
- "pg": "^8.13.1",
- "pino": "^9.5.0",
- "pino-pretty": "^13.0.0",
+ "next": "^15.5.7",
+ "next-auth": "^4.24.13",
+ "next-i18n-router": "^5.5.5",
+ "next-i18next": "^15.4.3",
+ "next-themes": "^0.4.6",
+ "node-cron": "^4.2.1",
+ "nodemailer": "^7.0.11",
+ "nuqs": "^2.8.3",
+ "oracledb": "^6.10.0",
+ "pg": "^8.16.3",
+ "pino": "^9.14.0",
+ "pino-pretty": "^13.1.3",
"pretty-bytes": "^6.1.1",
"react": "^18.3.1",
"react-cookie": "^7.2.2",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
- "react-dropzone": "^14.3.5",
- "react-hook-form": "^7.54.1",
- "react-i18next": "^15.2.0",
- "react-resizable-panels": "^2.1.7",
- "recharts": "^2.15.0",
- "sharp": "^0.34.2",
- "soap": "^1.1.12",
- "sonner": "^1.7.1",
- "swr": "^2.3.3",
- "tailwind-merge": "^2.5.5",
+ "react-dropzone": "^14.3.8",
+ "react-hook-form": "^7.68.0",
+ "react-i18next": "^15.7.4",
+ "react-resizable-panels": "^2.1.9",
+ "recharts": "^2.15.4",
+ "sharp": "^0.34.5",
+ "soap": "^1.6.1",
+ "sonner": "^1.7.4",
+ "swr": "^2.3.7",
+ "tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
- "tiptap-extension-resize-image": "^1.3.0",
+ "tiptap-extension-resize-image": "^1.3.2",
"tmp-promise": "^3.0.3",
- "ua-parser-js": "^2.0.4",
- "uuid": "^11.0.5",
+ "ua-parser-js": "^2.0.6",
+ "uuid": "^11.1.0",
"vaul": "^1.1.2",
"xml2js": "^0.6.2",
- "zod": "^3.24.1"
+ "zod": "^3.25.76"
},
"devDependencies": {
- "@eslint/eslintrc": "^3",
- "@faker-js/faker": "^9.3.0",
+ "@eslint/eslintrc": "^3.3.3",
+ "@faker-js/faker": "^9.9.0",
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "^2.0.7",
- "@types/jsonwebtoken": "^9.0.7",
- "@types/node": "^20",
- "@types/nodemailer": "^6.4.17",
- "@types/pg": "^8.11.10",
- "@types/react": "^19",
- "@types/react-dom": "^19",
+ "@types/jsonwebtoken": "^9.0.10",
+ "@types/node": "^20.19.25",
+ "@types/nodemailer": "^6.4.21",
+ "@types/pg": "^8.15.6",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
"@types/sharp": "^0.31.1",
"@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^10.0.0",
- "drizzle-kit": "^0.30.1",
- "eslint": "^9",
+ "drizzle-kit": "^0.30.6",
+ "eslint": "^9.39.1",
"eslint-config-next": "15.1.0",
- "postcss": "^8",
- "tailwindcss": "^3.4.1",
+ "postcss": "^8.5.6",
+ "tailwindcss": "^3.4.18",
"ts-node": "^10.9.2",
- "tsx": "^4.19.2",
- "typescript": "^5.7.2"
- },
- "overrides": {
- "rimraf": "3.0.2",
- "@toast-ui/react-editor": {
- "react": "^18.3.1"
- }
+ "tsx": "^4.21.0",
+ "typescript": "^5.9.3"
}
}
diff --git a/types/table.d.ts b/types/table.d.ts
index 9fc96687..266470e8 100644
--- a/types/table.d.ts
+++ b/types/table.d.ts
@@ -74,5 +74,13 @@ declare module '@tanstack/react-table' {
paddingFactor?: number
minWidth?: number
maxWidth?: number
+
+ // Server-side feature flags
+ /** 서버에서 GROUP BY 가능 여부 (DB 컬럼에 직접 매핑되어야 함) */
+ serverGroupable?: boolean
+ /** 서버에서 정렬 가능 여부 */
+ serverSortable?: boolean
+ /** 서버에서 필터 가능 여부 */
+ serverFilterable?: boolean
}
}