//형태는 ?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 `
${code.text}
` 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 = `${item} : ${data[item]}
`; container.innerHTML += line; } }) } // ----------------------------------------------------------------- // 오픈소스 문서 직접 뷰잉 및 PDF 폴백 함수 정의 // ----------------------------------------------------------------- function initFallbackPdfButton(dataId, path_name, resourcePath) { const btn = document.getElementById('fallback-pdf-btn'); if (!btn) return; // 이전 등록된 리스너 제거를 위해 복사 대체 const newBtn = btn.cloneNode(true); btn.parentNode.replaceChild(newBtn, btn); newBtn.style.display = 'block'; newBtn.textContent = 'PDF로 보기'; newBtn.style.pointerEvents = 'auto'; newBtn.addEventListener('click', async () => { newBtn.textContent = '로딩 중...'; newBtn.style.pointerEvents = 'none'; try { // 1. 파일 메타데이터 (popup_key) 조회 let dataInfoRes = await fetch(`${path_name}/getDataInfo`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { dataIdArr: [dataId], isRemoved: false, debug: "popup fallback" } }) }); let dataInfo = await dataInfoRes.json(); let result = dataInfo.result; if (Array.isArray(result)) result = result[0]; let popupKey = result ? result.popup_key : null; // 2. 만약 PDF 변환본이 아직 없다면 백엔드 변환 요청 if (!popupKey) { newBtn.textContent = 'PDF 변환 요청 중...'; await fetch(`${path_name}/convertPdf`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ params: { dataId: dataId, resourcePath: resourcePath, userInfoString: JSON.stringify({ user_id: 'SYSTEM', user_nm: 'Viewer User' }), objectKey: result.object_key, storageType: result.storage_type } }) }); alert('서버 측 PDF 변환이 시작되었습니다. 잠시 후 다시 클릭해 주세요.'); newBtn.textContent = 'PDF로 보기'; newBtn.style.pointerEvents = 'auto'; return; } // 3. PDF용 Presigned URL 생성 let downloadUrlRes = await fetch(`${path_name}/generateDownloadUrl`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ objectKey: popupKey, resourcePath: resourcePath }) }); let downloadUrlData = await downloadUrlRes.json(); if (downloadUrlData.message === 'generateDownloadUrl_success') { let pdfUrl = downloadUrlData.url; // 화면 초기화 및 PDF 뷰어 로드 document.getElementById('popup_viewer').innerHTML = ''; newBtn.style.display = 'none'; _openPdf(pdfUrl, {}); } else { alert('PDF 미리보기 주소 획득에 실패했습니다.'); newBtn.textContent = 'PDF로 보기'; newBtn.style.pointerEvents = 'auto'; } } catch (e) { console.error(e); alert('PDF 변환 로드 과정 중 오류가 발생했습니다.'); newBtn.textContent = 'PDF로 보기'; newBtn.style.pointerEvents = 'auto'; } }); } function _openExcel(path, data) { const viewer = document.getElementById('popup_viewer'); viewer.innerHTML = '
엑셀 데이터를 불러오는 중...
'; fetch(path) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); return res.arrayBuffer(); }) .then(arrayBuffer => { viewer.innerHTML = ''; LuckyExcel.transformExcelToLucky(arrayBuffer, function(exportJson, luckysheetfile) { if(exportJson.sheets == null || exportJson.sheets.length == 0) { viewer.innerHTML = '
엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)
'; if (dataId && path_name) { initFallbackPdfButton(dataId, path_name, resourcePath); } return; } if (window.luckysheet) { window.luckysheet.destroy(); } viewer.style.position = 'relative'; const container = document.createElement('div'); container.id = 'luckysheet_inner'; container.style.margin = '0px'; container.style.padding = '0px'; container.style.position = 'absolute'; container.style.width = '100%'; container.style.height = '100%'; container.style.left = '0px'; container.style.top = '0px'; viewer.appendChild(container); try { window.luckysheet.create({ container: 'luckysheet_inner', data: exportJson.sheets, title: exportJson.info.name || document.title, lang: 'en', showinfobar: false, myFolderUrl: 'javascript:void(0)' }); } catch (createErr) { console.error("Luckysheet create error: ", createErr); viewer.innerHTML = `
엑셀 시트 생성 중 오류가 발생했습니다.
에러: ${createErr.message}
`; if (dataId && path_name) { initFallbackPdfButton(dataId, path_name, resourcePath); } } }, function(err) { console.error("Luckysheet transform error: ", err); viewer.innerHTML = `
엑셀 파일을 읽는 중 오류가 발생했습니다.
상세: ${err.message || err}
`; if (dataId && path_name) { initFallbackPdfButton(dataId, path_name, resourcePath); } }); }) .catch(err => { console.error(err); viewer.innerHTML = `
엑셀 파일을 불러오는데 실패했습니다.
에러: ${err.message}
`; if (dataId && path_name) { initFallbackPdfButton(dataId, path_name, resourcePath); } }); } function _openDocx(path, data) { const viewer = document.getElementById('popup_viewer'); viewer.innerHTML = '
워드 문서를 불러오는 중...
'; if (dataId && path_name) { initFallbackPdfButton(dataId, path_name, resourcePath); } fetch(path) .then(res => { if (!res.ok) throw new Error('Word fetch failed'); return res.arrayBuffer(); }) .then(arrayBuffer => { viewer.innerHTML = ''; const container = document.createElement('div'); container.style.width = '100%'; container.style.height = '100%'; container.style.overflow = 'auto'; container.style.padding = '20px'; container.style.boxSizing = 'border-box'; container.style.background = '#f5f5f5'; const docxInner = document.createElement('div'); docxInner.style.background = '#ffffff'; docxInner.style.margin = '0 auto'; docxInner.style.maxWidth = '800px'; docxInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)'; docxInner.style.padding = '40px'; container.appendChild(docxInner); viewer.appendChild(container); docx.renderAsync(arrayBuffer, docxInner) .then(() => console.log("docx rendered")) .catch(err => { console.error(err); docxInner.innerHTML = '
워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; }); }) .catch(err => { console.error(err); viewer.innerHTML = '
워드 문서를 불러오는데 실패했습니다.
'; }); } function _openHwp(path, data) { const viewer = document.getElementById('popup_viewer'); viewer.innerHTML = '
한글 문서를 불러오는 중...
'; if (dataId && path_name) { initFallbackPdfButton(dataId, path_name, resourcePath); } fetch(path) .then(res => { if (!res.ok) throw new Error('HWP fetch failed'); return res.blob(); }) .then(blob => { viewer.innerHTML = ''; const container = document.createElement('div'); container.style.width = '100%'; container.style.height = '100%'; container.style.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 = '
한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; } }; reader.readAsBinaryString(blob); }) .catch(err => { console.error(err); viewer.innerHTML = '
한글 문서를 불러오는데 실패했습니다.
'; }); } function _openPptx(path, data) { const viewer = document.getElementById('popup_viewer'); viewer.innerHTML = '
PPTX 문서를 불러오는 중...
'; 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 = '
PPTX 파싱 중 에러가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; } }) .catch(err => { console.error(err); viewer.innerHTML = '
PPTX 문서를 불러오는데 실패했습니다.
'; }); }