import type { ColumnDef, Row } from "@tanstack/react-table"; import { ClientDataTableColumnHeaderSimple } from "../client-data-table/data-table-column-simple-header"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; // Badge import 추가 import { Ellipsis } from "lucide-react"; import { formatDate } from "@/lib/utils"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { toast } from 'sonner'; import { createFilterFn } from "@/components/client-data-table/table-filters"; /** row 액션 관련 타입 */ export interface DataTableRowAction { row: Row; type: "open" | "edit" | "update" | "delete"; } /** 컬럼 타입 (필요에 따라 확장) */ export type ColumnType = "STRING" | "NUMBER" | "LIST"; export interface DataTableColumnJSON { key: string; /** 실제 Excel 등에서 구분용으로 쓰이는 label (고정) */ label: string; /** UI 표시용 label (예: 단위를 함께 표시) */ displayLabel?: string; type: ColumnType; options?: string[]; uom?: string; uomId?: string; shi?: string; /** 템플릿에서 가져온 추가 정보 */ hidden?: boolean; // true이면 컬럼 숨김 seq?: number; // 정렬 순서 head?: string; // 헤더 텍스트 (우선순위 가장 높음) } /** * getColumns 함수에 필요한 props * - TData: 테이블에 표시할 행(Row)의 타입 */ interface GetColumnsProps { columnsJSON: DataTableColumnJSON[]; setRowAction: React.Dispatch< React.SetStateAction | null> >; setReportData: React.Dispatch>; tempCount: number; // 체크박스 선택 관련 props selectedRows?: Record; onRowSelectionChange?: (updater: Record | ((prev: Record) => Record)) => void; // 새로 추가: templateData templateData?: any; } /** * 셀 주소(예: "A1", "B1", "AA1")에서 컬럼 순서를 추출하는 함수 * A=0, B=1, C=2, ..., Z=25, AA=26, AB=27, ... */ function getColumnOrderFromCellAddress(cellAddress: string): number { if (!cellAddress || typeof cellAddress !== 'string') { return 999999; // 유효하지 않은 경우 맨 뒤로 } // 셀 주소에서 알파벳 부분만 추출 (예: "A1" -> "A", "AA1" -> "AA") const match = cellAddress.match(/^([A-Z]+)/); if (!match) { return 999999; } const columnLetters = match[1]; let result = 0; // 알파벳을 숫자로 변환 (26진법과 유사하지만 0이 없는 체계) for (let i = 0; i < columnLetters.length; i++) { const charCode = columnLetters.charCodeAt(i) - 65 + 1; // A=1, B=2, ..., Z=26 result = result * 26 + charCode; } return result - 1; // 0부터 시작하도록 조정 } /** * templateData에서 SPREAD_LIST의 컬럼 순서 정보를 추출하여 seq를 업데이트하는 함수 */ function updateSeqFromTemplate(columnsJSON: DataTableColumnJSON[], templateData: any): DataTableColumnJSON[] { if (!templateData) { return columnsJSON; // templateData가 없으면 원본 그대로 반환 } // templateData가 배열인지 단일 객체인지 확인 let templates: any[]; if (Array.isArray(templateData)) { templates = templateData; } else { templates = [templateData]; } // SPREAD_LIST 타입의 템플릿 찾기 const spreadListTemplate = templates.find(template => template.TMPL_TYPE === 'SPREAD_LIST' && template.SPR_LST_SETUP?.DATA_SHEETS ); if (!spreadListTemplate) { return columnsJSON; // SPREAD_LIST 템플릿이 없으면 원본 그대로 반환 } // MAP_CELL_ATT에서 ATT_ID와 IN 매핑 정보 추출 const cellMappings = new Map(); // key: ATT_ID, value: IN (셀 주소) spreadListTemplate.SPR_LST_SETUP.DATA_SHEETS.forEach((dataSheet: any) => { if (dataSheet.MAP_CELL_ATT) { dataSheet.MAP_CELL_ATT.forEach((mapping: any) => { if (mapping.ATT_ID && mapping.IN) { cellMappings.set(mapping.ATT_ID, mapping.IN); } }); } }); // columnsJSON을 복사하여 seq 값 업데이트 const updatedColumns = columnsJSON.map(column => { const cellAddress = cellMappings.get(column.key); if (cellAddress) { // 셀 주소에서 컬럼 순서 추출 const newSeq = getColumnOrderFromCellAddress(cellAddress); console.log(`🔄 Updating seq for ${column.key}: ${column.seq} -> ${newSeq} (from ${cellAddress})`); return { ...column, seq: newSeq }; } return column; // 매핑이 없으면 원본 그대로 }); return updatedColumns; } /** * status 값에 따라 Badge variant를 결정하는 헬퍼 함수 */ function getStatusBadgeVariant(status: string): "default" | "secondary" | "destructive" | "outline" { const statusStr = String(status).toLowerCase(); switch (statusStr) { case 'NEW': case 'New': // case 'approved': return 'default'; // 초록색 계열 case 'Updated or Modified': // case 'in progress': // case 'processing': return 'secondary'; // 노란색 계열 case 'inactive': case 'rejected': case 'failed': case 'cancelled': return 'destructive'; // 빨간색 계열 default: return 'outline'; // 기본 회색 계열 } } /** * 헤더 텍스트를 결정하는 헬퍼 함수 * displayLabel이 있으면 사용, 없으면 label 사용 */ function getHeaderText(col: DataTableColumnJSON): string { if (col.displayLabel && col.displayLabel.trim()) { return col.displayLabel; } return col.label; } /** * 컬럼들을 seq 순서대로 배치하면서 연속된 같은 head를 가진 컬럼들을 그룹으로 묶는 함수 */ function groupColumnsByHead(columns: DataTableColumnJSON[]): ColumnDef[] { const result: ColumnDef[] = []; let i = 0; while (i < columns.length) { const currentCol = columns[i]; // head가 없거나 빈 문자열인 경우 일반 컬럼으로 처리 if (!currentCol.head || !currentCol.head.trim()) { result.push(createColumnDef(currentCol, false)); i++; continue; } // 같은 head를 가진 연속된 컬럼들을 찾기 const groupHead = currentCol.head.trim(); const groupColumns: DataTableColumnJSON[] = [currentCol]; let j = i + 1; while (j < columns.length && columns[j].head && columns[j].head.trim() === groupHead) { groupColumns.push(columns[j]); j++; } // 그룹에 컬럼이 하나만 있으면 일반 컬럼으로 처리 if (groupColumns.length === 1) { result.push(createColumnDef(currentCol, false)); } else { // 그룹 컬럼 생성 (구분선 스타일 적용) const groupColumn: ColumnDef = { id: `group-${groupHead.replace(/\s+/g, '-')}`, header: groupHead, columns: groupColumns.map(col => createColumnDef(col, true)), meta: { isGroupColumn: true, groupBorders: true, // 그룹 구분선 표시 플래그 } }; result.push(groupColumn); } i = j; // 다음 그룹으로 이동 } return result; } /** * 개별 컬럼 정의를 생성하는 헬퍼 함수 */ function createColumnDef(col: DataTableColumnJSON, isInGroup: boolean = false): ColumnDef { return { accessorKey: col.key, header: ({ column }) => ( ), filterFn: col.type === 'NUMBER' ? createFilterFn("number") : col.type === 'LIST' ? createFilterFn("multi-select"):createFilterFn("text"), meta: { excelHeader: col.label, minWidth: 80, paddingFactor: 1.2, maxWidth: col.key === "TAG_NO" ? 120 : 150, isReadOnly: col.shi === true, isInGroup, // 그룹 내 컬럼인지 표시 groupBorders: isInGroup, // 그룹 구분선 표시 플래그 }, cell: ({ row }) => { const cellValue = row.getValue(col.key); // SHI 필드만 읽기 전용으로 처리 const isReadOnly = col.shi === true; // 그룹 구분선 스타일 클래스 추가 const groupBorderClass = isInGroup ? "group-column-border" : ""; const readOnlyClass = isReadOnly ? "read-only-cell" : ""; const combinedClass = [groupBorderClass, readOnlyClass].filter(Boolean).join(" "); const cellStyle = { ...(isReadOnly && { backgroundColor: '#f5f5f5', color: '#666', cursor: 'not-allowed' }), ...(isInGroup && { borderLeft: '2px solid #e2e8f0', borderRight: '2px solid #e2e8f0', position: 'relative' as const }) }; // 툴팁 메시지 설정 (SHI 필드만) const tooltipMessage = isReadOnly ? "SHI 전용 필드입니다" : ""; // status 컬럼인 경우 Badge 적용 if (col.key === "status") { const statusValue = String(cellValue ?? ""); const badgeVariant = getStatusBadgeVariant(statusValue); return (
{statusValue}
); } // 데이터 타입별 처리 switch (col.type) { case "NUMBER": return (
{cellValue ? Number(cellValue).toLocaleString() : ""}
); case "LIST": return (
{String(cellValue ?? "")}
); case "STRING": default: return (
{String(cellValue ?? "")}
); } }, }; } /** * getColumns 함수 * 1) columnsJSON 배열을 필터링 (hidden이 true가 아닌 것들만) * 2) seq에 따라 정렬 * 3) seq 순서를 유지하면서 연속된 같은 head를 가진 컬럼들을 그룹으로 묶기 * 4) 체크박스 컬럼 추가 * 5) 마지막에 "Action" 칼럼 추가 */ export function getColumns({ columnsJSON, setRowAction, setReportData, tempCount, selectedRows = {}, onRowSelectionChange, templateData, // 새로 추가된 매개변수 }: GetColumnsProps): ColumnDef[] { const columns: ColumnDef[] = []; // (0) templateData에서 SPREAD_LIST인 경우 seq 값 업데이트 const processedColumnsJSON = updateSeqFromTemplate(columnsJSON, templateData); // (1) 컬럼 필터링 및 정렬 const visibleColumns = processedColumnsJSON .filter(col => col.hidden !== true) // hidden이 true가 아닌 것들만 .sort((a, b) => { // seq가 없는 경우 999999로 처리하여 맨 뒤로 보냄 const seqA = a.seq !== undefined ? a.seq : 999999; const seqB = b.seq !== undefined ? b.seq : 999999; return seqA - seqB; }); console.log('📊 Final column order after template processing:', visibleColumns.map(c => `${c.key}(seq:${c.seq})`)); // (2) 체크박스 컬럼 (항상 표시) const selectColumn: ColumnDef = { id: "select", header: ({ table }) => ( { table.toggleAllPageRowsSelected(!!value); // 모든 행 선택/해제 if (onRowSelectionChange) { const allRowsSelection: Record = {}; table.getRowModel().rows.forEach((row) => { allRowsSelection[row.id] = !!value; }); onRowSelectionChange(allRowsSelection); } }} aria-label="Select all" className="translate-y-[2px]" /> ), cell: ({ row }) => ( { row.toggleSelected(!!value); // 개별 행 선택 상태 업데이트 if (onRowSelectionChange) { onRowSelectionChange(prev => ({ ...prev, [row.id]: !!value })); } }} aria-label="Select row" className="translate-y-[2px]" /> ), enableSorting: false, enableHiding: false, enablePinning: true, size: 40, }; columns.push(selectColumn); // (3) 기본 컬럼들 (seq 순서를 유지하면서 head에 따라 그룹핑 처리) const groupedColumns = groupColumnsByHead(visibleColumns); columns.push(...groupedColumns); // (4) 액션 칼럼 - update 버튼 예시 const actionColumn: ColumnDef = { id: "update", header: "", cell: ({ row }) => ( { setRowAction({ row, type: "update" }); }} > Edit { if(tempCount > 0){ const { original } = row; setReportData([original]); } else { toast.error("업로드된 Template File이 없습니다."); } }} > Create Document { setRowAction({ row, type: "delete" }); }} className="text-red-600 focus:text-red-600" > Delete ), size: 40, enablePinning: true, }; columns.push(actionColumn); // (5) 최종 반환 return columns; }