summaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
Diffstat (limited to 'components')
-rw-r--r--components/data-table/data-table-advanced-toolbar-detail.tsx118
-rw-r--r--components/data-table/data-table-detail.tsx226
-rw-r--r--components/data-table/data-table-global-filter-detail.tsx45
3 files changed, 389 insertions, 0 deletions
diff --git a/components/data-table/data-table-advanced-toolbar-detail.tsx b/components/data-table/data-table-advanced-toolbar-detail.tsx
new file mode 100644
index 00000000..b752d575
--- /dev/null
+++ b/components/data-table/data-table-advanced-toolbar-detail.tsx
@@ -0,0 +1,118 @@
+"use client"
+
+import * as React from "react"
+import type { DataTableAdvancedFilterField } from "@/types/table"
+import { type Table } from "@tanstack/react-table"
+import { LayoutGrid, TableIcon } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+import { cn } from "@/lib/utils"
+import { DataTableFilterList } from "@/components/data-table/data-table-filter-list"
+import { DataTableSortList } from "@/components/data-table/data-table-sort-list"
+import { DataTableViewOptions } from "@/components/data-table/data-table-view-options"
+import { DataTablePinList } from "./data-table-pin"
+import { PinLeftButton } from "./data-table-pin-left"
+import { PinRightButton } from "./data-table-pin-right"
+import { DataTableGlobalFilterDetail } from "./data-table-global-filter-detail"
+import { DataTableGroupList } from "./data-table-group-list"
+
+interface DataTableAdvancedToolbarDetailProps<TData>
+ extends React.HTMLAttributes<HTMLDivElement> {
+ /**
+ * The table instance returned from useReactTable hook
+ * @type Table<TData>
+ */
+ table: Table<TData>
+
+ /**
+ * An array of filter field configurations for the data table.
+ * @type DataTableAdvancedFilterField<TData>[]
+ */
+ filterFields: DataTableAdvancedFilterField<TData>[]
+
+ /**
+ * Debounce time (ms) for filter updates to enhance performance during rapid input.
+ * @default 300
+ */
+ debounceMs?: number
+
+ /**
+ * 컴팩트 모드를 사용할지 여부 (토글 버튼을 숨기려면 null)
+ * @default true
+ */
+ enableCompactToggle?: boolean | null
+
+ /**
+ * 초기 컴팩트 모드 상태
+ * @default false
+ */
+ initialCompact?: boolean
+
+ /**
+ * 컴팩트 모드가 변경될 때 호출될 콜백 함수
+ */
+ onCompactChange?: (isCompact: boolean) => void
+}
+
+export function DataTableAdvancedToolbarDetail<TData>({
+ table,
+ filterFields = [],
+ debounceMs = 300,
+ enableCompactToggle = true,
+ initialCompact = false,
+ onCompactChange,
+ children,
+ className,
+ ...props
+}: DataTableAdvancedToolbarDetailProps<TData>) {
+ const [isCompact, setIsCompact] = React.useState(initialCompact)
+
+ const handleCompactToggle = React.useCallback(() => {
+ const newCompact = !isCompact
+ setIsCompact(newCompact)
+ onCompactChange?.(newCompact)
+ }, [isCompact, onCompactChange])
+
+ return (
+ <div
+ className={cn(
+ "flex w-full items-center justify-between gap-2 overflow-auto p-1",
+ className
+ )}
+ {...props}
+ >
+ <div className="flex items-center gap-2">
+ {enableCompactToggle !== null && (
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleCompactToggle}
+ title={isCompact ? "확장 보기로 전환" : "컴팩트 보기로 전환"}
+ className="h-8 px-2"
+ >
+ {isCompact ? <LayoutGrid size={16} /> : <TableIcon size={16} />}
+ </Button>
+ )}
+ <DataTableViewOptions table={table} />
+
+ <DataTableFilterList
+ table={table}
+ filterFields={filterFields}
+ debounceMs={debounceMs}
+ />
+
+ <DataTableSortList
+ table={table}
+ debounceMs={debounceMs}
+ />
+ <DataTableGroupList table={table} debounceMs={debounceMs} />
+ <PinLeftButton table={table} />
+ <PinRightButton table={table} />
+ <DataTableGlobalFilterDetail table={table} />
+ </div>
+ <div className="flex items-center gap-2">
+ {children}
+ </div>
+ </div>
+ )
+} \ No newline at end of file
diff --git a/components/data-table/data-table-detail.tsx b/components/data-table/data-table-detail.tsx
new file mode 100644
index 00000000..c34f9e73
--- /dev/null
+++ b/components/data-table/data-table-detail.tsx
@@ -0,0 +1,226 @@
+"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 { getCommonPinningStylesWithBorder } 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 DataTableDetailProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
+ table: TanstackTable<TData>
+ floatingBar?: React.ReactNode | null
+ autoSizeColumns?: boolean
+ compact?: boolean
+}
+
+// ✅ compactStyles를 정적으로 정의 (매번 새로 생성 방지)
+const COMPACT_STYLES = {
+ row: "h-7", // 행 높이 축소
+ cell: "py-1 px-2 text-sm", // 셀 패딩 축소 및 폰트 크기 조정
+ groupRow: "py-1 bg-muted/20 text-sm", // 그룹 행 패딩 축소
+ emptyRow: "h-16", // 데이터 없을 때 행 높이 조정
+ header: "py-1 px-2 text-sm", // 헤더 패딩 축소
+ headerHeight: "h-8", // 헤더 높이 축소
+};
+
+const NORMAL_STYLES = {
+ row: "",
+ cell: "",
+ groupRow: "bg-muted/20",
+ emptyRow: "h-24",
+ header: "",
+ headerHeight: "",
+};
+
+/**
+ * 디테일 시트 전용 DataTable - URL 상태와 연결되지 않는 독립적인 테이블
+ */
+export function DataTableDetail<TData>({
+ table,
+ floatingBar = null,
+ autoSizeColumns = true,
+ compact = false,
+ children,
+ className,
+ maxHeight,
+ ...props
+}: DataTableDetailProps<TData> & { maxHeight?: string | number }) {
+
+ useAutoSizeColumns(table, autoSizeColumns)
+
+ // ✅ compactStyles를 useMemo로 메모이제이션
+ const compactStyles = React.useMemo(() =>
+ compact ? COMPACT_STYLES : NORMAL_STYLES,
+ [compact]
+ );
+
+ return (
+ <div className={cn("w-full space-y-2.5 overflow-auto", className)} {...props}>
+ {children}
+ <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={compactStyles.headerHeight}>
+ {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={compactStyles.header}
+ style={{
+ ...getCommonPinningStylesWithBorder({
+ column: header.column,
+ isHeader: true
+ }),
+ 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={{
+ ...getCommonPinningStylesWithBorder({ column: cell.column }),
+ width: cell.column.getSize(),
+ }}
+ >
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+ </TableCell>
+ )
+ })}
+ </TableRow>
+ )
+ })
+ ) : (
+ // 데이터가 없을 때
+ <TableRow>
+ <TableCell
+ colSpan={table.getAllColumns().length}
+ className={compactStyles.emptyRow + " 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
diff --git a/components/data-table/data-table-global-filter-detail.tsx b/components/data-table/data-table-global-filter-detail.tsx
new file mode 100644
index 00000000..1db85bec
--- /dev/null
+++ b/components/data-table/data-table-global-filter-detail.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import * as React from "react"
+import { Input } from "@/components/ui/input"
+import { useDebouncedCallback } from "@/hooks/use-debounced-callback"
+import { type Table } from "@tanstack/react-table"
+
+interface DataTableGlobalFilterDetailProps<TData> {
+ table: Table<TData>
+ placeholder?: string
+ className?: string
+}
+
+/**
+ * 디테일 시트 전용 글로벌 필터 - URL 상태와 연결되지 않는 독립적인 검색
+ */
+export function DataTableGlobalFilterDetail<TData>({
+ table,
+ placeholder = "Search...",
+ className = "h-8 w-24 sm:w-40"
+}: DataTableGlobalFilterDetailProps<TData>) {
+ const [value, setValue] = React.useState("")
+
+ // Debounced callback that sets the table's global filter
+ const debouncedSetGlobalFilter = useDebouncedCallback((value: string) => {
+ table.setGlobalFilter(value)
+ }, 300)
+
+ // When user types, update local value immediately,
+ // then call the debounced function to update the table filter
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const val = e.target.value
+ setValue(val)
+ debouncedSetGlobalFilter(val)
+ }
+
+ return (
+ <Input
+ value={value}
+ onChange={handleChange}
+ placeholder={placeholder}
+ className={className}
+ />
+ )
+} \ No newline at end of file