import { createContext, useContext, type ReactNode, type HTMLAttributes, type TdHTMLAttributes, type ThHTMLAttributes } from 'react'; import { tv } from 'tailwind-variants'; // ============================================================================ // Styles // ============================================================================ const tableStyles = tv({ slots: { wrapper: 'flex flex-col gap-2 w-full', scrollContainer: 'relative flex-1 overflow-y-auto overflow-x-auto min-h-0', table: 'w-full border-collapse', thead: 'sticky top-0 z-1 bg-primary-tertiary02', th: 'px-2 h-[42px] align-middle whitespace-pre-line text-sm font-bold text-primary-secondary', thGrouped: 'h-[38px] border-l border-r border-white text-sm font-bold text-primary-secondary', thGroupedBottom: 'h-[38px] border-l border-r border-b border-white text-sm font-bold text-primary-secondary', tr: 'cursor-default transition-colors duration-200', trEmpty: 'text-center align-middle text-sm text-dabeeo-gray-44', td: 'border-b border-dabeeo-gray-eb py-2.5 px-2 text-sm text-dabeeo-gray-44 align-middle whitespace-nowrap overflow-hidden text-ellipsis font-medium', caption: 'flex justify-between text-sm', captionLeft: 'flex items-end gap-6', total: 'font-medium text-dabeeo-gray-44', totalCount: 'font-bold text-primary', loading: 'flex py-2 gap-2 w-full items-center justify-center h-16', spinner: 'w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin', }, variants: { isLayoutFixed: { true: { table: 'table-fixed' }, }, isFullHeight: { true: { wrapper: 'h-full' }, }, isHoverable: { true: { tr: 'hover:bg-dabeeo-gray-eb' }, }, isClickable: { true: { tr: 'cursor-pointer' }, }, isSelected: { true: { tr: 'bg-primary-tertiary01' }, }, }, }); // ============================================================================ // Context // ============================================================================ type TableContextValue = { isLayoutFixed: boolean; isFullHeight: boolean; isHoverable: boolean; isClickable: boolean; columnCount: number; setColumnCount: (count: number) => void; }; const TableContext = createContext(null); function useTableContext() { const context = useContext(TableContext); if (!context) { throw new Error('Table compound components must be used within a Table component'); } return context; } // ============================================================================ // Table (Root) // ============================================================================ type TableRootProps = { children: ReactNode; isLayoutFixed?: boolean; isFullHeight?: boolean; isHoverable?: boolean; isClickable?: boolean; className?: string; }; function TableRoot({ children, isLayoutFixed = true, isFullHeight = true, isHoverable = true, isClickable = true, className, }: TableRootProps) { const [columnCount, setColumnCount] = useState(0); const styles = tableStyles({ isLayoutFixed, isFullHeight }); return (
{children}
); } // ============================================================================ // Caption // ============================================================================ type CaptionProps = { children?: ReactNode; className?: string; }; function Caption({ children, className }: CaptionProps) { const styles = tableStyles(); return
{children}
; } type CaptionLeftProps = { children?: ReactNode; className?: string; }; function CaptionLeft({ children, className }: CaptionLeftProps) { const styles = tableStyles(); return
{children}
; } type CaptionRightProps = { children?: ReactNode; className?: string; }; function CaptionRight({ children, className }: CaptionRightProps) { return
{children}
; } type TotalProps = { count: number; className?: string; }; function Total({ count, className }: TotalProps) { const styles = tableStyles(); return (
Total {count.toLocaleString()}
); } // ============================================================================ // Container (scroll wrapper + table) // ============================================================================ type ContainerProps = { children: ReactNode; className?: string; }; function Container({ children, className }: ContainerProps) { const { isLayoutFixed } = useTableContext(); const styles = tableStyles({ isLayoutFixed }); return (
{children}
); } // ============================================================================ // Colgroup // ============================================================================ type ColgroupProps = { children: ReactNode; }; function Colgroup({ children }: ColgroupProps) { return {children}; } type ColProps = { width?: number | string; }; function Col({ width }: ColProps) { const widthStyle = typeof width === 'number' ? `${width}px` : width || 'auto'; return ; } // ============================================================================ // Header // ============================================================================ type HeaderProps = { children: ReactNode; className?: string; }; function Header({ children, className }: HeaderProps) { const styles = tableStyles(); return {children}; } type HeaderRowProps = { children: ReactNode; className?: string; }; function HeaderRow({ children, className }: HeaderRowProps) { return {children}; } type HeaderCellProps = ThHTMLAttributes & { children?: ReactNode; align?: 'left' | 'center' | 'right'; isGroupTitle?: boolean; isGroupChild?: boolean; className?: string; }; function HeaderCell({ children, align = 'left', isGroupTitle = false, isGroupChild = false, className, style, ...props }: HeaderCellProps) { const styles = tableStyles(); let cellClass = styles.th(); if (isGroupTitle) { cellClass = styles.thGroupedBottom(); } else if (isGroupChild) { cellClass = styles.thGrouped(); } return ( {children} ); } // ============================================================================ // Body // ============================================================================ type BodyProps = { children: ReactNode; className?: string; }; function Body({ children, className }: BodyProps) { return {children}; } type RowProps = HTMLAttributes & { children: ReactNode; isSelected?: boolean; className?: string; }; function Row({ children, isSelected = false, className, ...props }: RowProps) { const { isHoverable, isClickable } = useTableContext(); const styles = tableStyles({ isHoverable, isClickable, isSelected }); return ( {children} ); } type CellProps = TdHTMLAttributes & { children?: ReactNode; align?: 'left' | 'center' | 'right'; className?: string; }; function Cell({ children, align = 'left', className, style, ...props }: CellProps) { const styles = tableStyles(); return ( {children} ); } // ============================================================================ // Empty & Loading // ============================================================================ type EmptyProps = { colSpan: number; children?: ReactNode; className?: string; }; function Empty({ colSpan, children = '데이터가 없습니다.', className }: EmptyProps) { const styles = tableStyles(); return ( {children} ); } type LoadingProps = { colSpan: number; className?: string; }; function Loading({ colSpan, className }: LoadingProps) { const styles = tableStyles(); return (
); } // ============================================================================ // Import useState // ============================================================================ import { useState } from 'react'; // ============================================================================ // Export // ============================================================================ export const Table = Object.assign(TableRoot, { Caption, CaptionLeft, CaptionRight, Total, Container, Colgroup, Col, Header, HeaderRow, HeaderCell, Body, Row, Cell, Empty, Loading, });