//형태는 ?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]}