Files
PM_test/local_pdf_worker.js
2026-06-15 16:10:05 +09:00

200 lines
7.8 KiB
JavaScript

/**
* 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)}`);
// 5. CLI 실행을 통해 PDF 변환 수행
await new Promise((resolve, reject) => {
execFile(exePath, [tempInputPath, tempOutputPath], (err, stdout, stderr) => {
if (err) {
console.error(`[Job ${job.id}] ❌ 변환 CLI 실행 에러:`, err.message);
return reject(err);
}
console.log(`[Job ${job.id}] 📝 변환기 출력:`, stdout.trim());
resolve();
});
});
if (!fs.existsSync(tempOutputPath)) {
throw new Error(`변환 성공했으나 결과 PDF 파일이 존재하지 않습니다: ${tempOutputPath}`);
}
// 6. 생성된 PDF를 MinIO 스토리지에 업로드
console.log(`[Job ${job.id}] 📤 변환된 PDF MinIO 스토리지 업로드 중...`);
const pdfBuffer = fs.readFileSync(tempOutputPath);
// preview_key, popup_key 네이밍 규칙 적용
let previewKey = objectKey.replace('archive/origin/', 'archive/preview/').replace(/\.[^.]+$/, '.pdf');
let 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}] ✅ 데이터베이스 업데이트 완료.`);
// 8. 로컬 임시 파일 삭제
try {
fs.unlinkSync(tempInputPath);
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);
});