From 37f55540833c2d5894513eca9fc8f7c6233fc2d2 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 29 May 2025 05:17:13 +0000
Subject: (대표님) 0529 14시 16분 변경사항 저장 (Vendor Data, Docu)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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

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

diff --git a/components/data-table/infinite-data-table.tsx b/components/data-table/infinite-data-table.tsx
new file mode 100644
index 00000000..fcac56ee
--- /dev/null
+++ b/components/data-table/infinite-data-table.tsx
@@ -0,0 +1,294 @@
+"use client"
+
+import * as React from "react"
+import { flexRender, type Table as TanstackTable } from "@tanstack/react-table"
+import { ChevronRight, ChevronUp, Loader2 } from "lucide-react"
+import { useIntersection } from "@mantine/hooks"
+
+import { cn } from "@/lib/utils"
+import { getCommonPinningStyles } from "@/lib/data-table"
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table"
+import { Button } from "@/components/ui/button"
+import { DataTableResizer } from "@/components/data-table/data-table-resizer"
+import { useAutoSizeColumns } from "@/hooks/useAutoSizeColumns"
+
+interface InfiniteDataTableProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
+  table: TanstackTable<TData>
+  floatingBar?: React.ReactNode | null
+  autoSizeColumns?: boolean
+  compact?: boolean
+  // 무한 스크롤 관련 props
+  hasNextPage?: boolean
+  isLoadingMore?: boolean
+  onLoadMore?: () => void
+  totalCount?: number | null
+  isEmpty?: boolean
+}
+
+/**
+ * 무한 스크롤 지원 DataTable
+ */
+export function InfiniteDataTable<TData>({
+  table,
+  floatingBar = null,
+  autoSizeColumns = true,
+  compact = false,
+  hasNextPage = false,
+  isLoadingMore = false,
+  onLoadMore,
+  totalCount = null,
+  isEmpty = false,
+  children,
+  className,
+  maxHeight,
+  ...props
+}: InfiniteDataTableProps<TData> & { maxHeight?: string | number }) {
+
+  useAutoSizeColumns(table, autoSizeColumns)
+
+  // Intersection Observer for infinite scroll
+  const { ref: loadMoreRef, entry } = useIntersection({
+    threshold: 0.1,
+  })
+
+  // 자동 로딩 트리거
+  React.useEffect(() => {
+    if (entry?.isIntersecting && hasNextPage && !isLoadingMore && onLoadMore) {
+      onLoadMore()
+    }
+  }, [entry?.isIntersecting, hasNextPage, isLoadingMore, onLoadMore])
+
+  // 컴팩트 모드를 위한 클래스 정의
+  const compactStyles = compact ? {
+    row: "h-7",
+    cell: "py-1 px-2 text-sm",
+    groupRow: "py-1 bg-muted/20 text-sm",
+    emptyRow: "h-16",
+  } : {
+    row: "",
+    cell: "",
+    groupRow: "bg-muted/20",
+    emptyRow: "h-24",
+  }
+
+  return (
+    <div className={cn("w-full space-y-2.5 overflow-auto", className)} {...props}>
+      {children}
+      
+      {/* 총 개수 표시 */}
+      {totalCount !== null && (
+        <div className="text-sm text-muted-foreground">
+          총 {totalCount.toLocaleString()}개 항목
+          {table.getRowModel().rows.length > 0 && (
+            <span className="ml-2">
+              (현재 {table.getRowModel().rows.length.toLocaleString()}개 로드됨)
+            </span>
+          )}
+        </div>
+      )}
+
+      <div 
+        className="max-w-[100vw] overflow-auto" 
+        style={{ maxHeight: maxHeight || '35rem' }}
+      >
+        <Table className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed">
+          {/* 테이블 헤더 */}
+          <TableHeader>
+            {table.getHeaderGroups().map((headerGroup) => (
+              <TableRow key={headerGroup.id} className={compact ? "h-8" : ""}>
+                {headerGroup.headers.map((header) => {
+                  if (header.column.getIsGrouped()) {
+                    return null
+                  }
+
+                  return (
+                    <TableHead
+                      key={header.id}
+                      colSpan={header.colSpan}
+                      data-column-id={header.column.id}
+                      className={compact ? "py-1 px-2 text-sm" : ""}
+                      style={{
+                        ...getCommonPinningStyles({ column: header.column }),
+                        width: header.getSize(),
+                      }}
+                    >
+                      <div style={{ position: "relative" }}>
+                        {header.isPlaceholder
+                          ? null
+                          : flexRender(
+                            header.column.columnDef.header,
+                            header.getContext()
+                          )}
+
+                        {header.column.getCanResize() && (
+                          <DataTableResizer header={header} />
+                        )}
+                      </div>
+                    </TableHead>
+                  )
+                })}
+              </TableRow>
+            ))}
+          </TableHeader>
+
+          {/* 테이블 바디 */}
+          <TableBody>
+            {table.getRowModel().rows?.length ? (
+              <>
+                {table.getRowModel().rows.map((row) => {
+                  // 그룹핑 헤더 Row
+                  if (row.getIsGrouped()) {
+                    const groupingColumnId = row.groupingColumnId ?? ""
+                    const groupingColumn = table.getColumn(groupingColumnId)
+
+                    let columnLabel = groupingColumnId
+                    if (groupingColumn) {
+                      const headerDef = groupingColumn.columnDef.meta?.excelHeader
+                      if (typeof headerDef === "string") {
+                        columnLabel = headerDef
+                      }
+                    }
+
+                    return (
+                      <TableRow
+                        key={row.id}
+                        className={compactStyles.groupRow}
+                        data-state={row.getIsExpanded() && "expanded"}
+                      >
+                        <TableCell 
+                          colSpan={table.getVisibleFlatColumns().length}
+                          className={compact ? "py-1 px-2" : ""}
+                        >
+                          {row.getCanExpand() && (
+                            <button
+                              onClick={row.getToggleExpandedHandler()}
+                              className="inline-flex items-center justify-center mr-2 w-5 h-5"
+                              style={{
+                                marginLeft: `${row.depth * 1.5}rem`,
+                              }}
+                            >
+                              {row.getIsExpanded() ? (
+                                <ChevronUp size={compact ? 14 : 16} />
+                              ) : (
+                                <ChevronRight size={compact ? 14 : 16} />
+                              )}
+                            </button>
+                          )}
+
+                          <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>
+                    )
+                  }
+
+                  // 일반 Row
+                  return (
+                    <TableRow
+                      key={row.id}
+                      className={compactStyles.row}
+                      data-state={row.getIsSelected() && "selected"}
+                    >
+                      {row.getVisibleCells().map((cell) => {
+                        if (cell.column.getIsGrouped()) {
+                          return null
+                        }
+
+                        return (
+                          <TableCell
+                            key={cell.id}
+                            data-column-id={cell.column.id}
+                            className={compactStyles.cell}
+                            style={{
+                              ...getCommonPinningStyles({ column: cell.column }),
+                              width: cell.column.getSize(),
+                            }}
+                          >
+                            {flexRender(
+                              cell.column.columnDef.cell,
+                              cell.getContext()
+                            )}
+                          </TableCell>
+                        )
+                      })}
+                    </TableRow>
+                  )
+                })}
+              </>
+            ) : isEmpty ? (
+              // 데이터가 없을 때
+              <TableRow>
+                <TableCell
+                  colSpan={table.getAllColumns().length}
+                  className={compactStyles.emptyRow + " text-center"}
+                >
+                  No results.
+                </TableCell>
+              </TableRow>
+            ) : null}
+          </TableBody>
+        </Table>
+      </div>
+
+      {/* 무한 스크롤 로딩 영역 */}
+      <div className="flex flex-col items-center space-y-4 py-4">
+        {hasNextPage && (
+          <>
+            {/* Intersection Observer 타겟 */}
+            <div ref={loadMoreRef} className="h-1" />
+            
+            {isLoadingMore && (
+              <div className="flex items-center space-x-2">
+                <Loader2 className="h-4 w-4 animate-spin" />
+                <span className="text-sm text-muted-foreground">
+                  로딩 중...
+                </span>
+              </div>
+            )}
+            
+            {/* 수동 로드 버튼 (자동 로딩 실패 시 대안) */}
+            {!isLoadingMore && onLoadMore && (
+              <Button
+                variant="outline"
+                onClick={onLoadMore}
+                className="w-full max-w-md"
+              >
+                더 보기
+              </Button>
+            )}
+          </>
+        )}
+        
+        {!hasNextPage && table.getRowModel().rows.length > 0 && (
+          <p className="text-sm text-muted-foreground">
+            모든 데이터를 불러왔습니다.
+          </p>
+        )}
+      </div>
+
+      <div className="flex flex-col gap-2.5">
+        {/* 선택된 행 정보 */}
+        {table.getFilteredSelectedRowModel().rows.length > 0 && (
+          <div className="text-sm text-muted-foreground">
+            {table.getFilteredSelectedRowModel().rows.length} of{" "}
+            {table.getRowModel().rows.length} row(s) selected.
+          </div>
+        )}
+
+        {/* Floating Bar (선택된 행 있을 때) */}
+        {table.getFilteredSelectedRowModel().rows.length > 0 && floatingBar}
+      </div>
+    </div>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

