From 2c02afd48a4d9276a4f5c132e088540a578d0972 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Tue, 30 Sep 2025 10:08:53 +0000 Subject: (대표님) 폼리스트, spreadjs 관련 변경사항, 벤더문서 뷰 쿼리 수정, 이메일 템플릿 추가 등 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/form-data/spreadJS-dialog.tsx | 725 ++++++++++++++++--------------- 1 file changed, 381 insertions(+), 344 deletions(-) (limited to 'components/form-data/spreadJS-dialog.tsx') diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index d001463e..91d5672c 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -107,9 +107,9 @@ interface LoadingProgressProps { const LoadingProgress: React.FC = ({ phase, progress, total, isVisible }) => { const percentage = total > 0 ? Math.round((progress / total) * 100) : 0; - + if (!isVisible) return null; - + return (
@@ -117,11 +117,11 @@ const LoadingProgress: React.FC = ({ phase, progress, tota Loading Template
- +
{phase}
-
@@ -161,7 +161,7 @@ export function TemplateViewDialog({ const [validationErrors, setValidationErrors] = React.useState([]); const [selectedTemplateId, setSelectedTemplateId] = React.useState(""); const [availableTemplates, setAvailableTemplates] = React.useState([]); - + // 🆕 로딩 상태 추가 const [loadingProgress, setLoadingProgress] = React.useState<{ phase: string; @@ -244,102 +244,102 @@ export function TemplateViewDialog({ } return editableFieldsMap.get(selectedRow.TAG_NO) || []; } - + // SPREAD_LIST나 GRD_LIST의 경우 전역 editableFields는 사용하지 않음 return []; }, [templateType, selectedRow?.TAG_NO, editableFieldsMap]); - -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; - } - - if (attId === "TAG_NO" || attId === "TAG_DESC" || attId === "status") { - return false; - } - if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { - // 각 행의 TAG_NO를 기준으로 편집 가능 여부 판단 - if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { + 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; } - - const rowEditableFields = editableFieldsMap.get(rowData.TAG_NO) || []; - if (!rowEditableFields.includes(attId)) { + + if (attId === "TAG_NO" || attId === "TAG_DESC" || attId === "status") { return false; } - - if (rowData && (rowData.shi === "OUT" || rowData.shi === null)) { - return false; + + if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { + // 각 행의 TAG_NO를 기준으로 편집 가능 여부 판단 + if (!rowData?.TAG_NO || !editableFieldsMap.has(rowData.TAG_NO)) { + return false; + } + + const rowEditableFields = editableFieldsMap.get(rowData.TAG_NO) || []; + if (!rowEditableFields.includes(attId)) { + return false; + } + + if (rowData && (rowData.shi === "OUT" || rowData.shi === null)) { + return false; + } + return true; } + + // SPREAD_ITEM의 경우 기존 로직 유지 + if (templateType === 'SPREAD_ITEM') { + return editableFields.includes(attId); + } + return true; - } + }, [templateType, columnsJSON, editableFieldsMap]); // editableFields 의존성 제거 - // SPREAD_ITEM의 경우 기존 로직 유지 - if (templateType === 'SPREAD_ITEM') { - return editableFields.includes(attId); - } + const editableFieldsCount = React.useMemo(() => { + if (templateType === 'SPREAD_ITEM') { + // SPREAD_ITEM의 경우 기존 로직 유지 + return cellMappings.filter(m => m.isEditable).length; + } - return true; -}, [templateType, columnsJSON, editableFieldsMap]); // editableFields 의존성 제거 + if (templateType === 'SPREAD_LIST' || templateType === 'GRD_LIST') { + // 각 행별로 편집 가능한 필드 수를 계산 + let totalEditableCount = 0; -const editableFieldsCount = React.useMemo(() => { - if (templateType === 'SPREAD_ITEM') { - // SPREAD_ITEM의 경우 기존 로직 유지 - return cellMappings.filter(m => m.isEditable).length; - } - - 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++; + 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]); + + return totalEditableCount; + } + + return cellMappings.filter(m => m.isEditable).length; + }, [cellMappings, templateType, tableData, isFieldEditable]); // 🚀 배치 처리 함수들 const setBatchValues = React.useCallback(( - activeSheet: any, - valuesToSet: Array<{row: number, col: number, value: any}> + activeSheet: any, + valuesToSet: Array<{ row: number, col: number, value: any }> ) => { console.log(`🚀 Setting ${valuesToSet.length} values in batch`); - - const columnGroups = new Map>(); - - valuesToSet.forEach(({row, col, value}) => { + + const columnGroups = new Map>(); + + valuesToSet.forEach(({ row, col, value }) => { if (!columnGroups.has(col)) { columnGroups.set(col, []); } - columnGroups.get(col)!.push({row, value}); + columnGroups.get(col)!.push({ row, value }); }); - + columnGroups.forEach((values, col) => { values.sort((a, b) => a.row - b.row); - + let start = 0; while (start < values.length) { let end = start; while (end + 1 < values.length && values[end + 1].row === values[end].row + 1) { end++; } - + const rangeValues = values.slice(start, end + 1).map(v => v.value); const startRow = values[start].row; - + try { if (rangeValues.length === 1) { activeSheet.setValue(startRow, col, rangeValues[0]); @@ -356,7 +356,7 @@ const editableFieldsCount = React.useMemo(() => { } } } - + start = end + 1; } }); @@ -365,7 +365,7 @@ const editableFieldsCount = React.useMemo(() => { const createCellStyle = React.useCallback((activeSheet: any, row: number, col: number, isEditable: boolean) => { // 기존 스타일 가져오기 (없으면 새로 생성) const existingStyle = activeSheet.getStyle(row, col) || new GC.Spread.Sheets.Style(); - + // backColor만 수정 if (isEditable) { existingStyle.backColor = "#bbf7d0"; @@ -374,25 +374,25 @@ const editableFieldsCount = React.useMemo(() => { // 읽기 전용일 때만 텍스트 색상 변경 (선택사항) existingStyle.foreColor = "#4b5563"; } - + return existingStyle; }, []); const setBatchStyles = React.useCallback(( - activeSheet: any, - stylesToSet: Array<{row: number, col: number, isEditable: boolean}> + activeSheet: any, + stylesToSet: Array<{ row: number, col: number, isEditable: boolean }> ) => { console.log(`🎨 Setting ${stylesToSet.length} styles in batch`); - + // 🔧 개별 셀별로 스타일과 잠금 상태 설정 (편집 권한 보장) - stylesToSet.forEach(({row, col, isEditable}) => { + stylesToSet.forEach(({ row, col, isEditable }) => { try { const cell = activeSheet.getCell(row, col); const style = createCellStyle(activeSheet, row, col, isEditable); - + activeSheet.setStyle(row, col, style); cell.locked(!isEditable); // 편집 가능하면 잠금 해제 - + // 🆕 편집 가능한 셀에 기본 텍스트 에디터 설정 if (isEditable) { const textCellType = new GC.Spread.Sheets.CellTypes.Text(); @@ -511,7 +511,7 @@ const editableFieldsCount = React.useMemo(() => { 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); @@ -525,7 +525,7 @@ const editableFieldsCount = React.useMemo(() => { // ComboBox와 Validator 적용 activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType); activeSheet.setDataValidator(targetRow, cellPos.col, cellValidator); - + // 🚀 중요: 셀 잠금 해제 및 편집 가능 설정 const cell = activeSheet.getCell(targetRow, cellPos.col); cell.locked(false); @@ -546,7 +546,7 @@ const editableFieldsCount = React.useMemo(() => { const getSafeActiveSheet = React.useCallback((spread: any, functionName: string = 'unknown') => { if (!spread) return null; - + try { let activeSheet = spread.getActiveSheet(); if (!activeSheet) { @@ -568,7 +568,7 @@ const editableFieldsCount = React.useMemo(() => { const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => { try { if (!activeSheet) return false; - + const currentRowCount = activeSheet.getRowCount(); if (requiredRowCount > currentRowCount) { const newRowCount = requiredRowCount + 10; @@ -584,7 +584,7 @@ const editableFieldsCount = React.useMemo(() => { const ensureColumnCapacity = React.useCallback((activeSheet: any, requiredColumnCount: number) => { try { if (!activeSheet) return false; - + const currentColumnCount = activeSheet.getColumnCount(); if (requiredColumnCount > currentColumnCount) { const newColumnCount = requiredColumnCount + 10; @@ -606,206 +606,206 @@ const editableFieldsCount = React.useMemo(() => { }, []); // 🚀 최적화된 GRD_LIST 생성 - // 🚀 최적화된 GRD_LIST 생성 (TAG_DESC 컬럼 틀고정 포함) -const createGrdListTableOptimized = React.useCallback((activeSheet: any, template: TemplateItem) => { - console.log('🚀 Creating optimized GRD_LIST table with TAG_DESC freeze'); - - const visibleColumns = columnsJSON - .filter(col => col.hidden !== true) - .sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999)); - - if (visibleColumns.length === 0) return []; - - const startCol = 1; - const dataStartRow = 1; - const mappings: CellMapping[] = []; - - ensureColumnCapacity(activeSheet, startCol + visibleColumns.length); - ensureRowCapacity(activeSheet, dataStartRow + tableData.length); - - // 🧊 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`); + // 🚀 최적화된 GRD_LIST 생성 (TAG_DESC 컬럼 틀고정 포함) + const createGrdListTableOptimized = React.useCallback((activeSheet: any, template: TemplateItem) => { + console.log('🚀 Creating optimized GRD_LIST table with TAG_DESC freeze'); + + const visibleColumns = columnsJSON + .filter(col => col.hidden !== true) + .sort((a, b) => (a.seq ?? 999999) - (b.seq ?? 999999)); + + if (visibleColumns.length === 0) return []; + + const startCol = 1; + const dataStartRow = 1; + const mappings: CellMapping[] = []; + + ensureColumnCapacity(activeSheet, startCol + visibleColumns.length); + ensureRowCapacity(activeSheet, dataStartRow + tableData.length); + + // 🧊 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 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); - }); + // 헤더 생성 + 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 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); + 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); + }); + + // 🚀 데이터 배치 처리 준비 + 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); + } + }); + + if (editableRows.length > 0) { + dropdownConfigs.push({ + startRow: dataStartRow, + col: targetCol, + rowCount: tableData.length, + options: column.options, + editableRows: editableRows + }); } - }); - - if (editableRows.length > 0) { - dropdownConfigs.push({ - startRow: dataStartRow, + } + + 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, - rowCount: tableData.length, - options: column.options, - editableRows: editableRows + value: value ?? null + }); + + allStyles.push({ + row: targetRow, + col: targetCol, + isEditable: cellEditable }); - } - } - - 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}`); - if (safeOptions.length === 0) return; + const safeOptions = options + .filter(opt => opt !== null && opt !== undefined && opt !== '') + .map(opt => String(opt).trim()) + .filter(opt => opt.length > 0) + .slice(0, 20); - // 편집 가능한 행에만 드롭다운 적용 - editableRows.forEach(targetRow => { - try { - const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); - comboBoxCellType.items(safeOptions); - comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); + if (safeOptions.length === 0) return; - const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions.join(',')); - cellValidator.showInputMessage(false); - cellValidator.showErrorMessage(false); + // 편집 가능한 행에만 드롭다운 적용 + editableRows.forEach(targetRow => { + try { + const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); + comboBoxCellType.items(safeOptions); + comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); - activeSheet.setCellType(targetRow, col, comboBoxCellType); - activeSheet.setDataValidator(targetRow, col, cellValidator); - - // 🚀 편집 권한 명시적 설정 - const cell = activeSheet.getCell(targetRow, col); - cell.locked(false); + const cellValidator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions.join(',')); + cellValidator.showInputMessage(false); + cellValidator.showErrorMessage(false); - 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); - } - }); + activeSheet.setCellType(targetRow, col, comboBoxCellType); + activeSheet.setDataValidator(targetRow, col, cellValidator); - // 🧊 틀고정 설정 - 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); + // 🚀 편집 권한 명시적 설정 + 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); + } + }); + } 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 (styleError) { - console.warn(`⚠️ Failed to apply freeze border style to [${row}, ${col}]:`, styleError); } } + } catch (freezeError) { + console.error('❌ Failed to apply freeze:', freezeError); } - } catch (freezeError) { - console.error('❌ Failed to apply freeze:', freezeError); } - } - 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]); + 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`); @@ -817,22 +817,22 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat mappings.forEach((mapping) => { 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); - + if (mapping.isEditable) { // 🚀 편집 가능한 셀 설정 강화 cell.locked(false); - + 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); - + // DataValidation도 추가 const validator = GC.Spread.Sheets.DataValidation.createListValidator(columnConfig.options.join(',')); activeSheet.setDataValidator(cellPos.row, cellPos.col, validator); @@ -840,7 +840,7 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat // NUMBER 타입: 숫자 입력 허용 const textCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(cellPos.row, cellPos.col, textCellType); - + // 숫자 validation 추가 (에러 메시지 없이) const numberValidator = GC.Spread.Sheets.DataValidation.createNumberValidator( GC.Spread.Sheets.ConditionalFormatting.ComparisonOperators.between, @@ -854,11 +854,11 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat const textCellType = new GC.Spread.Sheets.CellTypes.Text(); activeSheet.setCellType(cellPos.row, cellPos.col, textCellType); } - + // 편집 가능 스타일 재적용 const editableStyle = createCellStyle(activeSheet, cellPos.row, cellPos.col, true); activeSheet.setStyle(cellPos.row, cellPos.col, editableStyle); - + console.log(`🔓 Cell [${cellPos.row}, ${cellPos.col}] ${mapping.attId} set as EDITABLE`); } else { // 읽기 전용 셀 @@ -930,7 +930,7 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat const dataRowIndex = exactMapping.dataRowIndex; if (dataRowIndex >= 0 && dataRowIndex < tableData.length) { const rowData = tableData[dataRowIndex]; - if (rowData?.shi === "OUT" || rowData?.shi === null ) { + 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; @@ -998,7 +998,7 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat console.log('🚀 Starting optimized spread initialization...'); setIsInitializing(true); updateProgress('Initializing...', 0, 100); - + setCurrentSpread(spread); setHasChanges(false); setValidationErrors([]); @@ -1007,7 +1007,7 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat spread.suspendPaint(); spread.suspendEvent(); spread.suspendCalcService(); - + updateProgress('Setting up workspace...', 10, 100); try { @@ -1021,19 +1021,19 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat if (templateType === 'GRD_LIST') { updateProgress('Creating dynamic table...', 20, 100); - + spread.clearSheets(); spread.addSheet(0); const sheet = spread.getSheet(0); sheet.name('Data'); spread.setActiveSheet('Data'); - + updateProgress('Processing table data...', 50, 100); mappings = createGrdListTableOptimized(sheet, workingTemplate); - + } else { updateProgress('Loading template structure...', 20, 100); - + let contentJson = workingTemplate.SPR_LST_SETUP?.CONTENT || workingTemplate.SPR_ITM_LST_SETUP?.CONTENT; let dataSheets = workingTemplate.SPR_LST_SETUP?.DATA_SHEETS || workingTemplate.SPR_ITM_LST_SETUP?.DATA_SHEETS; @@ -1042,10 +1042,10 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat } const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; - + updateProgress('Loading template layout...', 40, 100); spread.fromJSON(jsonData); - + activeSheet = getSafeActiveSheet(spread, 'after-fromJSON'); if (!activeSheet) { throw new Error('ActiveSheet became null after loading template'); @@ -1058,15 +1058,24 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat const activeSheetName = workingTemplate.SPR_LST_SETUP?.ACT_SHEET; - const matchingDataSheets = dataSheets.filter(ds => - ds.SHEET_NAME === activeSheetName - ); - if (activeSheetName && spread.getSheetFromName(activeSheetName)) { - spread.setActiveSheet(activeSheetName); - } + // 🔧 각 DATA_SHEET별로 처리 + dataSheets.forEach(dataSheet => { + const sheetName = dataSheet.SHEET_NAME; + + // 해당 시트가 존재하는지 확인 + const targetSheet = spread.getSheetFromName(sheetName); + if (!targetSheet) { + console.warn(`⚠️ Sheet '${sheetName}' not found in template`); + return; + } + + console.log(`📋 Processing sheet: ${sheetName}`); + + // 해당 시트로 전환 + spread.setActiveSheet(sheetName); + const currentSheet = spread.getActiveSheet(); - matchingDataSheets.forEach(dataSheet => { if (dataSheet.MAP_CELL_ATT && dataSheet.MAP_CELL_ATT.length > 0) { dataSheet.MAP_CELL_ATT.forEach((mapping: any) => { const { ATT_ID, IN } = mapping; @@ -1076,11 +1085,11 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat if (!cellPos) return; const requiredRows = cellPos.row + tableData.length; - if (!ensureRowCapacity(activeSheet, requiredRows)) return; + if (!ensureRowCapacity(currentSheet, requiredRows)) return; // 🚀 배치 데이터 준비 - const valuesToSet: Array<{row: number, col: number, value: any}> = []; - const stylesToSet: Array<{row: number, col: number, isEditable: boolean}> = []; + const valuesToSet: Array<{ row: number, col: number, value: any }> = []; + const stylesToSet: Array<{ row: number, col: number, isEditable: boolean }> = []; tableData.forEach((rowData, index) => { const targetRow = cellPos.row + index; @@ -1108,67 +1117,95 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat }); // 🚀 배치 처리 - setBatchValues(activeSheet, valuesToSet); - setBatchStyles(activeSheet, stylesToSet); + setBatchValues(currentSheet, valuesToSet); + setBatchStyles(currentSheet, stylesToSet); // 드롭다운 설정 const columnConfig = columnsJSON.find(col => col.key === ATT_ID); if (columnConfig?.type === "LIST" && columnConfig.options) { const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData)); if (hasEditableRows) { - setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length); + setupOptimizedListValidation(currentSheet, cellPos, columnConfig.options, tableData.length); } } }); } }); - + + // 🔧 마지막에 activeSheetName으로 다시 전환 + if (activeSheetName && spread.getSheetFromName(activeSheetName)) { + spread.setActiveSheet(activeSheetName); + activeSheet = spread.getActiveSheet(); + } + } else if (templateType === 'SPREAD_ITEM' && selectedRow) { updateProgress('Setting up form fields...', 60, 100); - const activeSheetName = workingTemplate.SPR_ITM_LST_SETUP?.ACT_SHEET; + const activeSheetName = workingTemplate.SPR_ITM_LST_SETUP?.ACT_SHEET; - const matchingDataSheets = dataSheets.filter(ds => - ds.SHEET_NAME === activeSheetName - ); - - if (activeSheetName && spread.getSheetFromName(activeSheetName)) { - spread.setActiveSheet(activeSheetName); + if (activeSheetName && spread.getSheetFromName(activeSheetName)) { + spread.setActiveSheet(activeSheetName); + } + + dataSheets.forEach(dataSheet => { + + const sheetName = dataSheet.SHEET_NAME; + // 해당 시트가 존재하는지 확인 + const targetSheet = spread.getSheetFromName(sheetName); + if (!targetSheet) { + console.warn(`⚠️ Sheet '${sheetName}' not found in template`); + return; } - matchingDataSheets.forEach(dataSheet => { + console.log(`📋 Processing sheet: ${sheetName}`); + + // 해당 시트로 전환 + spread.setActiveSheet(sheetName); + const currentSheet = spread.getActiveSheet(); + + dataSheet.MAP_CELL_ATT?.forEach((mapping: any) => { const { ATT_ID, IN } = mapping; const cellPos = parseCellAddress(IN); + + if (cellPos) { const isEditable = isFieldEditable(ATT_ID); const value = selectedRow[ATT_ID]; - + mappings.push({ attId: ATT_ID, cellAddress: IN, isEditable: isEditable, dataRowIndex: 0 }); - - const cell = activeSheet.getCell(cellPos.row, cellPos.col); + + const cell = currentSheet.getCell(cellPos.row, cellPos.col); cell.value(value ?? null); - - const style = createCellStyle(activeSheet, cellPos.row, cellPos.col, isEditable); - activeSheet.setStyle(cellPos.row, cellPos.col, style); - + + const style = createCellStyle(currentSheet, cellPos.row, cellPos.col, isEditable); + currentSheet.setStyle(cellPos.row, cellPos.col, style); + const columnConfig = columnsJSON.find(col => col.key === ATT_ID); if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { - setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1); + setupOptimizedListValidation(currentSheet, cellPos, columnConfig.options, 1); } } }); + + // 🔧 마지막에 activeSheetName으로 다시 전환 + if (activeSheetName && spread.getSheetFromName(activeSheetName)) { + spread.setActiveSheet(activeSheetName); + activeSheet = spread.getActiveSheet(); + } + + }); } } updateProgress('Configuring interactions...', 90, 100); setCellMappings(mappings); - + const finalActiveSheet = getSafeActiveSheet(spread, 'setupEvents'); if (finalActiveSheet) { setupSheetProtectionAndEvents(finalActiveSheet, mappings); @@ -1201,20 +1238,20 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat 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); @@ -1224,59 +1261,59 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat } } }); - + 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) { 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); 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; @@ -1286,18 +1323,18 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat } } }); - + if (hasRowChanges) { console.log(`💾 Saving row ${i} with changes`); dataToSave.TAG_NO = originalRow.TAG_NO; - + try { const { success, message } = await updateFormDataInDB( formCode, contractItemId, dataToSave ); - + if (success) { updatedRows.push(dataToSave); saveCount++; @@ -1317,9 +1354,9 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat 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); @@ -1328,10 +1365,10 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat 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"); @@ -1339,16 +1376,16 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat setIsPending(false); } }, [ - currentSpread, - hasChanges, - templateType, - selectedRow, - tableData, - formCode, - contractItemId, - onUpdateSuccess, - cellMappings, - columnsJSON, + currentSpread, + hasChanges, + templateType, + selectedRow, + tableData, + formCode, + contractItemId, + onUpdateSuccess, + cellMappings, + columnsJSON, validateAllData, isFieldEditable // 🔧 의존성 추가 ]); @@ -1357,11 +1394,11 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat const isDataValid = templateType === 'SPREAD_ITEM' ? !!selectedRow : tableData.length > 0; const dataCount = templateType === 'SPREAD_ITEM' ? 1 : tableData.length; - + return ( SEDP Template - {formCode} @@ -1439,13 +1476,13 @@ const createGrdListTableOptimized = React.useCallback((activeSheet: any, templat
{/* 🆕 로딩 프로그레스 오버레이 */} - - + {selectedTemplate && isClient && isDataValid ? (