summaryrefslogtreecommitdiff
path: root/components/data-table
diff options
context:
space:
mode:
Diffstat (limited to 'components/data-table')
-rw-r--r--components/data-table/data-table-advanced-toolbar.tsx100
-rw-r--r--components/data-table/data-table-compact-toggle.tsx35
-rw-r--r--components/data-table/data-table-filter-list.tsx2
-rw-r--r--components/data-table/data-table-group-list.tsx2
-rw-r--r--components/data-table/data-table-pin-left.tsx243
-rw-r--r--components/data-table/data-table-pin-right.tsx150
-rw-r--r--components/data-table/data-table-sort-list.tsx2
-rw-r--r--components/data-table/data-table-view-options.tsx44
-rw-r--r--components/data-table/data-table.tsx75
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>