Files
PM_test/views/main/jsm/officialDoc/docPageRenderer.js
2026-06-19 17:58:47 +09:00

2020 lines
89 KiB
JavaScript

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 => `<span class="ft-12">${text}</span>`).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 = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">엑셀 데이터를 불러오는 중...</div>';
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 = '<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;font-size:1.1rem;background:#fff;">엑셀 데이터를 파싱하지 못했습니다. (xls 확장자는 지원하지 않습니다.)</div>';
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 = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
<div>엑셀 시트 생성 중 오류가 발생했습니다.</div>
<div style="font-size:0.9rem;color:#999;margin-top:8px;">에러: ${createErr.message}</div>
</div>`;
}
}, function(err) {
console.error("Luckysheet transform error: ", err);
docVars.viewer.innerHTML = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
<div>엑셀 파일을 읽는 중 오류가 발생했습니다.</div>
<div style="font-size:0.9rem;color:#999;margin-top:8px;">상세: ${err.message || err}</div>
</div>`;
initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey);
});
})
.catch(err => {
console.error(err);
docVars.viewer.innerHTML = `<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">
<div>엑셀 파일을 불러오는데 실패했습니다.</div>
<div style="font-size:0.9rem;color:#999;margin-top:8px;">에러: ${err.message}</div>
</div>`;
initDocFallbackPdfButton(docId, resourcePath, objectKey, previewKey);
});
docVars.viewer.dataset.viewerType = 'excel';
}
function viewerWord(presignedUrl) {
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">워드 문서를 불러오는 중...</div>';
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 = '<div style="color:#d9534f;text-align:center;">워드 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
});
})
.catch(err => {
console.error(err);
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">워드 문서를 불러오는데 실패했습니다.</div>';
});
docVars.viewer.dataset.viewerType = 'word';
}
function viewerHwp(presignedUrl) {
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">한글 문서를 불러오는 중...</div>';
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 = '<div style="color:#d9534f;text-align:center;">한글 문서 파싱 중 오류가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
}
};
reader.readAsBinaryString(blob);
})
.catch(err => {
console.error(err);
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">한글 문서를 불러오는데 실패했습니다.</div>';
});
docVars.viewer.dataset.viewerType = 'hwp';
}
function viewerPptx(presignedUrl) {
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">PPTX 문서를 불러오는 중...</div>';
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 = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;padding:20px;text-align:center;">PPTX 파싱 중 에러가 발생했습니다. 상단의 "PDF로 보기" 버튼을 이용해 주세요.</div>';
}
})
.catch(err => {
console.error(err);
docVars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">PPTX 문서를 불러오는데 실패했습니다.</div>';
});
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;
}