/** * PM ver4.0 - Local PDF Conversion Worker * * 이 스크립트는 로컬 개발 환경에서 PDF 변환기(BullMQ Worker) 역할을 수행합니다. * 백엔드 서버가 'convert-pdf' 큐에 등록한 작업을 가져와, programs/ 하위의 .exe 변환기를 실행하고 * 변환된 PDF 파일을 MinIO 스토리지에 업로드한 후 DB를 업데이트합니다. * * [실행 전 요구사항] * 1. .NET 8 런타임 설치 필요 (https://dotnet.microsoft.com/ko-kr/download/dotnet/8.0) * 2. 한글 파일(.hwp/.hwpx) 변환을 테스트하려면 한컴오피스 한글이 로컬에 설치되어 있어야 합니다. * * [실행 방법] * node local_pdf_worker.js */ const { Worker } = require('bullmq'); const Redis = require('ioredis'); const fs = require('fs'); const path = require('path'); const { execFile } = require('child_process'); const axios = require('axios'); const { PutObjectCommand } = require('@aws-sdk/client-s3'); // 로컬 환경설정 로드 require('dotenv').config(); const pool = require('./db/pool.js'); const onPremiseClient = require('./config/onPremiseClient.js'); const cloudClient = require('./config/cloudClient.js'); console.log('============================================='); console.log(' PM ver4.0 로컬 PDF 변환 워커 시작 중... '); console.log('============================================='); console.log('Redis Host:', process.env.REDIS_HOST || 'localhost'); console.log('Redis Port:', process.env.REDIS_PORT || 6379); console.log('Database Host:', process.env.ONPREMISE_POSTGRES_HOST || 'localhost'); console.log('============================================='); const connection = new Redis({ host: process.env.REDIS_HOST || 'localhost', port: +(process.env.REDIS_PORT || 6379), maxRetriesPerRequest: null, password: process.env.REDIS_PASSWORD || undefined }); // 임시 디렉토리 생성 const tempDir = path.join(__dirname, 'temp_convert'); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir); } const worker = new Worker('convert-pdf', async (job) => { const { resourcePath, url, objectKey, bucket, storageType, dataId, projectId, userInfoString, userIp, initiator, type } = job.data; console.log(`\n[Job ${job.id}] 📥 PDF 변환 작업 감지: ${resourcePath} (ID: ${dataId}, Type: ${type})`); // 1. MinIO/S3 클라이언트 결정 const s3 = storageType === 'Cloud' ? cloudClient : onPremiseClient; // 2. Presigned URL을 사용하여 원본 파일 다운로드 console.log(`[Job ${job.id}] 🌐 원본 파일 다운로드 중...`); const response = await axios.get(url, { responseType: 'arraybuffer' }); const buffer = Buffer.from(response.data); // 3. 로컬 임시 폴더에 원본 파일 저장 const origFileName = path.basename(objectKey).split('__')[0]; const ext = path.extname(origFileName).toLowerCase().replace('.', ''); const tempInputPath = path.join(tempDir, `input_${job.id}_${Date.now()}.${ext}`); fs.writeFileSync(tempInputPath, buffer); console.log(`[Job ${job.id}] 💾 원본 임시 저장 완료: ${tempInputPath}`); // 4. 실행할 변환기 프로그램 선정 let exePath = ''; if (['hwp', 'hwpx'].includes(ext)) { exePath = path.join(__dirname, 'programs/hwpConverter/HwpToPdfConverter.exe'); } else if (['docx', 'xlsx', 'xlsm', 'xls', 'doc', 'ppt', 'pptx'].includes(ext)) { exePath = path.join(__dirname, 'programs/msofficeConverter/OfficeToPDFConverter.exe'); } else if (['dwg', 'dxf'].includes(ext)) { exePath = path.join(__dirname, 'programs/dwgToPdfConverter/DwgToPdfSwigConverter.exe'); } else { throw new Error(`지원하지 않는 파일 형식입니다: ${ext}`); } if (!fs.existsSync(exePath)) { throw new Error(`변환기 실행 파일을 찾을 수 없습니다: ${exePath}`); } const tempOutputPath = tempInputPath.replace(`.${ext}`, '.pdf'); console.log(`[Job ${job.id}] ⚙️ CLI 변환 실행: ${path.basename(exePath)}`); let previewKey = ''; let popupKey = ''; try { // 5. CLI 실행을 통해 PDF 변환 수행 const conversionResult = await new Promise((resolve) => { execFile(exePath, [tempInputPath, tempOutputPath], { encoding: 'buffer' }, (err, stdoutBuffer, stderrBuffer) => { const decoder = new TextDecoder('euc-kr'); const stdout = decoder.decode(stdoutBuffer || Buffer.alloc(0)).trim(); const stderr = decoder.decode(stderrBuffer || Buffer.alloc(0)).trim(); resolve({ err, stdout, stderr }); }); }); if (conversionResult.stdout) { console.log(`[Job ${job.id}] 📝 변환기 출력:\n${conversionResult.stdout}`); } if (conversionResult.stderr) { console.error(`[Job ${job.id}] 📝 변환기 에러 출력:\n${conversionResult.stderr}`); } if (!fs.existsSync(tempOutputPath)) { let errMsg = `PDF 변환 파일 생성에 실패했습니다. (결과 파일이 존재하지 않습니다.)`; if (conversionResult.stdout) { errMsg += `\n[변환기 출력]: ${conversionResult.stdout}`; } if (conversionResult.stderr) { errMsg += `\n[변환기 에러]: ${conversionResult.stderr}`; } if (conversionResult.err) { errMsg += `\n[실행 에러]: ${conversionResult.err.message}`; } throw new Error(errMsg); } // 6. 생성된 PDF를 MinIO 스토리지에 업로드 console.log(`[Job ${job.id}] 📤 변환된 PDF MinIO 스토리지 업로드 중...`); const pdfBuffer = fs.readFileSync(tempOutputPath); // preview_key, popup_key 네이밍 규칙 적용 previewKey = objectKey.replace('archive/origin/', 'archive/preview/').replace(/\.[^.]+$/, '.pdf'); popupKey = objectKey.replace('archive/origin/', 'archive/popup/').replace(/\.[^.]+$/, '.pdf'); if (previewKey === objectKey) { previewKey = `archive/preview/${path.basename(objectKey)}`.replace(/\.[^.]+$/, '.pdf'); } if (popupKey === objectKey) { popupKey = `archive/popup/${path.basename(objectKey)}`.replace(/\.[^.]+$/, '.pdf'); } // preview_key 업로드 await s3.send(new PutObjectCommand({ Bucket: bucket, Key: previewKey, Body: pdfBuffer, ContentType: 'application/pdf' })); // popup_key 업로드 await s3.send(new PutObjectCommand({ Bucket: bucket, Key: popupKey, Body: pdfBuffer, ContentType: 'application/pdf' })); console.log(`[Job ${job.id}] 🚀 업로드 성공: \n - Preview Key: ${previewKey}\n - Popup Key: ${popupKey}`); // 7. 데이터베이스 레코드 정보 업데이트 (preview_key, popup_key 반영) console.log(`[Job ${job.id}] 🗄️ 데이터베이스 정보 갱신 중...`); const envSetting = process.env.NODE_ENV || 'development'; const tbData = envSetting === 'production' ? 'tb_data' : '_test_tb_data'; if (type === 'archive') { const updateQuery = ` UPDATE ver4.${tbData} SET preview_key = $1, popup_key = $2, mod_date = NOW(), mod_activity = 'convertPdf' WHERE data_id = $3 `; await pool.query(updateQuery, [previewKey, popupKey, dataId]); } else if (type === 'officialDoc') { const updateQuery = ` UPDATE ver4.tb_official_doc_file SET preview_key = $1, popup_key = $2, mod_date = NOW(), mod_activity = 'convertDocPdf' WHERE doc_id = $3 `; await pool.query(updateQuery, [previewKey, popupKey, dataId]); } console.log(`[Job ${job.id}] ✅ 데이터베이스 업데이트 완료.`); } finally { // 8. 로컬 임시 파일 삭제 try { if (fs.existsSync(tempInputPath)) { fs.unlinkSync(tempInputPath); } if (fs.existsSync(tempOutputPath)) { fs.unlinkSync(tempOutputPath); } console.log(`[Job ${job.id}] 🧹 임시 파일 정리 완료.`); } catch (cleanErr) { console.warn(`[Job ${job.id}] ⚠️ 임시 파일 제거 실패:`, cleanErr.message); } } // 완료 후 completed 이벤트 리스너로 데이터 리턴 return { projectId, userInfoString, userIp, resourcePath, dataId, storageType, previewKey, popupKey }; }, { connection, concurrency: 1 // 한 번에 하나의 변환 프로세스만 처리 }); worker.on('ready', () => { console.log('🚀 PDF 변환 워커가 준비되었습니다. 작업을 대기 중입니다...'); }); worker.on('active', (job) => { console.log(`▶️ Job ${job.id} 시작`); }); worker.on('completed', (job, result) => { console.log(`✔️ Job ${job.id} 완료`); }); worker.on('failed', (job, err) => { console.error(`❌ Job ${job.id} 실패:`, err.message); });