Files
PM_test/views/main/jsm/popup.js
2026-06-18 08:52:23 +09:00

1163 lines
51 KiB
JavaScript

//형태는 ?path=fullPath(ex.http://gsim.hanmac..../1.jpg)&data=eadNadfl
//data는 json({key:value})을 stringify한 후에 btoa로 base64로 변경해서 달기.
//ex)
//const jsonData = JSON.stringify({ key1: "value1", key2: "value2" });
//const encodedData = btoa(jsonData);
//?path=${fullPath}&data=${encodedData}
//넘어간 data는 모두 표출됨, 숨길 data는 key 앞에 $붙여주기(type, password는 그냥써도 무조건 표출X)
//받는 형태는 pdf, img(png, jpg, jpeg, panorama), mp4, gsim(gsimViewer로 redirect), ifc, 미지원 6가지
const searchParam = new URLSearchParams(window.location.search);
//serchParam 비워주기 - debug off
// window.history.replaceState(null, null, '/')
const fullPath = searchParam.get('path');
let ext = fullPath.toLowerCase().split('.').pop();//가장 마지막.이후 확장자
const dataA = searchParam.get('data');
// title filename으로 변경
document.title = fullPath.split('/').pop();
let data = (dataA)?JSON.parse((dataA)?decodeURIComponent(escape(atob(dataA))):'{}'):undefined;
//data로 ext 넘어오면 무조건 변경
ext = data.$ext;
// 🚩3d모델뷰어 썸네일 생성을 위해 아래의 변수 추가
const thumbnail_key = decodeURIComponent(searchParam.get('thumbnail_key'));
const path_name = searchParam.get('path_name');
const resourcePath = decodeURIComponent(searchParam.get('resourcePath'));
const dataId = searchParam.get('dataId');
if(data && Object.keys(data).length>0 && (data.$type == 'text'|| data.type == 'text')){
//type text로 넘어왔을때 무조건 text뷰어로 연결
_openText(fullPath);
}else{
switch(ext){
case 'pdf':
_openPdf(fullPath,data);
break;
case 'xls':
case 'xlsx':
case 'xlsm':
_openExcel(fullPath, data);
break;
case 'docx':
_openDocx(fullPath, data);
break;
case 'pptx':
_openPptx(fullPath, data);
break;
case 'hwp':
case 'hwpx':
_openHwp(fullPath, data);
break;
case 'mp4':
case 'mov':
case 'webm':
_openVideo(fullPath);
break;
case 'png':
case 'jpg':
case 'jpeg':
case 'webp':
case 'gif':
if(data && Object.keys(data).length>0 && (data.$type == 'panorama'||data.type == 'panorama')){
// data 있으면서 type이 panorama인 경우
_openPano(fullPath);
}else{
_openImage(fullPath);
}
break;
case 'gsim':
// gsim viewer 주소로 변경 필요
window.location.href = `/libs/gsimViewer/gsimViewer.html?${searchParam.toString()}`;
break;
case 'ifc':
_openIfc(fullPath, thumbnail_key, resourcePath, dataId, path_name);
break;
case 'txt':
case 'log':
case 'md':
_openText(fullPath);
break;
case 'url':
_openUrl(fullPath);
break;
case 'zip':
_openZip(fullPath);
break;
case 'glb':
case 'gltf':
case 'obj':
case 'stl':
case 'fbx':
case '3dm':
_open3d(fullPath, thumbnail_key, resourcePath, dataId, path_name);
break;
case 'html':
_openHtml(fullPath);
break;
}
if(data && Object.keys(data).length>0){
let keys = Object.keys(data).filter(key=>key.indexOf('$') != 0);
if(keys.length > 0){
_drawMeta(data);
}
}
}
function _openText(path){
// ext를 매개변수로 받아도 md파일이 txt로 넘어오기때문에 경로에서 직접 ext 추출
let ext = path.split('.').pop();
ext = ext.split('%')[0];
fetch(path).then(res => res.text()).then(data=>{
let pre;
// md 파일 확장자 일때 파싱 후 HTML 삽입
if(ext === 'md'){
pre = document.createElement('div');
pre.classList.add('markdown-wrap');
// mermaid 시퀀스 다이어 그램 변환
const renderer = new marked.Renderer();
const originalCode = renderer.code.bind(renderer);
renderer.code = (code) => {
if(code.lang === 'mermaid') return `<pre class="mermaid">${code.text}</pre>`
return originalCode(code);
}
marked.setOptions({ gfm: true, breaks: true, renderer});
const div = document.createElement('div');
div.classList.add('markdown-body');
div.innerHTML = marked.parse(data);
// mermaid 초기화 및 생성
const mermaidEls = div.querySelectorAll('.mermaid');
if(mermaidEls.length > 0){
mermaid.initialize({ startOnLoad: false });
mermaid.init(undefined, mermaidEls);
}
pre.appendChild(div);
} else {
pre = document.createElement('pre');
pre.style.width = '100%';
pre.style.height = '100%';
pre.style.objectFit = 'contain';
pre.textContent = data;
}
document.getElementById('popup_viewer').appendChild(pre);
if(ext === 'md')hljs.highlightAll();
}).catch(err=>console.error('파일 로드 실패', err));
}
function _openZip(path){
fetch(path).then(async data=>{
let zblob = await data.blob();
const zip = new JSZip();
await zip.loadAsync(zblob);
let folderText = ``;
let fileText = ``;
zip.forEach((relativePath, zipEntry) => {
let slashIdx = relativePath.indexOf('/');
if(slashIdx == -1 || slashIdx == relativePath.length -1){
if(zipEntry.dir){
folderText += `(폴더) ${zipEntry.name.split('/')[0]} \n`;
}else{
fileText += `(파일) ${zipEntry.name} \n`
}
}
});
const pre = document.createElement('pre');
pre.style.width = '100%';
pre.style.height = '100%';
pre.style.objectFit = 'contain';
pre.textContent = `${folderText}${(folderText == ``)?'':'\n'}${fileText}`;
document.getElementById('popup_viewer').appendChild(pre);
})
}
function _openUrl(path){
fetch(path).then(res => res.text()).then(data=>{
let url = data.split('URL=')[1];
let iframe = document.createElement('iframe');
iframe.src = url;
iframe.style.width = '100%'; // 컨테이너에 맞게 너비 설정
iframe.style.height = '100%'; // 컨테이너에 맞게 높이 설정
iframe.style.border = 'none'; // 테두리 제거 (선택 사항)
document.getElementById('popup_viewer').appendChild(iframe);
}).catch(err=>console.error('파일 로드 실패', err));
}
function _openHtml(path){
fetch(path).then(res => res.text()).then(data=>{
let iframe = document.createElement('iframe');
iframe.srcdoc = data;
iframe.style.width = '100%'; // 컨테이너에 맞게 너비 설정
iframe.style.height = '100%'; // 컨테이너에 맞게 높이 설정
iframe.style.border = 'none'; // 테두리 제거 (선택 사항)
document.getElementById('popup_viewer').appendChild(iframe);
}).catch(err=>console.error('파일 로드 실패', err));
}
// function _openPdf(path,data){
// let pdf_options = {
// url: path,
// initialPage: 1,
// };
// if(data && Object.keys(data).length > 0 && (data.$password || data.password)){
// pdf_options.password = (data.$password)?data.$password:data.password;
// }
// let iframe = document.createElement('iframe');
// iframe.src = `/libs/pdfViewer/web/viewer.html`;
// document.getElementById('popup_viewer').appendChild(iframe);
// iframe.addEventListener('load', () => {
// // pdf 실행 시 무조건 1페이지부터 보이도록 기존 pdf 히스토리 삭제
// try {
// let appWin = iframe.contentWindow;
// // PDF.js의 기본 히스토리 키
// appWin.localStorage.removeItem('pdfjs.history');
// // 또는 여러 키 삭제
// Object.keys(appWin.localStorage).forEach(k => {
// if (k.startsWith('pdfjs.history') || k.startsWith('pdfjs.preferences')) {
// appWin.localStorage.removeItem(k);
// }
// });
// } catch (e) { /* ignore */ }
// let app = document.querySelector('#popup_viewer iframe').contentWindow.PDFViewerApplication;
// app.pdfCursorTools._handTool.activate();
// app.open(pdf_options);
// iframe.width = '100%';
// iframe.height = '100%';
// });
// }
function _openPdf(path, data) {
let pdf_options = {
url: path,
initialPage: 1,
};
if (data && Object.keys(data).length > 0 && (data.$password || data.password)) {
pdf_options.password = (data.$password) ? data.$password : data.password;
}
let iframe = document.createElement('iframe');
iframe.src = `/libs/pdfViewer/web/viewer.html`;
document.getElementById('popup_viewer').appendChild(iframe);
iframe.addEventListener('load', () => {
try {
let appWin = iframe.contentWindow;
// 히스토리 삭제
appWin.localStorage.removeItem('pdfjs.history');
Object.keys(appWin.localStorage).forEach(k => {
if (k.startsWith('pdfjs.history') || k.startsWith('pdfjs.preferences')) {
appWin.localStorage.removeItem(k);
}
});
let app = appWin.PDFViewerApplication;
let isRendering = false;
let lastScale = 1;
app.initializedPromise.then(() => {
const PDFPageView = appWin.PDFPageView;
// draw 함수를 완전히 오버라이드
const originalDraw = PDFPageView.prototype.draw;
PDFPageView.prototype.draw = function() {
// 고해상도 설정
this.outputScale = {
sx: (window.devicePixelRatio || 1) * 2,
sy: (window.devicePixelRatio || 1) * 2
};
return originalDraw.call(this);
};
// reset 함수도 오버라이드하여 캔버스 완전 삭제
const originalReset = PDFPageView.prototype.reset;
PDFPageView.prototype.reset = function() {
// 캔버스 완전히 제거
if (this.canvas) {
if (this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
}
this.canvas.width = 0;
this.canvas.height = 0;
this.canvas = null;
}
// 렌더 태스크 취소
if (this.renderTask) {
this.renderTask.cancel();
this.renderTask = null;
}
return originalReset.call(this);
};
if (app.pdfViewer) {
app.pdfViewer.textLayerMode = 1;
app.pdfViewer.useOnlyCssZoom = false;
}
// 확대 시 완전 재렌더링
app.eventBus.on('scalechanging', function(evt) {
if (isRendering) return;
const newScale = evt.scale;
const scaleDiff = Math.abs(newScale - lastScale);
// 스케일 변화가 있을 때만 재렌더링
if (scaleDiff > 0.01) {
isRendering = true;
setTimeout(() => {
if (app.pdfViewer && app.pdfViewer._pages) {
app.pdfViewer._pages.forEach(pageView => {
// 1. 렌더 태스크 강제 취소
if (pageView.renderTask) {
pageView.renderTask.cancel();
pageView.renderTask = null;
}
// 2. 캔버스 완전 제거
if (pageView.canvas) {
const parent = pageView.canvas.parentNode;
if (parent) {
parent.removeChild(pageView.canvas);
}
pageView.canvas.width = 0;
pageView.canvas.height = 0;
pageView.canvas = null;
}
// 3. viewport 업데이트
if (pageView.pdfPage) {
pageView.viewport = pageView.pdfPage.getViewport({
scale: newScale,
rotation: pageView.rotation || 0
});
}
// 4. 페이지 완전 리셋
pageView.reset();
// 5. 새로 그리기
pageView.draw();
});
}
lastScale = newScale;
isRendering = false;
}, 200); // 딜레이 증가
}
});
// 스크롤/페이지 변경 시에도 재렌더링
app.eventBus.on('updateviewarea', function(evt) {
if (isRendering) return;
// 현재 보이는 페이지만 재렌더링
const visiblePages = app.pdfViewer._getVisiblePages();
if (visiblePages && visiblePages.views) {
visiblePages.views.forEach(view => {
if (view && view.div && view.div.classList.contains('pdfPage')) {
const pageView = app.pdfViewer._pages[view.id - 1];
if (pageView && !pageView.renderTask) {
pageView.draw();
}
}
});
}
});
});
app.pdfCursorTools._handTool.activate();
app.open(pdf_options);
} catch (e) {
console.error('PDF 설정 오류:', e);
}
iframe.width = '100%';
iframe.height = '100%';
});
}
function _openVideo(path){
const popupVideo = document.createElement('video');
popupVideo.autoplay = true;
popupVideo.muted = true;
popupVideo.playsInline = true;
popupVideo.controls = true;
popupVideo.crossOrigin = 'anonymous';
const sourceElement = document.createElement('source');
sourceElement.src = path;
popupVideo.style.width = '100%';
popupVideo.style.height = '100%';
popupVideo.style.objectFit = 'contain';
document.getElementById('popup_viewer').appendChild(popupVideo);
popupVideo.appendChild(sourceElement);
}
function _openPano(path){
let panorama = pannellum.viewer('popup_viewer',{
"type": "equirectangular",
"panorama": path,
"autoLoad": true,
"compass":false,
})
}
function _openImage(path){
const img = document.createElement('img');
img.src = path;
document.getElementById('popup_viewer').appendChild(img);
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain';
img.addEventListener('click',()=>{
document.getElementById('large-img-container').style.display = 'block';
centerImage();
})
const container = document.getElementById('large-img-container');
const image = document.getElementById('large-image');
const zoomInfo = document.getElementById('large-img-zoomInfo');
image.src = path;
let isDragging = false;
let startX, startY;
let translateX = 0;
let translateY = 0;
let scale = 1;
// 드래그 시작
function dragStart(e) {
if (e.target !== image) return;
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
// 드래그 중일 때 transition 제거
image.style.transition = 'none';
}
// 드래그 중
function drag(e) {
if (!isDragging) return;
e.preventDefault();
// 현재 마우스 위치에서 시작 위치를 빼서 이동 거리 계산
translateX = e.clientX - startX;
translateY = e.clientY - startY;
updateImageTransform();
}
// 드래그 종료
function dragEnd() {
if (!isDragging) return;
isDragging = false;
// 드래그 종료 시 부드러운 transition 효과 복원
image.style.transition = 'transform 0.1s ease-out';
}
// 확대/축소
function zoom(e) {
e.preventDefault();
// 현재 이미지의 실제 위치와 크기
const imageRect = image.getBoundingClientRect();
// 마우스 포인터의 화면상 좌표
const mouseX = e.clientX;
const mouseY = e.clientY;
// 마우스 포인터의 이미지상 상대 좌표
const mouseImageX = mouseX - imageRect.left;
const mouseImageY = mouseY - imageRect.top;
// 이전 스케일 저장
const prevScale = scale;
// 새로운 스케일 계산
const delta = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail));
const scaleFactor = 0.1;
if (delta > 0) {
scale = Math.min(4, scale + scaleFactor);
} else {
scale = Math.max(0.1, scale - scaleFactor);
}
// 스케일 변화율
const scaleRatio = scale / prevScale;
// 새로운 translate 값 계산
// 마우스 포인터 위치는 고정되어야 하므로, 스케일 변화에 따른 위치 조정
translateX = mouseX - (((mouseX - translateX) * scaleRatio));
translateY = mouseY - (((mouseY - translateY) * scaleRatio));
updateImageTransform();
updateZoomInfo();
}
// 이미지 transform 업데이트
function updateImageTransform() {
// transform의 원점을 이미지의 중심으로 설정
const originX = image.offsetWidth / 2;
const originY = image.offsetHeight / 2;
image.style.transformOrigin = `${originX}px ${originY}px`;
image.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}
// 줌 정보 업데이트
function updateZoomInfo() {
zoomInfo.textContent = `${Math.round(scale * 100)}%`;
}
// 이미지 초기 위치 설정
function centerImage() {
translateX = (container.offsetWidth - image.offsetWidth) / 2;
translateY = (container.offsetHeight - image.offsetHeight) / 2;
updateImageTransform();
}
// 이벤트 리스너 등록
container.addEventListener('mousedown', dragStart);
window.addEventListener('mousemove', drag);
window.addEventListener('mouseup', dragEnd);
container.addEventListener('wheel', zoom);
container.addEventListener('contextmenu',(e)=>{
e.preventDefault();
container.style.display = 'none';
})
// 더블클릭으로 원래 크기로 복원
container.addEventListener('dblclick', () => {
scale = 1;
updateZoomInfo();
});
}
function _openIfc(path, thumbnail_key, resourcePath, dataId, path_name){
const iframe = document.createElement('iframe');
iframe.onload = () => {
iframe.contentWindow.postMessage({ path, thumbnail_key, resourcePath, dataId, path_name }, '*'); // path 값을 iframe에 전달
};
iframe.src = `/libs/ifcViewer/index.html`;
iframe.width = '100%';
iframe.height = '100%';
document.getElementById('popup_viewer').appendChild(iframe);
}
function _open3d(path, thumbnail_key, resourcePath, dataId, path_name){
const iframe = document.createElement('iframe');
iframe.onload = () => {
iframe.contentWindow.postMessage({ path, thumbnail_key, resourcePath, dataId, path_name }, '*'); // path 값을 iframe에 전달
};
iframe.src = `/libs/3dViewer/index.html`;
iframe.width = '100%';
iframe.height = '100%';
document.getElementById('popup_viewer').appendChild(iframe);
}
function _drawMeta(data){
let container = document.getElementById('meta-data');
container.style.display = 'block';
Object.keys(data).forEach(item=>{
if(!item.startsWith('$') && item != 'type' && item != 'password'){
let line = `<span class="key">${item}</span> : <span>${data[item]}</span><br>`;
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.overflowX = 'hidden';
container.style.overflowY = 'auto';
container.style.padding = '20px';
container.style.boxSizing = 'border-box';
container.style.background = '#f5f5f5';
const styleEl = document.createElement('style');
styleEl.textContent = `
.hwp-inner-container {
background: #ffffff;
margin: 0 auto;
max-width: 800px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
padding: 30px !important;
box-sizing: border-box !important;
min-height: 100%;
}
.hwp-inner-container > div > div {
max-width: 100% !important;
height: auto !important;
box-sizing: border-box !important;
padding-left: 20px !important;
padding-right: 20px !important;
margin-bottom: 20px !important;
}
.hwp-inner-container table {
max-width: 100% !important;
width: 100% !important;
table-layout: fixed !important;
}
.hwp-inner-container img {
max-width: 100% !important;
height: auto !important;
}
@media (max-width: 600px) {
.hwp-inner-container {
padding: 10px !important;
}
.hwp-inner-container > div > div {
padding-left: 10px !important;
padding-right: 10px !important;
}
}
`;
container.appendChild(styleEl);
const hwpInner = document.createElement('div');
hwpInner.classList.add('hwp-inner-container');
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>';
});
}
function _openPptx(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;">PPTX 문서를 불러오는 중...</div>';
if (dataId && path_name) {
initFallbackPdfButton(dataId, path_name, resourcePath);
}
fetch(path)
.then(res => {
if (!res.ok) throw new Error('PPTX fetch failed');
return res.arrayBuffer();
})
.then(async (arrayBuffer) => {
try {
const zip = await JSZip.loadAsync(arrayBuffer);
// Read presentation.xml to get slide size
const presentationXmlText = await zip.file("ppt/presentation.xml").async("text");
const parser = new DOMParser();
const presDoc = parser.parseFromString(presentationXmlText, "text/xml");
const sldSz = presDoc.getElementsByTagName("p:sldSz")[0];
const cx = sldSz ? (parseInt(sldSz.getAttribute("cx"), 10) || 12192000) : 12192000;
const cy = sldSz ? (parseInt(sldSz.getAttribute("cy"), 10) || 6858000) : 6858000;
const ratio = (cy / cx) * 100;
// Get slide files
const slideFiles = Object.keys(zip.files).filter(name => name.startsWith("ppt/slides/slide") && name.endsWith(".xml"));
slideFiles.sort((a, b) => {
const numA = parseInt(a.replace("ppt/slides/slide", "").replace(".xml", ""), 10);
const numB = parseInt(b.replace("ppt/slides/slide", "").replace(".xml", ""), 10);
return numA - numB;
});
viewer.innerHTML = '';
const slidesContainer = document.createElement('div');
slidesContainer.style.display = 'flex';
slidesContainer.style.flexDirection = 'column';
slidesContainer.style.gap = '20px';
slidesContainer.style.alignItems = 'center';
slidesContainer.style.background = '#f0f0f0';
slidesContainer.style.padding = '20px';
slidesContainer.style.width = '100%';
slidesContainer.style.height = '100%';
slidesContainer.style.overflow = 'auto';
slidesContainer.style.boxSizing = 'border-box';
viewer.appendChild(slidesContainer);
for (let i = 0; i < slideFiles.length; i++) {
const slideXmlText = await zip.file(slideFiles[i]).async("text");
const slideDoc = parser.parseFromString(slideXmlText, "text/xml");
const slideCard = document.createElement('div');
slideCard.className = 'pptx-slide-card';
slideCard.style.position = 'relative';
slideCard.style.width = '100%';
slideCard.style.maxWidth = '800px';
slideCard.style.backgroundColor = '#ffffff';
slideCard.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)';
slideCard.style.height = '0';
slideCard.style.paddingTop = ratio + '%';
slideCard.style.overflow = 'hidden';
slideCard.style.flexShrink = '0';
const slideContent = document.createElement('div');
slideContent.style.position = 'absolute';
slideContent.style.top = '0';
slideContent.style.left = '0';
slideContent.style.width = '100%';
slideContent.style.height = '100%';
slideCard.appendChild(slideContent);
slidesContainer.appendChild(slideCard);
// Parse relationships for this slide
const relMap = {};
try {
const slideName = slideFiles[i].split('/').pop();
const relsFileName = `ppt/slides/_rels/${slideName}.rels`;
const relsFile = zip.file(relsFileName);
if (relsFile) {
const relsXmlText = await relsFile.async("text");
const relsDoc = parser.parseFromString(relsXmlText, "text/xml");
const relationships = relsDoc.getElementsByTagName("Relationship");
for (let r = 0; r < relationships.length; r++) {
const id = relationships[r].getAttribute("Id");
const target = relationships[r].getAttribute("Target");
relMap[id] = target;
}
}
} catch (relErr) {
console.warn("Failed to parse relationships for slide:", slideFiles[i], relErr);
}
const elements = slideDoc.querySelectorAll('p\\:sp, sp, p\\:pic, pic, p\\:graphicFrame, graphicFrame');
for (const elem of elements) {
const xfrm = elem.querySelector('a\\:xfrm, xfrm');
if (!xfrm) continue;
const off = xfrm.querySelector('a\\:off, off');
const ext = xfrm.querySelector('a\\:ext, ext');
if (!off || !ext) continue;
const x = parseInt(off.getAttribute('x'), 10);
const y = parseInt(off.getAttribute('y'), 10);
const w = parseInt(ext.getAttribute('cx'), 10);
const h = parseInt(ext.getAttribute('cy'), 10);
const leftPct = (x / cx) * 100;
const topPct = (y / cy) * 100;
const widthPct = (w / cx) * 100;
const heightPct = (h / cy) * 100;
const itemDiv = document.createElement('div');
itemDiv.style.position = 'absolute';
itemDiv.style.left = leftPct + '%';
itemDiv.style.top = topPct + '%';
itemDiv.style.width = widthPct + '%';
itemDiv.style.height = heightPct + '%';
itemDiv.style.boxSizing = 'border-box';
const nodeName = elem.nodeName.toLowerCase();
if (nodeName.includes('pic')) {
let imgUrl = null;
try {
const blip = elem.querySelector('a\\:blip, blip');
const rId = blip ? (blip.getAttribute('r:embed') || blip.getAttribute('embed')) : null;
if (rId && relMap[rId]) {
const targetPath = relMap[rId].replace('../', 'ppt/');
const imgFile = zip.file(targetPath);
if (imgFile) {
const imgBlob = await imgFile.async("blob");
imgUrl = URL.createObjectURL(imgBlob);
}
}
} catch (imgErr) {
console.warn("Failed to extract slide image:", imgErr);
}
if (imgUrl) {
itemDiv.style.backgroundImage = `url("${imgUrl}")`;
itemDiv.style.backgroundRepeat = 'no-repeat';
itemDiv.style.backgroundPosition = 'center';
itemDiv.style.backgroundSize = 'contain';
} else {
itemDiv.style.border = '1px dashed #cccccc';
itemDiv.style.backgroundColor = '#f9f9f9';
itemDiv.style.display = 'flex';
itemDiv.style.alignItems = 'center';
itemDiv.style.justifyContent = 'center';
const label = document.createElement('span');
label.style.color = '#999999';
label.style.fontSize = '10px';
label.style.fontWeight = 'bold';
label.textContent = '[그림 영역]';
itemDiv.appendChild(label);
}
} else if (nodeName.includes('graphicframe')) {
const tbl = elem.querySelector('a\\:tbl, tbl');
if (tbl) {
const htmlTable = document.createElement('table');
htmlTable.style.width = '100%';
htmlTable.style.height = '100%';
htmlTable.style.borderCollapse = 'collapse';
htmlTable.style.fontSize = 'calc(0.4vw + 5px)';
htmlTable.style.fontFamily = 'sans-serif';
htmlTable.style.backgroundColor = '#ffffff';
htmlTable.style.boxShadow = '0 1px 3px rgba(0,0,0,0.05)';
const rows = tbl.querySelectorAll('a\\:tr, tr');
rows.forEach((row, rIdx) => {
const trEl = document.createElement('tr');
if (rIdx === 0) {
trEl.style.backgroundColor = '#f8f9fa';
trEl.style.fontWeight = '600';
} else if (rIdx % 2 === 0) {
trEl.style.backgroundColor = '#fafafa';
}
const cells = row.querySelectorAll('a\\:tc, tc');
cells.forEach(cell => {
const tdEl = document.createElement('td');
tdEl.style.border = '1px solid #e0e0e0';
tdEl.style.padding = '4px 6px';
tdEl.style.wordBreak = 'break-all';
tdEl.style.verticalAlign = 'middle';
const gridSpan = cell.getAttribute('gridSpan');
if (gridSpan) tdEl.setAttribute('colspan', gridSpan);
const rowSpan = cell.getAttribute('rowSpan');
if (rowSpan) tdEl.setAttribute('rowspan', rowSpan);
const txBody = cell.querySelector('a\\:txBody, txBody');
if (txBody) {
const paragraphs = txBody.querySelectorAll('a\\:p, p');
paragraphs.forEach(p => {
const runs = p.querySelectorAll('a\\:r, r');
let cellText = '';
runs.forEach(r => {
const t = r.querySelector('a\\:t, t');
if (t) cellText += t.textContent;
});
if (cellText.trim()) {
const pEl = document.createElement('p');
pEl.style.margin = '0';
pEl.style.lineHeight = '1.2';
pEl.textContent = cellText;
tdEl.appendChild(pEl);
}
});
}
trEl.appendChild(tdEl);
});
htmlTable.appendChild(trEl);
});
itemDiv.appendChild(htmlTable);
} else {
itemDiv.style.border = '1px dashed #dddddd';
itemDiv.style.backgroundColor = '#fdfdfd';
itemDiv.style.display = 'flex';
itemDiv.style.alignItems = 'center';
itemDiv.style.justifyContent = 'center';
const label = document.createElement('span');
label.style.color = '#aaaaaa';
label.style.fontSize = '10px';
label.style.fontWeight = 'bold';
label.textContent = '[차트 영역]';
itemDiv.appendChild(label);
}
} else {
const txBody = elem.querySelector('p\\:txBody, txBody');
if (txBody) {
itemDiv.style.overflow = 'hidden';
itemDiv.style.wordBreak = 'break-all';
itemDiv.style.fontSize = 'calc(0.5vw + 5px)';
itemDiv.style.fontFamily = 'sans-serif';
itemDiv.style.color = '#333333';
const paragraphs = txBody.querySelectorAll('a\\:p, p');
paragraphs.forEach(p => {
const runs = p.querySelectorAll('a\\:r, r');
let paraText = '';
runs.forEach(r => {
const t = r.querySelector('a\\:t, t');
if (t) paraText += t.textContent;
});
if (paraText.trim()) {
const pEl = document.createElement('p');
pEl.style.margin = '0 0 2px 0';
pEl.style.lineHeight = '1.2';
pEl.textContent = paraText;
itemDiv.appendChild(pEl);
}
});
} else {
itemDiv.style.border = '1px solid #eeeeee';
itemDiv.style.backgroundColor = 'rgba(0,0,0,0.01)';
}
}
slideContent.appendChild(itemDiv);
}
}
} catch (parseErr) {
console.error("PPTX parse error:", parseErr);
viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;padding:20px;text-align:center;">PPTX 파싱 중 에러가 발생했습니다. 상단의 "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;">PPTX 문서를 불러오는데 실패했습니다.</div>';
});
}