summaryrefslogtreecommitdiff
path: root/components/client-data-table/data-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/client-data-table/data-table.tsx')
-rw-r--r--components/client-data-table/data-table.tsx336
1 files changed, 336 insertions, 0 deletions
diff --git a/components/client-data-table/data-table.tsx b/components/client-data-table/data-table.tsx
new file mode 100644
index 00000000..ff10bfe4
--- /dev/null
+++ b/components/client-data-table/data-table.tsx
@@ -0,0 +1,336 @@
+"use client"
+
+import * as React from "react"
+import {
+ ColumnDef,
+ ColumnFiltersState,
+ SortingState,
+ VisibilityState,
+ flexRender,
+ getCoreRowModel,
+ getFacetedRowModel,
+ getFacetedUniqueValues,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable,
+ Table,
+ getGroupedRowModel,
+ getExpandedRowModel,
+ ColumnSizingState, ColumnPinningState
+} from "@tanstack/react-table"
+import {
+ Table as UiTable,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { getCommonPinningStyles } from "@/lib/data-table"
+import { ChevronRight, ChevronUp } from "lucide-react"
+
+import { ClientDataTableAdvancedToolbar } from "./data-table-toolbar"
+import { ClientDataTablePagination } from "./data-table-pagination"
+import { DataTableResizer } from "./data-table-resizer"
+import { useAutoSizeColumns } from "@/hooks/useAutoSizeColumns"
+
+interface DataTableProps<TData, TValue> {
+ columns: ColumnDef<TData, TValue>[]
+ data: TData[]
+ advancedFilterFields: any[]
+ autoSizeColumns?: boolean
+ onSelectedRowsChange?: (selected: TData[]) => void
+
+ /** 추가로 표시할 버튼/컴포넌트 */
+ children?: React.ReactNode
+}
+
+export function ClientDataTable<TData, TValue>({
+ columns,
+ data,
+ advancedFilterFields,
+ autoSizeColumns = true,
+ children,
+ onSelectedRowsChange
+}: DataTableProps<TData, TValue>) {
+
+
+ // (1) React Table 상태
+ const [rowSelection, setRowSelection] = React.useState({})
+ const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
+ const [sorting, setSorting] = React.useState<SortingState>([])
+ const [grouping, setGrouping] = React.useState<string[]>([])
+ const [columnSizing, setColumnSizing] = React.useState<ColumnSizingState>({})
+
+ // 실제 리사이징 상태만 추적
+ const [isResizing, setIsResizing] = React.useState(false)
+
+ // 리사이징 상태를 추적하기 위한 ref
+ const isResizingRef = React.useRef(false)
+
+ // 리사이징 이벤트 핸들러
+ const handleResizeStart = React.useCallback(() => {
+ isResizingRef.current = true
+ setIsResizing(true)
+ }, [])
+
+ const handleResizeEnd = React.useCallback(() => {
+ isResizingRef.current = false
+ setIsResizing(false)
+ }, [])
+
+ const [columnPinning, setColumnPinning] = React.useState<ColumnPinningState>({
+ left: [],
+ right: ["update"],
+ })
+
+ const table = useReactTable({
+ data,
+ columns,
+ state: {
+ sorting,
+ columnVisibility,
+ rowSelection,
+ columnFilters,
+ grouping,
+ columnSizing,
+ columnPinning
+ },
+ columnResizeMode: "onChange",
+ onColumnSizingChange: setColumnSizing,
+ enableRowSelection: true,
+ onRowSelectionChange: setRowSelection,
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onColumnVisibilityChange: setColumnVisibility,
+ onGroupingChange: setGrouping,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFacetedRowModel: getFacetedRowModel(),
+ getFacetedUniqueValues: getFacetedUniqueValues(),
+ getGroupedRowModel: getGroupedRowModel(),
+ autoResetPageIndex: false,
+ getExpandedRowModel: getExpandedRowModel(),
+ enableColumnPinning:true,
+ onColumnPinningChange:setColumnPinning
+
+ })
+
+ useAutoSizeColumns(table, autoSizeColumns)
+
+ // 컴포넌트 마운트 시 강제로 리사이징 상태 초기화
+ React.useEffect(() => {
+ // 강제로 초기 상태는 리사이징 비활성화
+ setIsResizing(false)
+ isResizingRef.current = false
+
+ // 전역 마우스 이벤트 핸들러
+ const handleMouseUp = () => {
+ if (isResizingRef.current) {
+ handleResizeEnd()
+ }
+ }
+
+ // 이벤트 리스너 등록
+ window.addEventListener('mouseup', handleMouseUp)
+ window.addEventListener('touchend', handleMouseUp)
+
+ return () => {
+ // 이벤트 리스너 정리
+ window.removeEventListener('mouseup', handleMouseUp)
+ window.removeEventListener('touchend', handleMouseUp)
+
+ // 컴포넌트 언마운트 시 정리
+ setIsResizing(false)
+ isResizingRef.current = false
+ }
+ }, [handleResizeEnd])
+
+ React.useEffect(() => {
+ if (!onSelectedRowsChange) return
+ const selectedRows = table
+ .getSelectedRowModel()
+ .flatRows.map((row) => row.original)
+ onSelectedRowsChange(selectedRows)
+ }, [rowSelection, table, onSelectedRowsChange])
+
+ // (2) 렌더
+ return (
+ <div className="space-y-4">
+ {/* 툴바에 children을 넘기기 */}
+ <ClientDataTableAdvancedToolbar
+ table={table}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ {children}
+ </ClientDataTableAdvancedToolbar>
+
+ <div className="rounded-md border">
+ <div className="overflow-auto" style={{maxHeight:'33.6rem'}}>
+ <UiTable
+ className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed"
+ >
+ <TableHeader>
+ {table.getHeaderGroups().map((headerGroup) => (
+ <TableRow key={headerGroup.id}>
+ {headerGroup.headers.map((header) => {
+ // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음
+ if (header.column.getIsGrouped()) {
+ return null
+ }
+
+ return (
+ <TableHead
+ key={header.id}
+ colSpan={header.colSpan}
+ className="relative"
+ style={{
+ ...getCommonPinningStyles({ column: header.column }),
+ width: header.getSize()
+ }}
+ >
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ {/* 리사이즈 핸들 - 헤더에만 추가 */}
+ {header.column.getCanResize() && (
+ <DataTableResizer
+ header={header}
+ onResizeStart={handleResizeStart}
+ onResizeEnd={handleResizeEnd}
+ />
+ )}
+ </TableHead>
+ )
+ })}
+ </TableRow>
+ ))}
+ </TableHeader>
+ <TableBody>
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => {
+ // ---------------------------------------------------
+ // 1) "그룹핑 헤더" Row인지 확인
+ // ---------------------------------------------------
+ if (row.getIsGrouped()) {
+ // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음
+ const groupingColumnId = row.groupingColumnId ?? ""
+ const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체
+
+ // 컬럼 라벨 가져오기
+ let columnLabel = groupingColumnId
+ if (groupingColumn) {
+ const headerDef = groupingColumn.columnDef.meta?.excelHeader
+ if (typeof headerDef === "string") {
+ columnLabel = headerDef
+ }
+ }
+
+ return (
+ <TableRow
+ key={row.id}
+ className="bg-muted/20"
+ data-state={row.getIsExpanded() && "expanded"}
+ >
+ {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */}
+ <TableCell colSpan={table.getVisibleFlatColumns().length}>
+ {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */}
+ {row.getCanExpand() && (
+ <button
+ onClick={row.getToggleExpandedHandler()}
+ className="inline-flex items-center justify-center mr-2 w-5 h-5"
+ style={{
+ // row.depth: 0이면 top-level, 1이면 그 하위 등
+ marginLeft: `${row.depth * 1.5}rem`,
+ }}
+ >
+ {row.getIsExpanded() ? (
+ <ChevronUp size={16} />
+ ) : (
+ <ChevronRight size={16} />
+ )}
+ </button>
+ )}
+
+ {/* Group Label + 값 */}
+ <span className="font-semibold">
+ {columnLabel}: {row.getValue(groupingColumnId)}
+ </span>
+ <span className="ml-2 text-xs text-muted-foreground">
+ ({row.subRows.length} rows)
+ </span>
+ </TableCell>
+ </TableRow>
+ )
+ }
+
+ // ---------------------------------------------------
+ // 2) 일반 Row
+ // → "그룹핑된 컬럼"은 숨긴다
+ // ---------------------------------------------------
+ return (
+ <TableRow
+ key={row.id}
+ data-state={row.getIsSelected() && "selected"}
+ >
+ {row.getVisibleCells().map((cell) => {
+ // 이 셀의 컬럼이 grouped라면 숨긴다
+ if (cell.column.getIsGrouped()) {
+ return null
+ }
+
+ return (
+ <TableCell
+ key={cell.id}
+ data-column-id={cell.column.id}
+ style={{
+ ...getCommonPinningStyles({ column: cell.column }),
+ width: cell.column.getSize()
+ }}
+ >
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+ </TableCell>
+ )
+ })}
+ </TableRow>
+ )
+ })
+ ) : (
+ // ---------------------------------------------------
+ // 3) 데이터가 없을 때
+ // ---------------------------------------------------
+ <TableRow>
+ <TableCell
+ colSpan={table.getAllColumns().length}
+ className="h-24 text-center"
+ >
+ No results.
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </UiTable>
+
+ {/* 리사이징 시에만 캡처 레이어 활성화 */}
+ {isResizing && (
+ <div className="fixed inset-0 cursor-col-resize select-none z-50" />
+ )}
+ </div>
+ </div>
+
+ <ClientDataTablePagination table={table} />
+ </div>
+ )
+} \ No newline at end of file