import { checkProjectInactive } from '../main.js'; import { docVars } from './docVariable.js'; import { syncDocInfo, getDocDataBySelected, getGroupCompanyData, convertDocPdf } from './docDataManager.js'; import { getUserSettingsInDB, sendUserDataToServer } from './docModalManager.js'; import { splitBaseAndExt } from '../archive/common.js'; let main = document.querySelector('.official-doc-main'); // 화면 let fileContainer = document.querySelector('.official-doc-main .official-doc-list-container .official-doc-main-body .official-doc-file'); let recipientContainer = document.querySelector('.official-doc-main .official-doc-list-container .official-doc-main-body .official-doc-file .official-doc-top'); let recipientList = recipientContainer.querySelector('#doc-recipient-container ul'); let sendingContainer = document.querySelector('.official-doc-main .official-doc-list-container .official-doc-main-body .official-doc-file .official-doc-bottom'); let sendingList = sendingContainer.querySelector('#doc-sending-container ul'); let slider = document.querySelector('.official-doc-main .official-doc-list-container .official-doc-main-body .official-doc-file .official-doc-center-division'); let totalviewBtn = document.querySelector('.official-doc-main .official-doc-preview .official-doc-preview--container .viewer-header .wrap .btn'); let previewNotice = document.querySelector('.official-doc-main .official-doc-preview .official-doc-preview--notice'); let previewContainer = document.querySelector('.official-doc-main .official-doc-preview .official-doc-preview--container'); let viewerWrap = previewContainer?.querySelector('.viewer-wrap'); let infoWrap = previewContainer?.querySelector('.info-wrap'); let metadataContainer = previewContainer?.querySelector('.metadata'); export function initCategoryClicked() { document.querySelectorAll('.official-doc-category').forEach((btn) => { btn.addEventListener('click', async () => { document.querySelectorAll('.official-doc-category').forEach((b) => b.classList.remove('official-doc-tab-on')); btn.classList.add('official-doc-tab-on'); const categoryText = btn.querySelector('h6')?.innerText.trim(); const docCategory = categoryText === '전체보기' ? '' : categoryText; await syncDocInfo(['official', 'attach', null]); await getDocDataBySelected(); }); }); // 처음엔 전체보기(official-doc-category의 첫번째 요소임) 버튼 클릭되게 const totalListBtn = document.querySelector('.official-doc-category'); if (totalListBtn) totalListBtn.click(); } export function initDocSlider() { if (checkProjectInactive()) return; let isDocDragging = false; slider.addEventListener('mousedown', (e) => { e.preventDefault(); isDocDragging = true; }); document.addEventListener('mouseup', () => { isDocDragging = false; }); document.addEventListener('mousemove', (e) => { if (!isDocDragging) return; const containerRect = fileContainer.getBoundingClientRect(); const newY = containerRect.bottom - e.clientY; const minHeight = 45; // 최소높이 const maxHeight = containerRect.height; const halfMaxHeight = maxHeight / 2; const newHeight = Math.min(maxHeight, Math.max(minHeight, newY)); sendingContainer.style.height = `${newHeight}px`; recipientContainer.style.height = `${maxHeight - newHeight}px`; }); } /*******************************************************************************************************************************/ /*************************************************************************************************************** 메인화면 리스트 */ /*******************************************************************************************************************************/ export async function drawDocList(data, direction) { let entries = sortDocData(data); if (!recipientList || !sendingList) return; if (!direction) { recipientList.innerHTML = ''; sendingList.innerHTML = ''; } else if (direction == '수신') { recipientList.innerHTML = ''; } else if (direction == '발신') { sendingList.innerHTML = ''; } if (!entries || entries.length === 0) return; entries.forEach(async (entry, idx) => { let key = entry[0]; let value = entry[1]; const filePath = value.file_path; // /test1.pdf const groupId = value.group_id; const li = document.createElement('li'); li.classList.add('main-file-container'); li.dataset.docId = key; li.dataset.groupId = value.group_id; li.dataset.resourcePath = value.file_path; const listItemWrapper = document.createElement('div'); listItemWrapper.classList.add('official-doc-file-info', 'doc-list-item'); listItemWrapper.dataset.docId = key; // 번호 const numberDiv = document.createElement('div'); numberDiv.classList.add('official-doc-file--number', 'doc-sort-desc'); const numberH6 = document.createElement('h6'); numberH6.textContent = value.rownum; numberDiv.appendChild(numberH6); // 문서번호 const officeNumberDiv = document.createElement('div'); officeNumberDiv.classList.add('official-doc-file--office-number'); const officeNumberH6 = document.createElement('h6'); officeNumberH6.textContent = value.doc_number; officeNumberDiv.appendChild(officeNumberH6); // 날짜 const dateDiv = document.createElement('div'); dateDiv.classList.add('official-doc-file--date'); const dateH6 = document.createElement('h6'); dateH6.textContent = formatDate(value.doc_date); dateDiv.appendChild(dateH6); // 발신처 const fromDiv = document.createElement('div'); fromDiv.classList.add('official-doc-file--from'); const fromH6 = document.createElement('h6'); fromH6.textContent = value.sender_name_abbr; fromDiv.appendChild(fromH6); // 수신처 const toDiv = document.createElement('div'); toDiv.classList.add('official-doc-file--to'); const toH6 = document.createElement('h6'); toH6.textContent = value.recipient_name_abbr; toDiv.appendChild(toH6); // 제목 const titleDiv = document.createElement('div'); titleDiv.classList.add('official-doc-file--title'); const titleH4 = document.createElement('h4'); titleH4.textContent = value.doc_title; titleDiv.appendChild(titleH4); // 상태 const stateDiv = document.createElement('div'); stateDiv.classList.add('official-doc-file--state'); const stateH6 = document.createElement('div'); stateH6.classList.add('official-doc-file--stateText'); const fileExt = filePath.split('.').pop().toLowerCase(); //// 현재 공문파일은 pdf만 받게 되어있음 // ai 연동 if (fileExt === 'pdf') { stateH6.classList.add('viewable'); stateH6.textContent = '열람가능'; } else { stateH6.classList.add('unsupport'); stateH6.textContent = '미지원'; } stateDiv.appendChild(stateH6); // 조립 listItemWrapper.appendChild(numberDiv); listItemWrapper.appendChild(officeNumberDiv); listItemWrapper.appendChild(dateDiv); listItemWrapper.appendChild(fromDiv); listItemWrapper.appendChild(toDiv); listItemWrapper.appendChild(titleDiv); listItemWrapper.appendChild(stateDiv); li.appendChild(listItemWrapper); // border 추가 const border = document.createElement('div'); border.classList.add('official-doc-border'); li.appendChild(border); if (direction) { if (direction == '수신') { recipientList.appendChild(li); recipientList.appendChild(border); } else if (direction == '발신') { sendingList.appendChild(li); sendingList.appendChild(border); } } if (value.doc_direction === '수신') { recipientList.appendChild(li); recipientList.appendChild(border); } else if (value.doc_direction === '발신') { sendingList.appendChild(li); sendingList.appendChild(border); } // 첨부파일 체크 const attachData = docVars.allDocAttachData; let isAttach = false; if (groupId && attachData && attachData.length > 0) { isAttach = attachData.some((attach) => attach.group_id == groupId); } if (isAttach) { // 기존 첨부파일 wrapper 제거 const nextElement = li.nextElementSibling; if (nextElement?.classList.contains('attachment-wrapper')) { nextElement.remove(); } // 첨부파일 목록 필터링 const docFilePath = li.dataset.resourcePath; const attachList = attachData.filter((file) => { const [prefixPath] = file.file_path?.split('__attachment'); return prefixPath === docFilePath; }); // 첨부 없으면 그리지 않음 if (attachList.length === 0) return; const wrapper = document.createElement('div'); wrapper.className = 'attachment-wrapper'; attachList.forEach((file) => { const attachLi = document.createElement('li'); attachLi.className = 'attach-item'; attachLi.dataset.docId = file.doc_id; attachLi.dataset.groupId = file.group_id; attachLi.dataset.resourcePath = file.file_path; attachLi.dataset.fileExt = file.ext; const attachContainer = document.createElement('div'); attachContainer.className = 'attach-item-wrap attach doc-list-item'; attachContainer.dataset.docId = file.doc_id; // 리스트 왼쪽(심볼, 파일명) 그리기 const attachInfo = document.createElement('div'); attachInfo.classList.add('info'); const attachInfoLeft = document.createElement('div'); attachInfoLeft.classList.add('doc-attach--left'); const attachInfoLeftSymbol = document.createElement('span'); attachInfoLeftSymbol.classList.add('symbol'); attachInfoLeftSymbol.innerHTML = '└'; const attachInfoLeftText = document.createElement('span'); const label = file.doc_label; attachInfoLeftText.classList.add(label === 'attach' ? 'text--doc-attach' : 'text--doc-version'); attachInfoLeftText.innerHTML = label === 'attach' ? '첨부' : '버전'; const attachInfoRight = document.createElement('div'); attachInfoRight.classList.add('doc-attach--right'); const attachInfoRightText = document.createElement('h4'); let fileNameString = file.file_path.split('/').pop(); attachInfoRightText.innerHTML = fileNameString; attachInfoRight.appendChild(attachInfoRightText); attachInfoLeft.appendChild(attachInfoLeftSymbol); attachInfoLeft.appendChild(attachInfoLeftText); attachInfo.appendChild(attachInfoLeft); attachInfo.appendChild(attachInfoRight); // 리스트 오른쪽(열람가능, 변환필요) 그리기 let state = document.createElement('div'); state.classList.add('state'); let stateText = document.createElement('div'); stateText.classList.add('state-text'); let convertBtn = document.createElement('div'); convertBtn.classList.add('convert-btn'); convertBtn.dataset.resourcePath = file.file_path; let convertBtnImage = document.createElement('div'); let convertBtnText = document.createElement('div'); convertBtnImage.classList.add('convert-btn-image'); convertBtnText.classList.add('convert-btn-text'); convertBtn.appendChild(convertBtnImage); convertBtn.appendChild(convertBtnText); let addBtn = false; //////////////////////////////////////////////////////////////////////////// let needConvertExtArr = ['hwp', 'hwpx', 'doc', 'docx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'dwg', 'dxf', 'grm']; let notNeedConvertExtArr = ['pdf', 'ifc', 'gsim', 'mp4', 'webm', 'jpg', 'jpeg', 'png', 'txt', 'log', 'md', 'url', 'zip']; let supportedExtArr = [...needConvertExtArr, ...notNeedConvertExtArr]; let isSupported = false, needConvert = false, isConverted = false; let ext = file.ext.toLowerCase(); // 현재 파일 확장자가 지원여부 확장자 배열에 포함되어 있는지 확인 if (supportedExtArr.includes(ext)) isSupported = true; if (needConvertExtArr.includes(ext)) needConvert = true; // 변환이 필요한 확장자일 때 if (needConvert) { let objectKeyFileName = getFileNameFromKey(file.object_key); let previewKeyFileName = getFileNameFromKey(file.preview_key); let popupKeyFileName = getFileNameFromKey(file.popup_key); isConverted = objectKeyFileName === previewKeyFileName && objectKeyFileName === popupKeyFileName; } if (isSupported) { if (needConvert && !isConverted) { // 변환필요 addBtn = true; state.classList.add('convert'); convertBtnText.innerHTML = '변환필요'; } else { // 열람가능 state.classList.add('viewable'); stateText.innerHTML = '열람가능'; } } else { // 미지원 state.classList.add('unsupport'); stateText.innerHTML = '미지원'; } state.appendChild(stateText); if (addBtn) state.appendChild(convertBtn); attachContainer.appendChild(attachInfo); attachContainer.appendChild(state); attachLi.appendChild(attachContainer); wrapper.appendChild(attachLi); // 클릭 이벤트 attachLi.addEventListener('click', (e) => { resetViewer(); renderDocViewer(file.file_path, file.doc_id); docVars.lastClickedListTarget = e.target; docVars.currentDocId = file.doc_id; docVars.selectedDoc = attachLi; // 리스트 선택 요소 selected 클래스 지우기 itemBoxSelected(attachLi); }); // 오른쪽 클릭: 컨텍스트 메뉴 attachLi.addEventListener('contextmenu', (e) => { e.preventDefault(); resetViewer(); renderDocViewer(file.file_path, file.doc_id); docVars.lastClickedListTarget = e.target; docVars.currentDocId = file.doc_id; docVars.selectedDoc = attachLi; toggleDocContextmenu('.official-doc-contextmenu.attach', true, e); // 리스트 선택 요소 selected 클래스 지우기 itemBoxSelected(attachLi); }); // 컨버트버튼 클릭 이벤트 convertBtn.addEventListener('click', (e) => { e.stopPropagation(); docVars.lastClickedListTarget = e.target; convertDocPdf(file.file_path, file.doc_id); }); if (docVars.lastClickedListTarget) { //리스트(부모), 첨부리스트(자식) let target = docVars.lastClickedListTarget if (!target.classList.contains('doc-list-item')) { target = target.closest('.doc-list-item') } if (target.classList.contains('item-selected')) { target.classList.remove('item-selected') } if (!target.classList.contains('attach')) { if (target.classList.contains('attach-item-wrap')) return; let targetList = document.querySelector(`.official-doc-file-info.doc-list-item[data-doc-id="${target.dataset.docId}"]`) if (!targetList) return; targetList.classList.add('item-selected') } } }); // 리스트 최종 조립 li.appendChild(wrapper); const nearestFileInfo = li.closest('li')?.querySelector('.official-doc-file-info'); if (!nearestFileInfo) return; nearestFileInfo.parentNode.insertBefore(wrapper, nearestFileInfo.nextSibling); const docId = docVars.lastClickedListTarget?.dataset?.docId; if (docId) { const targetList = document.querySelector(`.attach-item-wrap.doc-list-item[data-doc-id="${docId}"]`); targetList?.classList.add('item-selected'); } } // 첨부파일 끝 const docListItem = li.querySelector('.doc-list-item'); // 왼쪽 클릭시 리스트 색칠되고 뷰어 띄우기 docListItem.addEventListener('click', async (e) => { e.preventDefault(); itemBoxSelected(li); docVars.lastClickedListTarget = e.target; docVars.selectedDoc = value; docVars.currentDocId = key; renderDocViewer(value.file_path, key); }); // 오른쪽 클릭시 컨텍스트메뉴 띄우고 리스트 색칠되고 뷰어 띄우기 docListItem.addEventListener('contextmenu', (e) => { e.preventDefault(); itemBoxSelected(li); docVars.lastClickedListTarget = e.target; docVars.lastContextTarget = e.target; docVars.selectedDoc = value; docVars.currentDocId = key; renderDocViewer(value.file_path, key); if (!document.querySelector('.official-doc-contextmenu.main')) return; toggleDocContextmenu('.official-doc-contextmenu.main', true, e); }); if (docVars.lastClickedListTarget) { // 지금 만들어진 리스트 요소 아이디 const listItemId = docVars.lastClickedListTarget.closest(`li.main-file-container`).dataset.docId; // 마지막으로 선택한 리스트 아이디 const lastClickId = docListItem.querySelector('.official-doc-file--title').closest('li.main-file-container').dataset.docId; if (lastClickId == listItemId) { if (!isAttach) { docListItem.classList.add('item-selected'); } docListItem.parentNode.classList.add('box-selected'); } } }) // 리스트 영역 외부 클릭 시 docVars.selectedDoc null 설정 및 뷰어 초기화 fileContainer?.addEventListener('click', (e) => { const topHeader = document.querySelector('.official-doc-main .official-doc-list-container .official-doc-main-body .official-doc-top .official-doc-list'); const bottomHeader = document.querySelector('.official-doc-main .official-doc-list-container .official-doc-main-body .official-doc-bottom .official-doc-list'); if(e.target.parentElement == topHeader || e.target.parentElement.parentElement == topHeader) return; if(e.target.parentElement == bottomHeader || e.target.parentElement.parentElement == bottomHeader) return; clearMetadata(); resetViewer(); const clickedInsideList = e.target.closest('li.main-file-container'); const isInContextMenu = e.target.closest('.official-doc-contextmenu'); if (!clickedInsideList && !isInContextMenu) { docVars.selectedDoc = null; docVars.lastClickedListTarget = null; docVars.itemSelected = null; docVars.boxSelected = null; document.querySelectorAll('.doc-list-item.item-selected').forEach((el) => el.classList.remove('item-selected')); document.querySelectorAll('.main-file-container.box-selected').forEach((el) => el.classList.remove('box-selected')); previewNotice.style.display = 'flex'; previewContainer.style.display = 'none'; } }); } // 공문 미리보기 하단 메타데이터 표시 function createMetadataItem(keyText, itemClass = "", valueText = "") { const item = document.createElement('div'); item.classList.add('item', itemClass); const key = document.createElement('div'); key.classList.add('key', 'ft-12'); key.innerHTML = keyText.split('').map(text => `${text}`).join(''); const valueWrap = document.createElement('div'); valueWrap.classList.add('value-wrap'); let value; value = document.createElement('div'); value.classList.add('value', 'ft-14', 'scrollbar'); value.textContent = valueText; valueWrap.appendChild(value); item.appendChild(key); item.appendChild(valueWrap); return item; } async function viewerMetadata(doc) { const metadataWrap = document.createElement('div'); metadataWrap.classList.add('metadata-item-wrap'); const line1 = document.createElement('div'); line1.classList.add('wrap', 'line1'); line1.appendChild(createMetadataItem('제목요약', 'doc-title-summary', doc.doc_title_summary)); metadataWrap.appendChild(line1); const line2 = document.createElement('div'); line2.classList.add('wrap', 'line2'); line2.appendChild(createMetadataItem('공문번호', 'doc-number', doc.doc_number)); line2.appendChild(createMetadataItem('연관공문', 'doc-related-docs', doc.doc_related_docs)); line2.appendChild(createMetadataItem('종류', 'doc-category', doc.doc_category)); metadataWrap.appendChild(line2); const line3 = document.createElement('div'); line3.classList.add('wrap', 'line3'); line3.appendChild(createMetadataItem('내용요약', 'doc-content-summary', doc.doc_content_summary)); metadataWrap.appendChild(line3); const line4 = document.createElement('div'); line4.classList.add('wrap', 'line4'); line4.appendChild(createMetadataItem('특이사항', 'doc-memo', doc.doc_memo || '-')); metadataWrap.appendChild(line4); metadataContainer.appendChild(metadataWrap); } function clearMetadata() { if (metadataContainer) { metadataContainer.innerHTML = ''; } } // 메타데이터 뷰어 토글 infoWrap?.querySelector('.separator .toggle-btn').addEventListener('click', async (e) => { infoWrap.classList.toggle('open'); infoWrap.classList.toggle('close'); }) // 리스트 색칠하기 export function itemBoxSelected(li) { clearMetadata(); resetViewer(); recipientList.querySelectorAll('li').forEach((li) => li.classList.remove('box-selected')); sendingList.querySelectorAll('li').forEach((li) => li.classList.remove('box-selected')); const mainFileContainer = li.closest('.main-file-container'); mainFileContainer.classList.add('box-selected'); recipientList.querySelectorAll('.doc-list-item').forEach((item) => item.classList.remove('item-selected')); sendingList.querySelectorAll('.doc-list-item').forEach((item) => item.classList.remove('item-selected')); const listItem = li.querySelector('.doc-list-item'); listItem.classList.add('item-selected'); } export async function renderDocViewer(resourcePath, docId) { clearMetadata(); resetViewer(); await syncDocInfo(['official', 'attach', null]); previewNotice.style.display = 'none'; previewContainer.style.display = 'flex'; viewerWrap.style.display = 'flex'; totalviewBtn.style.display = 'flex'; docVars.viewer = viewerWrap.querySelector('.viewer'); if (resourcePath.includes('__attach')) { infoWrap.style.display = 'none'; } else { infoWrap.style.display = 'flex'; await viewerMetadata(docVars.allDocData?.find((doc) => doc.doc_id === docId)); } // fallback-pdf-btn 숨김 const docFallbackPdfBtn = document.getElementById('doc-fallback-pdf-btn'); if (docFallbackPdfBtn) { docFallbackPdfBtn.style.display = 'none'; } let ext = splitBaseAndExt(resourcePath).ext.toLowerCase(); let excelDirectArr = ['xls', 'xlsx', 'xlsm']; let hwpDirectArr = ['hwp', 'hwpx']; let wordDirectArr = ['docx']; let pptxDirectArr = []; let isDirectView = excelDirectArr.includes(ext) || hwpDirectArr.includes(ext) || wordDirectArr.includes(ext) || pptxDirectArr.includes(ext); let selectedDoc = docVars.allDocData?.find((doc) => doc.doc_id === docId); let previewKey = selectedDoc?.preview_key; let objectKey = selectedDoc?.object_key; let targetKey = isDirectView ? objectKey : previewKey; //Presigned URL let PresignedUrl = undefined; if (targetKey == undefined || targetKey == `` || targetKey == null) { let supportArr = ['hwp', 'hwpx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'doc', 'docx', 'dwg', 'dxf']; if (!supportArr.includes(ext)) { totalviewBtn.style.display = 'none'; viewerUnsupport(ext); } else { viewerConvert(resourcePath); } return; } let generateDownloadUrlParams = { objectKey: targetKey, resourcePath: resourcePath, }; let generateDownloadUrlRes = await axios.post(`${docVars.path_name}/generateDownloadDocUrl`, generateDownloadUrlParams); if (generateDownloadUrlRes.data.message == 'generateDownloadDocUrl_success') { PresignedUrl = generateDownloadUrlRes.data.url; } //Presigned URL end let pdfArr = ['pdf', 'hwp', 'hwpx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'doc', 'docx', 'dwg', 'dxf']; let gsimArr = ['gsim']; let ifcArr = ['ifc']; let imageArr = ['png', 'jpg', 'jpeg']; let videoArr = ['mp4', 'webm']; let textArr = ['txt', 'log']; let urlArr = ['url']; let zipArr = ['zip']; let threeArr = ['glb', 'gltf', 'obj', 'stl', 'fbx', '3dm']; let allArr = [...pdfArr, ...gsimArr, ...ifcArr, ...imageArr, ...videoArr, ...textArr, ...urlArr, ...zipArr, ...threeArr]; if (allArr.includes(ext)) { let pdfArrFiltered = pdfArr.filter(e => !excelDirectArr.includes(e) && !hwpDirectArr.includes(e) && !wordDirectArr.includes(e) && !pptxDirectArr.includes(e)); if (pdfArrFiltered.includes(ext)) viewerPdf(PresignedUrl); if (excelDirectArr.includes(ext)) viewerExcel(PresignedUrl); if (hwpDirectArr.includes(ext)) viewerHwp(PresignedUrl); if (wordDirectArr.includes(ext)) viewerWord(PresignedUrl); if (pptxDirectArr.includes(ext)) viewerPptx(PresignedUrl); if (gsimArr.includes(ext)) viewerGsim(PresignedUrl); if (ifcArr.includes(ext)) viewerIfc(PresignedUrl); if (threeArr.includes(ext)) viewer3d(PresignedUrl); if (imageArr.includes(ext)) viewerImage(PresignedUrl); if (videoArr.includes(ext)) viewerVideo(PresignedUrl); if (textArr.includes(ext)) viewerText(PresignedUrl); if (urlArr.includes(ext)) viewerURL(PresignedUrl); if (zipArr.includes(ext)) viewerZIP(PresignedUrl); } else { viewerUnsupport(ext); } function viewerUnsupport(ext) { resetViewer(); let viewerUnsupportWrap = document.createElement('div'); viewerUnsupportWrap.classList.add('viewer-unsupport-wrap'); let text = document.createElement('div'); text.classList.add('text'); text.innerText = `${ext} 파일 형식은 현재 지원되지 않습니다.`; viewerUnsupportWrap.appendChild(text); docVars.viewer.appendChild(viewerUnsupportWrap); docVars.viewer.dataset.viewerType = 'unsupport'; } function viewerConvert() { let viewerConvertWrap = document.createElement('div'); viewerConvertWrap.classList.add('viewer-convert-wrap'); let text = document.createElement('div'); text.classList.add('text'); text.innerText = `해당 파일은 변환 후 열람이 가능합니다.`; viewerConvertWrap.appendChild(text); docVars.viewer.appendChild(viewerConvertWrap); docVars.viewer.dataset.viewerType = 'convert'; } // ----------------------------------------------------------------- // 오픈소스 문서 직접 뷰잉 및 PDF 폴백 함수 정의 (Doc Viewer) // ----------------------------------------------------------------- function initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey) { const btn = document.getElementById('doc-fallback-pdf-btn'); if (!btn) return; // 이전 등록된 리스너 제거를 위해 복사 대체 const newBtn = btn.cloneNode(true); btn.parentNode.replaceChild(newBtn, btn); newBtn.style.display = 'flex'; newBtn.querySelector('.text').textContent = 'PDF로 보기'; newBtn.style.pointerEvents = 'auto'; newBtn.addEventListener('click', async () => { newBtn.querySelector('.text').textContent = '로딩 중...'; newBtn.style.pointerEvents = 'none'; try { // 1. 최신 메타데이터 (preview_key) 조회 if (!previewKey) { await syncDocInfo(['official', 'attach', null]); let selectedDoc = docVars.allDocData?.find((doc) => doc.doc_id === docId); previewKey = selectedDoc?.preview_key; objectKey = selectedDoc?.object_key; } // 2. 만약 PDF 변환본이 아직 없다면 백엔드 변환 요청 if (!previewKey) { newBtn.querySelector('.text').textContent = 'PDF 변환 요청 중...'; await convertDocPdf(resourcePath, docId); alert('서버 측 PDF 변환이 시작되었습니다. 잠시 후 다시 클릭해 주세요.'); newBtn.querySelector('.text').textContent = 'PDF로 보기'; newBtn.style.pointerEvents = 'auto'; return; } // 3. PDF용 Presigned URL 생성 let generateDownloadUrlParams = { objectKey: previewKey, resourcePath: resourcePath } let generateDownloadUrlRes = await axios.post(`${docVars.path_name}/generateDownloadDocUrl`, generateDownloadUrlParams); if (generateDownloadUrlRes.data.message == 'generateDownloadDocUrl_success') { let pdfUrl = generateDownloadUrlRes.data.url; // 화면 초기화 및 PDF 뷰어 로드 docVars.viewer = viewerWrap.querySelector('.viewer'); docVars.viewer.innerHTML = ''; newBtn.style.display = 'none'; viewerPdf(pdfUrl); } else { alert('PDF 미리보기 주소 획득에 실패했습니다.'); newBtn.querySelector('.text').textContent = 'PDF로 보기'; newBtn.style.pointerEvents = 'auto'; } } catch (err) { console.error(err); alert('PDF 변환 및 조회 중 오류가 발생했습니다.'); newBtn.querySelector('.text').textContent = 'PDF로 보기'; newBtn.style.pointerEvents = 'auto'; } }); } function viewerExcel(presignedUrl) { docVars.viewer.innerHTML = '
엑셀 데이터를 불러오는 중...
'; initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); fetch(presignedUrl) .then(res => { if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); return res.arrayBuffer(); }) .then(arrayBuffer => { docVars.viewer.innerHTML = ''; LuckyExcel.transformExcelToLucky(arrayBuffer, function(exportJson, luckysheetfile) { if(exportJson.sheets == null || exportJson.sheets.length == 0) { docVars.viewer.innerHTML = '
엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)
'; initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); return; } if (window.luckysheet) { window.luckysheet.destroy(); } docVars.viewer.style.position = 'relative'; const container = document.createElement('div'); container.id = 'luckysheet_inner_doc'; container.style.margin = '0px'; container.style.padding = '0px'; container.style.position = 'absolute'; container.style.width = '100%'; container.style.height = '100%'; container.style.left = '0px'; container.style.top = '0px'; docVars.viewer.appendChild(container); try { window.luckysheet.create({ container: 'luckysheet_inner_doc', data: exportJson.sheets, title: exportJson.info.name || 'Excel Viewer', lang: 'en', showinfobar: false, myFolderUrl: 'javascript:void(0)' }); } catch (createErr) { console.error("Luckysheet create error: ", createErr); docVars.viewer.innerHTML = `
엑셀 시트 생성 중 오류가 발생했습니다.
에러: ${createErr.message}
`; } }, function(err) { console.error("Luckysheet transform error: ", err); docVars.viewer.innerHTML = `
엑셀 파일을 읽는 중 오류가 발생했습니다.
상세: ${err.message || err}
`; initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); }); }) .catch(err => { console.error(err); docVars.viewer.innerHTML = `
엑셀 파일을 불러오는데 실패했습니다.
에러: ${err.message}
`; initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); }); docVars.viewer.dataset.viewerType = 'excel'; } function viewerWord(presignedUrl) { docVars.viewer.innerHTML = '
워드 문서를 불러오는 중...
'; initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); fetch(presignedUrl) .then(res => { if (!res.ok) throw new Error('Word fetch failed'); return res.arrayBuffer(); }) .then(arrayBuffer => { docVars.viewer.innerHTML = ''; const container = document.createElement('div'); container.style.width = '100%'; container.style.height = '100%'; container.style.overflow = 'auto'; container.style.padding = '20px'; container.style.boxSizing = 'border-box'; container.style.background = '#f5f5f5'; const docxInner = document.createElement('div'); docxInner.style.background = '#ffffff'; docxInner.style.margin = '0 auto'; docxInner.style.maxWidth = '800px'; docxInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)'; docxInner.style.padding = '40px'; container.appendChild(docxInner); docVars.viewer.appendChild(container); docx.renderAsync(arrayBuffer, docxInner) .then(() => console.log("docx rendered")) .catch(err => { console.error(err); docxInner.innerHTML = '
워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; }); }) .catch(err => { console.error(err); docVars.viewer.innerHTML = '
워드 문서를 불러오는데 실패했습니다.
'; }); docVars.viewer.dataset.viewerType = 'word'; } function viewerHwp(presignedUrl) { docVars.viewer.innerHTML = '
한글 문서를 불러오는 중...
'; initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); fetch(presignedUrl) .then(res => { if (!res.ok) throw new Error('HWP fetch failed'); return res.blob(); }) .then(blob => { docVars.viewer.innerHTML = ''; const container = document.createElement('div'); container.style.width = '100%'; container.style.height = '100%'; container.style.overflowX = 'auto'; 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; width: max-content; min-width: 800px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); padding: 0 !important; box-sizing: border-box !important; min-height: 100%; } .hwp-inner-container img { max-width: 100% !important; height: auto !important; } `; container.appendChild(styleEl); const hwpInner = document.createElement('div'); hwpInner.classList.add('hwp-inner-container'); container.appendChild(hwpInner); docVars.viewer.appendChild(container); const reader = new FileReader(); reader.onload = (e) => { const bstr = e.target.result; try { new hwp.Viewer(hwpInner, bstr); } catch (err) { console.error("hwp.js error: ", err); hwpInner.innerHTML = '
한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; } }; reader.readAsBinaryString(blob); }) .catch(err => { console.error(err); docVars.viewer.innerHTML = '
한글 문서를 불러오는데 실패했습니다.
'; }); docVars.viewer.dataset.viewerType = 'hwp'; } function viewerPptx(presignedUrl) { docVars.viewer.innerHTML = '
PPTX 문서를 불러오는 중...
'; initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey); fetch(presignedUrl) .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; }); docVars.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'; docVars.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); docVars.viewer.innerHTML = '
PPTX 파싱 중 에러가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.
'; } }) .catch(err => { console.error(err); docVars.viewer.innerHTML = '
PPTX 문서를 불러오는데 실패했습니다.
'; }); docVars.viewer.dataset.viewerType = 'pptx'; } async function viewerPdf(PresignedUrl) { resetViewer(); // iframe 초기화 const existingIframe = docVars.viewer.querySelector('iframe'); if (existingIframe) { existingIframe.remove(); } let iframe = document.createElement('iframe'); iframe.src = `/libs/pdfViewer/web/viewer.html`; let pdfOptions = { url: PresignedUrl, initialPage: 1, }; 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('.official-doc-preview--container .viewer-wrap .viewer iframe').contentWindow.PDFViewerApplication; app.pdfCursorTools._handTool.activate(); app.open(pdfOptions); app.appConfig.mainContainer.classList.add('scrollbar'); app.appConfig.mainContainer.style.marginLeft = '4px'; }); docVars.viewer.appendChild(iframe); docVars.viewer.dataset.viewerType = 'pdf'; } function viewerImage(PresignedUrl) { resetViewer(); let viewerImageWrap = document.createElement('div'); viewerImageWrap.classList.add('viewer-image-wrap'); let viewerImage = document.createElement('img'); viewerImage.classList.add('viewer-image'); viewerImage.src = PresignedUrl; viewerImageWrap.appendChild(viewerImage); docVars.viewer.appendChild(viewerImageWrap); docVars.viewer.dataset.viewerType = 'image'; viewerImage.addEventListener('load', function () { const resizeObserver = new ResizeObserver((entries) => { for (let entry of entries) { let { width, height } = entry.contentRect; if (height > width) viewerImage.style.height = `${width}px`; resizeObserver.disconnect(); // 측정 후 observer 해제 } }); resizeObserver.observe(viewerImageWrap); }); } function viewerVideo(PresignedUrl) { resetViewer(); let viewerVideoWrap = document.createElement('div'); viewerVideoWrap.classList.add('viewer-video-wrap'); let viewerVideo = document.createElement('video'); viewerVideo.classList.add('viewer-video'); viewerVideo.autoplay = true; viewerVideo.muted = true; viewerVideo.playsInline = true; viewerVideo.controls = true; viewerVideo.crossOrigin = 'anonymous'; let sourceElement = document.createElement('source'); sourceElement.src = PresignedUrl; sourceElement.type = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; viewerVideo.appendChild(sourceElement); viewerVideoWrap.appendChild(viewerVideo); docVars.viewer.appendChild(viewerVideoWrap); docVars.viewer.dataset.viewerType = 'video'; } function viewerText(PresignedUrl) { resetViewer(); let viewerTextWrap = document.createElement('div'); viewerTextWrap.classList.add('viewer-text-wrap'); viewerTextWrap.classList.add('scrollbar'); fetch(PresignedUrl) .then((res) => res.text()) .then((data) => { let viewerText = document.createElement('div'); viewerText.classList.add('viewer-text'); viewerText.textContent = data; viewerTextWrap.appendChild(viewerText); docVars.viewer.appendChild(viewerTextWrap); docVars.viewer.dataset.viewerType = 'text'; }); } function viewerGsim(PresignedUrl) { resetViewer(); let viewerGsim = document.createElement('div'); viewerGsim.classList.add('viewer-gsim'); let iframe = document.createElement('iframe'); iframe.onload = () => { iframe.contentWindow.postMessage({ path: PresignedUrl }, '*'); // path 값을 iframe에 전달 }; iframe.src = `/libs/gsimViewer/gsimViewer.html?path=${encodeURIComponent(PresignedUrl)}`; viewerGsim.appendChild(iframe); docVars.viewer.appendChild(viewerGsim); docVars.viewer.dataset.viewerType = 'gsim'; } function viewerIfc(PresignedUrl) { resetViewer(); let viewerIfc = document.createElement('div'); viewerIfc.classList.add('viewer-ifc'); let iframe = document.createElement('iframe'); iframe.onload = () => { iframe.contentWindow.postMessage({ path: PresignedUrl }, '*'); // path 값을 iframe에 전달 }; iframe.src = `/libs/ifcViewer/index.html`; iframe.width = '100%'; iframe.height = '100%'; iframe.style.border = 'none'; viewerIfc.appendChild(iframe); docVars.viewer.appendChild(viewerIfc); docVars.viewer.dataset.viewerType = 'ifc'; } function viewer3d(PresignedUrl) { resetViewer(); let viewer3d = document.createElement('div'); viewer3d.classList.add('viewer-3d'); let iframe = document.createElement('iframe'); iframe.onload = () => { iframe.contentWindow.postMessage({ path: PresignedUrl }, '*'); // path 값을 iframe에 전달 }; iframe.src = `/libs/3dViewer/index.html`; iframe.width = '100%'; iframe.height = '100%'; iframe.style.border = 'none'; viewer3d.appendChild(iframe); docVars.viewer.appendChild(viewer3d); docVars.viewer.dataset.viewerType = '3d'; } function viewerURL(PresignedUrl) { resetViewer(); let viewerURLWrap = document.createElement('div'); viewerURLWrap.classList.add('viewer-text-wrap'); fetch(PresignedUrl) .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'; // 테두리 제거 (선택 사항) viewerURLWrap.appendChild(iframe); docVars.viewer.appendChild(viewerURLWrap); docVars.viewer.dataset.viewerType = 'url'; }); } function viewerZIP(PresignedUrl) { resetViewer(); let viewerTextWrap = document.createElement('div'); viewerTextWrap.classList.add('viewer-text-wrap'); viewerTextWrap.classList.add('scrollbar'); fetch(PresignedUrl).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`; } } }); let viewerText = document.createElement('div'); viewerText.classList.add('viewer-text'); viewerText.textContent = `${folderText}${folderText == `` ? '' : '\n'}${fileText}`; viewerTextWrap.appendChild(viewerText); docVars.viewer.appendChild(viewerTextWrap); docVars.viewer.dataset.viewerType = 'text'; }); } } function resetViewer() { if (!docVars.viewer) return; if (docVars.viewer.dataset.viewerType == 'unsupport') { const unsupportWrap = docVars.viewer.querySelector('.viewer-unsupport-wrap'); if (unsupportWrap) { unsupportWrap.remove(); } } if (docVars.viewer.dataset.viewerType == 'convert') { const convertWrap = docVars.viewer.querySelector('.viewer-convert-wrap'); if (convertWrap) { convertWrap.remove(); } } if (docVars.viewer.dataset.viewerType == 'pdf') { const iframe = docVars.viewer.querySelector('.viewer iframe');; if (iframe) { const app = iframe.contentWindow?.PDFViewerApplication; app?.close?.(); iframe.remove(); } } if (docVars.viewer.dataset.viewerType == 'image') { const imageWrap = docVars.viewer.querySelector('.viewer-image-wrap'); if (imageWrap) { const image = imageWrap.querySelector('.viewer-image'); image.src = ''; image.remove(); imageWrap.remove(); } } if (docVars.viewer.dataset.viewerType == 'video') { const videoWrap = docVars.viewer.querySelector('.viewer-video-wrap'); if (videoWrap) { const video = docVars.viewer.querySelector('.viewer-video'); video.pause(); video.src = ''; video.load(); video.remove(); videoWrap.remove(); } } if (docVars.viewer.dataset.viewerType == 'text') { const textWrap = docVars.viewer.querySelector('.viewer-text-wrap'); if (textWrap) { textWrap.remove(); } } if (docVars.viewer.dataset.viewerType == 'ifc') { const viewerIfc = docVars.viewer.querySelector('.viewer-ifc'); if (viewerIfc) { viewerIfc.remove(); } } if (docVars.viewer.dataset.viewerType == 'gsim') { const viewerGsim = docVars.viewer.querySelector('.viewer-gsim'); if (viewerGsim) { viewerGsim.remove(); } } if (docVars.viewer.dataset.viewerType == 'url') { const textWrap = docVars.viewer.querySelector('.viewer-text-wrap'); if (textWrap) { textWrap.remove(); } } if (docVars.viewer.dataset.viewerType == 'excel') { if (window.luckysheet) { try { window.luckysheet.destroy(); } catch (e) { console.error("Luckysheet destroy error: ", e); } } } docVars.viewer.dataset.viewerType = ''; } // 우측 미리보기 새 창으로 열기 버튼 클릭 이벤트 document.querySelector('.official-doc-main .official-doc-preview .official-doc-preview--container .viewer-header .wrap .btn')?.addEventListener('click', async () => { let resourcePath = docVars.lastClickedListTarget.closest('li').dataset.resourcePath; let docId = docVars.lastClickedListTarget.closest('li').dataset.docId; let isLowerExt = true, ext = splitBaseAndExt(resourcePath, isLowerExt).ext; //Presigned URL let PresignedUrl; await syncDocInfo(['official', 'attach', null]); let directViewExtArr = ['xls', 'xlsx', 'xlsm', 'docx', 'hwp', 'hwpx']; let selectedDoc = docVars.allDocData?.find((doc) => doc.doc_id === docId); let objectKey = directViewExtArr.includes(ext) ? selectedDoc?.object_key : selectedDoc?.preview_key; if (objectKey == undefined || objectKey == `` || objectKey == null) { return; } let generateDownloadUrlParams = { objectKey: objectKey, resourcePath: resourcePath, }; let generateDownloadUrlRes = await axios.post(`${docVars.path_name}/generateDownloadDocUrl`, generateDownloadUrlParams); if (generateDownloadUrlRes.data.message == 'generateDownloadDocUrl_success') { PresignedUrl = generateDownloadUrlRes.data.url; } //Presigned URL end // 뷰어 기본 width값은 현재 화면 width값의 절반 let denominator = 2; // url 뷰어는 현재 화면 width값과 동일하게 설정 if (ext == 'url') denominator = 1; let width = screen.width / denominator; let height = screen.height; let left = screen.width * 2 + 10; let fullPath = encodeURIComponent(PresignedUrl); let open_ext = `pdf`; switch (ext) { case 'pdf': case 'ppt': case 'pptx': case 'doc': case 'dwg': case 'dxf': case 'grm': open_ext = 'pdf'; break; case 'hwp': case 'hwpx': open_ext = ext; break; case 'xls': case 'xlsx': case 'xlsm': open_ext = ext; break; case 'docx': open_ext = ext; break; case 'gsim': open_ext = 'gsim'; break; case 'ifc': open_ext = 'ifc'; break; case 'png': case 'jpg': case 'jpeg': open_ext = 'png'; break; case 'mp4': open_ext = 'mp4'; break; case 'log': case 'txt': case 'md': open_ext = 'txt'; break; case 'url': open_ext = 'url'; break; case 'zip': open_ext = 'zip'; break; } //presigned url은 ext를 읽을수 없엉... const jsonData = JSON.stringify({ $ext: open_ext }); const encodedData = btoa(jsonData); let popup = window.open(`/popup?path=${fullPath}&data=${encodedData}`, '', `width=${width}, height=${height}, left=${left}`); }); /*******************************************************************************************************************************/ /******************************************************************************************************************* 셀렉트 박스 */ /*******************************************************************************************************************************/ // depth1: 발주처/발주처외 // depth2: 기준(SY_JV, 삼안) // depth3: 상대기관(DPWH, 서영, 진우 ...) // depth1은 화면 메인 리스트 필터링에서 사용 + depth2, depth3 // depth2는 추가모달에서 사용 + depth3 // depth1 또는 depth2를 선택하면 뒤에 셀렉트박스의 첫번째 옵션값들이 자동으로 입력되게 /** * 1. 셀렉트박스에서 발주처를 선택했을 떄 기준SY_JV 과 상대기관DPWH 가 나오는 함수 (화면메인리스트) * * @param {*} selectedDepth1 * @example const { 기준, 상대기관 } = getValuesFromDepth1('발주처'); * @returns - { 기준: ['SY_JV'], 상대기관: ['DPWH'] } */ function getValuesFromDepth1(selectedDepth1) { const group = docVars.groupCompanyData[selectedDepth1]; if (!group) return { 기준: [], 상대기관: [] }; const extractNames = (list) => list.map((item) => item.name); return { 기준: extractNames(group.기준), 상대기관: extractNames(group.상대기관), }; } /** * 2. 셀렉트박스에서 depth2을 선택했을 때 depth1에 따라 depth3이 나오는 함수 (화면메인리스트, 추가모달) * * @param {*} selectedBase * @example const { depth1, 상대기관 } = getValuesFromBase('삼안'); * @returns - { depth1: '발주처외', 상대기관: (4) ['서영', '진우', '경동', '한국수출입은행'] } */ function getValuesFromBase(selectedBase) { for (const [depth1Key, group] of Object.entries(docVars.groupCompanyData)) { if (group.기준.includes(selectedBase)) { return { depth1: depth1Key, 상대기관: group.상대기관, }; } } return { depth1: null, 상대기관: [] }; } /** * 3. 셀렉트박스에서 depth3을 선택했을 때 depth1에 따라 depth2만 나오는 함수 (추가모달) * * @param {*} selectedTarget * @example const 기준 = getBaseFromTarget('서영'); * @returns - { 기준: '삼안' } */ function getBaseFromTarget(selectedTarget) { for (const group of Object.values(docVars.groupCompanyData)) { if (group.상대기관.includes(selectedTarget)) { return group.기준[0]; } } return null; } export async function initCustomSelectBoxes() { if (checkProjectInactive()) return; await getGroupCompanyData(); const selectDOM = { type: document.querySelector('#doc-select-type'), base: document.querySelector('#doc-select-base'), target: document.querySelector('#doc-select-target'), }; const updateSelectOptions = (wrapper, values, selectedValue, paramKey) => { const optionsContainer = wrapper.querySelector('.options'); const selectedLabel = wrapper.querySelector('.selected'); optionsContainer.innerHTML = ''; // 기본 텍스트 const defaultLabelMap = { typeOptions: '구분', baseOptions: '기준', targetOptions: '상대기관', }; const labelText = selectedValue || defaultLabelMap[paramKey] || '선택'; selectedLabel.innerText = labelText; docVars.selectParams[paramKey] = selectedValue || ''; // console.log(`[${paramKey}] 변경됨 →`, docVars.selectParams); values.forEach((value) => { const option = document.createElement('div'); option.classList.add('option'); // value가 오브젝트라면 const name = typeof value === 'object' ? value.name : value; const id = typeof value === 'object' ? value.id : ''; option.dataset.value = name; option.dataset.id = id; option.innerText = name; option.addEventListener('click', () => { selectedLabel.innerText = name; docVars.selectParams[paramKey] = name; if (paramKey === 'typeOptions') { const { 기준, 상대기관 } = getValuesFromDepth1(name); updateSelectOptions(selectDOM.base, 기준, 기준[0], 'baseOptions'); updateSelectOptions(selectDOM.target, 상대기관, 상대기관[0], 'targetOptions'); } else if (paramKey === 'baseOptions') { const { depth1, 상대기관 } = getValuesFromBase(name); if (depth1) { docVars.selectParams.typeOptions = depth1; selectDOM.type.querySelector('.selected').innerText = depth1; } updateSelectOptions(selectDOM.target, 상대기관, 상대기관[0], 'targetOptions'); } getDocDataBySelected(); }); optionsContainer.appendChild(option); }); }; // 초기화: 첫 번째 항목 기준 const firstType = Object.keys(docVars.groupCompanyData)[0]; const { 기준: firstBaseList, 상대기관: firstTargetList } = getValuesFromDepth1(firstType); updateSelectOptions(selectDOM.type, Object.keys(docVars.groupCompanyData), firstType, 'typeOptions'); updateSelectOptions(selectDOM.base, firstBaseList, firstBaseList[0], 'baseOptions'); updateSelectOptions(selectDOM.target, firstTargetList, firstTargetList[0], 'targetOptions'); } // 셀렉트박스 클릭 열기/닫기 기능 export function activateSelectUI() { document.querySelectorAll('.doc-custom-select').forEach((select) => { select.addEventListener('click', function () { this.classList.toggle('open'); }); }); document.addEventListener('click', (e) => { document.querySelectorAll('.doc-custom-select').forEach((select) => { if (!select.contains(e.target)) { select.classList.remove('open'); } }); }); } // 날짜 포맷 function formatDate(dateStr) { const date = new Date(dateStr); if (isNaN(date.getTime())) { // 날짜 형식이 아닌 경우 return 'ㅡ'; } const yy = String(date.getFullYear()).slice(2); const mm = String(date.getMonth() + 1).padStart(2, '0'); const dd = String(date.getDate()).padStart(2, '0'); return `${yy}. ${mm}. ${dd}.`; } function getFileNameFromKey(key) { let keySplit1 = key?.split('/')[key.split('/').length - 1]; let keySplit2 = keySplit1?.split('__')[0]; let keySplit3 = keySplit2?.split('.'); keySplit3?.pop(); let fileName = keySplit3?.join('.'); return fileName; } /*******************************************************************************************************************************/ /****************************************************************************************************************** 컨텍스트메뉴 */ /*******************************************************************************************************************************/ // 컨텍스트메뉴 state false main?.addEventListener('click', (e) => { toggleDocContextmenu('.official-doc-contextmenu', false); }); main?.addEventListener('wheel', (e) => { toggleDocContextmenu('.official-doc-contextmenu', false); }); document.querySelectorAll('.scroll-container')?.forEach((scrollContainer) => { scrollContainer.addEventListener('scroll', async (e) => { toggleDocContextmenu('.official-doc-contextmenu', false); }); }); // 컨텍스트 메뉴 열기 export function toggleDocContextmenu(targetSelector, state, event) { // targetSelector 공문(.main)과 첨부(.attach)를 구분하기 위함 // state는 t, f값 const allMenus = document.querySelectorAll('.official-doc-contextmenu'); const contextBox = document.querySelector(targetSelector); if (!contextBox) return; // 열려있는 컨텍스트메뉴 닫기 allMenus.forEach((menu) => { menu.style.display = 'none'; }); if (state == false) { contextBox.style.display = 'none'; } if (state == true) { let showContextMenu = true; if (showContextMenu) { contextBox.style.display = 'flex'; contextBox.style.left = event.clientX + 'px'; contextBox.style.top = event.clientY - 15 - document.querySelector('.banner-notice-area')?.clientHeight + 'px'; const contextMenuHeight = contextBox.offsetHeight; const remainingSpaceBelow = window.innerHeight - event.clientY - 15 - document.querySelector('.banner-notice-area')?.clientHeight - document.querySelector('.footer')?.clientHeight; // 아래에 공간이 부족하면 위로 표시 if (remainingSpaceBelow < contextMenuHeight) { contextBox.style.top = event.clientY - contextMenuHeight - 15 - document.querySelector('.banner-notice-area')?.clientHeight + 'px'; } } } } /*******************************************************************************************************************************/ /****************************************************************************************************************** 사용자 설정 */ /*******************************************************************************************************************************/ // 도움말 닫기 버튼 클릭으롷 닫기 document.querySelector('.official-doc-main .xs-btn-instructions')?.addEventListener('click', async () => { const instructionsEl = document.querySelector('.official-doc-instructions'); if (instructionsEl) { instructionsEl.style.display = 'none'; } // 도움말 보지않기 체크박스 const checkbox = document.querySelector('#doc-instructions-radio'); if (checkbox?.checked) { // 체크O 안보기 let isInstructionsChecked = false; await sendUserDataToServer(isInstructionsChecked); } }); // 초기화면 도움말 보기 export async function showInstructions() { const settings = await getUserSettingsInDB(); const checkInterval = setInterval(async () => { const instructions = document.querySelector('.official-doc-main .official-doc-instructions'); if (instructions) { clearInterval(checkInterval); // 기본적으로 체크박스 상태에 따른 도움말 보이기/숨기기 const checkbox = document.querySelector('#doc-instructions'); if (checkbox && checkbox.checked) { instructions.style.display = 'flex'; } else { instructions.style.display = 'none'; } try { const instructionsOption = settings.doc_option_instructions ?? 'false'; if (instructionsOption === 'true') { instructions.style.display = 'flex'; instructions.querySelector('.xs-btn-instructions')?.addEventListener('click', () => { instructions.style.display = 'none'; }); } } catch (err) { // console.error('설정 불러오기 실패:', err); console.log('공문 기본설정값 세팅'); } } }, 100); // 0.1초마다 확인 } /*******************************************************************************************************************************/ /****************************************************************************************************************** 리스트 정렬 */ /*******************************************************************************************************************************/ // 리스트 헤더 클릭 이벤트 함수 function initHeaderSort(containerSelector, direction) { const container = document.querySelector(containerSelector); const headers = container.querySelectorAll('div'); headers.forEach((header) => { header.addEventListener('click', (e) => { docVars.curSortCol = header.classList[0]; docVars.direction = direction; let target = e.target; if (target.matches('.official-doc-list-header-item')) { target = target.closest('.official-doc-list-header-item-wrap'); } headers.forEach((h) => { if (h === target) { if (h.classList.contains('doc-sort-asc') && !h.classList.contains('doc-sort-desc')) { h.classList.remove('doc-sort-asc'); h.classList.add('doc-sort-desc'); docVars.curSortOrder = 'desc'; } else if (!h.classList.contains('doc-sort-asc') && h.classList.contains('doc-sort-desc')) { h.classList.add('doc-sort-asc'); h.classList.remove('doc-sort-desc'); docVars.curSortOrder = 'asc'; } else { h.classList.add('doc-sort-asc'); docVars.curSortOrder = 'asc'; } } else { h.classList.remove('doc-sort-asc', 'doc-sort-desc'); } }); getDocDataBySelected(docVars.direction); }); }); } // 수신 헤더 이벤트 initHeaderSort( '.official-doc-main .official-doc-list-container .official-doc-main-body .official-doc-top .official-doc-list', '수신' ); // 발신 헤더 이벤트 initHeaderSort( '.official-doc-main .official-doc-list-container .official-doc-main-body .official-doc-bottom .official-doc-list', '발신' ); function sortDocData(data) { let sortData = {}; data.forEach(d => { const { doc_id, ...restData } = d; sortData[doc_id] = restData; }); const curSortCol = docVars.curSortCol; const curSortOrder = docVars.curSortOrder; let entries = Object.entries(sortData).sort((a, b) => { const fileA = a[0]; const fileB = b[0]; let col = curSortCol; let order = (curSortOrder == 'desc') ? -1 : 1; const parse = (fileNumber, info) => { // 리스트 헤더 명명 기준 return { base: fileNumber, rownum: info.rownum, docNumber: info.doc_number, docDate: info.doc_date, docFrom: info.sender_name_abbr, docTo: info.recipient_name_abbr, docTitle: info.doc_title, docState: document.querySelector(`.official-doc-main .main-file-container[data-doc-id="${fileNumber}"] .official-doc-file--stateText`)?.innerHTML || '', }; }; const A = parse(fileA, a[1]); const B = parse(fileB, b[1]); switch(col) { case 'number': const numCmp = B.rownum.localeCompare(A.rownum, undefined, { numeric: true, sensitivity: 'base' }); if(numCmp !== 0) return numCmp * order; case 'recipient-number': case 'sending-number': const fileNumCmp = A.base.localeCompare(B.base, undefined, { numeric: true, sensitivity: 'base' }); if(fileNumCmp !== 0) return fileNumCmp * order; break; case 'recipient-office-number': case 'sending-office-number': const officeNumCmp = A.docNumber.localeCompare(B.docNumber, undefined, { numeric: true, sensitivity: 'base' }); if(officeNumCmp !== 0) return officeNumCmp * order; break; case 'recipient-date': case 'sending-date': const dateCmp = A.docDate.localeCompare(B.docDate, undefined, { numeric: true, sensitivity: 'base' }); if(dateCmp !== 0) return dateCmp * order; break; case 'recipient-from': case 'sending-from': const fromCmp = A.docFrom.localeCompare(B.docFrom, undefined, { numeric: true, sensitivity: 'base' }); if(fromCmp !== 0) return fromCmp * order; break; case 'recipient-to': case 'sending-to': const toCmp = A.docTo.localeCompare(B.docTo, undefined, { numeric: true, sensitivity: 'base' }); if(toCmp !== 0) return toCmp * order; break; case 'recipient-title': case 'sending-title': const titleCmp = A.docTitle.localeCompare(B.docTitle, undefined, { numeric: true, sensitivity: 'base' }); if(titleCmp !== 0) return titleCmp * order; break; case 'recipient-state': case 'sending-state': const stateCmp = A.docState.localeCompare(B.docState, undefined, { numeric: true, sensitivity: 'base' }); if(stateCmp !== 0) return stateCmp * order; break; } return 0; }) return entries; }