From e0dfb55c5457aec489fc084c4567e791b4c65eb1 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Wed, 26 Mar 2025 00:37:41 +0000
Subject: 3/25 까지의 대표님 작업사항
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../client-data-table/data-table-group-list.tsx    | 279 +++++++++++++++++++++
 1 file changed, 279 insertions(+)
 create mode 100644 components/client-data-table/data-table-group-list.tsx

(limited to 'components/client-data-table/data-table-group-list.tsx')

diff --git a/components/client-data-table/data-table-group-list.tsx b/components/client-data-table/data-table-group-list.tsx
new file mode 100644
index 00000000..519b7327
--- /dev/null
+++ b/components/client-data-table/data-table-group-list.tsx
@@ -0,0 +1,279 @@
+"use client"
+
+import * as React from "react"
+import { type Table } from "@tanstack/react-table"
+import { Layers, Check, ChevronsUpDown, GripVertical, XCircle } from "lucide-react"
+
+import { toSentenceCase, cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"
+import {
+  Command,
+  CommandList,
+  CommandGroup,
+  CommandItem,
+  CommandInput,
+  CommandEmpty,
+} from "@/components/ui/command"
+import {
+  Sortable,
+  SortableItem,
+  SortableDragHandle,
+} from "@/components/ui/sortable"
+
+interface DataTableGroupListLocalProps<TData> {
+  /** TanStack Table 인스턴스 (grouping을 사용할 수 있어야 함) */
+  table: Table<TData>
+}
+
+export function ClientDataTableGroupList<TData>({
+  table,
+}: DataTableGroupListLocalProps<TData>) {
+  // -----------------------------
+  // 1) Local grouping state
+  // -----------------------------
+  const [grouping, setGrouping] = React.useState<string[]>(
+    (table.initialState.grouping as string[]) ?? []
+  )
+
+  // Keep the table grouping in sync
+  React.useEffect(() => {
+    table.setGrouping(grouping)
+  }, [grouping, table])
+
+  // Avoid duplicates (just in case)
+  const uniqueGrouping = React.useMemo(
+    () => [...new Set(grouping)],
+    [grouping]
+  )
+
+  // -----------------------------
+  // 2) Groupable columns
+  // -----------------------------
+  const groupableColumns = React.useMemo(
+    () =>
+      table
+        .getAllColumns()
+        .filter((col) => col.getCanGroup?.() !== false)
+        .map((col) => {
+          // If meta?.excelHeader is missing or undefined, fall back to `col.id`
+          const friendlyName =
+            typeof col.columnDef?.meta?.excelHeader === "string"
+              ? col.columnDef.meta.excelHeader
+              : col.id
+
+          return {
+            id: col.id,
+            // Ensure it's always a string, so no type error:
+            label: toSentenceCase(friendlyName),
+          }
+        }),
+    [table]
+  )
+
+  const ungroupedColumns = React.useMemo(
+    () => groupableColumns.filter((c) => !uniqueGrouping.includes(c.id)),
+    [groupableColumns, uniqueGrouping]
+  )
+
+  // -----------------------------
+  // 3) Handlers
+  // -----------------------------
+  // Add the first ungrouped column
+  function addGroup() {
+    const firstAvailable = ungroupedColumns[0]
+    if (!firstAvailable) return
+    setGrouping((prev) => [...prev, firstAvailable.id])
+  }
+
+  // Remove a group
+  function removeGroup(colId: string) {
+    setGrouping((prev) => prev.filter((g) => g !== colId))
+  }
+
+  // Reset grouping entirely
+  function resetGrouping() {
+    setGrouping([])
+  }
+
+  // Reorder groups via Sortable
+  function onGroupOrderChange(newGroups: string[]) {
+    setGrouping(newGroups)
+  }
+
+  // -----------------------------
+  // 4) Render
+  // -----------------------------
+  return (
+    <Sortable
+      value={uniqueGrouping.map((id) => ({ id }))}
+      onValueChange={(items) => onGroupOrderChange(items.map((i) => i.id))}
+      overlay={
+        <div className="flex items-center gap-2">
+          <div className="h-8 w-[11.25rem] rounded-sm bg-primary/10" />
+          <div className="h-8 w-24 rounded-sm bg-primary/10" />
+          <div className="size-8 shrink-0 rounded-sm bg-primary/10" />
+        </div>
+      }
+    >
+      <Popover>
+        <PopoverTrigger asChild>
+          <Button variant="outline" size="sm" className="gap-2">
+            <Layers className="size-3" aria-hidden="true" />
+            <span className="hidden sm:inline">Group</span>
+            {uniqueGrouping.length > 0 && (
+              <Badge
+                variant="secondary"
+                className="h-[1.14rem] rounded-[0.2rem] px-[0.32rem] font-mono text-[0.65rem] font-normal"
+              >
+                {uniqueGrouping.length}
+              </Badge>
+            )}
+          </Button>
+        </PopoverTrigger>
+
+        <PopoverContent
+          align="start"
+          collisionPadding={16}
+          className={cn(
+            "flex w-[calc(100vw-theme(spacing.20))] min-w-72 max-w-[25rem] flex-col p-4 sm:w-[25rem]",
+            uniqueGrouping.length > 0 ? "gap-3.5" : "gap-2"
+          )}
+        >
+          {uniqueGrouping.length > 0 ? (
+            <>
+              <h4 className="font-medium leading-none">Group by</h4>
+              <p className="text-sm text-muted-foreground">
+                Grouping is applied to the currently loaded data only.
+              </p>
+            </>
+          ) : (
+            <div className="flex flex-col gap-1">
+              <h4 className="font-medium leading-none">No grouping applied</h4>
+              <p className="text-sm text-muted-foreground">
+                Add grouping to organize your results.
+              </p>
+            </div>
+          )}
+
+          {/* Current groups */}
+          <div className="flex max-h-40 flex-col gap-2 overflow-y-auto p-0.5">
+            <div className="flex w-full flex-col gap-2">
+              {uniqueGrouping.map((colId) => {
+                // Find the column's friendly label
+                const colDef = groupableColumns.find((c) => c.id === colId)
+                const label = colDef?.label ?? toSentenceCase(colId)
+
+                return (
+                  <SortableItem key={colId} value={colId} asChild>
+                    <div className="flex items-center gap-2">
+                      <Popover modal>
+                        <PopoverTrigger asChild>
+                          <Button
+                            variant="outline"
+                            size="sm"
+                            role="combobox"
+                            className="h-8 w-44 justify-between gap-2 rounded focus:outline-none focus:ring-1 focus:ring-ring"
+                            aria-label={`Select column for group ${colId}`}
+                          >
+                            <span className="truncate">{label}</span>
+                            <div className="ml-auto flex items-center gap-1">
+                              <ChevronsUpDown
+                                className="size-4 shrink-0 opacity-50"
+                                aria-hidden="true"
+                              />
+                            </div>
+                          </Button>
+                        </PopoverTrigger>
+                        <PopoverContent
+                          className="w-[var(--radix-popover-trigger-width)] p-0"
+                        >
+                          <Command>
+                            <CommandInput placeholder="Search columns..." />
+                            <CommandList>
+                              <CommandEmpty>No columns found.</CommandEmpty>
+                              <CommandGroup>
+                                {ungroupedColumns.map((column) => (
+                                  <CommandItem
+                                    key={column.id}
+                                    value={column.id}
+                                    onSelect={(value) => {
+                                      // Replace colId with new value
+                                      setGrouping((prev) =>
+                                        prev.map((g) => (g === colId ? value : g))
+                                      )
+                                    }}
+                                  >
+                                    <span className="mr-1.5 truncate">
+                                      {column.label}
+                                    </span>
+                                    <Check
+                                      className={cn(
+                                        "ml-auto size-4 shrink-0",
+                                        column.id === colId
+                                          ? "opacity-100"
+                                          : "opacity-0"
+                                      )}
+                                      aria-hidden="true"
+                                    />
+                                  </CommandItem>
+                                ))}
+                              </CommandGroup>
+                            </CommandList>
+                          </Command>
+                        </PopoverContent>
+                      </Popover>
+
+                      {/* remove group */}
+                      <Button
+                        variant="outline"
+                        size="icon"
+                        aria-label={`Remove group ${colId}`}
+                        className="size-8 shrink-0 rounded"
+                        onClick={() => removeGroup(colId)}
+                      >
+                        <XCircle className="size-3.5" aria-hidden="true" />
+                      </Button>
+
+                      {/* drag handle */}
+                      <SortableDragHandle
+                        variant="outline"
+                        size="icon"
+                        className="size-8 shrink-0 rounded"
+                      >
+                        <GripVertical className="size-3.5" aria-hidden="true" />
+                      </SortableDragHandle>
+                    </div>
+                  </SortableItem>
+                )
+              })}
+            </div>
+          </div>
+
+          {/* Footer: "Add group" & "Reset grouping" */}
+          <div className="flex w-full items-center gap-2">
+            <Button
+              size="sm"
+              className="h-[1.85rem] rounded"
+              onClick={addGroup}
+              disabled={uniqueGrouping.length >= groupableColumns.length}
+            >
+              Add group
+            </Button>
+            {uniqueGrouping.length > 0 && (
+              <Button
+                size="sm"
+                variant="outline"
+                className="rounded"
+                onClick={resetGrouping}
+              >
+                Reset grouping
+              </Button>
+            )}
+          </div>
+        </PopoverContent>
+      </Popover>
+    </Sortable>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

