diff --git a/controllers/archiveController.js b/controllers/archiveController.js index fea4796..655a52d 100644 --- a/controllers/archiveController.js +++ b/controllers/archiveController.js @@ -3576,15 +3576,22 @@ exports.mgmtFunc_resetConvert = async(req, res) => { let { params } = req.body; let { resourcePath, dataId } = params; - // convertingDataArr.map(async data => { - // if (data.dataId == dataId) { - // let job = await convertPdfQueue.getJob(data.jobId); - // if (job) await redisConnection.set(`cancel:job:${data.jobId}`, '1'); - // } - // }) + for (const data of convertingDataArr) { + if (Number(data.dataId) === Number(dataId)) { + try { + const job = await convertPdfQueue.getJob(data.jobId); + if (job) { + await job.remove(); + console.log(`[Cancel] Successfully removed job ${data.jobId} from convert-pdf queue`); + } + } catch (jobErr) { + console.error('[Cancel] Error removing job:', jobErr); + } + } + } //// 배열에서 파일 정보 삭제 - convertingDataArr = convertingDataArr.filter(data => data.dataId !== dataId); + convertingDataArr = convertingDataArr.filter(data => Number(data.dataId) !== Number(dataId)); const waiting = await convertPdfQueue.getWaitingCount(); const active = await convertPdfQueue.getActiveCount(); diff --git a/libs/hwp.js b/libs/hwp.js new file mode 100644 index 0000000..76f981c Binary files /dev/null and b/libs/hwp.js differ diff --git a/local_clear_queue.js b/local_clear_queue.js new file mode 100644 index 0000000..13b13da --- /dev/null +++ b/local_clear_queue.js @@ -0,0 +1,52 @@ +/** + * PM ver4.0 - Redis convert-pdf 큐 전체 초기화 스크립트 + * + * 이 스크립트는 Redis에 쌓여있는 'convert-pdf' 대기열의 모든 작업(대기 중, 실패 등)을 제거합니다. + * + * [실행 방법] + * node local_clear_queue.js + */ + +const { Queue } = require('bullmq'); +const Redis = require('ioredis'); +require('dotenv').config(); + +console.log('Redis Host:', process.env.REDIS_HOST || 'localhost'); +console.log('Redis Port:', process.env.REDIS_PORT || 6379); + +const connection = new Redis({ + host: process.env.REDIS_HOST || 'localhost', + port: +(process.env.REDIS_PORT || 6379), + maxRetriesPerRequest: null, + password: process.env.REDIS_PASSWORD || undefined +}); + +connection.on('connect', async () => { + console.log('✔️ Connected to Redis. Clearing queue...'); + try { + const queueName = 'convert-pdf'; + const queue = new Queue(queueName, { connection }); + + // 대기 중인 작업 모두 제거 (drain) + await queue.drain(); + console.log(`🧹 ${queueName} 큐의 대기 중인 모든 작업을 제거했습니다.`); + + // 완료/실패 작업도 청소 + await queue.clean(0, 0, 'completed'); + await queue.clean(0, 0, 'failed'); + console.log('🧹 완료 및 실패 내역을 청소했습니다.'); + + const counts = await queue.getJobCounts(); + console.log('\n--- 현재 큐 상태 ---'); + console.log(JSON.stringify(counts, null, 2)); + + } catch (err) { + console.error('❌ 에러 발생:', err); + } finally { + connection.disconnect(); + } +}); + +connection.on('error', (err) => { + console.error('❌ Redis Connection Error:', err); +}); diff --git a/local_pdf_worker.js b/local_pdf_worker.js new file mode 100644 index 0000000..f7fd43a --- /dev/null +++ b/local_pdf_worker.js @@ -0,0 +1,199 @@ +/** + * 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); +}); diff --git a/logs/2026-06-15.exception.log b/logs/2026-06-15.exception.log index d28800f..198cbba 100644 --- a/logs/2026-06-15.exception.log +++ b/logs/2026-06-15.exception.log @@ -83,3 +83,9 @@ Error: listen EADDRINUSE: address already in use 0.0.0.0:6565 TypeError: Cannot read properties of undefined (reading 'message') at exports.removeTarget (D:\40. 개발소스\04. PM\pm_ver4\trunk\PM_ver4\controllers\archiveController.js:2895:33) at process.processTicksAndRejections (node:internal/process/task_queues:104:5) +2026-06-15 14:51:35 [error 테스트: ] error: uncaughtException: listen EADDRINUSE: address already in use 0.0.0.0:6565 +Error: listen EADDRINUSE: address already in use 0.0.0.0:6565 + at Server.setupListenHandle [as _listen2] (node:net:2008:16) + at listenInCluster (node:net:2065:12) + at node:net:2274:7 + at process.processTicksAndRejections (node:internal/process/task_queues:90:21) diff --git a/package-lock.json b/package-lock.json index b444b2f..c603262 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "express-session": "^1.18.0", "gpt-tokenizer": "^3.0.1", "helmet": "^8.1.0", + "hwp.js": "^0.0.3", "ioredis": "^5.6.1", "jsonwebtoken": "^9.0.3", "morgan": "^1.10.0", @@ -1964,6 +1965,15 @@ "node": ">= 0.6" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -2340,6 +2350,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3509,6 +3532,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/hwp.js": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/hwp.js/-/hwp.js-0.0.3.tgz", + "integrity": "sha512-qVdQjpyfR2rVmfZbbqd/8LAtt8YbwtZ2vm9FkBJj1UGPTP0Zs9+miUDrsPSfARpqkx3L7NeI1qR6hag5DaicyQ==", + "license": "Apache-2.0", + "dependencies": { + "cfb": "^1.2.0", + "pako": "^1.0.11" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4366,6 +4399,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", diff --git a/package.json b/package.json index c55debf..cf284c8 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "express-session": "^1.18.0", "gpt-tokenizer": "^3.0.1", "helmet": "^8.1.0", + "hwp.js": "^0.0.3", "ioredis": "^5.6.1", "jsonwebtoken": "^9.0.3", "morgan": "^1.10.0", diff --git a/queue.js b/queue.js index 3143735..e1a8f3a 100644 --- a/queue.js +++ b/queue.js @@ -40,11 +40,11 @@ convertPdfQueueEvents.on('failed', async ({ jobId, failedReason }) => { let jobName = job.name; let jobQueueName = job.queue.name; let { resourcePath, projectId, userInfoString, serviceName, dataId } = job.data; - if (!resourcePath) resourcePath = job.progress.resourcePath; - if (!projectId) projectId = job.progress.projectId; - if (!userInfoString) userInfoString = job.progress.userInfoString; - if (!serviceName) serviceName = job.progress.serviceName; - if (!dataId) dataId = job.progress.dataId; + if (!resourcePath && job.progress && typeof job.progress === 'object') resourcePath = job.progress.resourcePath; + if (!projectId && job.progress && typeof job.progress === 'object') projectId = job.progress.projectId; + if (!userInfoString && job.progress && typeof job.progress === 'object') userInfoString = job.progress.userInfoString; + if (!serviceName && job.progress && typeof job.progress === 'object') serviceName = job.progress.serviceName; + if (!dataId && job.progress && typeof job.progress === 'object') dataId = job.progress.dataId; let userInfo = JSON.parse(userInfoString); let { user_id, user_nm } = userInfo; diff --git a/temp_convert/input_1_1781504351932.hwpx b/temp_convert/input_1_1781504351932.hwpx new file mode 100644 index 0000000..b1ffd61 Binary files /dev/null and b/temp_convert/input_1_1781504351932.hwpx differ diff --git a/temp_convert/input_2_1781504354837.docx b/temp_convert/input_2_1781504354837.docx new file mode 100644 index 0000000..e1ede68 Binary files /dev/null and b/temp_convert/input_2_1781504354837.docx differ diff --git a/temp_convert/input_3_1781504356321.docx b/temp_convert/input_3_1781504356321.docx new file mode 100644 index 0000000..e1ede68 Binary files /dev/null and b/temp_convert/input_3_1781504356321.docx differ diff --git a/temp_convert/input_4_1781504357435.hwpx b/temp_convert/input_4_1781504357435.hwpx new file mode 100644 index 0000000..b1ffd61 Binary files /dev/null and b/temp_convert/input_4_1781504357435.hwpx differ diff --git a/temp_convert/input_5_1781504358912.hwpx b/temp_convert/input_5_1781504358912.hwpx new file mode 100644 index 0000000..b1ffd61 Binary files /dev/null and b/temp_convert/input_5_1781504358912.hwpx differ diff --git a/temp_convert/input_6_1781504360423.docx b/temp_convert/input_6_1781504360423.docx new file mode 100644 index 0000000..e1ede68 Binary files /dev/null and b/temp_convert/input_6_1781504360423.docx differ diff --git a/temp_convert/input_7_1781505810092.docx b/temp_convert/input_7_1781505810092.docx new file mode 100644 index 0000000..e1ede68 Binary files /dev/null and b/temp_convert/input_7_1781505810092.docx differ diff --git a/views/main/jsm/archive/common.js b/views/main/jsm/archive/common.js index fa2f041..526fc4b 100644 --- a/views/main/jsm/archive/common.js +++ b/views/main/jsm/archive/common.js @@ -333,7 +333,10 @@ export async function openNewWindowViewer() { } let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams } ); if (getDataInfoRes.data.message == 'getDataInfo_success') { - let objectKey = getDataInfoRes.data.result.popup_key; + let result = getDataInfoRes.data.result; + let directViewExtArr = ['xls', 'xlsx', 'xlsm', 'docx', 'hwp', 'hwpx']; + let objectKey = directViewExtArr.includes(ext) ? result.object_key : result.popup_key; + if(objectKey == undefined || objectKey == `` || objectKey == null){ return; } @@ -365,19 +368,26 @@ export async function openNewWindowViewer() { let open_ext = `pdf`; switch(ext){ case 'pdf' : - case 'hwp' : - case 'hwpx' : - case 'xls' : - case 'xlsm' : + case 'doc' : case 'ppt' : case 'pptx' : - case 'doc' : - case 'docx' : case 'dwg' : case 'dxf' : case 'grm' : open_ext = 'pdf'; - break + break; + case 'hwp' : + case 'hwpx' : + open_ext = ext; + break; + case 'xls' : + case 'xlsx' : + case 'xlsm' : + open_ext = ext; + break; + case 'docx' : + open_ext = ext; + break; case 'gsim' : open_ext = 'gsim'; break diff --git a/views/main/jsm/archive/pageRenderer.js b/views/main/jsm/archive/pageRenderer.js index d3ff1dc..f871b4c 100644 --- a/views/main/jsm/archive/pageRenderer.js +++ b/views/main/jsm/archive/pageRenderer.js @@ -5176,7 +5176,14 @@ export async function renderViewer(resourcePath, dataId, shouldAddClickLog = tru let isLowerExt = true; let ext = splitBaseAndExt(resourcePath, isLowerExt).ext; - let previewKey = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject).data.previewKey; + let excelDirectArr = ['xls', 'xlsx', 'xlsm']; + let hwpDirectArr = ['hwp', 'hwpx']; + let wordDirectArr = ['docx']; + let isDirectView = excelDirectArr.includes(ext) || hwpDirectArr.includes(ext) || wordDirectArr.includes(ext); + + let treeData = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject)?.data || {}; + let previewKey = treeData.previewKey; + let objectKey = treeData.objectKey || treeData.object_key; // 문서 뷰어 실행 시 미리보기 10장 제한 문구 표시 let thumbAlert = document.querySelector('.archive-main-right .viewer-container .viewer-header .thumb-alert'); @@ -5186,9 +5193,15 @@ export async function renderViewer(resourcePath, dataId, shouldAddClickLog = tru thumbAlert.style.display = 'none'; } + // fallback-pdf-btn 숨김 + const mainFallbackPdfBtn = document.getElementById('main-fallback-pdf-btn'); + if (mainFallbackPdfBtn) { + mainFallbackPdfBtn.style.display = 'none'; + } + // 지원 파일인 경우 뷰어 프로그레스 표시, 대기 시간 700ms로 설정, 전체보기 버튼 표시 let originViewBtn = document.querySelector('.archive-main-right .viewer-container .viewer-header .btn'); - if (allArr.includes(ext) && previewKey) { + if (allArr.includes(ext) && (previewKey || isDirectView)) { toggleViewerProgress(true); vars.viewerConnectingTime = 700; originViewBtn.style.display = 'flex'; @@ -5214,7 +5227,7 @@ export async function renderViewer(resourcePath, dataId, shouldAddClickLog = tru let PresignedUrl = undefined; let openFileViewer = true; - if (!previewKey) { + if (!previewKey || !objectKey) { let getDataInfoParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, @@ -5225,18 +5238,24 @@ export async function renderViewer(resourcePath, dataId, shouldAddClickLog = tru let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams } ); if (getDataInfoRes.data.message == 'getDataInfo_success') { - previewKey = getDataInfoRes.data.result.preview_key; + let result = getDataInfoRes.data.result; + if (result) { + previewKey = result.preview_key; + objectKey = result.object_key; + } } } + let targetKey = isDirectView ? objectKey : previewKey; + if (allArr.includes(ext)) { - if (previewKey == undefined || previewKey == `` || previewKey == null) { + if (targetKey == undefined || targetKey == `` || targetKey == null) { viewerConvert(); openFileViewer = false; shouldAddClickLog = false; } else { let generateDownloadUrlParams = { - objectKey: previewKey, + objectKey: targetKey, resourcePath: resourcePath } let generateDownloadUrlRes = await axios.post(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams); @@ -5252,12 +5271,16 @@ export async function renderViewer(resourcePath, dataId, shouldAddClickLog = tru if (openFileViewer) { let ext = (splitBaseAndExt(resourcePath).ext).toLowerCase(); + let pdfArrFiltered = pdfArr.filter(e => !excelDirectArr.includes(e) && !hwpDirectArr.includes(e) && !wordDirectArr.includes(e)); // 3D뷰어 썸네일 변수 const thumbnail_key = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject).data?.thumbnailKey; if (allArr.includes(ext)) { - if (pdfArr.includes(ext)) viewerPdf(PresignedUrl); + if (pdfArrFiltered.includes(ext)) viewerPdf(PresignedUrl); + if (excelDirectArr.includes(ext)) viewerExcel(PresignedUrl); + if (hwpDirectArr.includes(ext)) viewerHwp(PresignedUrl); + if (wordDirectArr.includes(ext)) viewerWord(PresignedUrl); if (gsimArr.includes(ext)) viewerGsim(PresignedUrl); if (ifcArr.includes(ext)) viewerIfc(PresignedUrl, thumbnail_key, resourcePath, dataId, vars.path_name); if (threeArr.includes(ext)) viewer3d(PresignedUrl, thumbnail_key, resourcePath, dataId, vars.path_name); @@ -5369,6 +5392,250 @@ export async function renderViewer(resourcePath, dataId, shouldAddClickLog = tru vars.viewer.dataset.viewerType = 'convert'; } + // ----------------------------------------------------------------- + // 오픈소스 문서 직접 뷰잉 및 PDF 폴백 함수 정의 (Main Viewer) + // ----------------------------------------------------------------- + function initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey) { + const btn = document.getElementById('main-fallback-pdf-btn'); + if (!btn) return; + + // 이전 등록된 리스너 제거를 위해 복사 대체 + const newBtn = btn.cloneNode(true); + btn.parentNode.replaceChild(newBtn, btn); + + newBtn.style.display = 'flex'; + newBtn.querySelector('.text').textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + + newBtn.addEventListener('click', async () => { + newBtn.querySelector('.text').textContent = '로딩 중...'; + newBtn.style.pointerEvents = 'none'; + + try { + // 1. 최신 메타데이터 (preview_key) 조회 + if (!previewKey) { + let getDataInfoParams = { + userInfoString: vars.userInfoString, + storageType: vars.storageType, + dataIdArr: [dataId], + isRemoved: false, + debug: "main fallback" + } + let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams } ); + if (getDataInfoRes.data.message == 'getDataInfo_success') { + let result = getDataInfoRes.data.result; + if (result) { + previewKey = result.preview_key; + objectKey = result.object_key; + } + } + } + + // 2. 만약 PDF 변환본이 아직 없다면 백엔드 변환 요청 + if (!previewKey) { + newBtn.querySelector('.text').textContent = 'PDF 변환 요청 중...'; + await convertPdf(resourcePath, dataId); + alert('서버 측 PDF 변환이 시작되었습니다. 잠시 후 다시 클릭해 주세요.'); + newBtn.querySelector('.text').textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + return; + } + + // 3. PDF용 Presigned URL 생성 + let generateDownloadUrlParams = { + objectKey: previewKey, + resourcePath: resourcePath + } + let generateDownloadUrlRes = await axios.post(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams); + if (generateDownloadUrlRes.data.message == 'generateDownloadUrl_success') { + let pdfUrl = generateDownloadUrlRes.data.url; + + // 화면 초기화 및 PDF 뷰어 로드 + vars.viewer = viewerWrap.querySelector('.viewer'); + vars.viewer.innerHTML = ''; + newBtn.style.display = 'none'; + viewerPdf(pdfUrl); + } else { + alert('PDF 미리보기 주소 획득에 실패했습니다.'); + newBtn.querySelector('.text').textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + } + } catch (err) { + console.error(err); + alert('PDF 변환 및 조회 중 오류가 발생했습니다.'); + newBtn.querySelector('.text').textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + } + }); + } + + function viewerExcel(presignedUrl) { + vars.viewer.innerHTML = '
엑셀 데이터를 불러오는 중...
'; + + fetch(presignedUrl) + .then(res => { + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + return res.arrayBuffer(); + }) + .then(arrayBuffer => { + vars.viewer.innerHTML = ''; + + LuckyExcel.transformExcelToLucky(arrayBuffer, function(exportJson, luckysheetfile) { + if(exportJson.sheets == null || exportJson.sheets.length == 0) { + vars.viewer.innerHTML = '
엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)
'; + initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey); + return; + } + + if (window.luckysheet) { + window.luckysheet.destroy(); + } + + vars.viewer.style.position = 'relative'; + const container = document.createElement('div'); + container.id = 'luckysheet_inner'; + container.style.margin = '0px'; + container.style.padding = '0px'; + container.style.position = 'absolute'; + container.style.width = '100%'; + container.style.height = '100%'; + container.style.left = '0px'; + container.style.top = '0px'; + vars.viewer.appendChild(container); + + try { + window.luckysheet.create({ + container: 'luckysheet_inner', + data: exportJson.sheets, + title: exportJson.info.name || 'Excel Viewer', + lang: 'en', + showinfobar: false, + myFolderUrl: 'javascript:void(0)' + }); + } catch (createErr) { + console.error("Luckysheet create error: ", createErr); + vars.viewer.innerHTML = `
+
엑셀 시트 생성 중 오류가 발생했습니다.
+
에러: ${createErr.message}
+
`; + } + }, function(err) { + console.error("Luckysheet transform error: ", err); + vars.viewer.innerHTML = `
+
엑셀 파일을 읽는 중 오류가 발생했습니다.
+
상세: ${err.message || err}
+
`; + initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey); + }); + }) + .catch(err => { + console.error(err); + vars.viewer.innerHTML = `
+
엑셀 파일을 불러오는데 실패했습니다.
+
에러: ${err.message}
+
`; + initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey); + }); + + vars.viewer.dataset.viewerType = 'excel'; + } + + function viewerWord(presignedUrl) { + vars.viewer.innerHTML = '
워드 문서를 불러오는 중...
'; + initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey); + + fetch(presignedUrl) + .then(res => { + if (!res.ok) throw new Error('Word fetch failed'); + return res.arrayBuffer(); + }) + .then(arrayBuffer => { + vars.viewer.innerHTML = ''; + + const container = document.createElement('div'); + container.style.width = '100%'; + container.style.height = '100%'; + container.style.overflow = 'auto'; + container.style.padding = '20px'; + container.style.boxSizing = 'border-box'; + container.style.background = '#f5f5f5'; + + const docxInner = document.createElement('div'); + docxInner.style.background = '#ffffff'; + docxInner.style.margin = '0 auto'; + docxInner.style.maxWidth = '800px'; + docxInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)'; + docxInner.style.padding = '40px'; + + container.appendChild(docxInner); + vars.viewer.appendChild(container); + + docx.renderAsync(arrayBuffer, docxInner) + .then(() => console.log("docx rendered")) + .catch(err => { + console.error(err); + docxInner.innerHTML = '
워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; + }); + }) + .catch(err => { + console.error(err); + vars.viewer.innerHTML = '
워드 문서를 불러오는데 실패했습니다.
'; + }); + + vars.viewer.dataset.viewerType = 'word'; + } + + function viewerHwp(presignedUrl) { + vars.viewer.innerHTML = '
한글 문서를 불러오는 중...
'; + initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey); + + fetch(presignedUrl) + .then(res => { + if (!res.ok) throw new Error('HWP fetch failed'); + return res.blob(); + }) + .then(blob => { + vars.viewer.innerHTML = ''; + + const container = document.createElement('div'); + container.style.width = '100%'; + container.style.height = '100%'; + container.style.overflow = 'auto'; + container.style.padding = '20px'; + container.style.boxSizing = 'border-box'; + container.style.background = '#f5f5f5'; + + const hwpInner = document.createElement('div'); + hwpInner.style.background = '#ffffff'; + hwpInner.style.margin = '0 auto'; + hwpInner.style.maxWidth = '800px'; + hwpInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)'; + hwpInner.style.padding = '40px'; + hwpInner.style.minHeight = '100%'; + + container.appendChild(hwpInner); + vars.viewer.appendChild(container); + + const reader = new FileReader(); + reader.onload = (e) => { + const bstr = e.target.result; + try { + new hwp.Viewer(hwpInner, bstr); + } catch (err) { + console.error("hwp.js error: ", err); + hwpInner.innerHTML = '
한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; + } + }; + reader.readAsBinaryString(blob); + }) + .catch(err => { + console.error(err); + vars.viewer.innerHTML = '
한글 문서를 불러오는데 실패했습니다.
'; + }); + + vars.viewer.dataset.viewerType = 'hwp'; + } + //// 원본 viewer async function viewerPdf(presignedUrl) { if(presignedUrl == undefined || presignedUrl == ``){ @@ -5830,6 +6097,16 @@ export function resetViewer() { } } + if (vars.viewer.dataset.viewerType == 'excel') { + if (window.luckysheet) { + try { + window.luckysheet.destroy(); + } catch (e) { + console.error("Luckysheet destroy error: ", e); + } + } + } + vars.viewer.dataset.viewerType = ''; } diff --git a/views/main/jsm/officialDoc/docPageRenderer.js b/views/main/jsm/officialDoc/docPageRenderer.js index 56e9bff..ebbd182 100644 --- a/views/main/jsm/officialDoc/docPageRenderer.js +++ b/views/main/jsm/officialDoc/docPageRenderer.js @@ -562,12 +562,29 @@ export async function renderDocViewer(resourcePath, docId) { await viewerMetadata(docVars.allDocData?.find((doc) => doc.doc_id === docId)); } + // fallback-pdf-btn 숨김 + const docFallbackPdfBtn = document.getElementById('doc-fallback-pdf-btn'); + if (docFallbackPdfBtn) { + docFallbackPdfBtn.style.display = 'none'; + } + + let ext = splitBaseAndExt(resourcePath).ext.toLowerCase(); + + let excelDirectArr = ['xls', 'xlsx', 'xlsm']; + let hwpDirectArr = ['hwp', 'hwpx']; + let wordDirectArr = ['docx']; + let isDirectView = excelDirectArr.includes(ext) || hwpDirectArr.includes(ext) || wordDirectArr.includes(ext); + + let selectedDoc = docVars.allDocData?.find((doc) => doc.doc_id === docId); + let previewKey = selectedDoc?.preview_key; + let objectKey = selectedDoc?.object_key; + + let targetKey = isDirectView ? objectKey : previewKey; + //Presigned URL let PresignedUrl = undefined; - let objectKey = docVars.allDocData?.find((doc) => doc.doc_id === docId)?.preview_key; - if (objectKey == undefined || objectKey == `` || objectKey == null) { - let ext = splitBaseAndExt(resourcePath).ext.toLowerCase(); + if (targetKey == undefined || targetKey == `` || targetKey == null) { let supportArr = ['hwp', 'hwpx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'doc', 'docx', 'dwg', 'dxf']; if (!supportArr.includes(ext)) { @@ -580,7 +597,7 @@ export async function renderDocViewer(resourcePath, docId) { } let generateDownloadUrlParams = { - objectKey: objectKey, + objectKey: targetKey, resourcePath: resourcePath, }; let generateDownloadUrlRes = await axios.post(`${docVars.path_name}/generateDownloadDocUrl`, generateDownloadUrlParams); @@ -589,7 +606,6 @@ export async function renderDocViewer(resourcePath, docId) { } //Presigned URL end - let ext = splitBaseAndExt(resourcePath).ext.toLowerCase(); let pdfArr = ['pdf', 'hwp', 'hwpx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'doc', 'docx', 'dwg', 'dxf']; let gsimArr = ['gsim']; let ifcArr = ['ifc']; @@ -601,7 +617,12 @@ export async function renderDocViewer(resourcePath, docId) { let threeArr = ['glb', 'gltf', 'obj', 'stl', 'fbx', '3dm']; let allArr = [...pdfArr, ...gsimArr, ...ifcArr, ...imageArr, ...videoArr, ...textArr, ...urlArr, ...zipArr, ...threeArr]; if (allArr.includes(ext)) { - if (pdfArr.includes(ext)) viewerPdf(PresignedUrl); + let pdfArrFiltered = pdfArr.filter(e => !excelDirectArr.includes(e) && !hwpDirectArr.includes(e) && !wordDirectArr.includes(e)); + + if (pdfArrFiltered.includes(ext)) viewerPdf(PresignedUrl); + if (excelDirectArr.includes(ext)) viewerExcel(PresignedUrl); + if (hwpDirectArr.includes(ext)) viewerHwp(PresignedUrl); + if (wordDirectArr.includes(ext)) viewerWord(PresignedUrl); if (gsimArr.includes(ext)) viewerGsim(PresignedUrl); if (ifcArr.includes(ext)) viewerIfc(PresignedUrl); if (threeArr.includes(ext)) viewer3d(PresignedUrl); @@ -642,6 +663,239 @@ export async function renderDocViewer(resourcePath, docId) { docVars.viewer.dataset.viewerType = 'convert'; } + // ----------------------------------------------------------------- + // 오픈소스 문서 직접 뷰잉 및 PDF 폴백 함수 정의 (Doc Viewer) + // ----------------------------------------------------------------- + function initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey) { + const btn = document.getElementById('doc-fallback-pdf-btn'); + if (!btn) return; + + // 이전 등록된 리스너 제거를 위해 복사 대체 + const newBtn = btn.cloneNode(true); + btn.parentNode.replaceChild(newBtn, btn); + + newBtn.style.display = 'flex'; + newBtn.querySelector('.text').textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + + newBtn.addEventListener('click', async () => { + newBtn.querySelector('.text').textContent = '로딩 중...'; + newBtn.style.pointerEvents = 'none'; + + try { + // 1. 최신 메타데이터 (preview_key) 조회 + if (!previewKey) { + await syncDocInfo(['official', 'attach', null]); + let selectedDoc = docVars.allDocData?.find((doc) => doc.doc_id === docId); + previewKey = selectedDoc?.preview_key; + objectKey = selectedDoc?.object_key; + } + + // 2. 만약 PDF 변환본이 아직 없다면 백엔드 변환 요청 + if (!previewKey) { + newBtn.querySelector('.text').textContent = 'PDF 변환 요청 중...'; + await convertDocPdf(resourcePath, docId); + alert('서버 측 PDF 변환이 시작되었습니다. 잠시 후 다시 클릭해 주세요.'); + newBtn.querySelector('.text').textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + return; + } + + // 3. PDF용 Presigned URL 생성 + let generateDownloadUrlParams = { + objectKey: previewKey, + resourcePath: resourcePath + } + let generateDownloadUrlRes = await axios.post(`${docVars.path_name}/generateDownloadDocUrl`, generateDownloadUrlParams); + if (generateDownloadUrlRes.data.message == 'generateDownloadDocUrl_success') { + let pdfUrl = generateDownloadUrlRes.data.url; + + // 화면 초기화 및 PDF 뷰어 로드 + docVars.viewer = viewerWrap.querySelector('.viewer'); + docVars.viewer.innerHTML = ''; + newBtn.style.display = 'none'; + viewerPdf(pdfUrl); + } else { + alert('PDF 미리보기 주소 획득에 실패했습니다.'); + newBtn.querySelector('.text').textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + } + } catch (err) { + console.error(err); + alert('PDF 변환 및 조회 중 오류가 발생했습니다.'); + newBtn.querySelector('.text').textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + } + }); + } + + function viewerExcel(presignedUrl) { + docVars.viewer.innerHTML = '
엑셀 데이터를 불러오는 중...
'; + + fetch(presignedUrl) + .then(res => { + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + return res.arrayBuffer(); + }) + .then(arrayBuffer => { + docVars.viewer.innerHTML = ''; + + LuckyExcel.transformExcelToLucky(arrayBuffer, function(exportJson, luckysheetfile) { + if(exportJson.sheets == null || exportJson.sheets.length == 0) { + docVars.viewer.innerHTML = '
엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)
'; + initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); + return; + } + + if (window.luckysheet) { + window.luckysheet.destroy(); + } + + docVars.viewer.style.position = 'relative'; + const container = document.createElement('div'); + container.id = 'luckysheet_inner_doc'; + container.style.margin = '0px'; + container.style.padding = '0px'; + container.style.position = 'absolute'; + container.style.width = '100%'; + container.style.height = '100%'; + container.style.left = '0px'; + container.style.top = '0px'; + docVars.viewer.appendChild(container); + + try { + window.luckysheet.create({ + container: 'luckysheet_inner_doc', + data: exportJson.sheets, + title: exportJson.info.name || 'Excel Viewer', + lang: 'en', + showinfobar: false, + myFolderUrl: 'javascript:void(0)' + }); + } catch (createErr) { + console.error("Luckysheet create error: ", createErr); + docVars.viewer.innerHTML = `
+
엑셀 시트 생성 중 오류가 발생했습니다.
+
에러: ${createErr.message}
+
`; + } + }, function(err) { + console.error("Luckysheet transform error: ", err); + docVars.viewer.innerHTML = `
+
엑셀 파일을 읽는 중 오류가 발생했습니다.
+
상세: ${err.message || err}
+
`; + initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); + }); + }) + .catch(err => { + console.error(err); + docVars.viewer.innerHTML = `
+
엑셀 파일을 불러오는데 실패했습니다.
+
에러: ${err.message}
+
`; + initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); + }); + + docVars.viewer.dataset.viewerType = 'excel'; + } + + function viewerWord(presignedUrl) { + docVars.viewer.innerHTML = '
워드 문서를 불러오는 중...
'; + initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); + + fetch(presignedUrl) + .then(res => { + if (!res.ok) throw new Error('Word fetch failed'); + return res.arrayBuffer(); + }) + .then(arrayBuffer => { + docVars.viewer.innerHTML = ''; + + const container = document.createElement('div'); + container.style.width = '100%'; + container.style.height = '100%'; + container.style.overflow = 'auto'; + container.style.padding = '20px'; + container.style.boxSizing = 'border-box'; + container.style.background = '#f5f5f5'; + + const docxInner = document.createElement('div'); + docxInner.style.background = '#ffffff'; + docxInner.style.margin = '0 auto'; + docxInner.style.maxWidth = '800px'; + docxInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)'; + docxInner.style.padding = '40px'; + + container.appendChild(docxInner); + docVars.viewer.appendChild(container); + + docx.renderAsync(arrayBuffer, docxInner) + .then(() => console.log("docx rendered")) + .catch(err => { + console.error(err); + docxInner.innerHTML = '
워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; + }); + }) + .catch(err => { + console.error(err); + docVars.viewer.innerHTML = '
워드 문서를 불러오는데 실패했습니다.
'; + }); + + docVars.viewer.dataset.viewerType = 'word'; + } + + function viewerHwp(presignedUrl) { + docVars.viewer.innerHTML = '
한글 문서를 불러오는 중...
'; + initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); + + fetch(presignedUrl) + .then(res => { + if (!res.ok) throw new Error('HWP fetch failed'); + return res.blob(); + }) + .then(blob => { + docVars.viewer.innerHTML = ''; + + const container = document.createElement('div'); + container.style.width = '100%'; + container.style.height = '100%'; + container.style.overflow = 'auto'; + container.style.padding = '20px'; + container.style.boxSizing = 'border-box'; + container.style.background = '#f5f5f5'; + + const hwpInner = document.createElement('div'); + hwpInner.style.background = '#ffffff'; + hwpInner.style.margin = '0 auto'; + hwpInner.style.maxWidth = '800px'; + hwpInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)'; + hwpInner.style.padding = '40px'; + hwpInner.style.minHeight = '100%'; + + container.appendChild(hwpInner); + docVars.viewer.appendChild(container); + + const reader = new FileReader(); + reader.onload = (e) => { + const bstr = e.target.result; + try { + new hwp.Viewer(hwpInner, bstr); + } catch (err) { + console.error("hwp.js error: ", err); + hwpInner.innerHTML = '
한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; + } + }; + reader.readAsBinaryString(blob); + }) + .catch(err => { + console.error(err); + docVars.viewer.innerHTML = '
한글 문서를 불러오는데 실패했습니다.
'; + }); + + docVars.viewer.dataset.viewerType = 'hwp'; + } + async function viewerPdf(PresignedUrl) { resetViewer(); @@ -945,6 +1199,16 @@ function resetViewer() { } } + if (docVars.viewer.dataset.viewerType == 'excel') { + if (window.luckysheet) { + try { + window.luckysheet.destroy(); + } catch (e) { + console.error("Luckysheet destroy error: ", e); + } + } + } + docVars.viewer.dataset.viewerType = ''; } diff --git a/views/main/jsm/popup.js b/views/main/jsm/popup.js index fb15092..a946d98 100644 --- a/views/main/jsm/popup.js +++ b/views/main/jsm/popup.js @@ -36,6 +36,18 @@ if(data && Object.keys(data).length>0 && (data.$type == 'text'|| data.type == 't case 'pdf': _openPdf(fullPath,data); break; + case 'xls': + case 'xlsx': + case 'xlsm': + _openExcel(fullPath, data); + break; + case 'docx': + _openDocx(fullPath, data); + break; + case 'hwp': + case 'hwpx': + _openHwp(fullPath, data); + break; case 'mp4': case 'mov': case 'webm': @@ -571,4 +583,265 @@ function _drawMeta(data){ container.innerHTML += line; } }) +} + +// ----------------------------------------------------------------- +// 오픈소스 문서 직접 뷰잉 및 PDF 폴백 함수 정의 +// ----------------------------------------------------------------- + +function initFallbackPdfButton(dataId, path_name, resourcePath) { + const btn = document.getElementById('fallback-pdf-btn'); + if (!btn) return; + + // 이전 등록된 리스너 제거를 위해 복사 대체 + const newBtn = btn.cloneNode(true); + btn.parentNode.replaceChild(newBtn, btn); + + newBtn.style.display = 'block'; + newBtn.textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + + newBtn.addEventListener('click', async () => { + newBtn.textContent = '로딩 중...'; + newBtn.style.pointerEvents = 'none'; + + try { + // 1. 파일 메타데이터 (popup_key) 조회 + let dataInfoRes = await fetch(`${path_name}/getDataInfo`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ params: { dataIdArr: [dataId], isRemoved: false, debug: "popup fallback" } }) + }); + let dataInfo = await dataInfoRes.json(); + let result = dataInfo.result; + if (Array.isArray(result)) result = result[0]; + let popupKey = result ? result.popup_key : null; + + // 2. 만약 PDF 변환본이 아직 없다면 백엔드 변환 요청 + if (!popupKey) { + newBtn.textContent = 'PDF 변환 요청 중...'; + await fetch(`${path_name}/convertPdf`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + params: { + dataId: dataId, + resourcePath: resourcePath, + userInfoString: JSON.stringify({ user_id: 'SYSTEM', user_nm: 'Viewer User' }), + objectKey: result.object_key, + storageType: result.storage_type + } + }) + }); + alert('서버 측 PDF 변환이 시작되었습니다. 잠시 후 다시 클릭해 주세요.'); + newBtn.textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + return; + } + + // 3. PDF용 Presigned URL 생성 + let downloadUrlRes = await fetch(`${path_name}/generateDownloadUrl`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ objectKey: popupKey, resourcePath: resourcePath }) + }); + let downloadUrlData = await downloadUrlRes.json(); + if (downloadUrlData.message === 'generateDownloadUrl_success') { + let pdfUrl = downloadUrlData.url; + + // 화면 초기화 및 PDF 뷰어 로드 + document.getElementById('popup_viewer').innerHTML = ''; + newBtn.style.display = 'none'; + _openPdf(pdfUrl, {}); + } else { + alert('PDF 미리보기 주소 획득에 실패했습니다.'); + newBtn.textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + } + } catch (e) { + console.error(e); + alert('PDF 변환 로드 과정 중 오류가 발생했습니다.'); + newBtn.textContent = 'PDF로 보기'; + newBtn.style.pointerEvents = 'auto'; + } + }); +} + +function _openExcel(path, data) { + const viewer = document.getElementById('popup_viewer'); + viewer.innerHTML = '
엑셀 데이터를 불러오는 중...
'; + + fetch(path) + .then(res => { + if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); + return res.arrayBuffer(); + }) + .then(arrayBuffer => { + viewer.innerHTML = ''; + + LuckyExcel.transformExcelToLucky(arrayBuffer, function(exportJson, luckysheetfile) { + if(exportJson.sheets == null || exportJson.sheets.length == 0) { + viewer.innerHTML = '
엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)
'; + if (dataId && path_name) { + initFallbackPdfButton(dataId, path_name, resourcePath); + } + return; + } + + if (window.luckysheet) { + window.luckysheet.destroy(); + } + + viewer.style.position = 'relative'; + const container = document.createElement('div'); + container.id = 'luckysheet_inner'; + container.style.margin = '0px'; + container.style.padding = '0px'; + container.style.position = 'absolute'; + container.style.width = '100%'; + container.style.height = '100%'; + container.style.left = '0px'; + container.style.top = '0px'; + viewer.appendChild(container); + + try { + window.luckysheet.create({ + container: 'luckysheet_inner', + data: exportJson.sheets, + title: exportJson.info.name || document.title, + lang: 'en', + showinfobar: false, + myFolderUrl: 'javascript:void(0)' + }); + } catch (createErr) { + console.error("Luckysheet create error: ", createErr); + viewer.innerHTML = `
+
엑셀 시트 생성 중 오류가 발생했습니다.
+
에러: ${createErr.message}
+
`; + if (dataId && path_name) { + initFallbackPdfButton(dataId, path_name, resourcePath); + } + } + }, function(err) { + console.error("Luckysheet transform error: ", err); + viewer.innerHTML = `
+
엑셀 파일을 읽는 중 오류가 발생했습니다.
+
상세: ${err.message || err}
+
`; + if (dataId && path_name) { + initFallbackPdfButton(dataId, path_name, resourcePath); + } + }); + }) + .catch(err => { + console.error(err); + viewer.innerHTML = `
+
엑셀 파일을 불러오는데 실패했습니다.
+
에러: ${err.message}
+
`; + if (dataId && path_name) { + initFallbackPdfButton(dataId, path_name, resourcePath); + } + }); +} + +function _openDocx(path, data) { + const viewer = document.getElementById('popup_viewer'); + viewer.innerHTML = '
워드 문서를 불러오는 중...
'; + + if (dataId && path_name) { + initFallbackPdfButton(dataId, path_name, resourcePath); + } + + fetch(path) + .then(res => { + if (!res.ok) throw new Error('Word fetch failed'); + return res.arrayBuffer(); + }) + .then(arrayBuffer => { + viewer.innerHTML = ''; + + const container = document.createElement('div'); + container.style.width = '100%'; + container.style.height = '100%'; + container.style.overflow = 'auto'; + container.style.padding = '20px'; + container.style.boxSizing = 'border-box'; + container.style.background = '#f5f5f5'; + + const docxInner = document.createElement('div'); + docxInner.style.background = '#ffffff'; + docxInner.style.margin = '0 auto'; + docxInner.style.maxWidth = '800px'; + docxInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)'; + docxInner.style.padding = '40px'; + + container.appendChild(docxInner); + viewer.appendChild(container); + + docx.renderAsync(arrayBuffer, docxInner) + .then(() => console.log("docx rendered")) + .catch(err => { + console.error(err); + docxInner.innerHTML = '
워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; + }); + }) + .catch(err => { + console.error(err); + viewer.innerHTML = '
워드 문서를 불러오는데 실패했습니다.
'; + }); +} + +function _openHwp(path, data) { + const viewer = document.getElementById('popup_viewer'); + viewer.innerHTML = '
한글 문서를 불러오는 중...
'; + + if (dataId && path_name) { + initFallbackPdfButton(dataId, path_name, resourcePath); + } + + fetch(path) + .then(res => { + if (!res.ok) throw new Error('HWP fetch failed'); + return res.blob(); + }) + .then(blob => { + viewer.innerHTML = ''; + + const container = document.createElement('div'); + container.style.width = '100%'; + container.style.height = '100%'; + container.style.overflow = 'auto'; + container.style.padding = '20px'; + container.style.boxSizing = 'border-box'; + container.style.background = '#f5f5f5'; + + const hwpInner = document.createElement('div'); + hwpInner.style.background = '#ffffff'; + hwpInner.style.margin = '0 auto'; + hwpInner.style.maxWidth = '800px'; + hwpInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)'; + hwpInner.style.padding = '40px'; + hwpInner.style.minHeight = '100%'; + + container.appendChild(hwpInner); + viewer.appendChild(container); + + const reader = new FileReader(); + reader.onload = (e) => { + const bstr = e.target.result; + try { + new hwp.Viewer(hwpInner, bstr); + } catch (err) { + console.error("hwp.js error: ", err); + hwpInner.innerHTML = '
한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; + } + }; + reader.readAsBinaryString(blob); + }) + .catch(err => { + console.error(err); + viewer.innerHTML = '
한글 문서를 불러오는데 실패했습니다.
'; + }); } \ No newline at end of file diff --git a/views/main/main.html b/views/main/main.html index a1c7528..17dcbfc 100644 --- a/views/main/main.html +++ b/views/main/main.html @@ -519,6 +519,9 @@
전체보기
+
@@ -3039,6 +3042,9 @@
전체보기
+
@@ -3464,5 +3470,21 @@ + + + + + + + + + + + + + + + + diff --git a/views/main/popup.html b/views/main/popup.html index eb86081..018c3eb 100644 --- a/views/main/popup.html +++ b/views/main/popup.html @@ -106,6 +106,9 @@
100%
+ + + @@ -116,6 +119,22 @@ + + + + + + + + + + + + + + + +