Files
PM_test/views/main/jsm/archive/pageRenderer.js
2026-06-18 08:52:23 +09:00

7842 lines
339 KiB
JavaScript

import { vars } from './variable.js';
import { throttle, checkProjectInactive } from '../main.js';
import {
formatBytes,
addCommasToNumber,
splitBaseAndExt,
splitTopPathAndTargetName,
extractPathByLength,
getDataFromTreeObject,
getCurrentTreeObject,
targetFocus,
pxToRem,
headerBtnForceClick,
treeBtnForceClick,
closeInitProgress,
openNewWindowViewer,
initFileAreaUI,
syncGroupStyle
} from './common.js'
import {
toggleContextFocusBox,
toggleContextmenu,
toggleControlBox,
toggleArchiveMainRight,
showNotification,
} from './eventManager.js'
import {
openCreateFolderModal,
uploadData,
openRenameModal,
openEditAuthorModal,
openDownloadModal,
toggleRelocateCover,
openRemoveModal,
removeTarget,
openDeleteModal,
setDataPermission,
convertPdf,
updateAiButtonState,
} from './dataManager.js';
import {
mgmtFunc_addClickLog,
mgmtFunc_resetConvert
} from './managementFunctions.js';
import { toggleModal } from './modalManager.js';
import { OLMultiLayerManager } from './olPmtiles.js';
let mainNotice = document.querySelector('.main-notice');
let treeNotice = document.querySelector('.archive-main .archive-main-left .tree-container .tree-notice');
let treeWrap = document.querySelector('.archive-main .archive-main-left .tree-container .tree-wrap');
let listContainer = document.querySelector('.archive-main .archive-main-center .list-container');
let listNotice = document.querySelector('.archive-main .archive-main-center .list-notice');
let viewerContainer = document.querySelector('.archive-main .archive-main-right .viewer-container');
let viewerNotice = document.querySelector('.archive-main .archive-main-right .viewer-notice');
let viewerWrap = document.querySelector('.viewer-wrap');
let viewerToolbar = viewerContainer?.querySelector('.toolbar');
let infoWrap = viewerContainer?.querySelector('.info-wrap');
let metadata = infoWrap?.querySelector('.metadata');
let subCategoryList = ['version', 'attachment'];
let subCategoryData = {
version: {
idx: 1,
korean: '버전'
},
attachment: {
idx: 2,
korean: '첨부'
},
};
// 💡 글로벌 자동 삭제/보관 정책 설정 동적 변수 (기본값)
let FOLDER_KEEP_FILE_THRESHOLD = 3;
let FOLDER_KEEP_DAYS_THRESHOLD = 15;
let FOLDER_KEEP_POLICY_ACTIVE = false;
let isPolicyLoaded = false;
async function loadSystemPolicy() {
if (isPolicyLoaded) return;
try {
const res = await axios.get('/common/system-policy');
if (res.data) {
FOLDER_KEEP_POLICY_ACTIVE = res.data.is_active ?? false;
// 글로벌 정책이 켜져 있을 때만 DB 값을 적용하고, 꺼져 있으면 배지 노출을 차단합니다.
if (FOLDER_KEEP_POLICY_ACTIVE) {
FOLDER_KEEP_FILE_THRESHOLD = Number(res.data.limit_file_count) || 3;
FOLDER_KEEP_DAYS_THRESHOLD = Number(res.data.limit_days) || 15;
}
}
isPolicyLoaded = true;
} catch (err) {
console.error("Failed to load system policy:", err);
isPolicyLoaded = true;
}
}
export function updateSystemPolicyCache(policy) {
if (policy) {
FOLDER_KEEP_POLICY_ACTIVE = policy.is_active ?? false;
if (FOLDER_KEEP_POLICY_ACTIVE) {
FOLDER_KEEP_FILE_THRESHOLD = Number(policy.limit_file_count) || 3;
FOLDER_KEEP_DAYS_THRESHOLD = Number(policy.limit_days) || 15;
} else {
FOLDER_KEEP_FILE_THRESHOLD = 3;
FOLDER_KEEP_DAYS_THRESHOLD = 15;
}
} else {
isPolicyLoaded = false;
}
}
// 브라우저 뒤로가기, 앞으로가기 이벤트
window.addEventListener('popstate', async (e)=>{
// console.log(e);
if(window.location.search.split('?path=')[1]){
// let resourcePath = atob(decodeURIComponent(window.location.search.split('?path=')[1]));
let resourcePath = unicodeAtob(window.location.search.split('?path=')[1]);
// console.log(getDataFromTreeObject(resourcePath, 'folder').data);
// if (!getDataFromTreeObject(resourcePath, 'folder').data) {
// }
let resourcePathSplit = resourcePath.split('/');
resourcePathSplit.shift();
let headerBtnPath = `/${resourcePath.split('/')[1]}`;
// 휴지통 뒤로가기 - 임시
if (resourcePath == '/휴지통') return;
let treeItem;
if (resourcePath == '/과업개요' || resourcePath == '/공문') {
document.querySelector('.archive-main').style.display = 'none';
if (resourcePath == '/과업개요') {
document.querySelector('.overview-main').style.display = 'flex';
document.querySelector('.official-doc-main').style.display = 'none';
} else {
document.querySelector('.overview-main').style.display = 'none';
document.querySelector('.official-doc-main').style.display = 'flex';
}
let pageRanderingOption = {
scope: 'headerBtn_page',
resourcePath: resourcePath,
pushState: false,
}
await preparePageRendering(pageRanderingOption);
} else {
document.querySelector('.archive-main').style.display = 'flex';
document.querySelector('.overview-main').style.display = 'none';
document.querySelector('.official-doc-main').style.display = 'none';
// 휴지통 뒤로가기 - 임시
// if (resourcePathSplit[0] == '휴지통') {
// document.querySelector('.archive-main-left').style.display = 'none';
// } else {
// document.querySelector('.archive-main-left').style.display = 'flex';
// }
// console.log(resourcePathSplit);
if (resourcePathSplit.length == 1) {
listNotice.style.display = 'flex';
listContainer.style.display = 'none';
// viewerNotice.style.display = 'flex';
// viewerContainer.style.display = 'none';
document.querySelector('.archive-main .archive-main-left .tree-title').classList.add('selected');
let pageRanderingOption = {
scope: 'tree',
resourcePath: resourcePath,
pushState: false,
}
await preparePageRendering(pageRanderingOption);
} else {
listNotice.style.display = 'none';
listContainer.style.display = 'flex';
// viewerNotice.style.display = 'flex';
// viewerContainer.style.display = 'none';
let pageRanderingOption = {
scope: 'tree',
resourcePath: headerBtnPath,
pushState: false,
}
await preparePageRendering(pageRanderingOption);
pageRanderingOption = {
scope: 'list',
resourcePath: resourcePath,
pushState: false,
}
await preparePageRendering(pageRanderingOption);
treeItem = document.querySelector(`.archive-main .archive-main-left .tree-container .tree-item-wrap[data-resource-path="${resourcePath}"]`);
await changeTreeItemStyle(treeItem);
if (treeItem) targetFocus(treeItem);
}
}
let headerBtn = document.querySelector(`body > .header .center .left.wrap .menu-tab .btn[data-resource-path="${headerBtnPath}"]`);
if (!headerBtn) headerBtn = document.querySelector('.official-doc-btn');
await changeHeaderBtnStyle(headerBtn);
if (headerBtn) targetFocus(headerBtn);
}else{
window.history.back();
}
})
// base64 인코딩된 문자열을 한글로 디코딩하는 함수
function unicodeAtob(str) {
// base64 디코딩 후 UTF-8로 디코딩
return decodeURIComponent(
Array.prototype.map.call(
atob(str),
function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}
).join('')
);
}
// 한글을 btoa로 인코딩하는 함수
function unicodeBtoa(str) {
// 문자열을 UTF-8 인코딩된 바이너리로 변환 후 base64 인코딩
return btoa(
encodeURIComponent(str).replace(
/%([0-9A-F]{2})/g,
function(match, p1) {
return String.fromCharCode('0x' + p1);
}
)
);
}
// 확장자가 정상인지 체크하는 함수
function isValidExt(ext) {
let result = false;
if (!/\s/.test(ext) || /^[A-Za-z0-9]+$/.test(ext)) result = true;
return result;
}
// 월, 일 한 자리수 일 때 앞에 0 추가
function changeDateFormat(date) {
let dataSplit = date.split('.');
dataSplit.pop();
let [year, month, day] = dataSplit;
year = year.substr(2, 4);
month = month.replace(' ', '');
day = day.replace(' ', '');
if (month.length == 1) month = '0' + month;
if (day.length == 1) day = '0' + day;
return [year, month, day].join('-');
}
// 시간 형식 변환 (시:분:초 -> 시:분)
function changeTimeFormat(time) {
let [hours, minutes, seconds] = time.split(':');
return `${hours}:${minutes}`;
}
function getExpiryDate(lastFolderActDate) {
let result = {};
// 마지막으로 폴더에 저장된 시간
lastFolderActDate = new Date(lastFolderActDate);
// lastFolderActDate로부터 FOLDER_KEEP_DAYS_THRESHOLD일 뒤의 시간
let expiryDate = new Date(lastFolderActDate.getTime() + FOLDER_KEEP_DAYS_THRESHOLD * 24 * 60 * 60 * 1000);
// 현재 시간
let nowDate = new Date();
// expiryDate와 nowDate의 차이 (밀리세컨드)
let diffMs = expiryDate - nowDate;
if (diffMs <= 0) {
} else {
let diffSec = Math.floor(diffMs / 1000);
let days = Math.floor(diffSec / (24 * 60 * 60));
let hours = Math.floor((diffSec % (24 * 60 * 60)) / 3600);
let minutes = Math.floor((diffSec % 3600) / 60);
let seconds = diffSec % 60;
// 두 자리 포맷
let pad = n => String(n).padStart(2, '0');
let formatted = `${pad(days)}${pad(hours)}시간 ${pad(minutes)}${pad(seconds)}`;
result.days = days;
result.formatted = formatted;
}
return result;
}
// 만료 폴더 삭제 함수
function removeExpiredFolder(value) {
// useSocket true false ?
let params ={
resourcePathArr: [value.resourcePath],
dataIdArr: [value.dataId],
dataType: 'folder',
isExpiredFolder: true
}
removeTarget(params);
}
// 만료 기간 툴팁 및 타이머 삭제 함수
export function removeCountdownTooltip() {
let countdownTooltip = document.querySelector('.countdown-tooltip');
if (!countdownTooltip) return;
if (vars.countdownTimer) {
clearInterval(vars.countdownTimer);
vars.countdownTimer = null;
}
countdownTooltip.remove();
}
function sortData(data) {
let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex';
let curSortCol = (isRecycleBinModal) ? vars.curSortCol_bin : vars.curSortCol;
let curSortOrder = (isRecycleBinModal) ? vars.curSortOrder_bin : vars.curSortOrder;
let entries = Object.entries(data).sort((a, b) => {
const fileA = a[0];
const fileB = b[0];
let col = curSortCol;
let order = (curSortOrder == 'desc')?-1:1;
if (a[1].type == 'folder') {
col = 'folder';
order = 1;
}
const parse = (filename, info) => {
const match = filename.match(/^(.+?)(?:_(\d{4}-\d{2}-\d{2} \d{2}-\d{2}-\d{2}-\d{3}))?\.([^.]+)$/);
// depth4 파일에는 메모가 없지만 이 파일의 추가파일에는 메모가 있는 경우,
// depth4 파일에도 메모가 있다고 가정한 후 메모 정렬에 포함
// 메모 data : 원래 => String, 메모가 있다고 가정 => boolean(true)
const result = getFilenameWithMemoInDepth5ByDepth4Path(info.resourcePath);
if (info.depth == 4 && info.memo == '') {
if (filename === result.memoFilename) info.memo = true;
}
return {
base: match?.[1] || filename,
timestamp: match?.[2] || null,
ext: match?.[3] || '',
originalLocation: splitTopPathAndTargetName(info.resourcePath).topPath,
removeUser: info.modUserNm,
removeDate: info.modDate,
memo: info.memo,
author: info.authorNm,
uploader: info.userNm,
createDate: info.createDate,
size: info.size,
state: (info.needConvert)?`${info.isConverted}` : `${info.isSupported}`,
depth: info.depth,
isSub: result.isSub,
};
};
const A = parse(fileA, a[1]);
const B = parse(fileB, b[1]);
switch(col){
case 'original-location':
if (isRecycleBinModal) {
const originalLocationCmp = A.originalLocation.localeCompare(B.originalLocation, undefined, { numeric: true, sensitivity: 'base' });
if(originalLocationCmp !==0) return originalLocationCmp * order;
}
break;
case 'remove-user':
if (isRecycleBinModal) {
const removeUserCmp = A.removeUser.localeCompare(B.removeUser, undefined, { numeric: true, sensitivity: 'base' });
if(removeUserCmp !==0) return removeUserCmp * order;
}
break;
case 'remove-date':
if (isRecycleBinModal) {
const removeDateCmp = A.removeDate.localeCompare(B.removeDate, undefined, { numeric: true, sensitivity: 'base' });
if(removeDateCmp !==0) return removeDateCmp * order;
}
break;
case 'memo':
const getMemoPriority = (item) => { // 정렬 우선순위
if (item.depth == 4 && item.isSub == false && item.memo != '') return 0;
if (item.depth == 4 && item.isSub == true && item.memo != '' && item.memo != true) return 1;
if (item.depth == 4 && item.isSub == true && item.memo == true) return 2;
if (item.depth == 5 && item.memo != '') return 3;
return 99;
};
const memoA = getMemoPriority(A);
const memoB = getMemoPriority(B);
if (memoA !== memoB) return (memoA - memoB) * order;
case 'author':
if (A.author == '' || A.author == undefined || A.author == null) A.author = A.uploader;
if (B.author == '' || B.author == undefined || B.author == null) B.author = B.uploader;
const authorCmp = A.author.localeCompare(B.author, undefined, { numeric: true, sensitivity: 'base' });
if(authorCmp !==0) return authorCmp * order;
break;
case 'uploader':
const uploaderCmp = A.uploader.localeCompare(B.uploader, undefined, { numeric: true, sensitivity: 'base' });
if(uploaderCmp !==0) return uploaderCmp * order;
break;
case 'create-date':
const dateCmp = A.createDate.localeCompare(B.createDate, undefined, { numeric: true, sensitivity: 'base' });
if(dateCmp !==0) return dateCmp * order;
break;
case 'size':
const sizeCmp = A.size.localeCompare(B.size, undefined, { numeric: true, sensitivity: 'base' });
if(sizeCmp !==0) return sizeCmp * order;
break;
case 'state':
const stateCmp = A.state.localeCompare(B.state, undefined, { numeric: true, sensitivity: 'base' });
if(stateCmp !==0) return stateCmp * order;
break;
default :
// 1. base name 정렬
const baseCmp = A.base.localeCompare(B.base, undefined, { numeric: true, sensitivity: 'base' });
if (baseCmp !== 0) return baseCmp * order;
// 2. 확장자 정렬
const extCmp = A.ext.localeCompare(B.ext, undefined, { numeric: true, sensitivity: 'base' });
if (extCmp !== 0) return extCmp * order;
// 3. timestamp 없는 파일 먼저
if (A.timestamp && B.timestamp) {
return A.timestamp.localeCompare(B.timestamp) * order;
} else if (A.timestamp) {
return 1 * order; // A는 있음 → B가 먼저
} else if (B.timestamp) {
return -1 * order; // B는 있음 → A가 먼저
}
break;
}
return 0;
});
return entries;
}
// depth4 경로를 기준으로, 1. depth4 파일이 추가파일(depth5)을 가지고 있는지 판단 후 반환
// 2. depth4 자체에는 메모가 없고 하위 depth5 파일 중 메모가 하나라도 존재하면 그 depth4 파일의 이름을 반환
function getFilenameWithMemoInDepth5ByDepth4Path(depth4Path) {
let isSub = false;
let memoFilename = null;
for (const ele of subCategoryList) {
let sub = getDataFromTreeObject(`${depth4Path}_${ele}`, 'folder', vars.allTreeObject).data;
const subFiles = sub?.child?.file;
if (subFiles) {
isSub = true;
for (const key of Object.keys(subFiles)) {
if (subFiles[key]?.memo) {
memoFilename = subFiles[key].mainFileName;
break;
}
}
}
if (memoFilename) break;
}
return { isSub: isSub, memoFilename };
}
export function resetSort() {
let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex';
let listContainer, curSortCol, curSortOrder;
if (isRecycleBinModal) {
listContainer = document.querySelector('.recycle-bin-modal .list-container');
curSortCol = 'remove-date';
vars.curSortCol_bin = curSortCol;
vars.curSortOrder_bin = 'desc'
} else {
listContainer = document.querySelector('.archive-main-center .list-container');
curSortCol = 'name';
vars.curSortCol = curSortCol;
vars.curSortOrder = 'asc'
}
let headers = listContainer.querySelector('.list-header-area').querySelectorAll('div');
headers.forEach(h=>{
h.classList.remove('sort-asc', 'sort-desc');
if (h.classList.contains(curSortCol)) h.classList.add('sort-asc');
})
}
function translateActivity(activity) {
let result;
/*
< _ 분리 O >
uploadData_file
renameTarget_file
renameTarget_folder
downloadTarget_file
removeTarget_file
removeTarget_folder
setDataPermission_folder
addOn_version
addOn_attachment
< _ 분리 X >
createFolder
convertPdf
*/
let activitySplit = activity.split('_');
if (activitySplit[1]) {
let prefix, suffix;
if (activitySplit[1] == 'file') prefix = '파일';
if (activitySplit[1] == 'folder') prefix = '폴더';
if (activitySplit[1] == 'version') prefix = '버전파일';
if (activitySplit[1] == 'attachment') prefix = '첨부파일';
if (activitySplit[1] == 'subMaster') prefix = '부관리자';
if (activitySplit[1] == 'securityWorker') prefix = '보안참여자';
if (activitySplit[1] == 'worker') prefix = '일반참여자';
if (activitySplit[1] == 'viewer') prefix = '참관자';
if (activitySplit[0] == 'uploadData') suffix = '업로드';
if (activitySplit[0] == 'renameTarget') suffix = '이름 변경';
if (activitySplit[0] == 'relocateTarget') suffix = '이동';
if (activitySplit[0] == 'downloadTarget') suffix = '다운로드';
if (activitySplit[0] == 'removeTarget') {
suffix = (activitySplit[1] == 'file') ? '휴지통으로 이동' : '폴더 삭제';
if (activitySplit[2] && activitySplit[2] == 'expired') suffix = '폴더 자동 삭제 (파일 개수 미달)';
prefix = '';
}
if (activitySplit[0] == 'deleteTarget') suffix = '파일 삭제';
if (activitySplit[0] == 'setDataPermission') suffix = '권한 설정';
if (activitySplit[0] == 'addOn') suffix = '추가';
if (activitySplit[0] == 'addPermission') suffix = '권한 추가';
if (activitySplit[0] == 'deletePermission') suffix = '권한 삭제';
result = `${prefix} ${suffix}`;
} else {
if (activitySplit[0] == 'createFolder') result = '새 폴더 생성';
if (activitySplit[0] == 'convertPdf') result = 'PDF 변환';
if (activitySplit[0] == 'saveMemo') result = '메모 데이터 저장';
if (activitySplit[0] == 'editAuthor') result = '작성자 변경';
if (activitySplit[0] == 'summarizeAI') result = 'AI요약';
}
if (result) result = result.trim();
return result;
}
export async function preparePageRendering(pageRanderingOption) {
if (checkProjectInactive()) return;
// scope: headerBtn, tree(depth2, depth3), list
let headerBtn = false, tree = false, list = false;
let scope = pageRanderingOption.scope;
let resourcePath = pageRanderingOption.resourcePath;
let userCurPath = (pageRanderingOption.userCurPath) ? pageRanderingOption.userCurPath : resourcePath;
if (scope == 'headerBtn') headerBtn = true, resourcePath = '/';
if (scope == 'tree') tree = true;
if (scope == 'list') list = true;
if (scope == 'recycle_bin') list = true;
let isInit = (pageRanderingOption) ? pageRanderingOption.isInit : undefined;
if (isInit != true) isInit = false;
let getTreeObject = (pageRanderingOption) ? pageRanderingOption.getTreeObject : undefined;
if (getTreeObject != false) getTreeObject = true;
let pushState = (pageRanderingOption) ? pageRanderingOption.pushState : undefined;
if (pushState != false) pushState = true;
// 초기 실행 시
if (isInit) {
// // 데이터 로딩 프로그레스 시작
document.querySelector('.init-progress').style.display = 'flex';
// // 마지막 히스토리를 현재 경로로 대체
window.history.replaceState(window.history.length+1,'',window.location.pathname);
}
// 251114 김아름
// if (pushState && resourcePath != '/') window.history.pushState(window.history.length+1,'',window.location.pathname + '?path='+unicodeBtoa(resourcePath));
if (pushState) {
const isValidPath = resourcePath && resourcePath !== '/';
if (isValidPath) {
window.history.pushState(window.history.length+1,'',window.location.pathname + '?path='+unicodeBtoa(resourcePath));
} else {
window.history.pushState(window.history.length+1,'',window.location.pathname);
}
}
// 휴지통 리스트를 보고 있지 않는 경우에만 파일 리스트 화면으로 변경
if (vars.lastHeaderBtn && vars.lastHeaderBtn.dataset.resourcePath != '/휴지통') {
if (document.querySelector('.archive-main-left').style.display == 'none') {
// 좌측트리영역 표시
document.querySelector('.archive-main-left').style.display = 'flex';
// list-item-wrap에 추가했던 recycle-bin클래스 삭제
vars.listItemWrap.classList.remove('recycle-bin');
}
}
if (!scope) return;
if (scope == 'updateTreeObject') {
// TreeObject만 갱신하고 렌더링은 하지 않음
let userString = vars.userInfoString;
let user = JSON.parse(userString);
user.project_id = vars.project_id;
user.curPath = resourcePath;
if (userCurPath) user.curPath = userCurPath;
let getTreeObjectParams = {
userInfoString: JSON.stringify(user),
storageType: vars.storageType,
resourcePath: resourcePath
};
let getTreeObjectRes = await axios.get(`${vars.path_name}/getTreeObject`, {params: {params: getTreeObjectParams}});
if (getTreeObjectRes.data.message == 'getTreeObject_success') {
vars.currentTreeObject = getTreeObjectRes.data.currentTreeObject;
// allTreeObject 갱신
if (resourcePath != '/') {
let parentData = getDataFromTreeObject(resourcePath, 'folder', vars.allTreeObject).parentData;
let data = getDataFromTreeObject(resourcePath, 'folder', vars.allTreeObject).data;
if (data) {
data.child = vars.currentTreeObject;
let targetName = splitTopPathAndTargetName(resourcePath).targetName;
parentData.folder[targetName] = data;
}
}
}
return;
}
if (getTreeObject) {
let meString = vars.userInfoString;
let me = JSON.parse(meString);
me.project_id = vars.project_id;
me.curPath = resourcePath;
if (userCurPath) me.curPath = userCurPath;
vars.socket.emit('setUser',{me:me, stat:'no'});
vars.socket.emit('getMe');
if(me.bookmark && me.bookmark != '' && me.bookmark != 'null') me.bookmark = '';
let getTreeObjectParams = { userInfoString: JSON.stringify(me), storageType: vars.storageType, resourcePath: resourcePath };
try {
let getTreeObjectRes = await axios.get(`${vars.path_name}/getTreeObject`, { params: { params: getTreeObjectParams } });
if (getTreeObjectRes.data.message == 'getTreeObject_success') {
vars.convertingDataArr = getTreeObjectRes.data.convertingDataArr;
vars.currentTreeObject = getTreeObjectRes.data.currentTreeObject;
if (!vars.allTreeObject || resourcePath == '/') {
vars.allTreeObject = vars.currentTreeObject;
} else {
if (scope != 'recycle_bin') {
let parentData = getDataFromTreeObject(resourcePath, 'folder', vars.allTreeObject).parentData;
let data = getDataFromTreeObject(resourcePath, 'folder', vars.allTreeObject).data;
if (data) {
data.child = vars.currentTreeObject;
let targetName = splitTopPathAndTargetName(resourcePath).targetName;
parentData.folder[targetName] = data;
}
} else {
console.log(1234567890);
}
}
} else if (getTreeObjectRes.data.message == 'getTreeObject_failed') {
console.log('---------------- getTreeObject 실패');
closeInitProgress();
}
} catch (err) {
console.error('---------------- getTreeObject 에러 발생:', err);
closeInitProgress();
}
} else {
vars.allTreeObject = vars.allTreeObject;
vars.currentTreeObject = getCurrentTreeObject(resourcePath);
}
// if (!scope) return;
if (scope == 'headerBtn_page' || scope == 'updateTreeObject') return;
await (async function () {
// 과업개요 비활성화 된 경우 트리 오브젝트 생성 후에 시작경로가 없으면 초기 프로그레스 종료
// if (isInit && !vars.project.overview) {
// if (!vars.startPathDepth1 && !vars.startPathDepth2 && !vars.startPathDepth3) document.querySelector('.init-progress').style.display = 'none';
// }
let userInfo = JSON.parse(vars.userInfoString);
if (userInfo.permission != null && typeof userInfo.permission == 'number') {
if (headerBtn) {
//// header folder
let headerBtnParent = document.querySelector('body > .header .center .left.wrap .menu-tab');
await renderHeaderBtn(headerBtnParent, vars.currentTreeObject.folder, isInit);
// 헤더 버튼이 없으면 초기 프로그레스 종료 후 mainNotice 표시
if (document.querySelector('body > .header .center .left.wrap .menu-tab').children.length == 0) {
console.log('******** 헤더버튼 영역 렌더링 후 초기 프로그레스 종료 (과업개요버튼 없음, 헤더폴더버튼 없음)');
await closeInitProgress();
mainNotice.style.display = 'flex';
}
}
if (tree) {
let treeTitle = document.querySelector('.archive-main-left .tree-title');
let treeContainer = document.querySelector('.archive-main-left .tree-container');
let treeWrap = treeContainer.querySelector('.tree-wrap');
treeTitle.querySelector('.wrap .text').textContent = vars.lastHeaderBtn.querySelector('.name-text').textContent;
treeTitle.dataset.resourcePath = vars.lastHeaderBtn.dataset.resourcePath;
treeContainer.dataset.resourcePath = vars.lastHeaderBtn.dataset.resourcePath;
treeWrap.dataset.resourcePath = vars.lastHeaderBtn.dataset.resourcePath;
await renderTree(treeWrap, vars.currentTreeObject.folder);
//////// pm-bcmf 연결용 테스트 코드 - 파라미터로 전달받은 시작경로로 화면 전환 (depth3)
if (vars.startPathDepth3) {
let path = `/${vars.startPathDepth1}/${vars.startPathDepth2}/${vars.startPathDepth3}`;
let treeBtn = document.querySelector(`.tree-container .tree-wrap .tree-item-wrap[data-resource-path="${path}"] .tree-item`);
if (treeBtn) await treeBtnForceClick(treeBtn);
}
if (document.querySelector('.init-progress').style.display != 'none') {
console.log('******** 트리 영역 렌더링 후 초기 프로그레스 종료');
await closeInitProgress();
}
}
if (list) {
let listParent = document.querySelector('.archive-main-center .list-container .list-wrap.list-body .list-item-wrap');
renderList(listParent, vars.currentTreeObject.file);
}
}
})();
}
export async function prepareRecycleBinRendering() {
let resourcePath = '/휴지통';
let getRecycleBinObjectParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, resourcePath: resourcePath };
let getRecycleBinObjectRes = await axios.get(`${vars.path_name}/getRecycleBinObject`, { params: { params: getRecycleBinObjectParams } });
if (getRecycleBinObjectRes.data.message == 'getRecycleBinObject_success') {
vars.recycleBinObject = getRecycleBinObjectRes.data.recycleBinObject;
let listParent = document.querySelector('.recycle-bin-modal .recycle-bin-wrap .list-container .list-body .list-item-wrap');
listParent.classList.add('recycle-bin');
renderRecycleBin(listParent, vars.recycleBinObject.recycleBin);
}
}
export async function renderHeaderBtn(parent, data, isInit) {
let headerCenter = document.querySelector('body > .header .center');
let rightWrap = headerCenter.querySelector('.right.wrap');
let leftWrap = headerCenter.querySelector('.left.wrap');
let menuTab = leftWrap.querySelector('.menu-tab');
leftWrap.style.width = `${headerCenter.offsetWidth - rightWrap.offsetWidth - 16}px`; // body > .header .center -> gap: 16px;
// let me = JSON.parse(vars.userInfoString);
//// 과업개요 버튼 사용하는 프로젝트인 경우 과업개요 버튼 복제
let overviewBtnClone;
if (vars.project.overview && menuTab.querySelector('.overview-btn')) {
overviewBtnClone = menuTab.querySelector('.overview-btn').cloneNode(true);
//// 복제한 과업개요 버튼에 클릭 이벤트 추가
overviewBtnClone.addEventListener('click', async () => {
if (document.querySelector('.archive-main')) document.querySelector('.archive-main').style.display = 'none';
if (document.querySelector('.overview-main')) document.querySelector('.overview-main').style.display = 'flex';
if (document.querySelector('.official-doc-main')) document.querySelector('.official-doc-main').style.display = 'none';
if (mainNotice.style.display == 'flex') mainNotice.style.display = 'none';
//// vars에서 마지막 선택 아이템을 저장한 속성들 초기화 - 복제한 과업개요 버튼에 클릭 이벤트 추가
vars.lastMainTreeItem = undefined;
vars.lastListItem = undefined;
vars.lastListGroupTarget = undefined;
vars.lastContextTarget = undefined;
vars.lastSelectTarget = undefined;
vars.multiSelectListItemArr = [];
let pageRanderingOption = { scope: 'headerBtn_page', resourcePath: '/과업개요' };
await preparePageRendering(pageRanderingOption);
changeHeaderBtnStyle(overviewBtnClone);
// 컨트롤 박스 숨김
toggleControlBox(false);
})
}
//// menuTab 전체 초기화 -> menuTab에 복제한 과업개요 버튼 추가
menuTab.innerHTML = '';
if (overviewBtnClone) menuTab.appendChild(overviewBtnClone);
let docFragment = document.createDocumentFragment();
let entries = sortData(data);
entries.forEach(entry => {
let key = entry[0];
let value = entry[1];
let childState;
if (value.filesCount == 0) {
childState = 'empty';
} else {
childState = 'filled';
}
let resourcePath = value.resourcePath;
let btn = document.createElement('div');
btn.classList.add('btn');
btn.classList.add('folder-btn');
btn.classList.add(`depth${value.depth}`);
btn.classList.add(childState);
btn.classList.add('folder');
btn.dataset.resourcePath = resourcePath;
btn.dataset.id = value.dataId;
btn.dataset.size = value.size;
let wrap = document.createElement('div');
wrap.classList.add('wrap');
let image = document.createElement('div');
image.classList.add('image');
let nameText = document.createElement('div');
nameText.classList.add('name-text');
nameText.innerHTML = key;
wrap.appendChild(image);
wrap.appendChild(nameText);
let addPermissionBadge = false, permissionEng = '', permissionKor = '';
if (value.permission == 0) {
addPermissionBadge = true;
permissionEng = 'sub-master';
permissionKor = '관리';
}
if (value.permission == 8) {
addPermissionBadge = true;
permissionEng = 'security-worker';
permissionKor = '보안';
}
if (value.permission == 4) {
addPermissionBadge = true;
permissionEng = 'worker';
permissionKor = '일반';
}
// if (value.permission == 2) {
// addPermissionBadge = true;
// permissionEng = 'uploader';
// permissionKor = '업로더';
// }
if (addPermissionBadge) {
let permissionBadge = document.createElement('div');
permissionBadge.classList.add(`user-permission-${permissionEng}`);
// permissionBadge.style.marginLeft = '8px';
let h6 = document.createElement('h6');
h6.innerText = permissionKor;
permissionBadge.appendChild(h6);
wrap.appendChild(permissionBadge);
}
btn.appendChild(wrap);
docFragment.appendChild(btn);
btn.addEventListener('click', async (e) => {
headerBtnClickFunc(btn);
});
// depth1 폴더 우클릭 시 강조 및 컨텍스트 메뉴 열기
btn.addEventListener('contextmenu', async (e) => {
headerBtnContextmenuFunc(e);
});
});
parent.appendChild(docFragment);
processHeaderBtnOverflow();
if (isInit) {
// 헤더 버튼 최초 렌더링 시
let headerBtn;
// 시작경로가 있고 과업개요를 사용 안하는 프로젝트인 경우 시작경로로 화면 전환
if (vars.startPathDepth1 && !vars.project.overview) {
//////// pm-bcmf 연결용 테스트 코드 - 파라미터로 전달받은 시작경로로 화면 전환 (depth1)
headerBtn = document.querySelector(`body > .header .center .left.wrap .menu-tab .btn[data-resource-path="/${vars.startPathDepth1}"]`);
} else {
// 시작경로 없는 경우 첫 번째 헤더버튼으로 화면 전환
headerBtn = document.querySelectorAll('body > .header .center .left.wrap .menu-tab .btn')[0];
}
if (headerBtn) await headerBtnForceClick(headerBtn);
} else {
if (!vars.lastHeaderBtn) return;
let headerBtnPath = vars.lastHeaderBtn.dataset.resourcePath;
let headerBtn = document.querySelector(`body > .header .center .left.wrap .menu-tab .btn[data-resource-path="${headerBtnPath}"]`);
if (headerBtn == null || headerBtn == undefined || !headerBtn) vars.lastHeaderBtn = undefined;
if (headerBtn) await changeHeaderBtnStyle(headerBtn);
}
}
async function headerBtnClickFunc(btn) {
vars.lastSelectTarget = btn;
if (btn) targetFocus(btn);
if (document.querySelector('.archive-main')) document.querySelector('.archive-main').style.display = 'flex';
if (document.querySelector('.overview-main')) document.querySelector('.overview-main').style.display = 'none';
if (document.querySelector('.official-doc-main')) document.querySelector('.official-doc-main').style.display = 'none';
if (mainNotice.style.display == 'flex') mainNotice.style.display = 'none';
listContainer.style.display = 'none';
listNotice.style.display = 'flex';
resetViewer();
changeHeaderBtnStyle(btn);
let resourcePath = btn.dataset.resourcePath;
document.querySelector('.archive-main-left .tree-title').dataset.resourcePath = resourcePath;
document.querySelector('.archive-main-left .tree-container').dataset.resourcePath = resourcePath;
let pageRanderingOption = {
scope: 'tree',
resourcePath: resourcePath
}
await preparePageRendering(pageRanderingOption);
document.querySelector('.archive-main-left .tree-title').classList.add('selected');
// 파일 이동 모드(listViewerCover display flex)일 때 헤더 버튼 클릭하면 선택 폴더 경로 초기화
let listViewerCover = document.querySelector('.list-viewer-cover');
if (listViewerCover.style.display == 'flex') {
listViewerCover.querySelector('.new-path .value').innerText = '-';
} else {
//// vars에서 마지막 선택 아이템을 저장한 속성들 초기화 - renderTree
// 파일 이동 모드에서 초기화하면 이동 전, 후 경로를 가져올 수 없어서 파일 이동 모드 아닐 때에만 초기화
vars.lastMainTreeItem = undefined;
vars.lastListItem = undefined;
vars.lastListGroupTarget = undefined;
vars.lastContextTarget = undefined;
vars.multiSelectListItemArr = [];
}
//// 리스트 영역 가이드 화면 표시
const viewer = document.querySelector('.list-notice-viewer');
viewer.dataset.resourcePath = resourcePath;
let param = { resourcePath };
let res = await axios.get(`${vars.path_name}/isMainTitleImage`, {params: param});
if(res.data.result || !res.data.result == undefined) {
let mainTitleImg = res.data.result.path2;
if(mainTitleImg.includes('MAIN_TITLE_IMAGE___')) {
let generateImageUrlParams = {
resourcePath: `${vars.lastHeaderBtn.dataset.resourcePath}`
};
let imgUrlRes = await axios.get(`${vars.path_name}/generateImageUrl`, { params: generateImageUrlParams });
let imageUrl = imgUrlRes.data.result.url;
document.querySelector('.list-notice-viewer').src = imageUrl;
document.querySelector('.list-notice-viewer').style.display = 'flex';
document.querySelector('.list-notice-top').style.display = 'none';
document.querySelector('.list-notice-bottom').style.display = 'none';
} else {
document.querySelector('.list-notice-viewer').style.display = 'none';
document.querySelector('.list-notice-top').style.display = 'flex';
document.querySelector('.list-notice-bottom').style.display = 'flex';
}
}else {
document.querySelector('.list-notice-viewer').style.display = 'none';
document.querySelector('.list-notice-top').style.display = 'flex';
document.querySelector('.list-notice-bottom').style.display = 'flex';
}
// 컨트롤 박스 숨김
toggleControlBox(false);
// // 아카이브 우측 영역 표시
// let option = {
// from: '헤더 버튼 클릭'
// }
// toggleArchiveMainRight(true, option);
toggleArchiveMainRight(false);
}
function headerBtnContextmenuFunc(event) {
let scope = 'headerBtn';
toggleContextFocusBox(true, event.target, scope);
toggleContextmenu(true, event, scope);
}
export function processHeaderBtnOverflow() {
if (checkProjectInactive()) return;
let headerCenter = document.querySelector('body > .header .center');
let leftWrap = headerCenter.querySelector('.left.wrap');
let leftWrapWidth = leftWrap.offsetWidth;
let menuTab = leftWrap.querySelector('.menu-tab');
let menuAdd = leftWrap.querySelector('.menu-add');
let menuAddWidth = 0;
if (menuAdd) menuAddWidth = menuAdd.offsetWidth;
let menuTabMaxWidth = leftWrapWidth - menuAddWidth - 16 - 16 - 16; // .left.wrap => padding-left: 16px / gap: 16px / padding-right: 16px;
let btnsWidth = 0;
Object.values(menuTab.children).forEach(btn => {
btn.style.display = 'flex';
btnsWidth += (btn.offsetWidth + 4); // 버튼에 margin: 0 2px 설정되어 있어서 좌우 마진값 4 추가
})
let scrollLeft = document.querySelector('.scroll-left');
let scrollRight = document.querySelector('.scroll-right');
if (btnsWidth > menuTabMaxWidth) {
leftWrap.style.paddingRight = '80px';
scrollLeft.style.display = 'flex';
scrollRight.style.display = 'flex';
} else {
leftWrap.style.paddingRight = '16px';
scrollLeft.style.display = 'none';
scrollRight.style.display = 'none';
}
}
export async function renderTree(parent, data, isSub) {
await loadSystemPolicy();
if (parent.matches('.tree-wrap')) {
document.querySelectorAll('.tree-wrap').forEach(treeWrap => {
treeWrap.innerHTML = '';
})
}
let entries = sortData(data);
if (entries.length != 0) {
if (!isSub) {
treeNotice.style.display = 'none';
treeWrap.style.display = 'flex';
}
let docFragment = document.createDocumentFragment();
let tree = document.createElement('div');
tree.classList.add('tree');
if (isSub) tree.classList.add('sub');
docFragment.appendChild(tree);
// entries.forEach(async entry => {
entries.forEach(async (entry, idx) => {
let key = entry[0];
let value = entry[1];
let childState;
if (value.filesCount == 0) {
childState = 'empty';
} else {
childState = 'filled';
}
let treeItemWrap = document.createElement('div');
treeItemWrap.classList.add('tree-item-wrap');
treeItemWrap.classList.add(`depth${value.depth}`);
treeItemWrap.classList.add(childState);
treeItemWrap.classList.add('folder');
treeItemWrap.dataset.resourcePath = value.resourcePath;
treeItemWrap.dataset.id = value.dataId;
treeItemWrap.dataset.size = value.size;
tree.appendChild(treeItemWrap);
if (FOLDER_KEEP_POLICY_ACTIVE && value.depth == 3 && value.filesCount < FOLDER_KEEP_FILE_THRESHOLD) {
if (value.folderType == null || value.folderType == undefined) {
treeItemWrap.classList.add('default-folder');
} else {
treeItemWrap.classList.add(`${value.folderType}-folder`);
}
let lastFolderActDate = new Date(value.lastFolderActDate);
let expiryDate = getExpiryDate(lastFolderActDate);
if (expiryDate.days < 1) {
treeItemWrap.classList.add('expired-warn');
// 타이머 동작 부분
let tick = () => {
let expiryDate = getExpiryDate(lastFolderActDate);
if (!expiryDate.formatted) {
if (treeItemWrap.countdownTimer) {
clearInterval(treeItemWrap.countdownTimer);
treeItemWrap.countdownTimer = null;
}
// 폴더 삭제
removeExpiredFolder(value);
return;
}
};
// 즉시 1회 갱신 후 1초 간격으로 갱신
tick();
treeItemWrap.countdownTimer = setInterval(tick, 1000);
}
}
let treeItem = document.createElement('div');
treeItem.classList.add('tree-item');
let nameWrap = document.createElement('div');
nameWrap.classList.add('name-wrap');
let image = document.createElement('div');
image.classList.add('image');
let textWrap = document.createElement('div');
textWrap.classList.add('text-wrap');
let nameText = document.createElement('div');
nameText.classList.add('name-text');
nameText.innerHTML = key;
textWrap.appendChild(nameText);
if (value.depth == 3) nameWrap.appendChild(image);
nameWrap.appendChild(textWrap);
treeItem.appendChild(nameWrap);
let addPermissionBadge = false, permissionEng = '', permissionKor = '';
if (value.permission == 0) {
addPermissionBadge = true;
permissionEng = 'sub-master';
permissionKor = '관리';
}
if (value.permission == 8) {
addPermissionBadge = true;
permissionEng = 'security-worker';
permissionKor = '보안';
}
if (value.permission == 4) {
addPermissionBadge = true;
permissionEng = 'worker';
permissionKor = '일반';
}
// if (value.permission == 2) {
// addPermissionBadge = true;
// permissionEng = 'uploader';
// permissionKor = '업로더';
// }
if (addPermissionBadge) {
let permissionBadge = document.createElement('div');
permissionBadge.classList.add(`user-permission-${permissionEng}`);
// permissionBadge.style.marginLeft = '8px';
let h6 = document.createElement('h6');
h6.innerText = permissionKor;
permissionBadge.appendChild(h6);
nameWrap.appendChild(permissionBadge);
}
// depth3일 때 파일 개수 표시
if (isSub) {
let fileCount = document.createElement('div');
fileCount.classList.add('file-count');
// 파일 개수가 3개 미만이면, lastFolderActDate랑 현재 날짜 비교해서 만료 기간 계산
// 만료 기간 지나면 폴더 삭제 및 하위 파일 휴지통으로 이동
if (FOLDER_KEEP_POLICY_ACTIVE && value.filesCount < FOLDER_KEEP_FILE_THRESHOLD) {
// 트리 생성할 때 이미 만료 기간이 지난 폴더는 바로 삭제
let lastFolderActDate = new Date(value.lastFolderActDate);
let expiryDate = getExpiryDate(lastFolderActDate);
if (!expiryDate.formatted) {
// 폴더 삭제
removeExpiredFolder(value);
return;
}
let countdown = document.createElement('div');
countdown.classList.add('countdown');
let countdownText = document.createElement('div');
countdownText.classList.add('text');
countdownText.innerHTML = `D-${expiryDate.days}`;
countdown.appendChild(countdownText);
treeItem.appendChild(countdown);
let countdownTooltip;
treeItem.addEventListener('pointerenter', () => {
// 마우스가 treeItem에 진입하면 기존 countdownTooltip 제거
removeCountdownTooltip();
// treeItem이랑 countdownTooltip이 세로 기준으로 가운데 정렬 되도록 위치 조정
let treeItemRect = treeItemWrap.getBoundingClientRect();
let rectTop = treeItemRect.top;
let rectLeft = treeItemRect.left;
let rectWidth = treeItemRect.width;
let rectHeight = treeItemRect.height;
let top = rectTop + (rectHeight/2);
let left = rectLeft + rectWidth - 8;
countdownTooltip = document.createElement('div');
countdownTooltip.classList.add('countdown-tooltip');
countdownTooltip.style.top = `${pxToRem(top)}rem`;
countdownTooltip.style.left = `${pxToRem(left)}rem`;
// countdownTooltip 내용 구성 (메시지, 기간 연장 버튼)
let countdownTooltipWrap = document.createElement('div');
countdownTooltipWrap.classList.add('wrap');
let message1 = document.createElement('pre');
message1.classList.add('message');
message1.innerHTML = `해당 폴더의 파일 개수가 <span class="bold color">${FOLDER_KEEP_FILE_THRESHOLD}개</span> 미만이므로,`;
let message2 = document.createElement('pre');
message2.classList.add('message');
message2.innerHTML = `만료 기간이 지나면 자동으로 삭제되고,`;
let message3 = document.createElement('pre');
message3.classList.add('message');
message3.innerHTML = `폴더 내 파일은 휴지통으로 이동됩니다.`;
let message4 = document.createElement('pre');
message4.classList.add('message', 'expiry-date');
let countdownSpan = document.createElement('span');
countdownSpan.classList.add('bold', 'color', 'countdown');
countdownSpan.textContent = 'dd일 hh시간 mm분 ss초'; // 초기 표시
message4.innerHTML = `</div><span class="bold">만료 기간</span>`;
message4.appendChild(countdownSpan);
let renewExpiryDateBtn = document.createElement('div');
renewExpiryDateBtn.classList.add('renew-expiry-date-btn');
renewExpiryDateBtn.dataset.resourcePath = value.resourcePath;
renewExpiryDateBtn.dataset.dataId = value.dataId;
let renewExpiryDateBtnText = document.createElement('div');
renewExpiryDateBtnText.classList.add('text');
renewExpiryDateBtnText.classList.add('ft-12');
renewExpiryDateBtnText.innerHTML = '만료 기간 갱신';
renewExpiryDateBtn.appendChild(renewExpiryDateBtnText);
countdownTooltipWrap.appendChild(message1);
countdownTooltipWrap.appendChild(message2);
countdownTooltipWrap.appendChild(message3);
countdownTooltipWrap.appendChild(message4);
countdownTooltipWrap.appendChild(renewExpiryDateBtn);
let devRenewExpiryDateBtn;
if (vars.permission.checkPermission('dev-menu')) {
devRenewExpiryDateBtn = document.createElement('div');
devRenewExpiryDateBtn.classList.add('dev-renew-expiry-date-btn');
devRenewExpiryDateBtn.dataset.resourcePath = value.resourcePath;
devRenewExpiryDateBtn.dataset.dataId = value.dataId;
let devRenewExpiryDateBtnText = document.createElement('div');
devRenewExpiryDateBtnText.classList.add('text');
devRenewExpiryDateBtnText.classList.add('ft-12');
devRenewExpiryDateBtnText.innerHTML = '(개발자) 만료 기간 변경';
devRenewExpiryDateBtn.appendChild(devRenewExpiryDateBtnText);
countdownTooltipWrap.appendChild(devRenewExpiryDateBtn);
}
countdownTooltip.appendChild(countdownTooltipWrap);
document.querySelector('body').appendChild(countdownTooltip);
// 타이머 동작 부분
let lastFolderActDate = new Date(value.lastFolderActDate);
let tick = () => {
let expiryDate = getExpiryDate(lastFolderActDate);
if (!expiryDate.formatted) {
// 만료됨: 0초로 고정하거나, 툴팁을 닫을 수도 있습니다.
countdownSpan.textContent = '00일 00시간 00분 00초';
if (vars.countdownTimer) {
clearInterval(vars.countdownTimer);
vars.countdownTimer = null;
}
// 폴더 삭제
removeExpiredFolder(value);
return;
}
countdownSpan.textContent = expiryDate.formatted;
};
// 즉시 1회 갱신 후 1초 간격으로 갱신
tick();
vars.countdownTimer = setInterval(tick, 1000);
// 편의성 - countdownTooltip이 화면 영역 아래로 들어가지 않도록 위치 조정
if (countdownTooltip.offsetHeight + rectTop > window.innerHeight) {
countdownTooltip.style.top = `${pxToRem(window.innerHeight - (countdownTooltip.offsetHeight/2) - 1)}rem`;
}
// 마우스가 countdownTooltip을 벗어나면 countdownTooltip 제거
countdownTooltip.addEventListener('pointerleave', () => {
removeCountdownTooltip();
})
// renewExpiryDateBtn 클릭 이벤트 추가
renewExpiryDateBtn.addEventListener('click', async (e) => {
let renewExpiryDateParams = {
userInfoString: vars.userInfoString,
storageType: vars.storageType,
resourcePath: value.resourcePath,
dataId: value.dataId,
depth3DataIdArr: [e.target.dataset.dataId],
}
let renewExpiryDateRes = await axios.post(`${vars.path_name}/renewExpiryDate`, { params: renewExpiryDateParams });
if (renewExpiryDateRes.data.message == 'renewExpiryDate_success') {
console.log('renewExpiryDate_success');
// renewExpiryDateBtn을 클릭하면 countdownTooltip dom 제거 및 툴팁타이머, 트리아이템타이머 제거
removeCountdownTooltip();
clearInterval(document.querySelector(`.tree-item-wrap[data-resource-path="${value.resourcePath}"]`).countdownTimer);
document.querySelector(`.tree-item-wrap[data-resource-path="${value.resourcePath}"]`).countdownTimer = null;
let toggleParams = {
text: `'${key}' 폴더의 만료 기간이 ${FOLDER_KEEP_DAYS_THRESHOLD}일로 갱신되었습니다.`,
// text: `'${key}' 폴더의 만료 기간이 ${FOLDER_KEEP_DAYS_THRESHOLD-1}일로 갱신되었습니다.`,
type: 'alertModal'
};
toggleModal(true, toggleParams);
}
})
// 개발자용 devRenewExpiryDateBtn 클릭 이벤트 추가
if (devRenewExpiryDateBtn) {
devRenewExpiryDateBtn.addEventListener('click', async (e) => {
let toggleParams = {
text: `<span>입력한 초(sec)만큼 만료 기간이 변경됩니다.</span><span>(입력 안하고 확인을 누르면 15일로 변경됩니다.)</span>`,
type: 'devRenewExpiryDate',
resourcePath: value.resourcePath,
dataId: value.dataId,
depth3DataIdArr: [e.target.dataset.dataId],
folderName: key
};
toggleModal(true, toggleParams);
})
}
})
treeItem.addEventListener('pointerleave', (e) => {
// 마우스가 treeItem을 벗어난 직후 마우스 위치 대상이 countdownTooltip인 경우 리턴
let next = e.relatedTarget;
if (next && next.closest('.countdown-tooltip')) return;
// 마우스가 treeItem을 벗어나면 countdownTooltip 제거
removeCountdownTooltip();
})
} else {
let fileCountText = document.createElement('div');
fileCountText.classList.add('file-count-text');
fileCountText.innerHTML = addCommasToNumber(value.filesCount);
fileCount.appendChild(fileCountText);
treeItem.appendChild(fileCount);
}
}
treeItemWrap.appendChild(treeItem);
if (value.depth == 2) renderTree(treeItemWrap, value.child.folder, true);
treeItem.addEventListener('click', async (e) => {
vars.lastSelectTarget = treeItem;
let parent = treeItem.parentElement;
if (value.depth == 2) {
// console.log('depth2');
// let targetTree = 'main';
// changeTreeItemStyle(treeItem, targetTree);
}
if (value.depth == 3) {
if (vars.lastHeaderBtn) targetFocus(vars.lastHeaderBtn);
listContainer.querySelector('.list-body .list-item-wrap').innerHTML = '';
listContainer.style.display = 'flex';
listNotice.style.display = 'none';
resetViewer();
let isMainTree = e.target.closest('.archive-main-left .tree-container');
// let isModalTree = e.target.closest('.archive-main-center .list-container');
let targetTree;
if (isMainTree) targetTree = 'main';
// if (isModalTree) targetTree = 'modal';
let selectedDepth3TreeItemWrap = document.querySelector(`.archive-${targetTree} .tree-item-wrap.depth3.selected`);
if (selectedDepth3TreeItemWrap) selectedDepth3TreeItemWrap.classList.remove('selected');
treeItemWrap.classList.add('selected');
// document.querySelector('.control-box .file-area-mode-btn.selected')?.classList.remove('selected');
// document.querySelector(`.control-box .file-area-mode-btn.${fileAreaMode}`).classList.add('selected');
// let targetTree = 'main';
// if (option.modalTree) {
// targetTree = 'modal';
// compareMovePath(resourcePath);
// }
changeTreeItemStyle(treeItemWrap, targetTree);
// let childFileData = getDataFromTreeObject(resourcePath, 'folder', vars.allTreeObject).data.child.file;
// console.log(childFileData);
// let listParent = document.querySelector('.main-center .list-container .list-wrap.list-body .list-item-wrap');
// renderList(listParent, childFileData);
// let pageRanderingOption = {
// isInit: isInit,
// // favorite: 즐겨찾기 한 경로
// }
// .list-wrap.list-header, .list-wrap.list-body에 dataset.resourcePath 설정
document.querySelector('.archive-main-center .list-container .list-wrap.list-header').dataset.resourcePath = value.resourcePath;
document.querySelector('.archive-main-center .list-container .list-wrap.list-body').dataset.resourcePath = value.resourcePath;
// 트리 폴더 버튼 클릭할 때 리스트 생성 순서 뒤집히지 않도록 list sort 초기화
resetSort();
// 기존 리스트 영역 화면이 위치 수정 모드인 경우 초기화
if (vars.mapMode == 'edit') {
vars.mapMode = 'normal';
document.querySelector('.map-container').classList.remove('edit-mode');
}
// 다중 선택 아이템 배열 초기화
vars.multiSelectListItemArr = [];
let pageRanderingOption = {
scope: 'list',
resourcePath: value.resourcePath
}
await preparePageRendering(pageRanderingOption);
// 파일 이동 모드(listViewerCover display flex)일 때 트리 depth3 메뉴 클릭하면 선택 폴더 경로 변경
let listViewerCover = document.querySelector('.list-viewer-cover');
if (listViewerCover.style.display == 'flex') {
listViewerCover.querySelector('.new-path .value').textContent = value.resourcePath;
} else {
//// vars에서 마지막 선택 아이템을 저장한 속성들 초기화 - renderTree
// 파일 이동 모드에서 초기화하면 이동 전, 후 경로를 가져올 수 없어서 파일 이동 모드 아닐 때에만 초기화
vars.lastListItem = undefined;
vars.lastListGroupTarget = undefined;
vars.lastContextTarget = undefined;
}
}
})
})
parent.appendChild(docFragment);
document.querySelector('.archive-main-left .tree-title').classList.add('selected');
} else {
if (!isSub) {
treeNotice.style.display = 'flex';
treeWrap.style.display = 'none';
}
}
}
async function renderRecycleBin(parent, data) {
let listItemWrap = document.querySelector('.recycle-bin-modal .list-container .list-wrap.list-body .list-item-wrap');
listItemWrap.innerHTML = '';
listItemWrap.style.display = 'flex';
initFileAreaUI('list');
await createRecycleBinItems(parent, data);
}
function createRecycleBinItems(parent, data) {
let docFragment = document.createDocumentFragment();
let entries = sortData(data);
entries.forEach(async (entry, idx) => {
let key = entry[0].split('___[recycle-bin]___')[0];
let value = entry[1];
let ext = (value.ext).toLowerCase();
// 파일 resourcePath 맨 마지막에 / 붙어있으면 제거
let resourcePathSplit = value.resourcePath.split('/');
if (resourcePathSplit[resourcePathSplit.length-1] == '') resourcePathSplit.pop();
let resourcePath = resourcePathSplit.join('/');
let dataId = value.dataId;
let listItem = document.createElement('div');
listItem.classList.add('list-item');
listItem.classList.add(`depth${value.depth}`);
listItem.classList.add('file');
listItem.classList.add('main-list-item');
listItem.dataset.resourcePath = resourcePath;
listItem.dataset.id = dataId;
listItem.dataset.size = value.size;
listItem.dataset.mainFileName = `${dataId}_${resourcePath}`;
// 확장자가 유효한 경우 클래스에 ext 추가
if (isValidExt(ext)) listItem.classList.add(ext);
let wrap = document.createElement('div');
wrap.classList.add('wrap');
// 파일 타입
// let type = document.createElement('div');
// type.classList.add('type');
// type.classList.add('list-item-data');
// let typeImage = document.createElement('div');
// typeImage.classList.add('type-image');
// type.appendChild(typeImage);
// 파일명
let name = document.createElement('div');
name.classList.add('name');
name.classList.add('list-item-data');
let typeImage = document.createElement('div');
typeImage.classList.add('type-image');
name.appendChild(typeImage);
// let typeImage = document.createElement('div');
// typeImage.classList.add('type-image');
// name.appendChild(typeImage);
let nameText = document.createElement('div');
nameText.classList.add('name-text');
nameText.classList.add('text');
nameText.classList.add('ft-14');
nameText.innerHTML = key;
name.appendChild(nameText);
// 원래 위치
let originalLocation = document.createElement('div');
originalLocation.classList.add('original-location');
originalLocation.classList.add('list-item-data');
let originalLocationText = document.createElement('div');
originalLocationText.classList.add('text');
originalLocationText.classList.add('ft-12');
originalLocationText.innerHTML = splitTopPathAndTargetName(resourcePath).topPath;
originalLocation.appendChild(originalLocationText);
// 삭제자
let removeUser = document.createElement('div');
removeUser.classList.add('remove-user');
removeUser.classList.add('list-item-data');
let removeUserText = document.createElement('div');
removeUserText.classList.add('text');
removeUserText.classList.add('ft-12');
removeUserText.innerHTML = value.modUserNm;
removeUser.appendChild(removeUserText);
// 삭제일자
let removeDate = document.createElement('div');
removeDate.classList.add('remove-date');
removeDate.classList.add('list-item-data');
let removeDateText = document.createElement('div');
removeDateText.classList.add('text');
removeDateText.classList.add('ft-12');
removeDateText.innerHTML = '-';
if (value.modDate) {
let date = new Date(value.modDate);
removeDateText.innerHTML = `${changeDateFormat(date.toLocaleDateString('ko-KR'))} ${changeTimeFormat(date.toLocaleTimeString('en-US', {hour12: false}))}`;
}
removeDate.appendChild(removeDateText);
// 작성자
let author = document.createElement('div');
author.classList.add('author');
author.classList.add('list-item-data');
let authorText = document.createElement('div');
authorText.classList.add('text');
authorText.classList.add('ft-12');
authorText.innerHTML = (value.authorNm) ? value.authorNm : value.userNm;
author.appendChild(authorText);
// 등록자
let uploader = document.createElement('div');
uploader.classList.add('uploader');
uploader.classList.add('list-item-data');
let uploaderText = document.createElement('div');
uploaderText.classList.add('text');
uploaderText.classList.add('ft-12');
uploaderText.innerHTML = '-';
if (value.userNm) uploaderText.innerHTML = value.userNm;
uploader.appendChild(uploaderText);
// 등록일자
let createDate = document.createElement('div');
createDate.classList.add('create-date');
createDate.classList.add('list-item-data');
let createDateText = document.createElement('div');
createDateText.classList.add('text');
createDateText.classList.add('ft-12');
createDateText.innerHTML = '-';
if (value.createDate) {
let date = new Date(value.createDate);
createDateText.innerHTML = `${changeDateFormat(date.toLocaleDateString('ko-KR'))} ${changeTimeFormat(date.toLocaleTimeString('en-US', {hour12: false}))}`;
}
createDate.appendChild(createDateText);
// 용량
let size = document.createElement('div');
size.classList.add('size');
size.classList.add('list-item-data');
let sizeText = document.createElement('div');
sizeText.classList.add('text');
sizeText.classList.add('ft-12');
sizeText.innerHTML = formatBytes(value.size);
if (value.size >= 1073741824) sizeText.classList.add('large');
size.appendChild(sizeText);
// 상태
let state = document.createElement('div');
state.classList.add('state');
state.classList.add('list-item-data');
let stateText = document.createElement('div');
stateText.classList.add('state-text');
stateText.classList.add('ft-12');
if (value.isSupported) {
// 지원
if (value.needConvert && !value.isConverted) {
state.classList.add('convert');
stateText.innerHTML = '변환필요';
} else {
// 열람가능 - 변환 된 파일 또는 변환 필요 없는 파일일 때
state.classList.add('viewable');
stateText.innerHTML = '열람가능';
}
} else {
// 미지원
state.classList.add('unsupport');
stateText.innerHTML = '미지원';
}
state.appendChild(stateText);
// wrap.appendChild(type);
wrap.appendChild(name);
wrap.appendChild(originalLocation);
wrap.appendChild(removeUser);
wrap.appendChild(removeDate);
wrap.appendChild(author);
wrap.appendChild(uploader);
wrap.appendChild(createDate);
wrap.appendChild(size);
wrap.appendChild(state);
listItem.appendChild(wrap);
docFragment.appendChild(listItem);
wrap.addEventListener('dblclick', async (e) => {
openNewWindowViewer();
})
wrap.addEventListener('click', async (e) => {
let isCtrl = e.ctrlKey;
let isShift = e.shiftKey;
let target = e.target;
// 좌클릭이 아닌 경우 무시
if (e.button !== 0) return;
let clickedListItem = target.closest('.list-item');
if (!clickedListItem) return;
let allItems = [...document.querySelectorAll('.recycle-bin-modal .list-container .list-body .list-item')];
let clickedIndex = allItems.indexOf(clickedListItem);
let isClickFinished = false;
// --- Ctrl + Shift ---
if (!isClickFinished && isCtrl && isShift) {
isClickFinished = true;
let anchorIndex = allItems.indexOf(vars.lastListItem_bin);
if (anchorIndex == -1) anchorIndex = 0;
let [start, end] = [anchorIndex, clickedIndex].sort((a, b) => a - b);
for (let i = start; i <= end; i++) {
let item = allItems[i];
if (!vars.multiSelectListItemArr_bin.includes(item)) {
vars.multiSelectListItemArr_bin.push(item);
}
item.classList.add('selected');
item.classList.add('group-style');
}
isClickFinished = true;
}
// --- Shift only ---
if (!isClickFinished && isShift) {
let anchorIndex = allItems.indexOf(vars.lastListItem_bin);
if (anchorIndex == -1) anchorIndex = 0;
// 기존 선택 해제
vars.multiSelectListItemArr_bin = [];
allItems.forEach(item => {
item.classList.remove('selected');
});
let [start, end] = [anchorIndex, clickedIndex].sort((a, b) => a - b);
for (let i = start; i <= end; i++) {
let item = allItems[i];
vars.multiSelectListItemArr_bin.push(item);
item.classList.add('selected');
}
//// shift 다중 선택 후 delete키로 휴지통으로 이동
vars.lastSelectTarget_bin = vars.multiSelectListItemArr_bin[0];
isClickFinished = true;
}
// --- Ctrl only ---
if (!isClickFinished && isCtrl) {
if (vars.multiSelectListItemArr_bin.includes(clickedListItem)) {
vars.multiSelectListItemArr_bin = vars.multiSelectListItemArr_bin.filter(item => item !== clickedListItem);
clickedListItem.classList.remove('selected');
} else {
vars.multiSelectListItemArr_bin.push(clickedListItem);
clickedListItem.classList.add('selected');
}
vars.lastListItem_bin = clickedListItem;
isClickFinished = true;
}
if (!isClickFinished) {
// --- 일반 클릭 (기존 선택 제거 후 단일 선택 + 동작 실행) ---
vars.multiSelectListItemArr_bin = [];
document.querySelectorAll('.recycle-bin-modal .list-container .list-body .list-item').forEach((elem) => {
elem.classList.remove('selected');
});
clickedListItem.classList.add('selected');
vars.lastListItem_bin = clickedListItem;
if (!isCtrl && !isShift) {
vars.lastSelectTarget_bin = clickedListItem;
}
changeListItemStyle(clickedListItem, '리스트 아이템 클릭');
}
await syncGroupStyle();
});
})
parent.appendChild(docFragment);
}
export async function renderList(parent, data, isSub, subCategory) {
// 폴더타입 지정
// fileAreaMode = document.querySelector('.control-box .file-area-mode-btn.selected').id;
let fileAreaMode = vars.lastFileAreaMode;
// 컨트롤 박스 표시
toggleControlBox(true, fileAreaMode);
listContainer.classList.remove('list');
listContainer.classList.add(fileAreaMode);
let listItemWrap = listContainer.querySelector('.list-body .list-item-wrap');
listItemWrap.innerHTML = '';
// 파일 영역 모드에 맞게 파일 영역 UI 초기화
initFileAreaUI(fileAreaMode);
// 폴더타입에 따라 아이템 생성
if (fileAreaMode == 'list') {
if (!isSub) {
// 아카이브 우측 영역 표시
let option = {
from: '리스트 렌더링'
}
await toggleArchiveMainRight(true, option);
}
await createListItems(parent, data, isSub, subCategory);
}
if (fileAreaMode == 'grid') {
// 아카이브 우측 영역 숨김
let option = {
from: '그리드 렌더링'
}
await toggleArchiveMainRight(false, option);
// vars.listOrder 배열 초기화 후 createListOrder 함수에서 vars.listOrder 배열에 리스트 기준 순서로 파일명, 데이터id 담긴 객체 추가
vars.listOrder = [];
await createListOrder(data, isSub, subCategory);
// newData에 depth4, depth5 데이터 추가해서 레벨 평탄화
let entries = Object.entries(data);
let newData = {};
entries.forEach(async (entry) => {
let key = entry[0];
let value = entry[1];
newData[key] = value;
subCategoryList.forEach(async (ele) => {
let sub = getDataFromTreeObject(`${value.resourcePath}_${ele}`, 'folder', vars.allTreeObject).data;
if (sub) {
let subEntries = Object.entries(sub.child.file);
subEntries.forEach(async (subEntry) => {
let subKey = subEntry[0];
let subValue = subEntry[1];
subValue.name = `${key}___${subKey}`;
newData[`${key}___${subKey}`] = subValue;
})
}
})
})
// dataId 기준으로 newData 정렬한 map 생성
let newDataById = new Map(
Object.values(newData).map(item => [item.dataId, item])
);
// vars.listOrder 순서대로 dataId 사용해서 map에서 데이터 꺼내서 orderedData에 추가
let orderedData = {};
for (const item of vars.listOrder) {
const data = newDataById.get(item.dataId);
if (data) orderedData[data.name] = data;
}
// 평탄화, 리스트 기준 정렬 된 orderedData로 그리드 화면 생성
await createGridItems(parent, orderedData);
}
if (fileAreaMode == 'map') {
await createMapItems(data);
}
if (vars.multiSelectListItemArr.length > 0) {
let newMultiSelectListArr = [];
vars.multiSelectListItemArr.forEach(item => {
let dataId = item.dataset.id;
let newItem = listContainer.querySelector(`.list-item[data-id="${dataId}"]`);
if (newItem) {
newItem.classList.add('selected');
newItem.classList.add('group-style');
newMultiSelectListArr.push(newItem);
}
});
vars.multiSelectListItemArr = newMultiSelectListArr;
if (vars.lastListItem) await changeListItemStyle(vars.lastListItem, 'renderList 끝부분');
await syncGroupStyle();
}
}
function createListItems(parent, data, isSub, subCategory) {
let docFragment = document.createDocumentFragment();
let entries = sortData(data);
entries.forEach(async (entry, idx) => {
let key = entry[0];
let value = entry[1];
let ext = (value.ext).toLowerCase();
// 파일 resourcePath 맨 마지막에 / 붙어있으면 제거
let resourcePathSplit = value.resourcePath.split('/');
if (resourcePathSplit[resourcePathSplit.length-1] == '') resourcePathSplit.pop();
let resourcePath = resourcePathSplit.join('/');
let dataId = value.dataId;
let listItem = document.createElement('div');
listItem.classList.add('list-item');
listItem.classList.add(`depth${value.depth}`);
listItem.classList.add('file');
listItem.dataset.resourcePath = resourcePath;
listItem.dataset.id = dataId;
listItem.dataset.size = value.size;
listItem.dataset.mainFileName = value.mainFileName;
// 확장자가 유효한 경우 클래스에 ext 추가
if (isValidExt(ext)) listItem.classList.add(ext);
let wrap = document.createElement('div');
wrap.classList.add('wrap');
// 파일 타입
// let type = document.createElement('div');
// type.classList.add('type');
// type.classList.add('list-item-data');
// let typeImage = document.createElement('div');
// typeImage.classList.add('type-image');
// type.appendChild(typeImage);
// 파일명
let name = document.createElement('div');
name.classList.add('name');
name.classList.add('list-item-data');
if (isSub) {
listItem.classList.add('sub-list-item');
let subSign = document.createElement('div');
subSign.classList.add('sub-sign');
let symbol = document.createElement('div');
symbol.classList.add('symbol');
symbol.innerText = '└';
let subSignText = document.createElement('div');
subSignText.classList.add('text');
subSign.classList.add(subCategory);
subSignText.innerText = subCategoryData[subCategory].korean;
subSign.appendChild(symbol);
subSign.appendChild(subSignText);
name.appendChild(subSign);
} else {
listItem.classList.add('main-list-item');
let typeImage = document.createElement('div');
typeImage.classList.add('type-image');
name.appendChild(typeImage);
}
// let typeImage = document.createElement('div');
// typeImage.classList.add('type-image');
// name.appendChild(typeImage);
let nameText = document.createElement('div');
nameText.classList.add('name-text');
nameText.classList.add('text');
nameText.classList.add('ft-14');
nameText.innerHTML = key;
name.appendChild(nameText);
// 권한설정 - 파일에 권한설정?
// let addPermissionBadge = false, permissionEng = '', permissionKor = '';
// if (value.permission == 0) {
// nameText.classList.add('sub-master');
// addPermissionBadge = true;
// permissionEng = 'sub-master';
// permissionKor = '부관리자';
// }
// if (value.permission == 8) {
// nameText.classList.add('worker');
// addPermissionBadge = true;
// permissionEng = 'worker';
// permissionKor = '참여자';
// }
// if (addPermissionBadge) {
// let permissionBadge = document.createElement('div');
// permissionBadge.classList.add(`user-permission-${permissionEng}`);
// permissionBadge.style.marginLeft = '8px';
// // let image = document.createElement('img');
// // image.src = `/main/img/permission/${permissionEng}.svg`;
// // image.alt = permissionKor;
// let h6 = document.createElement('h6');
// h6.innerText = permissionKor;
// // permissionBadge.appendChild(image);
// permissionBadge.appendChild(h6);
// }
// name.appendChild(permissionBadge);
// 작성자
let author = document.createElement('div');
author.classList.add('author');
author.classList.add('list-item-data');
let authorText = document.createElement('div');
authorText.classList.add('text');
authorText.classList.add('ft-12');
authorText.innerHTML = (value.authorNm) ? value.authorNm : value.userNm;
author.appendChild(authorText);
// 등록자
let uploader = document.createElement('div');
uploader.classList.add('uploader');
uploader.classList.add('list-item-data');
let uploaderText = document.createElement('div');
uploaderText.classList.add('text');
uploaderText.classList.add('ft-12');
uploaderText.innerHTML = '-';
if (value.userNm) uploaderText.innerHTML = value.userNm;
uploader.appendChild(uploaderText);
// 등록일자
let createDate = document.createElement('div');
createDate.classList.add('create-date');
createDate.classList.add('list-item-data');
let createDateText = document.createElement('div');
createDateText.classList.add('text');
createDateText.classList.add('ft-12');
createDateText.innerHTML = '-';
if (value.createDate) {
let date = new Date(value.createDate);
createDateText.innerHTML = `${changeDateFormat(date.toLocaleDateString('ko-KR'))} ${changeTimeFormat(date.toLocaleTimeString('en-US', {hour12: false}))}`;
}
createDate.appendChild(createDateText);
// 용량
let size = document.createElement('div');
size.classList.add('size');
size.classList.add('list-item-data');
let sizeText = document.createElement('div');
sizeText.classList.add('text');
sizeText.classList.add('ft-12');
sizeText.innerHTML = formatBytes(value.size);
if (value.size >= 1073741824) sizeText.classList.add('large');
size.appendChild(sizeText);
// 상태
let state = document.createElement('div');
state.classList.add('state');
state.classList.add('list-item-data');
let stateText = document.createElement('div');
stateText.classList.add('state-text');
stateText.classList.add('ft-12');
let convertBtn = document.createElement('div');
convertBtn.classList.add('convert-btn');
let convertBtnImage = document.createElement('div');
let convertBtnText = document.createElement('div');
convertBtnImage.classList.add('convert-btn-image');
convertBtnText.classList.add('convert-btn-text');
convertBtnText.classList.add('ft-12');
convertBtn.appendChild(convertBtnImage);
convertBtn.appendChild(convertBtnText);
let addBtn = false;
if (value.isSupported) {
// 지원
if (vars.convertingDataArr.some(data => Number(data.dataId) === Number(dataId))) {
// 변환중
addBtn = true;
state.classList.add('working');
convertBtnText.innerHTML = '변환중';
} else {
if (value.needConvert && !value.isConverted) {
// ** 권한 관련
// 변환필요 - 변환 필요한 파일이면서 변환이 안 된 상태일 때
let permission = JSON.parse(vars.userInfoString).permission;
if (permission == 1) {
state.classList.add('convert');
stateText.innerHTML = '변환필요';
} else {
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);
// 메모유무 아이콘
let isMemo = document.createElement('div');
isMemo.classList.add('memo');
isMemo.classList.add('list-item-data');
let memoIconWrap = document.createElement('div');
memoIconWrap.classList.add('memo-item-wrap');
let memoIcon = document.createElement('img');
memoIcon.classList.add('memo-icon');
memoIcon.src = '/main/img/archive/icon__list-memo.svg';
if (!(value.memo == '' || value.memo == undefined || value.memo == null || value.memo == true)) memoIconWrap.appendChild(memoIcon);
isMemo.appendChild(memoIconWrap);
// 위치 텍스트 저장
let gpsData = document.createElement('div');
gpsData.classList.add('gps-data');
gpsData.style.display = 'none';
let gpsDataText = document.createElement('div');
gpsDataText.classList.add('text');
gpsDataText.innerHTML = `경도: ${(value.lon) ? value.lon : '없음'} / 위도: ${(value.lat) ? value.lat : '없음'}`;
gpsDataText.style.display = 'none';
gpsData.appendChild(gpsDataText);
// wrap.appendChild(type);
wrap.appendChild(name);
wrap.appendChild(author);
wrap.appendChild(uploader);
wrap.appendChild(createDate);
wrap.appendChild(size);
wrap.appendChild(state);
wrap.appendChild(isMemo);
wrap.appendChild(gpsData);
listItem.appendChild(wrap);
// 추가 파일이 있는 경우 추가 파일 생성
subCategoryList.forEach(async (ele) => {
let sub = getDataFromTreeObject(`${resourcePath}_${ele}`, 'folder', vars.allTreeObject).data;
if (sub) {
let subListItemWrap = document.createElement('div');
subListItemWrap.classList.add('list-item-wrap');
subListItemWrap.classList.add('sub-list-item-wrap');
listItem.appendChild(subListItemWrap);
renderList(subListItemWrap, sub.child.file, true, ele);
}
})
docFragment.appendChild(listItem);
if (vars.lastListItem?.dataset.resourcePath == resourcePath) {
// const memo = getDataFromTreeObject(value.resourcePath, 'file', vars.allTreeObject).data;
let res = await axios.get(`${vars.path_name}/getMemoInfo`,
{ params: { userInfoString: vars.userInfoString, resourcePath: resourcePath, dataId: dataId } }
);
let memo = res.data.result.memo;
setTimeout(() => {
renderMemo (memo, dataId);
}, 100);
}
wrap.addEventListener('dblclick', async (e) => {
openNewWindowViewer();
})
wrap.addEventListener('click', async (e) => {
let isCtrl = e.ctrlKey;
let isShift = e.shiftKey;
let target = e.target;
// 좌클릭이 아닌 경우 무시
if (e.button !== 0) return;
let clickedListItem = target.closest('.list-item');
if (!clickedListItem) return;
if (vars.lastHeaderBtn) targetFocus(vars.lastHeaderBtn);
if (vars.lastMainTreeItem) targetFocus(vars.lastMainTreeItem);
let allItems = [...document.querySelectorAll('.archive-main .list-container .list-body .list-item')];
let clickedIndex = allItems.indexOf(clickedListItem);
let isClickFinished = false;
// --- Ctrl + Shift ---
if (!isClickFinished && isCtrl && isShift) {
isClickFinished = true;
let anchorIndex = allItems.indexOf(vars.lastListItem);
if (anchorIndex == -1) anchorIndex = 0;
let [start, end] = [anchorIndex, clickedIndex].sort((a, b) => a - b);
for (let i = start; i <= end; i++) {
let item = allItems[i];
if (!vars.multiSelectListItemArr.includes(item)) {
vars.multiSelectListItemArr.push(item);
}
item.classList.add('selected');
item.classList.add('group-style');
}
isClickFinished = true;
resetViewer();
}
// --- Shift only ---
if (!isClickFinished && isShift) {
let anchorIndex = allItems.indexOf(vars.lastListItem);
if (anchorIndex == -1) anchorIndex = 0;
// 기존 선택 해제
vars.multiSelectListItemArr = [];
allItems.forEach(item => {
item.classList.remove('selected');
});
let [start, end] = [anchorIndex, clickedIndex].sort((a, b) => a - b);
for (let i = start; i <= end; i++) {
let item = allItems[i];
vars.multiSelectListItemArr.push(item);
item.classList.add('selected');
}
//// shift 다중 선택 후 delete키로 휴지통으로 이동
vars.lastSelectTarget = vars.multiSelectListItemArr[0];
isClickFinished = true;
resetViewer();
}
// --- Ctrl only ---
if (!isClickFinished && isCtrl) {
if (vars.multiSelectListItemArr.includes(clickedListItem)) {
vars.multiSelectListItemArr = vars.multiSelectListItemArr.filter(item => item !== clickedListItem);
clickedListItem.classList.remove('selected');
} else {
vars.multiSelectListItemArr.push(clickedListItem);
clickedListItem.classList.add('selected');
}
vars.lastListItem = clickedListItem;
isClickFinished = true;
resetViewer();
}
if (!isClickFinished) {
// --- 일반 클릭 (기존 선택 제거 후 단일 선택 + 동작 실행) ---
vars.multiSelectListItemArr = [];
document.querySelectorAll('.archive-main .list-container .list-body .list-item').forEach((elem) => {
elem.classList.remove('selected');
});
clickedListItem.classList.add('selected');
vars.lastListItem = clickedListItem;
if (!isCtrl && !isShift) {
vars.lastSelectTarget = clickedListItem;
let res = await axios.get(`${vars.path_name}/getMemoInfo`, {
params: { userInfoString: vars.userInfoString, resourcePath , dataId }
});
let memo = res.data.result.memo;
setTimeout(() => {
renderMemo(memo, dataId);
}, 100);
// 정보영역 표시
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.add('open');
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.remove('close');
vars.viewerConnectingTime = vars.viewerConnectingTimeValue;
if (value.isSupported == false || (value.needConvert == true && value.isConverted == false)) {
// 미지원 파일인 경우 -> value.isSupported == false
// 변환 필요 파일인 경우 -> value.needConvert == true && value.isConverted == false (변환해야되지만 아직 변환 안한 상태)
// 뷰어 프로그레스 표시 시간 0으로 설정
vars.viewerConnectingTime = 0;
}
renderViewer(resourcePath, dataId);
if (target === convertBtn && state.classList.contains('convert')) {
convertPdf(resourcePath, dataId);
}
} else {
}
changeListItemStyle(clickedListItem, '리스트 아이템 클릭');
}
await syncGroupStyle();
});
})
parent.appendChild(docFragment);
if (vars.lastSelectTarget && vars.lastSelectTarget.classList.contains('list-item')) {
let dataId = vars.lastSelectTarget.dataset.id;
let item = document.querySelector(`.list-item[data-id="${dataId}"]`);
if (item) {
changeListItemStyle(item, '리스트 아이템 생성');
targetFocus(item, 'instant');
}
}
}
async function createListOrder(data, isSub, subCategory) {
let entries = sortData(data);
entries.forEach(async (entry, idx) => {
let obj = {};
let key = entry[0];
let value = entry[1];
let dataId = value.dataId;
// 파일 resourcePath 맨 마지막에 / 붙어있으면 제거
let resourcePathSplit = value.resourcePath.split('/');
if (resourcePathSplit[resourcePathSplit.length-1] == '') resourcePathSplit.pop();
let resourcePath = resourcePathSplit.join('/');
obj.name = key;
if (isSub) obj.name = `${value.mainFileName}___${key}`;
obj.dataId = dataId;
// obj.author = (value.authorNm) ? value.authorNm : value.userNm;;
// obj.uploader = (value.userNm) ? value.userNm : '-';
// let date = new Date(value.createDate);
// obj.createDate = (value.createDate) ? `${changeDateFormat(date.toLocaleDateString('ko-KR'))} ${changeTimeFormat(date.toLocaleTimeString('en-US', {hour12: false}))}` : '-';
// obj.size = formatBytes(value.size);
// obj.isSub = isSub;
// obj.subCategory = subCategory;
vars.listOrder.push(obj);
subCategoryList.forEach(async (ele) => {
let sub = getDataFromTreeObject(`${resourcePath}_${ele}`, 'folder', vars.allTreeObject).data;
if (sub) {
await createListOrder(sub.child.file, true, ele);
}
})
})
}
async function createGridItems(parent, data) {
vars.curSortCol = document.querySelector('input[type="radio"][name="category"]:checked').value;
vars.curSortOrder = document.querySelector('input[type="radio"][name="sort"]:checked').value;
//// 썸네일 모드에서 vars.curSortCol이 none(라벨 숨김)으로 저장된 채 리스트 모드로 바꾼 뒤
//// 바로 파일명 정렬을 하면 무시되는 문제가 생기기 때문에 vars.curSortCol이 none인 경우 name으로 강제 저장
if (vars.curSortCol == 'none') vars.curSortCol = 'name';
let docFragment = document.createDocumentFragment();
let entries = Object.entries(data);
// let imageExtArr = ['jpg', 'jpeg', 'png'];
let videoExtArr = ['mp4', 'mov', 'webm'];
let currentTreeItem = vars.lastMainTreeItem;
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
let key = entry[0];
let value = entry[1];
let ext = (value.ext).toLowerCase();
// 파일 resourcePath 맨 마지막에 / 붙어있으면 제거
let resourcePathSplit = value.resourcePath.split('/');
if (resourcePathSplit[resourcePathSplit.length-1] == '') resourcePathSplit.pop();
let resourcePath = resourcePathSplit.join('/');
let dataId = value.dataId;
let gridItem = document.createElement('div');
gridItem.classList.add('list-item');
gridItem.classList.add('grid-item');
gridItem.classList.add(`depth${value.depth}`);
gridItem.classList.add('file');
gridItem.dataset.resourcePath = resourcePath;
gridItem.dataset.id = dataId;
gridItem.dataset.size = value.size;
gridItem.dataset.mainFileName = value.mainFileName;
// 확장자가 유효한 경우 클래스에 ext 추가
if (isValidExt(ext)) gridItem.classList.add(ext);
// 마지막으로 선택한 리스트 아이템에 그룹상태 클래스 추가
let wrap = document.createElement('div');
wrap.classList.add('wrap');
// 동영상 표시 아이콘
let play = document.createElement('div');
play.classList.add('play');
let playImage = document.createElement('div');
playImage.classList.add('image');
play.appendChild(playImage);
// 그리드 아이템 선택됐을 때 강조표시용 div
let highlight = document.createElement('div');
highlight.classList.add('highlight');
// 추가 파일일 때(value.depth == 5) 추가 파일 종류 뱃지 추가
if (value.depth == 5) {
gridItem.classList.add('sub-list-item');
gridItem.classList.add(value.subCategory);
let subSign = document.createElement('div');
subSign.classList.add('sub-sign');
let subSignText = document.createElement('div');
subSignText.classList.add('text');
subSignText.classList.add('ft-14');
subSign.classList.add(value.subCategory);
subSignText.innerText = subCategoryData[value.subCategory].korean;
subSign.appendChild(subSignText);
wrap.appendChild(subSign);
} else {
gridItem.classList.add('main-list-item');
}
// 위치 유무 아이콘 및 위치 텍스트 저장
let gpsData = document.createElement('div');
gpsData.classList.add('gps-data');
gpsData.classList.add('text-wrap');
let gpsDataWrap = document.createElement('div');
gpsDataWrap.classList.add('gps-data-wrap');
let gpsDataTooltip = document.createElement('div');
gpsDataTooltip.classList.add('tooltip');
gpsDataTooltip.classList.add('ft-14');
if (value.lon && value.lat) {
gpsData.classList.add('gps-data-yes');
gpsDataTooltip.innerHTML = '위치있음';
} else {
gpsData.classList.add('gps-data-no');
gpsDataTooltip.innerHTML = '위치없음';
}
let gpsDataImage = document.createElement('div');
gpsDataImage.classList.add('image');
let gpsDataText = document.createElement('div');
gpsDataText.classList.add('text');
gpsDataText.innerHTML = `경도: ${(value.lon) ? value.lon : '없음'} / 위도: ${(value.lat) ? value.lat : '없음'}`;
gpsDataText.style.display = 'none';
gpsDataWrap.appendChild(gpsDataTooltip);
gpsDataWrap.appendChild(gpsDataImage);
gpsDataWrap.appendChild(gpsDataText);
gpsData.appendChild(gpsDataWrap);
// 썸네일
// let extType;
// if (imageExtArr.includes(ext)) extType = 'img';
// if (videoExtArr.includes(ext)) extType = 'video';
// let defaultThumbnail = `/main/img/archive/file_extension/${extType}.svg`;
// let defaultThumbnail = `/main/img/archive/file_extension/${ext}.svg`;
// let thumbnail = document.createElement('img');
// thumbnail.classList.add('thumbnail');
// thumbnail.src = defaultThumbnail;
// thumbnail.onerror = function() {
// this.onerror = null; // 무한루프 방지
// this.src = defaultThumbnail;
// };
let thumbnail = document.createElement('div');
thumbnail.classList.add('thumbnail');
let thumbnailImg = document.createElement('img');
thumbnail.appendChild(thumbnailImg);
// 파일명
let name = document.createElement('div');
name.classList.add('name');
name.classList.add('thumbnail-title');
name.classList.add('text-wrap');
let nameText = document.createElement('div');
nameText.classList.add('name-text');
nameText.classList.add('text');
nameText.classList.add('ft-14');
nameText.classList.add('ellipsis');
// 그리드 아이템 파일명 줄바꿈 적용
// nameText.classList.add('line-break');
nameText.innerHTML = key;
if (key.split('___')[1] && key.split('___')[0] == value.mainFileName) {
nameText.innerHTML = key.split('___')[1];
}
name.appendChild(nameText);
// 등록자
let uploader = document.createElement('div');
uploader.classList.add('uploader');
uploader.classList.add('thumbnail-title');
uploader.classList.add('text-wrap');
let uploaderText = document.createElement('div');
uploaderText.classList.add('text');
uploaderText.classList.add('ft-14');
uploaderText.innerHTML = '-';
if (value.userNm) uploaderText.innerHTML = value.userNm;
// if (value.userNm) uploaderText.innerHTML = value.userNm.split('___')[1];
uploader.appendChild(uploaderText);
// 등록일자
let createDate = document.createElement('div');
createDate.classList.add('create-date');
createDate.classList.add('thumbnail-title');
createDate.classList.add('text-wrap');
let createDateText = document.createElement('div');
createDateText.classList.add('text');
createDateText.classList.add('ft-14');
createDateText.innerHTML = '-';
if (value.createDate) {
let date = new Date(value.createDate);
// let date = new Date(value.createDate.split('___')[1]);
createDateText.innerHTML = `${changeDateFormat(date.toLocaleDateString('ko-KR'))} ${changeTimeFormat(date.toLocaleTimeString('en-US', {hour12: false}))}`;
}
createDate.appendChild(createDateText);
// 용량
let size = document.createElement('div');
size.classList.add('size');
size.classList.add('thumbnail-title');
size.classList.add('text-wrap');
let sizeText = document.createElement('div');
sizeText.classList.add('text');
sizeText.classList.add('ft-14');
sizeText.innerHTML = formatBytes(value.size);
// sizeText.innerHTML = formatBytes(value.size.split('___')[1]);
if (value.size >= 1073741824) sizeText.classList.add('large');
size.appendChild(sizeText);
// 작성자
let author = document.createElement('div');
author.classList.add('author');
author.classList.add('thumbnail-title');
author.classList.add('text-wrap');
let authorText = document.createElement('div');
authorText.classList.add('text');
authorText.classList.add('ft-14');
authorText.innerHTML = (value.authorNm) ? value.authorNm : value.userNm;
author.appendChild(authorText);
// name.style.display = 'none';
uploader.style.display = 'none';
createDate.style.display = 'none';
size.style.display = 'none';
author.style.display = 'none';
if (videoExtArr.includes(ext)) wrap.appendChild(play);
wrap.appendChild(highlight);
wrap.appendChild(gpsData);
wrap.appendChild(thumbnail);
wrap.appendChild(name);
wrap.appendChild(uploader);
wrap.appendChild(createDate);
wrap.appendChild(size);
wrap.appendChild(author);
gridItem.appendChild(wrap);
docFragment.appendChild(gridItem);
// 썸네일에서 위치있음/없음 버튼 클릭 시 위치 수정 모드 전환 확인 모달창 표시
gpsData.addEventListener('click', async function(e) {
vars.lastSelectTarget = gridItem;
let toggleParams = {
title: '위치 수정',
text: '확인 버튼을 누르면 위치 수정 모드로 전환됩니다.',
type: 'editPosition',
};
toggleModal(true, toggleParams);
let res = await axios.get(`${vars.path_name}/getMemoInfo`, {
params: { userInfoString: vars.userInfoString, resourcePath, dataId }
});
let memo = res.data.result.memo;
setTimeout(() => {
renderMemo(memo, dataId);
}, 100);
// 정보영역 숨김
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.remove('open');
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.add('close');
vars.viewerConnectingTime = vars.viewerConnectingTimeValue;
if (value.isSupported == false || (value.needConvert == true && value.isConverted == false)) {
// 미지원 파일인 경우 -> value.isSupported == false
// 변환 필요 파일인 경우 -> value.needConvert == true && value.isConverted == false (변환해야되지만 아직 변환 안한 상태)
// 뷰어 프로그레스 표시 시간 0으로 설정
vars.viewerConnectingTime = 0;
}
renderViewer(resourcePath, dataId);
})
gridItem.addEventListener('dblclick', async (e) => {
if (e.target.classList.contains('gps-data')) return;
openNewWindowViewer();
})
gridItem.addEventListener('click', async (e) => {
// 위치있음/없음 버튼 클릭하면 위치 수정 모드 전환 확인 모달창 표시로 연결되어야 하므로 여기서는 리턴
if (e.target.classList.contains('gps-data')) return;
let isCtrl = e.ctrlKey;
let isShift = e.shiftKey;
let target = e.target;
// let resourcePath = value.resourcePath;
// 좌클릭이 아닌 경우 무시
if (e.button !== 0) return;
let clickedListItem = target.closest('.list-item');
if (!clickedListItem) return;
if (vars.lastHeaderBtn) targetFocus(vars.lastHeaderBtn);
if (vars.lastMainTreeItem) targetFocus(vars.lastMainTreeItem);
//// non-selected: 그리드 모드에서 선택되지 않은 아이템은 thumbnail의 opacity를 0.5로 설정
//// selected: 그리도 모드에서 선택된 아이템은 thumbnail의 opacity를 1로 설정
let allItems = [...document.querySelectorAll('.archive-main .list-container .list-body .list-item')];
allItems.forEach(item => {
if (!item.classList.contains('selected')) item.classList.add('non-selected');
});
let clickedIndex = allItems.indexOf(clickedListItem);
let isClickFinished = false;
// --- Ctrl + Shift ---
if (!isClickFinished && isCtrl && isShift) {
let anchorIndex = allItems.indexOf(vars.lastListItem);
if (anchorIndex == -1) anchorIndex = 0;
let [start, end] = [anchorIndex, clickedIndex].sort((a, b) => a - b);
for (let i = start; i <= end; i++) {
let item = allItems[i];
if (!vars.multiSelectListItemArr.includes(item)) {
vars.multiSelectListItemArr.push(item);
}
item.classList.add('group-style');
item.classList.add('selected');
item.classList.remove('non-selected');
}
isClickFinished = true;
}
// --- Shift only ---
if (!isClickFinished && isShift) {
let anchorIndex = allItems.indexOf(vars.lastListItem);
if (anchorIndex == -1) anchorIndex = 0;
// 기존 선택 해제
vars.multiSelectListItemArr = [];
allItems.forEach(item => {
item.classList.remove('selected');
item.classList.add('non-selected');
});
let [start, end] = [anchorIndex, clickedIndex].sort((a, b) => a - b);
for (let i = start; i <= end; i++) {
let item = allItems[i];
vars.multiSelectListItemArr.push(item);
item.classList.add('selected');
item.classList.remove('non-selected');
}
//// shift 다중 선택 후 delete키로 휴지통으로 이동
vars.lastSelectTarget = vars.multiSelectListItemArr[0];
isClickFinished = true;
}
// --- Ctrl only ---
if (!isClickFinished && isCtrl) {
if (vars.multiSelectListItemArr.includes(clickedListItem)) {
vars.multiSelectListItemArr = vars.multiSelectListItemArr.filter(item => item !== clickedListItem);
clickedListItem.classList.remove('selected');
clickedListItem.classList.add('non-selected');
} else {
vars.multiSelectListItemArr.push(clickedListItem);
clickedListItem.classList.add('selected');
clickedListItem.classList.remove('non-selected');
}
vars.lastListItem = clickedListItem;
isClickFinished = true;
}
if (!isClickFinished) {
// --- 일반 클릭 (기존 선택 제거 후 단일 선택 + 동작 실행) ---
vars.multiSelectListItemArr = [];
document.querySelectorAll('.archive-main .list-container .list-body .list-item').forEach((elem) => {
elem.classList.remove('selected');
elem.classList.add('non-selected');
});
clickedListItem.classList.add('selected');
clickedListItem.classList.remove('non-selected');
vars.lastListItem = clickedListItem;
if (!isCtrl && !isShift) {
vars.lastSelectTarget = clickedListItem;
let res = await axios.get(`${vars.path_name}/getMemoInfo`, {
params: { userInfoString: vars.userInfoString, resourcePath, dataId }
});
let memo = res.data.result.memo;
setTimeout(() => {
renderMemo(memo, dataId);
}, 100);
// 정보영역 숨김
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.remove('open');
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.add('close');
vars.viewerConnectingTime = vars.viewerConnectingTimeValue;
if (value.isSupported == false || (value.needConvert == true && value.isConverted == false)) {
// 미지원 파일인 경우 -> value.isSupported == false
// 변환 필요 파일인 경우 -> value.needConvert == true && value.isConverted == false (변환해야되지만 아직 변환 안한 상태)
// 뷰어 프로그레스 표시 시간 0으로 설정
vars.viewerConnectingTime = 0;
}
renderViewer(resourcePath, dataId);
} else {
}
changeListItemStyle(clickedListItem, '그리드 아이템 클릭');
}
await syncGroupStyle();
});
}
parent.appendChild(docFragment);
if (vars.lastSelectTarget && vars.lastSelectTarget.classList.contains('list-item')) {
let dataId = vars.lastSelectTarget.dataset.id;
let item = document.querySelector(`.grid-item[data-id="${dataId}"]`);
if (item) {
changeListItemStyle(item, '그리드 아이템 생성');
targetFocus(item, 'instant');
}
}
// 갤러리 폴더 헤더에서 분류 라디오버튼 클릭 시 썸네일 타이틀에 클릭된 라디오버튼 value에 해당하는 텍스트 표시
if (vars.curSortCol) {
document.querySelectorAll('.list-container.grid .list-body .list-item .thumbnail-title').forEach(thumbnailTitle => {
thumbnailTitle.style.display = 'none';
})
document.querySelectorAll(`.list-container.grid .list-body .list-item .thumbnail-title.${vars.curSortCol}`).forEach(thumbnailTitle => {
thumbnailTitle.style.display = 'flex';
})
}
// 썸네일 오브젝트키로 이미지 가져와서 화면에 표시
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
let value = entry[1];
// 파일 resourcePath 맨 마지막에 / 붙어있으면 제거
let resourcePathSplit = value.resourcePath.split('/');
if (resourcePathSplit[resourcePathSplit.length-1] == '') resourcePathSplit.pop();
let resourcePath = resourcePathSplit.join('/');
let thumbnailKey = value.thumbnailKey;
if (thumbnailKey) {
// 썸네일키가 있으면 바로 썸네일에 표시
let generateDownloadUrlParams = {
objectKey: thumbnailKey,
resourcePath: resourcePath,
isThumbnail: true
}
let generateDownloadUrlRes = await axios.post(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams);
// grid-item이 만들어지는 depth3 폴더와 현재 보고 있는 depth3 폴더가 다르면 리턴
if (currentTreeItem != vars.lastMainTreeItem) return;
if (generateDownloadUrlRes.data.message == 'generateDownloadUrl_success') {
let presignedUrl = generateDownloadUrlRes.data.url;
let thumbnailImg = document.querySelector(`.list-container.grid .grid-item[data-resource-path="${resourcePath}"] .thumbnail img`);
if (thumbnailImg) {
thumbnailImg.src = presignedUrl;
}
}
} else {
// console.log('그리드 아이템 생성 완료 / 썸네일키 없음');
let src = '/main/img/archive/file_extension/file.svg';
if (value.ext == 'pdf') src = '/main/img/archive/file_extension/pdf.svg';
if (value.ext == 'hwp') src = '/main/img/archive/file_extension/hwp.svg';
if (value.ext == 'hwpx') src = '/main/img/archive/file_extension/hwp.svg';
if (value.ext == 'doc') src = '/main/img/archive/file_extension/doc.svg';
if (value.ext == 'docx') src = '/main/img/archive/file_extension/doc.svg';
if (value.ext == 'xls') src = '/main/img/archive/file_extension/xls.svg';
if (value.ext == 'xlsx') src = '/main/img/archive/file_extension/xls.svg';
if (value.ext == 'xlsm') src = '/main/img/archive/file_extension/xls.svg';
if (value.ext == 'ppt') src = '/main/img/archive/file_extension/ppt.svg';
if (value.ext == 'pptx') src = '/main/img/archive/file_extension/ppt.svg';
if (value.ext == 'dwg') src = '/main/img/archive/file_extension/dwg.svg';
if (value.ext == 'dxf') src = '/main/img/archive/file_extension/dwg.svg';
if (value.ext == 'grm') src = '/main/img/archive/file_extension/dwg.svg';
if (value.ext == 'mp3') src = '/main/img/archive/file_extension/audio.svg';
if (value.ext == 'wav') src = '/main/img/archive/file_extension/audio.svg';
if (value.ext == 'jpg') src = '/main/img/archive/file_extension/img.svg';
if (value.ext == 'jpeg') src = '/main/img/archive/file_extension/img.svg';
if (value.ext == 'png') src = '/main/img/archive/file_extension/img.svg';
if (value.ext == 'mp4') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'webm') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'mov') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'avi') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'mkv') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'zip') src = '/main/img/archive/file_extension/zip.svg';
if (value.ext == 'txt') src = '/main/img/archive/file_extension/text.svg';
if (value.ext == 'gsim') src = '/main/img/archive/file_extension/gsim.svg';
if (value.ext == 'ifc') src = '/main/img/archive/file_extension/3d.svg';
let thumbnailImg = document.querySelector(`.list-container.grid .grid-item[data-resource-path="${resourcePath}"] .thumbnail img`);
if (thumbnailImg) {
thumbnailImg.src = src;
thumbnailImg.classList.add('default-image'); // default-image 클래스가 붙으면 css에서 width, height를 70%로 설정
}
}
}
}
async function createMapItems(data) {
// 지도 초기화
initOlMap();
// let imageExtArr = ['jpg', 'jpeg', 'png'];
let videoExtArr = ['mp4', 'mov', 'webm'];
let currentTreeItem = vars.lastMainTreeItem;
// 오버레이 및 피쳐를 담아두는 배열
let overlayArr = [];
let featureArr = [];
// 포인트 레이어 설정
let pointSource = new ol.source.Vector();
let pointStyle = new ol.style.Style({
image: new ol.style.Circle({
radius: 8, // 점 크기(px)
fill: new ol.style.Fill({ color: '#fff' }), // 내부색
stroke: new ol.style.Stroke({ color: '#000', width: 4 }) // 테두리
})
});
let pointLayer = new ol.layer.Vector({
source: pointSource,
style: pointStyle,
zIndex: 11, // 클러스터 레이어 zIndex를 더 크게 두면 위에 보임
});
pointLayer.setVisible(false);
// pointLayer.setVisible(true);
// 전역변수와 ol 지도에 포인트 레이어 저장 및 추가
vars.olPointLayer = pointLayer;
ol.map.addLayer(pointLayer);
let entries = sortData(data);
for (let i = 0; i < entries.length; i++) {
let entry = entries[i];
let key = entry[0];
let value = entry[1];
let ext = (value.ext).toLowerCase();
// 파일 resourcePath 맨 마지막에 / 붙어있으면 제거
let resourcePathSplit = value.resourcePath.split('/');
if (resourcePathSplit[resourcePathSplit.length-1] == '') resourcePathSplit.pop();
let resourcePath = resourcePathSplit.join('/');
let dataId = value.dataId;
let lon = value.lon;
let lat = value.lat;
// let height = value.height;
//// 경도/위도 정보가 있으면 지도에 오버레이 표시
if (lon && lat) {
// 지도 준비중 프로그레스
let progressData = {};
progressData.fileName = key;
progressData.count = entries.length;
progressData.idx = i+1;
await toggleMapProgress(true, progressData);
let mapItem = document.createElement('div');
mapItem.classList.add('list-item');
mapItem.classList.add('map-item');
mapItem.classList.add(`depth${value.depth}`);
mapItem.classList.add('file');
mapItem.dataset.resourcePath = resourcePath;
mapItem.dataset.id = dataId;
mapItem.dataset.size = value.size;
mapItem.classList.add('main-list-item');
// 확장자가 유효한 경우 클래스에 ext 추가
if (isValidExt(ext)) mapItem.classList.add(ext);
let wrap = document.createElement('div');
wrap.classList.add('wrap');
// 동영상 표시 아이콘
let play = document.createElement('div');
play.classList.add('play');
let playImage = document.createElement('div');
playImage.classList.add('image');
play.appendChild(playImage);
// 위치 텍스트 저장
let gpsData = document.createElement('div');
gpsData.classList.add('gps-data');
let gpsDataText = document.createElement('div');
gpsDataText.classList.add('text');
gpsDataText.innerHTML = `경도: ${(value.lon) ? value.lon : '없음'} / 위도: ${(value.lat) ? value.lat : '없음'}`;
gpsDataText.style.display = 'none';
gpsData.appendChild(gpsDataText);
// 썸네일
let thumbnail = document.createElement('img');
thumbnail.classList.add('thumbnail');
thumbnail.classList.add('cover');
// 파일명
let name = document.createElement('div');
name.classList.add('name');
name.classList.add('thumbnail-title');
let nameText = document.createElement('div');
nameText.classList.add('name-text');
nameText.classList.add('text');
nameText.classList.add('ft-14');
nameText.classList.add('ellipsis');
// nameText.classList.add('line-break');
nameText.innerHTML = key;
name.appendChild(nameText);
// 등록자
let uploader = document.createElement('div');
uploader.classList.add('uploader');
uploader.classList.add('thumbnail-title');
let uploaderText = document.createElement('div');
uploaderText.classList.add('text');
uploaderText.classList.add('ft-14');
uploaderText.innerHTML = '-';
if (value.userNm) uploaderText.innerHTML = value.userNm;
uploader.appendChild(uploaderText);
// 등록일자
let createDate = document.createElement('div');
createDate.classList.add('create-date');
createDate.classList.add('thumbnail-title');
let createDateText = document.createElement('div');
createDateText.classList.add('text');
createDateText.classList.add('ft-14');
createDateText.innerHTML = '-';
if (value.createDate) {
let date = new Date(value.createDate);
createDateText.innerHTML = `${changeDateFormat(date.toLocaleDateString('ko-KR'))} ${changeTimeFormat(date.toLocaleTimeString('en-US', {hour12: false}))}`;
}
createDate.appendChild(createDateText);
// 용량
let size = document.createElement('div');
size.classList.add('size');
size.classList.add('thumbnail-title');
let sizeText = document.createElement('div');
sizeText.classList.add('text');
sizeText.classList.add('ft-14');
sizeText.innerHTML = formatBytes(value.size);
if (value.size >= 1073741824) sizeText.classList.add('large');
size.appendChild(sizeText);
// 작성자
let author = document.createElement('div');
author.classList.add('author');
author.classList.add('thumbnail-title');
let authorText = document.createElement('div');
authorText.classList.add('text');
authorText.classList.add('ft-14');
authorText.innerHTML = (value.authorNm) ? value.authorNm : value.userNm;
author.appendChild(authorText);
name.style.display = 'none';
uploader.style.display = 'none';
createDate.style.display = 'none';
size.style.display = 'none';
author.style.display = 'none';
if (videoExtArr.includes(ext)) wrap.appendChild(play);
wrap.appendChild(gpsData);
wrap.appendChild(thumbnail);
wrap.appendChild(name);
wrap.appendChild(uploader);
wrap.appendChild(createDate);
wrap.appendChild(size);
wrap.appendChild(author);
mapItem.appendChild(wrap);
// mapItem 숨김 상태로 생성
mapItem.style.display = 'none';
mapItem.addEventListener('click', async function(e) {
// 갤러리 폴더에서 위치 수정 모드가 실행중인 경우 리턴
if (vars.mapMode == 'edit') return;
//// 컨트롤, 쉬프트 조합 클릭 이벤트 추가?
// 이전에 선택된 클러스터 선택 상태 해제
if (clusterLayer && vars.lastSelectCluster) {
vars.lastSelectCluster.set('selected', false);
vars.lastSelectCluster.set('clicked', false);
vars.lastSelectCluster = null;
clusterLayer.changed();
}
// clusterListWrap 숨김
clusterListWrap.style.display = 'none';
let option = {
from: '오버레이 클릭'
}
toggleArchiveMainRight(true, option);
let res = await axios.get(`${vars.path_name}/getMemoInfo`, {
params: { userInfoString: vars.userInfoString, resourcePath, dataId }
});
let memo = res.data.result.memo;
setTimeout(() => {
renderMemo(memo, dataId);
}, 100);
// 정보영역 숨김
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.remove('open');
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.add('close');
vars.viewerConnectingTime = vars.viewerConnectingTimeValue;
if (value.isSupported == false || (value.needConvert == true && value.isConverted == false)) {
// 미지원 파일인 경우 -> value.isSupported == false
// 변환 필요 파일인 경우 -> value.needConvert == true && value.isConverted == false (변환해야되지만 아직 변환 안한 상태)
// 뷰어 프로그레스 표시 시간 0으로 설정
vars.viewerConnectingTime = 0;
}
renderViewer(resourcePath, dataId);
vars.lastSelectTarget = mapItem;
vars.multiSelectListItemArr = [];
changeListItemStyle(e.target);
})
let zoom = ol.map.getView().getZoom();
let scale = Math.max(0.5, Math.min(1.5, zoom / 5));
mapItem.style.setProperty('--overlay-scale-transform', `scale(${scale})`);
let coord3857 = ol.proj.fromLonLat([lon, lat]);
// 오버레이 생성
let overlay = new ol.Overlay({
element: mapItem,
position: coord3857,
positioning: 'bottom-center',
stopEvent: false
});
// 지도에 오버레이 추가
ol.map.addOverlay(overlay);
// 피쳐 생성
let feature = new ol.Feature({
geometry: new ol.geom.Point(coord3857),
dataId: dataId
});
// 피쳐에 오버레이 속성 추가
// -> 클러스터 클릭 시 해당 클러스터에 포함된 피쳐를 탐색하고,
// 각 피쳐에 포함된 오버레이 속성을 클러스터 리스트 렌더에 사용
feature.set('overlay', overlay);
// 포인트 소스에 각 피쳐 추가
pointSource.addFeature(feature);
// overlayArr/featureArr에 각각 오버레이/피쳐 추가
overlayArr.push(overlay);
featureArr.push(feature);
if (value.thumbnailKey) {
let objectKey = value.thumbnailKey;
let generateDownloadUrlParams = {
objectKey: objectKey,
resourcePath: resourcePath,
isThumbnail: true
}
let generateDownloadUrlRes = await axios.post(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams);
// map-item이 만들어지는 depth3 폴더와 현재 보고 있는 depth3 폴더가 다르면 리턴
if (currentTreeItem != vars.lastMainTreeItem) return;
if (generateDownloadUrlRes.data.message == 'generateDownloadUrl_success') {
let presignedUrl = generateDownloadUrlRes.data.url;
thumbnail.src = presignedUrl;
}
} else {
let src = '/main/img/archive/file_extension/file.svg';
if (value.ext == 'pdf') src = '/main/img/archive/file_extension/pdf.svg';
if (value.ext == 'hwp') src = '/main/img/archive/file_extension/hwp.svg';
if (value.ext == 'hwpx') src = '/main/img/archive/file_extension/hwp.svg';
if (value.ext == 'doc') src = '/main/img/archive/file_extension/doc.svg';
if (value.ext == 'docx') src = '/main/img/archive/file_extension/doc.svg';
if (value.ext == 'xls') src = '/main/img/archive/file_extension/xls.svg';
if (value.ext == 'xlsx') src = '/main/img/archive/file_extension/xls.svg';
if (value.ext == 'xlsm') src = '/main/img/archive/file_extension/xls.svg';
if (value.ext == 'ppt') src = '/main/img/archive/file_extension/ppt.svg';
if (value.ext == 'pptx') src = '/main/img/archive/file_extension/ppt.svg';
if (value.ext == 'dwg') src = '/main/img/archive/file_extension/dwg.svg';
if (value.ext == 'dxf') src = '/main/img/archive/file_extension/dwg.svg';
if (value.ext == 'grm') src = '/main/img/archive/file_extension/dwg.svg';
if (value.ext == 'mp3') src = '/main/img/archive/file_extension/audio.svg';
if (value.ext == 'wav') src = '/main/img/archive/file_extension/audio.svg';
if (value.ext == 'jpg') src = '/main/img/archive/file_extension/img.svg';
if (value.ext == 'jpeg') src = '/main/img/archive/file_extension/img.svg';
if (value.ext == 'png') src = '/main/img/archive/file_extension/img.svg';
if (value.ext == 'mp4') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'webm') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'mov') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'avi') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'mkv') src = '/main/img/archive/file_extension/video.svg';
if (value.ext == 'zip') src = '/main/img/archive/file_extension/zip.svg';
if (value.ext == 'txt') src = '/main/img/archive/file_extension/text.svg';
if (value.ext == 'gsim') src = '/main/img/archive/file_extension/gsim.svg';
if (value.ext == 'ifc') src = '/main/img/archive/file_extension/3d.svg';
thumbnail.src = src;
}
}
}
// 아카이브 우측 영역 표시/숨김
if (vars.lastListItem && document.querySelector(`.map-item[data-id="${vars.lastListItem.dataset.id}"]`) && vars.mapMode == 'normal') {
console.log(vars.mapMode);
let option = {
from: '지도 렌더링 - 미리보기 표시',
}
toggleArchiveMainRight(true, option);
} else {
let option = {
from: '지도 렌더링 - 미리보기 숨김',
}
toggleArchiveMainRight(false, option);
}
if (vars.lastSelectTarget && vars.lastSelectTarget.classList.contains('list-item')) {
let dataId = vars.lastSelectTarget.dataset.id;
let item = document.querySelector(`.map-item[data-id="${dataId}"]`);
if (item) {
changeListItemStyle(item);
// 갤러리 폴더 상단 라디오버튼 변경 시 뷰어 영역도 새로 그리려면 renderViewer 실행
// let resourcePath = vars.lastSelectTarget.dataset.resourcePath;
// renderViewer(resourcePath, dataId);
}
}
// 지도 모드 전환 시 모든 오버레이의 범위에 맞게 카메라 이동하도록 overlayPositionArr 변수 생성
let overlayPositionArr = overlayArr.map(overlay => overlay.getPosition());
let extent = new ol.extent.boundingExtent(overlayPositionArr);
vars.allOverlayExtent = extent;
if (vars.mapMode == 'edit') {
let validCoordResult = isValidCoord();
if (validCoordResult == true) {
let lonInput = document.querySelector('.edit-mode-ui .coord-input.lon');
let latInput = document.querySelector('.edit-mode-ui .coord-input.lat');
let lon = lonInput.value;
let lat = latInput.value;
let overlayPosition = ol.proj.fromLonLat([lon, lat]);
// 위치 수정 모드 실행 시 경도/위도 정보가 있으면 해당 위치로 카메라 이동하도록 overlayPositionArr 변수 갱신
overlayPositionArr = [overlayPosition];
// 마커 표시
if(vars.markerOverlay) {
vars.markerOverlay.setPosition(overlayPosition);
}
}
}
let overlayFitBtn = document.querySelector('.map-container .control-btn-wrap .overlays-fit-btn');
if (overlayPositionArr.length > 0) {
// overlayPositionArr의 length가 1 이상이면 오버레이의 범위에 맞게 카메라 이동
await fitToOverlayExtent(overlayPositionArr, async () => {
// 다음 로직 실행 - 지도 프로그레스 종료
await toggleMapProgress(false);
});
let maxZoom;
if (overlayPositionArr.length != 1 && !vars.lastListItem) maxZoom = vars.allOverlayVisibleZoom - 1;
overlayFitBtn.style.display = 'flex';
overlayFitBtn.addEventListener('click', async() => {
let paddingValue = 100;
let maxZoom = vars.allOverlayVisibleZoom;
if (overlayPositionArr.length != 1 && !vars.lastListItem) maxZoom = vars.allOverlayVisibleZoom - 1;
ol.map.getView().fit(vars.allOverlayExtent, {
padding: [paddingValue, paddingValue, paddingValue, paddingValue],
// duration: 500,
// constrainResolution: true,
maxZoom: maxZoom
});
if (vars.lastSelectCluster) {
renderClusterList(vars.lastSelectCluster.get('features'), vars.olClusterLayer);
}
})
} else {
// overlayPositionArr의 length가 0이면 바로 지도 프로그레스 종료
await toggleMapProgress(false);
overlayFitBtn.style.display = 'none';
}
// await toggleMapProgress(false);
// 줌 레벨에 따라 오버레이 크기 조절
await scaleOverlaysByZoom();
//// 클러스터 코드
let clusterListWrap = document.querySelector('.map-container .cluster-list-wrap');
let rawSource = new ol.source.Vector({ features: featureArr });
let clusterSource = new ol.source.Cluster({
distance: vars.clusterDistance,
minDistance: 0,
source: rawSource
});
// 최소 거리(minDistance) 계산 후 적용
let radius = 20; // circle radius (스타일에서 쓰는 값과 동일)
let strokeWidth = 4; // stroke width
let padding = 6; // 여유
let wantMin = 2 * radius + strokeWidth + padding; // 대략 50px
clusterSource.setDistance(Math.max(clusterSource.getDistance(), wantMin));
if (clusterSource.setMinDistance) {
clusterSource.setMinDistance(wantMin);
}
clusterSource.refresh();
let clusterStyleCache = { normal: {}, hover: {}, selected: {}, clicked: {} };
let clusterLayer = new ol.layer.Vector({
source: clusterSource,
declutter: false,
style: (clusterFeature) => {
const members = clusterFeature.get('features');
const size = members?.length || 0;
if (size <= 1) return null;
let selectedId = vars.lastSelectTarget?.dataset?.id;
let isSelectedCluster = selectedId && members.some(f => String(f.get('dataId')) === String(selectedId));
let state = 'normal';
if (isSelectedCluster) state = 'selected';
if (clusterFeature.get('hover')) state = 'hover';
if (clusterFeature.get('clicked')) state = 'clicked';
return clusterStyleFor(size, state, clusterStyleCache);
},
zIndex: 12,
});
// 전역변수와 ol 지도에 클러스터 레이어 저장 및 추가
vars.olClusterLayer = clusterLayer;
ol.map.addLayer(clusterLayer);
let lastHoveredCluster = null; // 마지막으로 hover된 클러스터
let hoverSeq = 0; // 비동기 응답 순서 보호용
ol.map.on('pointermove', (evt) => {
// 드래그 중인 경우 리턴 (성능/깜빡임 방지)
if (evt.dragging) return;
// 위치 수정 모드인 경우 리턴
if (document.querySelector('.list-container .list-body .map-container').classList.contains('edit-mode')) return;
// 클러스터 레이어가 숨겨진 상태인 경우 리턴
if (clusterLayer.getVisible() == false) return;
const seq = ++hoverSeq;
// 클러스터 레이어만 히트 검사 (비동기)
clusterLayer.getFeatures(evt.pixel).then((hits) => {
if (seq !== hoverSeq) return; // 이전 pointermove의 지연 응답 무시
let cf = null;
if (hits && hits.length) {
const cand = hits[0];
const members = cand.get('features');
if (members && members.length > 1) cf = cand; // 진짜 클러스터만 hover 대상
}
// 기존 hover 해제
if (lastHoveredCluster && lastHoveredCluster !== cf) {
lastHoveredCluster.set('hover', false);
lastHoveredCluster = null;
clusterLayer.changed(); // 재렌더
}
// 새 hover 설정
if (cf) {
if (lastHoveredCluster !== cf) {
cf.set('hover', true);
lastHoveredCluster = cf;
clusterLayer.changed();
}
ol.map.getTargetElement().style.cursor = 'pointer';
} else {
ol.map.getTargetElement().style.cursor = 'default';
}
});
});
// 맵 영역을 벗어났을 때 hover 리셋
ol.map.getTargetElement().addEventListener('mouseleave', () => {
if (lastHoveredCluster) {
lastHoveredCluster.set('hover', false);
lastHoveredCluster = null;
clusterLayer.changed();
}
ol.map.getTargetElement().style.cursor = 'default';
});
// 초기 1회 동기화 - clusterSource가 클러스터 피처를 만든 뒤 그 프레임이 그려진 다음에 실행
// clusterSource.refresh();
// ol.map.once('rendercomplete', () => {
// syncOverlaysByCluster(overlayArr, clusterSource);
// });
let startZoom, endZoom;
ol.map.on('movestart', () => {
startZoom = ol.map.getView().getZoom();
// vars.olPointLayer.setVisible(true);
// 지도 이동 및 줌 변경 시작할 때 map-container에 overlay-suspend 클래스 추가해서 오버레이 숨김
document.querySelector('.map-container').classList.add('overlay-suspend');
})
// 이동/줌 종료 시 동기화 - 렌더가 끝난 뒤에 한 번만 실행
ol.map.on('moveend', async (e) => {
if (!vars.lastSelectTarget || !vars.lastSelectTarget.classList.contains('map-item')) {
// vars.lastSelectTarget이 없거나 vars.lastSelectTarget의 클래스 리스트에 map-item이 없는 경우
vars.lastSelectCluster = undefined;
}
endZoom = ol.map.getView().getZoom();
if (endZoom >= vars.allOverlayVisibleZoom) {
// 줌 변경 종료 시점의 줌 레벨이 vars.allOverlayVisibleZoom 이상이면 클러스터레이어 숨기고 포인트 레이어 표시
vars.olClusterLayer.setVisible(false);
vars.olPointLayer.setVisible(true);
} else {
// 줌 변경 종료 시점의 줌 레벨이 vars.allOverlayVisibleZoom 보다 작으면 클러스터레이어 표시하고 포인트 레이어 숨김
vars.olClusterLayer.setVisible(true);
vars.olPointLayer.setVisible(false);
await requestAnimationFrame(() => {
reapplyClickedByLastOverlay(clusterSource, clusterLayer, startZoom, endZoom);
})
clusterLayer.changed();
}
ol.map.once('rendercomplete', async () => {
// vars.lastSelectTarget이 없으면 클러스터 아이템 리스트 숨김
// if (!vars.lastSelectTarget) clusterListWrap.style.display = 'none';
// 클러스터에 포함된 오버레이는 숨기고 포함되지 않은 오버레이는 표시
// bufferPx - 양수: 뷰포트 범위 바깥까지 오버레이 표시 영역 확장 / 음수: 뷰포트 범위 안쪽으로 오버레이 표시 영역 축소
let bufferPx = -50;
await syncOverlaysByCluster(overlayArr, clusterSource, bufferPx);
clusterLayer.changed();
await requestAnimationFrame(() => {
// 지도 이동 및 줌 변경 종료할 때 map-container에 overlay-suspend 클래스 제거해서 오버레이 표시
document.querySelector('.map-container').classList.remove('overlay-suspend');
})
});
});
ol.map.on('click', (e) => {
// 클릭한 대상이 오버레이인 경우 리턴
if (e.originalEvent?.target.classList.contains('list-item')) return;
// 위치 수정 모드인 경우 리턴
if (document.querySelector('.list-container .list-body .map-container').classList.contains('edit-mode')) return;
// 클러스터 레이어가 숨겨진 상태인 경우 리턴
if (clusterLayer.getVisible() == false) return;
// 이 레이어(clusteLayer)에서만 히트 검사
clusterLayer.getFeatures(e.pixel).then(async (clickedFeatures) => {
if (!clickedFeatures || !clickedFeatures.length) {
// 아무 것도 안 눌렀으면 이전에 선택된 클러스터 선택 상태 해제
if (vars.lastSelectCluster) {
// vars.lastSelectCluster.set('selected', false);
vars.lastSelectCluster.set('clicked', false);
vars.lastSelectCluster = null;
clusterLayer.changed();
}
// 클러스터 아이템 리스트 닫기
clusterListWrap.style.display = 'none';
return;
}
// 클릭된 건(보통 1개)의 내부 멤버(원본 피처들)
let clusterFeature = clickedFeatures[0];
let items = clusterFeature.get('features');
if (!items || items.length <= 1) {
// 단건이면 이전에 선택된 클러스터 선택 상태 해제
if (vars.lastSelectCluster) {
// vars.lastSelectCluster.set('selected', false);
vars.lastSelectCluster.set('clicked', false);
vars.lastSelectCluster = null;
clusterLayer.changed();
}
return;
}
// 기존 선택된 클러스터 선택 상태 해제
for (let i = 0; i < clusterSource.getFeatures().length; i++) {
let feature = clusterSource.getFeatures()[i];
// feature.set('selected', false);
feature.set('clicked', false);
}
// 새로 선택한 클러스터에 선택 상태 설정
// clusterFeature.set('selected', true);
// clusterFeature.set('clicked', true);
vars.lastSelectCluster = clusterFeature;
// 스타일 갱신
clusterLayer.changed();
// 갤러리 폴더에서 위치 수정 모드가 실행중인 경우 리턴
if (vars.mapMode == 'edit') return;
// 클러스터 리스트 렌더링
await renderClusterList(items, clusterLayer);
// 새로 선택한 클러스터에 선택 상태 설정
// clusterFeature.set('selected', true);
clusterFeature.set('clicked', true);
vars.lastSelectCluster.set('clicked', true);
// 스타일 갱신
clusterLayer.changed();
});
let option = {
from: '지도 영역 클릭'
}
toggleArchiveMainRight(false, option);
});
}
async function initOlMap() {
// 경도/위도 입력란
let lonInput = document.querySelector('.edit-mode-ui .coord-input.lon');
let latInput = document.querySelector('.edit-mode-ui .coord-input.lat');
// 기존 지도 완전 삭제
if (ol.map) {
ol.map.getOverlays()?.clear();
ol.map.getLayers()?.forEach(l => {
const src = l.getSource?.();
// 벡터라면 데이터까지 비우기(하드 클리어)
if (src && src instanceof ol.source.Vector) src.clear(true);
});
ol.map.getLayers()?.clear();
// 3) 타깃 해제(맵이 DOM과 이벤트 연결을 끊음)
ol.map.setTarget(null);
// 4) 가능하다면 dispose()로 내부 렌더러/리스너 완전 정리
if (typeof ol.map.dispose === 'function') {
ol.map.dispose();
}
document.querySelector('.map-container .ol-viewport')?.remove();
ol.map = undefined;
}
ol.map = new ol.Map({
target: 'map-container',
// 지도 생성 시 일반 지도 1개만 등록 후 지도 url 변경 방식
// layers: [vars.baseLayer],
// 지도 생성 시 3가지 지도 모두 등록 후 교체 방식
layers: [vars.roadLayer, vars.satelliteLayer, vars.hybridLayer],
view: new ol.View({
projection: 'EPSG:3857',
center: ol.proj.fromLonLat([127.8, 35.9]),
zoom: 7,
maxZoom: 22,
constrainResolution: true,
smoothResolutionConstraint: true
})
});
let olZoom = document.querySelector('.ol-zoom');
let controlBtnWrap = document.querySelector('.list-container.grid .list-wrap.list-body .map-container .control-btn-wrap');
controlBtnWrap.appendChild(olZoom);
proj4.defs([
['EPSG:5185', '+proj=tmerc +lat_0=38 +lon_0=125 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs'],
['EPSG:5186', '+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs'],
['EPSG:5187', '+proj=tmerc +lat_0=38 +lon_0=129 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs'],
['EPSG:5188', '+proj=tmerc +lat_0=38 +lon_0=131 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs'],
['EPSG:4978', '+proj=geocent +datum=WGS84 +units=m +no_defs'],
['EPSG:4326', '+proj=longlat +datum=WGS84 +no_defs'],
])
ol.proj.proj4.register(proj4);
//// pmtiles 표시용 테스트 코드
vars.layerManager = new OLMultiLayerManager(ol.map);
await vars.layerManager.toggleLayer(vars.project_id, 'pmtiles');
let clusterListWrap = document.querySelector('.map-container .cluster-list-wrap');
clusterListWrap.style.display = 'none';
// 클릭 지점 마커 생성
let marker = document.createElement('div');
marker.classList.add('marker');
vars.markerOverlay = new ol.Overlay({
element: marker,
positioning: 'center-center',
stopEvent: false,
zIndex: 9999
});
ol.map.addOverlay(vars.markerOverlay);
// 오픈레이어스 지도 단일 클릭 이벤트 - 위치 수정 모드에서 지도 클릭 시 경도/위도 입력란에 해당 위치 좌표 입력
ol.map.on('singleclick', function (e) {
// 갤러리 폴더에서 위치 수정 모드가 실행중인 경우 리턴
if (vars.mapMode == 'normal') return;
// 클릭 위치 경도/위도로 변환
let coord3857 = e.coordinate;
let coord4326 = ol.proj.toLonLat(coord3857);
let lon = coord4326[0];
let lat = coord4326[1];
lonInput.value = lon;
latInput.value = lat;
// 클릭 지점 마커 위치 업데이트
ol.map.addOverlay(vars.markerOverlay);
vars.markerOverlay.setPosition(coord3857);
})
// js 지도 영역 클릭 이벤트 - 지도 영역 클릭 시 선택된 리스트 아이템 및 뷰어 초기화
ol.map.addEventListener('click', function(e) {
if (vars.mapMode == 'edit') return;
vars.lastSelectTarget = undefined;
vars.lastListItem = undefined;
vars.multiSelectListItemArr = [];
document.querySelectorAll('.archive-main .list-container .list-body .list-item').forEach((elem) => {
elem.classList.remove('selected');
elem.classList.remove('group-style');
});
resetViewer();
toggleArchiveMainRight(false);
})
// 오픈레이어스 지도 viewport에서 휠 이벤트 발생할 때
let viewport = ol.map.getViewport();
viewport.addEventListener('wheel', function() {
// 선택된 map-item이 있는 경우 리턴
if (vars.lastListItem) return;
// 선택된 map-item이 없는 경우 클러스터 리스트 숨김
document.querySelector('.map-container .cluster-list-wrap').style.display = 'none';
})
// 경도/위도 입력란에 '없음' 텍스트로 표시된 경우 입력란 클릭하면 초기화
lonInput.addEventListener('click', function(e) {
if (e.target.value == '없음') e.target.value = '';
})
latInput.addEventListener('click', function(e) {
if (e.target.value == '없음') e.target.value = '';
})
// 경도/위도 입력란 수정하면 마커 위치 업데이트 및 마커 위치로 화면 이동
lonInput.addEventListener('input', function(e) {
let result = checkCoordInputValue(e.target.value, 'lon');
if (e.target.value !== result) e.target.value = result;
if (result == '') e.target.value = '없음';
ol.map.removeOverlay(vars.markerOverlay);
updateMarker();
})
latInput.addEventListener('input', function(e) {
let result = checkCoordInputValue(e.target.value, 'lat');
if (e.target.value !== result) e.target.value = result;
if (result == '') e.target.value = '없음';
ol.map.removeOverlay(vars.markerOverlay);
updateMarker();
})
// 위치 보기 버튼을 누를 때 경도/위도 값이 제대로 입력되어 있으면 마커 위치로 화면 이동
let focusBtn = document.querySelector('.edit-mode-ui .focus-btn');
focusBtn.addEventListener('click', function() {
let validCoordResult = isValidCoord();
if (validCoordResult == true) {
updateMarker();
} else {
let toggleParams = {
text: '경도/위도 값을 확인해주세요.<br><span style="text-align: left;">경도 범위: -180 ~ 180<br>위도 범위: -90 ~ 90</span>',
type: 'alertModal',
};
toggleModal(true, toggleParams);
}
})
// 저장 버튼을 누를 때 경도/위도 값이 제대로 입력되어 있으면 db에 경도/위도 값 저장
let saveBtn = document.querySelector('.edit-mode-ui .save-btn');
saveBtn.addEventListener('click', async function() {
let validCoordResult = isValidCoord();
if (validCoordResult == true) {
let dataId = document.querySelector('.edit-mode-ui .info-wrap .wrap').dataset.id;
let editPositionParams = {
dataId: dataId,
lon: lonInput.value,
lat: latInput.value,
};
let editPositionResult = await axios.post(`${vars.path_name}/editPosition`, { params: editPositionParams });
if (editPositionResult.data.message == 'editPosition_success') {
// TreeObject 업데이트
let resourcePath = document.querySelector(`.edit-mode-ui .info-wrap .wrap`).dataset.resourcePath;
await preparePageRendering({
scope: 'updateTreeObject',
resourcePath: resourcePath.substring(0, resourcePath.lastIndexOf('/')),
pushState: false
});
// 파일 영역 재렌더링
document.querySelector(`.control-box .file-area-mode-btn.${vars.lastFileAreaMode}`).click();
// 뷰어 재렌더링하여 메타데이터 업데이트
setTimeout(async() => {
if (vars.lastListItem) {
let updateResourcePath = vars.lastListItem.dataset.resourcePath;
let updatedDataId = vars.lastListItem.dataset.id;
await renderViewer(updateResourcePath, updatedDataId, false);
}
}, 300);
} else {
// 경도/위도 값에 '없음'이 입력되어 있으면 db에 null로 저장
if (lonInput.value == '없음' && latInput.value == '없음') {
let editPositionParams = {
dataId: document.querySelector('.edit-mode-ui .info-wrap .wrap').dataset.id,
lon: null,
lat: null,
height: null,
};
let editPositionResult = await axios.post(`${vars.path_name}/editPosition`, { params: editPositionParams });
if (editPositionResult.data.message == 'editPosition_success') {
document.querySelector(`.control-box .file-area-mode-btn.${vars.lastFileAreaMode}`).click();
}
} else {
let toggleParams = {
text: '경도/위도 값을 확인해주세요.<br><span style="text-align: left;">경도 범위: -180 ~ 180<br>위도 범위: -90 ~ 90</span>',
type: 'alertModal',
};
toggleModal(true, toggleParams);
}
}
}
})
// 위치 삭제 버튼을 누르면 모달 표시
let deleteBtn = document.querySelector('.edit-mode-ui .delete-btn');
deleteBtn.addEventListener('click', function() {
let toggleParams = {
title: '위치 삭제',
text: '확인 버튼을 누르면 위치가 삭제됩니다.',
type: 'deletePosition',
};
toggleModal(true, toggleParams);
})
// 취소 버튼을 누르면 그리드 모드로 전환
let cancelBtn = document.querySelector('.edit-mode-ui .cancel-btn');
cancelBtn.addEventListener('click', function() {
document.querySelector(`.control-box .file-area-mode-btn.${vars.lastFileAreaMode}`).click();
})
}
/// 251104 박지은 ol 소켓처리
// // fileAreaMode가 map 일때는 새 파일만 지도에 추가
// export async function createNewMapItems(newResourcePathArr) {
// let videoExtArr = ['mp4', 'mov', 'webm'];
// let currentTreeItem = vars.lastMainTreeItem;
// // 오버레이 및 피쳐를 담아두는 배열
// let overlayArr = [];
// let featureArr = [];
// //// 기존 포인트 레이어 가져오기 또는 생성
// let pointSource, pointLayer;
// if (vars.olPointLayer) {
// // 기존 레이어 재사용
// pointLayer = vars.olPointLayer;
// pointSource = pointLayer.getSource();
// } else {
// // 새로 생성
// pointSource = new ol.source.Vector();
// let pointStyle = new ol.style.Style({
// image: new ol.style.Circle({
// radius: 8, // 점 크기(px)
// fill: new ol.style.Fill({ color: '#fff' }), // 내부색
// stroke: new ol.style.Stroke({ color: '#000', width: 4 }) // 테두리
// })
// });
// pointLayer = new ol.layer.Vector({
// source: pointSource,
// style: pointStyle,
// zIndex: 11, // 클러스터 레이어 zIndex를 더 크게 두면 위에 보임
// });
// pointLayer.setVisible(false);
// // 전역변수와 ol 지도에 포인트 레이어 저장 및 추가
// vars.olPointLayer = pointLayer;
// ol.map.addLayer(pointLayer);
// }
// // 새로 업로드된 파일 데이터만 가져오기
// let newFiles = [];
// if (newResourcePathArr) {
// for (let path of newResourcePathArr) {
// // let fileName = path.split('/').pop();
// let fileName;
// if (typeof path === 'object') {
// fileName = path.to.split('/').pop();
// } else {
// fileName = path.split('/').pop();
// }
// // 마지막이 빈 문자열이면 제거
// if (fileName === '') {
// let pathSplit = path.split('/');
// pathSplit.pop();
// fileName = pathSplit.pop();
// }
// // if (vars.currentTreeObject.file && vars.currentTreeObject.file[fileName]) {
// // newFiles[fileName] = vars.currentTreeObject.file[fileName];
// // }
// // newFiles[fileName] = vars.currentTreeObject.file[fileName];
// // let fileName;
// // if (typeof path === 'object') {
// // fileName = path.to.split('/').pop();
// // } else {
// // fileName = path.split('/').pop();
// // }
// // if (fileName === '') {
// // let pathSplit = path.split('/');
// // pathSplit.pop();
// // fileName = pathSplit.pop();
// // }
// // 현재 폴더에서 먼저 찾고, 없으면 전체 트리에서 찾기 (파일업로드/파일이동)
// let fileObject = vars.currentTreeObject.file?.[fileName] || findFileInTree(vars.currentTreeObject, fileName);
// if (fileObject) newFiles[fileName] = fileObject;
// }
// }
// // 새 파일이 없으면 리턴
// if (Object.keys(newFiles).length === 0) return;
// // 새로 추가된 dataId 추적
// let newlyAddedIds = [];
// // 새 파일에 대해서만 map item 생성
// let entries = sortData(newFiles);
// for (let i = 0; i<entries.length; i++) {
// let entry = entries[i];
// let key = entry[0];
// let value = entry[1];
// let ext = (value.ext).toLowerCase();
// // 파일 resourcePath 맨 마지막에 / 붙어있으면 제거
// let resourcePathSplit = value.resourcePath.split('/');
// if(resourcePathSplit[resourcePathSplit.length-1] == '') resourcePathSplit.pop();
// let resourcePath = resourcePathSplit.join('/');
// let dataId = value.dataId;
// let lon = value.lon;
// let lat = value.lat;
// //// 경도/위도 정보가 있으면 지도에 오버레이 표시
// if (lon && lat) {
// let mapItem = document.createElement('div');
// mapItem.classList.add('list-item');
// mapItem.classList.add('map-item');
// mapItem.classList.add(`depth${value.depth}`);
// mapItem.classList.add('file');
// mapItem.dataset.resourcePath = resourcePath;
// mapItem.dataset.id = dataId;
// mapItem.dataset.size = value.size;
// mapItem.classList.add('main-list-item');
// // 확장자가 유효한 경우 클래스에 ext 추가
// if (isValidExt(ext)) mapItem.classList.add(ext);
// let wrap = document.createElement('div');
// wrap.classList.add('wrap');
// // 동영상 표시 아이콘
// let play = document.createElement('div');
// play.classList.add('play');
// let playImage = document.createElement('div');
// playImage.classList.add('image');
// play.appendChild(playImage);
// // 위치 텍스트 저장
// let gpsData = document.createElement('div');
// gpsData.classList.add('gps-data');
// let gpsDataText = document.createElement('div');
// gpsDataText.classList.add('text');
// gpsDataText.innerHTML = `경도: ${(value.lon) ? value.lon : '없음'} / 위도: ${(value.lat) ? value.lat : '없음'}`;
// gpsDataText.style.display = 'none';
// gpsData.appendChild(gpsDataText);
// // 썸네일
// let thumbnail = document.createElement('img');
// thumbnail.classList.add('thumbnail');
// thumbnail.classList.add('cover');
// // 파일명
// let name = document.createElement('div');
// name.classList.add('name');
// name.classList.add('thumbnail-title');
// let nameText = document.createElement('div');
// nameText.classList.add('name-text');
// nameText.classList.add('text');
// nameText.classList.add('ft-14');
// nameText.classList.add('ellipsis');
// nameText.innerHTML = key;
// name.appendChild(nameText);
// // 등록자
// let uploader = document.createElement('div');
// uploader.classList.add('uploader');
// uploader.classList.add('thumbnail-title');
// let uploaderText = document.createElement('div');
// uploaderText.classList.add('text');
// uploaderText.classList.add('ft-14');
// uploaderText.innerHTML = '-';
// if (value.userNm) uploaderText.innerHTML = value.userNm;
// uploader.appendChild(uploaderText);
// // 등록일자
// let createDate = document.createElement('div');
// createDate.classList.add('create-date');
// createDate.classList.add('thumbnail-title');
// let createDateText = document.createElement('div');
// createDateText.classList.add('text');
// createDateText.classList.add('ft-14');
// createDateText.innerHTML = '-';
// if (value.createDate) {
// let date = new Date(value.createDate);
// createDateText.innerHTML = `${changeDateFormat(date.toLocaleDateString('ko-KR'))} ${changeTimeFormat(date.toLocaleTimeString('en-US', {hour12: false}))}`;
// }
// createDate.appendChild(createDateText);
// // 용량
// let size = document.createElement('div');
// size.classList.add('size');
// size.classList.add('thumbnail-title');
// let sizeText = document.createElement('div');
// sizeText.classList.add('text');
// sizeText.classList.add('ft-14');
// sizeText.innerHTML = formatBytes(value.size);
// if (value.size >= 1073741824) sizeText.classList.add('large');
// size.appendChild(sizeText);
// // 작성자
// let author = document.createElement('div');
// author.classList.add('author');
// author.classList.add('thumbnail-title');
// let authorText = document.createElement('div');
// authorText.classList.add('text');
// authorText.classList.add('ft-14');
// authorText.innerHTML = (value.authorNm) ? value.authorNm : value.userNm;
// author.appendChild(authorText);
// name.style.display = 'none';
// uploader.style.display = 'none';
// createDate.style.display = 'none';
// size.style.display = 'none';
// author.style.display = 'none';
// if (videoExtArr.includes(ext)) wrap.appendChild(play);
// wrap.appendChild(gpsData);
// wrap.appendChild(thumbnail);
// wrap.appendChild(name);
// wrap.appendChild(uploader);
// wrap.appendChild(createDate);
// wrap.appendChild(size);
// wrap.appendChild(author);
// mapItem.appendChild(wrap);
// // mapItem 숨김 상태로 생성
// mapItem.style.display = 'none';
// mapItem.addEventListener('click', async function(e) {
// // 갤러리 폴더에서 위치 수정 모드가 실행중인 경우 리턴
// if (vars.mapMode == 'edit') return;
// // 이전에 선택된 클러스터 선택 상태 해제
// if (vars.olClusterLayer && vars.lastSelectCluster) {
// vars.lastSelectCluster.set('selected', false);
// vars.lastSelectCluster.set('clicked', false);
// vars.lastSelectCluster = null;
// vars.olClusterLayer.changed();
// }
// // clusterListWrap 숨김
// let clusterListWrap = document.querySelector('.map-container .cluster-list-wrap');
// if (clusterListWrap) clusterListWrap.style.display = 'none';
// let option = {
// from: '오버레이 클릭'
// }
// toggleArchiveMainRight(true, option);
// let res = await axios.get(`${vars.path_name}/getMemoInfo`, {
// params: { userInfoString: vars.userInfoString, resourcePath, dataId }
// });
// let memo = res.data.result.memo;
// setTimeout(() => {
// renderMemo(memo, dataId);
// }, 100);
// vars.viewerConnectingTime = vars.viewerConnectingTimeValue;
// if (value.isSupported == false || (value.needConvert == true && value.isConverted == false)) {
// // 미지원 파일인 경우 -> value.isSupported == false
// // 변환 필요 파일인 경우 -> value.needConvert == true && value.isConverted == false (변환해야되지만 아직 변환 안한 상태)
// // 뷰어 프로그레스 표시 시간 0으로 설정
// vars.viewerConnectingTime = 0;
// }
// renderViewer(resourcePath, dataId);
// vars.lastSelectTarget = mapItem;
// vars.multiSelectListItemArr = [];
// changeListItemStyle(e.target);
// })
// let zoom = ol.map.getView().getZoom();
// let scale = Math.max(0.5, Math.min(2, zoom / 10));
// mapItem.style.setProperty('--overlay-scale-transform', `scale(${scale})`);
// let coord3857 = ol.proj.fromLonLat([lon, lat]);
// // 오버레이 생성
// let overlay = new ol.Overlay({
// element: mapItem,
// position: coord3857,
// positioning: 'bottom-center',
// stopEvent: false
// });
// // 지도에 오버레이 추가
// ol.map.addOverlay(overlay);
// // 피쳐 생성
// let feature = new ol.Feature({
// geometry: new ol.geom.Point(coord3857),
// dataId: dataId
// });
// // 피쳐에 오버레이 속성 추가
// // -> 클러스터 클릭 시 해당 클러스터에 포함된 피쳐를 탐색하고,
// // 각 피쳐에 포함된 오버레이 속성을 클러스터 리스트 렌더에 사용
// feature.set('overlay', overlay);
// // 포인트 소스에 피쳐 추가
// pointSource.addFeature(feature);
// // overlayArr/featureArr에 각각 오버레이/피쳐 추가
// overlayArr.push(overlay);
// featureArr.push(feature);
// newlyAddedIds.push(dataId); // 새로 추가된 ID 기록
// if (value.thumbnailKey) {
// let objectKey = value.thumbnailKey;
// let generateDownloadUrlParams = {
// objectKey: objectKey,
// resourcePath: resourcePath,
// isThumbail: true
// };
// let generateDownloadUrlRes = await axios.post(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams);
// // map-item이 만들어지는 depth3 폴더와 현재 보고 있는 depth3 폴더가 다르면 continue
// if (currentTreeItem != vars.lastMainTreeItem) continue;
// if (generateDownloadUrlRes.data.message == 'generateDownloadUrl_success') {
// let presigendUrl = generateDownloadUrlRes.data.url;
// thumbnail.src = presigendUrl;
// } else {
// let src = '/main/img/archive/file_extension/file.svg';
// if (value.ext == 'pdf') src = '/main/img/archive/file_extension/pdf.svg';
// if (value.ext == 'hwp') src = '/main/img/archive/file_extension/hwp.svg';
// if (value.ext == 'hwpx') src = '/main/img/archive/file_extension/hwp.svg';
// if (value.ext == 'doc') src = '/main/img/archive/file_extension/doc.svg';
// if (value.ext == 'docx') src = '/main/img/archive/file_extension/doc.svg';
// if (value.ext == 'xls') src = '/main/img/archive/file_extension/xls.svg';
// if (value.ext == 'xlsx') src = '/main/img/archive/file_extension/xls.svg';
// if (value.ext == 'xlsm') src = '/main/img/archive/file_extension/xls.svg';
// if (value.ext == 'ppt') src = '/main/img/archive/file_extension/ppt.svg';
// if (value.ext == 'pptx') src = '/main/img/archive/file_extension/ppt.svg';
// if (value.ext == 'dwg') src = '/main/img/archive/file_extension/dwg.svg';
// if (value.ext == 'dxf') src = '/main/img/archive/file_extension/dwg.svg';
// if (value.ext == 'grm') src = '/main/img/archive/file_extension/dwg.svg';
// if (value.ext == 'mp3') src = '/main/img/archive/file_extension/audio.svg';
// if (value.ext == 'wav') src = '/main/img/archive/file_extension/audio.svg';
// if (value.ext == 'jpg') src = '/main/img/archive/file_extension/img.svg';
// if (value.ext == 'jpeg') src = '/main/img/archive/file_extension/img.svg';
// if (value.ext == 'png') src = '/main/img/archive/file_extension/img.svg';
// if (value.ext == 'mp4') src = '/main/img/archive/file_extension/video.svg';
// if (value.ext == 'webm') src = '/main/img/archive/file_extension/video.svg';
// if (value.ext == 'mov') src = '/main/img/archive/file_extension/video.svg';
// if (value.ext == 'avi') src = '/main/img/archive/file_extension/video.svg';
// if (value.ext == 'mkv') src = '/main/img/archive/file_extension/video.svg';
// if (value.ext == 'zip') src = '/main/img/archive/file_extension/zip.svg';
// if (value.ext == 'txt') src = '/main/img/archive/file_extension/text.svg';
// if (value.ext == 'gsim') src = '/main/img/archive/file_extension/gsim.svg';
// if (value.ext == 'ifc') src = '/main/img/archive/file_extension/3d.svg';
// thumbnail.src = src;
// }
// } else {
// // thumbnailKey가 없는 경우 기본 아이콘
// let src = '/main/img/archive/file_extension/file.svg';
// if (value.ext == 'pdf') src = '/main/img/archive/file_extension/pdf.svg';
// if (value.ext == 'hwp' || value.ext == 'hwpx') src = '/main/img/archive/file_extension/hwp.svg';
// if (value.ext == 'doc' || value.ext == 'docx') src = '/main/img/archive/file_extension/doc.svg';
// if (value.ext == 'xls' || value.ext == 'xlsx' || value.ext == 'xlsm') src = '/main/img/archive/file_extension/xls.svg';
// if (value.ext == 'ppt' || value.ext == 'pptx') src = '/main/img/archive/file_extension/ppt.svg';
// if (value.ext == 'dwg' || value.ext == 'dxf' || value.ext == 'grm') src = '/main/img/archive/file_extension/dwg.svg';
// if (value.ext == 'mp3' || value.ext == 'wav') src = '/main/img/archive/file_extension/audio.svg';
// if (value.ext == 'jpg' || value.ext == 'jpeg' || value.ext == 'png') src = '/main/img/archive/file_extension/img.svg';
// if (value.ext == 'mp4' || value.ext == 'webm' || value.ext == 'mov' || value.ext == 'avi' || value.ext == 'mkv') src = '/main/img/archive/file_extension/video.svg';
// if (value.ext == 'zip') src = '/main/img/archive/file_extension/zip.svg';
// if (value.ext == 'txt') src = '/main/img/archive/file_extension/text.svg';
// if (value.ext == 'gsim') src = '/main/img/archive/file_extension/gsim.svg';
// if (value.ext == 'ifc') src = '/main/img/archive/file_extension/3d.svg';
// thumbnail.src = src;
// }
// }
// }
// //// 클러스터 레이어 재생성
// let clusterListWrap = document.querySelector('.map-container .cluster-list-wrap');
// // 1. 기존 클러스터 레이어 제거
// if (vars.olClusterLayer) {
// ol.map.removeLayer(vars.olClusterLayer);
// }
// // 2. 전체 피처로 클러스터 소스 생성 (기존 + 새로운 피처)
// let allFeatures = pointSource.getFeatures(); // 포인트 소스의 모든 피처 가져오기
// let rawSource = new ol.source.Vector({ features: allFeatures });
// let clusterSource = new ol.source.Cluster({
// distance: vars.clusterDistance,
// minDistance: 0,
// source: rawSource
// });
// // 최소 거리(minDistance) 계산 후 적용
// let radius = 20; // circle radius (스타일에서 쓰는 값과 동일)
// let strokeWidth = 4; // stroke width
// let padding = 6; // 여유
// let wantMin = 2 * radius + strokeWidth + padding; // 대략 50px
// clusterSource.setDistance(Math.max(clusterSource.getDistance(), wantMin));
// if (clusterSource.setMinDistance) {
// clusterSource.setMinDistance(wantMin);
// }
// // 클러스터 소스 강제 갱신 및 완료 대기
// clusterSource.refresh();
// // 클러스터링이 완료될 때까지 명시적으로 대기
// await new Promise(resolve => {
// // 클러스터 소스의 변경 이벤트를 감지
// const onClusterChange = () => {
// clusterSource.un('change', onClusterChange);
// resolve();
// };
// clusterSource.on('change', onClusterChange);
// // 타임아웃 설정 (최대 100ms 대기)
// setTimeout(resolve, 100);
// });
// let clusterStyleCache = { normal: {}, hover: {}, selected: {}, clicked: {} };
// let clusterLayer = new ol.layer.Vector({
// source: clusterSource,
// declutter: false,
// style: (clusterFeature) => {
// const members = clusterFeature.get('features');
// const size = members?.length || 0;
// if (size <= 1) return null;
// let selectedId = vars.lastSelectTarget?.dataset?.id;
// let isSelectedCluster = selectedId && members.some(f => String(f.get('dataId')) === String(selectedId));
// let state = 'normal';
// if (isSelectedCluster) state = 'selected';
// if (clusterFeature.get('hover')) state = 'hover';
// if (clusterFeature.get('clicked')) state = 'clicked';
// return clusterStyleFor(size, state, clusterStyleCache);
// }
// });
// // 3. 새 클러스터 레이어 추가
// vars.olClusterLayer = clusterLayer;
// ol.map.addLayer(clusterLayer);
// let lastHoveredCluster = null; // 마지막으로 hover된 클러스터
// let hoverSeq = 0; // 비동기 응답 순서 보호용
// ol.map.on('pointermove', (evt) => {
// // 드래그 중인 경우 리턴 (성능/깜빡임 방지)
// if (evt.dragging) return;
// // 위치 수정 모드인 경우 리턴
// if (document.querySelector('.list-container .list-body .map-container').classList.contains('edit-mode')) return;
// // 클러스터 레이어가 숨겨진 상태인 경우 리턴
// if (clusterLayer.getVisible() == false) return;
// const seq = ++hoverSeq;
// // 클러스터 레이어만 히트 검사 (비동기)
// clusterLayer.getFeatures(evt.pixel).then((hits) => {
// if (seq !== hoverSeq) return; // 이전 pointermove의 지연 응답 무시
// let cf = null;
// if (hits && hits.length) {
// const cand = hits[0];
// const members = cand.get('features');
// if (members && members.length > 1) cf = cand; // 진짜 클러스터만 hover 대상
// }
// // 기존 hover 해제
// if (lastHoveredCluster && lastHoveredCluster !== cf) {
// lastHoveredCluster.set('hover', false);
// lastHoveredCluster = null;
// clusterLayer.changed(); // 재렌더
// }
// // 새 hover 설정
// if (cf) {
// if (lastHoveredCluster !== cf) {
// cf.set('hover', true);
// lastHoveredCluster = cf;
// clusterLayer.changed();
// }
// ol.map.getTargetElement().style.cursor = 'pointer';
// } else {
// ol.map.getTargetElement().style.cursor = 'default';
// }
// });
// });
// // 맵 영역을 벗어났을 때 hover 리셋
// ol.map.getTargetElement().addEventListener('mouseleave', () => {
// if (lastHoveredCluster) {
// lastHoveredCluster.set('hover', false);
// lastHoveredCluster = null;
// clusterLayer.changed();
// }
// ol.map.getTargetElement().style.cursor = 'default';
// });
// let startZoom, endZoom;
// ol.map.on('movestart', () => {
// startZoom = ol.map.getView().getZoom();
// // 지도 이동 및 줌 변경 시작할 때 map-container에 overlay-suspend 클래스 추가해서 오버레이 숨김
// document.querySelector('.map-container').classList.add('overlay-suspend');
// })
// // 이동/줌 종료 시 동기화 - 렌더가 끝난 뒤에 한 번만 실행
// ol.map.on('moveend', async (e) => {
// endZoom = ol.map.getView().getZoom();
// if (endZoom >= vars.allOverlayVisibleZoom) {
// // 줌 변경 종료 시점의 줌 레벨이 vars.allOverlayVisibleZoom 이상이면 클러스터레이어 숨기고 포인트 레이어 표시
// vars.olClusterLayer.setVisible(false);
// vars.olPointLayer.setVisible(true);
// } else {
// // 줌 변경 종료 시점의 줌 레벨이 vars.allOverlayVisibleZoom 보다 작으면 클러스터레이어 표시하고 포인트 레이어 숨김
// vars.olClusterLayer.setVisible(true);
// vars.olPointLayer.setVisible(false);
// await requestAnimationFrame(() => {
// reapplyClickedByLastOverlay(clusterSource, clusterLayer, startZoom, endZoom);
// })
// clusterLayer.changed();
// }
// ol.map.once('rendercomplete', async() => {
// // 클러스터에 포함된 오버레이는 숨기고 포함되지 않은 오버레이는 표시
// // bufferPx - 양수: 뷰포트 범위 바깥까지 오버레이 표시 영역 확장 / 음수: 뷰포트 범위 안쪽으로 오버레이 표시 영역 축소
// let bufferPx = -50;
// let allOverlays = ol.map.getOverlays().getArray();
// // moveend에서는 newlyAddedIds를 빈 배열로 전달 (모든 오버레이를 일반적으로 처리)
// await syncOverlaysByCluster(allOverlays, clusterSource, bufferPx, []);
// clusterLayer.changed();
// await requestAnimationFrame(() => {
// // 지도 이동 및 줌 변경 종료할 때 map-container에 overlay-suspend 클래스 제거해서 오버레이 표시
// document.querySelector('.map-container').classList.remove('overlay-suspend');
// })
// })
// })
// ol.map.on('click', (e) => {
// // 클릭한 대상이 오버레이인 경우 리턴
// if (e.originalEvent?.target.classList.contains('list-item')) return;
// // 위치 수정 모드인 경우 리턴
// if (document.querySelector('.list-container .list-body .map-container').classList.contains('edit-mode')) return;
// // 클러스터 레이어가 숨겨진 상태인 경우 리턴
// if (clusterLayer.getVisible() == false) return;
// // 이 레이어(clusteLayer)에서만 히트 검사
// clusterLayer.getFeatures(e.pixel).then(async (clickedFeatures) => {
// if (!clickedFeatures || !clickedFeatures.length) {
// // 아무 것도 안 눌렀으면 이전에 선택된 클러스터 선택 상태 해제
// if (vars.lastSelectCluster) {
// // vars.lastSelectCluster.set('selected', false);
// vars.lastSelectCluster.set('clicked', false);
// vars.lastSelectCluster = null;
// clusterLayer.changed();
// }
// // 클러스터 아이템 리스트 닫기
// clusterListWrap.style.display = 'none';
// return;
// }
// // 클릭된 건(보통 1개)의 내부 멤버(원본 피처들)
// let clusterFeature = clickedFeatures[0];
// let items = clusterFeature.get('features');
// if (!items || items.length <= 1) {
// // 단건이면 이전에 선택된 클러스터 선택 상태 해제
// if (vars.lastSelectCluster) {
// // vars.lastSelectCluster.set('selected', false);
// vars.lastSelectCluster.set('clicked', false);
// vars.lastSelectCluster = null;
// clusterLayer.changed();
// }
// return;
// }
// // 기존 선택된 클러스터 선택 상태 해제
// for (let i = 0; i < clusterSource.getFeatures().length; i++) {
// let feature = clusterSource.getFeatures()[i];
// feature.set('clicked', false);
// }
// // 새로 선택한 클러스터에 선택 상태 설정
// vars.lastSelectCluster = clusterFeature;
// // 스타일 갱신
// clusterLayer.changed();
// // 갤러리 폴더에서 위치 수정 모드가 실행중인 경우 리턴
// if (vars.mapMode == 'edit') return;
// // 클러스터 리스트 렌더링
// await renderClusterList(items, clusterLayer);
// // 새로 선택한 클러스터에 선택 상태 설정
// clusterFeature.set('clicked', true);
// vars.lastSelectCluster.set('clicked', true);
// // 스타일 갱신
// clusterLayer.changed();
// });
// let option = {
// from: '지도 영역 클릭'
// }
// toggleArchiveMainRight(false, option);
// });
// // 아카이브 우측 영역 표시/숨김
// if (vars.lastListItem && document.querySelector(`.map-item[data-id="${vars.lastListItem.dataset.id}"]`) && vars.mapMode == 'normal') {
// let option = {
// from: '지도 렌더링 -미리보기 표시',
// }
// toggleArchiveMainRight(true, option);
// } else {
// let option = {
// from: '지도 렌더링 - 미리보기 숨김',
// }
// toggleArchiveMainRight(false, option);
// }
// if (vars.lastSelectTarget && vars.lastSelectTarget.classList.contains('list-item')) {
// let dataId = vars.lastSelectTarget.dataset.id;
// let item = document.querySelector(`.map-item[data-id="${dataId}"]`);
// if (item) {
// changeListItemStyle(item);
// }
// }
// // 지도 모드 전환 시 모든 오버레이의 범위에 맞게 카메라 이동하도록 overlayPositionArr 변수 생성
// let overlayPositionArr = overlayArr.map(overlay => overlay.getPosition());
// if (vars.mapMode == 'edit') {
// let validCoordResult = isValidCoord();
// if (validCoordResult == true) {
// let lonInput = document.querySelector('.edit-mode-ui .coord-input.lon');
// let latInput = document.querySelector('.edit-mode-ui .coord-input.lat');
// let lon = lonInput.value;
// let lat = latInput.value;
// let overlayPosition = ol.proj.fromLonLat([lon, lat]);
// // 위치 수정 모드 실행 시 경도/위도 정보가 있으면 해당 위치로 카메라 이동하도록 overlayPositionArr 변수 갱신
// overlayPositionArr = [overlayPosition];
// // 마커 표시
// vars.markerOverlay.setPosition(overlayPosition);
// }
// }
// // 줌 레벨에 따라 오버레이 크기 조절
// await scaleOverlaysByZoom();
// //// 오버레이 동기화 시 새로 추가된 ID 전달
// let bufferPx = -50;
// // 모든 오버레이 배열 가져오기 (기존 + 신규)
// let allOverlays = ol.map.getOverlays().getArray();
// // 클러스터 소스가 완전히 준비될 때까지 추가 대기
// await new Promise(resolve => setTimeout(resolve, 50));
// // **즉시 클러스터 상태 반영하여 오버레이 동기화**
// await syncOverlaysByCluster(allOverlays, clusterSource, bufferPx, newlyAddedIds);
// // 클러스터 레이어 강제 갱신
// clusterLayer.changed();
// // 포인트 레이어도 갱신
// pointLayer.changed();
// // requestAnimationFrame을 사용하여 DOM 업데이트 보장
// await new Promise(resolve => {
// requestAnimationFrame(() => {
// // 지도 렌더링 강제 트리거
// ol.map.render();
// // 한 프레임 더 대기 후 완료
// requestAnimationFrame(() => {
// document.querySelector('.map-container')?.classList.remove('overlay-suspend');
// resolve();
// });
// });
// });
// }
// 전체 트리에서 파일 찾는 함수
// function findFileInTree(obj, fileName) {
// if (obj.file && obj.file[fileName]) {
// return obj.file[fileName]
// };
// if (obj.folder) {
// for (let key in obj.folder) {
// const result = findFileInTree(obj.folder[key], fileName);
// if (result) return result;
// }
// };
// if (obj.child) {
// const result = findFileInTree(obj.child, fileName);
// if (result) return result;
// }
// return null;
// }
function isValidCoord() {
let lonInput = document.querySelector('.edit-mode-ui .coord-input.lon');
let latInput = document.querySelector('.edit-mode-ui .coord-input.lat');
let lon = lonInput.value;
let lat = latInput.value;
let re = /^\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*$/;
let lonCheck = re.test(lon);
let latCheck = re.test(lat);
if (lonCheck == false || latCheck == false) {
return false;
} else {
return true;
}
}
function updateMarker() {
let lonInput = document.querySelector('.edit-mode-ui .coord-input.lon');
let latInput = document.querySelector('.edit-mode-ui .coord-input.lat');
let lon = lonInput.value;
let lat = latInput.value;
if (lon == '없음' || lat == '없음') {
ol.map.removeOverlay(vars.markerOverlay);
return;
}
let validCoordResult = isValidCoord();
if (validCoordResult == false) return;
let coord4326 = ol.proj.fromLonLat([lon, lat]);
vars.markerOverlay.setPosition(coord4326);
ol.map.addOverlay(vars.markerOverlay);
let extent = new ol.extent.boundingExtent([coord4326]);
let paddingValue = 100;
ol.map.getView().fit(extent, {
padding: [paddingValue, paddingValue, paddingValue, paddingValue],
constrainResolution: true,
// maxZoom: 15
maxZoom: ol.map.getView().getZoom()
});
}
function checkCoordInputValue(value, type) {
// 공백 제거
let v = value.replace(/\s+/g, '');
// 허용 문자만 남기기 (숫자/.-)
v = v.replace(/[^\d\.\-]/g, '');
// 맨 앞의 '-'만 허용
v = v.replace(/(?!^)-/g, '');
if (v.indexOf('-') > 0) v = v.replace(/-/g, '');
// 점 하나만 허용
const firstDot = v.indexOf('.');
if (firstDot !== -1) {
v = v.slice(0, firstDot + 1) + v.slice(firstDot + 1).replace(/\./g, '');
}
// ".12" → "0.12", "-.12" → "-0.12"
if (v.startsWith('.')) v = '0' + v;
if (v.startsWith('-.')) v = v.replace('-.', '-0.');
// 입력 중 과도기 상태는 그대로 허용 (삭제/추가 편의)
if (v === '' || v === '-' || v === '0.' || v === '-0.') return v;
// 타입별 범위 계산
const maxAbs = type === 'lon' ? 180 : type === 'lat' ? 90 : null;
// 숫자로 해석되면 범위 클램프
const n = Number(v);
if (Number.isFinite(n) && maxAbs != null) {
const sign = n < 0 ? -1 : 1;
const abs = Math.abs(n);
if (abs > maxAbs) {
v = (sign < 0 ? '-' : '') + String(maxAbs); // 경계값으로 고정
// 필요시 소수 0채우기 등 추가 처리 가능
}
}
return v;
}
function fitToOverlayExtent(overlayPositionArr, onFitEnd) {
let extent = new ol.extent.boundingExtent(overlayPositionArr);
let paddingValue = 100;
function handlePostRender() {
ol.map.un('postrender', handlePostRender); // 한 번만 실행되도록 제거
if (typeof onFitEnd === 'function') {
onFitEnd();
}
}
ol.map.on('postrender', handlePostRender);
// maxZoom 기본값은 모든 오버레이를 표시하는 줌 레벨(vars.allOverlayVisibleZoom)로 설정
let maxZoom = vars.allOverlayVisibleZoom;
// overlayPositionArr의 length가 1이 아니고, 선택되어 있는 파일 아이템이 없는 경우
// 모든 오버레이를 표시하는 줌 레벨(vars.allOverlayVisibleZoom)보다 하나 아래 레벨로 설정해서 클러스터 표시
if (overlayPositionArr.length != 1 && !vars.lastListItem) maxZoom = vars.allOverlayVisibleZoom - 1;
ol.map.getView().fit(extent, {
padding: [paddingValue, paddingValue, paddingValue, paddingValue],
// duration: 500,
// constrainResolution: true,
maxZoom: maxZoom
});
}
function toggleMapProgress(state, progressData) {
let mapProgress = document.querySelector('.map-progress');
let mapCountname = document.querySelector('.map-progress .count-progress .fileName span');
let mapCountIdx = document.querySelector('.map-progress .count-progress .count .index span');
let mapCountTotal = document.querySelector('.map-progress .count-progress .count .total span');
if (state == false) mapProgress.style.display = 'none';
if (state == true) {
mapProgress.style.display = 'flex';
mapCountname.textContent = `${progressData.fileName}`;
mapCountIdx.textContent = `${progressData.idx}`;
mapCountTotal.textContent = `${progressData.count}`;
}
}
function scaleOverlaysByZoom() {
ol.map.getView().on('change:resolution', () => {
const zoom = ol.map.getView().getZoom();
const scale = Math.max(0.5, Math.min(2, zoom / 10));
document.querySelectorAll('.map-item').forEach(overlay => {
overlay.style.setProperty('--overlay-scale-transform', `scale(${scale})`);
overlay.style.transformOrigin = 'bottom center';
});
});
}
// size > 1 클러스터에 속한 feature id 집합 만들기
function getClusteredIds(clusterSource) {
const ids = new Set();
clusterSource.getFeatures().forEach(cf => {
const list = cf.get('features');
if (!list || list.length <= 1) return;
list.forEach(f => {
// 위에서 feature 생성 시 dataId를 넣었으므로 그 값을 사용
const id = f.get('dataId');
if (id != null) ids.add(String(id));
});
});
return ids;
}
/// 251104 박지은 ol 소켓처리
// async function syncOverlaysByCluster(overlayArr, clusterSource, bufferPx = 0, newlyAddedIds = []) {
// ID 매칭으로 오버레이 표시/숨김 동기화
async function syncOverlaysByCluster(overlayArr, clusterSource, bufferPx = 0) {
// 뷰포트(+버퍼) extent
const extent = getBufferedViewExtent(bufferPx);
const inView = (coord) => !extent || (coord && ol.extent.containsCoordinate(extent, coord));
// 현재 클러스터 멤버셋
const clustered = getClusteredIds(clusterSource); // Set<string>
/// 251104 박지은 ol 소켓처리
// 새로 추가된 ID들을 Set으로 변환 (빠른 검색)
// const newlyAddedSet = new Set(newlyAddedIds.map(id => String(id)));
// 줌 임계
const zoom = ol.map.getView().getZoom();
// 오버레이 DOM 토글
for (let ov of overlayArr) {
const el = ov.getElement();
if (!el) continue;
// map-item이 아니면 스킵
if (!el.classList.contains('map-item')) continue;
const id = el.dataset.id;
const pos = ov.getPosition();
const inside = inView(pos);
/// 251104 박지은 ol 소켓처리
// 새로 추가된 오버레이인지 확인
// const isNewlyAdded = newlyAddedSet.has(String(id));
// 클러스터에 포함되었는지 확인
const isInCluster = clustered.has(String(id));
let show;
if (zoom >= vars.allOverlayVisibleZoom) {
// 고배율: 항상 뷰포트(+버퍼) 안만 표시
show = inside;
} else {
// 저배율: 클러스터 멤버면 숨김 + 뷰포트 밖이면 숨김
show = !clustered.has(String(id)) && inside;
}
/// 251104 박지은 ol 소켓처리
// if (zoom >= vars.allOverlayVisibleZoom) {
// // 고배율: 클러스터 상관없이 뷰포트 안만 표시
// show = inside;
// } else {
// // 저배율: 클러스터에 포함되면 무조건 숨김
// if (isInCluster) {
// show = false;
// } else {
// // 클러스터에 없으면 뷰포트 확인
// // 새로 추가된 것은 뷰포트 체크 무시
// if (isNewlyAdded) {
// show = true;
// } else {
// show = inside;
// }
// }
// }
const currentDisplay = el.style.display;
const hidden = currentDisplay === 'none';
if (show && hidden) {
el.style.display = 'flex';
} else if (!show && !hidden) {
el.style.display = 'none';
}
// 실제 표시 중일 때만 리스트 닫고 하이라이트
if (show && vars.lastSelectTarget && vars.lastSelectTarget.dataset.id == id) {
let clusterListWrap = document.querySelector('.map-container .cluster-list-wrap');
if (clusterListWrap) {
clusterListWrap.innerHTML = '';
clusterListWrap.style.display = 'none';
}
changeListItemStyle(el);
}
}
}
function getBufferedViewExtent(bufferPx = 0) {
const map = ol.map;
const view = map.getView();
const size = map.getSize();
if (!view || !size) return null;
let extent = view.calculateExtent(size);
if (bufferPx !== 0) {
const res = view.getResolution(); // map-units/px
if (!res) return extent;
const b = bufferPx * res; // px → map-units
const w = extent[2] - extent[0];
const h = extent[3] - extent[1];
// 음수(축소)일 때 영역이 뒤집히지 않도록 클램프
const maxInset = Math.min(w, h) / 2 - 1e-6; // 남기는 최소 여유
const clamped = Math.max(-maxInset, b); // 너무 큰 음수 방지
extent = ol.extent.buffer(extent, clamped);
}
return extent;
}
// function clusterStyleFor(size, state, clusterStyleCache) {
// let cache = clusterStyleCache[state];
// if (!cache[size]) {
// let scale =
// state === 'selected' ? 1.2 :
// state === 'hover' ? 1.2 : 1.1;
// let strokeWidth =
// state === 'selected' ? 4.5 :
// state === 'hover' ? 4.5 : 4;
// let radius = 20 * scale;
// cache[size] = new ol.style.Style({
// image: new ol.style.Circle({
// radius,
// fill: new ol.style.Fill({ color: '#fff' }),
// stroke: new ol.style.Stroke({ color: state === 'selected' ? '#f9613b' : '#000', width: strokeWidth })
// }),
// // image: new ol.style.RegularShape({
// // points: 4, // 꼭짓점 4개
// // radius: radius * Math.SQRT2, // 원과 같은 가로폭(지름)을 맞추고 싶으면 √2 배
// // angle: Math.PI / 4, // 45도 회전 (각 변이 수평/수직)
// // fill: new ol.style.Fill({ color: '#fff' }),
// // stroke: new ol.style.Stroke({ color: state === 'selected' ? '#f9613b' : '#000', width: strokeWidth })
// // }),
// text: new ol.style.Text({
// font:
// state === 'selected' ? '14px sans-serif' :
// state === 'hover' ? '14px sans-serif' : '12px sans-serif',
// text: String(size),
// fill: new ol.style.Fill({ color: '#fff' }),
// stroke: new ol.style.Stroke({ color: '#000', width: 6 })
// })
// });
// }
// return cache[size];
// }
function clusterStyleFor(size, state, clusterStyleCache) {
let cache = clusterStyleCache[state];
if (!cache[size]) {
let scale, strokeWidth, strokeColor, font;
if (state == 'selected') {
scale = 1.2;
strokeWidth = 4.5;
strokeColor = '#f9613b';
font = '14px sans-serif';
}
if (state == 'hover') {
scale = 1.2;
strokeWidth = 4.5;
strokeColor = '#000';
font = '14px sans-serif';
}
if (state == 'clicked') {
scale = 1.2;
strokeWidth = 4.5;
strokeColor = '#3bb5f9';
font = '14px sans-serif';
}
if (state == 'normal') {
scale = 1.1;
strokeWidth = 4;
strokeColor = '#000';
font = '12px sans-serif';
}
let radius = 20 * scale;
cache[size] = new ol.style.Style({
image: new ol.style.Circle({
radius,
fill: new ol.style.Fill({ color: '#fff' }),
stroke: new ol.style.Stroke({ color: strokeColor, width: strokeWidth })
}),
text: new ol.style.Text({
font: font,
text: String(size),
fill: new ol.style.Fill({ color: '#fff' }),
stroke: new ol.style.Stroke({ color: '#000', width: 6 })
})
});
}
return cache[size];
}
function reapplyClickedByLastOverlay(clusterSource, clusterLayer, startZoom, endZoom) {
const el = vars.lastSelectTarget;
if (!el) return;
const id = el.dataset?.id;
if (!id) return;
// 현재 프레임의 클러스터들 중에서 해당 id를 포함하는 클러스터 찾기
const feats = clusterSource.getFeatures();
let target = null;
for (const cf of feats) {
const members = cf.get('features');
if (!members || members.length <= 1) continue;
if (members.some(f => String(f.get('dataId')) === String(id))) {
target = cf; break;
}
}
// 줌 변경 시에만(그리고 normal 모드일 때만) 리스트 다시 그림
if (startZoom && endZoom) {
if (target && startZoom != endZoom && vars.mapMode == 'normal') {
renderClusterList(target.get('features'), clusterLayer);
}
}
// 선택된 id를 기준으로 스타일이 다시 평가되도록 강제 리렌더
clusterLayer.changed();
}
function renderClusterList(items, clusterLayer) {
let clusterListWrap = document.querySelector('.map-container .cluster-list-wrap');
clusterListWrap.innerHTML = '';
clusterListWrap.style.display = 'flex';
for(let i = 0; i < items.length; i++) {
let item = items[i];
let element = item.values_.overlay.values_.element;
let thumbnailWrapClone = element.querySelector('.wrap').cloneNode(true);
thumbnailWrapClone.querySelector('.gps-data').remove();
thumbnailWrapClone.querySelector('.name').remove();
thumbnailWrapClone.querySelector('.uploader').remove();
thumbnailWrapClone.querySelector('.create-date').remove();
thumbnailWrapClone.querySelector('.size').remove();
let nameClone = element.querySelector('.name').cloneNode(true);
// nameClone.querySelector('.name-text').classList.remove('ft-14')
// nameClone.querySelector('.name-text').classList.add('ft-12')
nameClone.style.display = 'flex';
let resourcePath = element.dataset.resourcePath;
let dataId = element.dataset.id;
let clusterList = document.createElement('div');
clusterList.classList.add('cluster-list');
clusterList.dataset.id = dataId;
clusterList.dataset.resourcePath = resourcePath;
clusterList.appendChild(thumbnailWrapClone);
clusterList.appendChild(nameClone);
// vars.lastSelectTarget의 dataId와 동일한 dataId를 가진 clusterList에 selected 클래스 추가해서 선택 상태로 표시하고 포커스
if (vars.lastSelectTarget && vars.lastSelectTarget.dataset.id == dataId) {
clusterList.classList.add('selected');
// requestAnimationFrame(() => {
requestAnimationFrame(() => {
// clusterList.scrollIntoView({ behavior: 'instant', block: 'center', inline: 'nearest' });
targetFocus(clusterList, 'instant');
});
// });
}
clusterListWrap.appendChild(clusterList);
clusterList.addEventListener('click', async function(e) {
let option = {
from: '클러스터 리스트 클릭'
}
toggleArchiveMainRight(true, option);
let res = await axios.get(`${vars.path_name}/getMemoInfo`, {
params: { userInfoString: vars.userInfoString, resourcePath, dataId }
});
let memo = res.data.result.memo;
setTimeout(() => {
renderMemo(memo, dataId);
}, 100);
// 정보영역 숨김
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.remove('open');
// document.querySelector('.archive-main .archive-main-right .viewer-container .info-wrap').classList.add('close');
vars.viewerConnectingTime = vars.viewerConnectingTimeValue;
renderViewer(resourcePath, dataId);
vars.lastSelectTarget = document.querySelector(`.map-container .list-item[data-resource-path="${resourcePath}"]`);
changeListItemStyle(vars.lastSelectTarget);
vars.lastListItem = vars.lastSelectTarget;
vars.multiSelectListItemArr = [];
document.querySelectorAll('.cluster-list-wrap .cluster-list').forEach(clusterList => {
clusterList.classList.remove('selected');
})
e.target.classList.add('selected');
clusterLayer.changed();
// 클러스터 아이템 리스트에서 클릭한 대상의 위치로 지도 이동
let data = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject).data;
let overlayPosition = ol.proj.fromLonLat([data.lon, data.lat]);
let overlayPositionArr = [overlayPosition];
await fitToOverlayExtent(overlayPositionArr, async () => {});
})
}
}
export async function initEditPositionMode() {
document.querySelector('.control-box .file-area-mode-btn.map').click();
toggleArchiveMainRight(false);
vars.mapMode = 'edit';
document.querySelector('.map-container').classList.add('edit-mode');
// 위치 입력 부분 높이값
let coordWrapHeightRem = pxToRem(document.querySelector('.coord-wrap').getBoundingClientRect().height);
// 썸네일 wrap 복제
let thumbnailWrapClone = vars.lastListItem.querySelector('.wrap').cloneNode(true);
thumbnailWrapClone.querySelector('.highlight')?.remove();
thumbnailWrapClone.querySelector('.gps-data')?.remove();
thumbnailWrapClone.querySelector('.name')?.remove();
thumbnailWrapClone.querySelector('.uploader')?.remove();
thumbnailWrapClone.querySelector('.create-date')?.remove();
thumbnailWrapClone.querySelector('.size')?.remove();
thumbnailWrapClone.style.width = `${coordWrapHeightRem}rem`;
thumbnailWrapClone.style.minWidth = `${coordWrapHeightRem}rem`;
thumbnailWrapClone.style.maxWidth = `${coordWrapHeightRem}rem`;
thumbnailWrapClone.style.height = `${coordWrapHeightRem}rem`;
thumbnailWrapClone.style.minHeight = `${coordWrapHeightRem}rem`;
thumbnailWrapClone.style.maxHeight = `${coordWrapHeightRem}rem`;
thumbnailWrapClone.dataset.id = vars.lastListItem.dataset.id;
thumbnailWrapClone.dataset.resourcePath = vars.lastListItem.dataset.resourcePath;
// 파일명 복제
let nameClone = vars.lastListItem.querySelector('.name').cloneNode(true);
nameClone.style.display = 'flex';
// 현재 위치 수정중인 파일 정보 wrap에 복제한 썸네일 wrap, 파일명 추가
let editModeUi = document.querySelector('.edit-mode-ui');
let infoWrap = editModeUi.querySelector('.info-wrap');
infoWrap.style.height = `${coordWrapHeightRem}rem`;
infoWrap.innerHTML = '';
infoWrap.appendChild(thumbnailWrapClone);
infoWrap.appendChild(nameClone);
// 썸네일 wrap에 호버 이벤트 추가
thumbnailWrapClone.addEventListener('pointerover', function() {
if (document.querySelector('.popup-thumbnail')) document.querySelector('.popup-thumbnail').remove();
let popupThumbnailWrapClone = thumbnailWrapClone.cloneNode(true);
popupThumbnailWrapClone.classList.add('popup-thumbnail');
document.querySelector('.map-container .edit-mode-ui').appendChild(popupThumbnailWrapClone);
let editModeUiWidth = editModeUi.getBoundingClientRect().width;
let editModeUiHeight = editModeUi.getBoundingClientRect().height;
let rem;
if (editModeUiWidth >= editModeUiHeight) rem = pxToRem(editModeUiHeight);
if (editModeUiWidth < editModeUiHeight) rem = pxToRem(editModeUiWidth);
popupThumbnailWrapClone.style.width = `${rem*0.5}rem`;
popupThumbnailWrapClone.style.minWidth = `${rem*0.5}rem`;
popupThumbnailWrapClone.style.maxWidth = `${rem*0.5}rem`;
popupThumbnailWrapClone.style.height = `${rem*0.5}rem`;
popupThumbnailWrapClone.style.minHeight = `${rem*0.5}rem`;
popupThumbnailWrapClone.style.maxHeight = `${rem*0.5}rem`;
popupThumbnailWrapClone.style.background = '#aaa';
popupThumbnailWrapClone.style.border = '0.0625rem solid #ccc';
popupThumbnailWrapClone.style.borderRadius = '0.25rem';
popupThumbnailWrapClone.style.position = 'absolute';
popupThumbnailWrapClone.style.bottom = `${coordWrapHeightRem + 4}rem`;
popupThumbnailWrapClone.style.left = '50%';
popupThumbnailWrapClone.style.transform = 'translate(-50%, 0)';
popupThumbnailWrapClone.style.pointerEvents = 'none';
popupThumbnailWrapClone.style.zIndex = '9999';
})
thumbnailWrapClone.addEventListener('pointerleave', function() {
if (document.querySelector('.popup-thumbnail')) document.querySelector('.popup-thumbnail').remove();
})
let resourcePath = vars.lastListItem.dataset.resourcePath;
let data = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject).data;
let lon = (data.lon) ? data.lon : '없음';
let lat = (data.lat) ? data.lat : '없음';
let lonInput = document.querySelector('.edit-mode-ui .coord-input.lon');
let latInput = document.querySelector('.edit-mode-ui .coord-input.lat');
lonInput.value = lon;
latInput.value = lat;
}
export function deletePosition() {
let lonInput = document.querySelector('.edit-mode-ui .coord-input.lon');
let latInput = document.querySelector('.edit-mode-ui .coord-input.lat');
lonInput.value = '없음';
latInput.value = '없음';
updateMarker();
document.querySelector('.edit-mode-ui .save-btn').click();
}
export function renderMemo(fileData, dataId) {
// let infoWrap = document.querySelector('.archive-main-right .viewer-container .info-wrap');
const container = dataId
? document.querySelector(`.archive-main-right .viewer-container[data-data-id="${dataId}"]`)
: document.querySelector('.archive-main-right .viewer-container');
if (!container) return
let infoWrap = container.querySelector('.info-wrap'); // ★ scope 한정
if (!infoWrap) return;
let editBtn = infoWrap.querySelector('.memo .header .wrap .edit-btn');
let editMessage = infoWrap.querySelector('.memo .header .wrap .message');
let bodyWrap = infoWrap.querySelector('.memo .body .wrap');
let bodyTextarea = infoWrap.querySelector('.memo .body .wrap .textarea');
let bodyMessage = infoWrap.querySelector('.memo .body .wrap .message');
// editBtn 클래스에 edit 없으면 추가, save 있으면 삭제
// if (!editBtn.matches('.edit')) editBtn.classList.add('edit');
// if (editBtn.matches('.save')) editBtn.classList.remove('save');
// editBtn 텍스트 '수정'으로 변경
// if (editBtn) editBtn.querySelector('.text').innerText = '수정';
// 수정 상태 강제 초기화
if (editBtn?.classList.contains('save')) {
editBtn.classList.remove('save');
}
if (editBtn) editBtn.querySelector('.text').innerText = '수정';
// haederMessage 표시 상태인 경우 바로 숨김
editMessage.style.transition = '0s';
editMessage.style.opacity = '0';
// bodyWrap 테두리 강조 해제
bodyWrap.style.border = '1px solid #ddd';
bodyTextarea.disabled = true;
// bodyTextarea disabled 상태로 설정
bodyTextarea.disabled = true;
if(fileData == undefined || fileData == '') {
bodyTextarea.value = '';
}else{
bodyTextarea.value = fileData;
}
// 상태 초기화
// ** 권한 관련
let permission = JSON.parse(vars.userInfoString).permission;
if (!vars.permission.checkPermission('memo-text')) {
bodyMessage.innerText = '참관자 권한은 열람만 가능합니다.';
} else {
if(editBtn?.querySelector('.text').innerText == '수정') {
if (!editBtn.matches('.edit')) editBtn.classList.add ('edit');
bodyWrap.style.border = '1px solid #ddd';
bodyTextarea.disabled = true;
bodyMessage.innerText = '수정 버튼을 눌러 내용을 작성/수정할 수 있습니다.';
if (bodyTextarea.value == '') bodyMessage.style.color = '#777';
else bodyMessage.style.color = '#ddd';
}else {
if (editBtn?.matches('.save')) editBtn.classList.remove('save');
if (editBtn) editBtn.querySelector('.text').innerText = '저장';
bodyWrap.style.border = '1px solid #000'
bodyTextarea.disabled = false;
bodyMessage.innerText = '내용을 작성한 후 저장 버튼을 눌러주세요.';
}
}
}
let renderTimer = null;
let progressFrame = null;
let progressStart = null;
export async function renderViewer(resourcePath, dataId, shouldAddClickLog = true) {
viewerContainer.style.display = 'flex';
viewerNotice.style.display = 'none';
infoWrap.style.height = '15%';
viewerToolbar.style.display = 'none';
metadata.style.display = 'none';
viewerContainer.dataset.resourcePath = resourcePath;
viewerContainer.dataset.dataId = dataId;
// let memo = infoWrap.querySelector('.memo');
// memo.style.height = '100%';
let toggleBtnText = infoWrap.querySelector('.separator .toggle-btn .text');
//// 리스트 모드와 지도 표시 모드에서 미리보기 하단 영역 다르게 표시 --- 시작
// if (listContainer.classList.contains('grid')) {
// infoWrap.style.height = '30%';
// // // 그리드 파일 목록에서 뷰어 실행할 때 메타데이터영역, 뷰어툴바 표시
// // viewerWrap.style.display = 'flex';
// // metadata.style.display = 'flex';
// // viewerToolbar.style.display = 'flex';
// // 그리드 파일 목록에서 뷰어 실행할 때 메타데이터영역 표시
// metadata.style.display = 'flex';
// // memo.style.height = 'auto';
// // 그리드 아이템의 하위 dom에 담겨있는 정보를 가져와서 메타데이터 영역 내용 추가
// let currentItem = listContainer.querySelector(`.list-body .list-item[data-resource-path="${resourcePath}"]`);
// let arr = ['name', 'uploader', 'create-date', 'author', 'size', 'gps-data'];
// arr.map(a => {
// metadata.querySelector(`.${a} .value`).innerHTML = currentItem.querySelector(`.${a} .text`).innerHTML;
// })
// if (listContainer.querySelector('.list-body .list-item-wrap').style.display == 'flex') {
// // 그리드 파일 목록에서 뷰어 실행할 때 뷰어툴바 표시
// viewerToolbar.style.display = 'flex';
// // 메타데이터 영역 위(뷰어 하단)로 뷰어툴바 위치 조정
// // viewerToolbar.style.bottom = `${pxToRem(infoWrap.offsetHeight + 16)}rem`;
// // 뷰어툴바 이전/다음 버튼에 disabled 클래스 삭제
// let prevBtn = viewerToolbar.querySelector('.prev-btn');
// let nextBtn = viewerToolbar.querySelector('.next-btn');
// prevBtn.classList.remove('disabled');
// nextBtn.classList.remove('disabled');
// // 현재 선택된 아이템이 첫번째 아이템이면 이전버튼에, 마지막 아이템이면 다음버튼에 disabled 클래스 추가
// let listItemArr = [...listContainer.querySelector('.list-body .list-item-wrap').children];
// let currentIdx = listItemArr.indexOf(vars.lastListItem);
// if (currentIdx == 0) viewerToolbar.querySelector('.prev-btn').classList.add('disabled');
// if (currentIdx == listItemArr.length-1) viewerToolbar.querySelector('.next-btn').classList.add('disabled');
// }
// toggleBtnText.textContent = '메타데이터';
// } else {
// toggleBtnText.textContent = '메모 (AI 요약)';
// }
//// 리스트 모드와 지도 표시 모드에서 미리보기 하단 영역 다르게 표시 --- 끝
//// 리스트 모드와 지도 표시 모드에서 미리보기 하단 영역 메타데이터 형태로 동일하게 표시 --- 시작
infoWrap.style.height = '30%';
// // 그리드 파일 목록에서 뷰어 실행할 때 메타데이터영역, 뷰어툴바 표시
// viewerWrap.style.display = 'flex';
// metadata.style.display = 'flex';
// viewerToolbar.style.display = 'flex';
// 그리드 파일 목록에서 뷰어 실행할 때 메타데이터영역 표시
metadata.style.display = 'flex';
// memo.style.height = 'auto';
// 그리드 아이템의 하위 dom에 담겨있는 정보를 가져와서 메타데이터 영역 내용 추가
let parts = resourcePath.split('/').filter(Boolean);
let fileType = parts[3] ?? "";
let currentItem;
// 버전/첨부파일이 아닐 경우 (현재 버전/첨부파일은 지도표시에 표시안됨)
if(!fileType.includes('_version') && !fileType.includes('__attachment')) {
currentItem = listContainer.querySelector(`.list-body .list-item[data-resource-path="${resourcePath}"]`);
} else {
currentItem = vars.lastSelectTarget;
}
let arr = ['name', 'uploader', 'create-date', 'author', 'size', 'gps-data'];
arr.map(a => {
metadata.querySelector(`.${a} .value`).innerHTML = currentItem.querySelector(`.${a} .text`).innerHTML;
})
// if (listContainer.querySelector('.list-body .list-item-wrap').style.display == 'flex') {
// // 그리드 파일 목록에서 뷰어 실행할 때 뷰어툴바 표시
// viewerToolbar.style.display = 'flex';
// // 메타데이터 영역 위(뷰어 하단)로 뷰어툴바 위치 조정
// // viewerToolbar.style.bottom = `${pxToRem(infoWrap.offsetHeight + 16)}rem`;
// // 뷰어툴바 이전/다음 버튼에 disabled 클래스 삭제
// let prevBtn = viewerToolbar.querySelector('.prev-btn');
// let nextBtn = viewerToolbar.querySelector('.next-btn');
// prevBtn.classList.remove('disabled');
// nextBtn.classList.remove('disabled');
// // 현재 선택된 아이템이 첫번째 아이템이면 이전버튼에, 마지막 아이템이면 다음버튼에 disabled 클래스 추가
// let listItemArr = [...listContainer.querySelector('.list-body .list-item-wrap').children];
// let currentIdx = listItemArr.indexOf(vars.lastListItem);
// if (currentIdx == 0) viewerToolbar.querySelector('.prev-btn').classList.add('disabled');
// if (currentIdx == listItemArr.length-1) viewerToolbar.querySelector('.next-btn').classList.add('disabled');
// }
toggleBtnText.textContent = '메타데이터';
//// 리스트 모드와 지도 표시 모드에서 미리보기 하단 영역 메타데이터 형태로 동일하게 표시 --- 끝
// console.log('========= renderViewer');
// console.log(resourcePath);
// console.log(dataId);
let pdfArr = ['hwp', 'hwpx', 'doc', 'docx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'dwg', 'dxf', 'grm', 'pdf'];
let gsimArr = ['gsim'];
let ifcArr = ['ifc'];
let imageArr = ['png', 'jpg', 'jpeg', 'webp', 'gif'];
let videoArr = ['mp4', 'mov', 'webm'];
let textArr = ['txt', 'log', 'md'];
let urlArr = ['url'];
let zipArr = ['zip'];
let threeArr = ['glb', 'gltf', 'obj', 'stl', 'fbx', '3dm'];
let htmlArr = ['html'];
let allArr = [...pdfArr, ...videoArr, ...imageArr, ...ifcArr, ...gsimArr, ...textArr, ...urlArr, ...zipArr, ...threeArr, ...htmlArr];
let isLowerExt = true;
let ext = splitBaseAndExt(resourcePath, isLowerExt).ext;
let excelDirectArr = ['xls', 'xlsx', 'xlsm'];
let hwpDirectArr = ['hwp', 'hwpx'];
let wordDirectArr = ['docx'];
let isDirectView = excelDirectArr.includes(ext) || hwpDirectArr.includes(ext) || wordDirectArr.includes(ext);
let treeData = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject)?.data || {};
let previewKey = treeData.previewKey;
let objectKey = treeData.objectKey || treeData.object_key;
// 문서 뷰어 실행 시 미리보기 10장 제한 문구 표시
let thumbAlert = document.querySelector('.archive-main-right .viewer-container .viewer-header .thumb-alert');
if (pdfArr.includes(ext)) {
thumbAlert.style.display = 'flex';
} else {
thumbAlert.style.display = 'none';
}
// fallback-pdf-btn 숨김
const mainFallbackPdfBtn = document.getElementById('main-fallback-pdf-btn');
if (mainFallbackPdfBtn) {
mainFallbackPdfBtn.style.display = 'none';
}
// 지원 파일인 경우 뷰어 프로그레스 표시, 대기 시간 700ms로 설정, 전체보기 버튼 표시
let originViewBtn = document.querySelector('.archive-main-right .viewer-container .viewer-header .btn');
if (allArr.includes(ext) && (previewKey || isDirectView)) {
toggleViewerProgress(true);
vars.viewerConnectingTime = 700;
originViewBtn.style.display = 'flex';
} else {
toggleViewerProgress(false);
vars.viewerConnectingTime = 0;
originViewBtn.style.display = 'none';
}
if (!shouldAddClickLog) vars.viewerConnectingTime = 0;
clearTimeout(renderTimer);
renderTimer = setTimeout(async () => {
toggleViewerProgress(false);
// 뷰어 초기화
vars.viewer = viewerWrap.querySelector('.viewer');
vars.viewer.innerHTML = '';
vars.viewer.style.display = 'flex';
// Presigned URL
let PresignedUrl = undefined;
let openFileViewer = true;
if (!previewKey || !objectKey) {
let getDataInfoParams = {
userInfoString: vars.userInfoString,
storageType: vars.storageType,
dataIdArr: [dataId],
isRemoved: false,
debug: "'renderViewer'에서 실행"
}
let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams } );
if (getDataInfoRes.data.message == 'getDataInfo_success') {
let result = getDataInfoRes.data.result;
if (result) {
previewKey = result.preview_key;
objectKey = result.object_key;
}
}
}
let targetKey = isDirectView ? objectKey : previewKey;
if (allArr.includes(ext)) {
if (targetKey == undefined || targetKey == `` || targetKey == null) {
viewerConvert();
openFileViewer = false;
shouldAddClickLog = false;
} else {
let generateDownloadUrlParams = {
objectKey: targetKey,
resourcePath: resourcePath
}
let generateDownloadUrlRes = await axios.post(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams);
if (generateDownloadUrlRes.data.message == 'generateDownloadUrl_success') {
PresignedUrl = generateDownloadUrlRes.data.url;
}
}
} else {
viewerUnsupport(ext);
openFileViewer = false;
shouldAddClickLog = false;
}
if (openFileViewer) {
let ext = (splitBaseAndExt(resourcePath).ext).toLowerCase();
let pdfArrFiltered = pdfArr.filter(e => !excelDirectArr.includes(e) && !hwpDirectArr.includes(e) && !wordDirectArr.includes(e));
// 3D뷰어 썸네일 변수
const thumbnail_key = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject).data?.thumbnailKey;
if (allArr.includes(ext)) {
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 (gsimArr.includes(ext)) viewerGsim(PresignedUrl);
if (ifcArr.includes(ext)) viewerIfc(PresignedUrl, thumbnail_key, resourcePath, dataId, vars.path_name);
if (threeArr.includes(ext)) viewer3d(PresignedUrl, thumbnail_key, resourcePath, dataId, vars.path_name);
if (imageArr.includes(ext)) viewerImage(PresignedUrl);
if (videoArr.includes(ext)) viewerVideo(PresignedUrl);
if (textArr.includes(ext)) viewerText(PresignedUrl, ext);
if (urlArr.includes(ext)) viewerURL(PresignedUrl);
if (zipArr.includes(ext)) viewerZIP(PresignedUrl);
if (htmlArr.includes(ext)) viewerHTML(PresignedUrl);
} else {
viewerUnsupport(ext);
shouldAddClickLog = false;
}
}
renderTimer = null;
// 클릭 로그 추가 (renderViewer)
if (shouldAddClickLog) {
// console.log('클릭 로그 추가 (renderViewer)');
let params = {
userInfoString: vars.userInfoString,
dataIdArr: [String(dataId)],
resourcePathArr: [resourcePath],
activity: 'click_file'
}
mgmtFunc_addClickLog(params);
}
// AI 버튼 상태 복원 (뷰어 렌더링 완료 후 실행)
// restoreAiButtonState(resourcePath, viewerContainer.dataset.dataId);
}, vars.viewerConnectingTime);
function toggleViewerProgress(state) {
let viewerProgress = document.querySelector('.viewer-progress');
let viewerProgressFill = viewerProgress.querySelector('.wrap .bar .fill');
if (state == false) {
viewerProgress.style.display = 'none';
if (progressFrame) {
cancelAnimationFrame(progressFrame);
progressFrame = null;
}
if (viewerProgressFill) viewerProgressFill.style.transform = 'scaleX(0)';
return;
}
if (state == true) {
viewerProgress.style.display = 'flex';
if (progressFrame) cancelAnimationFrame(progressFrame);
progressStart = performance.now();
function animate(now) {
let elapsed = now - progressStart;
let t = Math.min(elapsed / vars.viewerConnectingTime, 1);
let eased = 1 - (1 - t) ** 5;
if (viewerProgressFill) viewerProgressFill.style.transform = `scaleX(${eased})`;
if (elapsed < vars.viewerConnectingTime) progressFrame = requestAnimationFrame(animate);
}
progressFrame = requestAnimationFrame(animate);
}
}
function viewerUnsupport(ext) {
let viewerUnsupportWrap = document.createElement('div');
viewerUnsupportWrap.classList.add('viewer-unsupport-wrap');
let text = document.createElement('div');
text.classList.add('text');
if (isValidExt(ext)) {
// 확장자가 유효한 경우
text.innerText = `${ext} 파일 형식은 현재 지원되지 않습니다.`;
} else {
// 확장자가 유효하지 않은 경우
text.innerText = `확장자가 유효하지 않은 파일은 지원되지 않습니다.`;
}
viewerUnsupportWrap.appendChild(text);
vars.viewer.appendChild(viewerUnsupportWrap);
vars.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 = `해당 파일은 변환 후 열람이 가능합니다.`;
// let convertBtn = document.createElement('div');
// convertBtn.textContent = '변환';
// convertBtn.classList.add('convert-btn');
// convertBtn.addEventListener('click', () => {
// convertPdf(resourcePath);
// })
viewerConvertWrap.appendChild(text);
// viewerConvertWrap.appendChild(convertBtn);
vars.viewer.appendChild(viewerConvertWrap);
vars.viewer.dataset.viewerType = 'convert';
}
// -----------------------------------------------------------------
// 오픈소스 문서 직접 뷰잉 및 PDF 폴백 함수 정의 (Main Viewer)
// -----------------------------------------------------------------
function initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey) {
const btn = document.getElementById('main-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) {
let getDataInfoParams = {
userInfoString: vars.userInfoString,
storageType: vars.storageType,
dataIdArr: [dataId],
isRemoved: false,
debug: "main fallback"
}
let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams } );
if (getDataInfoRes.data.message == 'getDataInfo_success') {
let result = getDataInfoRes.data.result;
if (result) {
previewKey = result.preview_key;
objectKey = result.object_key;
}
}
}
// 2. 만약 PDF 변환본이 아직 없다면 백엔드 변환 요청
if (!previewKey) {
newBtn.querySelector('.text').textContent = 'PDF 변환 요청 중...';
await convertPdf(resourcePath, dataId);
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(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams);
if (generateDownloadUrlRes.data.message == 'generateDownloadUrl_success') {
let pdfUrl = generateDownloadUrlRes.data.url;
// 화면 초기화 및 PDF 뷰어 로드
vars.viewer = viewerWrap.querySelector('.viewer');
vars.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) {
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">엑셀 데이터를 불러오는 중...</div>';
fetch(presignedUrl)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
return res.arrayBuffer();
})
.then(arrayBuffer => {
vars.viewer.innerHTML = '';
LuckyExcel.transformExcelToLucky(arrayBuffer, function(exportJson, luckysheetfile) {
if(exportJson.sheets == null || exportJson.sheets.length == 0) {
vars.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>';
initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey);
return;
}
if (window.luckysheet) {
window.luckysheet.destroy();
}
vars.viewer.style.position = 'relative';
const container = document.createElement('div');
container.id = 'luckysheet_inner';
container.style.margin = '0px';
container.style.padding = '0px';
container.style.position = 'absolute';
container.style.width = '100%';
container.style.height = '100%';
container.style.left = '0px';
container.style.top = '0px';
vars.viewer.appendChild(container);
try {
window.luckysheet.create({
container: 'luckysheet_inner',
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);
vars.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);
vars.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>`;
initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey);
});
})
.catch(err => {
console.error(err);
vars.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>`;
initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey);
});
vars.viewer.dataset.viewerType = 'excel';
}
function viewerWord(presignedUrl) {
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">워드 문서를 불러오는 중...</div>';
initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey);
fetch(presignedUrl)
.then(res => {
if (!res.ok) throw new Error('Word fetch failed');
return res.arrayBuffer();
})
.then(arrayBuffer => {
vars.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);
vars.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);
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">워드 문서를 불러오는데 실패했습니다.</div>';
});
vars.viewer.dataset.viewerType = 'word';
}
function viewerHwp(presignedUrl) {
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;font-size:1.2rem;color:#666;background:#fff;">한글 문서를 불러오는 중...</div>';
initMainFallbackPdfButton(dataId, resourcePath, objectKey, previewKey);
fetch(presignedUrl)
.then(res => {
if (!res.ok) throw new Error('HWP fetch failed');
return res.blob();
})
.then(blob => {
vars.viewer.innerHTML = '';
const container = document.createElement('div');
container.style.width = '100%';
container.style.height = '100%';
container.style.overflow = 'auto';
container.style.padding = '20px';
container.style.boxSizing = 'border-box';
container.style.background = '#f5f5f5';
const hwpInner = document.createElement('div');
hwpInner.style.background = '#ffffff';
hwpInner.style.margin = '0 auto';
hwpInner.style.maxWidth = '800px';
hwpInner.style.boxShadow = '0 4px 10px rgba(0,0,0,0.1)';
hwpInner.style.padding = '40px';
hwpInner.style.minHeight = '100%';
container.appendChild(hwpInner);
vars.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);
vars.viewer.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100%;color:#d9534f;background:#fff;">한글 문서를 불러오는데 실패했습니다.</div>';
});
vars.viewer.dataset.viewerType = 'hwp';
}
//// 원본 viewer
async function viewerPdf(presignedUrl) {
if(presignedUrl == undefined || presignedUrl == ``){
viewerConvert();
return;
}
let pdfOptions = {
url : presignedUrl,
initialPage: 1,
}
let iframe = document.createElement('iframe');
iframe.src = `/libs/pdfViewer/web/viewer.html`;
iframe.addEventListener('load', async () => {
// 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('.viewer-container .viewer-wrap .viewer iframe').contentWindow.PDFViewerApplication;
app.pdfCursorTools._handTool.activate();
app.open(pdfOptions);
// 미리보기 10페이지 표시 제한
app.appConfig.mainContainer.classList.add('scrollbar');
app.appConfig.mainContainer.style.marginLeft = '4px';
iframe.contentWindow.document.querySelector('body').style.backgroundColor = '#383838';
// 미리보기 10페이지 표시 제한
app.eventBus.on('pagesloaded', async function onPagesLoaded() {
// PDF가 완전히 로드된 후에 실행
// 미리보기 PDF 페이지 수가 10장 이하면 리턴
if (app.pagesCount <= 10) return;
let mainContainer = iframe.contentWindow.document.querySelector('#mainContainer');
let viewerContainer = mainContainer.querySelector('#viewerContainer');
let viewer = viewerContainer.querySelector('#viewer');
let pages = viewer.querySelectorAll('.page');
let numPages = mainContainer.querySelector('#toolbarContainer #numPages');
// 페이지가 10장 초과인 경우 툴바에 최대 페이지 수 10페이지로 고정
if (app.pagesCount > 10) numPages.innerText = '/ 10';
// 최대 페이지 수 표시
numPages.style.display = 'flex';
// 페이지 숫자 입력 input disabled 처리
let pageNumberInput = mainContainer.querySelector('#toolbarContainer #pageNumber');
pageNumberInput.disabled = true;
pageNumberInput.style.background = '#ddd';
pageNumberInput.style.color = '#999';
// 다음 페이지 버튼 클릭 시 10페이지 넘어가면 10 페이지로 고정
let nextBtn = mainContainer.querySelector('#toolbarContainer #next.toolbarButton');
nextBtn.addEventListener('click', function() {
if (pageNumberInput.value > 10) {
pageNumberInput.value = 10;
app.page = 10;
}
})
// 10페이지 높이값 설정
// -> 한 페이지 높이에 10을 곱한 값에 iframe 높이를 빼야 스크롤이 제일 아래로 내려갔을 때 마지막 페이지 끝에 맞음
let pageHeightLimit = (pages[0].offsetHeight * 10) - iframe.offsetHeight;
// pdf뷰어 실행 시 항상 스크롤 처음으로 고정
// viewerContainer.scrollTop = 0;
viewerContainer.addEventListener('scroll', function() {
if (viewerContainer.scrollTop >= pageHeightLimit) {
// 스크롤이 10페이지 높이값에 도달하면 스크롤위치를 10페이지 끝(높이값)으로 고정
viewerContainer.scrollTop = pageHeightLimit;
// 스크롤이 10페이지 높이값에 도달하면 11페이지부터 숨김처리
for (let i = 0; i < pages.length; i++) {
let page = pages[i];
if (i >= 10) {
if (page.querySelector('.canvasWrapper')) page.querySelector('.canvasWrapper').remove(); // 또는 숨김
page.style.backgroundColor = '#383838';
}
}
}
});
// 가상 스크롤바 생성
// 마우스 다운 상태에서 가상 스크롤바 영역 벗어났을 때 이벤트 처리 필요
await setupFakeScrollbar(iframe, 10);
// 이벤트 리스너 제거 (중복방지)
app.eventBus.off('pagesloaded', onPagesLoaded);
});
})
vars.viewer.appendChild(iframe);
vars.viewer.dataset.viewerType = 'pdf';
// 미리보기 10페이지 표시 제한
function setupFakeScrollbar(iframe, visiblePages) {
const doc = iframe.contentWindow.document;
const container = doc.querySelector('#viewerContainer');
if (!container) return;
// fakeScrollbar div 생성
let fakeScrollbar = document.createElement('div');
fakeScrollbar.className = 'fake-scrollbar';
// thumb 생성
let fakeThumb = document.createElement('div');
fakeThumb.className = 'fake-thumb';
fakeScrollbar.appendChild(fakeThumb);
// iframe 위에 fakeScrollbar 추가
iframe.parentNode.appendChild(fakeScrollbar);
// 10페이지 높이 합산
let pageDivs = doc.querySelectorAll('.page');
let totalHeight = 0;
for (let i = 0; i < visiblePages && i < pageDivs.length; i++) {
totalHeight += pageDivs[i].offsetHeight;
}
// 실제 컨테이너 전체 높이
let contentHeight = doc.querySelector('#viewer').offsetHeight;
let viewportHeight = container.clientHeight;
// fakeThumb의 높이와 이동가능 범위 계산
let thumbHeight = Math.max(
(viewportHeight / totalHeight) * viewportHeight,
40
);
fakeThumb.style.height = thumbHeight + 'px';
// fakeThumb 위치 업데이트 함수
function updateFakeThumb() {
let maxScroll = totalHeight - viewportHeight;
let scroll = Math.min(container.scrollTop, maxScroll);
let top = (scroll / maxScroll) * (viewportHeight - thumbHeight);
fakeThumb.style.top = (isNaN(top) ? 0 : top) + 'px';
}
// 스크롤 이벤트 연결
container.addEventListener('scroll', (e) => {
updateFakeThumb();
});
// container.addEventListener('wheel', (e) => {
// console.log(e);
// updateFakeThumb();
// });
// fakeThumb 드래그로 실제 스크롤 조정
let dragging = false, dragStartY = 0, startScroll = 0;
fakeThumb.addEventListener('mousedown', (e) => {
dragging = true;
dragStartY = e.clientY;
startScroll = container.scrollTop;
document.body.style.userSelect = 'none';
});
window.addEventListener('mousemove', (e) => {
if (!dragging) return;
let delta = e.clientY - dragStartY;
let maxScroll = totalHeight - viewportHeight;
let trackLen = viewportHeight - thumbHeight;
let scroll = startScroll + (delta / trackLen) * maxScroll;
scroll = Math.max(0, Math.min(maxScroll, scroll));
container.scrollTop = scroll;
});
window.addEventListener('mouseup', () => {
dragging = false;
document.body.style.userSelect = '';
});
// fakeThumb와 컨테이너 위치 초기 동기화
updateFakeThumb();
fakeScrollbar.style.pointerEvents = 'auto';
}
}
function viewerImage(presignedUrl) {
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);
vars.viewer.appendChild(viewerImageWrap);
vars.viewer.dataset.viewerType = 'image';
}
function viewerVideo(presignedUrl) {
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);
vars.viewer.appendChild(viewerVideoWrap);
vars.viewer.dataset.viewerType = 'video';
}
function viewerText(presignedUrl, ext) {
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');
if(ext === 'md'){
viewerText.style.whiteSpace = 'normal';
viewerText.classList.add('markdown-body');
// mermaid 시퀀스 다이어그램 변환
const renderer = new marked.Renderer();
const originalCode = renderer.code.bind(renderer);
// 머메이드만 변환하고 나머지는 기존 렌더러 사용
renderer.code = (code) => {
if(code.lang === 'mermaid') return `<pre class="mermaid">${code.text}</pre>`;
return originalCode(code);
}
//md파일 파싱 후 삽입
marked.setOptions({ gfm: true, breaks: true, renderer});
viewerText.innerHTML = marked.parse(data);
// mermaid 초기화 및 생성
const mermaidEls = viewerText.querySelectorAll('.mermaid')
if(mermaidEls.length > 0){
mermaid.initialize({ startOnLoad: false });
mermaid.init(undefined, mermaidEls);
}
} else {
viewerText.textContent = data;
}
viewerTextWrap.appendChild(viewerText);
vars.viewer.appendChild(viewerTextWrap);
if(ext === 'md')hljs.highlightAll();
vars.viewer.dataset.viewerType = 'text';
})
}
function viewerGsim(presignedUrl) {
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);
vars.viewer.appendChild(viewerGsim);
vars.viewer.dataset.viewerType = 'gsim';
}
function viewerIfc(presignedUrl, thumbnail_key, resourcePath, dataId, path_name) {
let viewerIfc = document.createElement('div');
viewerIfc.classList.add('viewer-ifc');
let iframe = document.createElement('iframe');
iframe.onload = () => {
iframe.contentWindow.postMessage({ path: presignedUrl, thumbnail_key, resourcePath, dataId, path_name }, '*'); // presignedUrl을 iframe에 전달
};
iframe.src = `/libs/ifcViewer/index.html`;
iframe.width = '100%';
iframe.height = '100%';
iframe.style.border = 'none';
viewerIfc.appendChild(iframe);
vars.viewer.appendChild(viewerIfc);
vars.viewer.dataset.viewerType = 'ifc';
}
function viewer3d(presignedUrl, thumbnail_key, resourcePath, dataId, path_name) {
let viewer3d = document.createElement('div');
viewer3d.classList.add('viewer-3d');
let iframe = document.createElement('iframe');
// 데이터(payload)를 보냄
iframe.onload = () => {
iframe.contentWindow.postMessage({ path: presignedUrl, thumbnail_key, resourcePath, dataId, path_name}, '*');
};
iframe.src = `/libs/3dViewer/index.html`;
iframe.width = '100%';
iframe.height = '100%';
iframe.style.border = 'none';
viewer3d.appendChild(iframe);
vars.viewer.appendChild(viewer3d);
vars.viewer.dataset.viewerType = '3d';
}
function viewerURL(presignedUrl) {
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);
vars.viewer.appendChild(viewerURLWrap);
vars.viewer.dataset.viewerType = 'url';
})
}
function viewerHTML(presignedUrl){
let viewerHTMLWrap = document.createElement('div');
viewerHTMLWrap.classList.add('viewer-text-wrap');
fetch(presignedUrl).then(res => res.text()).then(data => {
let iframe = document.createElement('iframe');
iframe.srcdoc = data;
iframe.style.width = '100%'; // 컨테이너에 맞게 너비 설정
iframe.style.height = '100%'; // 컨테이너에 맞게 높이 설정
iframe.style.border = 'none'; // 테두리 제거 (선택 사항)
viewerHTMLWrap.appendChild(iframe);
vars.viewer.appendChild(viewerHTMLWrap);
vars.viewer.dataset.viewerType = 'url';
})
}
function viewerZIP(presignedUrl) {
let viewerTextWrap = document.createElement('div');
viewerTextWrap.classList.add('viewer-text-wrap');
viewerTextWrap.classList.add('scrollbar');
let selectItem = vars.lastListItem.dataset.resourcePath;
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`
}
}
});
if(!vars.lastListItem.dataset.resourcePath || vars.lastListItem.dataset.resourcePath != selectItem) return;
let viewerText = document.createElement('div');
viewerText.classList.add('viewer-text');
viewerText.textContent = `${folderText}${(folderText == ``)?'':'\n'}${fileText}`;
viewerTextWrap.appendChild(viewerText);
vars.viewer.appendChild(viewerTextWrap);
vars.viewer.dataset.viewerType = 'text';
})
}
// 뷰 렌더 후, 서버 상태를 한번 동기화
await syncAiStateRestore(resourcePath, dataId);
async function syncAiStateRestore(resourcePath, dataId) {
if (!dataId) return;
try {
const res = await axios.get(`${vars.path_name}/summarizeState`, {
params: { resourcePath, dataId }
});
if (res.data.message === 'summarizeState_success') {
const dataInfo = res.data.dataInfo;
if (dataInfo) {
updateAiButtonState(dataId, 'loading');
} else {
updateAiButtonState(dataId, 'initial');
}
}
} catch(e) {
console.warn(`서버 상태 동기화 실패 (dataId: ${dataId}):`, e);
updateAiButtonState(dataId, 'initial');
}
}
}
export function resetViewer() {
viewerContainer.style.display = 'none';
viewerContainer.dataset.resourcePath = '';
viewerContainer.dataset.dataId = '';
viewerNotice.style.display = 'flex';
if (!vars.viewer) return;
if (vars.viewer.dataset.viewerType == 'pdf') {
const iframe = vars.viewer.querySelector('.viewer iframe');
if (iframe) {
const app = iframe.contentWindow?.PDFViewerApplication;
app?.close?.();
iframe.remove();
}
}
if (vars.viewer.dataset.viewerType == 'image') {
const imageWrap = vars.viewer.querySelector('.viewer-image-wrap');
if (imageWrap) {
const image = imageWrap.querySelector('.viewer-image');
image.src = '';
image.remove();
imageWrap.remove();
}
}
if (vars.viewer.dataset.viewerType == 'video') {
const videoWrap = vars.viewer.querySelector('.viewer-video-wrap');
if (videoWrap) {
const video = vars.viewer.querySelector('.viewer-video');
video.pause();
video.src = '';
video.load();
video.remove();
videoWrap.remove();
}
}
if (vars.viewer.dataset.viewerType == 'text') {
const textWrap = vars.viewer.querySelector('.viewer-text-wrap');
if (textWrap) {
textWrap.remove();
}
}
if (vars.viewer.dataset.viewerType == '3d') {
const viewer3d = vars.viewer.querySelector('.viewer-3d');
if (viewer3d) {
viewer3d.remove();
}
}
if (vars.viewer.dataset.viewerType == 'excel') {
if (window.luckysheet) {
try {
window.luckysheet.destroy();
} catch (e) {
console.error("Luckysheet destroy error: ", e);
}
}
}
vars.viewer.dataset.viewerType = '';
}
export function renderContextmenu(event, scope) {
/*
depth1 폴더 우클릭 메뉴 트리 우클릭 메뉴 리스트 우클릭 메뉴
- 새 폴더 - 파일 업로드
- 이름 변경 - 이름 변경 - 이름 변경
- 다운로드 - 다운로드 - 다운로드
- 이동
- 삭제 - 삭제 - 삭제
- 권한설정(?) - 권한설정 - 담당자 변경
* 트리 이동 : depth3 폴더는 depth2 폴더 선택해서 이동, depth2 폴더는 depth1 폴더 선택해서 이동
* 리스트 이동 : depth3 폴더 선택해서 이동
* 다른 depth1 폴더 내의 폴더로도 이동하도록 해야될까?
* depth1 폴더 폴더 업로드 : depth1 - depth2 - depth3 - files 순서로 구성된 폴더 업로드 하도록 가이드 표시
가이드 지켜지지 않는 경우 depth1, depth2에 포함된 파일 및 depth3에 포함된 폴더 무시
* 트리 폴더 업로드 (depth2) : depth2 - depth3 - files 순서로 구성된 폴더 업로드 하도록 가이드 표시
가이드 지켜지지 않는 경우 depth2에 포함된 파일 및 depth3에 포함된 폴더 무시
* 트리 폴더 업로드 (depth3) : depth3 - files 순서로 구성된 폴더 업로드 하도록 가이드 표시
가이드 지켜지지 않는 경우 depth3에 포함된 폴더 무시
*/
// addContextmenuItem('createFolder');
// addContextmenuItem('uploadFolder', true);
// addContextmenuItem('uploadFile', true);
// addContextmenuItem('renameTarget');
// addContextmenuItem('downloadTarget');
// addContextmenuItem('deleteTarget');
// addContextmenuItem('relocateTarget');
// addContextmenuItem('setPermission');
// addContextmenuItem('editAuthor');
// addContextmenuItem('lev_1');
// addContextmenuItem('lev_2');
// addContextmenuItem('lev_4');
// addContextmenuItem('lev_8');
// addContextmenuItem('lev_0');
let contextmenu = document.querySelector('.contextmenu.main-menu');
contextmenu.style.display = "none";
let contextmenuContainer = contextmenu.querySelector('.contextmenu-container');
contextmenuContainer.innerHTML = '';
let userInfo = JSON.parse(vars.userInfoString);
let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex';
// ** 권한 관련
if (!vars.permission.checkPermission('context-menu-viewer')) {
addContextmenuWarn('viewer');
} else {
if (scope == 'headerBtn') {
// ** 권한 관련
if (!vars.permission.checkPermission('context-menu-sub-master')) {
addContextmenuWarn('min-sub-master');
} else {
// addContextmenuItem('createFolder');
addContextmenuItem('renameTarget');
// addContextmenuItem('deleteTarget');
addContextmenuItem('removeTarget');
addContextmenuItem('setPermission');
if(vars.permission.checkPermission('download-folder')) addContextmenuItem('downloadTarget-folder');
}
}
if (scope == 'tree') {
addContextmenuItem('createFolder');
}
if (scope == 'tree-item-wrap-depth2') {
addContextmenuItem('createFolder');
// addContextmenuItem('createFolder-drawing');
// addContextmenuItem('createFolder-gallery');
addContextmenuItem('renameTarget');
// addContextmenuItem('deleteTarget');
addContextmenuItem('removeTarget');
// ** 권한 관련
if (vars.permission.checkPermission('context-menu-sub-master')) addContextmenuItem('setPermission');
if(vars.permission.checkPermission('download-folder')) addContextmenuItem('downloadTarget-folder');
}
if (scope == 'tree-item-wrap-depth3') {
addContextmenuItem('renameTarget');
// addContextmenuItem('deleteTarget');
addContextmenuItem('removeTarget');
// ** 권한 관련
if (vars.permission.checkPermission('context-menu-sub-master')) addContextmenuItem('setPermission');
if(vars.permission.checkPermission('download-folder')) addContextmenuItem('downloadTarget-folder');
}
if (scope == 'list') {
// 갤러리 폴더 지도모드일 때 안내 표시 및 리턴
let mapContainer = listContainer?.querySelector('.map-container');
if (mapContainer.style.display == 'flex') {
let notificatitonParams = { text: '지도모드에서는 우클릭 기능이 제한됩니다.' }
showNotification(notificatitonParams);
return;
}
addContextmenuItem('uploadFile', true, event);
}
if (scope == 'list-item') {
// 파일 이동 모드인 경우 리턴
if (document.querySelector('.list-viewer-cover').style.display == 'flex') return;
if (isRecycleBinModal) {
addContextmenuItem('downloadTarget');
addContextmenuItem('deleteTarget');
} else {
let listItem = event.target.closest('.list-item');
let state = listItem.getElementsByClassName('state')[0];
if (state && state.classList.contains('working')) {
// 우클릭한 파일이 작업중인 경우 경고 표시
addContextmenuItem('uploadFile', true);
addContextmenuWarn('working');
} else {
let existsWorkingListItem;
Array.from(listItem.getElementsByClassName('state')).forEach(state => {
if (state.classList.contains('working')) existsWorkingListItem = true;
})
if (existsWorkingListItem) {
// 우클릭한 파일의 추가 파일중에 작업중인 파일이 있는 경우 경고 표시
addContextmenuItem('uploadFile', true);
addContextmenuWarn('includesWorking');
} else {
addContextmenuItem('uploadFile', true);
addContextmenuItem('addOn_version', true);
addContextmenuItem('addOn_attachment', true);
if (listItem.classList.contains('dwg') || listItem.classList.contains('dxf')) {
//// 도면 ctb xref 관련 코드
// addContextmenuItem('addOn_ctb', true);
// addContextmenuItem('addOn_xref', true);
}
addContextmenuItem('renameTarget');
addContextmenuItem('editAuthor');
addContextmenuItem('downloadTarget');
addContextmenuItem('relocateTarget'); // 파일이동
addContextmenuItem('removeTarget');
}
}
}
if (!isRecycleBinModal && userInfo.group == 'dev') {
addContextmenuItem('mgmtFunc_resetConvert');
}
// 다중삭제, 다중이동 시 컨텍스트메뉴 추가
}
if (scope == 'grid-item') {
let gridItem = event.target.closest('.grid-item');
// 파일 이동 모드인 경우 리턴
if (document.querySelector('.list-viewer-cover').style.display == 'flex') return;
addContextmenuItem('uploadFile', true, event);
if (gridItem.classList.contains('dwg') || gridItem.classList.contains('dxf')) {
//// 도면 ctb xref 관련 코드
// addContextmenuItem('addOn_ctb', true);
// addContextmenuItem('addOn_xref', true);
}
addContextmenuItem('renameTarget');
addContextmenuItem('editAuthor');
addContextmenuItem('downloadTarget');
addContextmenuItem('relocateTarget'); // 파일이동
addContextmenuItem('removeTarget');
}
if (scope == 'map-item') {
// 갤러리 폴더 지도모드일 때 안내 표시 및 리턴
let mapContainer = listContainer?.querySelector('.map-container');
if (mapContainer.style.display == 'flex') {
let notificatitonParams = { text: '지도모드에서는 우클릭 기능이 제한됩니다.' }
showNotification(notificatitonParams);
return;
}
// addContextmenuItem('uploadFile', true, event);
// addContextmenuItem('renameTarget');
// addContextmenuItem('downloadTarget');
// addContextmenuItem('relocateTarget'); // 파일이동
// addContextmenuItem('removeTarget');
}
if (scope == 'sub-list-item') {
let listItem = event.target.closest('.list-item');
// 추가 파일에 disabled 클래스가 포함되어 있으면 리턴 (파일 이동 모드)
if (listItem.classList.contains('disabled')) return;
let state = listItem.getElementsByClassName('state')[0];
if (state && state.classList.contains('working')) {
// 우클릭한 추가 파일이 작업중인 경우 경고 표시
addContextmenuWarn('working');
} else {
addContextmenuItem('renameTarget');
addContextmenuItem('editAuthor');
addContextmenuItem('downloadTarget');
addContextmenuItem('relocateTarget');
addContextmenuItem('removeTarget');
// addContextmenuItem('deleteTarget');
}
if (userInfo.group == 'dev') addContextmenuItem('mgmtFunc_resetConvert');
// 다중삭제, 다중이동 시 컨텍스트메뉴 추가
}
if (scope == 'multi-select-list') {
if (isRecycleBinModal) {
addContextmenuItem('downloadTarget');
addContextmenuItem('deleteTarget');
} else {
let existsWorkingListItem;
let existsMainListItem;
let extArr = [];
vars.multiSelectListItemArr.map(listItem => {
if (listItem.classList.contains('main-list-item')) {
existsMainListItem = true;
Array.from(listItem.getElementsByClassName('state')).forEach(state => {
if (state.classList.contains('working')) existsWorkingListItem = true;
})
let resourcePath = listItem.dataset.resourcePath;
let isLowerExt = true;
let ext = splitBaseAndExt(resourcePath, isLowerExt).ext;
extArr.push(ext);
} else {
let state = listItem.getElementsByClassName('state')[0];
if (state && state.classList.contains('working')) existsWorkingListItem = true;
}
})
// 확장자 배열 내에서 중복요소 제거
extArr = extArr.filter((element, index) => {
return extArr.indexOf(element) === index;
});
if (existsWorkingListItem) {
// vars.multiSelectListItemArr에 포함된 파일 혹은 파일의 추가 파일중에 작업중인 파일이 있는 경우 경고 표시
addContextmenuWarn('includesWorking');
} else {
// 도면 확장자
let drawingExtArr = ['dwg', 'dxf'];
// 확장자 배열에 도면 확장자들만 들어있는지 확인
let isDrawingExtMatch = (extArr, drawingExtArr) => {
let extArrSet = new Set(extArr);
// 최소 1개 이상 있어야 함
if (extArrSet.size === 0) return false;
// extArrSet이 drawingExtArr의 부분집합인지 확인
for (let ext of extArrSet) {
if (!drawingExtArr.includes(ext)) return false;
}
return true;
};
// 우클릭 한 대상이 main-list-item이고, 확장자 배열에 도면 확장자들만 들어있으면 'CTB파일 일괄 추가', 'XREF파일 일괄 추가' 메뉴 추가
let listItem = event.target.closest('.list-item');
if (listItem.classList.contains('main-list-item') && isDrawingExtMatch(extArr, drawingExtArr)) {
//// 도면 ctb xref 관련 코드
// addContextmenuItem('addOn_ctb_bulk', true);
// addContextmenuItem('addOn_xref_bulk', true);
}
addContextmenuItem('editAuthor');
addContextmenuItem('downloadTarget');
addContextmenuItem('removeTarget');
addContextmenuItem('relocateTarget');
}
// addContextmenuItem('deleteTarget');
}
}
}
contextmenu.style.display = "flex";
contextmenu.style.left = event.clientX + 5 + "px";
contextmenu.style.top = event.clientY + 10 + "px";
// 편의성 - contextmenu가 화면 영역 아래/옆으로 들어가지 않도록 위치 조정
if (contextmenu.offsetHeight + event.clientY > window.innerHeight) {
contextmenu.style.top = (event.clientY - contextmenu.offsetHeight) + "px";
}
if (contextmenu.offsetWidth + event.clientX > window.innerWidth) {
contextmenu.style.left = (event.clientX - contextmenu.offsetWidth) + "px";
}
function addContextmenuWarn(target) {
// target : min-sub-master (부관리자 미만) / viewer (참관자)
let html;
if (target == 'min-sub-master') html = `상단 메뉴의 생성/편집/삭제는 부관리자 권한 이상에서만 가능합니다.`;
if (target == 'viewer') html = `참관자 권한은 열람만 가능합니다.`;
if (target == 'working') html = `변환중인 파일은 편집/삭제가 불가능합니다.`;
if (target == 'includesWorking') html = `선택한 파일 또는 관련된 버전/첨부 파일 중 변환중인 항목이 있어 편집/삭제가 불가능합니다.`;
let contextmenuWarn = document.createElement('div');
contextmenuWarn.classList.add('contextmenu-warn');
contextmenuWarn.classList.add('contextmenu-padding');
contextmenuContainer.appendChild(contextmenuWarn);
let wrap = document.createElement('div');
wrap.classList.add('wrap');
let text = document.createElement('div');
text.classList.add('text');
text.innerHTML = html;
wrap.appendChild(text);
contextmenuWarn.appendChild(wrap);
}
function addContextmenuItem(id, useInputFile, event) {
let menuData = getMenuData(id);
let contextmenuItem = document.createElement('div');
contextmenuItem.id = id;
contextmenuItem.classList.add('contextmenu-item');
let img = document.createElement('img');
let imgFileName = id.replace('_bulk', '');
img.src = `/main/img/archive/contextmenu/${imgFileName}.svg`;
img.alt = `${imgFileName}_icon`;
let wrap = document.createElement('div');
wrap.classList.add('wrap');
let name = document.createElement('div');
name.classList.add('name');
name.innerText = menuData.menuNameKor;
wrap.appendChild(name);
// let shortcut;
// if (menuData.shortcut) {
// shortcut = document.createElement('div');
// shortcut.classList.add('shortcut');
// shortcut.innerText = menuData.shortcut;
// wrap.appendChild(shortcut);
// }
if (id == 'removeTarget' && vars.lastContextTarget.matches('.folder')) {
img.src = `/main/img/archive/contextmenu/deleteTarget.svg`;
img.alt = `deleteTarget_icon`;
name.innerText = '삭제';
}
if (useInputFile) {
let isGalleryFolder = false, isDrawingFolder = false;
if (vars.lastFolderType == 'gallery') isGalleryFolder = true;
if (vars.lastFolderType == 'drawing') isDrawingFolder = true;
let form = document.createElement('form');
form.classList.add('btn');
form.enctype = 'multipart/form-data';
let input = document.createElement('input');
input.type = 'file';
input.id = `${id}Input`;
if (id.includes('File') || id.includes('addOn')) {
input.setAttribute('multiple', '');
input.setAttribute('onclick', 'this.value=null;');
}
if (id.includes('Folder')) {
input.setAttribute('webkitdirectory', '');
input.setAttribute('directory', '');
input.setAttribute('onclick', 'this.value=null;');
}
let label = document.createElement('label');
label.id = `${id}Label`;
label.classList.add('contextmenu-padding');
label.setAttribute('for', `${id}Input`);
label.appendChild(img);
label.appendChild(wrap);
form.appendChild(input);
form.appendChild(label);
contextmenuItem.appendChild(form);
} else {
contextmenuItem.classList.add('contextmenu-padding');
contextmenuItem.appendChild(img);
contextmenuItem.appendChild(wrap);
if (id == 'createFolder') {
// 새 폴더 메뉴에 마우스오버 시 폴더 만들어지는 경로 툴팁 표시?
contextmenuItem.addEventListener('mouseover', (e) => {
// console.log(e.target);
// console.log('새 폴더');
})
}
if (id == 'relocateTarget') {
}
if (id == 'setPermission') {
contextmenuItem.classList.add('mouseover');
let toggle = document.createElement('div');
toggle.classList.add('image');
toggle.classList.add('toggle');
wrap.appendChild(toggle);
let subContextmenu = document.createElement('div');
subContextmenu.classList.add('contextmenu');
subContextmenu.classList.add('sub-menu');
let subContextmenuContainer = document.createElement('div');
subContextmenuContainer.classList.add('contextmenu-container');
let resourcePath = vars.lastContextTarget.dataset.resourcePath;
let data = getDataFromTreeObject(resourcePath, 'folder').data;
let dataPermission = data.permission;
let lev1 = getSubContextmenuItem('lev_1', dataPermission);
// let lev2 = getSubContextmenuItem('lev_2', dataPermission);
let lev4 = getSubContextmenuItem('lev_4', dataPermission);
let lev8 = getSubContextmenuItem('lev_8', dataPermission);
let lev0 = getSubContextmenuItem('lev_0', dataPermission);
subContextmenuContainer.appendChild(lev1);
// subContextmenuContainer.appendChild(lev2);
subContextmenuContainer.appendChild(lev4);
subContextmenuContainer.appendChild(lev8);
subContextmenuContainer.appendChild(lev0);
subContextmenu.appendChild(subContextmenuContainer);
contextmenuItem.appendChild(subContextmenu);
// 권한 설정 메뉴 마우스오버 시 하위 메뉴 표시
document.addEventListener('mousemove', throttle((e) => {
if (document.querySelector('.contextmenu.main-menu').style.display == 'flex') {
let subMenu = document.querySelector('.contextmenu.sub-menu');
if (!subMenu) return;
if (e.target.id == 'setPermission' || subMenu.contains(e.target)) {
subMenu.style.display = 'flex';
let parentHeight = subMenu.parentElement.offsetHeight;
//편의성 - subMenu가 화면 영역 아래로 들어가지 않도록 위치 조정
if (subMenu.offsetHeight + e.clientY > window.innerHeight) {
subMenu.style.top = `-${subMenu.offsetHeight - parentHeight - 4}px`;
}
} else {
subMenu.style.display = 'none';
}
}
}, 100))
}
}
contextmenuContainer.appendChild(contextmenuItem);
contextmenuItem.addEventListener('click', (e) => {
let targetId = e.target.id;
let resourcePath, dataId;
let lastContextTarget;
let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex';
if (isRecycleBinModal) {
lastContextTarget = vars.lastContextTarget_bin;
} else {
lastContextTarget = vars.lastContextTarget;
}
if (lastContextTarget) resourcePath = lastContextTarget.dataset.resourcePath;
if (lastContextTarget) dataId = lastContextTarget.dataset.id;
if (targetId == 'createFolder') openCreateFolderModal(resourcePath);
if (targetId == 'renameTarget') openRenameModal(resourcePath);
// if (targetId == 'downloadTarget') downloadTarget();
if (targetId == 'downloadTarget') openDownloadModal(resourcePath);
if (targetId == 'downloadTarget-folder') openDownloadModal(resourcePath);
if (targetId == 'relocateTarget') toggleRelocateCover(true, resourcePath);
if (targetId == 'removeTarget') openRemoveModal(resourcePath);
if (targetId == 'deleteTarget') openDeleteModal(resourcePath);
// if (targetId == 'setPermission')
if (targetId == 'editAuthor') openEditAuthorModal(resourcePath);
if (targetId == 'lev_1') setDataPermission(resourcePath, targetId);
// if (targetId == 'lev_2')
if (targetId == 'lev_4') setDataPermission(resourcePath, targetId);
if (targetId == 'lev_8') setDataPermission(resourcePath, targetId);
if (targetId == 'lev_0') setDataPermission(resourcePath, targetId);
if (targetId == 'mgmtFunc_resetConvert') mgmtFunc_resetConvert(resourcePath, dataId);
})
contextmenuItem.addEventListener('change', (e) => {
let uploadDataOption = {
functionId: id,
dataType: 'file',
}
if (id == 'uploadFile') {
// console.log(111);
let files = e.target.files;
uploadData(files, uploadDataOption);
// 컨텍스트메뉴 초기화
contextmenu.querySelector('.contextmenu-container').innerHTML = '';
}
if (id == 'addOn_version' || id == 'addOn_attachment') {
// console.log(222);
let files = e.target.files;
let uploadDataOption = {
functionId: id,
dataType: 'file',
addOnTarget: vars.lastContextTarget.dataset.resourcePath
}
uploadData(files, uploadDataOption);
// 컨텍스트메뉴 초기화
contextmenu.querySelector('.contextmenu-container').innerHTML = '';
}
//// 도면 ctb xref 관련 코드
if (id.includes('addOn_ctb') || id.includes('addOn_xref')) {
// console.log(333);
// vars.multiSelectListItemArr 으로 배열 돌려서 아래 uploadData를 여러번 실행?
// let files = e.target.files
// let uploadDataOption = {
// functionId: id,
// dataType: 'file',
// addOnTarget: vars.lastContextTarget.dataset.resourcePath
// }
// uploadData(files, uploadDataOption);
// // 컨텍스트메뉴 초기화
// contextmenu.querySelector('.contextmenu-container').innerHTML = '';
}
})
}
function getSubContextmenuItem(id, dataPermission) {
let menuData = getMenuData(id);
let subContextmenuItem = document.createElement('div');
subContextmenuItem.id = id;
subContextmenuItem.classList.add('contextmenu-item');
subContextmenuItem.classList.add('contextmenu-padding');
subContextmenuItem.classList.add('set-permission-item');
// ** 권한 관련 (클래스에는 포함 안될 예정)
// id : lev_1, lev_2, lev_4, lev_8, lev_0
if (id.split('_')[1] == dataPermission) subContextmenuItem.classList.add('check');
let img = document.createElement('img');
img.src = `/main/img/permission/${menuData.menuNameEng}.svg`;
img.alt = `${id}_icon`;
let wrap = document.createElement('div');
wrap.classList.add('wrap');
let name = document.createElement('div');
name.classList.add('name');
name.innerText = menuData.menuNameKor;
let image = document.createElement('div');
image.classList.add('image');
wrap.appendChild(name);
wrap.appendChild(image);
subContextmenuItem.appendChild(img);
subContextmenuItem.appendChild(wrap);
return subContextmenuItem;
}
function getMenuData(id) {
let result = { menuNameKor: undefined, shortcut: undefined };
if (id == 'createFolder') result.menuNameKor = '새 폴더', result.shortcut = 'Ctrl ';
if (id == 'uploadFolder') result.menuNameKor = '폴더 업로드', result.shortcut = 'Ctrl ';
if (id == 'uploadFile') result.menuNameKor = '파일 업로드', result.shortcut = 'Ctrl ';
// if (id == 'uploadFile') result.menuNameKor = '현재 폴더에 파일 업로드', result.shortcut = 'Ctrl ';
if (id == 'addOn_version') result.menuNameKor = '버전파일 추가', result.shortcut = 'Ctrl ';
if (id == 'addOn_attachment') result.menuNameKor = '첨부파일 추가', result.shortcut = 'Ctrl ';
if (id == 'addOn_ctb') result.menuNameKor = 'CTB파일 추가', result.shortcut = 'Ctrl ';
if (id == 'addOn_xref') result.menuNameKor = 'XREF파일 추가', result.shortcut = 'Ctrl ';
if (id == 'addOn_ctb_bulk') result.menuNameKor = 'CTB파일 일괄 추가', result.shortcut = 'Ctrl ';
if (id == 'addOn_xref_bulk') result.menuNameKor = 'XREF파일 일괄 추가', result.shortcut = 'Ctrl ';
if (id == 'renameTarget') result.menuNameKor = '이름 변경', result.shortcut = 'F2';
if (id == 'relocateTarget') result.menuNameKor = '위치 이동', result.shortcut = 'Ctrl ';
if (id == 'downloadTarget') result.menuNameKor = '다운로드', result.shortcut = 'Ctrl ';
if (id == 'downloadTarget-folder') result.menuNameKor = '폴더다운로드(관리자)', result.shortcut = 'Ctrl ';
if (id == 'removeTarget') result.menuNameKor = '휴지통으로 이동', result.shortcut = 'Del';
if (id == 'deleteTarget') result.menuNameKor = '삭제', result.shortcut = 'Del';
if (id == 'editAuthor') result.menuNameKor = '작성자 변경', result.shortcut = 'Ctrl ';
if (id == 'setPermission') result.menuNameKor = '권한 설정';
if (id == 'lev_1') result.menuNameKor = '참관자', result.menuNameEng = 'viewer';
// if (id == 'lev_2') result.menuNameKor = '업로더';
if (id == 'lev_4') result.menuNameKor = '일반참여자', result.menuNameEng = 'worker';
if (id == 'lev_8') result.menuNameKor = '보안참여자', result.menuNameEng = 'security-worker';
if (id == 'lev_0') result.menuNameKor = '부관리자', result.menuNameEng = 'sub-master';
if (id == 'mgmtFunc_resetConvert') result.menuNameKor = '(개발자) 변환 초기화';
return result;
}
}
export async function renderSizeBar() {
if (checkProjectInactive()) return;
// return;
let getFolderSizeParams = { storageType: vars.storageType };
let getFolderSizeRes = await axios.get(`${vars.path_name}/getFolderSize`, { params: { params: getFolderSizeParams } });
if (getFolderSizeRes.data.message == 'getFolderSize_success') {
let result = getFolderSizeRes.data.result;
let totalSize = 0, totalCount = 0;
for(let i = 0; i < result.length; i++) {
let item = result[i];
let key = item.key;
let size = Number(item.size);
let count = Number(item.count);
item.text = getSizeText(key);
totalSize += size, totalCount += count;
}
vars.bucketSize = totalSize;
vars.remainingSize = vars.bucketMaxSize - vars.bucketSize;
vars.foldersSize = result;
//// 용량 / 최대용량, 용량 막대, 퍼센트 적용 버전
// let footerSizeWrap = document.querySelector('body > .footer .left .size-wrap');
// let sizeTextNumerator = footerSizeWrap.querySelector('.size-text .numerator');
// let sizeTextDenominator = footerSizeWrap.querySelector('.size-text .denominator');
// let sizeBarBase = footerSizeWrap.querySelector('.size-bar .base');
// let sizeBarValue = footerSizeWrap.querySelector('.size-bar .value');
// let sizePercent = footerSizeWrap.querySelector('.size-percent');
// // 사용중인 용량 / 최대 용량 텍스트
// sizeTextNumerator.innerHTML = formatBytes(vars.bucketSize);
// sizeTextDenominator.innerHTML = formatBytes(vars.bucketMaxSize);
// // 최대 용량 대비 사용중인 용량 비율
// let usageRatio = (vars.project) ? vars.bucketSize / vars.bucketMaxSize : 0;
// // 최대 용량 막대 길이
// let sizeBarBaseLength = sizeBarBase.offsetWidth;
// // 사용중인 용량 막대 길이
// let sizeBarValueLength = sizeBarBaseLength * usageRatio;
// if (sizeBarValueLength > sizeBarBaseLength) sizeBarValueLength = sizeBarBaseLength;
// sizeBarValue.style.width = `${sizeBarValueLength}px`;
// // 최대 용량 대비 사용중인 용량 퍼센트
// let usagePercent = usageRatio * 100;
// sizePercent.innerText = `${formatPercent(usagePercent)} %`;
// if (usagePercent > 90) {
// sizeTextNumerator.classList.add('over');
// sizeBarValue.classList.add('over');
// sizePercent.classList.add('over');
// } else {
// sizeTextNumerator.classList.remove('over');
// sizeBarValue.classList.remove('over');
// sizePercent.classList.remove('over');
// }
//// 퍼센트 적용 버전
let footerSizeWrap = document.querySelector('body > .footer .left .size-wrap');
let sizePercent = footerSizeWrap.querySelector('.size-percent');
// 최대 용량 대비 사용중인 용량 비율
let usageRatio = (vars.project) ? vars.bucketSize / vars.bucketMaxSize : 0;
// 최대 용량 대비 사용중인 용량 퍼센트
let usagePercent = usageRatio * 100;
sizePercent.innerText = `${formatPercent(usagePercent)} %`;
if (usagePercent > 90) {
sizePercent.classList.add('over');
} else {
sizePercent.classList.remove('over');
}
//// 모달 저장공간 파이그래프
let dom = document.querySelector('.archive-modal .modal-body .size-wrap .chart');
let chart = echarts.init(dom, null, {
renderer: 'canvas',
useDirtyRect: false
});
let chartColor = {
'여유공간': '#ccc',
'휴지통': '#777',
'아카이브 원본': '#03AEFC',
'아카이브 변환PDF': '#4DB251',
'아카이브 미리보기PDF': '#FF3D00',
'공문 원본': '#FFBF00',
'공문 변환PDF': '#B92ED1',
'과업개요': '#4255BD'
};
// rawData 배열을 생성하고, rowData를 다시 용도별 데이터 배열로 가공
let rawData = [
{
value: (vars.remainingSize < 0) ? 0 : vars.remainingSize, // 여유공간이 0보다 작으면 0으로 표시
name: `여유공간 (${formatBytes(vars.remainingSize)})`,
itemStyle: { color: chartColor['여유공간'] }
}
];
for (let i = 0; i < vars.foldersSize.length; i++) {
let item = vars.foldersSize[i];
let obj = {};
if (item.size == 0) continue;
obj.value = item.size;
obj.name = `${item.text} (${formatBytes(item.size)} / ${item.count} 개)`;
obj.itemStyle = { color: chartColor[item.text] };
rawData.push(obj);
}
// 파이그래프용 실제 데이터
let mainData = rawData.filter(d => d.value > 0);
// 파이그래프용 더미 데이터
let dummyData = rawData.filter(d => d.value <= 0).map(d => ({
...d,
name: `{over|${d.name}}`,
value: 1
}));
// 범례용 데이터 (여유공간이 0 이하인 경우 rich 옵션 사용을 위해 {style_name|} 추가)
let legendData = rawData.map(d => ({
name: (d.value <= 0) ? `{over|${d.name}}` : d.name,
icon: 'circle',
}));
// 타이틀용 텍스트 (여유공간이 0 이하인 경우 rich 옵션 사용을 위해 {style_name|} 추가)
// let titleText = (vars.remainingSize <= 0)
// ? `{normal|전체공간 (${formatBytes(vars.bucketMaxSize)})} │ {normal|사용공간 (${formatBytes(vars.bucketSize)})} │ {over|여유공간 (${formatBytes(vars.remainingSize)})}`
// : `전체공간 (${formatBytes(vars.bucketMaxSize)}) │ 사용공간 (${formatBytes(vars.bucketSize)}) │ 여유공간 (${formatBytes(vars.remainingSize)})`
let titleText = (usagePercent > 90)
? `{normal|전체공간} {bold|${formatBytes(vars.bucketMaxSize)}}\n{normal|사용공간} {bold|${formatBytes(vars.bucketSize)}}\n{normal_over|여유공간} {bold_over|${formatBytes(vars.remainingSize)} (${formatPercent(usagePercent)} %)}`
: `{normal|전체공간} {bold|${formatBytes(vars.bucketMaxSize)}}\n{normal|사용공간} {bold|${formatBytes(vars.bucketSize)}}\n{normal|여유공간} {bold|${formatBytes(vars.remainingSize)} (${formatPercent(usagePercent)} %)}`
let option;
let fontFamily = 'Pretendard Variable, Pretendard';
let fontSize = 12;
let fontWeight = 600;
option = {
title: {
text: titleText,
// left: 'center',
// top: 10,
left: 20,
top: 12,
textStyle: {
lineHeight: 30,
rich: {
// rich 옵션
normal: {
fontFamily: fontFamily,
fontSize: fontSize,
color: '#000000'
},
normal_over: {
fontFamily: fontFamily,
fontSize: fontSize,
color: '#ff0000'
},
bold: {
fontFamily: fontFamily,
fontSize: fontSize,
color: '#000000',
fontWeight: fontWeight
},
bold_over: {
fontFamily: fontFamily,
fontSize: fontSize,
color: '#ff0000',
fontWeight: fontWeight
},
}
},
},
textStyle: {
fontFamily: 'Pretendard Variable, Pretendard',
fontSize: 12,
},
tooltip: {
trigger: 'item',
formatter: function (params) {
return `${params.marker} ${params.name}`;
}
},
legend: {
orient: 'vertical',
left: 20,
bottom: 20,
// top: 'middle',
itemWidth: 16,
itemHeight: 16,
data: legendData,
textStyle: {
fontFamily: 'Pretendard Variable, Pretendard',
fontSize: 12,
rich: {
// rich 옵션
over: { color: '#ccc' }
}
}
},
series: [
{
// 실제 파이 그래프
type: 'pie',
radius: '60%',
center: ['65%', '50%'],
data: mainData,
label: {
formatter: function (params) {
return `${params.name.split(' (')[0]}`;
},
minMargin: 5,
position: 'outer',
alignTo: 'labelLine',
bleedMargin: 20,
},
labelLine: {
show: true,
length: 80,
length2: 0,
},
minAngle: 2, // 최소 각도 설정
itemStyle: {
borderRadius: 4,
borderColor: '#ffffff',
borderWidth: 1
},
emphasis: {
// 마우스 hover 강조 시 색상 유지
itemStyle: {
color: 'inherit',
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
},
{
// 더미 파이그래프 (파이그래프는 표시하지 않고 범례만 표시)
type: 'pie',
radius: ['0%', '0%'], // 보이지 않도록 반지름 0%
center: ['70%', '50%'],
label: { show: false }, // 파이그래프 주변 라벨 표시 안함
labelLine: { show: false }, // 파이그래프와 파이그래프 주변 라벨을 연결하는 선 표시 안함
data: dummyData.map(d => ({
...d,
value: 1 // 범례 표시를 위해 value 설정
})),
emphasis: {
disabled: true // 마우스 hover 강조 비활성화
}
}
]
};
if (option && typeof option === 'object') {
chart.setOption(option);
}
chart.resize();
function formatPercent(percent) {
let result = percent.toFixed(2);
let decimalPointNum = result.split('.')[1];
// 소수점 아래 숫자가 00인 경우
if (decimalPointNum == '00') result = result.split('.')[0];
// 소수점 아래 두번째 숫자가 0인 경우
else if (decimalPointNum[1] == '0') result = `${result.split('.')[0]}.${decimalPointNum[0]}`;
return result;
}
function getSizeText(key) {
let result;
let keySplit = key.split('/');
if (keySplit[1]) {
let prefix, suffix;
if (keySplit[0] == 'archive') prefix = '아카이브';
if (keySplit[0] == 'official_doc') prefix = '공문';
if (keySplit[1] == 'origin') suffix = '원본';
if (keySplit[1] == 'pdf') suffix = '변환PDF';
if (keySplit[1] == 'pdf_thumb') suffix = '미리보기PDF';
result = `${prefix} ${suffix}`;
} else {
if (keySplit[0] == 'recycle_bin') result = '휴지통';
if (keySplit[0] == 'overview') result = '과업개요';
}
return result;
}
}
}
export async function renderLog(isInit, addFooterLog, filterValues) {
if (checkProjectInactive()) return;
// addFooterLog : 푸터 로그 추가 여부
// 기본은 true, 활동 로그 모달 열 때 실행하는 renderLog에서는 false
if (addFooterLog == undefined) addFooterLog = true;
let footerLatestLog = document.querySelector('body > .footer .left .log-wrap .latest-log');
let modalLogItemWrap = document.querySelector('.archive-modal .modal-body .log-wrap .log-item-wrap.log-body');
// 로그 데이터 가져와서 전역 변수에 저장
if (filterValues != undefined) {
vars.logData = filterValues;
}else {
let logRes = await axios.get(`${vars.path_name}/getLog`);
vars.logData = logRes.data.logData;
}
modalLogItemWrap.innerHTML = '';
// 새로 로그 렌더하기 전에 마지막으로 추가된 log-item 애니메이션 초기화
vars.newLogItemArr.forEach(newLogItem => {
newLogItem.style.setProperty('--log-highlight', 'none');
})
vars.newLogItemArr = [];
Object.values(vars.logData).forEach((data, idx) => {
// 로그 id (시퀀스 번호)
let logId = data.log_id;
// let existingLogItem = document.querySelector(`.log-${logId}`);
// if (existingLogItem) {
// // 애니메이션 초기화
// // existingLogItem.style.setProperty('--log-highlight', 'none');
// } else {
// 로그 시간 (yy. MM. dd. hh:mm:ss)
let logDate = new Date(data.log_date);
logDate = `${changeDateFormat(logDate.toLocaleDateString('ko-KR'))} ${logDate.toLocaleTimeString('en-US', {hour12: false})}`;
// 영어로 된 활동 id를 한글로 번역
let activity = data.activity;
let activityKor = translateActivity(activity);
let activityType = activity.split('_')[0];
// data에서 유저 관련 변수들 저장 후 조합해서 유저 정보 변수 생성
// let userId = data.user_id;
// let userCompany = data.user_company;
// let userDept = data.user_dept;
let userNm = data.user_nm;
let userPosition = '';
if (data.user_position) userPosition = data.user_position;
// let user = `${userCompany} ${userDept} ${userNm} ${userPosition}`;
let user = (`${userNm} ${userPosition}`).trim();
if (activity == 'removeTarget_folder_expired') user = '-';
// 활동 별 log 및 id 생성해서 각각 배열에 추가
let pathArr;
try {
pathArr = typeof data.path_arr === 'string' ? JSON.parse(data.path_arr) : data.path_arr;
} catch (e) {
pathArr = Array.isArray(data.path_arr) ? data.path_arr : [data.path_arr];
}
let idArr = [], dateArr = [], userArr = [], activityArr = [], logArr = [];
if (activity == 'uploadData_file') {
// 파일 업로드 경우 다중 업로드가 가능하므로 업로드 된 파일마다 로그 생성
for (let i = 0; i < pathArr.length; i++) {
let id = `${logId}_${logDate}_${user}_${activityKor}_${pathArr[i]}`;
let log = document.createElement('div');
log.classList.add('log');
log.classList.add(activityType);
let logText = document.createElement('div');
logText.classList.add('text');
logText.innerHTML = `${pathArr[i]}`;
log.appendChild(logText);
idArr.push(id);
dateArr.push(logDate);
userArr.push(user);
activityArr.push(activityKor);
logArr.push(log);
}
} else if (activity.startsWith('renameTarget')) {
let id = `${logId}_${logDate}_${user}_${activityKor}_${pathArr[0]}`;
if (pathArr[1]) id += `_${pathArr[1]}`;
let log = document.createElement('div');
log.classList.add('log');
log.classList.add(activityType);
let logText1 = document.createElement('div');
logText1.classList.add('text');
logText1.innerHTML = `변경 전 : ${pathArr[0]}`;
let logText2 = document.createElement('div');
logText2.classList.add('text');
logText2.innerHTML = `변경 후 : ${pathArr[1]}`;
log.appendChild(logText1);
log.appendChild(logText2);
let width1 = getTextPixelWidth(`변경 전 : `);
logText1.style.textIndent = `-${pxToRem(width1-2)}rem`;
logText1.style.paddingLeft = `${pxToRem(width1-2)}rem`;
let width2 = getTextPixelWidth(`변경 후 : `);
logText2.style.textIndent = `-${pxToRem(width2-2)}rem`;
logText2.style.paddingLeft = `${pxToRem(width2-2)}rem`;
idArr.push(id);
dateArr.push(logDate);
userArr.push(user);
activityArr.push(activityKor);
logArr.push(log);
} else if (activity.startsWith('relocateTarget')) {
for (let i = 0; i < pathArr.length; i++) {
let id = `${logId}_${logDate}_${user}_${activityKor}_${pathArr[i].from}_${pathArr[i].to}`;
let log = document.createElement('div');
log.classList.add('log');
log.classList.add(activityType);
let logText1 = document.createElement('div');
logText1.classList.add('text');
logText1.innerHTML = `이동 전 : ${pathArr[i].from}`;
let logText2 = document.createElement('div');
logText2.classList.add('text');
logText2.innerHTML = `이동 후 : ${pathArr[i].to}`;
log.appendChild(logText1);
log.appendChild(logText2);
let width1 = getTextPixelWidth(`변경 전 : `);
logText1.style.textIndent = `-${pxToRem(width1-2)}rem`;
logText1.style.paddingLeft = `${pxToRem(width1-2)}rem`;
let width2 = getTextPixelWidth(`변경 후 : `);
logText2.style.textIndent = `-${pxToRem(width2-2)}rem`;
logText2.style.paddingLeft = `${pxToRem(width2-2)}rem`;
idArr.push(id);
dateArr.push(logDate);
userArr.push(user);
activityArr.push(activityKor);
logArr.push(log);
}
} else if (activity.startsWith('setDataPermission')) {
let path = pathArr[0].resourcePath;
let beforePermission = pathArr[0].beforePermission;
let newPermission = pathArr[0].newPermission;
let id = `${logId}_${logDate}_${user}_${activityKor}_${path}_${beforePermission}_${newPermission}`;
let log = document.createElement('div');
log.classList.add('log');
log.classList.add(activityType);
let logText1 = document.createElement('div');
logText1.classList.add('text');
logText1.innerHTML = `${path}`;
let logText2 = document.createElement('div');
logText2.classList.add('text');
logText2.innerHTML = `기존 권한 : ${beforePermission} → 신규 권한 : ${newPermission}`;
log.appendChild(logText1);
log.appendChild(logText2);
idArr.push(id);
dateArr.push(logDate);
userArr.push(user);
activityArr.push(activityKor);
logArr.push(log);
} else if (activity.startsWith('addPermission') || activity.startsWith('deletePermission')){
let id = `${logId}_${logDate}_${user}_${activityKor}_${pathArr[0]}`;
if (pathArr[1]) id += `_${pathArr[1]}`;
let log = document.createElement('div');
log.classList.add('log');
log.classList.add(activityType);
let logText = document.createElement('div');
logText.classList.add('text');
// 유저권한 로그는 한명 초과시 누구 외 O명으로 표현
logText.innerHTML = pathArr.length === 1 ? pathArr[0] : `${pathArr[0]}${pathArr.length - 1}`;
log.appendChild(logText);
idArr.push(id);
dateArr.push(logDate);
userArr.push(user);
activityArr.push(activityKor);
logArr.push(log);
} else if (activity == 'editAuthor') {
for (let i = 0; i < pathArr.length; i++) {
let path = pathArr[i].resourcePath;
let prevAuthorNm = pathArr[i].prevAuthorNm;
let newAuthorNm = pathArr[i].newAuthorNm;
let id = `${logId}_${logDate}_${user}_${activityKor}_${path}_${prevAuthorNm}_${newAuthorNm}`;
let log = document.createElement('div');
log.classList.add('log');
log.classList.add(activityType);
let logText1 = document.createElement('div');
logText1.classList.add('text');
logText1.innerHTML = `${path}`;
let logText2 = document.createElement('div');
logText2.classList.add('text');
logText2.innerHTML = `기존 작성자 : ${prevAuthorNm} → 신규 작성자 : ${newAuthorNm}`;
log.appendChild(logText1);
log.appendChild(logText2);
idArr.push(id);
dateArr.push(logDate);
userArr.push(user);
activityArr.push(activityKor);
logArr.push(log);
}
} else {
for (let i = 0; i < pathArr.length; i++) {
let id = `${logId}_${logDate}_${user}_${activityKor}_${pathArr[0]}`;
if (pathArr[1]) id += `_${pathArr[1]}`;
let log = document.createElement('div');
log.classList.add('log');
log.classList.add(activityType);
let logText = document.createElement('div');
logText.classList.add('text');
logText.innerHTML = `${pathArr[i]}`;
log.appendChild(logText);
idArr.push(id);
dateArr.push(logDate);
userArr.push(user);
activityArr.push(activityKor);
logArr.push(log);
}
}
for (let i = 0; i < logArr.length; i++) {
let id = idArr[i];
let date = dateArr[i];
let user = userArr[i];
let activity = activityArr[i];
let log = logArr[i];
let dateDiv = document.createElement('div');
dateDiv.classList.add('date');
let dateText = document.createElement('div');
dateText.classList.add('text');
dateText.innerHTML = date;
dateDiv.appendChild(dateText);
let userDiv = document.createElement('div');
userDiv.classList.add('user');
let userText = document.createElement('div');
userText.classList.add('text');
userText.innerHTML = user;
userDiv.appendChild(userText);
let activityDiv = document.createElement('div');
activityDiv.classList.add('activity');
let activityText = document.createElement('div');
activityText.classList.add('text');
activityText.innerHTML = activity;
activityDiv.appendChild(activityText);
let logItem = document.createElement('div');
logItem.id = id;
logItem.classList.add('log-item');
logItem.classList.add(`log-${logId}`);
logItem.appendChild(dateDiv);
logItem.appendChild(userDiv);
logItem.appendChild(activityDiv);
logItem.appendChild(log);
// modalLogItemWrap.prepend(logItem);
modalLogItemWrap.prepend(logItem);
// vars.newLogItemArr 배열에 새로운 로그 아이템 추가
vars.newLogItemArr.push(logItem);
}
// }
})
// vars.newLogItemArr 배열에 있는 로그 아이템에 애니메이션 추가
for (let i = 0; i < vars.newLogItemArr.length; i++) {
let newLogItem = vars.newLogItemArr[i];
if (!isInit) {
newLogItem.style.setProperty('--log-highlight', 'log-highlight 1s linear(0 0%, 1 50%, 0 100%) 0s 2 normal none running');
}
}
// 모달 로그 아이템 중 최신 로그 복제
if (addFooterLog) {
let modalLatestLog = modalLogItemWrap.children[0];
if (modalLatestLog) {
let modalLatestLogClone = modalLatestLog.cloneNode(true);
// 푸터 로그 영역 비우고 복제한 로그 추가
if (footerLatestLog) {
let dateClone = modalLatestLogClone.querySelector('.date').cloneNode(true);
let userClone = modalLatestLogClone.querySelector('.user').cloneNode(true);
let activityClone = modalLatestLogClone.querySelector('.activity').cloneNode(true);
let logClone = modalLatestLogClone.querySelector('.log').cloneNode(true);
let separator1 = document.createElement('div');
separator1.classList.add('log-separator');
let separator2 = document.createElement('div');
separator2.classList.add('log-separator');
let separator3 = document.createElement('div');
separator3.classList.add('log-separator');
footerLatestLog.innerHTML = '';
footerLatestLog.appendChild(modalLatestLogClone);
modalLatestLogClone.innerHTML = '';
modalLatestLogClone.appendChild(dateClone);
modalLatestLogClone.appendChild(separator1);
modalLatestLogClone.appendChild(userClone);
modalLatestLogClone.appendChild(separator2);
modalLatestLogClone.appendChild(activityClone);
modalLatestLogClone.appendChild(separator3);
modalLatestLogClone.appendChild(logClone);
}
}
}
//// 활동로그 모달에서 시간, 유저, 활동종류 각각 제일 긴 항목 기준으로 width 조정
if (document.querySelector('.archive-modal .modal-body .log-wrap').style.display == 'flex') {
// 활동로그가 열려있을 때에만 적용
let dateDivs = modalLogItemWrap.querySelectorAll('.log-item .date');
let userDivs = modalLogItemWrap.querySelectorAll('.log-item .user');
let activityDivs = modalLogItemWrap.querySelectorAll('.log-item .activity');
resizeLogDivs(dateDivs);
resizeLogDivs(userDivs);
resizeLogDivs(activityDivs);
let firstLog = document.querySelector('.log-item-wrap.log-body').childNodes[0];
if (firstLog?.classList?.contains('log-item')) {
let dateWidth = firstLog.getElementsByClassName('date')[0].offsetWidth;
let userWidth = firstLog.getElementsByClassName('user')[0].offsetWidth;
let activityWidth = firstLog.getElementsByClassName('activity')[0].offsetWidth;
document.querySelector('.log-item-wrap.log-header .date').style.width = `${dateWidth}px`;
document.querySelector('.log-item-wrap.log-header .user').style.width = `${userWidth}px`;
document.querySelector('.log-item-wrap.log-header .activity').style.width = `${activityWidth}px`;
}
}
document.querySelector('.archive-modal .modal-body > .log-wrap').style.opacity = '1';
function getTextPixelWidth(text) {
let remPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
let fontPx = remPx * 0.8;
let font = `${fontPx}px "Noto Sans KR"`;
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
context.font = font;
let width = context.measureText(text).width;
return width;
}
function resizeLogDivs(divs) {
let maxTextWidth = 0;
divs.forEach(div => {
let text = div.querySelector('.text');
let textWidth = getTextPixelWidth(text.textContent);
if (textWidth > maxTextWidth) maxTextWidth = textWidth;
})
divs.forEach(div => {
div.style.width = `${pxToRem(maxTextWidth+8)}rem`;
})
}
}
export async function renderEditAuthorUserList(inputWrap, keyword) {
let confirmBtn = document.querySelector('.archive-modal .modal-body .btn-wrap .confirm-btn');
confirmBtn.classList.add('disabled');
// 기존 경고문구 있으면 삭제
if (document.querySelector('.archive-modal .input-wrap .warn')) {
inputWrap.removeChild(document.querySelector('.archive-modal .input-wrap .warn'));
}
// 경고문구 dom 생성
let warn = document.createElement('div');
warn.classList.add('warn');
warn.style.top = `${inputWrap.offsetHeight}px`;
inputWrap.appendChild(warn);
// 이름으로 필터링 이후 직급으로 정렬
let users = vars.permissionList.all;
let positionMap = {'회장': 1, '부회장':2, '사장':3, '상임고문':4, '기술위원':5, '부사장':6, '고문':7, '전무이사':8, '수석연구원':9, '상무이사':10, '이사':11, '책임연구원':12, '부장':13, '차장':14, '선임연구원':15, '과장':16, '연구원':17, '대리':18, '사원':19};
let resultArr = users.filter(user => user.user_nm?.toLowerCase().includes(keyword)).sort((a,b) => {
let aPosition = positionMap[a.position] ?? 99;
let bPosition = positionMap[b.position] ?? 99;
if(aPosition !== bPosition) return aPosition - bPosition;
return a.user_id.localeCompare(b.user_id);
})
// 상황에 따라 경고문구 텍스트 추가 또는 renameTarget 진행
if (keyword == '') {
warn.innerText = '검색창에 이름을 입력해주세요.';
} else if (resultArr.length === 0) {
warn.innerText = '검색 결과가 없습니다.';
} else {
// 경고문구 dom 삭제
inputWrap.removeChild(warn);
// 직원 리스트 생성
let userListWrap = document.querySelector('.archive-modal .modal-body .user-list-wrap');
let userItemWrap = userListWrap.querySelector('.user-item-wrap');
// 작성자 변경 선택 결과 숨김
// let selectedUserItemWrap = userListWrap.querySelector('.selected-user-item-wrap');
// let selectedUserItem = selectedUserItemWrap.querySelector('.selected-user-item');
userListWrap.style.display = 'flex';
userListWrap.style.width = `${pxToRem(inputWrap.getBoundingClientRect().width)}rem`;
userItemWrap.innerHTML = '';
// 작성자 변경 선택 결과 숨김
// selectedUserItemWrap.style.display = 'none';
// selectedUserItem.innerHTML = '';
for (let result of resultArr) {
let userItem = document.createElement('div');
userItem.classList.add('user-item');
userItem.dataset.userId = result.user_id;
userItem.dataset.userNm = result.user_nm;
let userImage = document.createElement('img');
userImage.classList.add('user-image');
let defaultImageUrl = '/main/img/archive/empty-profile.svg';
let userImageUrl = defaultImageUrl;
if (/^[a-zA-Z]{1}\d{5}$/.test(result.user_id)) userImageUrl = `http://erp.hanmaceng.co.kr/erpphoto/${result.user_id}.jpg`;
userImage.src = userImageUrl;
// onerror 핸들러 바인딩 (한번만 동작하도록)
userImage.onerror = function() {
this.onerror = null; // 무한루프 방지
this.src = defaultImageUrl;
};
let userInfo = document.createElement('div');
userInfo.classList.add('user-info');
let userCompany = document.createElement('div');
userCompany.classList.add('user-company');
let userCompanyIcon = document.createElement('img');
userCompanyIcon.classList.add('user-company-icon');
userCompanyIcon.src = `/main/img/permission/s-icon__${result.company}-small-logo.svg`;
let userCompanyText = document.createElement('div');
userCompanyText.classList.add('user-company-text');
userCompanyText.innerHTML = result.company;
let userName = document.createElement('div');
userName.classList.add('user-name');
userName.innerHTML = (`${result.user_nm} ${result.position ?? ''}`).trim();
userItemWrap.appendChild(userItem);
userItem.appendChild(userImage);
userItem.appendChild(userInfo);
userInfo.appendChild(userCompany);
userInfo.appendChild(userName);
userCompany.appendChild(userCompanyIcon);
userCompany.appendChild(userCompanyText);
userItem.addEventListener('click', (e) => {
userItemWrap.childNodes.forEach(item => {
if (e.target == item) {
item.classList.add('selected');
} else {
item.classList.remove('selected');
}
})
// 작성자 변경 선택 결과 숨김
// let userItemClone = userItem.cloneNode(true);
// if (selectedUserItemWrap.style.display != 'flex') selectedUserItemWrap.style.display = 'flex';
// selectedUserItem.innerHTML = '';
// selectedUserItem.appendChild(userItemClone);
document.querySelector('.archive-modal .modal-body .btn-wrap .confirm-btn').classList.remove('disabled');
})
}
}
}
export function changeHeaderBtnStyle(headerBtn) {
// if (vars.lastHeaderBtn) vars.lastHeaderBtn.classList.remove('selected');
// document.querySelectorAll('.header-center .menu-tab .btn').forEach(e => {
document.querySelectorAll('body > .header .center .btn').forEach(e => {
e.classList.remove('selected');
});
// if (document.querySelector('.official-doc-btn')) document.querySelector('.official-doc-btn').classList.remove('selected');
if (headerBtn) vars.lastHeaderBtn = headerBtn;
if (vars.lastHeaderBtn) {
vars.lastHeaderBtn.classList.add('selected');
// let treeTitle = document.querySelector()
}
// vars.users[JSON.parse(vars.userInfoString).clientId].curPath = resourcePath;
// vars.lastMainTreeItem = undefined;
// vars.lastListItem = undefined;
// vars.lastContextTarget = undefined;
// vars.lastSelectTarget = undefined;
// document.querySelector('.archive-main-center .list-container .list-wrap.list-header').dataset.resourcePath = '';
// document.querySelector('.archive-main-center .list-container .list-wrap.list-body').dataset.resourcePath = '';
}
export function changeTreeItemStyle(treeItem, targetTree='main') {
if (targetTree == 'main') {
// if (vars.lastMainTreeItem) vars.lastMainTreeItem.style.background = '';
// vars.lastMainTreeItem = treeItem;
// vars.lastMainTreeItem.style.background = '#bccbc8';
document.querySelectorAll('.archive-main-left .tree-container .tree .tree-item-wrap.depth3').forEach(e => {
e.classList.remove('selected');
});
document.querySelector('.archive-main-left .tree-title').classList.remove('selected');
if (treeItem) vars.lastMainTreeItem = treeItem;
if (vars.lastMainTreeItem) {
vars.lastMainTreeItem.classList.add('selected');
vars.lastSelectTarget = vars.lastMainTreeItem.querySelector('.tree-item');
}
// } else {
// if (vars.lastModalTreeItem) vars.lastModalTreeItem.style.background = '';
// vars.lastModalTreeItem = treeItem;
// vars.lastModalTreeItem.style.background = '#bccbc8';
}
}
export function changeListItemStyle(listItem, from) {
let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex';
if (isRecycleBinModal) {
//// 서브 list item 있는 경우 배경 색상 변경
if (vars.lastListGroupTarget_bin) {
vars.lastListGroupTarget_bin.classList.remove('group-style');
}
if (listItem.matches('.main-list-item')) {
vars.lastListGroupTarget_bin = listItem;
}
if (listItem.matches('.sub-list-item')) {
vars.lastListGroupTarget_bin = document.querySelector(`.list-item.main-list-item[data-main-file-name="${listItem.dataset.mainFileName}"]`);
// if (listItem.matches('.grid-item')) {
// vars.lastListGroupTarget_bin = document.querySelector(`.grid-item.main-list-item[data-main-file-name="${listItem.dataset.mainFileName}"]`);
// } else {
// vars.lastListGroupTarget_bin = listItem.closest('.main-list-item');
// }
}
if (vars.lastListGroupTarget_bin) {
vars.lastListGroupTarget_bin.classList.add('group-style');
}
//// 단일 list item 배경 색상 변경
if (vars.lastListItem_bin) {
vars.lastListItem_bin.classList.remove('selected');
}
vars.lastListItem_bin = listItem;
vars.lastListItem_bin.classList.add('selected');
vars.lastSelectTarget_bin = vars.lastListItem_bin;
let isExistsDataId = false;
vars.multiSelectListItemArr_bin.forEach((item) => {
if (item.dataset.dataId === listItem.dataset.dataId) isExistsDataId = true;
})
if (!isExistsDataId) vars.multiSelectListItemArr_bin.push(listItem);
} else {
//// 서브 list item 있는 경우 배경 색상 변경
if (vars.lastListGroupTarget) {
vars.lastListGroupTarget.classList.remove('group-style');
}
if (listItem.matches('.main-list-item')) {
vars.lastListGroupTarget = listItem;
}
if (listItem.matches('.sub-list-item')) {
vars.lastListGroupTarget = document.querySelector(`.list-item.main-list-item[data-main-file-name="${listItem.dataset.mainFileName}"]`);
// if (listItem.matches('.grid-item')) {
// vars.lastListGroupTarget = document.querySelector(`.grid-item.main-list-item[data-main-file-name="${listItem.dataset.mainFileName}"]`);
// } else {
// vars.lastListGroupTarget = listItem.closest('.main-list-item');
// }
}
if (vars.lastListGroupTarget) {
vars.lastListGroupTarget.classList.add('group-style');
}
//// 단일 list item 배경 색상 변경
if (vars.lastListItem) {
vars.lastListItem.classList.remove('selected');
}
vars.lastListItem = listItem;
vars.lastListItem.classList.add('selected');
vars.lastSelectTarget = vars.lastListItem;
let isExistsDataId = false;
vars.multiSelectListItemArr.forEach((item) => {
if (item.dataset.dataId === listItem.dataset.dataId) isExistsDataId = true;
})
if (!isExistsDataId) vars.multiSelectListItemArr.push(listItem);
}
let me = JSON.parse(vars.userInfoString);
me.selected = listItem.dataset.resourcePath;
vars.socket.emit('fileSelect',{me : me});
}
// 구성 모달 데이터 렌더링
export async function renderCompositionData() {
const modal = document.querySelector('.composition-modal');
let userString = vars.userInfoString;
let user = JSON.parse(userString);
// 최상단 폴더 (탭)
const tabList = Object.keys(vars.allTreeObject.folder).sort(naturalSort);
// 리스트 wrap
const listWrap = modal.querySelector('.modal-wrap > ul');
listWrap.innerHTML = ''; // 초기화
// 각 최상단 폴더(탭)에 대한 처리
for (const tab of tabList) {
let getTreeObjectParams = {
userInfoString: JSON.stringify(user),
storageType: vars.storageType,
resourcePath: tab
};
let getTreeObjectRes = await axios.get(`${vars.path_name}/getTreeObject`, { params: {params: getTreeObjectParams} });
if(getTreeObjectRes.data.message == 'getTreeObject_success') {
const treeData = getTreeObjectRes.data.currentTreeObject;
// 최상단 폴더 li 생성
const tabLi = document.createElement('li');
// 최상단 폴더 제목 (h3)
const tabTitle = document.createElement('h3');
tabTitle.textContent = tab;
tabLi.appendChild(tabTitle);
// 상단 폴더 (카테고리) ul 생성
const categoryUl = document.createElement('ul');
// 상단 폴더 (카테고리) 데이터가 있다면 처리
if (treeData && treeData.folder && Object.keys(treeData.folder).length > 0) {
const categories = Object.keys(treeData.folder).sort(naturalSort);
categories.forEach(category => {
// 상단 폴더 li 생성
const categoryLi = document.createElement('li');
// 상단 폴더 제목 (h5)
const categoryTitle = document.createElement('h5');
categoryTitle.textContent = category;
categoryLi.appendChild(categoryTitle);
// 폴더 ul 생성
const folderUl = document.createElement('ul');
const folders = treeData.folder[category] || [];
const folderObj = folders.child?.folder || {};
const folderKeys = Object.keys(folderObj).sort(naturalSort);
// 폴더가 없을 때 빈 항목 표시
if (folderKeys.length === 0) {
// const emptyFolderLi = document.createElement('li');
// const emptyIcon = document.createElement('i');
// emptyFolderLi.appendChild(emptyIcon);
// const emptyName = document.createElement('h6');
// emptyName.textContent = '-';
// emptyFolderLi.appendChild(emptyName);
// const emptyCount = document.createElement('h6');
// emptyCount.textContent = '-';
// emptyFolderLi.appendChild(emptyCount);
// folderUl.appendChild(emptyFolderLi);
} else {
// 폴더가 있을 때
folderKeys.forEach(folderName => {
const folderData = folderObj[folderName];
// 폴더 내 파일 개수 계산
let fileCount = 0;
// child.file의 파일 개수
fileCount += Object.keys(folderData.child?.file || {}).length;
// child.folder 안의 모든 파일들도 카운트 (첨부, 버전 파일)
if (folderData.child?.folder) {
Object.keys(folderData.child.folder).forEach(subFolderName => {
const subFolder = folderData.child.folder[subFolderName];
if (subFolder.child?.file) {
fileCount += Object.keys(subFolder.child.file).length;
}
});
}
// 폴더 li 생성
const folderLi = document.createElement('li');
// 폴더 아이콘
const folderIcon = document.createElement('i');
folderLi.appendChild(folderIcon);
// 폴더명
const folderNameEl = document.createElement('h6');
folderNameEl.textContent = folderName;
folderLi.appendChild(folderNameEl);
// 파일 개수
const folderCountEl = document.createElement('h6');
folderCountEl.textContent = fileCount > 0 ? String(fileCount) : '-';
folderLi.appendChild(folderCountEl);
folderUl.appendChild(folderLi);
});
}
categoryLi.appendChild(folderUl);
categoryUl.appendChild(categoryLi);
});
} else {
// 카테고리가 없을 때 빈 항목 표시
const emptyCategoryLi = document.createElement('li');
const emptyCategoryTitle = document.createElement('h5');
emptyCategoryTitle.textContent = '-';
emptyCategoryLi.appendChild(emptyCategoryTitle);
const emptyFolderUl = document.createElement('ul');
const emptyFolderLi = document.createElement('li');
const emptyIcon = document.createElement('i');
emptyFolderLi.appendChild(emptyIcon);
const emptyName = document.createElement('h6');
emptyName.textContent = '-';
emptyFolderLi.appendChild(emptyName);
const emptyCount = document.createElement('h6');
emptyCount.textContent = '-';
emptyFolderLi.appendChild(emptyCount);
emptyFolderUl.appendChild(emptyFolderLi);
emptyCategoryLi.appendChild(emptyFolderUl);
categoryUl.appendChild(emptyCategoryLi);
}
tabLi.appendChild(categoryUl);
listWrap.appendChild(tabLi);
}
}
// 렌더링 완료 후 높이 동기화
setTimeout(() => {
const allLis = listWrap.querySelectorAll(':scope > li');
if (allLis.length === 0) return;
// 모든 li의 최대 높이 찾기
let maxHeight = 0;
allLis.forEach(li => {
const height = li.scrollHeight;
if (height > maxHeight) maxHeight = height;
});
// 모든 li 에 최대 높이 적용
allLis.forEach(li => {
li.style.minHeight = `${maxHeight}px`;
})
}, 0);
}
// 정렬 함수
function naturalSort(a, b) {
const regex = /(\d+)|(\D+)/g;
const aParts = a.match(regex);
const bParts = b.match(regex);
for (let i = 0; i < Math.min(aParts.length, bParts.length); i++) {
const aPart = aParts[i];
const bPart = bParts[i];
// 둘 다 숫자인 경우
if (!isNaN(aPart) && !isNaN(bPart)) {
const diff = parseInt(aPart) - parseInt(bPart);
if (diff !== 0) return diff;
} else {
// 문자열 비교
if (aPart !== bPart) {
return aPart.localeCompare(bPart);
}
}
}
return aParts.length - bParts.length;
}