ERP-node/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx

222 lines
10 KiB
TypeScript
Raw Normal View History

2025-09-23 15:31:27 +09:00
"use client";
import React from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { ColumnConfig } from "./types";
interface SingleTableWithStickyProps {
visibleColumns: ColumnConfig[];
data: Record<string, any>[];
columnLabels: Record<string, string>;
sortColumn: string | null;
sortDirection: "asc" | "desc";
tableConfig: any;
isDesignMode: boolean;
isAllSelected: boolean;
handleSort: (columnName: string) => void;
handleSelectAll: (checked: boolean) => void;
handleRowClick: (row: any) => void;
renderCheckboxCell: (row: any, index: number) => React.ReactNode;
formatCellValue: (value: any, format?: string, columnName?: string) => string;
getColumnWidth: (column: ColumnConfig) => number;
containerWidth?: string; // 컨테이너 너비 설정
2025-09-23 15:31:27 +09:00
}
export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
visibleColumns,
data,
columnLabels,
sortColumn,
sortDirection,
tableConfig,
isDesignMode,
isAllSelected,
handleSort,
handleSelectAll,
handleRowClick,
renderCheckboxCell,
formatCellValue,
getColumnWidth,
containerWidth,
2025-09-23 15:31:27 +09:00
}) => {
const checkboxConfig = tableConfig.checkbox || {};
return (
<div
className="relative h-full overflow-x-auto overflow-y-auto rounded-2xl border border-gray-200/40 bg-white shadow-sm backdrop-blur-sm"
style={{
width: "100%",
maxWidth: "100%",
maxHeight: "100%", // 최대 높이 제한으로 스크롤 활성화
boxSizing: "border-box",
}}
>
<Table
className="w-full"
style={{
width: "100%",
minWidth: "100%",
tableLayout: "auto", // 테이블 크기 자동 조정
boxSizing: "border-box",
}}
>
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-gradient-to-r from-slate-50/90 to-gray-50/70 backdrop-blur-sm border-b border-gray-200/40" : "bg-gradient-to-r from-slate-50/90 to-gray-50/70 backdrop-blur-sm border-b border-gray-200/40"}>
2025-09-29 17:21:47 +09:00
<TableRow className="border-b border-gray-200/40">
2025-09-23 15:31:27 +09:00
{visibleColumns.map((column, colIndex) => {
// 왼쪽 고정 컬럼들의 누적 너비 계산
const leftFixedWidth = visibleColumns
.slice(0, colIndex)
.filter((col) => col.fixed === "left")
.reduce((sum, col) => sum + getColumnWidth(col), 0);
// 오른쪽 고정 컬럼들의 누적 너비 계산
const rightFixedColumns = visibleColumns.filter((col) => col.fixed === "right");
const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName);
const rightFixedWidth =
rightFixedIndex >= 0
? rightFixedColumns.slice(rightFixedIndex + 1).reduce((sum, col) => sum + getColumnWidth(col), 0)
: 0;
return (
<TableHead
key={column.columnName}
2025-09-23 15:31:27 +09:00
className={cn(
column.columnName === "__checkbox__"
? "h-12 border-0 px-6 py-4 text-center align-middle"
: "h-12 cursor-pointer border-0 px-6 py-4 text-left align-middle font-semibold whitespace-nowrap text-gray-700 select-none transition-all duration-200 hover:text-gray-900",
2025-09-23 15:31:27 +09:00
`text-${column.align}`,
column.sortable && "hover:bg-orange-200/70",
2025-09-23 15:31:27 +09:00
// 고정 컬럼 스타일
column.fixed === "left" && "sticky z-10 border-r border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70 shadow-sm",
column.fixed === "right" && "sticky z-10 border-l border-gray-200/40 bg-gradient-to-r from-slate-50/90 to-gray-50/70 shadow-sm",
2025-09-23 15:31:27 +09:00
// 숨김 컬럼 스타일 (디자인 모드에서만)
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
)}
style={{
width: getColumnWidth(column),
minWidth: "100px", // 최소 너비 보장
maxWidth: "300px", // 최대 너비 제한
boxSizing: "border-box",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap", // 텍스트 줄바꿈 방지
2025-09-23 15:31:27 +09:00
// sticky 위치 설정
...(column.fixed === "left" && { left: leftFixedWidth }),
...(column.fixed === "right" && { right: rightFixedWidth }),
}}
onClick={() => column.sortable && handleSort(column.columnName)}
>
<div className="flex items-center gap-2">
{column.columnName === "__checkbox__" ? (
checkboxConfig.selectAll && (
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" style={{ zIndex: 1 }} />
2025-09-23 15:31:27 +09:00
)
) : (
<>
<span className="flex-1 truncate">
{columnLabels[column.columnName] || column.displayName || column.columnName}
</span>
2025-09-30 18:42:33 +09:00
{column.sortable && sortColumn === column.columnName && (
2025-09-29 17:21:47 +09:00
<span className="ml-2 flex h-5 w-5 items-center justify-center rounded-md bg-white/50 shadow-sm">
2025-09-30 18:42:33 +09:00
{sortDirection === "asc" ? (
<ArrowUp className="h-3.5 w-3.5 text-blue-600" />
2025-09-23 15:31:27 +09:00
) : (
2025-09-30 18:42:33 +09:00
<ArrowDown className="h-3.5 w-3.5 text-blue-600" />
2025-09-23 15:31:27 +09:00
)}
</span>
)}
</>
)}
</div>
</TableHead>
);
})}
</TableRow>
</TableHeader>
<TableBody>
{data.length === 0 ? (
<TableRow>
2025-09-29 17:21:47 +09:00
<TableCell colSpan={visibleColumns.length} className="py-12 text-center">
<div className="flex flex-col items-center justify-center space-y-3">
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-br from-gray-100 to-gray-200">
<svg className="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<span className="text-sm font-medium text-gray-500"> </span>
<span className="text-xs text-gray-400 bg-gray-100 px-3 py-1 rounded-full"> </span>
</div>
2025-09-23 15:31:27 +09:00
</TableCell>
</TableRow>
) : (
data.map((row, index) => (
<TableRow
key={`row-${index}`}
2025-09-23 15:31:27 +09:00
className={cn(
"h-12 cursor-pointer border-b border-gray-100/40 leading-none transition-all duration-200",
tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-orange-50/80 hover:to-orange-100/60 hover:shadow-sm",
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/30",
2025-09-23 15:31:27 +09:00
)}
2025-09-29 17:21:47 +09:00
style={{ minHeight: "48px", height: "48px", lineHeight: "1" }}
2025-09-23 15:31:27 +09:00
onClick={() => handleRowClick(row)}
>
{visibleColumns.map((column, colIndex) => {
// 왼쪽 고정 컬럼들의 누적 너비 계산
const leftFixedWidth = visibleColumns
.slice(0, colIndex)
.filter((col) => col.fixed === "left")
.reduce((sum, col) => sum + getColumnWidth(col), 0);
// 오른쪽 고정 컬럼들의 누적 너비 계산
const rightFixedColumns = visibleColumns.filter((col) => col.fixed === "right");
const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName);
const rightFixedWidth =
rightFixedIndex >= 0
? rightFixedColumns.slice(rightFixedIndex + 1).reduce((sum, col) => sum + getColumnWidth(col), 0)
: 0;
return (
<TableCell
key={`cell-${column.columnName}`}
2025-09-23 15:31:27 +09:00
className={cn(
"h-12 px-6 py-4 align-middle text-sm whitespace-nowrap text-gray-600 transition-all duration-200",
2025-09-23 15:31:27 +09:00
`text-${column.align}`,
// 고정 컬럼 스타일
column.fixed === "left" && "sticky z-10 border-r border-gray-200/40 bg-white/90 backdrop-blur-sm",
column.fixed === "right" && "sticky z-10 border-l border-gray-200/40 bg-white/90 backdrop-blur-sm",
2025-09-23 15:31:27 +09:00
)}
style={{
2025-09-29 17:21:47 +09:00
minHeight: "48px",
height: "48px",
2025-09-23 15:31:27 +09:00
verticalAlign: "middle",
width: getColumnWidth(column),
minWidth: "100px", // 최소 너비 보장
maxWidth: "300px", // 최대 너비 제한
boxSizing: "border-box",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
2025-09-23 15:31:27 +09:00
// sticky 위치 설정
...(column.fixed === "left" && { left: leftFixedWidth }),
...(column.fixed === "right" && { right: rightFixedWidth }),
}}
>
{column.columnName === "__checkbox__"
? renderCheckboxCell(row, index)
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
2025-09-23 15:31:27 +09:00
</TableCell>
);
})}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
);
};