//형태는 ?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 '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.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 = '
한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; } }; reader.readAsBinaryString(blob); }) .catch(err => { console.error(err); viewer.innerHTML = '
한글 문서를 불러오는데 실패했습니다.
'; }); }