오픈소스 직접뷰어 기능 추가

This commit is contained in:
koj729
2026-06-15 16:10:05 +09:00
parent d13c414d7f
commit cb0c42fbeb
21 changed files with 1202 additions and 33 deletions

View File

@@ -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

View File

@@ -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 = '';
}

View File

@@ -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 = '';
}

View File

@@ -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>';
});
}

View File

@@ -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>

View File

@@ -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>