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;
|
2025-09-24 18:07:36 +09:00
|
|
|
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,
|
2025-09-24 18:07:36 +09:00
|
|
|
containerWidth,
|
2025-09-23 15:31:27 +09:00
|
|
|
}) => {
|
|
|
|
|
const checkboxConfig = tableConfig.checkbox || {};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-09-24 18:07:36 +09:00
|
|
|
<div
|
2025-09-29 17:21:47 +09:00
|
|
|
className="relative h-full overflow-auto rounded-xl border border-gray-200/60 bg-gradient-to-br from-white to-gray-50/30 shadow-sm"
|
2025-09-24 18:07:36 +09:00
|
|
|
style={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
maxWidth: "100%",
|
|
|
|
|
boxSizing: "border-box",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Table
|
|
|
|
|
className="w-full"
|
|
|
|
|
style={{
|
|
|
|
|
width: "100%",
|
|
|
|
|
maxWidth: "100%",
|
|
|
|
|
tableLayout: "fixed",
|
|
|
|
|
boxSizing: "border-box",
|
|
|
|
|
}}
|
|
|
|
|
>
|
2025-09-29 17:21:47 +09:00
|
|
|
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-gradient-to-r from-slate-50 to-blue-50/30 border-b border-gray-200/60" : "bg-gradient-to-r from-slate-50 to-blue-50/30 border-b border-gray-200/60"}>
|
|
|
|
|
<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
|
2025-09-24 18:07:36 +09:00
|
|
|
key={column.columnName}
|
2025-09-23 15:31:27 +09:00
|
|
|
className={cn(
|
|
|
|
|
column.columnName === "__checkbox__"
|
2025-09-29 17:21:47 +09:00
|
|
|
? "h-12 border-0 px-4 py-3 text-center align-middle"
|
|
|
|
|
: "h-12 cursor-pointer border-0 px-4 py-3 text-left align-middle font-semibold whitespace-nowrap text-slate-700 select-none transition-all duration-200",
|
2025-09-23 15:31:27 +09:00
|
|
|
`text-${column.align}`,
|
2025-09-29 17:21:47 +09:00
|
|
|
column.sortable && "hover:bg-blue-50/50 hover:text-blue-700",
|
2025-09-23 15:31:27 +09:00
|
|
|
// 고정 컬럼 스타일
|
2025-09-29 17:21:47 +09:00
|
|
|
column.fixed === "left" && "sticky z-10 border-r border-gray-200/60 bg-gradient-to-r from-slate-50 to-blue-50/30 shadow-sm",
|
|
|
|
|
column.fixed === "right" && "sticky z-10 border-l border-gray-200/60 bg-gradient-to-r from-slate-50 to-blue-50/30 shadow-sm",
|
2025-09-23 15:31:27 +09:00
|
|
|
// 숨김 컬럼 스타일 (디자인 모드에서만)
|
|
|
|
|
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
|
|
|
|
|
)}
|
|
|
|
|
style={{
|
|
|
|
|
width: getColumnWidth(column),
|
|
|
|
|
minWidth: getColumnWidth(column),
|
|
|
|
|
maxWidth: getColumnWidth(column),
|
2025-09-24 18:07:36 +09:00
|
|
|
boxSizing: "border-box",
|
|
|
|
|
overflow: "hidden",
|
|
|
|
|
textOverflow: "ellipsis",
|
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 && (
|
2025-09-24 18:07:36 +09:00
|
|
|
<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>
|
|
|
|
|
{column.sortable && (
|
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-23 15:31:27 +09:00
|
|
|
{sortColumn === column.columnName ? (
|
|
|
|
|
sortDirection === "asc" ? (
|
2025-09-29 17:21:47 +09:00
|
|
|
<ArrowUp className="h-3.5 w-3.5 text-blue-600" />
|
2025-09-23 15:31:27 +09:00
|
|
|
) : (
|
2025-09-29 17:21:47 +09:00
|
|
|
<ArrowDown className="h-3.5 w-3.5 text-blue-600" />
|
2025-09-23 15:31:27 +09:00
|
|
|
)
|
|
|
|
|
) : (
|
2025-09-29 17:21:47 +09:00
|
|
|
<ArrowUpDown className="h-3.5 w-3.5 text-gray-400" />
|
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
|
2025-09-24 18:07:36 +09:00
|
|
|
key={`row-${index}`}
|
2025-09-23 15:31:27 +09:00
|
|
|
className={cn(
|
2025-09-29 17:21:47 +09:00
|
|
|
"h-12 cursor-pointer border-b border-gray-100/60 leading-none transition-all duration-200",
|
|
|
|
|
tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-blue-50/30 hover:to-indigo-50/20 hover:shadow-sm",
|
|
|
|
|
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gradient-to-r from-slate-50/30 to-gray-50/20",
|
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
|
2025-09-24 18:07:36 +09:00
|
|
|
key={`cell-${column.columnName}`}
|
2025-09-23 15:31:27 +09:00
|
|
|
className={cn(
|
2025-09-29 17:21:47 +09:00
|
|
|
"h-12 px-4 py-3 align-middle text-sm whitespace-nowrap text-slate-600 transition-all duration-200",
|
2025-09-23 15:31:27 +09:00
|
|
|
`text-${column.align}`,
|
|
|
|
|
// 고정 컬럼 스타일
|
2025-09-29 17:21:47 +09:00
|
|
|
column.fixed === "left" && "sticky z-10 border-r border-gray-200/60 bg-white/90 backdrop-blur-sm",
|
|
|
|
|
column.fixed === "right" && "sticky z-10 border-l border-gray-200/60 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",
|
2025-09-24 18:07:36 +09:00
|
|
|
width: getColumnWidth(column),
|
|
|
|
|
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)
|
2025-09-24 18:07:36 +09:00
|
|
|
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
2025-09-23 15:31:27 +09:00
|
|
|
</TableCell>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|