feat: Table, Section 컴포넌트 추가
This commit is contained in:
23
web-app/app/shared/components/section/Section.tsx
Normal file
23
web-app/app/shared/components/section/Section.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
type SectionVariant = 'base' | 'card' | 'list' | 'searchFilterGray';
|
||||
|
||||
const variantClassMap: Record<SectionVariant, string> = {
|
||||
base: 'flex-center w-full bg-white shadow-card p-6',
|
||||
card: 'flex-center bg-white shadow-card p-6',
|
||||
list: 'flex flex-col h-full min-h-0 gap-4 bg-white shadow-card p-6',
|
||||
searchFilterGray: 'flex items-center bg-gray-100 py-3 px-7 text-kc-black-2a text-sm font-medium gap-3',
|
||||
};
|
||||
|
||||
export type SectionProps = {
|
||||
variant: SectionVariant;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Section = ({ variant, children, className }: SectionProps) => {
|
||||
const baseClassName = variantClassMap[variant];
|
||||
const mergedClassName = className ? `${baseClassName} ${className}` : baseClassName;
|
||||
|
||||
return <section className={mergedClassName}>{children}</section>;
|
||||
};
|
||||
357
web-app/app/shared/components/table/Table.tsx
Normal file
357
web-app/app/shared/components/table/Table.tsx
Normal file
@@ -0,0 +1,357 @@
|
||||
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<TableContextValue | null>(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 (
|
||||
<TableContext.Provider
|
||||
value={{ isLayoutFixed, isFullHeight, isHoverable, isClickable, columnCount, setColumnCount }}
|
||||
>
|
||||
<div className={styles.wrapper({ className })}>{children}</div>
|
||||
</TableContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Caption
|
||||
// ============================================================================
|
||||
|
||||
type CaptionProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Caption({ children, className }: CaptionProps) {
|
||||
const styles = tableStyles();
|
||||
return <div className={styles.caption({ className })}>{children}</div>;
|
||||
}
|
||||
|
||||
type CaptionLeftProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CaptionLeft({ children, className }: CaptionLeftProps) {
|
||||
const styles = tableStyles();
|
||||
return <div className={styles.captionLeft({ className })}>{children}</div>;
|
||||
}
|
||||
|
||||
type CaptionRightProps = {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function CaptionRight({ children, className }: CaptionRightProps) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
type TotalProps = {
|
||||
count: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Total({ count, className }: TotalProps) {
|
||||
const styles = tableStyles();
|
||||
return (
|
||||
<div className={styles.total({ className })}>
|
||||
Total <span className={styles.totalCount()}>{count.toLocaleString()}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Container (scroll wrapper + table)
|
||||
// ============================================================================
|
||||
|
||||
type ContainerProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Container({ children, className }: ContainerProps) {
|
||||
const { isLayoutFixed } = useTableContext();
|
||||
const styles = tableStyles({ isLayoutFixed });
|
||||
|
||||
return (
|
||||
<div className={styles.scrollContainer({ className })}>
|
||||
<table className={styles.table()}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Colgroup
|
||||
// ============================================================================
|
||||
|
||||
type ColgroupProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function Colgroup({ children }: ColgroupProps) {
|
||||
return <colgroup>{children}</colgroup>;
|
||||
}
|
||||
|
||||
type ColProps = {
|
||||
width?: number | string;
|
||||
};
|
||||
|
||||
function Col({ width }: ColProps) {
|
||||
const widthStyle = typeof width === 'number' ? `${width}px` : width || 'auto';
|
||||
return <col style={{ width: widthStyle }} />;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Header
|
||||
// ============================================================================
|
||||
|
||||
type HeaderProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Header({ children, className }: HeaderProps) {
|
||||
const styles = tableStyles();
|
||||
return <thead className={styles.thead({ className })}>{children}</thead>;
|
||||
}
|
||||
|
||||
type HeaderRowProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function HeaderRow({ children, className }: HeaderRowProps) {
|
||||
return <tr className={className}>{children}</tr>;
|
||||
}
|
||||
|
||||
type HeaderCellProps = ThHTMLAttributes<HTMLTableCellElement> & {
|
||||
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 (
|
||||
<th align={align} className={`${cellClass} ${className || ''}`} style={style} {...props}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Body
|
||||
// ============================================================================
|
||||
|
||||
type BodyProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Body({ children, className }: BodyProps) {
|
||||
return <tbody className={className}>{children}</tbody>;
|
||||
}
|
||||
|
||||
type RowProps = HTMLAttributes<HTMLTableRowElement> & {
|
||||
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 (
|
||||
<tr className={styles.tr({ className })} {...props}>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
type CellProps = TdHTMLAttributes<HTMLTableCellElement> & {
|
||||
children?: ReactNode;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Cell({ children, align = 'left', className, style, ...props }: CellProps) {
|
||||
const styles = tableStyles();
|
||||
|
||||
return (
|
||||
<td className={styles.td({ className })} style={{ textAlign: align, ...style }} {...props}>
|
||||
{children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Empty & Loading
|
||||
// ============================================================================
|
||||
|
||||
type EmptyProps = {
|
||||
colSpan: number;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Empty({ colSpan, children = '데이터가 없습니다.', className }: EmptyProps) {
|
||||
const styles = tableStyles();
|
||||
return (
|
||||
<tr className={styles.trEmpty({ className })}>
|
||||
<td colSpan={colSpan} className="py-2.5">
|
||||
{children}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
type LoadingProps = {
|
||||
colSpan: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function Loading({ colSpan, className }: LoadingProps) {
|
||||
const styles = tableStyles();
|
||||
return (
|
||||
<tr className={styles.trEmpty({ className })}>
|
||||
<td colSpan={colSpan} className="py-2.5">
|
||||
<div className={styles.loading()}>
|
||||
<div className={styles.spinner()} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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,
|
||||
});
|
||||
1
web-app/app/shared/components/table/index.ts
Normal file
1
web-app/app/shared/components/table/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Table } from './Table';
|
||||
Reference in New Issue
Block a user