오픈소스 직접뷰어 기능 추가
This commit is contained in:
@@ -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();
|
||||
|
||||
BIN
libs/hwp.js
Normal file
BIN
libs/hwp.js
Normal file
Binary file not shown.
52
local_clear_queue.js
Normal file
52
local_clear_queue.js
Normal file
@@ -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);
|
||||
});
|
||||
199
local_pdf_worker.js
Normal file
199
local_pdf_worker.js
Normal file
@@ -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);
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
39
package-lock.json
generated
39
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
10
queue.js
10
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;
|
||||
|
||||
|
||||
BIN
temp_convert/input_1_1781504351932.hwpx
Normal file
BIN
temp_convert/input_1_1781504351932.hwpx
Normal file
Binary file not shown.
BIN
temp_convert/input_2_1781504354837.docx
Normal file
BIN
temp_convert/input_2_1781504354837.docx
Normal file
Binary file not shown.
BIN
temp_convert/input_3_1781504356321.docx
Normal file
BIN
temp_convert/input_3_1781504356321.docx
Normal file
Binary file not shown.
BIN
temp_convert/input_4_1781504357435.hwpx
Normal file
BIN
temp_convert/input_4_1781504357435.hwpx
Normal file
Binary file not shown.
BIN
temp_convert/input_5_1781504358912.hwpx
Normal file
BIN
temp_convert/input_5_1781504358912.hwpx
Normal file
Binary file not shown.
BIN
temp_convert/input_6_1781504360423.docx
Normal file
BIN
temp_convert/input_6_1781504360423.docx
Normal file
Binary file not shown.
BIN
temp_convert/input_7_1781505810092.docx
Normal file
BIN
temp_convert/input_7_1781505810092.docx
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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 = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">엑셀 데이터를 불러오는 중...</div>';
|
||||
|
||||
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 = '<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;font-size:1.1rem;background:#fff;">엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)</div>';
|
||||
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 = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 시트 생성 중 오류가 발생했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">에러: ${createErr.message}</div>
|
||||
</div>`;
|
||||
}
|
||||
}, function(err) {
|
||||
console.error("Luckysheet transform error: ", err);
|
||||
vars.viewer.innerHTML = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 파일을 읽는 중 오류가 발생했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">상세: ${err.message || err}</div>
|
||||
</div>`;
|
||||
initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
vars.viewer.innerHTML = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 파일을 불러오는데 실패했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">에러: ${err.message}</div>
|
||||
</div>`;
|
||||
initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey);
|
||||
});
|
||||
|
||||
vars.viewer.dataset.viewerType = 'excel';
|
||||
}
|
||||
|
||||
function viewerWord(presignedUrl) {
|
||||
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">워드 문서를 불러오는 중...</div>';
|
||||
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 = '<div style="color:#d9534f;text-align:center;">워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">워드 문서를 불러오는데 실패했습니다.</div>';
|
||||
});
|
||||
|
||||
vars.viewer.dataset.viewerType = 'word';
|
||||
}
|
||||
|
||||
function viewerHwp(presignedUrl) {
|
||||
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">한글 문서를 불러오는 중...</div>';
|
||||
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 = '<div style="color:#d9534f;text-align:center;">한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
|
||||
}
|
||||
};
|
||||
reader.readAsBinaryString(blob);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">한글 문서를 불러오는데 실패했습니다.</div>';
|
||||
});
|
||||
|
||||
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 = '';
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">엑셀 데이터를 불러오는 중...</div>';
|
||||
|
||||
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 = '<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;font-size:1.1rem;background:#fff;">엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)</div>';
|
||||
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 = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 시트 생성 중 오류가 발생했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">에러: ${createErr.message}</div>
|
||||
</div>`;
|
||||
}
|
||||
}, function(err) {
|
||||
console.error("Luckysheet transform error: ", err);
|
||||
docVars.viewer.innerHTML = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 파일을 읽는 중 오류가 발생했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">상세: ${err.message || err}</div>
|
||||
</div>`;
|
||||
initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
docVars.viewer.innerHTML = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 파일을 불러오는데 실패했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">에러: ${err.message}</div>
|
||||
</div>`;
|
||||
initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey);
|
||||
});
|
||||
|
||||
docVars.viewer.dataset.viewerType = 'excel';
|
||||
}
|
||||
|
||||
function viewerWord(presignedUrl) {
|
||||
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">워드 문서를 불러오는 중...</div>';
|
||||
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 = '<div style="color:#d9534f;text-align:center;">워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">워드 문서를 불러오는데 실패했습니다.</div>';
|
||||
});
|
||||
|
||||
docVars.viewer.dataset.viewerType = 'word';
|
||||
}
|
||||
|
||||
function viewerHwp(presignedUrl) {
|
||||
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">한글 문서를 불러오는 중...</div>';
|
||||
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 = '<div style="color:#d9534f;text-align:center;">한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
|
||||
}
|
||||
};
|
||||
reader.readAsBinaryString(blob);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">한글 문서를 불러오는데 실패했습니다.</div>';
|
||||
});
|
||||
|
||||
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 = '';
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">엑셀 데이터를 불러오는 중...</div>';
|
||||
|
||||
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 = '<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;font-size:1.1rem;background:#fff;">엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)</div>';
|
||||
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 = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 시트 생성 중 오류가 발생했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">에러: ${createErr.message}</div>
|
||||
</div>`;
|
||||
if (dataId && path_name) {
|
||||
initFallbackPdfButton(dataId, path_name, resourcePath);
|
||||
}
|
||||
}
|
||||
}, function(err) {
|
||||
console.error("Luckysheet transform error: ", err);
|
||||
viewer.innerHTML = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 파일을 읽는 중 오류가 발생했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">상세: ${err.message || err}</div>
|
||||
</div>`;
|
||||
if (dataId && path_name) {
|
||||
initFallbackPdfButton(dataId, path_name, resourcePath);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
viewer.innerHTML = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
|
||||
<div>엑셀 파일을 불러오는데 실패했습니다.</div>
|
||||
<div style="font-size:0.9rem;color:#999;margin-top:8px;">에러: ${err.message}</div>
|
||||
</div>`;
|
||||
if (dataId && path_name) {
|
||||
initFallbackPdfButton(dataId, path_name, resourcePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _openDocx(path, data) {
|
||||
const viewer = document.getElementById('popup_viewer');
|
||||
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">워드 문서를 불러오는 중...</div>';
|
||||
|
||||
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 = '<div style="color:#d9534f;text-align:center;">워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">워드 문서를 불러오는데 실패했습니다.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function _openHwp(path, data) {
|
||||
const viewer = document.getElementById('popup_viewer');
|
||||
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">한글 문서를 불러오는 중...</div>';
|
||||
|
||||
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 = '<div style="color:#d9534f;text-align:center;">한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
|
||||
}
|
||||
};
|
||||
reader.readAsBinaryString(blob);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">한글 문서를 불러오는데 실패했습니다.</div>';
|
||||
});
|
||||
}
|
||||
@@ -519,6 +519,9 @@
|
||||
<div class="btn">
|
||||
<div class="text ft-12">전체보기</div>
|
||||
</div>
|
||||
<div class="fallback-btn btn-sec" id="main-fallback-pdf-btn" style="display: none; background: #007bff; color: white; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 8px; font-weight: bold; align-items: center; justify-content: center;">
|
||||
<div class="text">PDF로 보기</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viewer-wrap">
|
||||
@@ -3039,6 +3042,9 @@
|
||||
<div class="btn">
|
||||
<div class="text ft-12">전체보기</div>
|
||||
</div>
|
||||
<div class="fallback-btn btn-sec" id="doc-fallback-pdf-btn" style="display: none; background: #007bff; color: white; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-left: 8px; font-weight: bold; align-items: center; justify-content: center;">
|
||||
<div class="text">PDF로 보기</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viewer-wrap">
|
||||
@@ -3464,5 +3470,21 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/exifr/dist/lite.umd.js"></script>
|
||||
<script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
|
||||
|
||||
<!-- Luckysheet 및 Luckyexcel (Excel 뷰어) 스타일 & 스크립트 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/css/pluginsCss.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/plugins.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/css/luckysheet.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/assets/iconfont/iconfont.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/js/plugin.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/luckysheet.umd.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luckyexcel@1.0.1/dist/luckyexcel.umd.js"></script>
|
||||
|
||||
<!-- docx-preview (Word 뷰어) 스타일 & 스크립트 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docx-preview@0.1.18/dist/docx-preview.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/docx-preview@0.1.18/dist/docx-preview.min.js"></script>
|
||||
|
||||
<!-- hwp.js (한글 뷰어) 스크립트 -->
|
||||
<script src="/libs/hwp.js"></script>
|
||||
|
||||
<script src="socket.io.js" type="module"></script>
|
||||
<script src="/main/jsm/main.js" type="module"></script>
|
||||
|
||||
@@ -106,6 +106,9 @@
|
||||
<div class="zoom-info" id="large-img-zoomInfo">100%</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF 폴백 버튼 -->
|
||||
<div class="fallback-btn" id="fallback-pdf-btn" style="display: none; position: absolute; top: 10px; right: 10px; z-index: 1000; background: #007bff; color: white; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 0.875rem; font-weight: bold; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">PDF로 보기</div>
|
||||
|
||||
<!-- meta정보 -->
|
||||
<div class="meta" id="meta-data" style="display: none;">
|
||||
</div>
|
||||
@@ -116,6 +119,22 @@
|
||||
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.js"></script>
|
||||
|
||||
<script src="https://api.digitalarchive.work/hmCesium/lib/jszip/dist/jszip.min.js"></script>
|
||||
|
||||
<!-- Luckysheet 및 Luckyexcel (Excel 뷰어) 스타일 & 스크립트 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/css/pluginsCss.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/plugins.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/css/luckysheet.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/assets/iconfont/iconfont.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/plugins/js/plugin.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luckysheet@2.1.13/dist/luckysheet.umd.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/luckyexcel@1.0.1/dist/luckyexcel.umd.js"></script>
|
||||
|
||||
<!-- docx-preview (Word 뷰어) 스타일 & 스크립트 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docx-preview@0.1.18/dist/docx-preview.css" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/docx-preview@0.1.18/dist/docx-preview.min.js"></script>
|
||||
|
||||
<!-- hwp.js (한글 뷰어) 스크립트 -->
|
||||
<script src="/libs/hwp.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user