diff options
| -rw-r--r-- | components/form-data/spreadJS-dialog.tsx | 760 | ||||
| -rw-r--r-- | lib/pdftron/serverSDK/createBasicContractPdf.ts | 133 | ||||
| -rw-r--r-- | lib/vendors/table/request-pq-dialog.tsx | 267 | ||||
| -rw-r--r-- | pages/api/pdftron/createBasicContractPdf.ts | 16 |
4 files changed, 695 insertions, 481 deletions
diff --git a/components/form-data/spreadJS-dialog.tsx b/components/form-data/spreadJS-dialog.tsx index 7ed861c2..1d0796fe 100644 --- a/components/form-data/spreadJS-dialog.tsx +++ b/components/form-data/spreadJS-dialog.tsx @@ -16,7 +16,7 @@ import { DataTableColumnJSON, ColumnType } from "./form-data-table-columns"; // SpreadSheets를 동적으로 import (SSR 비활성화) const SpreadSheets = dynamic( () => import("@mescius/spread-sheets-react").then(mod => mod.SpreadSheets), - { + { ssr: false, loading: () => ( <div className="flex items-center justify-center h-full"> @@ -77,6 +77,13 @@ interface ValidationError { message: string; } +interface CellMapping { + attId: string; + cellAddress: string; + isEditable: boolean; + dataRowIndex?: number; +} + interface TemplateViewDialogProps { isOpen: boolean; onClose: () => void; @@ -110,7 +117,7 @@ export function TemplateViewDialog({ const [isPending, setIsPending] = React.useState(false); const [hasChanges, setHasChanges] = React.useState(false); const [currentSpread, setCurrentSpread] = React.useState<any>(null); - const [cellMappings, setCellMappings] = React.useState<Array<{attId: string, cellAddress: string, isEditable: boolean}>>([]); + const [cellMappings, setCellMappings] = React.useState<CellMapping[]>([]); const [isClient, setIsClient] = React.useState(false); const [templateType, setTemplateType] = React.useState<'SPREAD_LIST' | 'SPREAD_ITEM' | null>(null); const [validationErrors, setValidationErrors] = React.useState<ValidationError[]>([]); @@ -125,25 +132,25 @@ export function TemplateViewDialog({ // 사용 가능한 템플릿들을 필터링하고 설정 React.useEffect(() => { if (!templateData) return; - + let templates: TemplateItem[]; if (Array.isArray(templateData)) { templates = templateData as TemplateItem[]; } else { templates = [templateData as TemplateItem]; } - + // CONTENT가 있는 템플릿들 필터링 const validTemplates = templates.filter(template => { const hasSpreadListContent = template.SPR_LST_SETUP?.CONTENT; const hasSpreadItemContent = template.SPR_ITM_LST_SETUP?.CONTENT; const isValidType = template.TMPL_TYPE === "SPREAD_LIST" || template.TMPL_TYPE === "SPREAD_ITEM"; - + return isValidType && (hasSpreadListContent || hasSpreadItemContent); }); - + setAvailableTemplates(validTemplates); - + // 첫 번째 유효한 템플릿을 기본으로 선택 if (validTemplates.length > 0 && !selectedTemplateId) { setSelectedTemplateId(validTemplates[0].TMPL_ID); @@ -159,7 +166,7 @@ export function TemplateViewDialog({ setTemplateType(template.TMPL_TYPE as 'SPREAD_LIST' | 'SPREAD_ITEM'); setHasChanges(false); setValidationErrors([]); - + // SpreadSheets 재초기화 if (currentSpread) { const template = availableTemplates.find(t => t.TMPL_ID === templateId); @@ -175,12 +182,26 @@ export function TemplateViewDialog({ return availableTemplates.find(t => t.TMPL_ID === selectedTemplateId); }, [availableTemplates, selectedTemplateId]); + // 편집 가능한 필드 목록 계산 const editableFields = React.useMemo(() => { - if (!selectedRow?.TAG_NO || !editableFieldsMap.has(selectedRow.TAG_NO)) { - return []; + // SPREAD_ITEM인 경우: selectedRow의 TAG_NO로 확인 + if (templateType === 'SPREAD_ITEM' && selectedRow?.TAG_NO) { + if (!editableFieldsMap.has(selectedRow.TAG_NO)) { + return []; + } + return editableFieldsMap.get(selectedRow.TAG_NO) || []; + } + + // SPREAD_LIST인 경우: 첫 번째 행의 TAG_NO를 기준으로 처리 + if (templateType === 'SPREAD_LIST' && tableData.length > 0) { + const firstRowTagNo = tableData[0]?.TAG_NO; + if (firstRowTagNo && editableFieldsMap.has(firstRowTagNo)) { + return editableFieldsMap.get(firstRowTagNo) || []; + } } - return editableFieldsMap.get(selectedRow.TAG_NO) || []; - }, [selectedRow?.TAG_NO, editableFieldsMap]); + + return []; + }, [templateType, selectedRow?.TAG_NO, tableData, editableFieldsMap]); // 필드가 편집 가능한지 판별하는 함수 const isFieldEditable = React.useCallback((attId: string, rowData?: GenericData) => { @@ -189,15 +210,36 @@ export function TemplateViewDialog({ if (columnConfig?.shi === true) { return false; // columnsJSON에서 shi가 true이면 편집 불가 } - + // TAG_NO와 TAG_DESC는 기본적으로 편집 가능 (columnsJSON의 shi가 false인 경우) if (attId === "TAG_NO" || attId === "TAG_DESC") { return true; } + + // SPREAD_ITEM인 경우: editableFields 체크 + if (templateType === 'SPREAD_ITEM') { + return editableFields.includes(attId); + } - // SPREAD_LIST인 경우는 기본적으로 편집 가능 (개별 행의 shi 상태는 저장시 확인) + // SPREAD_LIST인 경우: 개별 행의 편집 가능성도 고려 + if (templateType === 'SPREAD_LIST') { + // 기본적으로 editableFields에 포함되어야 함 + if (!editableFields.includes(attId)) { + return false; + } + + // rowData가 제공된 경우 해당 행의 shi 상태도 확인 + if (rowData && rowData.shi === true) { + return false; + } + + return true; + } + + // 기본적으로는 editableFields 체크 + // return editableFields.includes(attId); return true; - }, [templateType, selectedRow, columnsJSON, editableFieldsMap]); + }, [templateType, columnsJSON, editableFields]); // 편집 가능한 필드 개수 계산 const editableFieldsCount = React.useMemo(() => { @@ -205,22 +247,22 @@ export function TemplateViewDialog({ }, [cellMappings]); // 셀 주소를 행과 열로 변환하는 함수 - const parseCellAddress = (address: string): {row: number, col: number} | null => { + const parseCellAddress = (address: string): { row: number, col: number } | null => { if (!address || address.trim() === "") return null; - + const match = address.match(/^([A-Z]+)(\d+)$/); if (!match) return null; - + const [, colStr, rowStr] = match; - + let col = 0; for (let i = 0; i < colStr.length; i++) { col = col * 26 + (colStr.charCodeAt(i) - 65 + 1); } col -= 1; - + const row = parseInt(rowStr) - 1; - + return { row, col }; }; @@ -248,7 +290,7 @@ export function TemplateViewDialog({ // 커스텀 타입의 경우 추가 검증 로직이 필요할 수 있음 break; } - + return null; }; @@ -270,7 +312,7 @@ export function TemplateViewDialog({ // 단일 행 검증 const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); - + if (errorMessage) { errors.push({ cellAddress: mapping.cellAddress, @@ -281,312 +323,502 @@ export function TemplateViewDialog({ }); } } else if (templateType === 'SPREAD_LIST') { - // 복수 행 검증 - for (let i = 0; i < tableData.length; i++) { - const targetRow = cellPos.row + i; - const cellValue = activeSheet.getValue(targetRow, cellPos.col); - const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); - - if (errorMessage) { - errors.push({ - cellAddress: `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`, - attId: mapping.attId, - value: cellValue, - expectedType: columnConfig.type, - message: errorMessage - }); - } + // 복수 행 검증 - 각 매핑은 이미 개별 행을 가리킴 + const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); + const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); + + if (errorMessage) { + errors.push({ + cellAddress: mapping.cellAddress, + attId: mapping.attId, + value: cellValue, + expectedType: columnConfig.type, + message: errorMessage + }); } } }); setValidationErrors(errors); return errors; - }, [currentSpread, selectedTemplate, cellMappings, columnsJSON, templateType, tableData]); - - // LIST 타입 컬럼에 드롭다운 설정 - const setupListValidation = React.useCallback((activeSheet: any, cellPos: {row: number, col: number}, options: string[], rowCount: number = 1) => { - // ComboBox 셀 타입 생성 - const comboBoxCellType = new GC.Spread.Sheets.CellTypes.ComboBox(); - comboBoxCellType.items(options); - comboBoxCellType.editorValueType(GC.Spread.Sheets.CellTypes.EditorValueType.text); - - // 단일 셀 또는 범위에 적용 - for (let i = 0; i < rowCount; i++) { - const targetRow = cellPos.row + i; - activeSheet.setCellType(targetRow, cellPos.col, comboBoxCellType); + }, [currentSpread, selectedTemplate, cellMappings, columnsJSON, templateType]); + + // ═══════════════════════════════════════════════════════════════════════════════ + // 🛠️ 헬퍼 함수들 + // ═══════════════════════════════════════════════════════════════════════════════ + + // 🎨 셀 스타일 생성 + const createCellStyle = React.useCallback((isEditable: boolean) => { + const style = new GC.Spread.Sheets.Style(); + if (isEditable) { + style.backColor = "#f0fdf4"; // 연한 초록 (편집 가능) + } else { + style.backColor = "#f9fafb"; // 연한 회색 (읽기 전용) + style.foreColor = "#6b7280"; + } + return style; + }, []); + + // 📋 최적화된 LIST 드롭다운 설정 +const setupOptimizedListValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { + try { + console.log(`🎯 Setting up DataValidation dropdown for ${rowCount} rows with options:`, options); + + // 🚨 성능 임계점 확인 + if (rowCount > 100) { + console.warn(`⚡ Large dataset (${rowCount} rows): Using simple validation only`); + setupSimpleValidation(activeSheet, cellPos, options, rowCount); + return; + } + + // ✅ 1단계: options 철저하게 정규화 (이것이 에러 방지의 핵심!) + let safeOptions; + try { + safeOptions = options + .filter(opt => opt !== null && opt !== undefined && opt !== '') // null, undefined, 빈값 제거 + .map(opt => { + // 모든 값을 안전한 문자열로 변환 + let str = String(opt); + // 특수 문자나 문제 있는 문자 처리 + str = str.replace(/[\r\n\t]/g, ' '); // 줄바꿈, 탭을 공백으로 + str = str.replace(/[^\x20-\x7E\u00A1-\uFFFF]/g, ''); // 제어 문자 제거 + return str.trim(); + }) + .filter(opt => opt.length > 0 && opt.length < 255) // 빈값과 너무 긴 값 제거 + .filter((opt, index, arr) => arr.indexOf(opt) === index) // 중복 제거 + .slice(0, 100); // 최대 100개로 제한 + + console.log(`📋 Original options:`, options); + console.log(`📋 Safe options:`, safeOptions); + } catch (filterError) { + console.error('❌ Options filtering failed:', filterError); + safeOptions = ['Option1', 'Option2']; // 안전한 폴백 옵션 + } + + if (safeOptions.length === 0) { + console.warn(`⚠️ No valid options found, using fallback`); + safeOptions = ['Please Select']; + } + + // ✅ 2단계: DataValidation 생성 (엑셀 스타일 드롭다운) + let validator; + try { + validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions); + console.log(`✅ DataValidation validator created successfully`); + } catch (validatorError) { + console.error('❌ Failed to create validator:', validatorError); + return; + } + + // ✅ 3단계: 셀/범위에 적용 + try { + if (rowCount > 1) { + // 범위에 적용 + const range = activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1); + range.dataValidator(validator); + console.log(`✅ DataValidation applied to range [${cellPos.row}, ${cellPos.col}, ${rowCount}, 1]`); + } else { + // 단일 셀에 적용 + activeSheet.setDataValidator(cellPos.row, cellPos.col, validator); + console.log(`✅ DataValidation applied to single cell [${cellPos.row}, ${cellPos.col}]`); + } + } catch (applicationError) { + console.error('❌ Failed to apply DataValidation:', applicationError); - // 추가로 데이터 검증도 설정 - const validator = GC.Spread.Sheets.DataValidation.createListValidator(options); - activeSheet.setDataValidator(targetRow, cellPos.col, validator); + // 폴백: 개별 셀에 하나씩 적용 + console.log('🔄 Trying individual cell application...'); + for (let i = 0; i < Math.min(rowCount, 50); i++) { + try { + const individualValidator = GC.Spread.Sheets.DataValidation.createListValidator([...safeOptions]); + activeSheet.setDataValidator(cellPos.row + i, cellPos.col, individualValidator); + console.log(`✅ Individual DataValidation set for row ${cellPos.row + i}`); + } catch (individualError) { + console.warn(`⚠️ Failed individual cell ${cellPos.row + i}:`, individualError); + } + } + } + + console.log(`✅ DataValidation dropdown setup completed`); + + } catch (error) { + console.error('❌ DataValidation setup failed completely:', error); + console.error('Error stack:', error.stack); + console.log('🔄 Falling back to no validation'); + } +}, []); + +// ⚡ 단순 검증 설정 (드롭다운 없이 검증만) +const setupSimpleValidation = React.useCallback((activeSheet: any, cellPos: { row: number, col: number }, options: string[], rowCount: number) => { + try { + console.log(`⚡ Setting up simple validation (no dropdown UI) for ${rowCount} rows`); + + const safeOptions = options + .filter(opt => opt != null && opt !== '') + .map(opt => String(opt).trim()) + .filter(opt => opt.length > 0); + + if (safeOptions.length === 0) return; + + const validator = GC.Spread.Sheets.DataValidation.createListValidator(safeOptions); + + // 범위로 적용 시도 + try { + activeSheet.getRange(cellPos.row, cellPos.col, rowCount, 1).dataValidator(validator); + console.log(`✅ Simple validation applied to range`); + } catch (rangeError) { + console.warn('Range validation failed, trying individual cells'); + // 폴백: 개별 적용 + for (let i = 0; i < Math.min(rowCount, 100); i++) { + try { + activeSheet.setDataValidator(cellPos.row + i, cellPos.col, validator); + } catch (individualError) { + // 개별 실패해도 계속 + } + } + } + + } catch (error) { + console.error('❌ Simple validation failed:', error); + } +}, []); + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🔍 디버깅용: 에러 발생 원인 추적 +// ═══════════════════════════════════════════════════════════════════════════════ + +const debugDropdownError = (options: any[], attId: string) => { + console.group(`🔍 Debugging dropdown for ${attId}`); + + console.log('Original options type:', typeof options); + console.log('Is array:', Array.isArray(options)); + console.log('Length:', options?.length); + console.log('Raw options:', options); + + if (Array.isArray(options)) { + options.forEach((opt, index) => { + console.log(`[${index}] Type: ${typeof opt}, Value: "${opt}", String: "${String(opt)}"`); + + // 문제 있는 값 체크 + if (opt === null) console.warn(` ⚠️ NULL value at index ${index}`); + if (opt === undefined) console.warn(` ⚠️ UNDEFINED value at index ${index}`); + if (typeof opt === 'object') console.warn(` ⚠️ OBJECT value at index ${index}:`, opt); + if (typeof opt === 'string' && opt.includes('\n')) console.warn(` ⚠️ NEWLINE in string at index ${index}`); + if (typeof opt === 'string' && opt.length === 0) console.warn(` ⚠️ EMPTY STRING at index ${index}`); + }); + } + + console.groupEnd(); +}; + + // 🚀 행 용량 확보 + const ensureRowCapacity = React.useCallback((activeSheet: any, requiredRowCount: number) => { + const currentRowCount = activeSheet.getRowCount(); + if (requiredRowCount > currentRowCount) { + activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가 + console.log(`📈 Expanded sheet to ${requiredRowCount + 10} rows`); } }, []); + // 🛡️ 시트 보호 및 이벤트 설정 + const setupSheetProtectionAndEvents = React.useCallback((activeSheet: any, mappings: CellMapping[]) => { + console.log(`🛡️ Setting up protection and events for ${mappings.length} mappings`); + + // 시트 보호 설정 + activeSheet.options.isProtected = true; + activeSheet.options.protectionOptions = { + allowSelectLockedCells: true, + allowSelectUnlockedCells: true, + allowSort: false, + allowFilter: false, + allowEditObjects: false, + allowResizeRows: false, + allowResizeColumns: false + }; + + // 🎯 변경 감지 이벤트 + const changeEvents = [ + GC.Spread.Sheets.Events.CellChanged, + GC.Spread.Sheets.Events.ValueChanged, + GC.Spread.Sheets.Events.ClipboardPasted + ]; + + changeEvents.forEach(eventType => { + activeSheet.bind(eventType, () => { + console.log(`📝 ${eventType} detected`); + setHasChanges(true); + }); + }); + + // 🚫 편집 시작 권한 확인 (수정됨) + activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { + console.log(`🎯 EditStarting: Row ${info.row}, Col ${info.col}`); + + // ✅ 정확한 매핑 찾기 (행/열 정확히 일치) + const exactMapping = mappings.find(m => { + const cellPos = parseCellAddress(m.cellAddress); + return cellPos && cellPos.row === info.row && cellPos.col === info.col; + }); + + if (!exactMapping) { + console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - allowing edit`); + return; // 매핑이 없으면 허용 (템플릿 영역 밖) + } + + console.log(`📋 Found mapping: ${exactMapping.attId} at ${exactMapping.cellAddress}`); + + // 기본 편집 권한 확인 + if (!exactMapping.isEditable) { + console.log(`🚫 Field ${exactMapping.attId} is not editable`); + toast.warning(`${exactMapping.attId} field is read-only`); + info.cancel = true; + return; + } + + // SPREAD_LIST 개별 행 SHI 확인 + if (templateType === 'SPREAD_LIST' && exactMapping.dataRowIndex !== undefined) { + const dataRowIndex = exactMapping.dataRowIndex; + + console.log(`🔍 Checking SHI for data row ${dataRowIndex}`); + + if (dataRowIndex >= 0 && dataRowIndex < tableData.length) { + const rowData = tableData[dataRowIndex]; + if (rowData?.shi === true) { + console.log(`🚫 Row ${dataRowIndex} is in SHI mode`); + toast.warning(`Row ${dataRowIndex + 1}: ${exactMapping.attId} field is read-only (SHI mode)`); + info.cancel = true; + return; + } + } else { + console.warn(`⚠️ Invalid dataRowIndex: ${dataRowIndex} (tableData.length: ${tableData.length})`); + } + } + + console.log(`✅ Edit allowed for ${exactMapping.attId}`); + }); + + // ✅ 편집 완료 검증 (수정됨) + activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (event: any, info: any) => { + console.log(`🏁 EditEnded: Row ${info.row}, Col ${info.col}`); + + // ✅ 정확한 매핑 찾기 + const exactMapping = mappings.find(m => { + const cellPos = parseCellAddress(m.cellAddress); + return cellPos && cellPos.row === info.row && cellPos.col === info.col; + }); + + if (!exactMapping) { + console.log(`ℹ️ No mapping found for [${info.row}, ${info.col}] - skipping validation`); + return; + } + + const columnConfig = columnsJSON.find(col => col.key === exactMapping.attId); + if (columnConfig) { + const cellValue = activeSheet.getValue(info.row, info.col); + console.log(`🔍 Validating ${exactMapping.attId}: "${cellValue}"`); + + const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); + const cell = activeSheet.getCell(info.row, info.col); + + if (errorMessage) { + console.log(`❌ Validation failed: ${errorMessage}`); + + // 🚨 에러 스타일 적용 (편집 가능 상태 유지) + const errorStyle = new GC.Spread.Sheets.Style(); + errorStyle.backColor = "#fef2f2"; + errorStyle.foreColor = "#dc2626"; + errorStyle.borderLeft = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); + errorStyle.borderRight = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); + errorStyle.borderTop = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); + errorStyle.borderBottom = new GC.Spread.Sheets.LineBorder("#dc2626", GC.Spread.Sheets.LineStyle.thick); + + activeSheet.setStyle(info.row, info.col, errorStyle); + cell.locked(!exactMapping.isEditable); + + toast.warning(`Invalid value in ${exactMapping.attId}: ${errorMessage}. Please correct the value.`, { duration: 5000 }); + } else { + console.log(`✅ Validation passed`); + + // ✅ 정상 스타일 복원 + const normalStyle = createCellStyle(exactMapping.isEditable); + activeSheet.setStyle(info.row, info.col, normalStyle); + cell.locked(!exactMapping.isEditable); + } + } + }); + + console.log(`🛡️ Protection and events configured for ${mappings.length} mappings`); + }, [templateType, tableData, createCellStyle, validateCellValue, columnsJSON]); + + // ═══════════════════════════════════════════════════════════════════════════════ + // 🏗️ 메인 SpreadSheets 초기화 함수 + // ═══════════════════════════════════════════════════════════════════════════════ + const initSpread = React.useCallback((spread: any, template?: TemplateItem) => { const workingTemplate = template || selectedTemplate; if (!spread || !workingTemplate) return; try { + // 🔄 초기 설정 setCurrentSpread(spread); setHasChanges(false); setValidationErrors([]); - // SPR_LST_SETUP.CONTENT와 SPR_ITM_LST_SETUP.CONTENT 중에서 값이 있는 것을 찾아서 사용 + // 📋 템플릿 콘텐츠 및 데이터 시트 추출 let contentJson = null; let dataSheets = null; - - // SPR_LST_SETUP.CONTENT가 있으면 우선 사용 + + // SPR_LST_SETUP.CONTENT 우선 사용 if (workingTemplate.SPR_LST_SETUP?.CONTENT) { contentJson = workingTemplate.SPR_LST_SETUP.CONTENT; dataSheets = workingTemplate.SPR_LST_SETUP.DATA_SHEETS; - console.log('Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME, '(TMPL_TYPE:', workingTemplate.TMPL_TYPE, ')'); - } - // SPR_ITM_LST_SETUP.CONTENT가 있으면 사용 + console.log('✅ Using SPR_LST_SETUP.CONTENT for template:', workingTemplate.NAME); + } + // SPR_ITM_LST_SETUP.CONTENT 대안 사용 else if (workingTemplate.SPR_ITM_LST_SETUP?.CONTENT) { contentJson = workingTemplate.SPR_ITM_LST_SETUP.CONTENT; dataSheets = workingTemplate.SPR_ITM_LST_SETUP.DATA_SHEETS; - console.log('Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME, '(TMPL_TYPE:', workingTemplate.TMPL_TYPE, ')'); + console.log('✅ Using SPR_ITM_LST_SETUP.CONTENT for template:', workingTemplate.NAME); } if (!contentJson) { - console.warn('No CONTENT found in template:', workingTemplate.NAME); + console.warn('❌ No CONTENT found in template:', workingTemplate.NAME); return; } - console.log(`Loading template content for: ${workingTemplate.NAME} (Type: ${workingTemplate.TMPL_TYPE})`); + // 🏗️ SpreadSheets 초기화 + const jsonData = typeof contentJson === 'string' ? JSON.parse(contentJson) : contentJson; - const jsonData = typeof contentJson === 'string' - ? JSON.parse(contentJson) - : contentJson; - - // 렌더링 일시 중단 + // 성능을 위한 렌더링 일시 중단 spread.suspendPaint(); try { - // fromJSON으로 템플릿 구조 로드 + // 템플릿 구조 로드 spread.fromJSON(jsonData); - - // 활성 시트 가져오기 const activeSheet = spread.getActiveSheet(); - // 시트 보호 먼저 해제 + // 시트 보호 해제 (편집을 위해) activeSheet.options.isProtected = false; - // MAP_CELL_ATT 정보를 사용해서 데이터 매핑 + // 📊 셀 매핑 및 데이터 처리 if (dataSheets && dataSheets.length > 0) { - const mappings: Array<{attId: string, cellAddress: string, isEditable: boolean}> = []; + const mappings: CellMapping[] = []; + // 🔄 각 데이터 시트의 매핑 정보 처리 dataSheets.forEach(dataSheet => { if (dataSheet.MAP_CELL_ATT) { dataSheet.MAP_CELL_ATT.forEach(mapping => { const { ATT_ID, IN } = mapping; - + if (IN && IN.trim() !== "") { const cellPos = parseCellAddress(IN); if (cellPos) { - const isEditable = isFieldEditable(ATT_ID); const columnConfig = columnsJSON.find(col => col.key === ATT_ID); - - mappings.push({ - attId: ATT_ID, - cellAddress: IN, - isEditable: isEditable - }); - - // 템플릿 타입에 따라 다른 데이터 처리 + + // 🎯 템플릿 타입별 데이터 처리 if (templateType === 'SPREAD_ITEM' && selectedRow) { - // 단일 행 처리 (기존 로직) + // 📝 단일 행 처리 (SPREAD_ITEM) + const isEditable = isFieldEditable(ATT_ID); + + // 매핑 정보 저장 + mappings.push({ + attId: ATT_ID, + cellAddress: IN, + isEditable: isEditable, + dataRowIndex: 0 + }); + const cell = activeSheet.getCell(cellPos.row, cellPos.col); const value = selectedRow[ATT_ID]; - if (value !== undefined && value !== null) { - cell.value(value); - } - if (value === undefined || value === null) { - cell.value(null); - } + // 값 설정 + cell.value(value ?? null); + + // 🎨 스타일 및 편집 권한 설정 + cell.locked(!isEditable); + const style = createCellStyle(isEditable); + activeSheet.setStyle(cellPos.row, cellPos.col, style); - // LIST 타입 컬럼에 드롭다운 설정 + // 📋 LIST 타입 드롭다운 설정 if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { - setupListValidation(activeSheet, cellPos, columnConfig.options, 1); + setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, 1); } - // 스타일 적용 - cell.locked(!isEditable); - const existingStyle = activeSheet.getStyle(cellPos.row, cellPos.col); - const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style(); - - if (isEditable) { - newStyle.backColor = "#f0fdf4"; - } else { - newStyle.backColor = "#f9fafb"; - newStyle.foreColor = "#6b7280"; - } - - activeSheet.setStyle(cellPos.row, cellPos.col, newStyle); - } else if (templateType === 'SPREAD_LIST' && tableData.length > 0) { - // 복수 행 처리 - 첫 번째 행부터 시작해서 아래로 채움 + // 📊 복수 행 처리 (SPREAD_LIST) - 성능 최적화됨 + console.log(`🔄 Processing SPREAD_LIST for ${ATT_ID} with ${tableData.length} rows`); - // LIST 타입 컬럼에 드롭다운 설정 (모든 행에 대해) - if (columnConfig?.type === "LIST" && columnConfig.options && isEditable) { - setupListValidation(activeSheet, cellPos, columnConfig.options, tableData.length); - } + // 🚀 행 확장 (필요시) + ensureRowCapacity(activeSheet, cellPos.row + tableData.length); + + // 📋 각 행마다 개별 매핑 생성 + tableData.forEach((rowData, index) => { + const targetRow = cellPos.row + index; + const targetCellAddress = `${String.fromCharCode(65 + cellPos.col)}${targetRow + 1}`; + const cellEditable = isFieldEditable(ATT_ID, rowData); + + // 개별 매핑 추가 + mappings.push({ + attId: ATT_ID, + cellAddress: targetCellAddress, // 각 행마다 다른 주소 + isEditable: cellEditable, + dataRowIndex: index // 원본 데이터 인덱스 + }); + + console.log(`📝 Mapping ${ATT_ID} Row ${index}: ${targetCellAddress} (${cellEditable ? 'Editable' : 'ReadOnly'})`); + }); - // 필요한 경우 행 추가 (tableData 길이만큼 충분히 확보) - const currentRowCount = activeSheet.getRowCount(); - const requiredRowCount = cellPos.row + tableData.length; - if (requiredRowCount > currentRowCount) { - activeSheet.setRowCount(requiredRowCount + 10); // 여유분 추가 + // 📋 LIST 타입 드롭다운 설정 (조건부) + if (columnConfig?.type === "LIST" && columnConfig.options) { + // 편집 가능한 행이 하나라도 있으면 드롭다운 설정 + const hasEditableRows = tableData.some((rowData) => isFieldEditable(ATT_ID, rowData)); + if (hasEditableRows) { + setupOptimizedListValidation(activeSheet, cellPos, columnConfig.options, tableData.length); + } } + // 🎨 개별 셀 데이터 및 스타일 설정 tableData.forEach((rowData, index) => { const targetRow = cellPos.row + index; const cell = activeSheet.getCell(targetRow, cellPos.col); const value = rowData[ATT_ID]; - if (value !== undefined && value !== null) { - cell.value(value); - } - - if (value === undefined || value === null) { - cell.value(null); - } + // 값 설정 + cell.value(value ?? null); + console.log(`📝 Row ${targetRow}: ${ATT_ID} = "${value}"`); + // 편집 권한 및 스타일 설정 const cellEditable = isFieldEditable(ATT_ID, rowData); cell.locked(!cellEditable); - - // 스타일 적용 - const existingStyle = activeSheet.getStyle(targetRow, cellPos.col); - const newStyle = existingStyle ? Object.assign(new GC.Spread.Sheets.Style(), existingStyle) : new GC.Spread.Sheets.Style(); - - if (cellEditable) { - newStyle.backColor = "#f0fdf4"; - } else { - newStyle.backColor = "#f9fafb"; - newStyle.foreColor = "#6b7280"; - } - - activeSheet.setStyle(targetRow, cellPos.col, newStyle); + const style = createCellStyle(cellEditable); + activeSheet.setStyle(targetRow, cellPos.col, style); }); } - - console.log(`Mapped ${ATT_ID} to cell ${IN} - ${isEditable ? 'Editable' : 'Read-only'}`); + + console.log(`📌 Mapped ${ATT_ID} → ${IN} (${templateType})`); } } }); } }); - - setCellMappings(mappings); - - // 시트 보호 설정 - activeSheet.options.isProtected = true; - activeSheet.options.protectionOptions = { - allowSelectLockedCells: true, - allowSelectUnlockedCells: true, - allowSort: false, - allowFilter: false, - allowEditObjects: false, - allowResizeRows: false, - allowResizeColumns: false - }; - - // 이벤트 리스너 추가 - activeSheet.bind(GC.Spread.Sheets.Events.CellChanged, (event: any, info: any) => { - console.log('Cell changed:', info); - setHasChanges(true); - }); - - activeSheet.bind(GC.Spread.Sheets.Events.ValueChanged, (event: any, info: any) => { - console.log('Value changed:', info); - setHasChanges(true); - }); - - // 복사 붙여넣기 이벤트 추가 - activeSheet.bind(GC.Spread.Sheets.Events.ClipboardPasted, (event: any, info: any) => { - console.log('Clipboard pasted:', info); - setHasChanges(true); - }); - - // 편집 시작 시 읽기 전용 셀 확인 - activeSheet.bind(GC.Spread.Sheets.Events.EditStarting, (event: any, info: any) => { - const mapping = mappings.find(m => { - const cellPos = parseCellAddress(m.cellAddress); - return cellPos && cellPos.row <= info.row && cellPos.col === info.col; - }); - - if (mapping) { - // columnsJSON에서 해당 필드의 shi 확인 - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - if (columnConfig?.shi === true) { - toast.warning(`${mapping.attId} field is read-only (Column configuration)`); - info.cancel = true; - return; - } - - // SPREAD_LIST인 경우 해당 행의 데이터에서 shi 확인 - if (templateType === 'SPREAD_LIST') { - const dataRowIndex = info.row - parseCellAddress(mapping.cellAddress)!.row; - const rowData = tableData[dataRowIndex]; - if (rowData && rowData.shi === true) { - toast.warning(`Row ${dataRowIndex + 1}: ${mapping.attId} field is read-only (SHI mode)`); - info.cancel = true; - return; - } - } - - if (!mapping.isEditable) { - toast.warning(`${mapping.attId} field is read-only`); - info.cancel = true; - } - } - }); - // 편집 종료 시 데이터 검증 - activeSheet.bind(GC.Spread.Sheets.Events.EditEnded, (event: any, info: any) => { - const mapping = mappings.find(m => { - const cellPos = parseCellAddress(m.cellAddress); - return cellPos && cellPos.row <= info.row && cellPos.col === info.col; - }); - - if (mapping) { - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - if (columnConfig) { - const cellValue = activeSheet.getValue(info.row, info.col); - const errorMessage = validateCellValue(cellValue, columnConfig.type, columnConfig.options); - - if (errorMessage) { - toast.warning(`Invalid value in ${mapping.attId}: ${errorMessage}`); - // 스타일을 오류 상태로 변경 - const errorStyle = new GC.Spread.Sheets.Style(); - errorStyle.backColor = "#fef2f2"; - errorStyle.foreColor = "#dc2626"; - activeSheet.setStyle(info.row, info.col, errorStyle); - } else { - // 정상 스타일로 복원 - const cellEditable = isFieldEditable(mapping.attId); - const normalStyle = new GC.Spread.Sheets.Style(); - normalStyle.backColor = cellEditable ? "#f0fdf4" : "#f9fafb"; - normalStyle.foreColor = cellEditable ? "#000000" : "#6b7280"; - activeSheet.setStyle(info.row, info.col, normalStyle); - } - } - } - }); + // 💾 매핑 정보 저장 및 이벤트 설정 + setCellMappings(mappings); + setupSheetProtectionAndEvents(activeSheet, mappings); } + } finally { + // 렌더링 재개 spread.resumePaint(); } } catch (error) { - console.error('Error initializing spread:', error); + console.error('❌ Error initializing spread:', error); toast.error('Failed to load template'); - if (spread && spread.resumePaint) { + if (spread?.resumePaint) { spread.resumePaint(); } } - }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, setupListValidation, validateCellValue]); + }, [selectedTemplate, templateType, selectedRow, tableData, isFieldEditable, columnsJSON, createCellStyle, setupOptimizedListValidation, ensureRowCapacity, setupSheetProtectionAndEvents]); // 변경사항 저장 함수 const handleSaveChanges = React.useCallback(async () => { @@ -608,7 +840,7 @@ export function TemplateViewDialog({ const activeSheet = currentSpread.getActiveSheet(); if (templateType === 'SPREAD_ITEM' && selectedRow) { - // 단일 행 저장 (기존 로직) + // 단일 행 저장 const dataToSave = { ...selectedRow }; cellMappings.forEach(mapping => { @@ -649,21 +881,21 @@ export function TemplateViewDialog({ // 각 매핑에 대해 해당 행의 값 확인 cellMappings.forEach(mapping => { - // columnsJSON에서 해당 필드의 shi 확인 - const columnConfig = columnsJSON.find(col => col.key === mapping.attId); - const isColumnEditable = columnConfig?.shi !== true; - const isRowEditable = originalRow.shi !== true; - - if (mapping.isEditable && isColumnEditable && isRowEditable) { - const cellPos = parseCellAddress(mapping.cellAddress); - if (cellPos) { - const targetRow = cellPos.row + i; - const cellValue = activeSheet.getValue(targetRow, cellPos.col); - - // 값이 변경되었는지 확인 - if (cellValue !== originalRow[mapping.attId]) { - dataToSave[mapping.attId] = cellValue; - hasRowChanges = true; + if (mapping.dataRowIndex === i && mapping.isEditable) { + const columnConfig = columnsJSON.find(col => col.key === mapping.attId); + const isColumnEditable = columnConfig?.shi !== true; + const isRowEditable = originalRow.shi !== true; + + if (isColumnEditable && isRowEditable) { + const cellPos = parseCellAddress(mapping.cellAddress); + if (cellPos) { + const cellValue = activeSheet.getValue(cellPos.row, cellPos.col); + + // 값이 변경되었는지 확인 + if (cellValue !== originalRow[mapping.attId]) { + dataToSave[mapping.attId] = cellValue; + hasRowChanges = true; + } } } } @@ -715,9 +947,9 @@ export function TemplateViewDialog({ return ( <Dialog open={isOpen} onOpenChange={onClose}> - <DialogContent - className="w-[80%] max-w-none h-[80vh] flex flex-col" - style={{maxWidth:"80vw"}} + <DialogContent + className="w-[80%] max-w-none h-[80vh] flex flex-col" + style={{ maxWidth: "80vw" }} > <DialogHeader className="flex-shrink-0"> <DialogTitle>SEDP Template - {formCode}</DialogTitle> @@ -791,11 +1023,11 @@ export function TemplateViewDialog({ </div> </DialogDescription> </DialogHeader> - + {/* SpreadSheets 컴포넌트 영역 */} <div className="flex-1 overflow-hidden"> {selectedTemplate && isClient && isDataValid ? ( - <SpreadSheets + <SpreadSheets key={`${selectedTemplate.TMPL_TYPE}-${selectedTemplate.TMPL_ID}-${selectedTemplateId}`} workbookInitialized={initSpread} hostStyle={hostStyle} @@ -823,10 +1055,10 @@ export function TemplateViewDialog({ <Button variant="outline" onClick={onClose}> Close </Button> - + {hasChanges && ( - <Button - variant="default" + <Button + variant="default" onClick={handleSaveChanges} disabled={isPending || validationErrors.length > 0} > @@ -845,8 +1077,8 @@ export function TemplateViewDialog({ )} {validationErrors.length > 0 && ( - <Button - variant="outline" + <Button + variant="outline" onClick={validateAllData} className="text-red-600 border-red-300 hover:bg-red-50" > diff --git a/lib/pdftron/serverSDK/createBasicContractPdf.ts b/lib/pdftron/serverSDK/createBasicContractPdf.ts index a2e0b350..706508e6 100644 --- a/lib/pdftron/serverSDK/createBasicContractPdf.ts +++ b/lib/pdftron/serverSDK/createBasicContractPdf.ts @@ -1,4 +1,7 @@ const { PDFNet } = require("@pdftron/pdfnet-node"); +const fs = require('fs').promises; +const path = require('path'); +import { file as tmpFile } from "tmp-promise"; type CreateBasicContractPdf = ( templateBuffer: Buffer, @@ -15,99 +18,43 @@ export const createBasicContractPdf: CreateBasicContractPdf = async ( templateBuffer, templateData ) => { - const main = async () => { - await PDFNet.initialize(process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY); + const result = await PDFNet.runWithCleanup(async () => { console.log("🔄 PDFTron 기본계약서 PDF 변환 시작"); console.log("📝 템플릿 데이터:", JSON.stringify(templateData, null, 2)); - // 템플릿 데이터가 있는 경우 변수 치환 후 PDF 변환 - if (Object.keys(templateData).length > 0) { - console.log("🔄 템플릿 변수 치환 시작"); - - try { - // createReport.ts 방식처럼 템플릿 변수 치환 (UTF-8 인코딩 지원) - const options = new PDFNet.Convert.OfficeToPDFOptions(); - - // UTF-8 인코딩 명시 설정 시도 - try { - options.setCharset("UTF-8"); - console.log("✅ UTF-8 인코딩 설정 완료"); - } catch (charsetError) { - console.warn("⚠️ UTF-8 인코딩 설정 실패, 기본 설정 사용:", charsetError); - } - - // 템플릿 데이터를 UTF-8로 명시적으로 인코딩 - const templateDataJson = JSON.stringify(templateData, null, 2); - const utf8TemplateData = Buffer.from(templateDataJson, 'utf8').toString('utf8'); - console.log("📝 UTF-8 인코딩된 템플릿 데이터:", utf8TemplateData); - - const tempPath = `/tmp/temp_template_${Date.now()}.docx`; - - // 파일도 UTF-8로 저장 (바이너리 데이터는 그대로 유지) - require('fs').writeFileSync(tempPath, templateBuffer, { encoding: null }); // 바이너리로 저장 + // 임시 파일 생성 + const { path: tempDocxPath, cleanup } = await tmpFile({ + postfix: ".docx", + }); + + try { + // 템플릿 버퍼를 임시 파일로 저장 + await fs.writeFile(tempDocxPath, templateBuffer); + + let resultDoc; + + // 템플릿 데이터가 있는 경우 변수 치환, 없으면 단순 변환 + if (templateData && Object.keys(templateData).length > 0) { + console.log("🔄 템플릿 변수 치환 시작"); - // Office 템플릿 생성 및 변수 치환 - const templateDoc = await PDFNet.Convert.createOfficeTemplateWithPath( - tempPath, - options + const template = await PDFNet.Convert.createOfficeTemplateWithPath( + tempDocxPath ); - - const filledDoc = await templateDoc.fillTemplateJson(utf8TemplateData); - - // 임시 파일 삭제 - require('fs').unlinkSync(tempPath); - - console.log("✅ 템플릿 변수 치환 및 PDF 변환 완료"); - - const buffer = await filledDoc.saveMemoryBuffer( - PDFNet.SDFDoc.SaveOptions.e_linearized + resultDoc = await template.fillTemplateJson( + JSON.stringify(templateData) ); - - return { - result: true, - buffer, - }; - } catch (templateError) { - console.warn("⚠️ 템플릿 변수 치환 실패, 기본 변환 수행:", templateError); - - // 템플릿 처리 실패 시 기본 PDF 변환만 수행 (UTF-8 인코딩 적용) - const fallbackOptions = new PDFNet.Convert.OfficeToPDFOptions(); - try { - fallbackOptions.setCharset("UTF-8"); - } catch (charsetError) { - console.warn("⚠️ 폴백 UTF-8 인코딩 설정 실패:", charsetError); - } + console.log("✅ 템플릿 변수 치환 및 PDF 변환 완료"); + } else { + console.log("📄 단순 PDF 변환 수행"); - const buf = await PDFNet.Convert.office2PDFBuffer(templateBuffer, fallbackOptions); - const templateDoc = await PDFNet.PDFDoc.createFromBuffer(buf); + resultDoc = await PDFNet.Convert.office2PDF(tempDocxPath); - const buffer = await templateDoc.saveMemoryBuffer( - PDFNet.SDFDoc.SaveOptions.e_linearized - ); - - return { - result: true, - buffer, - }; + console.log("✅ 단순 PDF 변환 완료"); } - } else { - // 템플릿 데이터가 없는 경우 단순 변환 (UTF-8 인코딩 적용) - console.log("📄 단순 PDF 변환 수행 (UTF-8 인코딩)"); - - const simpleOptions = new PDFNet.Convert.OfficeToPDFOptions(); - try { - simpleOptions.setCharset("UTF-8"); - console.log("✅ 단순 변환 UTF-8 인코딩 설정 완료"); - } catch (charsetError) { - console.warn("⚠️ 단순 변환 UTF-8 인코딩 설정 실패:", charsetError); - } - - const buf = await PDFNet.Convert.office2PDFBuffer(templateBuffer, simpleOptions); - const templateDoc = await PDFNet.PDFDoc.createFromBuffer(buf); - - const buffer = await templateDoc.saveMemoryBuffer( + + const buffer = await resultDoc.saveMemoryBuffer( PDFNet.SDFDoc.SaveOptions.e_linearized ); @@ -115,23 +62,13 @@ export const createBasicContractPdf: CreateBasicContractPdf = async ( result: true, buffer, }; + + } finally { + // 임시 파일 정리 + await cleanup(); } - }; - - const result = await PDFNet.runWithCleanup( - main, + }, process.env.NEXT_PUBLIC_PDFTRON_SERVER_KEY - ) - .catch((err: any) => { - console.error("❌ PDFTron 기본계약서 PDF 변환 오류:", err); - return { - result: false, - error: err, - }; - }) - .then(async (data: any) => { - return data; - }); - + ); return result; }; diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx index 1df2d72c..226a053f 100644 --- a/lib/vendors/table/request-pq-dialog.tsx +++ b/lib/vendors/table/request-pq-dialog.tsx @@ -43,6 +43,7 @@ import { useSession } from "next-auth/react" import { DatePicker } from "@/components/ui/date-picker"
import { getALLBasicContractTemplates } from "@/lib/basic-contract/service"
import type { BasicContractTemplate } from "@/db/schema"
+// import { PQContractViewer } from "../pq-contract-viewer" // 더 이상 사용하지 않음
interface RequestPQDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> {
vendors: Row<Vendor>["original"][]
@@ -78,6 +79,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro const [selectedTemplateIds, setSelectedTemplateIds] = React.useState<number[]>([])
const [isLoadingTemplates, setIsLoadingTemplates] = React.useState(false)
+
React.useEffect(() => {
if (type === "PROJECT") {
setIsLoadingProjects(true)
@@ -104,6 +106,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro setPqItems("")
setExtraNote("")
setSelectedTemplateIds([])
+
}
}, [props.open])
@@ -116,7 +119,7 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro startApproveTransition(async () => {
try {
// 1단계: PQ 생성
- console.log("🚀 1단계: PQ 생성 시작")
+ console.log("🚀 PQ 생성 시작")
const { error: pqError } = await requestPQVendors({
ids: vendors.map((v) => v.id),
userId: Number(session.user.id),
@@ -133,128 +136,156 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro toast.error(`PQ 생성 실패: ${pqError}`)
return
}
- console.log("✅ 1단계: PQ 생성 완료")
+ console.log("✅ PQ 생성 완료")
+ toast.success("PQ가 성공적으로 요청되었습니다")
- // 2단계 & 3단계: 기본계약서 템플릿이 선택된 경우에만 실행 (여러 템플릿 처리)
+ // 2단계: 기본계약서 템플릿이 선택된 경우 백그라운드에서 처리
if (selectedTemplateIds.length > 0) {
- console.log(`🚀 2단계 & 3단계: ${selectedTemplateIds.length}개 템플릿 처리 시작`)
+ const templates = basicContractTemplates.filter(t =>
+ selectedTemplateIds.includes(t.id)
+ )
- let successCount = 0
- let errorCount = 0
- const errors: string[] = []
-
- // 템플릿별로 반복 처리
- for (let i = 0; i < selectedTemplateIds.length; i++) {
- const templateId = selectedTemplateIds[i]
- const selectedTemplate = basicContractTemplates.find(t => t.id === templateId)
-
- if (!selectedTemplate) {
- console.error(`템플릿 ID ${templateId}를 찾을 수 없습니다`)
- errorCount++
- errors.push(`템플릿 ID ${templateId}를 찾을 수 없습니다`)
- continue
- }
-
- try {
- console.log(`📄 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 2단계: DOCX to PDF 변환 시작`)
-
- // 템플릿 파일을 가져와서 PDF로 변환
- const formData = new FormData()
-
- // 템플릿 파일 가져오기 (서버에서 파일 읽기)
- const templateResponse = await fetch('/api/basic-contract/get-template', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ templateId })
- })
-
- if (!templateResponse.ok) {
- throw new Error(`템플릿 파일을 가져올 수 없습니다: ${selectedTemplate.templateName}`)
- }
- console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 템플릿 파일 가져오기 완료`)
-
- const templateBlob = await templateResponse.blob()
- const templateFile = new File([templateBlob], selectedTemplate.fileName || 'template.docx')
-
- // 템플릿 데이터 생성 (첫 번째 협력업체 정보 기반)
- const firstVendor = vendors[0]
- const templateData = {
- // 영문 변수명으로 변경 (PDFTron이 한글 변수명을 지원하지 않음)
- vendor_name: firstVendor?.vendorName || '협력업체명',
- address: firstVendor?.address || '주소',
- representative_name: firstVendor?.representativeName || '대표자명',
- today_date: new Date().toLocaleDateString('ko-KR'),
- }
-
- console.log(`📝 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 생성된 템플릿 데이터:`, templateData)
-
- formData.append('templateFile', templateFile)
- formData.append('outputFileName', `${selectedTemplate.templateName}_converted.pdf`)
- formData.append('templateData', JSON.stringify(templateData))
-
- // PDF 변환 호출
- const pdfResponse = await fetch('/api/pdftron/createBasicContractPdf', {
- method: 'POST',
- body: formData,
- })
- console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - PDF 변환 호출 완료`)
-
- if (!pdfResponse.ok) {
- const errorText = await pdfResponse.text()
- throw new Error(`PDF 변환 실패 (${selectedTemplate.templateName}): ${errorText}`)
- }
-
- const pdfBuffer = await pdfResponse.arrayBuffer()
- console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - PDF 변환 완료`)
-
- // 3단계: 변환된 PDF로 기본계약 생성
- console.log(`📋 [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 3단계: 기본계약 생성 시작`)
- const { error: contractError } = await requestBasicContractInfo({
- vendorIds: vendors.map((v) => v.id),
- requestedBy: Number(session.user.id),
- templateId,
- pdfBuffer: new Uint8Array(pdfBuffer), // ArrayBuffer를 Uint8Array로 변환하여 전달
- })
-
- if (contractError) {
- console.error(`기본계약 생성 오류 (${selectedTemplate.templateName}):`, contractError)
- errorCount++
- errors.push(`${selectedTemplate.templateName}: ${contractError}`)
- } else {
- console.log(`✅ [${i+1}/${selectedTemplateIds.length}] ${selectedTemplate.templateName} - 3단계: 기본계약 생성 완료`)
- successCount++
- }
- } catch (templateError) {
- console.error(`템플릿 처리 오류 (${selectedTemplate.templateName}):`, templateError)
- errorCount++
- errors.push(`${selectedTemplate.templateName}: ${templateError instanceof Error ? templateError.message : '알 수 없는 오류'}`)
- }
- }
-
- // 결과 토스트 메시지
- if (successCount > 0 && errorCount === 0) {
- toast.success(`PQ 요청 및 ${successCount}개 기본계약서 생성이 모두 완료되었습니다!`)
- } else if (successCount > 0 && errorCount > 0) {
- toast.success(`PQ는 성공적으로 요청되었습니다. ${successCount}개 기본계약서 성공, ${errorCount}개 실패`)
- console.error('기본계약서 생성 오류들:', errors)
- } else if (errorCount > 0) {
- toast.error(`PQ는 성공적으로 요청되었지만, 모든 기본계약서 생성이 실패했습니다`)
- console.error('기본계약서 생성 오류들:', errors)
- }
- } else {
- // 기본계약서 템플릿이 선택되지 않은 경우
- toast.success("PQ가 성공적으로 요청되었습니다")
+ console.log("📋 기본계약서 백그라운드 처리 시작", templates.length, "개 템플릿")
+ await processBasicContractsInBackground(templates, vendors)
}
-
+
+ // 완료 후 다이얼로그 닫기
props.onOpenChange?.(false)
onSuccess?.()
+
} catch (error) {
- console.error('전체 프로세스 오류:', error)
+ console.error('PQ 생성 오류:', error)
toast.error(`처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`)
}
})
}
+ // 백그라운드에서 기본계약서 처리
+ const processBasicContractsInBackground = async (templates: BasicContractTemplate[], vendors: any[]) => {
+ if (!session?.user?.id) {
+ toast.error("인증 정보가 없습니다")
+ return
+ }
+
+ try {
+ const totalContracts = templates.length * vendors.length
+ let processedCount = 0
+
+ // 각 벤더별로, 각 템플릿을 처리
+ for (let vendorIndex = 0; vendorIndex < vendors.length; vendorIndex++) {
+ const vendor = vendors[vendorIndex]
+
+ // 벤더별 템플릿 데이터 생성
+ const templateData = {
+ vendor_name: vendor.vendorName || '협력업체명',
+ address: vendor.address || '주소',
+ representative_name: vendor.representativeName || '대표자명',
+ today_date: new Date().toLocaleDateString('ko-KR'),
+ }
+
+ console.log(`🔄 벤더 ${vendorIndex + 1}/${vendors.length} 템플릿 데이터:`, templateData)
+
+ // 해당 벤더에 대해 각 템플릿을 순차적으로 처리
+ for (let templateIndex = 0; templateIndex < templates.length; templateIndex++) {
+ const template = templates[templateIndex]
+ processedCount++
+
+ console.log(`📄 처리 중: ${vendor.vendorName} - ${template.templateName} (${processedCount}/${totalContracts})`)
+
+ // 개별 벤더에 대한 기본계약 생성
+ await processTemplate(template, templateData, [vendor])
+
+ console.log(`✅ 완료: ${vendor.vendorName} - ${template.templateName}`)
+ }
+ }
+
+ toast.success(`총 ${totalContracts}개 기본계약이 모두 생성되었습니다`)
+
+ } catch (error) {
+ console.error('기본계약 처리 중 오류:', error)
+ toast.error(`기본계약 처리 중 오류가 발생했습니다: ${error instanceof Error ? error.message : '알 수 없는 오류'}`)
+ }
+ }
+
+ const processTemplate = async (template: BasicContractTemplate, templateData: any, vendors: any[]) => {
+ try {
+ // 1. 템플릿 파일 가져오기
+ const templateResponse = await fetch('/api/basic-contract/get-template', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ templateId: template.id })
+ })
+
+ if (!templateResponse.ok) {
+ throw new Error(`템플릿 파일을 가져올 수 없습니다: ${template.templateName}`)
+ }
+
+ const templateBlob = await templateResponse.blob()
+
+ // 2. PDFTron을 사용해서 변수 치환 및 PDF 변환
+ // @ts-ignore
+ const WebViewer = await import("@pdftron/webviewer").then(({ default: WebViewer }) => WebViewer)
+
+ // 임시 WebViewer 인스턴스 생성 (DOM에 추가하지 않음)
+ const tempDiv = document.createElement('div')
+ tempDiv.style.display = 'none'
+ document.body.appendChild(tempDiv)
+
+ const instance = await WebViewer(
+ {
+ path: "/pdftronWeb",
+ licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
+ fullAPI: true,
+ },
+ tempDiv
+ )
+
+ try {
+ const { Core } = instance
+ const { createDocument } = Core
+
+ // 3. 템플릿 문서 생성 및 변수 치환
+ const templateDoc = await createDocument(templateBlob, {
+ filename: template.fileName || 'template.docx',
+ extension: 'docx',
+ })
+
+ console.log("🔄 변수 치환 시작:", templateData)
+ await templateDoc.applyTemplateValues(templateData)
+ console.log("✅ 변수 치환 완료")
+
+ // 4. PDF 변환
+ const fileData = await templateDoc.getFileData()
+ const pdfBuffer = await Core.officeToPDFBuffer(fileData, { extension: 'docx' })
+
+ console.log(`✅ PDF 변환 완료: ${template.templateName}`, `크기: ${pdfBuffer.byteLength} bytes`)
+
+ // 5. 기본계약 생성 요청
+ const { error: contractError } = await requestBasicContractInfo({
+ vendorIds: vendors.map((v) => v.id),
+ requestedBy: Number(session!.user.id),
+ templateId: template.id,
+ pdfBuffer: new Uint8Array(pdfBuffer),
+ })
+
+ if (contractError) {
+ throw new Error(contractError)
+ }
+
+ console.log(`✅ 기본계약 생성 완료: ${template.templateName}`)
+
+ } finally {
+ // 임시 WebViewer 정리
+ instance.UI.dispose()
+ document.body.removeChild(tempDiv)
+ }
+
+ } catch (error) {
+ console.error(`❌ 템플릿 처리 실패: ${template.templateName}`, error)
+ throw error
+ }
+ }
+
const dialogContent = (
<div className="space-y-4 py-2">
{/* 선택된 협력업체 정보 */}
@@ -309,7 +340,15 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro <Label htmlFor="dueDate">PQ 제출 마감일</Label>
<DatePicker
date={dueDate ? new Date(dueDate) : undefined}
- onSelect={(date?: Date) => setDueDate(date ? date.toISOString().slice(0, 10) : "")}
+ onSelect={(date?: Date) => {
+ if (date) {
+ // 한국 시간대로 날짜 변환 (UTC 변환으로 인한 날짜 변경 방지)
+ const kstDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000)
+ setDueDate(kstDate.toISOString().slice(0, 10))
+ } else {
+ setDueDate("")
+ }
+ }}
placeholder="마감일 선택"
/>
</div>
@@ -421,6 +460,8 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro </Button>
</DialogFooter>
</DialogContent>
+
+
</Dialog>
)
}
@@ -452,6 +493,8 @@ export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...pro </Button>
</DrawerFooter>
</DrawerContent>
+
+
</Drawer>
)
}
diff --git a/pages/api/pdftron/createBasicContractPdf.ts b/pages/api/pdftron/createBasicContractPdf.ts index 1122c022..376d8540 100644 --- a/pages/api/pdftron/createBasicContractPdf.ts +++ b/pages/api/pdftron/createBasicContractPdf.ts @@ -294,13 +294,15 @@ export default async function handler( // 4. 원본 파일 읽기 const originalBuffer = await fs.readFile(templateFile.filepath); - + // const publicDir = path.join(process.cwd(), "public", "basicContract"); + // const testBuffer = await fs.readFile(path.join(publicDir, "test123.docx")); + // console.log(testBuffer); // 5. DRM 복호화 처리 (보안 검증 포함) - console.log(`🔐 [${requestId}] DRM 복호화 시작: ${templateFile.originalFilename || 'unknown'}`); - const decryptedBuffer = await decryptBufferWithDRM( - originalBuffer, - templateFile.originalFilename || 'template.docx' - ); + // console.log(`🔐 [${requestId}] DRM 복호화 시작: ${templateFile.originalFilename || 'unknown'}`); + // const decryptedBuffer = await decryptBufferWithDRM( + // originalBuffer, + // templateFile.originalFilename || 'template.docx' + // ); // 6. 복호화된 버퍼로 기본계약서 PDF 생성 console.log(`📄 [${requestId}] 기본계약서 PDF 생성 시작`); @@ -308,7 +310,7 @@ export default async function handler( result, buffer: pdfBuffer, error, - } = await createBasicContractPdf(decryptedBuffer, templateData); + } = await createBasicContractPdf(originalBuffer, templateData); if (result && pdfBuffer) { console.log(`✅ [${requestId}] 기본계약서 PDF 생성 성공: ${outputFileName}`); |
