diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-11 00:17:59 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-08-11 00:17:59 +0000 |
| commit | 7d39ab4b1e9f92d14d640506d9639a4b054154a9 (patch) | |
| tree | 7452ce866921abb93edcb90ab7882097695dbdea /components/form-data/spreadJS-dialog.tsx | |
| parent | 3230371034bb9a28d6be464b834c5a91ee598022 (diff) | |
(대표님) spreadJS 로직 수정
Diffstat (limited to 'components/form-data/spreadJS-dialog.tsx')
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 760 |
1 files changed, 496 insertions, 264 deletions
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index 7ed861c2..1d0796fe 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -16,7 +16,7 @@ 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: () => ( <div className="flex items-center justify-center h-full"> @@ -77,6 +77,13 @@ interface ValidationError { message: string; } +interface CellMapping { + attId: string; + cellAddress: string; + isEditable: boolean; + dataRowIndex?: number; +} + interface TemplateViewDialogProps { isOpen: boolean; onClose: () => void; @@ -110,7 +117,7 @@ export function TemplateViewDialog({ const [isPending, setIsPending] = React.useState(false); const [hasChanges, setHasChanges] = React.useState(false); const [currentSpread, setCurrentSpread] = React.useState<any>(null); - const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]); + const [cellMappings, setCellMappings] = React.useState<CellMapping[]>([]); const [isClient, setIsClient] = React.useState(false); const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | null>(null); const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]); @@ -125,25 +132,25 @@ export function TemplateViewDialog({ // 사용 가능한 템플릿들을 필터링하고 설정 React.useEffect(() => { if (!templateData) return; - + let templates: TemplateItem[]; if (Array.isArray(templateData)) { templates = templateData as TemplateItem[]; } else { 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"; - + return isValidType && (hasSpreadListContent || hasSpreadItemContent); }); - + setAvailableTemplates(validTemplates); - + // 첫 번째 유효한 템플릿을 기본으로 선택 if (validTemplates.length > 0 && !selectedTemplateId) { setSelectedTemplateId(validTemplates[0].TMPL_ID); @@ -159,7 +166,7 @@ export function TemplateViewDialog({ setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'); setHasChanges(false); setValidationErrors([]); - + // SpreadSheets 재초기화 if (currentSpread) { const template = availableTemplates.find(t => t.TMPL_ID === templateId); @@ -175,12 +182,26 @@ export function TemplateViewDialog({ return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId); }, [availableTemplates, selectedTemplateId]); + // 편집 가능한 필드 목록 계산 const editableFields = React.useMemo(() => { - if (!selectedRow?.TAG_NO || !editableFieldsMap.has(selectedRow.TAG_NO)) { - return []; + // 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인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리 + if (templateType === 'SPREAD_LIST' && tableData.length > 0) { + const firstRowTagNo = tableData[0]?.TAG_NO; + if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) { + return editableFieldsMap.get(firstRowTagNo) || []; + } } - return editableFieldsMap.get(selectedRow.TAG_NO) || []; - }, [selectedRow?.TAG_NO, editableFieldsMap]); + + return []; + }, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]); // 필드가 편집 가능한지 판별하는 함수 const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => { @@ -189,15 +210,36 @@ export function TemplateViewDialog({ if (columnConfig?.shi === true) { return false; // columnsJSON에서 shi가 true이면 편집 불가 } - + // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우) if (attId === "TAG_NO" || attId === "TAG_DESC") { return true; } + + // SPREAD_ITEM인 경우: editableFields 체크 + if (templateType === 'SPREAD_ITEM') { + return editableFields.includes(attId); + } - // SPREAD_LIST인 경우는 기본적으로 편집 가능 (개별 행의 shi 상태는 저장시 확인) + // SPREAD_LIST인 경우: 개별 행의 편집 가능성도 고려 + if (templateType === 'SPREAD_LIST') { + // 기본적으로 editableFields에 포함되어야 함 + if (!editableFields.includes(attId)) { + return false; + } + + // rowData가 제공된 경우 해당 행의 shi 상태도 확인 + if (rowData && rowData.shi === true) { + return false; + } + + return true; + } + + // 기본적으로는 editableFields 체크 + // return editableFields.includes(attId); return true; - }, [templateType, selectedRow, columnsJSON, editableFieldsMap]); + }, [templateType, columnsJSON, editableFields]); // 편집 가능한 필드 개수 계산 const editableFieldsCount = React.useMemo(() => { @@ -205,22 +247,22 @@ export function TemplateViewDialog({ }, [cellMappings]); // 셀 주소를 행과 열로 변환하는 함수 - const parseCellAddress = (address: string): {row: number, col: number} | null => { + 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 }; }; @@ -248,7 +290,7 @@ export function TemplateViewDialog({ // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음 break; } - + return null; }; @@ -270,7 +312,7 @@ export function TemplateViewDialog({ // 단일 행 검증 const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); - + if (errorMessage) { errors.push({ cellAddress: mapping.cellAddress, @@ -281,312 +323,502 @@ export function TemplateViewDialog({ }); } } else if (templateType === 'SPREAD_LIST') { - // 복수 행 검증 - for (let i = 0; i < tableData.length; i++) { - const targetRow = cellPos.row + i; - const cellValue = activeSheet.getValue(targetRow, cellPos.col); - const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); - - if (errorMessage) { - errors.push({ - cellAddress: `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`, - attId: mapping.attId, - value: cellValue, - expectedType: columnConfig.type, - message: errorMessage - }); - } + // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴 + 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, tableData]); - - // LIST 타입 컬럼에 드롭다운 설정 - const setupListValidation = React.useCallback((activeSheet: any, cellPos: {row: number, col: number}, options: string[], rowCount: number = 1) => { - // ComboBox 셀 타입 생성 - const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); - comboBoxCellType.items(options); - comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); - - // 단일 셀 또는 범위에 적용 - for (let i = 0; i < rowCount; i++) { - const targetRow = cellPos.row + i; - activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType); + }, [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; + }, []); + + // 📋 최적화된 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; + 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']; // 안전한 폴백 옵션 + } + + if (safeOptions.length === 0) { + console.warn(`⚠️ No valid options found, using fallback`); + safeOptions = ['Please Select']; + } + + // ✅ 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; + } + + // ✅ 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); - // 추가로 데이터 검증도 설정 - const validator = GC.Spread.Sheets.DataValidation.createListValidator(options); - activeSheet.setDataValidator(targetRow, cellPos.col, validator); + // 폴백: 개별 셀에 하나씩 적용 + console.log('🔄 Trying individual cell application...'); + for (let i = 0; i < Math.min(rowCount, 50); 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); + } + } + } + + console.log(`✅ DataValidation dropdown setup completed`); + + } catch (error) { + console.error('❌ DataValidation setup failed completely:', error); + console.error('Error stack:', error.stack); + console.log('🔄 Falling back to no validation'); + } +}, []); + +// ⚡ 단순 검증 설정 (드롭다운 없이 검증만) +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`); + + const safeOptions = options + .filter(opt => opt != null && opt !== '') + .map(opt => String(opt).trim()) + .filter(opt => opt.length > 0); + + if (safeOptions.length === 0) return; + + const validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions); + + // 범위로 적용 시도 + 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) { + // 개별 실패해도 계속 + } + } + } + + } 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)}"`); + + // 문제 있는 값 체크 + 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}`); + }); + } + + 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`); } }, []); + // 🛡️ 시트 보호 및 이벤트 설정 + const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => { + console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`); + + // 시트 보호 설정 + activeSheet.options.isProtected = true; + activeSheet.options.protectionOptions = { + allowSelectLockedCells: true, + allowSelectUnlockedCells: true, + allowSort: false, + allowFilter: false, + allowEditObjects: false, + allowResizeRows: false, + allowResizeColumns: false + }; + + // 🎯 변경 감지 이벤트 + 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}`); + + // 기본 편집 권한 확인 + 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 개별 행 SHI 확인 + if (templateType === 'SPREAD_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}`); + + // ✅ 정확한 매핑 찾기 + 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); + } + } + }); + + console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`); + }, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]); + + // ═══════════════════════════════════════════════════════════════════════════════ + // 🏗️ 메인 SpreadSheets 초기화 함수 + // ═══════════════════════════════════════════════════════════════════════════════ + const initSpread = React.useCallback((spread: any, template?: TemplateItem) => { const workingTemplate = template || selectedTemplate; if (!spread || !workingTemplate) return; try { + // 🔄 초기 설정 setCurrentSpread(spread); setHasChanges(false); setValidationErrors([]); - // SPR_LST_SETUP.CONTENT와 SPR_ITM_LST_SETUP.CONTENT 중에서 값이 있는 것을 찾아서 사용 + // 📋 템플릿 콘텐츠 및 데이터 시트 추출 let contentJson = null; let dataSheets = null; - - // SPR_LST_SETUP.CONTENT가 있으면 우선 사용 + + // 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, '(TMPL_TYPE:', workingTemplate.TMPL_TYPE, ')'); - } - // SPR_ITM_LST_SETUP.CONTENT가 있으면 사용 + 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, '(TMPL_TYPE:', workingTemplate.TMPL_TYPE, ')'); + console.log('✅ Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME); } if (!contentJson) { - console.warn('No CONTENT found in template:', workingTemplate.NAME); + console.warn('❌ No CONTENT found in template:', workingTemplate.NAME); return; } - console.log(`Loading template content for: ${workingTemplate.NAME} (Type: ${workingTemplate.TMPL_TYPE})`); + // 🏗️ SpreadSheets 초기화 + const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; - const jsonData = typeof contentJson === 'string' - ? JSON.parse(contentJson) - : contentJson; - - // 렌더링 일시 중단 + // 성능을 위한 렌더링 일시 중단 spread.suspendPaint(); try { - // fromJSON으로 템플릿 구조 로드 + // 템플릿 구조 로드 spread.fromJSON(jsonData); - - // 활성 시트 가져오기 const activeSheet = spread.getActiveSheet(); - // 시트 보호 먼저 해제 + // 시트 보호 해제 (편집을 위해) activeSheet.options.isProtected = false; - // MAP_CELL_ATT 정보를 사용해서 데이터 매핑 + // 📊 셀 매핑 및 데이터 처리 if (dataSheets && dataSheets.length > 0) { - const mappings: Array<{attId: string, cellAddress: string, isEditable: boolean}> = []; + const mappings: CellMapping[] = []; + // 🔄 각 데이터 시트의 매핑 정보 처리 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 isEditable = isFieldEditable(ATT_ID); const columnConfig = columnsJSON.find(col => col.key === ATT_ID); - - mappings.push({ - attId: ATT_ID, - cellAddress: IN, - isEditable: isEditable - }); - - // 템플릿 타입에 따라 다른 데이터 처리 + + // 🎯 템플릿 타입별 데이터 처리 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]; - if (value !== undefined && value !== null) { - cell.value(value); - } - if (value === undefined || value === null) { - cell.value(null); - } + // 값 설정 + cell.value(value ?? null); + + // 🎨 스타일 및 편집 권한 설정 + cell.locked(!isEditable); + const style = createCellStyle(isEditable); + activeSheet.setStyle(cellPos.row, cellPos.col, style); - // LIST 타입 컬럼에 드롭다운 설정 + // 📋 LIST 타입 드롭다운 설정 if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { - setupListValidation(activeSheet, cellPos, columnConfig.options, 1); + setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1); } - // 스타일 적용 - cell.locked(!isEditable); - const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col); - const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style(); - - if (isEditable) { - newStyle.backColor = "#f0fdf4"; - } else { - newStyle.backColor = "#f9fafb"; - newStyle.foreColor = "#6b7280"; - } - - activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); - } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { - // 복수 행 처리 - 첫 번째 행부터 시작해서 아래로 채움 + // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨 + console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`); - // LIST 타입 컬럼에 드롭다운 설정 (모든 행에 대해) - if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { - setupListValidation(activeSheet, cellPos, columnConfig.options, tableData.length); - } + // 🚀 행 확장 (필요시) + 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'})`); + }); - // 필요한 경우 행 추가 (tableData 길이만큼 충분히 확보) - const currentRowCount = activeSheet.getRowCount(); - const requiredRowCount = cellPos.row + tableData.length; - if (requiredRowCount > currentRowCount) { - activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가 + // 📋 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]; - if (value !== undefined && value !== null) { - cell.value(value); - } - - if (value === undefined || value === null) { - cell.value(null); - } + // 값 설정 + cell.value(value ?? null); + console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`); + // 편집 권한 및 스타일 설정 const cellEditable = isFieldEditable(ATT_ID, rowData); cell.locked(!cellEditable); - - // 스타일 적용 - const existingStyle = activeSheet.getStyle(targetRow, cellPos.col); - const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style(); - - if (cellEditable) { - newStyle.backColor = "#f0fdf4"; - } else { - newStyle.backColor = "#f9fafb"; - newStyle.foreColor = "#6b7280"; - } - - activeSheet.setStyle(targetRow, cellPos.col, newStyle); + const style = createCellStyle(cellEditable); + activeSheet.setStyle(targetRow, cellPos.col, style); }); } - - console.log(`Mapped ${ATT_ID} to cell ${IN} - ${isEditable ? 'Editable' : 'Read-only'}`); + + console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`); } } }); } }); - - setCellMappings(mappings); - - // 시트 보호 설정 - activeSheet.options.isProtected = true; - activeSheet.options.protectionOptions = { - allowSelectLockedCells: true, - allowSelectUnlockedCells: true, - allowSort: false, - allowFilter: false, - allowEditObjects: false, - allowResizeRows: false, - allowResizeColumns: false - }; - - // 이벤트 리스너 추가 - activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => { - console.log('Cell changed:', info); - setHasChanges(true); - }); - - activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => { - console.log('Value changed:', info); - setHasChanges(true); - }); - - // 복사 붙여넣기 이벤트 추가 - activeSheet.bind(GC.Spread.Sheets.Events.ClipboardPasted, (event: any, info: any) => { - console.log('Clipboard pasted:', info); - setHasChanges(true); - }); - - // 편집 시작 시 읽기 전용 셀 확인 - activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { - const mapping = mappings.find(m => { - const cellPos = parseCellAddress(m.cellAddress); - return cellPos && cellPos.row <= info.row && cellPos.col === info.col; - }); - - if (mapping) { - // columnsJSON에서 해당 필드의 shi 확인 - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - if (columnConfig?.shi === true) { - toast.warning(`${mapping.attId} field is read-only (Column configuration)`); - info.cancel = true; - return; - } - - // SPREAD_LIST인 경우 해당 행의 데이터에서 shi 확인 - if (templateType === 'SPREAD_LIST') { - const dataRowIndex = info.row - parseCellAddress(mapping.cellAddress)!.row; - const rowData = tableData[dataRowIndex]; - if (rowData && rowData.shi === true) { - toast.warning(`Row ${dataRowIndex + 1}: ${mapping.attId} field is read-only (SHI mode)`); - info.cancel = true; - return; - } - } - - if (!mapping.isEditable) { - toast.warning(`${mapping.attId} field is read-only`); - info.cancel = true; - } - } - }); - // 편집 종료 시 데이터 검증 - activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (event: any, info: any) => { - const mapping = mappings.find(m => { - const cellPos = parseCellAddress(m.cellAddress); - return cellPos && cellPos.row <= info.row && cellPos.col === info.col; - }); - - if (mapping) { - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - if (columnConfig) { - const cellValue = activeSheet.getValue(info.row, info.col); - const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); - - if (errorMessage) { - toast.warning(`Invalid value in ${mapping.attId}: ${errorMessage}`); - // 스타일을 오류 상태로 변경 - const errorStyle = new GC.Spread.Sheets.Style(); - errorStyle.backColor = "#fef2f2"; - errorStyle.foreColor = "#dc2626"; - activeSheet.setStyle(info.row, info.col, errorStyle); - } else { - // 정상 스타일로 복원 - const cellEditable = isFieldEditable(mapping.attId); - const normalStyle = new GC.Spread.Sheets.Style(); - normalStyle.backColor = cellEditable ? "#f0fdf4" : "#f9fafb"; - normalStyle.foreColor = cellEditable ? "#000000" : "#6b7280"; - activeSheet.setStyle(info.row, info.col, normalStyle); - } - } - } - }); + // 💾 매핑 정보 저장 및 이벤트 설정 + setCellMappings(mappings); + setupSheetProtectionAndEvents(activeSheet, mappings); } + } finally { + // 렌더링 재개 spread.resumePaint(); } } catch (error) { - console.error('Error initializing spread:', error); + console.error('❌ Error initializing spread:', error); toast.error('Failed to load template'); - if (spread && spread.resumePaint) { + if (spread?.resumePaint) { spread.resumePaint(); } } - }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, setupListValidation, validateCellValue]); + }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]); // 변경사항 저장 함수 const handleSaveChanges = React.useCallback(async () => { @@ -608,7 +840,7 @@ export function TemplateViewDialog({ const activeSheet = currentSpread.getActiveSheet(); if (templateType === 'SPREAD_ITEM' && selectedRow) { - // 단일 행 저장 (기존 로직) + // 단일 행 저장 const dataToSave = { ...selectedRow }; cellMappings.forEach(mapping => { @@ -649,21 +881,21 @@ export function TemplateViewDialog({ // 각 매핑에 대해 해당 행의 값 확인 cellMappings.forEach(mapping => { - // columnsJSON에서 해당 필드의 shi 확인 - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - const isColumnEditable = columnConfig?.shi !== true; - const isRowEditable = originalRow.shi !== true; - - if (mapping.isEditable && isColumnEditable && isRowEditable) { - const cellPos = parseCellAddress(mapping.cellAddress); - if (cellPos) { - const targetRow = cellPos.row + i; - const cellValue = activeSheet.getValue(targetRow, cellPos.col); - - // 값이 변경되었는지 확인 - if (cellValue !== originalRow[mapping.attId]) { - dataToSave[mapping.attId] = cellValue; - hasRowChanges = true; + 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; + } } } } @@ -715,9 +947,9 @@ export function TemplateViewDialog({ return ( <Dialog open={isOpen} onOpenChange={onClose}> - <DialogContent - className="w-[80%] max-w-none h-[80vh] flex flex-col" - style={{maxWidth:"80vw"}} + <DialogContent + className="w-[80%] max-w-none h-[80vh] flex flex-col" + style={{ maxWidth: "80vw" }} > <DialogHeader className="flex-shrink-0"> <DialogTitle>SEDP Template - {formCode}</DialogTitle> @@ -791,11 +1023,11 @@ export function TemplateViewDialog({ </div> </DialogDescription> </DialogHeader> - + {/* SpreadSheets 컴포넌트 영역 */} <div className="flex-1 overflow-hidden"> {selectedTemplate && isClient && isDataValid ? ( - <SpreadSheets + <SpreadSheets key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`} workbookInitialized={initSpread} hostStyle={hostStyle} @@ -823,10 +1055,10 @@ export function TemplateViewDialog({ <Button variant="outline" onClick={onClose}> Close </Button> - + {hasChanges && ( - <Button - variant="default" + <Button + variant="default" onClick={handleSaveChanges} disabled={isPending || validationErrors.length > 0} > @@ -845,8 +1077,8 @@ export function TemplateViewDialog({ )} {validationErrors.length > 0 && ( - <Button - variant="outline" + <Button + variant="outline" onClick={validateAllData} className="text-red-600 border-red-300 hover:bg-red-50" > |
