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