summaryrefslogtreecommitdiff
path: root/components/client-table-v2/client-table-column-header.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/client-table-v2/client-table-column-header.tsx')
-rw-r--r--components/client-table-v2/client-table-column-header.tsx235
1 files changed, 235 insertions, 0 deletions
diff --git a/components/client-table-v2/client-table-column-header.tsx b/components/client-table-v2/client-table-column-header.tsx
new file mode 100644
index 00000000..2d8e5bce
--- /dev/null
+++ b/components/client-table-v2/client-table-column-header.tsx
@@ -0,0 +1,235 @@
+"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/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>
+ )
+}