diff options
Diffstat (limited to 'components/form-data/spreadJS-dialog.tsx')
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 736 |
1 files changed, 456 insertions, 280 deletions
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index 1d0796fe..11d37911 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -119,7 +119,7 @@ export function TemplateViewDialog({ const [currentSpread, setCurrentSpread] = React.useState<any>(null); const [cellMappings, setCellMappings] = React.useState<CellMapping[]>([]); const [isClient, setIsClient] = React.useState(false); - const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | null>(null); + const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST' | null>(null); const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]); const [selectedTemplateId, setSelectedTemplateId] = React.useState<string>(""); const [availableTemplates, setAvailableTemplates] = React.useState<TemplateItem[]>([]); @@ -140,21 +140,35 @@ export function TemplateViewDialog({ templates = [templateData as TemplateItem]; } - // CONTENT가 있는 템플릿들 필터링 + // 유효한 템플릿들 필터링 const validTemplates = templates.filter(template => { const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT; const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT; - const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || template.TMPL_TYPE === "SPREAD_ITEM"; + const hasGrdListSetup = template.GRD_LST_SETUP && columnsJSON.length > 0; // GRD_LIST 조건: GRD_LST_SETUP 존재 + columnsJSON 있음 + + const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || + template.TMPL_TYPE === "SPREAD_ITEM" || + template.TMPL_TYPE === "GRD_LIST"; // GRD_LIST 타입 추가 - return isValidType && (hasSpreadListContent || hasSpreadItemContent); + return isValidType && (hasSpreadListContent || hasSpreadItemContent || hasGrdListSetup); }); setAvailableTemplates(validTemplates); // 첫 번째 유효한 템플릿을 기본으로 선택 if (validTemplates.length > 0 && !selectedTemplateId) { - setSelectedTemplateId(validTemplates[0].TMPL_ID); - setTemplateType(validTemplates[0].TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'); + const firstTemplate = validTemplates[0]; + setSelectedTemplateId(firstTemplate.TMPL_ID); + + // 템플릿 타입 결정 + let templateTypeToSet: 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST'; + if (firstTemplate.GRD_LST_SETUP && columnsJSON.length > 0) { + templateTypeToSet = 'GRD_LIST'; + } else { + templateTypeToSet = firstTemplate.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'; + } + + setTemplateType(templateTypeToSet); } }, [templateData, selectedTemplateId]); @@ -163,7 +177,16 @@ export function TemplateViewDialog({ const template = availableTemplates.find(t => t.TMPL_ID === templateId); if (template) { setSelectedTemplateId(templateId); - setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'); + + // 템플릿 타입 결정 + let templateTypeToSet: 'SPREAD_LIST' | 'SPREAD_ITEM' | 'GRD_LIST'; + if (template.GRD_LST_SETUP && columnsJSON.length > 0) { + templateTypeToSet = 'GRD_LIST'; + } else { + templateTypeToSet = template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'; + } + + setTemplateType(templateTypeToSet); setHasChanges(false); setValidationErrors([]); @@ -192,8 +215,8 @@ export function TemplateViewDialog({ return editableFieldsMap.get(selectedRow.TAG_NO) || []; } - // SPREAD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리 - if (templateType === 'SPREAD_LIST' && tableData.length > 0) { + // 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) || []; @@ -221,8 +244,8 @@ export function TemplateViewDialog({ return editableFields.includes(attId); } - // SPREAD_LIST인 경우: 개별 행의 편집 가능성도 고려 - if (templateType === 'SPREAD_LIST') { + // SPREAD_LIST 또는 GRD_LIST인 경우: 개별 행의 편집 가능성도 고려 + if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { // 기본적으로 editableFields에 포함되어야 함 if (!editableFields.includes(attId)) { return false; @@ -236,8 +259,6 @@ export function TemplateViewDialog({ return true; } - // 기본적으로는 editableFields 체크 - // return editableFields.includes(attId); return true; }, [templateType, columnsJSON, editableFields]); @@ -266,6 +287,17 @@ export function TemplateViewDialog({ 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 === "") { @@ -322,7 +354,7 @@ export function TemplateViewDialog({ message: errorMessage }); } - } else if (templateType === 'SPREAD_LIST') { + } else if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴 const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); @@ -359,164 +391,282 @@ export function TemplateViewDialog({ return style; }, []); - // 📋 최적화된 LIST 드롭다운 설정 -const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { - try { - console.log(`🎯 Setting up DataValidation dropdown for ${rowCount} rows with options:`, options); - - // 🚨 성능 임계점 확인 - if (rowCount > 100) { - console.warn(`⚡ Large dataset (${rowCount} rows): Using simple validation only`); - setupSimpleValidation(activeSheet, cellPos, options, rowCount); - return; - } - - // ✅ 1단계: options 철저하게 정규화 (이것이 에러 방지의 핵심!) - let safeOptions; + // 🎯 드롭다운 설정 + const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { try { - safeOptions = options - .filter(opt => opt !== null && opt !== undefined && opt !== '') // null, undefined, 빈값 제거 - .map(opt => { - // 모든 값을 안전한 문자열로 변환 - let str = String(opt); - // 특수 문자나 문제 있는 문자 처리 - str = str.replace(/[\r\n\t]/g, ' '); // 줄바꿈, 탭을 공백으로 - str = str.replace(/[^\x20-\x7E\u00A1-\uFFFF]/g, ''); // 제어 문자 제거 - return str.trim(); - }) - .filter(opt => opt.length > 0 && opt.length < 255) // 빈값과 너무 긴 값 제거 - .filter((opt, index, arr) => arr.indexOf(opt) === index) // 중복 제거 - .slice(0, 100); // 최대 100개로 제한 - - console.log(`📋 Original options:`, options); - console.log(`📋 Safe options:`, safeOptions); - } catch (filterError) { - console.error('❌ Options filtering failed:', filterError); - safeOptions = ['Option1', 'Option2']; // 안전한 폴백 옵션 - } + 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; + } - if (safeOptions.length === 0) { - console.warn(`⚠️ No valid options found, using fallback`); - safeOptions = ['Please Select']; - } + console.log(`📋 Safe options:`, safeOptions); - // ✅ 2단계: DataValidation 생성 (엑셀 스타일 드롭다운) - let validator; - try { - validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions); - console.log(`✅ DataValidation validator created successfully`); - } catch (validatorError) { - console.error('❌ Failed to create validator:', validatorError); - return; - } + // ✅ DataValidation용 문자열 준비 + const optionsString = safeOptions.join(','); - // ✅ 3단계: 셀/범위에 적용 - try { - if (rowCount > 1) { - // 범위에 적용 - const range = activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1); - range.dataValidator(validator); - console.log(`✅ DataValidation applied to range [${cellPos.row}, ${cellPos.col}, ${rowCount}, 1]`); - } else { - // 단일 셀에 적용 - activeSheet.setDataValidator(cellPos.row, cellPos.col, validator); - console.log(`✅ DataValidation applied to single cell [${cellPos.row}, ${cellPos.col}]`); - } - } catch (applicationError) { - console.error('❌ Failed to apply DataValidation:', applicationError); - - // 폴백: 개별 셀에 하나씩 적용 - console.log('🔄 Trying individual cell application...'); - for (let i = 0; i < Math.min(rowCount, 50); i++) { + // 🔑 핵심 수정: 각 셀마다 개별 ComboBox 인스턴스 생성! + for (let i = 0; i < rowCount; i++) { try { - const individualValidator = GC.Spread.Sheets.DataValidation.createListValidator([...safeOptions]); - activeSheet.setDataValidator(cellPos.row + i, cellPos.col, individualValidator); - console.log(`✅ Individual DataValidation set for row ${cellPos.row + i}`); - } catch (individualError) { - console.warn(`⚠️ Failed individual cell ${cellPos.row + i}:`, individualError); + 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(`✅ DataValidation dropdown setup completed`); + console.log(`✅ Safe dropdown setup completed for ${rowCount} cells`); - } catch (error) { - console.error('❌ DataValidation setup failed completely:', error); - console.error('Error stack:', error.stack); - console.log('🔄 Falling back to no validation'); - } -}, []); + } catch (error) { + console.error('❌ Dropdown setup failed:', error); + } + }, []); + + // 🚀 행 용량 확보 + const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => { + const currentRowCount = activeSheet.getRowCount(); + if (requiredRowCount > currentRowCount) { + activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가 + console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`); + } + }, []); -// ⚡ 단순 검증 설정 (드롭다운 없이 검증만) -const setupSimpleValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { - try { - console.log(`⚡ Setting up simple validation (no dropdown UI) for ${rowCount} rows`); + // 🆕 GRD_LIST용 동적 테이블 생성 함수 + const createGrdListTable = React.useCallback((activeSheet: any, template: TemplateItem) => { + console.log('🏗️ Creating GRD_LIST table'); - const safeOptions = options - .filter(opt => opt != null && opt !== '') - .map(opt => String(opt).trim()) - .filter(opt => opt.length > 0); + // columnsJSON의 visible 컬럼들을 seq 순서로 정렬하여 사용 + const visibleColumns = columnsJSON + .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; + }); - if (safeOptions.length === 0) return; + console.log('📊 Using columns:', visibleColumns.map(c => `${c.key}(seq:${c.seq})`)); - const validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions); + if (visibleColumns.length === 0) { + console.warn('❌ No visible columns found in columnsJSON'); + return []; + } + + // 테이블 생성 시작 + const mappings: CellMapping[] = []; + const startCol = 1; // A열 제외하고 B열부터 시작 - // 범위로 적용 시도 - try { - activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1).dataValidator(validator); - console.log(`✅ Simple validation applied to range`); - } catch (rangeError) { - console.warn('Range validation failed, trying individual cells'); - // 폴백: 개별 적용 - for (let i = 0; i < Math.min(rowCount, 100); i++) { - try { - activeSheet.setDataValidator(cellPos.row + i, cellPos.col, validator); - } catch (individualError) { - // 개별 실패해도 계속 + // 🔍 그룹 헤더 분석 + 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 headerCol = startCol + colIndex; + const headerCell = activeSheet.getCell(columnHeaderRow, headerCol); + headerCell.value(column.label); + activeSheet.setStyle(columnHeaderRow, headerCol, columnHeaderStyle); + headerCell.locked(true); + + console.log(`📝 Header [${columnHeaderRow}, ${headerCol}]: ${column.label}`); + }); } - - } catch (error) { - console.error('❌ Simple validation failed:', error); - } -}, []); - -// ═══════════════════════════════════════════════════════════════════════════════ -// 🔍 디버깅용: 에러 발생 원인 추적 -// ═══════════════════════════════════════════════════════════════════════════════ - -const debugDropdownError = (options: any[], attId: string) => { - console.group(`🔍 Debugging dropdown for ${attId}`); - - console.log('Original options type:', typeof options); - console.log('Is array:', Array.isArray(options)); - console.log('Length:', options?.length); - console.log('Raw options:', options); - - if (Array.isArray(options)) { - options.forEach((opt, index) => { - console.log(`[${index}] Type: ${typeof opt}, Value: "${opt}", String: "${String(opt)}"`); + + // 데이터 행 생성 + const dataRowCount = tableData.length; + ensureRowCapacity(activeSheet, dataStartRow + dataRowCount); + + tableData.forEach((rowData, rowIndex) => { + const targetRow = dataStartRow + rowIndex; - // 문제 있는 값 체크 - if (opt === null) console.warn(` ⚠️ NULL value at index ${index}`); - if (opt === undefined) console.warn(` ⚠️ UNDEFINED value at index ${index}`); - if (typeof opt === 'object') console.warn(` ⚠️ OBJECT value at index ${index}:`, opt); - if (typeof opt === 'string' && opt.includes('\n')) console.warn(` ⚠️ NEWLINE in string at index ${index}`); - if (typeof opt === 'string' && opt.length === 0) console.warn(` ⚠️ EMPTY STRING at index ${index}`); + visibleColumns.forEach((column, colIndex) => { + const targetCol = startCol + colIndex; + const cellAddress = getCellAddress(targetRow, targetCol); + const isEditable = isFieldEditable(column.key, rowData); + + // 매핑 정보 추가 + mappings.push({ + attId: column.key, + cellAddress: cellAddress, + isEditable: isEditable, + dataRowIndex: rowIndex + }); + + // 셀 값 설정 + const cell = activeSheet.getCell(targetRow, targetCol); + const value = rowData[column.key]; + cell.value(value ?? null); + + // 스타일 적용 + const style = createCellStyle(isEditable); + activeSheet.setStyle(targetRow, targetCol, style); + cell.locked(!isEditable); + + console.log(`📝 Data [${targetRow}, ${targetCol}]: ${column.key} = "${value}" (${isEditable ? 'Editable' : 'ReadOnly'})`); + + // LIST 타입 드롭다운 설정 + if (column.type === "LIST" && column.options && isEditable) { + setupOptimizedListValidation(activeSheet, { row: targetRow, col: targetCol }, column.options, 1); + } + }); }); - } - - console.groupEnd(); -}; - // 🚀 행 용량 확보 - const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => { - const currentRowCount = activeSheet.getRowCount(); - if (requiredRowCount > currentRowCount) { - activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가 - console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`); + console.log(`🏗️ GRD_LIST table created with ${mappings.length} mappings, hasGroups: ${hasGroups}`); + return mappings; + }, [tableData, columnsJSON, isFieldEditable, createCellStyle, ensureRowCapacity, setupOptimizedListValidation]); + + // 🔍 컬럼 그룹 분석 함수 + 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 }; }, []); // 🛡️ 시트 보호 및 이벤트 설정 @@ -574,8 +724,8 @@ const debugDropdownError = (options: any[], attId: string) => { return; } - // SPREAD_LIST 개별 행 SHI 확인 - if (templateType === 'SPREAD_LIST' && exactMapping.dataRowIndex !== undefined) { + // 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}`); @@ -663,149 +813,167 @@ const debugDropdownError = (options: any[], attId: string) => { setHasChanges(false); setValidationErrors([]); - // 📋 템플릿 콘텐츠 및 데이터 시트 추출 - 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.CONTENT 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.CONTENT for template:', workingTemplate.NAME); - } - - if (!contentJson) { - console.warn('❌ No CONTENT found in template:', workingTemplate.NAME); - return; - } - - // 🏗️ SpreadSheets 초기화 - const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; - // 성능을 위한 렌더링 일시 중단 spread.suspendPaint(); try { - // 템플릿 구조 로드 - spread.fromJSON(jsonData); const activeSheet = spread.getActiveSheet(); // 시트 보호 해제 (편집을 위해) activeSheet.options.isProtected = false; - // 📊 셀 매핑 및 데이터 처리 - if (dataSheets && dataSheets.length > 0) { - const mappings: CellMapping[] = []; + let mappings: CellMapping[] = []; + + // 🆕 GRD_LIST 처리 + if (templateType === 'GRD_LIST' && workingTemplate.GRD_LST_SETUP) { + console.log('🏗️ Processing GRD_LIST template'); - // 🔄 각 데이터 시트의 매핑 정보 처리 - dataSheets.forEach(dataSheet => { - if (dataSheet.MAP_CELL_ATT) { - dataSheet.MAP_CELL_ATT.forEach(mapping => { - const { ATT_ID, IN } = mapping; - - if (IN && IN.trim() !== "") { - const cellPos = parseCellAddress(IN); - if (cellPos) { - const columnConfig = columnsJSON.find(col => col.key === ATT_ID); - - // 🎯 템플릿 타입별 데이터 처리 - if (templateType === 'SPREAD_ITEM' && selectedRow) { - // 📝 단일 행 처리 (SPREAD_ITEM) - const isEditable = isFieldEditable(ATT_ID); - - // 매핑 정보 저장 - mappings.push({ - attId: ATT_ID, - cellAddress: IN, - isEditable: isEditable, - dataRowIndex: 0 - }); - - const cell = activeSheet.getCell(cellPos.row, cellPos.col); - const value = selectedRow[ATT_ID]; - - // 값 설정 - cell.value(value ?? null); - - // 🎨 스타일 및 편집 권한 설정 - cell.locked(!isEditable); - 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); - } + // 기본 워크북 설정 + 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.CONTENT 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.CONTENT for template:', workingTemplate.NAME); + } - } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { - // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨 - console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`); - - // 🚀 행 확장 (필요시) - ensureRowCapacity(activeSheet, cellPos.row + tableData.length); - - // 📋 각 행마다 개별 매핑 생성 - tableData.forEach((rowData, index) => { - const targetRow = cellPos.row + index; - const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`; - const cellEditable = isFieldEditable(ATT_ID, rowData); + if (!contentJson) { + console.warn('❌ No CONTENT found in template:', workingTemplate.NAME); + return; + } + + // 🏗️ SpreadSheets 초기화 + const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; + + // 템플릿 구조 로드 + spread.fromJSON(jsonData); + + // 📊 셀 매핑 및 데이터 처리 + if (dataSheets && dataSheets.length > 0) { + + // 🔄 각 데이터 시트의 매핑 정보 처리 + dataSheets.forEach(dataSheet => { + if (dataSheet.MAP_CELL_ATT) { + dataSheet.MAP_CELL_ATT.forEach(mapping => { + const { ATT_ID, IN } = mapping; + + if (IN && IN.trim() !== "") { + const cellPos = parseCellAddress(IN); + if (cellPos) { + const columnConfig = columnsJSON.find(col => col.key === ATT_ID); + + // 🎯 템플릿 타입별 데이터 처리 + if (templateType === 'SPREAD_ITEM' && selectedRow) { + // 📝 단일 행 처리 (SPREAD_ITEM) + const isEditable = isFieldEditable(ATT_ID); - // 개별 매핑 추가 + // 매핑 정보 저장 mappings.push({ attId: ATT_ID, - cellAddress: targetCellAddress, // 각 행마다 다른 주소 - isEditable: cellEditable, - dataRowIndex: index // 원본 데이터 인덱스 + cellAddress: IN, + isEditable: isEditable, + dataRowIndex: 0 }); - - console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`); - }); - - // 📋 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; - const cell = activeSheet.getCell(targetRow, cellPos.col); - const value = rowData[ATT_ID]; + const cell = activeSheet.getCell(cellPos.row, cellPos.col); + const value = selectedRow[ATT_ID]; // 값 설정 cell.value(value ?? null); - console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`); + + // 🎨 스타일 및 편집 권한 설정 + cell.locked(!isEditable); + 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); + } + + } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { + // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨 + console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`); - // 편집 권한 및 스타일 설정 - const cellEditable = isFieldEditable(ATT_ID, rowData); - cell.locked(!cellEditable); - const style = createCellStyle(cellEditable); - activeSheet.setStyle(targetRow, cellPos.col, style); - }); - } + // 🚀 행 확장 (필요시) + ensureRowCapacity(activeSheet, cellPos.row + tableData.length); + + // 📋 각 행마다 개별 매핑 생성 + tableData.forEach((rowData, index) => { + const targetRow = cellPos.row + index; + const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`; + const cellEditable = isFieldEditable(ATT_ID, rowData); + + // 개별 매핑 추가 + mappings.push({ + attId: ATT_ID, + cellAddress: targetCellAddress, // 각 행마다 다른 주소 + isEditable: cellEditable, + dataRowIndex: index // 원본 데이터 인덱스 + }); + + console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`); + }); - console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`); - } - } - }); - } - }); + // 📋 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); + } + } - // 💾 매핑 정보 저장 및 이벤트 설정 - setCellMappings(mappings); - setupSheetProtectionAndEvents(activeSheet, mappings); + // 🎨 개별 셀 데이터 및 스타일 설정 + tableData.forEach((rowData, index) => { + const targetRow = cellPos.row + index; + const cell = activeSheet.getCell(targetRow, cellPos.col); + const value = rowData[ATT_ID]; + + // 값 설정 + cell.value(value ?? null); + // console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`); + + // 편집 권한 및 스타일 설정 + const cellEditable = isFieldEditable(ATT_ID, rowData); + cell.locked(!cellEditable); + const style = createCellStyle(cellEditable); + activeSheet.setStyle(targetRow, cellPos.col, style); + }); + } + + // console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`); + } + } + }); + } + }); + } } + // 💾 매핑 정보 저장 및 이벤트 설정 + setCellMappings(mappings); + setupSheetProtectionAndEvents(activeSheet, mappings); + } finally { // 렌더링 재개 spread.resumePaint(); @@ -818,7 +986,7 @@ const debugDropdownError = (options: any[], attId: string) => { spread.resumePaint(); } } - }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]); + }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents, createGrdListTable]); // 변경사항 저장 함수 const handleSaveChanges = React.useCallback(async () => { @@ -869,8 +1037,8 @@ const debugDropdownError = (options: any[], attId: string) => { toast.success("Changes saved successfully!"); onUpdateSuccess?.(dataToSave); - } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { - // 복수 행 저장 + } else if ((templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && tableData.length > 0) { + // 복수 행 저장 (SPREAD_LIST와 GRD_LIST 동일 처리) const updatedRows: GenericData[] = []; let saveCount = 0; @@ -966,7 +1134,11 @@ const debugDropdownError = (options: any[], attId: string) => { <SelectContent> {availableTemplates.map(template => ( <SelectItem key={template.TMPL_ID} value={template.TMPL_ID}> - {template.NAME} ({template.TMPL_TYPE}) + {template.NAME} ({ + template.GRD_LST_SETUP && columnsJSON.length > 0 + ? 'GRD_LIST' + : template.TMPL_TYPE + }) </SelectItem> ))} </SelectContent> @@ -978,12 +1150,16 @@ const debugDropdownError = (options: any[], attId: string) => { {selectedTemplate && ( <div className="flex items-center gap-4 text-sm"> <span className="font-medium text-blue-600"> - Template Type: {selectedTemplate.TMPL_TYPE === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : 'Item View (SPREAD_ITEM)'} + Template Type: { + templateType === 'SPREAD_LIST' ? 'List View (SPREAD_LIST)' : + templateType === 'SPREAD_ITEM' ? 'Item View (SPREAD_ITEM)' : + 'Grid List View (GRD_LIST)' + } </span> {templateType === 'SPREAD_ITEM' && selectedRow && ( <span>• Selected TAG_NO: {selectedRow.TAG_NO || 'N/A'}</span> )} - {templateType === 'SPREAD_LIST' && ( + {(templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') && ( <span>• {dataCount} rows</span> )} {hasChanges && ( @@ -1028,7 +1204,7 @@ const debugDropdownError = (options: any[], attId: string) => { <div className="flex-1 overflow-hidden"> {selectedTemplate && isClient && isDataValid ? ( <SpreadSheets - key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`} + key={`${templateType}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`} workbookInitialized={initSpread} hostStyle={hostStyle} /> |
