From f7f5069a2209cfa39b65f492f32270a5f554bed0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 23 Oct 2025 10:10:21 +0000 Subject: (대표님) EDP 해양 관련 개발 사항들 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../form-data-plant/form-data-table-columns.tsx | 546 +++++++++++++++++++++ 1 file changed, 546 insertions(+) create mode 100644 components/form-data-plant/form-data-table-columns.tsx (limited to 'components/form-data-plant/form-data-table-columns.tsx') diff --git a/components/form-data-plant/form-data-table-columns.tsx b/components/form-data-plant/form-data-table-columns.tsx new file mode 100644 index 00000000..d453f6c2 --- /dev/null +++ b/components/form-data-plant/form-data-table-columns.tsx @@ -0,0 +1,546 @@ +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"; +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; // 헤더 텍스트 (우선순위 가장 높음) +} + +// Register 인터페이스 추가 +export interface Register { + PROJ_NO: string; + TYPE_ID: string; + EP_ID: string; + DESC: string; + REMARK: string | null; + NEW_TAG_YN: boolean; + ALL_TAG_YN: boolean; + VND_YN: boolean; + SEQ: number; + CMPLX_YN: boolean; + CMPL_SETT: any | null; + MAP_ATT: Array<{ ATT_ID: string; [key: string]: any }>; + MAP_CLS_ID: string[]; + MAP_OPER: any | null; + LNK_ATT: any[]; + JOIN_TABLS: any[]; + DELETED: boolean; + CRTER_NO: string; + CRTE_DTM: string; + CHGER_NO: string | null; + CHGE_DTM: string | null; + _id: 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; + // 새로 추가: registers (필수 필드 체크용) + registers?: Register[]; +} + +/** + * 셀 주소(예: "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; +} + +/** + * Register의 MAP_ATT에 해당 ATT_ID가 있는지 확인하는 함수 + * 필수 필드인지 체크 + */ +function isRequiredField(attId: string, registers?: Register[]): boolean { + if (!registers || registers.length === 0) { + return false; + } + + // 모든 레지스터의 MAP_ATT를 확인 + return registers.some(register => + register.MAP_ATT && + register.MAP_ATT.some(att => att.ATT_ID === attId) + ); +} + +/** + * status 값에 따라 Badge variant를 결정하는 헬퍼 함수 + */ +function getStatusBadgeVariant(status: string): "default" | "secondary" | "destructive" | "outline" { + const statusStr = String(status).toLowerCase(); + + switch (statusStr) { + case 'NEW': + case 'New': + return 'default'; // 초록색 계열 + case 'Updated or Modified': + return 'secondary'; // 노란색 계열 + case 'inactive': + case 'rejected': + case 'failed': + case 'cancelled': + return 'destructive'; // 빨간색 계열 + default: + return 'outline'; // 기본 회색 계열 + } +} + +/** + * 헤더 텍스트를 결정하는 헬퍼 함수 + * displayLabel이 있으면 사용, 없으면 label 사용 + * 필수 필드인 경우 빨간색 * 추가 + */ +function getHeaderText(col: DataTableColumnJSON, isRequired: boolean): React.ReactNode { + const baseText = col.displayLabel && col.displayLabel.trim() ? col.displayLabel : col.label; + + if (isRequired) { + return ( + + {baseText} + * + + ); + } + + return baseText; +} + +/** + * 컬럼들을 seq 순서대로 배치하면서 연속된 같은 head를 가진 컬럼들을 그룹으로 묶는 함수 + */ +function groupColumnsByHead(columns: DataTableColumnJSON[], registers?: Register[]): 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, registers)); + 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, registers)); + } else { + // 그룹 컬럼 생성 (구분선 스타일 적용) + const groupColumn: ColumnDef = { + id: `group-${groupHead.replace(/\s+/g, '-')}`, + header: groupHead, + columns: groupColumns.map(col => createColumnDef(col, true, registers)), + meta: { + isGroupColumn: true, + groupBorders: true, // 그룹 구분선 표시 플래그 + } + }; + result.push(groupColumn); + } + + i = j; // 다음 그룹으로 이동 + } + + return result; +} + +/** + * 개별 컬럼 정의를 생성하는 헬퍼 함수 + */ +function createColumnDef(col: DataTableColumnJSON, isInGroup: boolean = false, registers?: Register[]): ColumnDef { + const isRequired = isRequiredField(col.key, registers); + + 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, // 그룹 구분선 표시 플래그 + isRequired, // 필수 필드 표시 + }, + + 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, // 새로 추가된 매개변수 + registers, // 필수 필드 체크를 위한 레지스터 데이터 +}: 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, registers); + 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; +} \ No newline at end of file -- cgit v1.2.3