From 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Tue, 25 Mar 2025 15:55:45 +0900
Subject: initial commit

---
 components/data-table/data-table.tsx | 209 +++++++++++++++++++++++++++++++++++
 1 file changed, 209 insertions(+)
 create mode 100644 components/data-table/data-table.tsx

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

diff --git a/components/data-table/data-table.tsx b/components/data-table/data-table.tsx
new file mode 100644
index 00000000..3d01994a
--- /dev/null
+++ b/components/data-table/data-table.tsx
@@ -0,0 +1,209 @@
+"use client"
+
+import * as React from "react"
+import { flexRender, type Table as TanstackTable } from "@tanstack/react-table"
+import { ChevronRight, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { getCommonPinningStyles } from "@/lib/data-table"
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table"
+import { DataTablePagination } from "@/components/data-table/data-table-pagination"
+import { DataTableResizer } from "@/components/data-table/data-table-resizer"
+import { useAutoSizeColumns } from "@/hooks/useAutoSizeColumns"
+
+interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
+  table: TanstackTable<TData>
+  floatingBar?: React.ReactNode | null
+  autoSizeColumns?: boolean
+}
+
+/**
+ * 멀티 그룹핑 + 그룹 토글 + 그룹 컬럼/헤더 숨김 + Indent + 리사이징
+ */
+export function DataTable<TData>({
+  table,
+  floatingBar = null,
+  autoSizeColumns = true,
+  children,
+  className,
+  ...props
+}: DataTableProps<TData>) {
+
+  useAutoSizeColumns(table, autoSizeColumns)
+
+  return (
+    <div className={cn("w-full space-y-2.5 overflow-auto", className)} {...props}>
+      {children}
+      <div className="max-w-[100vw]  overflow-auto" style={{maxHeight:'36.1rem'}}>
+      <Table className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed">
+          {/* -------------------------------
+              Table Header
+              → 그룹핑된 컬럼의 헤더는 숨김 처리
+             ------------------------------- */}
+          <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}
+                      data-column-id={header.column.id} 
+                      style={{
+                        ...getCommonPinningStyles({ column: header.column }),
+                        width: header.getSize(), // 리사이징을 위한 너비 설정
+                        position: "relative" // 리사이저를 위한 포지셔닝
+                      }}
+                    >
+                      {header.isPlaceholder
+                        ? null
+                        : flexRender(
+                            header.column.columnDef.header,
+                            header.getContext()
+                          )}
+                      
+                      {/* 리사이즈 핸들 - 별도의 컴포넌트로 분리 */}
+                      {header.column.getCanResize() && (
+                        <DataTableResizer header={header} />
+                      )}
+                    </TableHead>
+                  )
+                })}
+              </TableRow>
+            ))}
+          </TableHeader>
+
+          {/* -------------------------------
+              Table Body
+             ------------------------------- */}
+          <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>
+        </Table>
+      </div>
+
+      <div className="flex flex-col gap-2.5">
+        {/* Pagination */}
+        <DataTablePagination table={table} />
+
+        {/* Floating Bar (선택된 행 있을 때) */}
+        {table.getFilteredSelectedRowModel().rows.length > 0 && floatingBar}
+      </div>
+    </div>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

