summaryrefslogtreecommitdiff
path: root/components/data-table/expandable-data-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/data-table/expandable-data-table.tsx')
-rw-r--r--components/data-table/expandable-data-table.tsx707
1 files changed, 486 insertions, 221 deletions
diff --git a/components/data-table/expandable-data-table.tsx b/components/data-table/expandable-data-table.tsx
index 9005e2fb..112e9448 100644
--- a/components/data-table/expandable-data-table.tsx
+++ b/components/data-table/expandable-data-table.tsx
@@ -5,7 +5,7 @@ import { flexRender, type Table as TanstackTable } from "@tanstack/react-table"
import { ChevronRight, ChevronUp, ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
-import { getCommonPinningStyles } from "@/lib/data-table"
+import { getCommonPinningStylesWithBorder, debugPinningInfo } from "@/lib/data-table"
import {
Table,
TableBody,
@@ -26,18 +26,13 @@ interface ExpandableDataTableProps<TData> extends React.HTMLAttributes<HTMLDivEl
expandedRows?: Set<string>
setExpandedRows?: React.Dispatch<React.SetStateAction<Set<string>>>
renderExpandedContent?: (row: TData) => React.ReactNode
- expandable?: boolean // 확장 기능 활성화 여부
+ expandable?: boolean
maxHeight?: string | number
- expandedRowClassName?: string // 확장된 행의 커스텀 클래스
+ expandedRowClassName?: string
+ debug?: boolean // 디버깅 옵션 추가
+ simpleExpansion?: boolean // 🎯 간단한 확장 방식 선택 옵션 추가
}
-/**
- * 확장 가능한 데이터 테이블 - 행 확장 시 바로 아래에 컨텐츠 표시
- * 개선사항:
- * - 가로스크롤과 확장된 내용 독립성 보장
- * - 동적 높이 계산으로 세로스크롤 문제 해결
- * - 향상된 접근성 및 키보드 네비게이션
- */
export function ExpandableDataTable<TData>({
table,
floatingBar = null,
@@ -49,6 +44,8 @@ export function ExpandableDataTable<TData>({
expandable = false,
maxHeight,
expandedRowClassName,
+ debug = false,
+ simpleExpansion = false, // 🎯 기본값 false (전체 확장 방식)
children,
className,
...props
@@ -56,11 +53,228 @@ export function ExpandableDataTable<TData>({
useAutoSizeColumns(table, autoSizeColumns)
- // 스크롤 컨테이너 참조
- const scrollContainerRef = React.useRef<HTMLDivElement>(null)
+ const containerRef = React.useRef<HTMLDivElement>(null)
+ const scrollRef = React.useRef<HTMLDivElement>(null) // 🎯 스크롤 컨테이너 ref 추가
const [expandedHeights, setExpandedHeights] = React.useState<Map<string, number>>(new Map())
+ const [isScrolled, setIsScrolled] = React.useState(false)
+ const [isScrolledToEnd, setIsScrolledToEnd] = React.useState(true) // 🎯 초기값을 true로 설정 (아직 계산 안됨)
+ const [isInitialized, setIsInitialized] = React.useState(false) // 🎯 초기화 상태 추가
+ const [containerWidth, setContainerWidth] = React.useState<number>(0)
- // 행 확장/축소 핸들러 (개선된 버전)
+ // 🎯 컨테이너 너비 감지 (패딩 제외된 실제 사용 가능한 너비)
+ React.useEffect(() => {
+ if (!containerRef.current) return
+
+ const updateContainerWidth = () => {
+ if (containerRef.current) {
+ // clientWidth는 패딩을 제외한 실제 사용 가능한 너비
+ setContainerWidth(containerRef.current.clientWidth)
+ }
+ }
+
+ const resizeObserver = new ResizeObserver(updateContainerWidth)
+ resizeObserver.observe(containerRef.current)
+
+ // 초기 너비 설정
+ updateContainerWidth()
+
+ return () => resizeObserver.disconnect()
+ }, [])
+
+ // 🎯 초기 스크롤 상태 체크 (더 확실한 초기화)
+ React.useEffect(() => {
+ if (!scrollRef.current) return
+
+ const checkScrollState = () => {
+ if (scrollRef.current) {
+ const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current
+ const newIsScrolled = scrollLeft > 0
+ const newIsScrolledToEnd = Math.abs(scrollWidth - clientWidth - scrollLeft) < 1
+
+ setIsScrolled(newIsScrolled)
+ setIsScrolledToEnd(newIsScrolledToEnd)
+ setIsInitialized(true)
+
+ if (debug) {
+ console.log('Initial scroll check:', {
+ scrollLeft,
+ scrollWidth,
+ clientWidth,
+ newIsScrolled,
+ newIsScrolledToEnd,
+ canScroll: scrollWidth > clientWidth
+ })
+ }
+ }
+ }
+
+ // 즉시 체크
+ checkScrollState()
+
+ // 짧은 지연 후 재체크 (테이블 렌더링 완료 대기)
+ const timeouts = [50, 100, 200].map(delay =>
+ setTimeout(checkScrollState, delay)
+ )
+
+ // ResizeObserver로 테이블 크기 변화 감지
+ const resizeObserver = new ResizeObserver(() => {
+ setTimeout(checkScrollState, 10)
+ })
+ resizeObserver.observe(scrollRef.current)
+
+ return () => {
+ timeouts.forEach(clearTimeout)
+ resizeObserver.disconnect()
+ }
+ }, [table, debug])
+
+ // 🎯 데이터 변경 시 스크롤 상태 재설정
+ React.useEffect(() => {
+ setIsInitialized(false) // 🎯 데이터 변경 시 초기화 상태 리셋
+ setIsScrolledToEnd(true) // 🎯 안전한 기본값으로 리셋
+ }, [table.getRowModel().rows.length])
+
+ const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
+ const scrollLeft = e.currentTarget.scrollLeft
+ const scrollWidth = e.currentTarget.scrollWidth
+ const clientWidth = e.currentTarget.clientWidth
+
+ // 🎯 왼쪽으로부터 스크롤 상태 (왼쪽 핀용)
+ setIsScrolled(scrollLeft > 0)
+
+ // 🎯 오른쪽 끝까지 스크롤 상태 (오른쪽 핀용)
+ const isAtEnd = Math.abs(scrollWidth - clientWidth - scrollLeft) < 1 // 1px 오차 허용
+ setIsScrolledToEnd(isAtEnd)
+ setIsInitialized(true) // 🎯 스크롤 이벤트 발생 시 초기화 완료
+
+ if (debug) {
+ console.log('Scroll state:', {
+ scrollLeft,
+ scrollWidth,
+ clientWidth,
+ isScrolled: scrollLeft > 0,
+ isScrolledToEnd: isAtEnd,
+ remainingScroll: scrollWidth - clientWidth - scrollLeft
+ })
+ }
+ }
+
+ // 🔧 개선된 핀 스타일 함수 (좌/우 핀 구분 처리)
+ const getPinnedStyle = React.useCallback((column: any, isHeader: boolean = false) => {
+ if (debug) {
+ debugPinningInfo(column)
+ }
+
+ try {
+ const baseStyle = getCommonPinningStylesWithBorder({
+ column,
+ withBorder: true
+ })
+
+ const pinnedSide = column.getIsPinned()
+
+ if (!pinnedSide) {
+ // width를 제외한 나머지 스타일만 반환
+ const { width, ...restStyle } = baseStyle
+ // 헤더인 경우 핀되지 않았어도 배경 필요 (sticky 때문에)
+ return {
+ ...restStyle,
+ ...(isHeader && {
+ background: "hsl(var(--background))",
+ transition: "none",
+ }),
+ }
+ }
+
+ // 확장 버튼이 있을 때 left pin된 컬럼들을 오른쪽으로 이동
+ let leftPosition = baseStyle.left
+ if (expandable && pinnedSide === "left") {
+ const expandButtonWidth = 40 // w-10 = 40px
+ if (typeof baseStyle.left === 'string') {
+ const currentLeft = parseFloat(baseStyle.left.replace('px', ''))
+ leftPosition = `${currentLeft + expandButtonWidth}px`
+ } else if (typeof baseStyle.left === 'number') {
+ leftPosition = `${baseStyle.left + expandButtonWidth}px`
+ } else {
+ leftPosition = `${expandButtonWidth}px`
+ }
+ }
+
+ // 🎯 핀 위치에 따른 배경 결정
+ let shouldShowBackground = false
+ if (isHeader) {
+ // 헤더는 항상 배경 표시
+ shouldShowBackground = true
+ } else {
+ // 바디 셀의 경우 핀 위치에 따라 다른 조건 적용
+ if (pinnedSide === "left") {
+ // 왼쪽 핀: 오른쪽으로 스크롤했을 때 배경 표시
+ shouldShowBackground = isScrolled
+ } else if (pinnedSide === "right") {
+ // 오른쪽 핀: 초기화 전이거나 오른쪽 끝까지 스크롤하지 않았을 때 배경 표시
+ shouldShowBackground = !isInitialized || !isScrolledToEnd
+ }
+ }
+
+ // width를 제외한 스타일 적용
+ const { width, ...restBaseStyle } = baseStyle
+
+ const finalStyle = {
+ ...restBaseStyle,
+ left: leftPosition,
+ background: shouldShowBackground
+ ? "hsl(var(--background))"
+ : "transparent",
+ transition: isHeader ? "none" : "background-color 0.15s ease-out",
+ }
+
+ if (debug) {
+ console.log("Final pinned style:", {
+ columnId: column.id,
+ pinnedSide,
+ isHeader,
+ isScrolled,
+ isScrolledToEnd,
+ isInitialized,
+ shouldShowBackground,
+ finalStyle
+ })
+ }
+
+ return finalStyle
+ } catch (error) {
+ console.error("Error in getPinnedStyle:", error)
+ // fallback 스타일
+ return {
+ position: 'relative' as const,
+ ...(isHeader && {
+ background: "hsl(var(--background))",
+ }),
+ }
+ }
+ }, [expandable, isScrolled, isScrolledToEnd, isInitialized, debug])
+
+ // 확장 버튼용 스타일 (안정성 개선)
+ const getExpandButtonStyle = React.useCallback(() => {
+ return {
+ position: 'sticky' as const,
+ left: 0,
+ zIndex: 1,
+ background: "hsl(var(--background))",
+ minWidth: '40px',
+ maxWidth: '40px',
+ width: '40px',
+ }
+ }, [])
+
+ // 🎯 테이블 총 너비 계산
+ const getTableWidth = React.useCallback(() => {
+ const expandButtonWidth = expandable ? 40 : 0
+ const totalSize = table.getCenterTotalSize() + table.getLeftTotalSize() + table.getRightTotalSize()
+ return Math.max(totalSize + expandButtonWidth, 800) // 최소 800px 보장
+ }, [table, expandable])
+
+ // 행 확장/축소 핸들러
const toggleRowExpansion = React.useCallback((rowId: string, event?: React.MouseEvent) => {
if (!setExpandedRows) return
@@ -71,7 +285,6 @@ export function ExpandableDataTable<TData>({
const newExpanded = new Set(expandedRows)
if (newExpanded.has(rowId)) {
newExpanded.delete(rowId)
- // 높이 정보도 제거
setExpandedHeights(prev => {
const newHeights = new Map(prev)
newHeights.delete(rowId)
@@ -91,7 +304,66 @@ export function ExpandableDataTable<TData>({
}
}, [toggleRowExpansion])
- // 확장된 내용의 높이 측정 및 업데이트
+ // 🎯 확장된 내용 스타일 계산 함수 (컨테이너 너비 기준)
+ const getExpandedContentStyle = React.useCallback(() => {
+ const expandButtonWidth = expandable ? 40 : 0
+ const availableWidth = containerWidth || 800 // 🎯 컨테이너 너비 사용 (fallback 800px)
+ const contentWidth = availableWidth - expandButtonWidth - 32 // 버튼 + 여백(16px * 2) 제외
+
+ // 🎯 디버그 정보
+ if (debug) {
+ console.log('Expanded content sizing:', {
+ containerWidth,
+ availableWidth,
+ expandButtonWidth,
+ contentWidth,
+ finalWidth: Math.max(contentWidth, 300)
+ })
+ }
+
+ return {
+ width: `${Math.max(contentWidth, 300)}px`, // 🎯 최소 300px 보장
+ marginLeft: `${expandButtonWidth + 8}px`, // 🎯 확장 버튼 + 여백
+ marginRight: '16px',
+ padding: '12px 16px',
+ backgroundColor: 'hsl(var(--background))',
+ borderRadius: '0 0 6px 6px',
+ boxShadow: '0 2px 4px -1px rgba(0, 0, 0, 0.1)',
+ border: '1px solid hsl(var(--border))',
+ borderTop: 'none',
+ marginBottom: '4px',
+ maxWidth: `${availableWidth - expandButtonWidth - 16}px`, // 🎯 컨테이너 너비 초과 방지
+ overflow: 'auto',
+ }
+ }, [expandable, containerWidth, debug])
+
+ // 🎯 간단한 확장 스타일 (컨테이너 너비 기준)
+ const getSimpleExpandedStyle = React.useCallback(() => {
+ const expandButtonWidth = expandable ? 40 : 0
+ const availableWidth = containerWidth || 800
+ const contentWidth = availableWidth - expandButtonWidth - 48 // 버튼 + 여백 제외
+
+ return {
+ marginLeft: `${expandButtonWidth + 8}px`,
+ marginRight: '16px',
+ width: `${Math.max(contentWidth, 300)}px`,
+ maxWidth: `${availableWidth - expandButtonWidth - 24}px`,
+ padding: '12px 16px',
+ backgroundColor: 'hsl(var(--muted) / 0.5)',
+ borderRadius: '6px',
+ border: '1px solid hsl(var(--border))',
+ marginTop: '8px',
+ marginBottom: '8px',
+ overflow: 'auto',
+ }
+ }, [expandable, containerWidth])
+
+ // 🎯 사용할 확장 스타일 선택 (props로 제어)
+ const useSimpleExpansion = simpleExpansion
+ const getExpandedPlaceholderHeight = React.useCallback((contentHeight: number) => {
+ const padding = 24 + 4 // padding (12px * 2) + marginBottom (4px)
+ return Math.max(contentHeight + padding, 220)
+ }, [])
const updateExpandedHeight = React.useCallback((rowId: string, height: number) => {
setExpandedHeights(prev => {
if (prev.get(rowId) !== height) {
@@ -103,22 +375,26 @@ export function ExpandableDataTable<TData>({
})
}, [])
- // 컴팩트 모드를 위한 클래스 정의 (개선된 버전)
+ // 컴팩트 모드 스타일
const compactStyles = compact ? {
row: "h-7",
cell: "py-1 px-2 text-sm",
+ header: "py-1 px-2 text-sm",
+ headerRow: "h-8",
expandedCell: "py-2 px-4",
groupRow: "py-1 bg-muted/20 text-sm",
emptyRow: "h-16",
} : {
row: "",
cell: "",
- expandedCell: "py-0 px-0", // 패딩 제거하여 확장 컨텐츠가 전체 영역 사용
+ header: "",
+ headerRow: "",
+ expandedCell: "py-0 px-0",
groupRow: "bg-muted/20",
emptyRow: "h-24",
}
- // 확장 버튼 렌더링 함수 (접근성 개선)
+ // 확장 버튼 렌더링
const renderExpandButton = (rowId: string) => {
if (!expandable || !setExpandedRows) return null
@@ -143,7 +419,7 @@ export function ExpandableDataTable<TData>({
)
}
- // 확장된 내용 래퍼 컴포넌트 (높이 측정 기능 포함)
+ // 확장된 내용 래퍼 컴포넌트
const ExpandedContentWrapper = React.memo<{
rowId: string
children: React.ReactNode
@@ -176,244 +452,233 @@ export function ExpandableDataTable<TData>({
<div className={cn("w-full space-y-2.5", className)} {...props}>
{children}
- {/* 메인 테이블 컨테이너 - 가로스크롤 문제 해결 */}
<div
- ref={scrollContainerRef}
+ ref={containerRef} // 🎯 컨테이너 wrapper ref (패딩 제외 너비 계산용)
className="relative rounded-md border"
style={{
- // maxHeight: maxHeight || '35rem',
- minHeight: '200px' // 최소 높이 보장
+ minHeight: '200px'
}}
>
- {/* 가로스크롤 영역 */}
- <div className="overflow-x-auto overflow-y-hidden h-full">
- {/* 세로스크롤 영역 (확장된 내용 포함) */}
- <div className="overflow-y-auto h-full">
- <Table className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10 table-fixed">
- {/* 테이블 헤더 */}
- <TableHeader className="bg-background">
- {table.getHeaderGroups().map((headerGroup) => (
- <TableRow key={headerGroup.id} className={compact ? "h-8" : ""}>
- {/* 확장 버튼 컬럼 헤더 */}
- {expandable && (
- <TableHead
- className={cn("w-10 bg-background", compact ? "py-1 px-2" : "")}
- style={{ position: 'sticky', left: 0, zIndex: 11 }}
- />
- )}
-
- {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={cn(
- compact ? "py-1 px-2 text-sm" : "",
- "bg-background"
- )}
- style={{
- ...getCommonPinningStyles({ column: header.column }),
- width: header.getSize(),
- }}
- >
- <div style={{ position: "relative" }}>
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
+ <div
+ ref={scrollRef} // 🎯 스크롤 컨테이너 ref 연결
+ className="overflow-auto"
+ style={{ maxHeight: maxHeight || '34rem' }}
+ onScroll={handleScroll} // 🎯 스크롤 이벤트 핸들러
+ >
+ <Table
+ className="[&>thead]:sticky [&>thead]:top-0 [&>thead]:z-10"
+ style={{
+ width: getTableWidth(), // 🎯 동적 너비 계산
+ minWidth: '100%'
+ }}
+ >
+ <TableHeader className="bg-background">
+ {table.getHeaderGroups().map((headerGroup) => (
+ <TableRow key={headerGroup.id} className={compactStyles.headerRow}>
+ {expandable && (
+ <TableHead
+ className={cn("w-10", compactStyles.header)}
+ style={getExpandButtonStyle()}
+ />
+ )}
+
+ {headerGroup.headers.map((header) => {
+ if (header.column.getIsGrouped()) {
+ return null
+ }
- {header.column.getCanResize() && (
- <DataTableResizer header={header} />
+ return (
+ <TableHead
+ key={header.id}
+ colSpan={header.colSpan}
+ data-column-id={header.column.id}
+ className={cn(
+ compactStyles.header,
+ "bg-background"
+ )}
+ style={{
+ ...getPinnedStyle(header.column, true), // 🎯 동적 스타일
+ width: header.getSize() // 🎯 width 별도 설정
+ }}
+ >
+ <div style={{ position: "relative" }}>
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
)}
- </div>
- </TableHead>
- )
- })}
- </TableRow>
- ))}
- </TableHeader>
-
- {/* 테이블 바디 */}
- <TableBody>
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => {
- const isExpanded = expandedRows.has(row.id)
- const expandedHeight = expandedHeights.get(row.id) || 0
-
- // 그룹핑 헤더 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 + (expandable ? 1 : 0)}
- 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 hover:bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
- style={{
- marginLeft: `${row.depth * 1.5}rem`,
- }}
- aria-label={row.getIsExpanded() ? "그룹 축소" : "그룹 확장"}
- >
- {row.getIsExpanded() ? (
- <ChevronUp size={compact ? 14 : 16} />
- ) : (
- <ChevronRight size={compact ? 14 : 16} />
- )}
- </button>
- )}
+ {header.column.getCanResize() && (
+ <DataTableResizer header={header} />
+ )}
+ </div>
+ </TableHead>
+ )
+ })}
+ </TableRow>
+ ))}
+ </TableHeader>
- <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>
- )
+ <TableBody>
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => {
+ const isExpanded = expandedRows.has(row.id)
+ const expandedHeight = expandedHeights.get(row.id) || 0
+
+ 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
+ }
}
- // 일반 Row와 확장된 컨텐츠를 함께 렌더링
return (
- <React.Fragment key={row.id}>
- {/* 메인 데이터 행 */}
- <TableRow
- className={cn(
- compactStyles.row,
- isExpanded && "bg-muted/30 border-b-0"
- )}
- data-state={row.getIsSelected() && "selected"}
+ <TableRow
+ key={row.id}
+ className={compactStyles.groupRow}
+ data-state={row.getIsExpanded() && "expanded"}
+ >
+ <TableCell
+ colSpan={table.getVisibleFlatColumns().length + (expandable ? 1 : 0)}
+ className={compact ? "py-1 px-2" : ""}
>
- {/* 확장 버튼 셀 */}
- {expandable && (
- <TableCell
- className={cn("w-10", compactStyles.cell)}
- style={{
- position: 'sticky',
- left: 0,
- zIndex: 1,
- backgroundColor: isExpanded ? 'rgb(248 250 252)' : 'white'
+ {row.getCanExpand() && (
+ <button
+ onClick={row.getToggleExpandedHandler()}
+ className="inline-flex items-center justify-center mr-2 w-5 h-5 hover:bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
+ style={{
+ marginLeft: `${row.depth * 1.5}rem`,
}}
+ aria-label={row.getIsExpanded() ? "그룹 축소" : "그룹 확장"}
>
- {renderExpandButton(row.id)}
- </TableCell>
+ {row.getIsExpanded() ? (
+ <ChevronUp size={compact ? 14 : 16} />
+ ) : (
+ <ChevronRight size={compact ? 14 : 16} />
+ )}
+ </button>
)}
- {/* 데이터 셀들 */}
- {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>
+ <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>
+ )
+ }
+
+ return (
+ <React.Fragment key={row.id}>
+ <TableRow
+ className={cn(
+ compactStyles.row,
+ isExpanded && "bg-muted/30 border-b-0"
+ )}
+ data-state={row.getIsSelected() && "selected"}
+ >
+ {expandable && (
+ <TableCell
+ className={cn("w-10", compactStyles.cell)}
+ style={getExpandButtonStyle()}
+ >
+ {renderExpandButton(row.id)}
+ </TableCell>
+ )}
- {/* 확장된 컨텐츠 행 - 가로스크롤 독립성 보장 */}
- {isExpanded && renderExpandedContent && (
- <TableRow className="hover:bg-transparent">
+ {row.getVisibleCells().map((cell) => {
+ if (cell.column.getIsGrouped()) {
+ return null
+ }
+
+ return (
<TableCell
- colSpan={table.getVisibleFlatColumns().length + (expandable ? 1 : 0)}
- className={cn(
- compactStyles.expandedCell,
- "border-t-0 relative",
- expandedRowClassName
- )}
+ key={cell.id}
+ data-column-id={cell.column.id}
+ className={compactStyles.cell}
style={{
- minHeight: expandedHeight || 'auto',
+ ...getPinnedStyle(cell.column, false), // 🎯 동적 스타일
+ width: cell.column.getSize() // 🎯 width 별도 설정
}}
>
- {/* 확장된 내용을 위한 고정 폭 컨테이너 */}
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+ </TableCell>
+ )
+ })}
+ </TableRow>
+
+ {isExpanded && renderExpandedContent && (
+ <TableRow className="hover:bg-transparent">
+ <TableCell
+ colSpan={table.getVisibleFlatColumns().length + (expandable ? 1 : 0)}
+ className={cn(
+ compactStyles.expandedCell,
+ "border-t-0 relative",
+ expandedRowClassName
+ )}
+ style={{
+ minHeight: expandedHeight || 'auto',
+ }}
+ >
+ {useSimpleExpansion ? (
+ // 🎯 간단한 확장 방식: 테이블 내부에서만 확장
+ <div style={getSimpleExpandedStyle()}>
+ <ExpandedContentWrapper rowId={row.id}>
+ {renderExpandedContent(row.original)}
+ </ExpandedContentWrapper>
+ </div>
+ ) : (
+ // 🎯 전체 화면 확장 방식: 테이블 너비 기준으로 개선
<div className="relative w-full">
- {/* 가로스크롤과 독립적인 확장 영역 */}
<div
- className="absolute left-0 right-0 top-0 border-t"
- style={{
- width: '80vw',
- marginLeft: 'calc(-48vw + 50%)',
- }}
+ className="absolute top-0"
+ style={getExpandedContentStyle()}
>
- <div className="max-w-none mx-auto">
- <ExpandedContentWrapper rowId={row.id}>
- {renderExpandedContent(row.original)}
- </ExpandedContentWrapper>
- </div>
+ <ExpandedContentWrapper rowId={row.id}>
+ {renderExpandedContent(row.original)}
+ </ExpandedContentWrapper>
</div>
- {/* 높이 유지를 위한 스페이서 */}
<div
className="opacity-0 pointer-events-none"
- style={{ height: Math.max(expandedHeight, 200) }}
+ style={{ height: getExpandedPlaceholderHeight(expandedHeight) }}
/>
</div>
- </TableCell>
- </TableRow>
- )}
- </React.Fragment>
- )
- })
- ) : (
- // 데이터가 없을 때
- <TableRow>
- <TableCell
- colSpan={table.getAllColumns().length + (expandable ? 1 : 0)}
- className={compactStyles.emptyRow + " text-center"}
- >
- No results.
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </Table>
- </div>
+ )}
+ </TableCell>
+ </TableRow>
+ )}
+ </React.Fragment>
+ )
+ })
+ ) : (
+ <TableRow>
+ <TableCell
+ colSpan={table.getAllColumns().length + (expandable ? 1 : 0)}
+ className={compactStyles.emptyRow + " text-center"}
+ >
+ No results.
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
</div>
</div>
<div className="flex flex-col gap-2.5">
- {/* Pagination */}
<DataTablePagination table={table} />
-
- {/* Floating Bar (선택된 행 있을 때) */}
{table.getFilteredSelectedRowModel().rows.length > 0 && floatingBar}
</div>
</div>