"use client"; /** * SingleTableWithSticky — shared table renderer * * 2026-05-19 FlowWidget 의 sticky 테이블 구현을 흡수한 헬퍼. * 현재는 `frontend/components/screen/widgets/FlowWidget.tsx` 에서만 사용한다. * * 본 파일은 자체 minimal `ColumnConfig` 를 export 하며 외부 타입을 의존하지 않는다. * canonical table runtime 은 `frontend/lib/registry/components/table/TableComponent.tsx` 사용. */ import React from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { ArrowUp, ArrowDown } from "lucide-react"; import { cn } from "@/lib/utils"; import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext"; /** * SingleTableWithSticky 가 실제 사용하는 컬럼 메타 minimal 정의. */ export interface ColumnConfig { columnName: string; displayName?: string; sortable?: boolean; align?: "left" | "center" | "right"; format?: string; fixed?: "left" | "right" | false; hidden?: boolean; width?: number; langKey?: string; [key: string]: any; } interface SingleTableWithStickyProps { visibleColumns?: TColumn[]; columns?: TColumn[]; data: Record[]; columnLabels: Record; sortColumn: string | null; sortDirection: "asc" | "desc"; tableConfig?: any; isDesignMode?: boolean; isAllSelected?: boolean; handleSort?: (columnName: string) => void; onSort?: (columnName: string) => void; handleSelectAll?: (checked: boolean) => void; handleRowClick?: (row: any, index: number, e: React.MouseEvent) => void; renderCheckboxCell?: (row: any, index: number) => React.ReactNode; renderCheckboxHeader?: () => React.ReactNode; formatCellValue: ( value: any, format?: string, columnName?: string, rowData?: Record, ) => React.ReactNode; getColumnWidth: (column: TColumn) => number; containerWidth?: string; loading?: boolean; error?: string | null; onCellDoubleClick?: (rowIndex: number, colIndex: number, columnName: string, value: any) => void; editingCell?: { rowIndex: number; colIndex: number; columnName: string; originalValue: any } | null; editingValue?: string; onEditingValueChange?: (value: string) => void; onEditKeyDown?: (e: React.KeyboardEvent) => void; editInputRef?: React.RefObject; searchHighlights?: Set; currentSearchIndex?: number; searchTerm?: string; } export function SingleTableWithSticky({ visibleColumns, columns, data, columnLabels, sortColumn, sortDirection, tableConfig, isDesignMode = false, isAllSelected = false, handleSort, onSort, handleSelectAll, handleRowClick, renderCheckboxCell, renderCheckboxHeader, formatCellValue, getColumnWidth, containerWidth, loading = false, error = null, onCellDoubleClick, editingCell, editingValue, onEditingValueChange, onEditKeyDown, editInputRef, searchHighlights, currentSearchIndex = 0, searchTerm = "", }: SingleTableWithStickyProps) { const { getTranslatedText } = useScreenMultiLang(); const checkboxConfig = tableConfig?.checkbox || {}; const actualColumns = visibleColumns || columns || []; const sortHandler = onSort || handleSort || (() => {}); const actualData = data || []; return (
{actualColumns.map((column, colIndex) => { const leftFixedWidth = actualColumns .slice(0, colIndex) .filter((col) => col.fixed === "left") .reduce((sum, col) => sum + getColumnWidth(col), 0); const rightFixedColumns = actualColumns.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; const isCheckboxCol = column.columnName === "__checkbox__"; const headCheckboxBaseClass = "bg-background h-9 border-0 px-3 py-1.5 text-center align-middle sm:px-4 sm:py-2"; const headDataBaseClass = "text-foreground hover:text-foreground bg-background h-9 cursor-pointer border-0 px-3 py-1.5 text-left align-middle text-xs font-semibold whitespace-nowrap transition-all duration-200 select-none sm:px-4 sm:py-2 sm:text-sm"; return ( column.sortable && sortHandler(column.columnName)} >
{isCheckboxCol ? ( checkboxConfig.selectAll && ( ) ) : ( <> {(column as any).langKey ? getTranslatedText( (column as any).langKey, columnLabels[column.columnName] || column.displayName || column.columnName, ) : columnLabels[column.columnName] || column.displayName || column.columnName} {column.sortable && sortColumn === column.columnName && ( {sortDirection === "asc" ? ( ) : ( )} )} )}
); })}
{actualData.length === 0 ? (
데이터가 없습니다 조건을 변경하여 다시 검색해보세요
) : ( actualData.map((row, index) => { const rowClass = cn( "bg-background h-10 cursor-pointer border-b transition-colors", tableConfig?.tableStyle?.hoverEffect && "hover:bg-muted/50", ); return ( handleRowClick?.(row, index, e)}> {actualColumns.map((column, colIndex) => { const leftFixedWidth = actualColumns .slice(0, colIndex) .filter((col) => col.fixed === "left") .reduce((sum, col) => sum + getColumnWidth(col), 0); const rightFixedColumns = actualColumns.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; const isEditing = editingCell?.rowIndex === index && editingCell?.colIndex === colIndex; const isCheckboxCol = column.columnName === "__checkbox__"; const cellKey = `${index}-${colIndex}`; const cellValueStr = String(row[column.columnName] ?? "").toLowerCase(); const hasSearchTerm = searchTerm ? cellValueStr.includes(searchTerm.toLowerCase()) : false; const isHighlighted = !isCheckboxCol && hasSearchTerm && (searchHighlights?.has(cellKey) ?? false); const highlightArray = searchHighlights ? Array.from(searchHighlights) : []; const isCurrentSearchResult = isHighlighted && currentSearchIndex >= 0 && currentSearchIndex < highlightArray.length && highlightArray[currentSearchIndex] === cellKey; const rawCellValue: React.ReactNode = formatCellValue(row[column.columnName], column.format, column.columnName, row) || " "; const renderCellContent = () => { if (!isHighlighted || !searchTerm || isCheckboxCol) { return rawCellValue; } const strValue = String(rawCellValue); const lowerValue = strValue.toLowerCase(); const lowerTerm = searchTerm.toLowerCase(); const startIndex = lowerValue.indexOf(lowerTerm); if (startIndex === -1) return rawCellValue; const before = strValue.slice(0, startIndex); const match = strValue.slice(startIndex, startIndex + searchTerm.length); const after = strValue.slice(startIndex + searchTerm.length); return ( <> {before} {match} {after} ); }; const cellClass = cn( "text-foreground h-10 px-3 py-1.5 align-middle text-xs whitespace-nowrap transition-colors sm:px-4 sm:py-2 sm:text-sm", `text-${column.align}`, column.fixed === "left" && "border-border bg-background/90 sticky z-10 border-r backdrop-blur-sm", column.fixed === "right" && "border-border bg-background/90 sticky z-10 border-l backdrop-blur-sm", onCellDoubleClick && !isCheckboxCol && "cursor-text", ); return ( { if (onCellDoubleClick && !isCheckboxCol) { e.stopPropagation(); onCellDoubleClick(index, colIndex, column.columnName, row[column.columnName]); } }} > {isCheckboxCol ? ( renderCheckboxCell?.(row, index) ) : isEditing ? ( onEditingValueChange?.(e.target.value)} onKeyDown={onEditKeyDown} onBlur={() => { if (onEditKeyDown) { const fakeEvent = { key: "Enter", preventDefault: () => {}, } as React.KeyboardEvent; onEditKeyDown(fakeEvent); } }} className="border-primary bg-background focus:ring-primary h-8 w-full rounded border px-2 text-xs focus:ring-2 focus:outline-none sm:text-sm" onClick={(e) => e.stopPropagation()} /> ) : ( renderCellContent() )} ); })} ); }) )}
); }