From 9cabe879404f1ec05dbf4e65d55162b5573aeced Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Fri, 28 Nov 2025 20:30:23 +0900
Subject: (김준회) dynamic table - init
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/client-table/client-virtual-table.tsx | 362 +++++++++++++++++++++++
 1 file changed, 362 insertions(+)
 create mode 100644 components/client-table/client-virtual-table.tsx

(limited to 'components/client-table/client-virtual-table.tsx')

diff --git a/components/client-table/client-virtual-table.tsx b/components/client-table/client-virtual-table.tsx
new file mode 100644
index 00000000..4825741f
--- /dev/null
+++ b/components/client-table/client-virtual-table.tsx
@@ -0,0 +1,362 @@
+"use client"
+
+import * as React from "react"
+import { rankItem } from "@tanstack/match-sorter-utils"
+import {
+  useReactTable,
+  getCoreRowModel,
+  getSortedRowModel,
+  getFilteredRowModel,
+  getPaginationRowModel,
+  ColumnDef,
+  SortingState,
+  ColumnFiltersState,
+  flexRender,
+  PaginationState,
+  OnChangeFn,
+  ColumnOrderState,
+  VisibilityState,
+  ColumnPinningState,
+  FilterFn,
+  Table,
+  RowSelectionState,
+  Row,
+} 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 { ClientTableToolbar } from "./client-table-toolbar"
+import { exportToExcel } from "./export-utils"
+import { ClientDataTablePagination } from "@/components/client-data-table/data-table-pagination"
+import { ClientTableColumnHeader } from "./client-table-column-header"
+import { ClientTableViewOptions } from "./client-table-view-options"
+
+export interface ClientVirtualTableProps<TData, TValue> {
+  data: TData[]
+  columns: ColumnDef<TData, TValue>[]
+  height?: string | number
+  className?: string
+  actions?: React.ReactNode
+  enableExport?: boolean
+  onExport?: (data: TData[]) => void
+  
+  // Pagination Props
+  enablePagination?: boolean
+  manualPagination?: boolean
+  pageCount?: number
+  rowCount?: number
+  pagination?: PaginationState
+  onPaginationChange?: OnChangeFn<PaginationState>
+
+  // Style Props
+  getRowClassName?: (originalRow: TData, index: number) => string
+
+  // Table Meta
+  meta?: any
+  
+  // Row ID
+  getRowId?: (originalRow: TData, index: number, parent?: any) => string
+
+  // Selection Props
+  enableRowSelection?: boolean | ((row: Row<TData>) => boolean)
+  enableMultiRowSelection?: boolean | ((row: Row<TData>) => boolean)
+  rowSelection?: RowSelectionState
+  onRowSelectionChange?: OnChangeFn<RowSelectionState>
+}
+
+function ClientVirtualTableInner<TData, TValue>(
+  {
+    data,
+    columns,
+    height = "500px", // Default height
+    className,
+    actions,
+    enableExport = true,
+    onExport,
+    
+    // Pagination defaults
+    enablePagination = false,
+    manualPagination = false,
+    pageCount,
+    rowCount,
+    pagination: controlledPagination,
+    onPaginationChange,
+
+    // Style defaults
+    getRowClassName,
+    
+    // Meta & RowID
+    meta,
+    getRowId,
+
+    // Selection defaults
+    enableRowSelection,
+    enableMultiRowSelection,
+    rowSelection: controlledRowSelection,
+    onRowSelectionChange,
+  }: ClientVirtualTableProps<TData, TValue>,
+  ref: React.Ref<Table<TData>>
+) {
+  // State
+  const [sorting, setSorting] = React.useState<SortingState>([])
+  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
+  const [globalFilter, setGlobalFilter] = React.useState("")
+  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
+  const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({ left: [], right: [] })
+  const [columnOrder, setColumnOrder] = React.useState<ColumnOrderState>(
+    () => columns.map((c) => c.id || (c as any).accessorKey) as string[]
+  )
+  
+  // Internal Pagination State
+  const [internalPagination, setInternalPagination] = React.useState<PaginationState>({
+    pageIndex: 0,
+    pageSize: 50,
+  })
+
+  // Internal Row Selection State
+  const [internalRowSelection, setInternalRowSelection] = React.useState<RowSelectionState>({})
+
+  // Fuzzy Filter
+  const fuzzyFilter: FilterFn<TData> = (row, columnId, value, addMeta) => {
+    const itemRank = rankItem(row.getValue(columnId), value)
+    addMeta({ itemRank })
+    return itemRank.passed
+  }
+
+  // Combine controlled and uncontrolled states
+  const pagination = controlledPagination ?? internalPagination
+  const setPagination = onPaginationChange ?? setInternalPagination
+  
+  const rowSelection = controlledRowSelection ?? internalRowSelection
+  const setRowSelection = onRowSelectionChange ?? setInternalRowSelection
+
+  // Table Instance
+  const table = useReactTable({
+    data,
+    columns,
+    state: {
+      sorting,
+      columnFilters,
+      globalFilter,
+      pagination,
+      columnVisibility,
+      columnPinning,
+      columnOrder,
+      rowSelection,
+    },
+    manualPagination,
+    pageCount: manualPagination ? pageCount : undefined,
+    rowCount: manualPagination ? rowCount : undefined,
+    onSortingChange: setSorting,
+    onColumnFiltersChange: setColumnFilters,
+    onGlobalFilterChange: setGlobalFilter,
+    onPaginationChange: setPagination,
+    onColumnVisibilityChange: setColumnVisibility,
+    onColumnPinningChange: setColumnPinning,
+    onColumnOrderChange: setColumnOrder,
+    onRowSelectionChange: setRowSelection,
+    enableRowSelection,
+    enableMultiRowSelection,
+    getCoreRowModel: getCoreRowModel(),
+    getSortedRowModel: getSortedRowModel(),
+    getFilteredRowModel: getFilteredRowModel(),
+    getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
+    columnResizeMode: "onChange",
+    filterFns: {
+      fuzzy: fuzzyFilter,
+    },
+    globalFilterFn: fuzzyFilter,
+    meta,
+    getRowId,
+  })
+
+  // Expose table instance via ref
+  React.useImperativeHandle(ref, () => table, [table])
+
+  // DnD Sensors
+  const sensors = useSensors(
+    useSensor(PointerSensor, {
+      activationConstraint: {
+        distance: 8, // 8px movement required to start drag
+      },
+    }),
+    useSensor(KeyboardSensor)
+  )
+
+  // Handle Drag End
+  const handleDragEnd = (event: DragEndEvent) => {
+    const { active, over } = event
+    if (active && over && active.id !== over.id) {
+      setColumnOrder((items) => {
+        const oldIndex = items.indexOf(active.id as string)
+        const newIndex = items.indexOf(over.id as string)
+        return arrayMove(items, oldIndex, newIndex)
+      })
+    }
+  }
+
+  // Virtualization
+  const tableContainerRef = React.useRef<HTMLDivElement>(null)
+  const { rows } = table.getRowModel()
+
+  const rowVirtualizer = useVirtualizer({
+    count: rows.length,
+    getScrollElement: () => tableContainerRef.current,
+    estimateSize: () => 40, // Estimated row height
+    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 Handler
+  const handleExport = async () => {
+    if (onExport) {
+      onExport(data)
+      return
+    }
+    const currentData = table.getFilteredRowModel().rows.map((row) => row.original)
+    await exportToExcel(currentData, columns, `export-${new Date().toISOString().slice(0,10)}.xlsx`)
+  }
+
+  return (
+    <div className={`flex flex-col gap-4 ${className || ""}`}>
+      <ClientTableToolbar
+        globalFilter={globalFilter}
+        setGlobalFilter={setGlobalFilter}
+        totalRows={manualPagination ? (rowCount ?? data.length) : data.length}
+        visibleRows={rows.length}
+        onExport={enableExport ? handleExport : undefined}
+        actions={
+          <>
+            {actions}
+            <ClientTableViewOptions table={table} />
+          </>
+        }
+      />
+
+      <div
+        ref={tableContainerRef}
+        className="relative border rounded-md overflow-auto"
+        style={{ height }}
+      >
+        <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-10 bg-muted">
+              {table.getHeaderGroups().map((headerGroup) => (
+                <tr key={headerGroup.id}>
+                  <SortableContext
+                    items={columnOrder}
+                    strategy={horizontalListSortingStrategy}
+                  >
+                    {headerGroup.headers.map((header) => (
+                      <ClientTableColumnHeader
+                        key={header.id}
+                        header={header}
+                        enableReordering={true}
+                      />
+                    ))}
+                  </SortableContext>
+                </tr>
+              ))}
+            </thead>
+            <tbody>
+              {paddingTop > 0 && (
+                <tr>
+                  <td style={{ height: `${paddingTop}px` }} />
+                </tr>
+              )}
+              {virtualRows.map((virtualRow) => {
+                const row = rows[virtualRow.index]
+                return (
+                  <tr
+                    key={row.id}
+                    className={cn(
+                        "hover:bg-muted/50 border-b last:border-0",
+                        getRowClassName ? getRowClassName(row.original, row.index) : ""
+                    )}
+                    style={{ height: `${virtualRow.size}px` }}
+                  >
+                    {row.getVisibleCells().map((cell) => {
+                      // Handle pinned cells
+                      const isPinned = cell.column.getIsPinned()
+                      const style: React.CSSProperties = {
+                        width: cell.column.getSize(),
+                      }
+                      if (isPinned === "left") {
+                         style.position = "sticky"
+                         style.left = `${cell.column.getStart("left")}px`
+                         style.zIndex = 10
+                         style.backgroundColor = "var(--background)" // Ensure opacity
+                      } else if (isPinned === "right") {
+                         style.position = "sticky"
+                         style.right = `${cell.column.getAfter("right")}px`
+                         style.zIndex = 10
+                         style.backgroundColor = "var(--background)"
+                      }
+
+                      return (
+                        <td
+                          key={cell.id}
+                          className="px-4 py-2 text-sm truncate border-b"
+                          style={style}
+                        >
+                          {flexRender(
+                            cell.column.columnDef.cell,
+                            cell.getContext()
+                          )}
+                        </td>
+                      )
+                    })}
+                  </tr>
+                )
+              })}
+              {paddingBottom > 0 && (
+                <tr>
+                  <td style={{ height: `${paddingBottom}px` }} />
+                </tr>
+              )}
+            </tbody>
+          </table>
+        </DndContext>
+      </div>
+      
+      {enablePagination && (
+         <ClientDataTablePagination table={table} />
+      )}
+    </div>
+  )
+}
+
+export const ClientVirtualTable = React.memo(
+  React.forwardRef(ClientVirtualTableInner)
+) as <TData, TValue>(
+  props: ClientVirtualTableProps<TData, TValue> & { ref?: React.Ref<Table<TData>> }
+) => React.ReactElement
-- 
cgit v1.2.3


From 81aa92fecc298d66eb420468316bcf7a7213171c Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Sun, 30 Nov 2025 17:12:17 +0900
Subject: (김준회) group 기능 추가, 기타 버그 수정, preset 기능추가, 테스트
 페이지 추가
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/[lng]/test/table/page.tsx                      | 168 ++++++++
 .../client-table/client-table-column-header.tsx    |  70 +++-
 components/client-table/client-table-preset.tsx    | 185 +++++++++
 components/client-table/client-table-save-view.tsx | 185 +++++++++
 components/client-table/client-table-toolbar.tsx   |  23 +-
 .../client-table/client-table-view-options.tsx     |   9 +-
 components/client-table/client-virtual-table.tsx   | 457 ++++++++++++++++-----
 components/client-table/preset-actions.ts          |  87 ++++
 components/client-table/preset-types.ts            |  13 +
 db/schema/index.ts                                 |   2 +
 db/schema/user-custom-data/userCustomData.ts       |  16 +
 11 files changed, 1093 insertions(+), 122 deletions(-)
 create mode 100644 app/[lng]/test/table/page.tsx
 create mode 100644 components/client-table/client-table-preset.tsx
 create mode 100644 components/client-table/client-table-save-view.tsx
 create mode 100644 components/client-table/preset-actions.ts
 create mode 100644 components/client-table/preset-types.ts
 create mode 100644 db/schema/user-custom-data/userCustomData.ts

(limited to 'components/client-table/client-virtual-table.tsx')

diff --git a/app/[lng]/test/table/page.tsx b/app/[lng]/test/table/page.tsx
new file mode 100644
index 00000000..88d050fc
--- /dev/null
+++ b/app/[lng]/test/table/page.tsx
@@ -0,0 +1,168 @@
+"use client"
+
+import * as React from "react"
+import { ColumnDef } from "@tanstack/react-table"
+import { ClientVirtualTable } from "@/components/client-table/client-virtual-table"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+
+// 1. Define the data type
+type TestData = {
+  id: string
+  name: string
+  email: string
+  role: "Admin" | "User" | "Guest"
+  status: "Active" | "Inactive" | "Pending"
+  lastLogin: string
+  amount: number
+}
+
+// 2. Generate dummy data
+const generateData = (count: number): TestData[] => {
+  const roles: TestData["role"][] = ["Admin", "User", "Guest"]
+  const statuses: TestData["status"][] = ["Active", "Inactive", "Pending"]
+  
+  return Array.from({ length: count }).map((_, i) => ({
+    id: `ID-${i + 1}`,
+    name: `User ${i + 1}`,
+    email: `user${i + 1}@example.com`,
+    role: roles[Math.floor(Math.random() * roles.length)],
+    status: statuses[Math.floor(Math.random() * statuses.length)],
+    lastLogin: new Date(Date.now() - Math.floor(Math.random() * 10000000000)).toISOString().split('T')[0],
+    amount: Math.floor(Math.random() * 10000),
+  }))
+}
+
+export default function TestTablePage() {
+  // State for data
+  const [data, setData] = React.useState<TestData[]>([])
+  const [isLoading, setIsLoading] = React.useState(true)
+
+  // Load data on mount
+  React.useEffect(() => {
+    const timer = setTimeout(() => {
+      setData(generateData(100000)) // Generate 1000 rows
+      setIsLoading(false)
+    }, 500)
+    return () => clearTimeout(timer)
+  }, [])
+
+  // 3. Define columns
+  const columns: ColumnDef<TestData>[] = [
+    {
+      accessorKey: "id",
+      header: "ID",
+      size: 80,
+    },
+    {
+      accessorKey: "name",
+      header: "Name",
+      size: 150,
+    },
+    {
+      accessorKey: "email",
+      header: "Email",
+      size: 200,
+    },
+    {
+      accessorKey: "role",
+      header: "Role",
+      size: 100,
+      cell: ({ getValue }) => {
+        const role = getValue() as string
+        return (
+          <Badge variant={role === "Admin" ? "default" : "secondary"}>
+            {role}
+          </Badge>
+        )
+      }
+    },
+    {
+      accessorKey: "status",
+      header: "Status",
+      size: 100,
+      cell: ({ getValue }) => {
+        const status = getValue() as string
+        let color = "bg-gray-500"
+        if (status === "Active") color = "bg-green-500"
+        if (status === "Inactive") color = "bg-red-500"
+        if (status === "Pending") color = "bg-yellow-500"
+        
+        return (
+          <div className="flex items-center gap-2">
+            <div className={`w-2 h-2 rounded-full ${color}`} />
+            <span>{status}</span>
+          </div>
+        )
+      }
+    },
+    {
+      accessorKey: "amount",
+      header: "Amount",
+      size: 200,
+      cell: ({ getValue }) => {
+        const amount = getValue() as number
+        return new Intl.NumberFormat("en-US", {
+          style: "currency",
+          currency: "USD",
+        }).format(amount)
+      },
+      meta: {
+        align: "right"
+      }
+    },
+    {
+      accessorKey: "lastLogin",
+      header: "Last Login",
+      size: 120,
+    },
+    {
+      id: "actions",
+      header: "Actions",
+      size: 100,
+      cell: () => (
+        <Button variant="ghost" size="sm">Edit</Button>
+      ),
+      enablePinning: true,
+    }
+  ]
+
+  return (
+    <div className="h-full flex flex-col p-6 space-y-4">
+      <div className="flex justify-between items-center">
+        <div>
+          <h1 className="text-2xl font-bold tracking-tight">Virtual Table Test</h1>
+          <p className="text-muted-foreground">
+            Testing the ClientVirtualTable component with 1000 generated rows.
+          </p>
+        </div>
+        <div className="flex gap-2">
+            <Button onClick={() => {
+                setIsLoading(true)
+                setTimeout(() => {
+                    setData(generateData(5000))
+                    setIsLoading(false)
+                }, 500)
+            }}>
+                Reload 5k Rows
+            </Button>
+        </div>
+      </div>
+
+      <div className="border rounded-lg overflow-auto h-[1000px]">
+        <ClientVirtualTable
+          data={data}
+          columns={columns}
+          height="100%"
+          isLoading={isLoading}
+          enablePagination={true}
+          enableRowSelection={true}
+          enableGrouping={true}
+          onRowClick={(row) => console.log("Row clicked:", row.original)}
+          enableUserPreset={true}
+          tableKey="test-table"
+        />
+      </div>
+    </div>
+  )
+}
diff --git a/components/client-table/client-table-column-header.tsx b/components/client-table/client-table-column-header.tsx
index 12dc57ac..2d8e5bce 100644
--- a/components/client-table/client-table-column-header.tsx
+++ b/components/client-table/client-table-column-header.tsx
@@ -1,7 +1,7 @@
 "use client"
 
 import * as React from "react"
-import { Header } from "@tanstack/react-table"
+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"
@@ -20,19 +20,29 @@ import {
   PinOff,
   MoveLeft,
   MoveRight,
+  Group,
+  Ungroup,
 } from "lucide-react"
 import { cn } from "@/lib/utils"
-import { ClientTableFilter } from "./client-table-filter"
+import { ClientTableFilter } from "../client-table/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>) {
@@ -46,7 +56,7 @@ export function ClientTableColumnHeader<TData, TValue>({
     isDragging,
   } = useSortable({
     id: header.id,
-    disabled: !enableReordering,
+    disabled: !enableReordering || column.getIsResizing(),
   })
 
   // -- Styles --
@@ -62,14 +72,18 @@ export function ClientTableColumnHeader<TData, TValue>({
 
   // 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 = 20
+    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 = 20
+    style.zIndex = 30 // Pinned columns needs to be higher than normal headers
   }
 
   // -- Handlers --
@@ -77,6 +91,7 @@ export function ClientTableColumnHeader<TData, TValue>({
   const handlePinLeft = () => column.pin("left")
   const handlePinRight = () => column.pin("right")
   const handleUnpin = () => column.pin(false)
+  const handleToggleGrouping = () => column.toggleGrouping()
 
   // -- Content --
   const content = (
@@ -100,12 +115,14 @@ export function ClientTableColumnHeader<TData, TValue>({
             )}
           </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",
@@ -117,6 +134,25 @@ export function ClientTableColumnHeader<TData, TValue>({
 
       {/* 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>
+        )
+      )}
     </>
   )
 
@@ -141,9 +177,9 @@ export function ClientTableColumnHeader<TData, TValue>({
           colSpan={header.colSpan}
           style={style}
           className={cn(
-            "border-b px-4 py-2 text-left text-sm font-medium bg-muted group",
+            "border-b px-4 py-2 text-left text-sm font-medium bg-muted group transition-colors",
             isDragging ? "opacity-50 bg-accent" : "",
-            isPinned ? "bg-muted shadow-[0_0_10px_rgba(0,0,0,0.1)]" : "",
+            isPinned ? "shadow-[0_0_10px_rgba(0,0,0,0.1)]" : "",
             className
           )}
           {...attributes}
@@ -158,6 +194,26 @@ export function ClientTableColumnHeader<TData, TValue>({
           <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" />
diff --git a/components/client-table/client-table-preset.tsx b/components/client-table/client-table-preset.tsx
new file mode 100644
index 00000000..64930e7a
--- /dev/null
+++ b/components/client-table/client-table-preset.tsx
@@ -0,0 +1,185 @@
+"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);
+    
+    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/client-table-save-view.tsx b/components/client-table/client-table-save-view.tsx
new file mode 100644
index 00000000..73935d00
--- /dev/null
+++ b/components/client-table/client-table-save-view.tsx
@@ -0,0 +1,185 @@
+"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 {
+  getUserCustomSettings,
+  saveUserCustomSetting,
+  deleteUserCustomSetting,
+} from "@/actions/user-custom-data";
+import { toast } from "sonner"; 
+
+interface ClientTableSaveViewProps<TData> {
+  table: Table<TData>;
+  tableKey: string;
+}
+
+export function ClientTableSaveView<TData>({
+  table,
+  tableKey,
+}: ClientTableSaveViewProps<TData>) {
+  const { data: session } = useSession();
+  const [savedViews, setSavedViews] = React.useState<{ id: string; customSettingName: string; customSetting: Record<string, any> }[]>([]);
+  const [isSaveDialogOpen, setIsSaveDialogOpen] = React.useState(false);
+  const [newViewName, setNewViewName] = 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 getUserCustomSettings(tableKey, userId);
+    if (res.success && res.data) {
+      // @ts-ignore - data from DB might need casting
+      setSavedViews(res.data);
+    }
+  }, [session, tableKey]);
+
+  React.useEffect(() => {
+    if (session) {
+      fetchSettings();
+    }
+  }, [fetchSettings, session]);
+
+  const handleSaveView = async () => {
+    const userIdVal = session?.user?.id;
+    if (!newViewName.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 saveUserCustomSetting(userId, tableKey, newViewName, settingToSave);
+    setIsLoading(false);
+
+    if (res.success) {
+      toast.success("View saved successfully");
+      setIsSaveDialogOpen(false);
+      setNewViewName("");
+      fetchSettings();
+    } else {
+      toast.error("Failed to save view");
+    }
+  };
+
+  const handleLoadView = (setting: { customSetting: Record<string, any> | unknown; customSettingName: string }) => {
+    const s = setting.customSetting 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);
+    
+    toast.success(`View "${setting.customSettingName}" loaded`);
+  };
+
+  const handleDeleteView = async (e: React.MouseEvent, id: string) => {
+    e.stopPropagation();
+    if (!confirm("Are you sure you want to delete this view?")) return;
+
+    const res = await deleteUserCustomSetting(id);
+    if (res.success) {
+      toast.success("View deleted");
+      fetchSettings();
+    } else {
+      toast.error("Failed to delete view");
+    }
+  };
+
+  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" />
+              Views
+            </Button>
+          </DropdownMenuTrigger>
+          <DropdownMenuContent align="end" className="w-[200px]">
+            <DropdownMenuLabel>Saved Views</DropdownMenuLabel>
+            <DropdownMenuSeparator />
+            {savedViews.length === 0 ? (
+                <div className="p-2 text-sm text-muted-foreground text-center">No saved views</div>
+            ) : (
+                savedViews.map((view) => (
+                    <DropdownMenuItem key={view.id} onClick={() => handleLoadView(view)} className="flex justify-between cursor-pointer">
+                        <span className="truncate flex-1">{view.customSettingName}</span>
+                        <Button variant="ghost" size="icon" className="h-4 w-4" onClick={(e) => handleDeleteView(e, view.id)}>
+                            <Trash2 className="h-3 w-3 text-destructive" />
+                        </Button>
+                    </DropdownMenuItem>
+                ))
+            )}
+            <DropdownMenuSeparator />
+            <DropdownMenuItem onClick={() => setIsSaveDialogOpen(true)} className="cursor-pointer">
+                <Save className="mr-2 h-4 w-4" />
+                Save Current View
+            </DropdownMenuItem>
+          </DropdownMenuContent>
+        </DropdownMenu>
+
+        <Dialog open={isSaveDialogOpen} onOpenChange={setIsSaveDialogOpen}>
+            <DialogContent>
+                <DialogHeader>
+                    <DialogTitle>Save View</DialogTitle>
+                    <DialogDescription>
+                        Save the current table configuration as a preset.
+                    </DialogDescription>
+                </DialogHeader>
+                <div className="grid gap-4 py-4">
+                    <Input 
+                        placeholder="View Name" 
+                        value={newViewName} 
+                        onChange={(e) => setNewViewName(e.target.value)}
+                    />
+                </div>
+                <DialogFooter>
+                    <Button variant="outline" onClick={() => setIsSaveDialogOpen(false)}>Cancel</Button>
+                    <Button onClick={handleSaveView} disabled={isLoading}>Save</Button>
+                </DialogFooter>
+            </DialogContent>
+        </Dialog>
+    </>
+  );
+}
diff --git a/components/client-table/client-table-toolbar.tsx b/components/client-table/client-table-toolbar.tsx
index 43b0a032..089501e1 100644
--- a/components/client-table/client-table-toolbar.tsx
+++ b/components/client-table/client-table-toolbar.tsx
@@ -12,6 +12,8 @@ interface ClientTableToolbarProps {
   visibleRows: number
   onExport?: () => void
   actions?: React.ReactNode
+  customToolbar?: React.ReactNode
+  viewOptions?: React.ReactNode
 }
 
 export function ClientTableToolbar({
@@ -21,11 +23,13 @@ export function ClientTableToolbar({
   visibleRows,
   onExport,
   actions,
+  customToolbar,
+  viewOptions,
 }: ClientTableToolbarProps) {
   return (
-    <div className="flex items-center justify-between gap-4 p-1">
-      <div className="flex items-center gap-2 flex-1">
-        <div className="relative flex-1 max-w-sm">
+    <div className="flex w-full items-center justify-between gap-4 p-1 overflow-x-auto">
+      <div className="flex items-center gap-2">
+        <div className="relative max-w-sm min-w-[200px]">
           <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
           <Input
             placeholder="Search all columns..."
@@ -37,18 +41,19 @@ export function ClientTableToolbar({
         <div className="text-sm text-muted-foreground whitespace-nowrap">
           Showing {visibleRows} of {totalRows}
         </div>
-      </div>
-
-      <div className="flex items-center gap-2">
-        {actions}
+        {viewOptions}
         {onExport && (
           <Button onClick={onExport} variant="outline" size="sm">
             <Download className="mr-2 h-4 w-4" />
             Export
           </Button>
-        )}
+        )}  
+      </div>
+
+      <div className="flex items-center gap-2 shrink-0">
+        {customToolbar}
+        {actions}
       </div>
     </div>
   )
 }
-
diff --git a/components/client-table/client-table-view-options.tsx b/components/client-table/client-table-view-options.tsx
index b65049b4..3b659fcd 100644
--- a/components/client-table/client-table-view-options.tsx
+++ b/components/client-table/client-table-view-options.tsx
@@ -42,14 +42,21 @@ export function ClientTableViewOptions<TData>({
               typeof column.accessorFn !== "undefined" && column.getCanHide()
           )
           .map((column) => {
+            const header = column.columnDef.header
+            let label = column.id
+            if (typeof header === "string") {
+              label = header
+            }
+
             return (
               <DropdownMenuCheckboxItem
                 key={column.id}
                 className="capitalize"
                 checked={column.getIsVisible()}
                 onCheckedChange={(value) => column.toggleVisibility(!!value)}
+                onSelect={(e) => e.preventDefault()} // default action close the select menu.
               >
-                {column.id}
+                {label}
               </DropdownMenuCheckboxItem>
             )
           })}
diff --git a/components/client-table/client-virtual-table.tsx b/components/client-table/client-virtual-table.tsx
index 4825741f..507057c7 100644
--- a/components/client-table/client-virtual-table.tsx
+++ b/components/client-table/client-virtual-table.tsx
@@ -8,6 +8,11 @@ import {
   getSortedRowModel,
   getFilteredRowModel,
   getPaginationRowModel,
+  getGroupedRowModel,
+  getExpandedRowModel,
+  getFacetedRowModel,
+  getFacetedUniqueValues,
+  getFacetedMinMaxValues,
   ColumnDef,
   SortingState,
   ColumnFiltersState,
@@ -21,6 +26,9 @@ import {
   Table,
   RowSelectionState,
   Row,
+  Column,
+  GroupingState,
+  ExpandedState,
 } from "@tanstack/react-table"
 import { useVirtualizer } from "@tanstack/react-virtual"
 import {
@@ -38,23 +46,41 @@ import {
   horizontalListSortingStrategy,
 } from "@dnd-kit/sortable"
 import { cn } from "@/lib/utils"
+import { Loader2, ChevronRight, ChevronDown } from "lucide-react"
 
-import { ClientTableToolbar } from "./client-table-toolbar"
-import { exportToExcel } from "./export-utils"
+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 { ClientTableColumnHeader } from "./client-table-column-header"
-import { ClientTableViewOptions } from "./client-table-view-options"
+import { ClientTableViewOptions } from "../client-table/client-table-view-options"
+import { ClientTablePreset } from "./client-table-preset"
+
+// Moved outside for stability (Performance Optimization)
+const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
+  const itemRank = rankItem(row.getValue(columnId), value)
+  addMeta({ itemRank })
+  return itemRank.passed
+}
 
 export interface ClientVirtualTableProps<TData, TValue> {
   data: TData[]
   columns: ColumnDef<TData, TValue>[]
   height?: string | number
+  estimateRowHeight?: number
   className?: string
   actions?: React.ReactNode
+  customToolbar?: React.ReactNode
   enableExport?: boolean
   onExport?: (data: TData[]) => void
-  
-  // Pagination Props
+  isLoading?: boolean
+
+  // --- User Preset Saving ---
+  enableUserPreset?: boolean
+  tableKey?: string
+
+  // --- State Control (Controlled or Uncontrolled) ---
+
+  // Pagination
   enablePagination?: boolean
   manualPagination?: boolean
   pageCount?: number
@@ -62,88 +88,184 @@ export interface ClientVirtualTableProps<TData, TValue> {
   pagination?: PaginationState
   onPaginationChange?: OnChangeFn<PaginationState>
 
-  // Style Props
-  getRowClassName?: (originalRow: TData, index: number) => string
+  // Sorting
+  sorting?: SortingState
+  onSortingChange?: OnChangeFn<SortingState>
 
-  // Table Meta
-  meta?: any
-  
-  // Row ID
-  getRowId?: (originalRow: TData, index: number, parent?: any) => string
+  // Filtering
+  columnFilters?: ColumnFiltersState
+  onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>
+  globalFilter?: string
+  onGlobalFilterChange?: OnChangeFn<string>
+
+  // Visibility
+  columnVisibility?: VisibilityState
+  onColumnVisibilityChange?: OnChangeFn<VisibilityState>
+
+  // Pinning
+  columnPinning?: ColumnPinningState
+  onColumnPinningChange?: OnChangeFn<ColumnPinningState>
 
-  // Selection Props
+  // Order
+  columnOrder?: ColumnOrderState
+  onColumnOrderChange?: OnChangeFn<ColumnOrderState>
+
+  // Selection
   enableRowSelection?: boolean | ((row: Row<TData>) => boolean)
   enableMultiRowSelection?: boolean | ((row: Row<TData>) => boolean)
   rowSelection?: RowSelectionState
   onRowSelectionChange?: OnChangeFn<RowSelectionState>
+
+  // Grouping
+  enableGrouping?: boolean
+  grouping?: GroupingState
+  onGroupingChange?: OnChangeFn<GroupingState>
+  expanded?: ExpandedState
+  onExpandedChange?: OnChangeFn<ExpandedState>
+
+  // --- Event Handlers ---
+  onRowClick?: (row: Row<TData>, event: React.MouseEvent) => void
+
+  // --- Styling ---
+  getRowClassName?: (originalRow: TData, index: number) => string
+
+  // --- Advanced ---
+  meta?: Record<string, any>
+  getRowId?: (originalRow: TData, index: number, parent?: any) => string
+
+  // Custom Header Visual Feedback
+  renderHeaderVisualFeedback?: (props: {
+    column: Column<TData, TValue>
+    isPinned: boolean | string
+    isSorted: boolean | string
+    isFiltered: boolean
+    isGrouped: boolean
+  }) => React.ReactNode
 }
 
 function ClientVirtualTableInner<TData, TValue>(
   {
     data,
     columns,
-    height = "500px", // Default height
+    height = "100%",
+    estimateRowHeight = 40,
     className,
     actions,
+    customToolbar,
     enableExport = true,
     onExport,
-    
-    // Pagination defaults
+    isLoading = false,
+
+    // User Preset Saving
+    enableUserPreset = false,
+    tableKey,
+
+    // Pagination
     enablePagination = false,
     manualPagination = false,
     pageCount,
     rowCount,
-    pagination: controlledPagination,
+    pagination: propPagination,
     onPaginationChange,
 
+    // Sorting
+    sorting: propSorting,
+    onSortingChange,
+
+    // Filtering
+    columnFilters: propColumnFilters,
+    onColumnFiltersChange,
+    globalFilter: propGlobalFilter,
+    onGlobalFilterChange,
+
+    // Visibility
+    columnVisibility: propColumnVisibility,
+    onColumnVisibilityChange,
+
+    // Pinning
+    columnPinning: propColumnPinning,
+    onColumnPinningChange,
+
+    // Order
+    columnOrder: propColumnOrder,
+    onColumnOrderChange,
+
+    // Selection
+    enableRowSelection,
+    enableMultiRowSelection,
+    rowSelection: propRowSelection,
+    onRowSelectionChange,
+
+    // Grouping
+    enableGrouping = false,
+    grouping: propGrouping,
+    onGroupingChange,
+    expanded: propExpanded,
+    onExpandedChange,
+
     // Style defaults
     getRowClassName,
-    
+
     // Meta & RowID
     meta,
     getRowId,
 
-    // Selection defaults
-    enableRowSelection,
-    enableMultiRowSelection,
-    rowSelection: controlledRowSelection,
-    onRowSelectionChange,
+    // Event Handlers
+    onRowClick,
+
+    // Custom Header Visual Feedback
+    renderHeaderVisualFeedback,
   }: ClientVirtualTableProps<TData, TValue>,
   ref: React.Ref<Table<TData>>
 ) {
-  // State
-  const [sorting, setSorting] = React.useState<SortingState>([])
-  const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
-  const [globalFilter, setGlobalFilter] = React.useState("")
-  const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
-  const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({ left: [], right: [] })
-  const [columnOrder, setColumnOrder] = React.useState<ColumnOrderState>(
+  // Internal States (used when props are undefined)
+  const [internalSorting, setInternalSorting] = React.useState<SortingState>([])
+  const [internalColumnFilters, setInternalColumnFilters] = React.useState<ColumnFiltersState>([])
+  const [internalGlobalFilter, setInternalGlobalFilter] = React.useState("")
+  const [internalColumnVisibility, setInternalColumnVisibility] = React.useState<VisibilityState>({})
+  const [internalColumnPinning, setInternalColumnPinning] = React.useState<ColumnPinningState>({ left: [], right: [] })
+  const [internalColumnOrder, setInternalColumnOrder] = React.useState<ColumnOrderState>(
     () => columns.map((c) => c.id || (c as any).accessorKey) as string[]
   )
-  
-  // Internal Pagination State
   const [internalPagination, setInternalPagination] = React.useState<PaginationState>({
     pageIndex: 0,
-    pageSize: 50,
+    pageSize: 10,
   })
-
-  // Internal Row Selection State
   const [internalRowSelection, setInternalRowSelection] = React.useState<RowSelectionState>({})
+  const [internalGrouping, setInternalGrouping] = React.useState<GroupingState>([])
+  const [internalExpanded, setInternalExpanded] = React.useState<ExpandedState>({})
 
-  // Fuzzy Filter
-  const fuzzyFilter: FilterFn<TData> = (row, columnId, value, addMeta) => {
-    const itemRank = rankItem(row.getValue(columnId), value)
-    addMeta({ itemRank })
-    return itemRank.passed
-  }
+  // Effective States
+  const sorting = propSorting ?? internalSorting
+  const setSorting = onSortingChange ?? setInternalSorting
+
+  const columnFilters = propColumnFilters ?? internalColumnFilters
+  const setColumnFilters = onColumnFiltersChange ?? setInternalColumnFilters
+
+  const globalFilter = propGlobalFilter ?? internalGlobalFilter
+  const setGlobalFilter = onGlobalFilterChange ?? setInternalGlobalFilter
+
+  const columnVisibility = propColumnVisibility ?? internalColumnVisibility
+  const setColumnVisibility = onColumnVisibilityChange ?? setInternalColumnVisibility
 
-  // Combine controlled and uncontrolled states
-  const pagination = controlledPagination ?? internalPagination
+  const columnPinning = propColumnPinning ?? internalColumnPinning
+  const setColumnPinning = onColumnPinningChange ?? setInternalColumnPinning
+
+  const columnOrder = propColumnOrder ?? internalColumnOrder
+  const setColumnOrder = onColumnOrderChange ?? setInternalColumnOrder
+
+  const pagination = propPagination ?? internalPagination
   const setPagination = onPaginationChange ?? setInternalPagination
-  
-  const rowSelection = controlledRowSelection ?? internalRowSelection
+
+  const rowSelection = propRowSelection ?? internalRowSelection
   const setRowSelection = onRowSelectionChange ?? setInternalRowSelection
 
+  const grouping = propGrouping ?? internalGrouping
+  const setGrouping = onGroupingChange ?? setInternalGrouping
+
+  const expanded = propExpanded ?? internalExpanded
+  const setExpanded = onExpandedChange ?? setInternalExpanded
+
   // Table Instance
   const table = useReactTable({
     data,
@@ -157,6 +279,8 @@ function ClientVirtualTableInner<TData, TValue>(
       columnPinning,
       columnOrder,
       rowSelection,
+      grouping,
+      expanded,
     },
     manualPagination,
     pageCount: manualPagination ? pageCount : undefined,
@@ -169,11 +293,28 @@ function ClientVirtualTableInner<TData, TValue>(
     onColumnPinningChange: setColumnPinning,
     onColumnOrderChange: setColumnOrder,
     onRowSelectionChange: setRowSelection,
+    onGroupingChange: setGrouping,
+    onExpandedChange: setExpanded,
     enableRowSelection,
     enableMultiRowSelection,
+    enableGrouping,
     getCoreRowModel: getCoreRowModel(),
-    getSortedRowModel: getSortedRowModel(),
+
+    // Systematic Order of Operations:
+    // 1. Filtering (Rows are filtered first)
     getFilteredRowModel: getFilteredRowModel(),
+    getFacetedRowModel: getFacetedRowModel(),
+    getFacetedUniqueValues: getFacetedUniqueValues(),
+    getFacetedMinMaxValues: getFacetedMinMaxValues(),
+
+    // 2. Sorting (Filtered rows are then sorted)
+    getSortedRowModel: getSortedRowModel(),
+
+    // 3. Grouping (Sorted rows are grouped)
+    getGroupedRowModel: enableGrouping ? getGroupedRowModel() : undefined,
+    getExpandedRowModel: enableGrouping ? getExpandedRowModel() : undefined,
+
+    // 4. Pagination (Final rows are paginated)
     getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
     columnResizeMode: "onChange",
     filterFns: {
@@ -191,7 +332,7 @@ function ClientVirtualTableInner<TData, TValue>(
   const sensors = useSensors(
     useSensor(PointerSensor, {
       activationConstraint: {
-        distance: 8, // 8px movement required to start drag
+        distance: 8,
       },
     }),
     useSensor(KeyboardSensor)
@@ -201,11 +342,29 @@ function ClientVirtualTableInner<TData, TValue>(
   const handleDragEnd = (event: DragEndEvent) => {
     const { active, over } = event
     if (active && over && active.id !== over.id) {
-      setColumnOrder((items) => {
-        const oldIndex = items.indexOf(active.id as string)
-        const newIndex = items.indexOf(over.id as string)
-        return arrayMove(items, oldIndex, newIndex)
-      })
+      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 of the active column
+        if (activePinState !== overPinState) {
+          activeColumn.pin(overPinState)
+        }
+
+        // Reorder the columns
+        setColumnOrder((items) => {
+          const currentItems = Array.isArray(items) ? items : []
+          const oldIndex = items.indexOf(activeId)
+          const newIndex = items.indexOf(overId)
+          return arrayMove(items, oldIndex, newIndex)
+        })
+      }
     }
   }
 
@@ -216,7 +375,7 @@ function ClientVirtualTableInner<TData, TValue>(
   const rowVirtualizer = useVirtualizer({
     count: rows.length,
     getScrollElement: () => tableContainerRef.current,
-    estimateSize: () => 40, // Estimated row height
+    estimateSize: () => estimateRowHeight,
     overscan: 10,
   })
 
@@ -236,30 +395,42 @@ function ClientVirtualTableInner<TData, TValue>(
       return
     }
     const currentData = table.getFilteredRowModel().rows.map((row) => row.original)
-    await exportToExcel(currentData, columns, `export-${new Date().toISOString().slice(0,10)}.xlsx`)
+    await exportToExcel(currentData, columns, `export-${new Date().toISOString().slice(0, 10)}.xlsx`)
   }
 
   return (
-    <div className={`flex flex-col gap-4 ${className || ""}`}>
+    <div
+      className={`flex flex-col gap-4 ${className || ""}`}
+      style={{ height }}
+    >
       <ClientTableToolbar
         globalFilter={globalFilter}
         setGlobalFilter={setGlobalFilter}
         totalRows={manualPagination ? (rowCount ?? data.length) : data.length}
         visibleRows={rows.length}
         onExport={enableExport ? handleExport : undefined}
-        actions={
+        viewOptions={
           <>
-            {actions}
             <ClientTableViewOptions table={table} />
+            {enableUserPreset && tableKey && (
+              <ClientTablePreset table={table} tableKey={tableKey} />
+            )}
           </>
         }
+        customToolbar={customToolbar}
+        actions={actions}
       />
 
       <div
         ref={tableContainerRef}
-        className="relative border rounded-md overflow-auto"
-        style={{ height }}
+        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}
@@ -269,11 +440,11 @@ function ClientVirtualTableInner<TData, TValue>(
             className="table-fixed border-collapse w-full min-w-full"
             style={{ width: table.getTotalSize() }}
           >
-            <thead className="sticky top-0 z-10 bg-muted">
+            <thead className="sticky top-0 z-40 bg-muted">
               {table.getHeaderGroups().map((headerGroup) => (
                 <tr key={headerGroup.id}>
                   <SortableContext
-                    items={columnOrder}
+                    items={headerGroup.headers.map((h) => h.id)}
                     strategy={horizontalListSortingStrategy}
                   >
                     {headerGroup.headers.map((header) => (
@@ -281,6 +452,7 @@ function ClientVirtualTableInner<TData, TValue>(
                         key={header.id}
                         header={header}
                         enableReordering={true}
+                        renderHeaderVisualFeedback={renderHeaderVisualFeedback}
                       />
                     ))}
                   </SortableContext>
@@ -293,51 +465,126 @@ function ClientVirtualTableInner<TData, TValue>(
                   <td style={{ height: `${paddingTop}px` }} />
                 </tr>
               )}
-              {virtualRows.map((virtualRow) => {
-                const row = rows[virtualRow.index]
-                return (
-                  <tr
-                    key={row.id}
-                    className={cn(
-                        "hover:bg-muted/50 border-b last:border-0",
-                        getRowClassName ? getRowClassName(row.original, row.index) : ""
-                    )}
-                    style={{ height: `${virtualRow.size}px` }}
-                  >
-                    {row.getVisibleCells().map((cell) => {
-                      // Handle pinned cells
-                      const isPinned = cell.column.getIsPinned()
-                      const style: React.CSSProperties = {
-                        width: cell.column.getSize(),
-                      }
-                      if (isPinned === "left") {
-                         style.position = "sticky"
-                         style.left = `${cell.column.getStart("left")}px`
-                         style.zIndex = 10
-                         style.backgroundColor = "var(--background)" // Ensure opacity
-                      } else if (isPinned === "right") {
-                         style.position = "sticky"
-                         style.right = `${cell.column.getAfter("right")}px`
-                         style.zIndex = 10
-                         style.backgroundColor = "var(--background)"
-                      }
-
-                      return (
+              {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
-                          key={cell.id}
-                          className="px-4 py-2 text-sm truncate border-b"
-                          style={style}
+                          colSpan={columns.length}
+                          className="px-4 py-2 text-left font-medium cursor-pointer"
+                          onClick={row.getToggleExpandedHandler()}
                         >
-                          {flexRender(
-                            cell.column.columnDef.cell,
-                            cell.getContext()
-                          )}
+                          <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>
-                )
-              })}
+                      </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) => {
+                        // Handle pinned cells
+                        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() ? (
+                              // If this cell is grouped, usually we don't render it here if we have a group header row,
+                              // but if we keep it, it acts as the expander for the next level (if multi-level grouping).
+                              // Since we used a full-width row for the group header, this branch might not be hit for the group row itself,
+                              // but for nested groups it might?
+                              // Wait, row.getIsGrouped() is true for the group row.
+                              // The cells inside the group row are not rendered because we return early above.
+                              // The cells inside the "leaf" rows (normal rows) are rendered here.
+                              // So cell.getIsGrouped() checks if the COLUMN is currently grouped.
+                              // If the column is grouped, the cell value is usually redundant or hidden in normal rows.
+                              // Standard practice: hide the cell content or dim it.
+                              null
+                            ) : cell.getIsAggregated() ? (
+                              // If this cell is an aggregation of the group
+                              flexRender(
+                                cell.column.columnDef.aggregatedCell ??
+                                cell.column.columnDef.cell,
+                                cell.getContext()
+                              )
+                            ) : (
+                              // Normal cell
+                              cell.getIsPlaceholder()
+                                ? null
+                                : flexRender(cell.column.columnDef.cell, cell.getContext())
+                            )}
+                          </td>
+                        )
+                      })}
+                    </tr>
+                  )
+                })
+              )}
               {paddingBottom > 0 && (
                 <tr>
                   <td style={{ height: `${paddingBottom}px` }} />
@@ -347,9 +594,9 @@ function ClientVirtualTableInner<TData, TValue>(
           </table>
         </DndContext>
       </div>
-      
+
       {enablePagination && (
-         <ClientDataTablePagination table={table} />
+        <ClientDataTablePagination table={table} />
       )}
     </div>
   )
diff --git a/components/client-table/preset-actions.ts b/components/client-table/preset-actions.ts
new file mode 100644
index 00000000..0b8b3adb
--- /dev/null
+++ b/components/client-table/preset-actions.ts
@@ -0,0 +1,87 @@
+"use server";
+
+import db from "@/db/db";
+import { userCustomData } from "@/db/schema/user-custom-data/userCustomData";
+import { eq, and } from "drizzle-orm";
+import { Preset, PresetRepository } from "./preset-types";
+
+// Drizzle Implementation of PresetRepository
+// This file acts as the concrete repository implementation.
+// To swap DBs, you would replace the logic here or create a new implementation file.
+
+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);
+
+    // Map DB entity to domain model
+    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/preset-types.ts b/components/client-table/preset-types.ts
new file mode 100644
index 00000000..072d918b
--- /dev/null
+++ b/components/client-table/preset-types.ts
@@ -0,0 +1,13 @@
+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/db/schema/index.ts b/db/schema/index.ts
index cd54e032..fdc25afc 100644
--- a/db/schema/index.ts
+++ b/db/schema/index.ts
@@ -50,6 +50,8 @@ export * from './permissions';
 
 export * from './fileSystem';
 
+export * from './user-custom-data/userCustomData';
+
 // 부서별 도메인 할당 관리
 export * from './departmentDomainAssignments';
 
diff --git a/db/schema/user-custom-data/userCustomData.ts b/db/schema/user-custom-data/userCustomData.ts
new file mode 100644
index 00000000..bf529679
--- /dev/null
+++ b/db/schema/user-custom-data/userCustomData.ts
@@ -0,0 +1,16 @@
+/** 
+ * user custom data
+ * 
+ * */
+import { integer, json, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
+import { users } from "../users";
+
+export const userCustomData = pgTable("user_custom_data", {
+  id: uuid("id").primaryKey().defaultRandom(),
+  userId: integer("user_id").references(() => users.id),
+  tableKey: varchar("table_key", { length: 255 }).notNull(),
+  customSettingName: varchar("custom_setting_name", { length: 255 }).notNull(),
+  customSetting: json("custom_setting"),
+  createdDate: timestamp("created_date", { withTimezone: true }).defaultNow().notNull(),
+  updatedDate: timestamp("updated_date", { withTimezone: true }).defaultNow().notNull(),
+});
-- 
cgit v1.2.3

