diff options
| author | 0-Zz-ang <s1998319@gmail.com> | 2025-08-04 14:59:15 +0900 |
|---|---|---|
| committer | 0-Zz-ang <s1998319@gmail.com> | 2025-08-04 14:59:15 +0900 |
| commit | 59b5715ebb3e1fd7bd4eb02ce50399715734f865 (patch) | |
| tree | 39ccd16482c1b90b6583ead73384822157254d88 /components | |
| parent | f0213de0d2fb5fcb931b3ddaddcbb6581cab5d28 (diff) | |
(박서영) docu-list-rule detail sheet 컴포넌트 추가 및 검색 필터 기능 오류 수정
Diffstat (limited to 'components')
| -rw-r--r-- | components/data-table/data-table-advanced-toolbar-detail.tsx | 118 | ||||
| -rw-r--r-- | components/data-table/data-table-detail.tsx | 226 | ||||
| -rw-r--r-- | components/data-table/data-table-global-filter-detail.tsx | 45 |
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 |
