"use client"; import * as React from "react"; import dynamic from "next/dynamic"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { GenericData } from "./export-excel-form"; import * as GC from "@mescius/spread-sheets"; import { toast } from "sonner"; import { updateFormDataInDB } from "@/lib/forms/services"; import { Loader, Save, AlertTriangle } from "lucide-react"; import '@mescius/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css'; import { DataTableColumnJSON, ColumnType } from "./form-data-table-columns"; // SpreadSheets를 동적으로 import (SSR 비활성화) const SpreadSheets = dynamic( () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), { ssr: false, loading: () => (
Loading SpreadSheets...
) } ); // 라이센스 키 설정을 클라이언트에서만 실행 if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_SPREAD_LICENSE) { GC.Spread.Sheets.LicenseKey = process.env.NEXT_PUBLIC_SPREAD_LICENSE; } interface TemplateItem { TMPL_ID: string; NAME: string; TMPL_TYPE: string; SPR_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; CONTENT?: string; DATA_SHEETS: Array<{ SHEET_NAME: string; REG_TYPE_ID: string; MAP_CELL_ATT: Array<{ ATT_ID: string; IN: string; }>; }>; }; GRD_LST_SETUP: { REG_TYPE_ID: string; SPR_ITM_IDS: Array; ATTS: Array<{}>; }; SPR_ITM_LST_SETUP: { ACT_SHEET: string; HIDN_SHEETS: Array; CONTENT?: string; DATA_SHEETS: Array<{ SHEET_NAME: string; REG_TYPE_ID: string; MAP_CELL_ATT: Array<{ ATT_ID: string; IN: string; }>; }>; }; } interface ValidationError { cellAddress: string; attId: string; value: any; expectedType: ColumnType; message: string; } interface CellMapping { attId: string; cellAddress: string; isEditable: boolean; dataRowIndex?: number; } interface TemplateViewDialogProps { isOpen: boolean; onClose: () => void; templateData: TemplateItem[] | any; selectedRow?: GenericData; // SPREAD_ITEM용 tableData?: GenericData[]; // SPREAD_LIST용 formCode: string; columnsJSON: DataTableColumnJSON[] contractItemId: number; editableFieldsMap?: Map; onUpdateSuccess?: (updatedValues: Record | GenericData[]) => void; } export function TemplateViewDialog({ isOpen, onClose, templateData, selectedRow, tableData = [], formCode, contractItemId, columnsJSON, editableFieldsMap = new Map(), onUpdateSuccess }: TemplateViewDialogProps) { const [hostStyle, setHostStyle] = React.useState({ width: '100%', height: '100%' }); const [isPending, setIsPending] = React.useState(false); const [hasChanges, setHasChanges] = React.useState(false); const [currentSpread, setCurrentSpread] = React.useState(null); const [cellMappings, setCellMappings] = React.useState([]); const [isClient, setIsClient] = React.useState(false); const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null>(null); const [validationErrors, setValidationErrors] = React.useState([]); const [selectedTemplateId, setSelectedTemplateId] = React.useState(""); const [availableTemplates, setAvailableTemplates] = React.useState([]); const determineTemplateType = React.useCallback((template: TemplateItem): 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null => { // 1. SPREAD_LIST: TMPL_TYPE이 SPREAD_LIST이고 SPR_LST_SETUP.CONTENT가 있음 if (template.TMPL_TYPE === "SPREAD_LIST" && (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT)) { return 'SPREAD_LIST'; } // 2. SPREAD_ITEM: TMPL_TYPE이 SPREAD_ITEM이고 SPR_ITM_LST_SETUP.CONTENT가 있음 if (template.TMPL_TYPE === "SPREAD_ITEM" && (template.SPR_LST_SETUP?.CONTENT || template.SPR_ITM_LST_SETUP?.CONTENT)) { return 'SPREAD_ITEM'; } // 3. GRD_LIST: GRD_LST_SETUP이 있고 columnsJSON이 있음 (동적 테이블) if (template.GRD_LST_SETUP && columnsJSON.length > 0) { return 'GRD_LIST'; } return null; // 유효하지 않은 템플릿 }, [columnsJSON]); const isValidTemplate = React.useCallback((template: TemplateItem): boolean => { return determineTemplateType(template) !== null; }, [determineTemplateType]); // 클라이언트 사이드에서만 렌더링되도록 보장 React.useEffect(() => { setIsClient(true); }, []); // 사용 가능한 템플릿들을 필터링하고 설정 React.useEffect(() => { if (!templateData) return; let templates: TemplateItem[]; if (Array.isArray(templateData)) { templates = templateData as TemplateItem[]; } else { templates = [templateData as TemplateItem]; } // 유효한 템플릿들만 필터링 const validTemplates = templates.filter(isValidTemplate); setAvailableTemplates(validTemplates); // 첫 번째 유효한 템플릿을 기본으로 선택 if (validTemplates.length > 0 && !selectedTemplateId) { const firstTemplate = validTemplates[0]; const templateTypeToSet = determineTemplateType(firstTemplate); setSelectedTemplateId(firstTemplate.TMPL_ID); setTemplateType(templateTypeToSet); } }, [templateData, selectedTemplateId, isValidTemplate, determineTemplateType]); // 선택된 템플릿 변경 처리 const handleTemplateChange = (templateId: string) => { const template = availableTemplates.find(t => t.TMPL_ID === templateId); if (template) { const templateTypeToSet = determineTemplateType(template); setSelectedTemplateId(templateId); setTemplateType(templateTypeToSet); setHasChanges(false); setValidationErrors([]); // SpreadSheets 재초기화 if (currentSpread && template) { initSpread(currentSpread, template); } } }; // 현재 선택된 템플릿 가져오기 const selectedTemplate = React.useMemo(() => { return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId); }, [availableTemplates, selectedTemplateId]); // 편집 가능한 필드 목록 계산 const editableFields = React.useMemo(() => { // SPREAD_ITEM인 경우: selectedRow의 TAG_NO로 확인 if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) { if (!editableFieldsMap.has(selectedRow.TAG_NO)) { return []; } return editableFieldsMap.get(selectedRow.TAG_NO) || []; } // SPREAD_LIST 또는 GRD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리 if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) { const firstRowTagNo = tableData[0]?.TAG_NO; if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) { return editableFieldsMap.get(firstRowTagNo) || []; } } return []; }, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]); // 필드가 편집 가능한지 판별하는 함수 const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => { // columnsJSON에서 해당 attId의 shi 값 확인 const columnConfig = columnsJSON.find(col => col.key === attId); if (columnConfig?.shi === true) { return false; // columnsJSON에서 shi가 true이면 편집 불가 } // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우) if (attId === "TAG_NO" || attId === "TAG_DESC") { return false; } if (attId === "status") { return false; } // SPREAD_ITEM인 경우: editableFields 체크 // if (templateType === 'SPREAD_ITEM') { // return editableFields.includes(attId); // } // SPREAD_LIST 또는 GRD_LIST인 경우: 개별 행의 편집 가능성도 고려 if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { // 기본적으로 editableFields에 포함되어야 함 // if (!editableFields.includes(attId)) { // return false; // } // rowData가 제공된 경우 해당 행의 shi 상태도 확인 if (rowData && rowData.shi === true) { return false; } return true; } return true; }, [templateType, columnsJSON, editableFields]); // 편집 가능한 필드 개수 계산 const editableFieldsCount = React.useMemo(() => { return cellMappings.filter(m => m.isEditable).length; }, [cellMappings]); // 셀 주소를 행과 열로 변환하는 함수 const parseCellAddress = (address: string): { row: number, col: number } | null => { if (!address || address.trim() === "") return null; const match = address.match(/^([A-Z]+)(\d+)$/); if (!match) return null; const [, colStr, rowStr] = match; let col = 0; for (let i = 0; i < colStr.length; i++) { col = col * 26 + (colStr.charCodeAt(i) - 65 + 1); } col -= 1; const row = parseInt(rowStr) - 1; return { row, col }; }; // 행과 열을 셀 주소로 변환하는 함수 (GRD_LIST용) const getCellAddress = (row: number, col: number): string => { let colStr = ''; let colNum = col; while (colNum >= 0) { colStr = String.fromCharCode((colNum % 26) + 65) + colStr; colNum = Math.floor(colNum / 26) - 1; } return colStr + (row + 1); }; // 데이터 타입 검증 함수 const validateCellValue = (value: any, columnType: ColumnType, options?: string[]): string | null => { if (value === undefined || value === null || value === "") { return null; // 빈 값은 별도 required 검증에서 처리 } switch (columnType) { case "NUMBER": if (isNaN(Number(value))) { return "Value must be a valid number"; } break; case "LIST": if (options && !options.includes(String(value))) { return `Value must be one of: ${options.join(", ")}`; } break; case "STRING": // STRING 타입은 대부분의 값을 허용 break; default: // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음 break; } return null; }; // 전체 데이터 검증 함수 const validateAllData = React.useCallback(() => { if (!currentSpread || !selectedTemplate) return []; const activeSheet = currentSpread.getActiveSheet(); const errors: ValidationError[] = []; cellMappings.forEach(mapping => { const columnConfig = columnsJSON.find(col => col.key === mapping.attId); if (!columnConfig) return; const cellPos = parseCellAddress(mapping.cellAddress); if (!cellPos) return; if (templateType === 'SPREAD_ITEM') { // 단일 행 검증 const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); if (errorMessage) { errors.push({ cellAddress: mapping.cellAddress, attId: mapping.attId, value: cellValue, expectedType: columnConfig.type, message: errorMessage }); } } else if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴 const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); if (errorMessage) { errors.push({ cellAddress: mapping.cellAddress, attId: mapping.attId, value: cellValue, expectedType: columnConfig.type, message: errorMessage }); } } }); setValidationErrors(errors); return errors; }, [currentSpread, selectedTemplate, cellMappings, columnsJSON, templateType]); // ═══════════════════════════════════════════════════════════════════════════════ // 🛠️ 헬퍼 함수들 // ═══════════════════════════════════════════════════════════════════════════════ // 🎨 셀 스타일 생성 const createCellStyle = React.useCallback((isEditable: boolean) => { const style = new GC.Spread.Sheets.Style(); if (isEditable) { style.backColor = "#f0fdf4"; // 연한 초록 (편집 가능) } else { style.backColor = "#f9fafb"; // 연한 회색 (읽기 전용) style.foreColor = "#6b7280"; } return style; }, []); // 🎯 드롭다운 설정 const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { try { console.log(`🎯 Setting up dropdown for ${rowCount} rows with options:`, options); // ✅ options 정규화 const safeOptions = options .filter(opt => opt !== null && opt !== undefined && opt !== '') .map(opt => String(opt).trim()) .filter(opt => opt.length > 0) .filter((opt, index, arr) => arr.indexOf(opt) === index) .slice(0, 20); if (safeOptions.length === 0) { console.warn(`⚠️ No valid options found, skipping`); return; } console.log(`📋 Safe options:`, safeOptions); // ✅ DataValidation용 문자열 준비 const optionsString = safeOptions.join(','); // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성! for (let i = 0; i < rowCount; i++) { try { const targetRow = cellPos.row + i; // ✅ 각 셀마다 새로운 ComboBox 인스턴스 생성 const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); comboBoxCellType.items(safeOptions); // 배열로 전달 comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); // ✅ 각 셀마다 새로운 DataValidation 인스턴스 생성 const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(optionsString); // ComboBox + DataValidation 둘 다 적용 activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType); activeSheet.setDataValidator(targetRow, cellPos.col, cellValidator); // 셀 잠금 해제 const cell = activeSheet.getCell(targetRow, cellPos.col); cell.locked(false); console.log(`✅ Individual dropdown applied to [${targetRow}, ${cellPos.col}]`); } catch (cellError) { console.warn(`⚠️ Failed to apply to row ${cellPos.row + i}:`, cellError); } } console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`); } catch (error) { console.error('❌ Dropdown setup failed:', error); } }, []); // 🛡️ 안전한 시트 검증 함수 추가 const validateActiveSheet = React.useCallback((activeSheet: any, functionName: string = 'unknown') => { console.log(`🔍 Validating activeSheet for ${functionName}:`); if (!activeSheet) { console.error(`❌ activeSheet is null/undefined in ${functionName}`); return false; } console.log(`✅ activeSheet exists (type: ${typeof activeSheet})`); console.log(`✅ constructor: ${activeSheet.constructor?.name}`); // 핵심 메서드들 존재 여부 확인 const requiredMethods = ['getRowCount', 'getColumnCount', 'setRowCount', 'setColumnCount', 'getCell', 'getValue', 'setStyle']; const missingMethods = requiredMethods.filter(method => typeof activeSheet[method] !== 'function'); if (missingMethods.length > 0) { console.error(`❌ Missing methods in ${functionName}:`, missingMethods); console.log(`📋 Available methods:`, Object.getOwnPropertyNames(activeSheet).filter(prop => typeof activeSheet[prop] === 'function').slice(0, 20)); return false; } console.log(`✅ All required methods available for ${functionName}`); return true; }, []); // 🛡️ 안전한 ActiveSheet 가져오기 함수 const getSafeActiveSheet = React.useCallback((spread: any, functionName: string = 'unknown') => { console.log(`🔍 Getting safe activeSheet for ${functionName}`); if (!spread) { console.error(`❌ Spread is null/undefined in ${functionName}`); return null; } try { // 현재 활성 시트 가져오기 let activeSheet = spread.getActiveSheet(); if (!activeSheet) { console.warn(`⚠️ ActiveSheet is null, attempting to get first sheet in ${functionName}`); // 첫 번째 시트 시도 const sheetCount = spread.getSheetCount(); console.log(`📊 Total sheets: ${sheetCount}`); if (sheetCount > 0) { activeSheet = spread.getSheet(0); if (activeSheet) { spread.setActiveSheetIndex(0); console.log(`✅ Successfully got first sheet in ${functionName}`); } } } if (!activeSheet) { console.error(`❌ Failed to get any valid sheet in ${functionName}`); return null; } // 시트 유효성 검증 const validation = validateActiveSheet(activeSheet, functionName); if (!validation) { console.error(`❌ Sheet validation failed in ${functionName}`); return null; } console.log(`✅ Got valid activeSheet for ${functionName}: ${activeSheet.name?.() || 'unnamed'}`); return activeSheet; } catch (error) { console.error(`❌ Error getting activeSheet in ${functionName}:`, error); return null; } }, [validateActiveSheet]); // 🛡️ 수정된 ensureRowCapacity 함수 const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => { try { // 🔍 상세한 null/undefined 체크 if (!activeSheet) { console.error('❌ activeSheet is null/undefined in ensureRowCapacity'); return false; } console.log('🔍 ActiveSheet validation in ensureRowCapacity:'); console.log(' - Type:', typeof activeSheet); console.log(' - Constructor:', activeSheet.constructor?.name); console.log(' - Is null:', activeSheet === null); console.log(' - Is undefined:', activeSheet === undefined); // 🔍 메서드 존재 여부 확인 if (typeof activeSheet.getRowCount !== 'function') { console.error('❌ getRowCount method does not exist on activeSheet'); console.log('📋 Available properties:', Object.getOwnPropertyNames(activeSheet).slice(0, 20)); return false; } // 🔍 시트 상태 확인 const currentRowCount = activeSheet.getRowCount(); console.log(`📊 Current row count: ${currentRowCount} (type: ${typeof currentRowCount})`); if (typeof currentRowCount !== 'number' || isNaN(currentRowCount)) { console.error('❌ getRowCount returned invalid value:', currentRowCount); return false; } if (requiredRowCount > currentRowCount) { // 🔍 setRowCount 메서드 확인 if (typeof activeSheet.setRowCount !== 'function') { console.error('❌ setRowCount method does not exist on activeSheet'); return false; } const newRowCount = requiredRowCount + 10; activeSheet.setRowCount(newRowCount); console.log(`📈 Expanded sheet: ${currentRowCount} → ${newRowCount} rows`); // 🔍 설정 후 검증 const verifyRowCount = activeSheet.getRowCount(); console.log(`✅ Verified new row count: ${verifyRowCount}`); return true; } else { console.log(`✅ Sheet already has sufficient rows: ${currentRowCount} >= ${requiredRowCount}`); return true; } } catch (error) { console.error('❌ Error in ensureRowCapacity:', error); console.error('❌ Error stack:', error.stack); return false; } }, []); // 🛡️ 안전한 컬럼 용량 확보 함수 const ensureColumnCapacity = React.useCallback((activeSheet: any, requiredColumnCount: number) => { try { // 🔍 상세한 null/undefined 체크 if (!activeSheet) { console.error('❌ activeSheet is null/undefined in ensureColumnCapacity'); return false; } console.log('🔍 ActiveSheet validation in ensureColumnCapacity:'); console.log(' - Type:', typeof activeSheet); console.log(' - Constructor:', activeSheet.constructor?.name); console.log(' - Is null:', activeSheet === null); console.log(' - Is undefined:', activeSheet === undefined); // 🔍 메서드 존재 여부 확인 if (typeof activeSheet.getColumnCount !== 'function') { console.error('❌ getColumnCount method does not exist on activeSheet'); console.log('📋 Available properties:', Object.getOwnPropertyNames(activeSheet).slice(0, 20)); return false; } const currentColumnCount = activeSheet.getColumnCount(); console.log(`📊 Current column count: ${currentColumnCount} (type: ${typeof currentColumnCount})`); if (typeof currentColumnCount !== 'number' || isNaN(currentColumnCount)) { console.error('❌ getColumnCount returned invalid value:', currentColumnCount); return false; } if (requiredColumnCount > currentColumnCount) { if (typeof activeSheet.setColumnCount !== 'function') { console.error('❌ setColumnCount method does not exist on activeSheet'); return false; } const newColumnCount = requiredColumnCount + 10; activeSheet.setColumnCount(newColumnCount); console.log(`📈 Expanded columns: ${currentColumnCount} → ${newColumnCount}`); // 🔍 설정 후 검증 const verifyColumnCount = activeSheet.getColumnCount(); console.log(`✅ Verified new column count: ${verifyColumnCount}`); return true; } else { console.log(`✅ Sheet already has sufficient columns: ${currentColumnCount} >= ${requiredColumnCount}`); return true; } } catch (error) { console.error('❌ Error in ensureColumnCapacity:', error); console.error('❌ Error stack:', error.stack); return false; } }, []); // 🎯 텍스트 너비 계산 함수들 (createGrdListTable 함수 위에 추가) const measureTextWidth = React.useCallback((text: string, fontSize: number = 12, fontFamily: string = 'Arial'): number => { // Canvas를 사용한 정확한 텍스트 너비 측정 const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); if (!context) return text.length * 8; // fallback context.font = `${fontSize}px ${fontFamily}`; const metrics = context.measureText(text || ''); return Math.ceil(metrics.width); }, []); const calculateColumnWidth = React.useCallback(( headerText: string, dataValues: any[] = [], minWidth: number = 80, maxWidth: number = 300, padding: number = 20 ): number => { // 헤더 텍스트 너비 계산 const headerWidth = measureTextWidth(headerText, 12, 'Arial'); // 데이터 값들의 최대 너비 계산 let maxDataWidth = 0; if (dataValues.length > 0) { maxDataWidth = Math.max( ...dataValues .slice(0, 10) // 성능을 위해 처음 10개만 샘플링 .map(value => measureTextWidth(String(value || ''), 11, 'Arial')) ); } // 헤더와 데이터 중 더 큰 너비 + 패딩 적용 const calculatedWidth = Math.max(headerWidth, maxDataWidth) + padding; // 최소/최대 너비 제한 적용 return Math.min(Math.max(calculatedWidth, minWidth), maxWidth); }, [measureTextWidth]); const setOptimalColumnWidths = React.useCallback((activeSheet: any, columns: any[], startCol: number, tableData: any[]) => { console.log('🎨 Setting optimal column widths...'); columns.forEach((column, colIndex) => { const targetCol = startCol + colIndex; // 해당 컬럼의 데이터 값들 추출 const dataValues = tableData.map(row => row[column.key]).filter(val => val != null); // 최적 너비 계산 const optimalWidth = calculateColumnWidth( column.label || column.key, dataValues, column.type === 'NUMBER' ? 100 : 80, // 숫자는 좀 더 넓게 column.type === 'STRING' ? 250 : 200, // 문자열은 더 넓게 column.type === 'LIST' ? 30 : 20 // 드롭다운은 여유 패딩 ); // 컬럼 너비 설정 activeSheet.setColumnWidth(targetCol, optimalWidth); console.log(`📏 Column ${targetCol} (${column.key}): width set to ${optimalWidth}px`); }); }, [calculateColumnWidth]); // 🔍 컬럼 그룹 분석 함수 const analyzeColumnGroups = React.useCallback((columns: DataTableColumnJSON[]) => { const groups: Array<{ head: string; isGroup: boolean; columns: DataTableColumnJSON[]; }> = []; let i = 0; while (i < columns.length) { const currentCol = columns[i]; // head가 없거나 빈 문자열인 경우 단일 컬럼으로 처리 if (!currentCol.head || !currentCol.head.trim()) { groups.push({ head: '', isGroup: false, columns: [currentCol] }); 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++; } // 그룹 추가 groups.push({ head: groupHead, isGroup: groupColumns.length > 1, columns: groupColumns }); i = j; // 다음 그룹으로 이동 } return { groups }; }, []); // 🆕 수정된 createGrdListTable 함수 // 🆕 개선된 GRD_LIST용 동적 테이블 생성 함수 const createGrdListTable = React.useCallback((activeSheet: any, template: TemplateItem) => { console.log('🏗️ Creating GRD_LIST table'); // columnsJSON의 visible 컬럼들을 seq 순서로 정렬하여 사용 const visibleColumns = columnsJSON .filter(col => col.hidden !== true) .sort((a, b) => { const seqA = a.seq !== undefined ? a.seq : 999999; const seqB = b.seq !== undefined ? b.seq : 999999; return seqA - seqB; }); console.log('📊 Using columns:', visibleColumns.map(c => `${c.key}(seq:${c.seq})`)); console.log(`📊 Total visible columns: ${visibleColumns.length}`); if (visibleColumns.length === 0) { console.warn('❌ No visible columns found in columnsJSON'); return []; } // ⭐ 컬럼 용량 확보 const startCol = 1; const requiredColumnCount = startCol + visibleColumns.length; ensureColumnCapacity(activeSheet, requiredColumnCount); // 테이블 생성 시작 const mappings: CellMapping[] = []; // 🔍 그룹 헤더 분석 const groupInfo = analyzeColumnGroups(visibleColumns); const hasGroups = groupInfo.groups.length > 0; // 헤더 행 계산: 그룹이 있으면 2행, 없으면 1행 const groupHeaderRow = 0; const columnHeaderRow = hasGroups ? 1 : 0; const dataStartRow = hasGroups ? 2 : 1; // 🎨 헤더 스타일 생성 const groupHeaderStyle = new GC.Spread.Sheets.Style(); groupHeaderStyle.backColor = "#1e40af"; groupHeaderStyle.foreColor = "#ffffff"; groupHeaderStyle.font = "bold 13px Arial"; groupHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; groupHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center; const columnHeaderStyle = new GC.Spread.Sheets.Style(); columnHeaderStyle.backColor = "#3b82f6"; columnHeaderStyle.foreColor = "#ffffff"; columnHeaderStyle.font = "bold 12px Arial"; columnHeaderStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; columnHeaderStyle.vAlign = GC.Spread.Sheets.VerticalAlign.center; let currentCol = startCol; // 🏗️ 그룹 헤더 및 컬럼 헤더 생성 if (hasGroups) { // 그룹 헤더가 있는 경우 groupInfo.groups.forEach(group => { if (group.isGroup) { // 그룹 헤더 생성 및 병합 const groupStartCol = currentCol; const groupEndCol = currentCol + group.columns.length - 1; // 그룹 헤더 셀 설정 const groupHeaderCell = activeSheet.getCell(groupHeaderRow, groupStartCol); groupHeaderCell.value(group.head); // 그룹 헤더 병합 if (group.columns.length > 1) { activeSheet.addSpan(groupHeaderRow, groupStartCol, 1, group.columns.length); } // 그룹 헤더 스타일 적용 for (let col = groupStartCol; col <= groupEndCol; col++) { activeSheet.setStyle(groupHeaderRow, col, groupHeaderStyle); activeSheet.getCell(groupHeaderRow, col).locked(true); } console.log(`📝 Group Header [${groupHeaderRow}, ${groupStartCol}-${groupEndCol}]: ${group.head}`); // 그룹 내 개별 컬럼 헤더 생성 group.columns.forEach((column, index) => { const colIndex = groupStartCol + index; const columnHeaderCell = activeSheet.getCell(columnHeaderRow, colIndex); columnHeaderCell.value(column.label); activeSheet.setStyle(columnHeaderRow, colIndex, columnHeaderStyle); columnHeaderCell.locked(true); console.log(`📝 Column Header [${columnHeaderRow}, ${colIndex}]: ${column.label}`); }); currentCol += group.columns.length; } else { // 그룹이 아닌 단일 컬럼 const column = group.columns[0]; // 그룹 헤더 행에는 빈 셀 const groupHeaderCell = activeSheet.getCell(groupHeaderRow, currentCol); groupHeaderCell.value(""); activeSheet.setStyle(groupHeaderRow, currentCol, groupHeaderStyle); groupHeaderCell.locked(true); // 컬럼 헤더 생성 const columnHeaderCell = activeSheet.getCell(columnHeaderRow, currentCol); columnHeaderCell.value(column.label); activeSheet.setStyle(columnHeaderRow, currentCol, columnHeaderStyle); columnHeaderCell.locked(true); console.log(`📝 Single Column [${columnHeaderRow}, ${currentCol}]: ${column.label}`); currentCol++; } }); } else { // 그룹이 없는 경우 visibleColumns.forEach((column, colIndex) => { const targetCol = startCol + colIndex; const columnConfig = columnsJSON.find(col => col.key === column.key); // 📋 각 행마다 개별 셀 설정 tableData.forEach((rowData, rowIndex) => { const targetRow = dataStartRow + rowIndex; const cell = activeSheet.getCell(targetRow, targetCol); const value = rowData[column.key]; const cellEditable = isFieldEditable(column.key, rowData); // 🔧 새로 추가: 셀 타입 및 편집기 설정 if (columnConfig) { setupCellTypeAndEditor(activeSheet, { row: targetRow, col: targetCol }, columnConfig, cellEditable, 1); } // 값 설정 cell.value(value ?? null); // 스타일 설정 const style = createCellStyle(cellEditable); activeSheet.setStyle(targetRow, targetCol, style); // 개별 매핑 추가 mappings.push({ attId: column.key, cellAddress: getCellAddress(targetRow, targetCol), isEditable: cellEditable, dataRowIndex: rowIndex }); }); }); } // 🔄 데이터 행 및 매핑 생성 (SPREAD_LIST 방식과 동일한 로직) const dataRowCount = tableData.length; ensureRowCapacity(activeSheet, dataStartRow + dataRowCount); // 📋 각 컬럼별로 매핑 생성 (SPREAD_LIST와 동일한 방식) visibleColumns.forEach((column, colIndex) => { const targetCol = startCol + colIndex; console.log(`🔄 Processing column ${column.key} with ${dataRowCount} rows`); // 📋 각 행마다 개별 매핑 생성 (SPREAD_LIST와 동일) tableData.forEach((rowData, rowIndex) => { const targetRow = dataStartRow + rowIndex; const cellAddress = getCellAddress(targetRow, targetCol); // 🛡️ readonly 체크 (SPREAD_LIST와 동일한 로직) const cellEditable = isFieldEditable(column.key, rowData); // 개별 매핑 추가 mappings.push({ attId: column.key, cellAddress: cellAddress, isEditable: cellEditable, dataRowIndex: rowIndex }); console.log(`📝 Mapping ${column.key} Row ${rowIndex}: ${cellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`); }); // 📋 LIST 타입 드롭다운 설정 (편집 가능한 행이 있는 경우만) if (column.type === "LIST" && column.options) { const hasEditableRows = tableData.some((rowData) => isFieldEditable(column.key, rowData)); if (hasEditableRows) { const cellPos = { row: dataStartRow, col: targetCol }; setupOptimizedListValidation(activeSheet, cellPos, column.options, dataRowCount); console.log(`📋 Dropdown set for ${column.key}: ${hasEditableRows ? 'Has editable rows' : 'All readonly'}`); } } }); // 🎨 개별 셀 데이터 및 스타일 설정 (SPREAD_LIST와 동일한 방식) tableData.forEach((rowData, rowIndex) => { const targetRow = dataStartRow + rowIndex; visibleColumns.forEach((column, colIndex) => { const targetCol = startCol + colIndex; const cell = activeSheet.getCell(targetRow, targetCol); const value = rowData[column.key]; // 값 설정 cell.value(value ?? null); // 🛡️ 편집 권한 및 스타일 재확인 (SPREAD_LIST와 동일) const cellEditable = isFieldEditable(column.key, rowData); cell.locked(!cellEditable); const style = createCellStyle(cellEditable); activeSheet.setStyle(targetRow, targetCol, style); // 🔍 디버깅: readonly 상태 로깅 if (!cellEditable) { const columnConfig = columnsJSON.find(col => col.key === column.key); const reasons = []; if (columnConfig?.shi === true) { reasons.push('column.shi=true'); } if (rowData.shi === true) { reasons.push('row.shi=true'); } if (!editableFields.includes(column.key) && column.key !== "TAG_NO" && column.key !== "TAG_DESC") { reasons.push('not in editableFields'); } console.log(`🔒 ReadOnly [${targetRow}, ${targetCol}] ${column.key}: ${reasons.join(', ')}`); } }); }); // 🎨 컬럼 너비 자동 설정 setOptimalColumnWidths(activeSheet, visibleColumns, startCol, tableData); console.log(`🏗️ GRD_LIST table created with ${mappings.length} mappings, hasGroups: ${hasGroups}`); console.log(`📊 Readonly analysis:`); console.log(` Total cells: ${mappings.length}`); console.log(` Editable cells: ${mappings.filter(m => m.isEditable).length}`); console.log(` Readonly cells: ${mappings.filter(m => !m.isEditable).length}`); return mappings; }, [tableData, columnsJSON, isFieldEditable, createCellStyle, ensureRowCapacity, ensureColumnCapacity, setupOptimizedListValidation, setOptimalColumnWidths, editableFields, getCellAddress, analyzeColumnGroups]); // 🛡️ 추가: readonly 상태 확인 헬퍼 함수 const analyzeReadonlyStatus = React.useCallback((column: DataTableColumnJSON, rowData: GenericData) => { const reasons: string[] = []; // 1. 컬럼 자체가 readonly인지 확인 if (column.shi === true) { reasons.push('Column marked as readonly (shi=true)'); } // 2. 행 자체가 readonly인지 확인 if (rowData.shi === true) { reasons.push('Row marked as readonly (shi=true)'); } // 3. editableFields에 포함되지 않은 경우 if (!editableFields.includes(column.key) && column.key !== "TAG_NO" && column.key !== "TAG_DESC") { reasons.push('Not in editable fields list'); } // 4. 특수 필드 체크 if (column.key === "TAG_NO" || column.key === "TAG_DESC") { // TAG_NO와 TAG_DESC는 기본 편집 가능하지만 다른 조건들은 적용됨 if (column.shi === true || rowData.shi === true) { // 다른 readonly 조건이 있으면 적용 } else { return { isEditable: true, reasons: ['Default editable field'] }; } } const isEditable = reasons.length === 0; return { isEditable, reasons: isEditable ? ['Editable'] : reasons }; }, [editableFields]); // 🛡️ 수정된 시트 보호 및 이벤트 설정 함수 const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => { console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`); // 🔧 1단계: 먼저 시트 보호를 완전히 해제하고 강력한 잠금 해제 실행 console.log('🔓 Step 1: Forcing unlock all editable cells...'); activeSheet.options.isProtected = false; // 🔧 2단계: 모든 편집 가능한 셀에 대해 강제 잠금 해제 및 CellType 설정 mappings.forEach((mapping, index) => { if (!mapping.isEditable) return; const cellPos = parseCellAddress(mapping.cellAddress); if (!cellPos) return; try { const cell = activeSheet.getCell(cellPos.row, cellPos.col); const columnConfig = columnsJSON.find(col => col.key === mapping.attId); // 강제 잠금 해제 cell.locked(false); // CellType 명시적 설정 if (columnConfig?.type === "LIST" && columnConfig.options) { // LIST 타입: ComboBox 설정 const comboBox = new GC.Spread.Sheets.CellTypes.ComboBox(); comboBox.items(columnConfig.options); comboBox.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); activeSheet.setCellType(cellPos.row, cellPos.col, comboBox); console.log(`📋 ComboBox set for ${mapping.attId} at ${mapping.cellAddress}`); } else { // 다른 모든 타입: 기본 텍스트 편집기 설정 const textCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(cellPos.row, cellPos.col, textCellType); console.log(`📝 Text editor set for ${mapping.attId} at ${mapping.cellAddress}`); // NUMBER 타입인 경우에만 validation 추가 (편집은 가능하게) if (columnConfig?.type === "NUMBER") { const numberValidator = GC.Spread.Sheets.DataValidation.createNumberValidator( GC.Spread.Sheets.ConditionalFormatting.ComparisonOperators.between, -999999999, 999999999, true ); numberValidator.showInputMessage(false); numberValidator.showErrorMessage(false); activeSheet.setDataValidator(cellPos.row, cellPos.col, numberValidator); } } // 편집 가능 스타일 명확히 표시 const editableStyle = createCellStyle(true); activeSheet.setStyle(cellPos.row, cellPos.col, editableStyle); console.log(`🔓 Forced unlock: ${mapping.attId} at ${mapping.cellAddress}`); } catch (error) { console.error(`❌ Error processing cell ${mapping.cellAddress}:`, error); } }); // 🔧 3단계: 시트 보호 재설정 (편집 허용하는 설정으로) activeSheet.options.isProtected = true; activeSheet.options.protectionOptions = { allowSelectLockedCells: true, allowSelectUnlockedCells: true, allowSort: false, allowFilter: false, allowEditObjects: true, // ✅ 편집 객체 허용 allowResizeRows: false, allowResizeColumns: false, allowFormatCells: false, allowInsertRows: false, allowInsertColumns: false, allowDeleteRows: false, allowDeleteColumns: false }; // 🔧 4단계: 편집 테스트 실행 console.log('🧪 Testing cell editability...'); const editableMapping = mappings.find(m => m.isEditable); if (editableMapping) { const cellPos = parseCellAddress(editableMapping.cellAddress); if (cellPos) { try { const cell = activeSheet.getCell(cellPos.row, cellPos.col); const testValue = 'TEST_' + Math.random().toString(36).substr(2, 5); const originalValue = cell.value(); console.log(`🧪 Testing ${editableMapping.attId} at ${editableMapping.cellAddress}`); console.log(`🧪 Locked status: ${cell.locked()}`); // 직접 값 설정 테스트 cell.value(testValue); const newValue = cell.value(); if (newValue === testValue) { console.log('✅ Cell edit test PASSED'); cell.value(originalValue); // 원래 값 복원 } else { console.log(`❌ Cell edit test FAILED: ${newValue} !== ${testValue}`); } } catch (testError) { console.error('❌ Edit test error:', testError); } } } // 🎯 변경 감지 이벤트 const changeEvents = [ GC.Spread.Sheets.Events.CellChanged, GC.Spread.Sheets.Events.ValueChanged, GC.Spread.Sheets.Events.ClipboardPasted ]; changeEvents.forEach(eventType => { activeSheet.bind(eventType, () => { console.log(`📝 ${eventType} detected`); setHasChanges(true); }); }); // 🚫 편집 시작 권한 확인 (수정됨) activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { console.log(`🎯 EditStarting: Row ${info.row}, Col ${info.col}`); // ✅ 정확한 매핑 찾기 (행/열 정확히 일치) const exactMapping = mappings.find(m => { const cellPos = parseCellAddress(m.cellAddress); return cellPos && cellPos.row === info.row && cellPos.col === info.col; }); if (!exactMapping) { console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - allowing edit`); return; // 매핑이 없으면 허용 (템플릿 영역 밖) } console.log(`📋 Found mapping: ${exactMapping.attId} at ${exactMapping.cellAddress}, isEditable: ${exactMapping.isEditable}`); // 🔍 추가 디버깅: 셀의 실제 상태 확인 const cell = activeSheet.getCell(info.row, info.col); const isLocked = cell.locked(); const cellValue = cell.value(); console.log(`🔍 Cell state check:`, { attId: exactMapping.attId, isEditable: exactMapping.isEditable, isLocked: isLocked, currentValue: cellValue }); // 🔧 추가: EditStarting 시점에서도 강제 잠금 해제 재시도 if (exactMapping.isEditable && isLocked) { console.log(`🔓 Re-unlocking cell during EditStarting...`); cell.locked(false); // CellType도 재설정 const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId); if (columnConfig?.type !== "LIST") { const textCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(info.row, info.col, textCellType); } } // 기본 편집 권한 확인 if (!exactMapping.isEditable) { console.log(`🚫 Field ${exactMapping.attId} is not editable`); toast.warning(`${exactMapping.attId} field is read-only`); info.cancel = true; return; } // SPREAD_LIST 또는 GRD_LIST 개별 행 SHI 확인 if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && exactMapping.dataRowIndex !== undefined) { const dataRowIndex = exactMapping.dataRowIndex; console.log(`🔍 Checking SHI for data row ${dataRowIndex}`); if (dataRowIndex >= 0 && dataRowIndex < tableData.length) { const rowData = tableData[dataRowIndex]; if (rowData?.shi === true) { console.log(`🚫 Row ${dataRowIndex} is in SHI mode`); toast.warning(`Row ${dataRowIndex + 1}: ${exactMapping.attId} field is read-only (SHI mode)`); info.cancel = true; return; } } else { console.warn(`⚠️ Invalid dataRowIndex: ${dataRowIndex} (tableData.length: ${tableData.length})`); } } console.log(`✅ Edit allowed for ${exactMapping.attId}`); }); // ✅ 편집 완료 검증 (기존 로직 유지) activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (event: any, info: any) => { console.log(`🏁 EditEnded: Row ${info.row}, Col ${info.col}, New value: ${activeSheet.getValue(info.row, info.col)}`); // ✅ 정확한 매핑 찾기 const exactMapping = mappings.find(m => { const cellPos = parseCellAddress(m.cellAddress); return cellPos && cellPos.row === info.row && cellPos.col === info.col; }); if (!exactMapping) { console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - skipping validation`); return; } const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId); if (columnConfig) { const cellValue = activeSheet.getValue(info.row, info.col); console.log(`🔍 Validating ${exactMapping.attId}: "${cellValue}"`); const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); const cell = activeSheet.getCell(info.row, info.col); if (errorMessage) { console.log(`❌ Validation failed: ${errorMessage}`); // 🚨 에러 스타일 적용 (편집 가능 상태 유지) const errorStyle = new GC.Spread.Sheets.Style(); errorStyle.backColor = "#fef2f2"; errorStyle.foreColor = "#dc2626"; errorStyle.borderLeft = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); errorStyle.borderRight = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); errorStyle.borderTop = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); errorStyle.borderBottom = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); activeSheet.setStyle(info.row, info.col, errorStyle); cell.locked(!exactMapping.isEditable); // 편집 가능 상태 유지 toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}. Please correct the value.`, { duration: 5000 }); } else { console.log(`✅ Validation passed`); // ✅ 정상 스타일 복원 const normalStyle = createCellStyle(exactMapping.isEditable); activeSheet.setStyle(info.row, info.col, normalStyle); cell.locked(!exactMapping.isEditable); } } // 🔄 변경 상태 업데이트 setHasChanges(true); }); // 🔧 5단계: 설정 완료 후 1초 뒤에 추가 잠금 해제 실행 (안전장치) setTimeout(() => { console.log('🔄 Running safety unlock after 1 second...'); mappings.forEach(mapping => { if (!mapping.isEditable) return; const cellPos = parseCellAddress(mapping.cellAddress); if (!cellPos) return; try { const cell = activeSheet.getCell(cellPos.row, cellPos.col); if (cell.locked()) { console.log(`🔓 Safety unlock: ${mapping.attId}`); cell.locked(false); } } catch (error) { console.error(`❌ Safety unlock error for ${mapping.cellAddress}:`, error); } }); }, 1000); console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`); console.log(`🔓 Editable cells: ${mappings.filter(m => m.isEditable).length}`); console.log(`🔒 Readonly cells: ${mappings.filter(m => !m.isEditable).length}`); }, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]); // 🔧 셀 타입 및 편집기 설정 함수 (initSpread 함수 내부에 추가) const setupCellTypeAndEditor = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, columnConfig: DataTableColumnJSON, isEditable: boolean, rowCount: number = 1) => { console.log(`🔧 Setting up cell type for ${columnConfig.key} (${columnConfig.type}) at [${cellPos.row}, ${cellPos.col}]`); try { // 편집 가능한 셀에만 적절한 셀 타입 설정 if (isEditable) { for (let i = 0; i < rowCount; i++) { const targetRow = cellPos.row + i; const cell = activeSheet.getCell(targetRow, cellPos.col); // 셀 잠금 해제 cell.locked(false); switch (columnConfig.type) { case "LIST": // 드롭다운은 기존 setupOptimizedListValidation 함수에서 처리 break; case "NUMBER": // 숫자 입력용 셀 타입 설정 const numberCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(targetRow, cellPos.col, numberCellType); // 숫자 validation 설정 (선택사항) const numberValidator = GC.Spread.Sheets.DataValidation.createNumberValidator( GC.Spread.Sheets.ConditionalFormatting.ComparisonOperators.between, -999999999, 999999999, true ); numberValidator.showInputMessage(true); numberValidator.inputTitle("Number Input"); numberValidator.inputMessage("Please enter a valid number"); activeSheet.setDataValidator(targetRow, cellPos.col, numberValidator); break; case "STRING": default: // 기본 텍스트 입력용 셀 타입 설정 const textCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(targetRow, cellPos.col, textCellType); break; } console.log(`✅ Cell type set for [${targetRow}, ${cellPos.col}]: ${columnConfig.type}`); } } else { // 읽기 전용 셀 설정 for (let i = 0; i < rowCount; i++) { const targetRow = cellPos.row + i; const cell = activeSheet.getCell(targetRow, cellPos.col); cell.locked(true); } } } catch (error) { console.error(`❌ Error setting cell type for ${columnConfig.key}:`, error); } }, []); // ═══════════════════════════════════════════════════════════════════════════════ // 🏗️ 메인 SpreadSheets 초기화 함수 // ═══════════════════════════════════════════════════════════════════════════════ // 🛡️ 수정된 initSpread 함수 - activeSheet 참조 문제 해결 const initSpread = React.useCallback((spread: any, template?: TemplateItem) => { const workingTemplate = template || selectedTemplate; if (!spread || !workingTemplate) { console.error('❌ Invalid spread or template in initSpread'); return; } try { // 🔄 초기 설정 setCurrentSpread(spread); setHasChanges(false); setValidationErrors([]); // 성능을 위한 렌더링 일시 중단 spread.suspendPaint(); try { // ⚠️ 초기 activeSheet 가져오기 let activeSheet = getSafeActiveSheet(spread, 'initSpread-initial'); if (!activeSheet) { throw new Error('Failed to get initial activeSheet'); } // 시트 보호 해제 (편집을 위해) activeSheet.options.isProtected = false; let mappings: CellMapping[] = []; // 🆕 GRD_LIST 처리 if (templateType === 'GRD_LIST' && workingTemplate.GRD_LST_SETUP) { console.log('🏗️ Processing GRD_LIST template'); // 기본 워크북 설정 spread.clearSheets(); spread.addSheet(0); const sheet = spread.getSheet(0); sheet.name('Data'); spread.setActiveSheet('Data'); // 동적 테이블 생성 mappings = createGrdListTable(sheet, workingTemplate); } else { // 🔍 SPREAD_LIST 및 SPREAD_ITEM 처리 let contentJson = null; let dataSheets = null; // SPR_LST_SETUP.CONTENT 우선 사용 if (workingTemplate.SPR_LST_SETUP?.CONTENT) { contentJson = workingTemplate.SPR_LST_SETUP.CONTENT; dataSheets = workingTemplate.SPR_LST_SETUP.DATA_SHEETS; console.log('✅ Using SPR_LST_SETUP for template:', workingTemplate.NAME); } // SPR_ITM_LST_SETUP.CONTENT 대안 사용 else if (workingTemplate.SPR_ITM_LST_SETUP?.CONTENT) { contentJson = workingTemplate.SPR_ITM_LST_SETUP.CONTENT; dataSheets = workingTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS; console.log('✅ Using SPR_ITM_LST_SETUP for template:', workingTemplate.NAME); } if (!contentJson) { throw new Error(`No template content found for ${workingTemplate.NAME}`); } if (!dataSheets || dataSheets.length === 0) { throw new Error(`No data sheets configuration found for ${workingTemplate.NAME}`); } console.log('🔍 Template info:', { templateName: workingTemplate.NAME, templateType: templateType, dataSheetsCount: dataSheets.length, hasSelectedRow: !!selectedRow, tableDataLength: tableData.length }); // 🏗️ SpreadSheets 템플릿 로드 const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; console.log('📥 Loading template JSON...'); spread.fromJSON(jsonData); console.log('✅ Template JSON loaded'); // ⚠️ 중요: 템플릿 로드 후 activeSheet 다시 가져오기 activeSheet = getSafeActiveSheet(spread, 'initSpread-after-fromJSON'); if (!activeSheet) { throw new Error('ActiveSheet became null after loading template'); } console.log('🔍 Active sheet after template load:', { name: activeSheet.name?.() || 'unnamed', rowCount: activeSheet.getRowCount(), colCount: activeSheet.getColumnCount() }); // 시트 보호 다시 해제 (템플릿 로드 후 다시 설정될 수 있음) activeSheet.options.isProtected = false; // 📊 데이터 매핑 및 로딩 처리 console.log(`🔄 Processing ${dataSheets.length} data sheets`); dataSheets.forEach((dataSheet, sheetIndex) => { console.log(`📋 Processing data sheet ${sheetIndex}:`, { sheetName: dataSheet.SHEET_NAME, mappingCount: dataSheet.MAP_CELL_ATT?.length || 0 }); if (dataSheet.MAP_CELL_ATT && dataSheet.MAP_CELL_ATT.length > 0) { dataSheet.MAP_CELL_ATT.forEach((mapping, mappingIndex) => { const { ATT_ID, IN } = mapping; if (!ATT_ID || !IN || IN.trim() === "") { console.warn(`⚠️ Invalid mapping: ATT_ID=${ATT_ID}, IN=${IN}`); return; } const cellPos = parseCellAddress(IN); if (!cellPos) { console.warn(`⚠️ Invalid cell address: ${IN}`); return; } const columnConfig = columnsJSON.find(col => col.key === ATT_ID); // 🎯 템플릿 타입별 데이터 처리 if (templateType === 'SPREAD_ITEM' && selectedRow) { console.log(`📝 Processing SPREAD_ITEM for ${ATT_ID}`); const isEditable = isFieldEditable(ATT_ID); const value = selectedRow[ATT_ID]; // 매핑 정보 저장 mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: isEditable, dataRowIndex: 0 }); // ⚠️ 안전한 셀 참조 및 값 설정 try { const cell = activeSheet.getCell(cellPos.row, cellPos.col); console.log(`🔄 Setting SPREAD_ITEM cell [${cellPos.row}, ${cellPos.col}] ${ATT_ID}: "${value}"`); // 🔧 새로 추가: 셀 타입 및 편집기 설정 setupCellTypeAndEditor(activeSheet, cellPos, columnConfig, isEditable, 1); // 값 설정 cell.value(value ?? null); // 스타일 설정 const style = createCellStyle(isEditable); activeSheet.setStyle(cellPos.row, cellPos.col, style); // LIST 타입 드롭다운 설정 (기존 코드 유지) if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1); } console.log(`✅ SPREAD_ITEM cell set successfully`); } catch (cellError) { console.error(`❌ Error setting SPREAD_ITEM cell:`, cellError); } } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { console.log(`📊 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`); // 🚀 행 확장 - 안전한 방법으로 const requiredRows = cellPos.row + tableData.length; console.log(`🚀 Ensuring ${requiredRows} rows for SPREAD_LIST`); // ⚠️ activeSheet 유효성 재검증 const currentActiveSheet = getSafeActiveSheet(spread, 'ensureRowCapacity'); if (!currentActiveSheet) { console.error(`❌ ActiveSheet is null before ensureRowCapacity`); return; } if (!ensureRowCapacity(currentActiveSheet, requiredRows)) { console.error(`❌ Failed to ensure row capacity for ${requiredRows} rows`); return; } // activeSheet 참조 업데이트 activeSheet = currentActiveSheet; // 매핑 생성 tableData.forEach((rowData, index) => { const targetRow = cellPos.row + index; const targetCellAddress = getCellAddress(targetRow, cellPos.col); const cellEditable = isFieldEditable(ATT_ID, rowData); mappings.push({ attId: ATT_ID, cellAddress: targetCellAddress, isEditable: cellEditable, dataRowIndex: index }); }); // LIST 타입 드롭다운 설정 if (columnConfig?.type === "LIST" && columnConfig.options) { const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData)); if (hasEditableRows) { setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length); } } // 개별 셀 데이터 및 스타일 설정 tableData.forEach((rowData, index) => { const targetRow = cellPos.row + index; try { const cell = activeSheet.getCell(targetRow, cellPos.col); const value = rowData[ATT_ID]; const cellEditable = isFieldEditable(ATT_ID, rowData); console.log(`🔄 Setting SPREAD_LIST Row ${index} ${ATT_ID}: "${value}"`); // 🔧 새로 추가: 각 셀에 대한 타입 및 편집기 설정 setupCellTypeAndEditor(activeSheet, { row: targetRow, col: cellPos.col }, columnConfig, cellEditable, 1); // 값 설정 cell.value(value ?? null); // 스타일 설정 const style = createCellStyle(cellEditable); activeSheet.setStyle(targetRow, cellPos.col, style); } catch (cellError) { console.error(`❌ Error setting SPREAD_LIST cell Row ${index}:`, cellError); } }); console.log(`✅ SPREAD_LIST processing completed for ${ATT_ID}`); } }); } }); } // 💾 매핑 정보 저장 및 이벤트 설정 setCellMappings(mappings); // ⚠️ 최종 activeSheet 재확인 후 이벤트 설정 const finalActiveSheet = getSafeActiveSheet(spread, 'setupSheetProtectionAndEvents'); if (finalActiveSheet) { setupSheetProtectionAndEvents(finalActiveSheet, mappings); } else { console.error('❌ Failed to get activeSheet for events setup'); } console.log(`✅ Template initialization completed with ${mappings.length} mappings`); } finally { // 렌더링 재개 spread.resumePaint(); } } catch (error) { console.error('❌ Error initializing spread:', error); // toast.error(`Failed to load template: ${error.message}`); if (spread?.resumePaint) { spread.resumePaint(); } } }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents, createGrdListTable, getCellAddress, getSafeActiveSheet, validateActiveSheet]); // 변경사항 저장 함수 const handleSaveChanges = React.useCallback(async () => { if (!currentSpread || !hasChanges) { toast.info("No changes to save"); return; } // 저장 전 데이터 검증 const errors = validateAllData(); if (errors.length > 0) { toast.error(`Cannot save: ${errors.length} validation errors found. Please fix them first.`); return; } try { setIsPending(true); const activeSheet = currentSpread.getActiveSheet(); if (templateType === 'SPREAD_ITEM' && selectedRow) { // 단일 행 저장 const dataToSave = { ...selectedRow }; cellMappings.forEach(mapping => { if (mapping.isEditable) { const cellPos = parseCellAddress(mapping.cellAddress); if (cellPos) { const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); dataToSave[mapping.attId] = cellValue; } } }); dataToSave.TAG_NO = selectedRow.TAG_NO; const { success, message } = await updateFormDataInDB( formCode, contractItemId, dataToSave ); if (!success) { toast.error(message); return; } toast.success("Changes saved successfully!"); onUpdateSuccess?.(dataToSave); } else if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) { // 복수 행 저장 (SPREAD_LIST와 GRD_LIST 동일 처리) const updatedRows: GenericData[] = []; let saveCount = 0; for (let i = 0; i < tableData.length; i++) { const originalRow = tableData[i]; const dataToSave = { ...originalRow }; let hasRowChanges = false; // 각 매핑에 대해 해당 행의 값 확인 cellMappings.forEach(mapping => { if (mapping.dataRowIndex === i && mapping.isEditable) { const columnConfig = columnsJSON.find(col => col.key === mapping.attId); const isColumnEditable = columnConfig?.shi !== true; const isRowEditable = originalRow.shi !== true; if (isColumnEditable && isRowEditable) { const cellPos = parseCellAddress(mapping.cellAddress); if (cellPos) { const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); // 값이 변경되었는지 확인 if (cellValue !== originalRow[mapping.attId]) { dataToSave[mapping.attId] = cellValue; hasRowChanges = true; } } } } }); // 변경사항이 있는 행만 저장 if (hasRowChanges) { dataToSave.TAG_NO = originalRow.TAG_NO; // TAG_NO는 절대 변경되지 않도록 const { success } = await updateFormDataInDB( formCode, contractItemId, dataToSave ); if (success) { updatedRows.push(dataToSave); saveCount++; } } else { updatedRows.push(originalRow); // 변경사항이 없으면 원본 유지 } } if (saveCount > 0) { toast.success(`${saveCount} rows saved successfully!`); onUpdateSuccess?.(updatedRows); } else { toast.info("No changes to save"); } } setHasChanges(false); setValidationErrors([]); } catch (error) { console.error("Error saving changes:", error); toast.error("An unexpected error occurred while saving"); } finally { setIsPending(false); } }, [currentSpread, hasChanges, templateType, selectedRow, tableData, formCode, contractItemId, onUpdateSuccess, cellMappings, columnsJSON, validateAllData]); if (!isOpen) return null; // 데이터 유효성 검사 const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0; const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length; return ( SEDP Template - {formCode}
{/* 템플릿 선택 */} {availableTemplates.length > 1 && (
Template:
)} {/* 템플릿 정보 */} {selectedTemplate && (
Template Type: { templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' : 'Grid List View (GRD_LIST)' } {templateType === 'SPREAD_ITEM' && selectedRow && ( • Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'} )} {(templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && ( • {dataCount} rows )} {hasChanges && ( • Unsaved changes )} {validationErrors.length > 0 && ( {validationErrors.length} validation errors )}
)} {/* 범례 */}
Editable fields Read-only fields Validation errors {cellMappings.length > 0 && ( {editableFieldsCount} of {cellMappings.length} fields editable )}
{/* SpreadSheets 컴포넌트 영역 */}
{selectedTemplate && isClient && isDataValid ? ( ) : (
{!isClient ? ( <> Loading... ) : !selectedTemplate ? ( "No template available" ) : !isDataValid ? ( `No ${templateType === 'SPREAD_ITEM' ? 'selected row' : 'data'} available` ) : ( "Template not ready" )}
)}
{hasChanges && ( )} {validationErrors.length > 0 && ( )}
); }