diff options
Diffstat (limited to 'components/form-data/spreadJS-dialog.tsx')
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 500 |
1 files changed, 317 insertions, 183 deletions
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index a223a849..14f4d3ea 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -235,46 +235,79 @@ export function TemplateViewDialog({ }, [availableTemplates, selectedTemplateId]); const editableFields = React.useMemo(() => { + // SPREAD_ITEM의 경우에만 전역 editableFields 사용 if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) { if (!editableFieldsMap.has(selectedRow.TAG_NO)) { return []; } return editableFieldsMap.get(selectedRow.TAG_NO) || []; } + + // SPREAD_LIST나 GRD_LIST의 경우 전역 editableFields는 사용하지 않음 + return []; + }, [templateType, selectedRow?.TAG_NO, editableFieldsMap]); - 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) || []; - } - } + +const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => { + const columnConfig = columnsJSON.find(col => col.key === attId); + if (columnConfig?.shi === "OUT" || columnConfig?.shi === null) { + return false; + } - return []; - }, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]); + if (attId === "TAG_NO" || attId === "TAG_DESC" || attId === "status") { + return false; + } - const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => { - const columnConfig = columnsJSON.find(col => col.key === attId); - if (columnConfig?.shi === true) { + if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { + // 각 행의 TAG_NO를 기준으로 편집 가능 여부 판단 + if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { return false; } - - if (attId === "TAG_NO" || attId === "TAG_DESC" || attId === "status") { + + const rowEditableFields = editableFieldsMap.get(rowData.TAG_NO) || []; + if (!rowEditableFields.includes(attId)) { return false; } - - if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { - if (rowData && rowData.shi === true) { - return false; - } - return true; + + if (rowData && (rowData.shi === "OUT" || rowData.shi === null)) { + return false; } - return true; - }, [templateType, columnsJSON]); + } + + // SPREAD_ITEM의 경우 기존 로직 유지 + if (templateType === 'SPREAD_ITEM') { + return editableFields.includes(attId); + } - const editableFieldsCount = React.useMemo(() => { + return true; +}, [templateType, columnsJSON, editableFieldsMap]); // editableFields 의존성 제거 + +const editableFieldsCount = React.useMemo(() => { + if (templateType === 'SPREAD_ITEM') { + // SPREAD_ITEM의 경우 기존 로직 유지 return cellMappings.filter(m => m.isEditable).length; - }, [cellMappings]); + } + + if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { + // 각 행별로 편집 가능한 필드 수를 계산 + let totalEditableCount = 0; + + tableData.forEach((rowData, rowIndex) => { + cellMappings.forEach(mapping => { + if (mapping.dataRowIndex === rowIndex) { + if (isFieldEditable(mapping.attId, rowData)) { + totalEditableCount++; + } + } + }); + }); + + return totalEditableCount; + } + + return cellMappings.filter(m => m.isEditable).length; +}, [cellMappings, templateType, tableData, isFieldEditable]); // 🚀 배치 처리 함수들 const setBatchValues = React.useCallback(( @@ -330,10 +363,10 @@ export function TemplateViewDialog({ const createCellStyle = React.useCallback((isEditable: boolean) => { const style = new GC.Spread.Sheets.Style(); if (isEditable) { - style.backColor = "#f0fdf4"; + style.backColor = "#bbf7d0"; } else { - style.backColor = "#f9fafb"; - style.foreColor = "#6b7280"; + style.backColor = "#e5e7eb"; + style.foreColor = "#4b5563"; } return style; }, []); @@ -569,153 +602,206 @@ export function TemplateViewDialog({ }, []); // 🚀 최적화된 GRD_LIST 생성 - const createGrdListTableOptimized = React.useCallback((activeSheet: any, template: TemplateItem) => { - console.log('🚀 Creating optimized GRD_LIST table'); - - const visibleColumns = columnsJSON - .filter(col => col.hidden !== true) - .sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999)); - - if (visibleColumns.length === 0) return []; + // 🚀 최적화된 GRD_LIST 생성 (TAG_DESC 컬럼 틀고정 포함) +const createGrdListTableOptimized = React.useCallback((activeSheet: any, template: TemplateItem) => { + console.log('🚀 Creating optimized GRD_LIST table with TAG_DESC freeze'); - const startCol = 1; - const dataStartRow = 1; - const mappings: CellMapping[] = []; + const visibleColumns = columnsJSON + .filter(col => col.hidden !== true) + .sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999)); - ensureColumnCapacity(activeSheet, startCol + visibleColumns.length); - ensureRowCapacity(activeSheet, dataStartRow + tableData.length); + if (visibleColumns.length === 0) return []; - // 헤더 생성 - const headerStyle = new GC.Spread.Sheets.Style(); - headerStyle.backColor = "#3b82f6"; - headerStyle.foreColor = "#ffffff"; - headerStyle.font = "bold 12px Arial"; - headerStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; + const startCol = 1; + const dataStartRow = 1; + const mappings: CellMapping[] = []; - visibleColumns.forEach((column, colIndex) => { - const targetCol = startCol + colIndex; - const cell = activeSheet.getCell(0, targetCol); - cell.value(column.label); - cell.locked(true); - activeSheet.setStyle(0, targetCol, headerStyle); - }); + ensureColumnCapacity(activeSheet, startCol + visibleColumns.length); + ensureRowCapacity(activeSheet, dataStartRow + tableData.length); - // 🚀 데이터 배치 처리 준비 - const allValues: Array<{row: number, col: number, value: any}> = []; - const allStyles: Array<{row: number, col: number, isEditable: boolean}> = []; + // 🧊 TAG_DESC 컬럼 위치 찾기 (틀고정용) + const tagDescColumnIndex = visibleColumns.findIndex(col => col.key === 'TAG_DESC'); + let freezeColumnCount = 0; + + if (tagDescColumnIndex !== -1) { + // TAG_DESC 컬럼까지 포함해서 고정 (startCol + tagDescColumnIndex + 1) + freezeColumnCount = startCol + tagDescColumnIndex + 1; + console.log(`🧊 TAG_DESC found at column index ${tagDescColumnIndex}, freezing ${freezeColumnCount} columns`); + } else { + // TAG_DESC가 없으면 TAG_NO까지만 고정 (일반적으로 첫 번째 컬럼) + const tagNoColumnIndex = visibleColumns.findIndex(col => col.key === 'TAG_NO'); + if (tagNoColumnIndex !== -1) { + freezeColumnCount = startCol + tagNoColumnIndex + 1; + console.log(`🧊 TAG_NO found at column index ${tagNoColumnIndex}, freezing ${freezeColumnCount} columns`); + } + } - // 🔧 편집 가능한 셀 정보 수집 (드롭다운용) - const dropdownConfigs: Array<{ - startRow: number; - col: number; - rowCount: number; - options: string[]; - editableRows: number[]; // 편집 가능한 행만 추적 - }> = []; + // 헤더 생성 + const headerStyle = new GC.Spread.Sheets.Style(); + headerStyle.backColor = "#3b82f6"; + headerStyle.foreColor = "#ffffff"; + headerStyle.font = "bold 12px Arial"; + headerStyle.hAlign = GC.Spread.Sheets.HorizontalAlign.center; + + visibleColumns.forEach((column, colIndex) => { + const targetCol = startCol + colIndex; + const cell = activeSheet.getCell(0, targetCol); + cell.value(column.label); + cell.locked(true); + activeSheet.setStyle(0, targetCol, headerStyle); + }); - visibleColumns.forEach((column, colIndex) => { - const targetCol = startCol + colIndex; - - // 드롭다운 설정을 위한 편집 가능한 행 찾기 - if (column.type === "LIST" && column.options) { - const editableRows: number[] = []; - tableData.forEach((rowData, rowIndex) => { - if (isFieldEditable(column.key, rowData)) { - editableRows.push(dataStartRow + rowIndex); - } - }); - - if (editableRows.length > 0) { - dropdownConfigs.push({ - startRow: dataStartRow, - col: targetCol, - rowCount: tableData.length, - options: column.options, - editableRows: editableRows - }); + // 🚀 데이터 배치 처리 준비 + const allValues: Array<{row: number, col: number, value: any}> = []; + const allStyles: Array<{row: number, col: number, isEditable: boolean}> = []; + + // 🔧 편집 가능한 셀 정보 수집 (드롭다운용) + const dropdownConfigs: Array<{ + startRow: number; + col: number; + rowCount: number; + options: string[]; + editableRows: number[]; // 편집 가능한 행만 추적 + }> = []; + + visibleColumns.forEach((column, colIndex) => { + const targetCol = startCol + colIndex; + + // 드롭다운 설정을 위한 편집 가능한 행 찾기 + if (column.type === "LIST" && column.options) { + const editableRows: number[] = []; + tableData.forEach((rowData, rowIndex) => { + if (isFieldEditable(column.key, rowData)) { // rowData 전달 + editableRows.push(dataStartRow + rowIndex); } - } + }); - tableData.forEach((rowData, rowIndex) => { - const targetRow = dataStartRow + rowIndex; - const cellEditable = isFieldEditable(column.key, rowData); - const value = rowData[column.key]; - - mappings.push({ - attId: column.key, - cellAddress: getCellAddress(targetRow, targetCol), - isEditable: cellEditable, - dataRowIndex: rowIndex - }); - - allValues.push({ - row: targetRow, + if (editableRows.length > 0) { + dropdownConfigs.push({ + startRow: dataStartRow, col: targetCol, - value: value ?? null - }); - - allStyles.push({ - row: targetRow, - col: targetCol, - isEditable: cellEditable + rowCount: tableData.length, + options: column.options, + editableRows: editableRows }); + } + } + + tableData.forEach((rowData, rowIndex) => { + const targetRow = dataStartRow + rowIndex; + const cellEditable = isFieldEditable(column.key, rowData); // rowData 전달 + const value = rowData[column.key]; + + mappings.push({ + attId: column.key, + cellAddress: getCellAddress(targetRow, targetCol), + isEditable: cellEditable, + dataRowIndex: rowIndex + }); + + allValues.push({ + row: targetRow, + col: targetCol, + value: value ?? null + }); + + allStyles.push({ + row: targetRow, + col: targetCol, + isEditable: cellEditable }); }); + }); - // 🚀 배치로 값과 스타일 설정 - setBatchValues(activeSheet, allValues); - setBatchStyles(activeSheet, allStyles); + // 🚀 배치로 값과 스타일 설정 + setBatchValues(activeSheet, allValues); + setBatchStyles(activeSheet, allStyles); - // 🎯 개선된 드롭다운 설정 (편집 가능한 셀에만) - dropdownConfigs.forEach(({ col, options, editableRows }) => { - try { - console.log(`🎯 Setting dropdown for column ${col}, editable rows: ${editableRows.length}`); - - const safeOptions = options - .filter(opt => opt !== null && opt !== undefined && opt !== '') - .map(opt => String(opt).trim()) - .filter(opt => opt.length > 0) - .slice(0, 20); + // 🎯 개선된 드롭다운 설정 (편집 가능한 셀에만) + dropdownConfigs.forEach(({ col, options, editableRows }) => { + try { + console.log(`🎯 Setting dropdown for column ${col}, editable rows: ${editableRows.length}`); + + const safeOptions = options + .filter(opt => opt !== null && opt !== undefined && opt !== '') + .map(opt => String(opt).trim()) + .filter(opt => opt.length > 0) + .slice(0, 20); - if (safeOptions.length === 0) return; + if (safeOptions.length === 0) return; - // 편집 가능한 행에만 드롭다운 적용 - editableRows.forEach(targetRow => { - try { - const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); - comboBoxCellType.items(safeOptions); - comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); + // 편집 가능한 행에만 드롭다운 적용 + editableRows.forEach(targetRow => { + try { + const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); + comboBoxCellType.items(safeOptions); + comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); - const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions.join(',')); - cellValidator.showInputMessage(false); - cellValidator.showErrorMessage(false); + const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions.join(',')); + cellValidator.showInputMessage(false); + cellValidator.showErrorMessage(false); - activeSheet.setCellType(targetRow, col, comboBoxCellType); - activeSheet.setDataValidator(targetRow, col, cellValidator); - - // 🚀 편집 권한 명시적 설정 - const cell = activeSheet.getCell(targetRow, col); - cell.locked(false); + activeSheet.setCellType(targetRow, col, comboBoxCellType); + activeSheet.setDataValidator(targetRow, col, cellValidator); + + // 🚀 편집 권한 명시적 설정 + const cell = activeSheet.getCell(targetRow, col); + cell.locked(false); - console.log(`✅ Dropdown applied to editable cell [${targetRow}, ${col}]`); - } catch (cellError) { - console.warn(`⚠️ Failed to apply dropdown to [${targetRow}, ${col}]:`, cellError); + console.log(`✅ Dropdown applied to editable cell [${targetRow}, ${col}]`); + } catch (cellError) { + console.warn(`⚠️ Failed to apply dropdown to [${targetRow}, ${col}]:`, cellError); + } + }); + } catch (error) { + console.error(`❌ Dropdown config failed for column ${col}:`, error); + } + }); + + // 🧊 틀고정 설정 + if (freezeColumnCount > 0) { + try { + activeSheet.frozenColumnCount(freezeColumnCount); + activeSheet.frozenRowCount(1); // 헤더 행도 고정 + + console.log(`🧊 Freeze applied: ${freezeColumnCount} columns, 1 row (header)`); + + // 🎨 고정된 컬럼에 특별한 스타일 추가 (선택사항) + for (let col = 0; col < freezeColumnCount; col++) { + for (let row = 0; row <= tableData.length; row++) { + try { + const currentStyle = activeSheet.getStyle(row, col) || new GC.Spread.Sheets.Style(); + + if (row === 0) { + // 헤더는 기존 스타일 유지 + continue; + } else { + // 데이터 셀에 고정 구분선 추가 + if (col === freezeColumnCount - 1) { + currentStyle.borderRight = new GC.Spread.Sheets.LineBorder("#2563eb", GC.Spread.Sheets.LineStyle.medium); + activeSheet.setStyle(row, col, currentStyle); + } + } + } catch (styleError) { + console.warn(`⚠️ Failed to apply freeze border style to [${row}, ${col}]:`, styleError); } - }); - } catch (error) { - console.error(`❌ Dropdown config failed for column ${col}:`, error); + } } - }); + } catch (freezeError) { + console.error('❌ Failed to apply freeze:', freezeError); + } + } - setOptimalColumnWidths(activeSheet, visibleColumns, startCol, tableData); - - console.log(`✅ Optimized GRD_LIST created:`); - console.log(` - Total mappings: ${mappings.length}`); - console.log(` - Editable cells: ${mappings.filter(m => m.isEditable).length}`); - console.log(` - Dropdown configs: ${dropdownConfigs.length}`); - - return mappings; - }, [tableData, columnsJSON, isFieldEditable, setBatchValues, setBatchStyles, ensureColumnCapacity, ensureRowCapacity, getCellAddress, setOptimalColumnWidths]); + setOptimalColumnWidths(activeSheet, visibleColumns, startCol, tableData); + + console.log(`✅ Optimized GRD_LIST created with freeze:`); + console.log(` - Total mappings: ${mappings.length}`); + console.log(` - Editable cells: ${mappings.filter(m => m.isEditable).length}`); + console.log(` - Dropdown configs: ${dropdownConfigs.length}`); + console.log(` - Frozen columns: ${freezeColumnCount}`); + + return mappings; +}, [tableData, columnsJSON, isFieldEditable, setBatchValues, setBatchStyles, ensureColumnCapacity, ensureRowCapacity, getCellAddress, setOptimalColumnWidths]); const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => { console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`); @@ -840,7 +926,7 @@ export function TemplateViewDialog({ const dataRowIndex = exactMapping.dataRowIndex; if (dataRowIndex >= 0 && dataRowIndex < tableData.length) { const rowData = tableData[dataRowIndex]; - if (rowData?.shi === true) { + if (rowData?.shi === "OUT" || rowData?.shi === null ) { console.log(`🚫 Row ${dataRowIndex} is in SHI mode`); toast.warning(`Row ${dataRowIndex + 1}: ${exactMapping.attId} field is read-only (SHI mode)`); info.cancel = true; @@ -1092,20 +1178,20 @@ export function TemplateViewDialog({ 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); @@ -1115,86 +1201,134 @@ export function TemplateViewDialog({ } } }); - + 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) { + console.log('🔍 Starting batch save process...'); + const updatedRows: GenericData[] = []; let saveCount = 0; - + let checkedCount = 0; + for (let i = 0; i < tableData.length; i++) { const originalRow = tableData[i]; const dataToSave = { ...originalRow }; let hasRowChanges = false; - + + console.log(`🔍 Processing row ${i} (TAG_NO: ${originalRow.TAG_NO})`); + 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) { + checkedCount++; + + // 🔧 isFieldEditable과 동일한 로직 사용 + const rowData = tableData[i]; + const fieldEditable = isFieldEditable(mapping.attId, rowData); + + console.log(` 📝 Field ${mapping.attId}: fieldEditable=${fieldEditable}, mapping.isEditable=${mapping.isEditable}`); + + if (fieldEditable) { const cellPos = parseCellAddress(mapping.cellAddress); if (cellPos) { const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); - if (cellValue !== originalRow[mapping.attId]) { + const originalValue = originalRow[mapping.attId]; + + // 🔧 개선된 값 비교 (타입 변환 및 null/undefined 처리) + const normalizedCellValue = cellValue === null || cellValue === undefined ? "" : String(cellValue).trim(); + const normalizedOriginalValue = originalValue === null || originalValue === undefined ? "" : String(originalValue).trim(); + + console.log(` 🔍 ${mapping.attId}: "${normalizedOriginalValue}" -> "${normalizedCellValue}"`); + + if (normalizedCellValue !== normalizedOriginalValue) { dataToSave[mapping.attId] = cellValue; hasRowChanges = true; + console.log(` ✅ Change detected for ${mapping.attId}`); } } } } }); - + if (hasRowChanges) { + console.log(`💾 Saving row ${i} with changes`); dataToSave.TAG_NO = originalRow.TAG_NO; - const { success } = await updateFormDataInDB( - formCode, - contractItemId, - dataToSave - ); - - if (success) { - updatedRows.push(dataToSave); - saveCount++; + + try { + const { success, message } = await updateFormDataInDB( + formCode, + contractItemId, + dataToSave + ); + + if (success) { + updatedRows.push(dataToSave); + saveCount++; + console.log(`✅ Row ${i} saved successfully`); + } else { + console.error(`❌ Failed to save row ${i}: ${message}`); + toast.error(`Failed to save row ${i + 1}: ${message}`); + updatedRows.push(originalRow); // 원본 데이터 유지 + } + } catch (error) { + console.error(`❌ Error saving row ${i}:`, error); + toast.error(`Error saving row ${i + 1}`); + updatedRows.push(originalRow); // 원본 데이터 유지 } } else { updatedRows.push(originalRow); + console.log(`ℹ️ No changes in row ${i}`); } } - + + console.log(`📊 Save summary: ${saveCount} saved, ${checkedCount} fields checked`); + if (saveCount > 0) { toast.success(`${saveCount} rows saved successfully!`); onUpdateSuccess?.(updatedRows); } else { - toast.info("No changes to save"); + console.warn(`⚠️ No changes detected despite hasChanges=${hasChanges}`); + toast.warning("No actual changes were found to save. Please check if the values were properly edited."); } } - + 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]); + }, [ + currentSpread, + hasChanges, + templateType, + selectedRow, + tableData, + formCode, + contractItemId, + onUpdateSuccess, + cellMappings, + columnsJSON, + validateAllData, + isFieldEditable // 🔧 의존성 추가 + ]); if (!isOpen) return null; |
