diff options
Diffstat (limited to 'components/data-table')
| -rw-r--r-- | components/data-table/data-table-advanced-toolbar.tsx | 100 | ||||
| -rw-r--r-- | components/data-table/data-table-compact-toggle.tsx | 35 | ||||
| -rw-r--r-- | components/data-table/data-table-filter-list.tsx | 2 | ||||
| -rw-r--r-- | components/data-table/data-table-group-list.tsx | 2 | ||||
| -rw-r--r-- | components/data-table/data-table-pin-left.tsx | 243 | ||||
| -rw-r--r-- | components/data-table/data-table-pin-right.tsx | 150 | ||||
| -rw-r--r-- | components/data-table/data-table-sort-list.tsx | 2 | ||||
| -rw-r--r-- | components/data-table/data-table-view-options.tsx | 44 | ||||
| -rw-r--r-- | components/data-table/data-table.tsx | 75 |
9 files changed, 535 insertions, 118 deletions
diff --git a/components/data-table/data-table-advanced-toolbar.tsx b/components/data-table/data-table-advanced-toolbar.tsx index 7c126c51..256dc125 100644 --- a/components/data-table/data-table-advanced-toolbar.tsx +++ b/components/data-table/data-table-advanced-toolbar.tsx @@ -3,6 +3,8 @@ 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" @@ -14,6 +16,36 @@ import { PinRightButton } from "./data-table-pin-right" import { DataTableGlobalFilter } from "./data-table-grobal-filter" import { DataTableGroupList } from "./data-table-group-list" +// 로컬 스토리지 사용을 위한 훅 +const useLocalStorage = <T,>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] => { + const [storedValue, setStoredValue] = React.useState<T>(() => { + if (typeof window === "undefined") { + return initialValue + } + try { + const item = window.localStorage.getItem(key) + return item ? JSON.parse(item) : initialValue + } catch (error) { + console.error(error) + return initialValue + } + }) + + const setValue = (value: T | ((val: T) => T)) => { + try { + const valueToStore = value instanceof Function ? value(storedValue) : value + setStoredValue(valueToStore) + if (typeof window !== "undefined") { + window.localStorage.setItem(key, JSON.stringify(valueToStore)) + } + } catch (error) { + console.error(error) + } + } + + return [storedValue, setValue] +} + interface DataTableAdvancedToolbarProps<TData> extends React.HTMLAttributes<HTMLDivElement> { /** @@ -58,6 +90,29 @@ interface DataTableAdvancedToolbarProps<TData> * @default true */ shallow?: boolean + + /** + * 컴팩트 모드를 사용할지 여부 (토글 버튼을 숨기려면 null) + * @default true + */ + enableCompactToggle?: boolean | null + + /** + * 초기 컴팩트 모드 상태 + * @default false + */ + initialCompact?: boolean + + /** + * 컴팩트 모드가 변경될 때 호출될 콜백 함수 + */ + onCompactChange?: (isCompact: boolean) => void + + /** + * 컴팩트 모드 상태를 저장할 로컬 스토리지 키 + * @default "dataTableCompact" + */ + compactStorageKey?: string } export function DataTableAdvancedToolbar<TData>({ @@ -65,10 +120,30 @@ export function DataTableAdvancedToolbar<TData>({ filterFields = [], debounceMs = 300, shallow = true, + enableCompactToggle = true, + initialCompact = false, + onCompactChange, + compactStorageKey = "dataTableCompact", children, className, ...props }: DataTableAdvancedToolbarProps<TData>) { + // 컴팩트 모드 상태 관리 + const [isCompact, setIsCompact] = useLocalStorage<boolean>( + compactStorageKey, + initialCompact + ) + + // 컴팩트 모드 변경 시 콜백 호출 + React.useEffect(() => { + onCompactChange?.(isCompact) + }, [isCompact, onCompactChange]) + + // 컴팩트 모드 토글 핸들러 + const handleToggleCompact = React.useCallback(() => { + setIsCompact(prev => !prev) + }, [setIsCompact]) + return ( <div className={cn( @@ -78,7 +153,19 @@ export function DataTableAdvancedToolbar<TData>({ {...props} > <div className="flex items-center gap-2"> - <DataTableViewOptions table={table} /> + {enableCompactToggle && ( + <Button + variant="outline" + size="sm" + onClick={handleToggleCompact} + title={isCompact ? "확장 보기로 전환" : "컴팩트 보기로 전환"} + className="h-8 px-2" + > + {isCompact ? <LayoutGrid size={16} /> : <TableIcon size={16} />} + {/* <span className="ml-2 text-xs">{isCompact ? "확장 보기" : "컴팩트 보기"}</span> */} + </Button> + )} + <DataTableViewOptions table={table} /> <DataTableFilterList table={table} filterFields={filterFields} @@ -90,15 +177,16 @@ export function DataTableAdvancedToolbar<TData>({ debounceMs={debounceMs} shallow={shallow} /> - <DataTableGroupList table={table} debounceMs={debounceMs}/> - <PinLeftButton table={table}/> - <PinRightButton table={table}/> + <DataTableGroupList table={table} debounceMs={debounceMs} /> + <PinLeftButton table={table} /> + <PinRightButton table={table} /> <DataTableGlobalFilter /> </div> <div className="flex items-center gap-2"> - {children} + {/* 컴팩트 모드 토글 버튼 */} + {children} </div> </div> ) -} +}
\ No newline at end of file diff --git a/components/data-table/data-table-compact-toggle.tsx b/components/data-table/data-table-compact-toggle.tsx new file mode 100644 index 00000000..5c162a03 --- /dev/null +++ b/components/data-table/data-table-compact-toggle.tsx @@ -0,0 +1,35 @@ +"use client" + +import * as React from "react" +import { Button } from "@/components/ui/button" +import { TableIcon, LayoutGrid } from "lucide-react" + +interface DataTableCompactToggleProps { + /** + * 현재 컴팩트 모드 상태 + */ + isCompact: boolean + + /** + * 컴팩트 모드 토글 시 호출될 함수 + */ + onToggleCompact: () => void +} + +export function DataTableCompactToggle({ + isCompact, + onToggleCompact +}: DataTableCompactToggleProps) { + return ( + <Button + variant="ghost" + size="sm" + onClick={onToggleCompact} + title={isCompact ? "확장 보기로 전환" : "컴팩트 보기로 전환"} + className="h-8 px-2" + > + {isCompact ? <LayoutGrid size={16} /> : <TableIcon size={16} />} + <span className="ml-2 text-xs">{isCompact ? "확장 보기" : "컴팩트 보기"}</span> + </Button> + ) +}
\ No newline at end of file diff --git a/components/data-table/data-table-filter-list.tsx b/components/data-table/data-table-filter-list.tsx index c51d4374..db9f8af9 100644 --- a/components/data-table/data-table-filter-list.tsx +++ b/components/data-table/data-table-filter-list.tsx @@ -82,7 +82,7 @@ export function DataTableFilterList<TData>({ }: DataTableFilterListProps<TData>) { const params = useParams(); - const lng = params.lng as string; + const lng = params ? (params.lng as string) : 'en'; const { t, i18n } = useTranslation(lng); diff --git a/components/data-table/data-table-group-list.tsx b/components/data-table/data-table-group-list.tsx index cde1cadd..fcae9a79 100644 --- a/components/data-table/data-table-group-list.tsx +++ b/components/data-table/data-table-group-list.tsx @@ -156,7 +156,7 @@ export function DataTableGroupList<TData>({ aria-controls={`${id}-group-dialog`} > <Layers className="size-3" aria-hidden="true" /> - <span className="hidden sm:inline">Group</span> + <span className="hidden sm:inline">그룹</span> {uniqueGrouping.length > 0 && ( <Badge variant="secondary" diff --git a/components/data-table/data-table-pin-left.tsx b/components/data-table/data-table-pin-left.tsx index 81e83564..e79e01eb 100644 --- a/components/data-table/data-table-pin-left.tsx +++ b/components/data-table/data-table-pin-left.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { type Table } from "@tanstack/react-table" +import { type Column, type Table } from "@tanstack/react-table" import { Check, ChevronsUpDown, MoveLeft } from "lucide-react" import { cn, toSentenceCase } from "@/lib/utils" @@ -13,6 +13,7 @@ import { CommandInput, CommandItem, CommandList, + CommandSeparator, } from "@/components/ui/command" import { Popover, @@ -21,14 +22,152 @@ import { } from "@/components/ui/popover" /** - * “Pin Left” Popover. Lists columns that can be pinned. - * If pinned===‘left’ → checked, if pinned!==‘left’ → unchecked. - * Toggling check => pin(‘left’) or pin(false). + * Helper function to check if a column is a parent column (has subcolumns) + */ +function isParentColumn<TData>(column: Column<TData>): boolean { + return column.columns && column.columns.length > 0 +} + +/** + * Helper function to pin all subcolumns of a parent column + */ +function pinSubColumns<TData>( + column: Column<TData>, + pinType: false | "left" | "right" +): void { + // If this is a parent column, pin all its subcolumns + if (isParentColumn(column)) { + column.columns.forEach((subColumn) => { + // Recursively handle nested columns + pinSubColumns(subColumn, pinType) + }) + } else { + // For leaf columns, apply the pin if possible + if (column.getCanPin?.()) { + column.pin?.(pinType) + } + } +} + +/** + * Checks if all subcolumns of a parent column are pinned to the specified side + */ +function areAllSubColumnsPinned<TData>( + column: Column<TData>, + pinType: "left" | "right" +): boolean { + if (isParentColumn(column)) { + // Check if all subcolumns are pinned + return column.columns.every((subColumn) => + areAllSubColumnsPinned(subColumn, pinType) + ) + } else { + // For leaf columns, check if it's pinned to the specified side + return column.getIsPinned?.() === pinType + } +} + +/** + * Helper function to get the display name of a column + */ +function getColumnDisplayName<TData>(column: Column<TData>): string { + // First try to use excelHeader from meta if available + const excelHeader = column.columnDef.meta?.excelHeader + if (excelHeader) { + return excelHeader + } + + // Fall back to converting the column ID to sentence case + return toSentenceCase(column.id) +} + +/** + * Array of column IDs that should be auto-pinned to the left when available + */ +const AUTO_PIN_LEFT_COLUMNS = ['select'] + +/** + * "Pin Left" Popover. Supports pinning both individual columns and header groups. */ export function PinLeftButton<TData>({ table }: { table: Table<TData> }) { const [open, setOpen] = React.useState(false) const triggerRef = React.useRef<HTMLButtonElement>(null) + // Try to auto-pin select and action columns if they exist + React.useEffect(() => { + AUTO_PIN_LEFT_COLUMNS.forEach((columnId) => { + const column = table.getColumn(columnId) + if (column?.getCanPin?.()) { + column.pin?.("left") + } + }) + }, [table]) + + // Get all columns that can be pinned (excluding auto-pinned columns) + const pinnableColumns = React.useMemo(() => { + return table.getAllColumns().filter((column) => { + // Skip auto-pinned columns + if (AUTO_PIN_LEFT_COLUMNS.includes(column.id)) { + return false + } + + // If it's a leaf column, check if it can be pinned + if (!isParentColumn(column)) { + return column.getCanPin?.() + } + + // If it's a parent column, check if at least one subcolumn can be pinned + return column.columns.some((subCol) => { + if (isParentColumn(subCol)) { + // Recursively check nested columns + return subCol.columns.some(c => c.getCanPin?.()) + } + return subCol.getCanPin?.() + }) + }) + }, [table]) + + // Get flat list of all leaf columns for display + const allPinnableLeafColumns = React.useMemo(() => { + const leafColumns: Column<TData>[] = [] + + // Function to recursively collect leaf columns + const collectLeafColumns = (column: Column<TData>) => { + if (isParentColumn(column)) { + column.columns.forEach(collectLeafColumns) + } else if (column.getCanPin?.() && !AUTO_PIN_LEFT_COLUMNS.includes(column.id)) { + leafColumns.push(column) + } + } + + // Process all columns + table.getAllColumns().forEach(collectLeafColumns) + + return leafColumns + }, [table]) + + // Handle column pinning + const handleColumnPin = React.useCallback((column: Column<TData>) => { + // For parent columns, pin/unpin all subcolumns + if (isParentColumn(column)) { + const allPinned = areAllSubColumnsPinned(column, "left") + pinSubColumns(column, allPinned ? false : "left") + } else { + // For leaf columns, toggle pin state + const isPinned = column.getIsPinned?.() === "left" + column.pin?.(isPinned ? false : "left") + } + }, []) + + // Check if a column or its subcolumns are pinned left + const isColumnPinned = React.useCallback((column: Column<TData>): boolean => { + if (isParentColumn(column)) { + return areAllSubColumnsPinned(column, "left") + } else { + return column.getIsPinned?.() === "left" + } + }, []) + return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> @@ -39,53 +178,105 @@ export function PinLeftButton<TData>({ table }: { table: Table<TData> }) { className="h-8 gap-2" > <MoveLeft className="size-4" /> - + <span className="hidden sm:inline"> - Left + 왼쪽 고정 </span> - <ChevronsUpDown className="ml-1 size-4 opacity-50 hidden sm:inline" /> </Button> </PopoverTrigger> - + <PopoverContent align="end" - className="w-44 p-0" + className="w-56 p-0" onCloseAutoFocus={() => triggerRef.current?.focus()} > <Command> <CommandInput placeholder="Search columns..." /> - <CommandList> + <CommandList className="max-h-[300px]"> <CommandEmpty>No columns found.</CommandEmpty> <CommandGroup> - {table - .getAllLeafColumns() - .filter((col) => col.getCanPin?.()) - .map((column) => { - const pinned = column.getIsPinned?.() // 'left'|'right'|false - // => pinned === 'left' => checked - return ( + {/* Parent Columns with subcolumns */} + {pinnableColumns + .filter(isParentColumn) + .map((parentColumn) => ( + <React.Fragment key={parentColumn.id}> + {/* Parent column header - can pin/unpin all children at once */} <CommandItem - key={column.id} onSelect={() => { - // if currently pinned===left => unpin - // else => pin left - column.pin?.(pinned === "left" ? false : "left") + handleColumnPin(parentColumn) }} + className="font-medium bg-muted/50" > <span className="truncate"> - {toSentenceCase(column.id)} + {getColumnDisplayName(parentColumn)} </span> - {/* Check if pinned===‘left’ */} <Check className={cn( "ml-auto size-4 shrink-0", - pinned === "left" ? "opacity-100" : "opacity-0" + isColumnPinned(parentColumn) ? "opacity-100" : "opacity-0" )} /> </CommandItem> - ) - })} + + {/* Individual subcolumns */} + {parentColumn.columns + .filter(col => !isParentColumn(col) && col.getCanPin?.()) + .map(subColumn => ( + <CommandItem + key={subColumn.id} + onSelect={() => { + handleColumnPin(subColumn) + }} + className="pl-6 text-sm" + > + <span className="truncate"> + {getColumnDisplayName(subColumn)} + </span> + <Check + className={cn( + "ml-auto size-4 shrink-0", + isColumnPinned(subColumn) ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </React.Fragment> + ))} + </CommandGroup> + + {/* Separator if we have both parent columns and standalone leaf columns */} + {pinnableColumns.some(isParentColumn) && + allPinnableLeafColumns.some(col => !pinnableColumns.find(parent => + isParentColumn(parent) && parent.columns.includes(col)) + ) && ( + <CommandSeparator /> + )} + + {/* Standalone leaf columns (not part of any parent column group) */} + <CommandGroup> + {allPinnableLeafColumns + .filter(col => !pinnableColumns.find(parent => + isParentColumn(parent) && parent.columns.includes(col) + )) + .map((column) => ( + <CommandItem + key={column.id} + onSelect={() => { + handleColumnPin(column) + }} + > + <span className="truncate"> + {getColumnDisplayName(column)} + </span> + <Check + className={cn( + "ml-auto size-4 shrink-0", + isColumnPinned(column) ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} </CommandGroup> </CommandList> </Command> diff --git a/components/data-table/data-table-pin-right.tsx b/components/data-table/data-table-pin-right.tsx index 3ed42402..ad52e44d 100644 --- a/components/data-table/data-table-pin-right.tsx +++ b/components/data-table/data-table-pin-right.tsx @@ -68,15 +68,48 @@ function areAllSubColumnsPinned<TData>( } /** + * Helper function to get the display name of a column + */ +function getColumnDisplayName<TData>(column: Column<TData>): string { + // First try to use excelHeader from meta if available + const excelHeader = column.columnDef.meta?.excelHeader + if (excelHeader) { + return excelHeader + } + + // Fall back to converting the column ID to sentence case + return toSentenceCase(column.id) +} + +/** + * Array of column IDs that should be auto-pinned to the right when available + */ +const AUTO_PIN_RIGHT_COLUMNS = ['actions'] + +/** * "Pin Right" Popover. Supports pinning both individual columns and header groups. */ export function PinRightButton<TData>({ table }: { table: Table<TData> }) { const [open, setOpen] = React.useState(false) const triggerRef = React.useRef<HTMLButtonElement>(null) - // Get all columns that can be pinned, including parent columns + // Try to auto-pin actions columns if they exist + React.useEffect(() => { + AUTO_PIN_RIGHT_COLUMNS.forEach((columnId) => { + const column = table.getColumn(columnId) + if (column?.getCanPin?.()) { + column.pin?.("right") + } + }) + }, [table]) + // Get all columns that can be pinned (excluding auto-pinned columns) const pinnableColumns = React.useMemo(() => { return table.getAllColumns().filter((column) => { + // Skip auto-pinned columns + if (AUTO_PIN_RIGHT_COLUMNS.includes(column.id)) { + return false + } + // If it's a leaf column, check if it can be pinned if (!isParentColumn(column)) { return column.getCanPin?.() @@ -93,6 +126,25 @@ export function PinRightButton<TData>({ table }: { table: Table<TData> }) { }) }, [table]) + // Get flat list of all leaf columns for display + const allPinnableLeafColumns = React.useMemo(() => { + const leafColumns: Column<TData>[] = [] + + // Function to recursively collect leaf columns + const collectLeafColumns = (column: Column<TData>) => { + if (isParentColumn(column)) { + column.columns.forEach(collectLeafColumns) + } else if (column.getCanPin?.() && !AUTO_PIN_RIGHT_COLUMNS.includes(column.id)) { + leafColumns.push(column) + } + } + + // Process all columns + table.getAllColumns().forEach(collectLeafColumns) + + return leafColumns + }, [table]) + // Handle column pinning const handleColumnPin = React.useCallback((column: Column<TData>) => { // For parent columns, pin/unpin all subcolumns @@ -127,7 +179,7 @@ export function PinRightButton<TData>({ table }: { table: Table<TData> }) { <MoveRight className="size-4" /> <span className="hidden sm:inline"> - Right + 오른 고정 </span> <ChevronsUpDown className="ml-1 size-4 opacity-50 hidden sm:inline" /> </Button> @@ -140,42 +192,72 @@ export function PinRightButton<TData>({ table }: { table: Table<TData> }) { > <Command> <CommandInput placeholder="Search columns..." /> - <CommandList> + <CommandList className="max-h-[300px]"> <CommandEmpty>No columns found.</CommandEmpty> <CommandGroup> - {/* Header Columns (Parent Columns) */} + {/* Parent Columns with subcolumns */} {pinnableColumns .filter(isParentColumn) - .map((column) => ( - <CommandItem - key={column.id} - onSelect={() => { - handleColumnPin(column) - }} - className="font-medium" - > - <span className="truncate"> - {column.id === "Basic Info" || column.id === "Metadata" - ? column.id // Use column ID directly for common groups - : toSentenceCase(column.id)} - </span> - <Check - className={cn( - "ml-auto size-4 shrink-0", - isColumnPinned(column) ? "opacity-100" : "opacity-0" - )} - /> - </CommandItem> + .map((parentColumn) => ( + <React.Fragment key={parentColumn.id}> + {/* Parent column header - can pin/unpin all children at once */} + <CommandItem + onSelect={() => { + handleColumnPin(parentColumn) + }} + className="font-medium bg-muted/50" + > + <span className="truncate"> + {getColumnDisplayName(parentColumn)} + </span> + <Check + className={cn( + "ml-auto size-4 shrink-0", + isColumnPinned(parentColumn) ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + + {/* Individual subcolumns */} + {parentColumn.columns + .filter(col => !isParentColumn(col) && col.getCanPin?.()) + .map(subColumn => ( + <CommandItem + key={subColumn.id} + onSelect={() => { + handleColumnPin(subColumn) + }} + className="pl-6 text-sm" + > + <span className="truncate"> + {getColumnDisplayName(subColumn)} + </span> + <Check + className={cn( + "ml-auto size-4 shrink-0", + isColumnPinned(subColumn) ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </React.Fragment> ))} - - {pinnableColumns.some(isParentColumn) && - pinnableColumns.some(col => !isParentColumn(col)) && ( - <CommandSeparator /> - )} - - {/* Leaf Columns (individual columns) */} - {pinnableColumns - .filter(col => !isParentColumn(col)) + </CommandGroup> + + {/* Separator if we have both parent columns and standalone leaf columns */} + {pinnableColumns.some(isParentColumn) && + allPinnableLeafColumns.some(col => !pinnableColumns.find(parent => + isParentColumn(parent) && parent.columns.includes(col)) + ) && ( + <CommandSeparator /> + )} + + {/* Standalone leaf columns (not part of any parent column group) */} + <CommandGroup> + {allPinnableLeafColumns + .filter(col => !pinnableColumns.find(parent => + isParentColumn(parent) && parent.columns.includes(col) + )) .map((column) => ( <CommandItem key={column.id} @@ -184,7 +266,7 @@ export function PinRightButton<TData>({ table }: { table: Table<TData> }) { }} > <span className="truncate"> - {toSentenceCase(column.id)} + {getColumnDisplayName(column)} </span> <Check className={cn( diff --git a/components/data-table/data-table-sort-list.tsx b/components/data-table/data-table-sort-list.tsx index 686545fc..c3c537ac 100644 --- a/components/data-table/data-table-sort-list.tsx +++ b/components/data-table/data-table-sort-list.tsx @@ -167,7 +167,7 @@ export function DataTableSortList<TData>({ <ArrowDownUp className="size-3" aria-hidden="true" /> <span className="hidden sm:inline"> - Sort + 정렬 </span> {uniqueSorting.length > 0 && ( diff --git a/components/data-table/data-table-view-options.tsx b/components/data-table/data-table-view-options.tsx index c55617ec..69666237 100644 --- a/components/data-table/data-table-view-options.tsx +++ b/components/data-table/data-table-view-options.tsx @@ -24,6 +24,12 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" // Sortable import { @@ -36,9 +42,11 @@ import { /** * ViewOptionsProps: * - table: TanStack Table instance + * - resetAutoSize: Function to reset autosize calculations (optional) */ interface DataTableViewOptionsProps<TData> { table: Table<TData> + resetAutoSize?: () => void } declare module "@tanstack/table-core" { @@ -56,13 +64,12 @@ declare module "@tanstack/table-core" { */ export function DataTableViewOptions<TData>({ table, + resetAutoSize, }: DataTableViewOptionsProps<TData>) { const triggerRef = React.useRef<HTMLButtonElement>(null) // 1) Identify columns that can be hidden const hideableCols = React.useMemo(() => { - - return table .getAllLeafColumns() .filter((col) => col.getCanHide()) @@ -103,7 +110,10 @@ export function DataTableViewOptions<TData>({ // Now we set the table's official column order table.setColumnOrder(finalOrder) - }, [columnOrder, hideableCols, table]) + + // Reset auto-size when column order changes + resetAutoSize?.() + }, [columnOrder, hideableCols, table, resetAutoSize]) return ( @@ -118,7 +128,7 @@ export function DataTableViewOptions<TData>({ className="gap-2" > <Settings2 className="size-4" /> - <span className="hidden sm:inline">View</span> + <span className="hidden sm:inline">보기</span> <ChevronsUpDown className="ml-auto size-4 shrink-0 opacity-50 hidden sm:inline" /> </Button> </PopoverTrigger> @@ -145,16 +155,19 @@ export function DataTableViewOptions<TData>({ {columnOrder.map((colId) => { // find column instance const column = hideableCols.find((c) => c.id === colId) - if (!column) return null + const columnLabel = column?.columnDef?.meta?.excelHeader || column.id + return ( <SortableItem key={colId} value={colId} asChild> <CommandItem - onSelect={() => + onSelect={() => { column.toggleVisibility(!column.getIsVisible()) - } + // Reset autosize calculations when toggling columns + resetAutoSize?.() + }} > {/* Drag handle on the left */} <SortableDragHandle @@ -165,10 +178,19 @@ export function DataTableViewOptions<TData>({ <GripVertical className="size-3.5" aria-hidden="true" /> </SortableDragHandle> - {/* label */} - <span className="truncate"> - {column?.columnDef?.meta?.excelHeader} - </span> + {/* label with tooltip for long names */} + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <span className="truncate"> + {columnLabel} + </span> + </TooltipTrigger> + <TooltipContent> + {columnLabel} + </TooltipContent> + </Tooltip> + </TooltipProvider> {/* check if visible */} <Check diff --git a/components/data-table/data-table.tsx b/components/data-table/data-table.tsx index b1027cc0..5aeefe21 100644 --- a/components/data-table/data-table.tsx +++ b/components/data-table/data-table.tsx @@ -22,15 +22,17 @@ interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivElement> { table: TanstackTable<TData> floatingBar?: React.ReactNode | null autoSizeColumns?: boolean + compact?: boolean // 컴팩트 모드 옵션 추가 } /** - * 멀티 그룹핑 + 그룹 토글 + 그룹 컬럼/헤더 숨김 + Indent + 리사이징 + * 멀티 그룹핑 + 그룹 토글 + 그룹 컬럼/헤더 숨김 + Indent + 리사이징 + 컴팩트 모드 */ export function DataTable<TData>({ table, floatingBar = null, autoSizeColumns = true, + compact = false, // 기본값은 false로 설정 children, className, ...props @@ -38,20 +40,29 @@ export function DataTable<TData>({ useAutoSizeColumns(table, autoSizeColumns) + // 컴팩트 모드를 위한 클래스 정의 + 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} - <div className="max-w-[100vw] overflow-auto" style={{ maxHeight: '36.1rem' }}> + <div className="max-w-[100vw] overflow-auto" style={{ maxHeight: '35rem' }}> <Table className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed"> - {/* ------------------------------- - Table Header - → 그룹핑된 컬럼의 헤더는 숨김 처리 - ------------------------------- */} + {/* 테이블 헤더 */} <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( - <TableRow key={headerGroup.id}> + <TableRow key={headerGroup.id} className={compact ? "h-8" : ""}> {headerGroup.headers.map((header) => { - // 만약 이 컬럼이 현재 "그룹핑" 상태라면 헤더도 표시하지 않음 if (header.column.getIsGrouped()) { return null } @@ -61,10 +72,10 @@ export function DataTable<TData>({ 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(), // 리사이징을 위한 너비 설정 - // position: "relative" // 리사이저를 위한 포지셔닝 + width: header.getSize(), }} > <div style={{ position: "relative" }}> @@ -75,7 +86,6 @@ export function DataTable<TData>({ header.getContext() )} - {/* 리사이즈 핸들 - 별도의 컴포넌트로 분리 */} {header.column.getCanResize() && ( <DataTableResizer header={header} /> )} @@ -87,21 +97,15 @@ export function DataTable<TData>({ ))} </TableHeader> - {/* ------------------------------- - Table Body - ------------------------------- */} + {/* 테이블 바디 */} <TableBody> {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => { - // --------------------------------------------------- - // 1) "그룹핑 헤더" Row인지 확인 - // --------------------------------------------------- + // 그룹핑 헤더 Row if (row.getIsGrouped()) { - // row.groupingColumnId로 어떤 컬럼을 기준으로 그룹화 되었는지 알 수 있음 const groupingColumnId = row.groupingColumnId ?? "" - const groupingColumn = table.getColumn(groupingColumnId) // 해당 column 객체 + const groupingColumn = table.getColumn(groupingColumnId) - // 컬럼 라벨 가져오기 let columnLabel = groupingColumnId if (groupingColumn) { const headerDef = groupingColumn.columnDef.meta?.excelHeader @@ -113,30 +117,29 @@ export function DataTable<TData>({ return ( <TableRow key={row.id} - className="bg-muted/20" + className={compactStyles.groupRow} data-state={row.getIsExpanded() && "expanded"} > - {/* 그룹 헤더는 한 줄에 합쳐서 보여주고, 토글 버튼 + 그룹 라벨 + 값 표기 */} - <TableCell colSpan={table.getVisibleFlatColumns().length}> - {/* 확장/축소 버튼 (아이콘 중앙 정렬 + Indent) */} + <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={{ - // row.depth: 0이면 top-level, 1이면 그 하위 등 marginLeft: `${row.depth * 1.5}rem`, }} > {row.getIsExpanded() ? ( - <ChevronUp size={16} /> + <ChevronUp size={compact ? 14 : 16} /> ) : ( - <ChevronRight size={16} /> + <ChevronRight size={compact ? 14 : 16} /> )} </button> )} - {/* Group Label + 값 */} <span className="font-semibold"> {columnLabel}: {row.getValue(groupingColumnId)} </span> @@ -148,17 +151,14 @@ export function DataTable<TData>({ ) } - // --------------------------------------------------- - // 2) 일반 Row - // → "그룹핑된 컬럼"은 숨긴다 - // --------------------------------------------------- + // 일반 Row return ( <TableRow key={row.id} + className={compactStyles.row} data-state={row.getIsSelected() && "selected"} > {row.getVisibleCells().map((cell) => { - // 이 셀의 컬럼이 grouped라면 숨긴다 if (cell.column.getIsGrouped()) { return null } @@ -167,9 +167,10 @@ export function DataTable<TData>({ <TableCell key={cell.id} data-column-id={cell.column.id} + className={compactStyles.cell} style={{ ...getCommonPinningStyles({ column: cell.column }), - width: cell.column.getSize(), // 리사이징을 위한 너비 설정 + width: cell.column.getSize(), }} > {flexRender( @@ -183,13 +184,11 @@ export function DataTable<TData>({ ) }) ) : ( - // --------------------------------------------------- - // 3) 데이터가 없을 때 - // --------------------------------------------------- + // 데이터가 없을 때 <TableRow> <TableCell colSpan={table.getAllColumns().length} - className="h-24 text-center" + className={compactStyles.emptyRow + " text-center"} > No results. </TableCell> |
