import { vars } from './variable.js'; import { formatBytes, getDepth, splitBaseAndExt, splitTopPathAndTargetName, extractPathByLength, hasSpecialChar, getNextFileName, getDataFromTreeObject, buildResourcePathFromSegments } from './common.js'; import { toggleModal } from './modalManager.js' import { showNotification, toggleContextmenu, toggleContextFocusBox } from './eventManager.js' import { renderMemo, resetViewer, preparePageRendering } from './pageRenderer.js' let listContainer = document.querySelector('.archive-main-center .list-container'); // 딜레이 함수 function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // ** 권한 관련 function changeUserPermission(num) { // 유저 퍼미션 숫자를 데이터 퍼미션 숫자와 비교하기 위해 변환 let result; if (num == 1) result = 1; if (num == 3) result = 2; if (num == 7) result = 4; if (num == 15) result = 8; if (num >= 191) result = 99999; return result; } async function checkTargetExists(dataType, resourcePathArr) { let result = {}; if (!Array.isArray(resourcePathArr)) resourcePathArr = [resourcePathArr]; if (dataType == 'folder' && resourcePathArr.length == 1) { // dataType이 folder이고, resourcePathArr의 length가 1인 경우는 폴더 생성/이름변경 경우만 해당 // depth1 폴더 생성/이름변경 시 헤더 페이지버튼(과업개요, 공문)과 중복 안되도록 처리 let depth = getDepth(resourcePathArr[0]); let pageBtnNameArr = ['/과업개요', '/공문', '/휴지통', '/위치기반모델']; if (depth == 1 && pageBtnNameArr.includes(resourcePathArr[0])) { result.isExists = true; return result; } } let checkTargetExistsParams = { storageType: vars.storageType, dataType: dataType, resourcePathArr: JSON.stringify(resourcePathArr) }; let checkTargetExistsRes = await axios.post(`${vars.path_name}/checkTargetExists`, { params: checkTargetExistsParams } ); if (checkTargetExistsRes.data.message == 'checkTargetExists_success') { let rows = checkTargetExistsRes.data.rows; result.existingPathArr = []; if (rows.length == 0) { result.isExists = false; } else { result.rows = rows; result.isExists = true; result.existingPathArr = buildResourcePathFromSegments(rows); } return result; } } // async function getPageCountQuick(file) { // try { // // 처음 100KB만 읽어서 페이지 수 확인 // const chunk = file.slice(0, Math.min(file.size, 1024 * 100)); // const arrayBuffer = await chunk.arrayBuffer(); // const text = new TextDecoder('latin1').decode(new Uint8Array(arrayBuffer)); // // /Count 값 찾기 // const countMatch = text.match(/\/Count\s+(\d+)/); // if (countMatch) { // return parseInt(countMatch[1]); // } // // /N 값 찾기 // const nMatch = text.match(/\/N\s+(\d+)/); // if (nMatch) { // return parseInt(nMatch[1]); // } // return null; // } catch (error) { // console.error('페이지 수 확인 실패:', error); // return null; // } // } export async function getPdfData(file) { const arrayBuffer = await file.arrayBuffer(); const loadingTask = window.pdfjsLib.getDocument({ data: new Uint8Array(arrayBuffer) }); const pdf = await loadingTask.promise; return pdf; } export async function createPdfThumbnail(pdf, targetWidth, outputFileName) { const page = await pdf.getPage(1); try { // 회전 반영 + targetWidth에 맞춰 스케일 계산 const rotation = page.rotate || 0; const initialViewport = page.getViewport({ scale: 1, rotation }); const scale = targetWidth / initialViewport.width; const viewport = page.getViewport({ scale, rotation }); // 캔버스 준비 const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d', { willReadFrequently: false }); canvas.width = Math.floor(viewport.width); canvas.height = Math.floor(viewport.height); // JPEG 저장 시 투명 배경이 검게 나오는 현상 방지용 (흰 배경) ctx.save(); ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.restore(); // 캔버스에 1페이지 렌더 await page.render({ canvasContext: ctx, viewport }).promise; // 캔버스를 JPEG Blob으로 변환 후 File로 래핑 let blob = await canvasToBlob(canvas, 0.9); let file = new File([blob], outputFileName, { type: blob.type }); // 캔버스 메모리 즉시 축소 canvas.width = 0; canvas.height = 0; return file; } finally { // pdf 문서 해제 try { await pdf.cleanup(); } catch {} try { await pdf.destroy(); } catch {} } } function fitScaleForWidth(viewport, targetWidth = 300) { const scale = targetWidth / viewport.width; return scale; } export async function createTextThumbnail(textFile, outputFileName) { return new Promise((resolve, reject) => { const viewerTextWrap = document.createElement('div'); viewerTextWrap.classList.add('viewer-text-wrap', 'scrollbar'); viewerTextWrap.style.position = 'absolute'; viewerTextWrap.style.left = '-9999px'; viewerTextWrap.style.top = '0'; viewerTextWrap.style.width = '800px'; viewerTextWrap.style.height = '600px'; viewerTextWrap.style.overflow = 'hidden'; viewerTextWrap.style.backgroundColor = 'white'; viewerTextWrap.style.zIndex= 9999; const ext = (() => { const parts = textFile.name.split('.'); return parts.length > 1 ? parts.pop().toLowerCase() : ''; })(); const reader = new FileReader(); reader.onload = async (e) => { try { const data = e.target.result; const viewerText = document.createElement('div'); viewerText.classList.add('viewer-text'); if (ext === 'md') { viewerText.style.whiteSpace = 'normal'; viewerText.classList.add('markdown-body'); const renderer = new marked.Renderer(); const originalCode = renderer.code.bind(renderer); renderer.code = (code, lang) => { if (lang === 'mermaid') { return `
${code}
`; } return originalCode(code, lang); }; marked.setOptions({ gfm: true, breaks: true, renderer }); viewerText.innerHTML = marked.parse(data); } else { viewerText.textContent = data; } viewerTextWrap.appendChild(viewerText); document.body.appendChild(viewerTextWrap); // highlight.js 적용 if (ext === 'md' && typeof hljs !== 'undefined') { hljs.highlightAll(); } // Mermaid 렌더링 if (ext === 'md' && typeof mermaid !== 'undefined') { mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' }); const mermaidDivs = viewerTextWrap.querySelectorAll('pre code.language-mermaid'); if (mermaidDivs.length > 0) { const renderPromises = []; mermaidDivs.forEach((div, i) => { const code = div.textContent; const uniqueId = `mermaid-${Date.now()}-${i}`; div.innerHTML = ''; // 이전 텍스트 제거 renderPromises.push( mermaid.render(uniqueId, code) .then(({ svg, bindFunctions }) => { div.innerHTML = svg; if (bindFunctions) bindFunctions(div); }) .catch(err => { console.error('Mermaid 렌더링 실패:', err); div.innerHTML = `
Mermaid 렌더링 오류:\n${code}
`; }) ); }); await Promise.all(renderPromises); // 렌더링 반영 기다리기 await new Promise(res => setTimeout(res, 100)); } } // html2canvas 캡처 const canvas = await html2canvas(viewerTextWrap, { logging: false, useCORS: true, backgroundColor: null, onclone: (clonedDoc) => { const clonedEl = clonedDoc.querySelector('.viewer-text-wrap'); if (clonedEl) { clonedEl.style.position = 'static'; clonedEl.style.left = '0'; clonedEl.style.top = '0'; clonedEl.style.width = '800px'; clonedEl.style.height = '600px'; } } }); viewerTextWrap.remove(); if (canvas.width === 0 || canvas.height === 0) { resolve(undefined); return; } const blob = await canvasToBlob(canvas, 0.8); let finalName = outputFileName; if (!finalName.toLowerCase().endsWith('.jpg')) { const base = finalName.split('.').slice(0, -1).join('.'); finalName = base + '.jpg'; } const file = new File([blob], finalName, { type: 'image/jpeg' }); resolve(file); } catch (err) { if (viewerTextWrap.parentNode) viewerTextWrap.remove(); reject(err); } }; reader.onerror = () => resolve(undefined); reader.readAsText(textFile); }); } //text썸네일 필요함수 async function textCanvasToBlob(canvas, quality = 0.9) { return new Promise(resolve => { // JPG 파일 형식으로 변환 canvas.toBlob(blob => { resolve(blob); }, 'image/jpeg', quality); }); } export async function createVideoThumbnail(videoFile, outputFileName) { return new Promise((resolve, reject) => { let video = document.createElement('video'); video.preload = 'metadata'; video.src = URL.createObjectURL(videoFile); video.crossOrigin = 'anonymous'; video.muted = true; video.addEventListener('loadeddata', async () => { try { video.currentTime = 1; video.addEventListener('seeked', async () => { let canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; // 동영상 문제로 캔버스가 생성되지 않는 경우 undefined 반환 if (canvas.width == 0 || canvas.height == 0) { resolve(undefined); return; } let ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); let blob = await canvasToBlob(canvas, 0.9); let file = new File([blob], outputFileName, { type: blob.type }); resolve(file); }, { once: true }); } catch (err) { reject(err); } }); video.addEventListener('error', () => { resolve(undefined); // reject(new Error('동영상 로딩 중 오류 발생')); }); }); } export async function resizeImage(img, maxSizeBytes, outputFileName) { let originalWidth = img.width; let originalHeight = img.height; let aspectRatio = originalHeight / originalWidth; let scale = 0.8; // 초기 스케일 let quality = 0.9; // 초기 캔버스 생성 let canvas = document.createElement('canvas'); canvas.width = Math.round(originalWidth * scale); canvas.height = Math.round(canvas.width * aspectRatio); let ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); // 반복하면서 canvas 다시 리사이즈 while (scale > 0.1) { let blob = await canvasToBlob(canvas, quality); let file = new File([blob], outputFileName, { type: blob.type }); if (file.size <= maxSizeBytes) { return file; } scale -= 0.09; quality -= 0.05; let newWidth = Math.round(originalWidth * scale); let newHeight = Math.round(newWidth * aspectRatio); // 현재 canvas → 새로운 크기로 줄이기 let resizedCanvas = document.createElement('canvas'); resizedCanvas.width = newWidth; resizedCanvas.height = newHeight; resizedCanvas.getContext('2d').drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, newWidth, newHeight); canvas = resizedCanvas; } // 루프에서 리사이즈 통과 안되면 최소 품질/크기로 강제 리사이즈 let finalBlob = await canvasToBlob(canvas, 0.3); return new File([finalBlob], outputFileName, { type: finalBlob.type }); } // function canvasToBlob(canvas, quality = 0.9) { // return new Promise(resolve => { // canvas.toBlob(blob => resolve(blob), 'image/jpeg', quality); // }); // } function canvasToBlob(canvas, quality = 0.9) { return new Promise(resolve => { canvas.toBlob( (blob) => { if (blob) { resolve(blob); } else { resolve(undefined); // reject(new Error('canvas.toBlob 실패: blob이 null입니다.')); } }, 'image/jpeg', quality ); }); } //////// createFolder - 새 폴더 생성 모달창 표시 if (document.querySelector('.menu-add .add-btn')) { document.querySelector('.menu-add .add-btn').addEventListener('click', () => { openCreateFolderModal('/'); }); } export function openCreateFolderModal(resourcePath) { let text = ` 폴더명을 입력한 후 확인을 눌러주세요. `; let toggleParams = { title: '새 폴더', text: text, type: 'createFolder', resourcePath: resourcePath }; toggleModal(true, toggleParams); } //////// createFolder - 클라이언트 측 createFolder export async function createFolder(inputWrap, resourcePath, folderType) { let folderName = (inputWrap.getElementsByTagName('input')[0].value).trim(); let folderPath = (resourcePath == '/') ? `/${folderName}` : `/${resourcePath}/${folderName}`.replaceAll('//', '/'); // 기존 경고문구 있으면 삭제 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 checkTargetExistsResult = await checkTargetExists('folder', folderPath); // 상황에 따라 경고문구 텍스트 추가 또는 createFolder 진행 if (!folderName || folderName == '' || folderName == null) { // 빈문자 체크 warn.innerText = '폴더명을 입력해주세요.'; } else if (hasSpecialChar(folderName)) { // 특수문자 체크 warn.innerHTML = `다음 특수문자는 사용할 수 없습니다.
\\ / : * ? ' " \` < > | #`; } else if (checkTargetExistsResult.isExists) { // 동일이름 여부 체크 let text = `동일한 폴더명이 존재합니다.`; // ** 권한 관련 if (checkTargetExistsResult.rows) { let userInfo = JSON.parse(vars.userInfoString); let changedUserPermission = changeUserPermission(userInfo.permission); let targetDataPermission = checkTargetExistsResult.rows[0].data_permission; targetDataPermission = (targetDataPermission == 0) ? 99999 : targetDataPermission; if (targetDataPermission > changedUserPermission) text = '접근이 제한되어 보이지 않는 폴더 중에\n' + text; } warn.innerText = text; } else { // 경고문구 dom 삭제 inputWrap.removeChild(warn); // 모달창 닫기 toggleModal(false); // createFolder 진행 let parentPermission = 1; if (getDepth(folderPath) > 1) { let parentPath = splitTopPathAndTargetName(folderPath).topPath; let parentData = getDataFromTreeObject(parentPath, 'folder').data; parentPermission = parentData.permission; } let createFolderParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, dateArr: [Date.now()], resourcePathArr: [folderPath], dataPermission: parentPermission, folderType: folderType }; let createFolderRes = await axios.post(`${vars.path_name}/createFolder`, { params: createFolderParams }); if (createFolderRes.data.message == 'createFolder_success') { console.log(createFolderRes.data.message); // 폴더 생성 성공 시 로컬 브라우저 화면 즉시 갱신 (소켓 지연/연결끊김 대비) let userCurPath = (vars.users && vars.socket && vars.users[vars.socket.id]?.curPath) || JSON.parse(vars.userInfoString).curPath || ''; let extractedPath = extractPathByLength(userCurPath, 1); // 1. 헤더 버튼 갱신 await preparePageRendering({ scope: 'headerBtn', from: 'createFolder - local', resourcePath: userCurPath, pushState: false }); // 2. 트리 갱신 await preparePageRendering({ scope: 'tree', resourcePath: extractedPath, userCurPath: userCurPath, pushState: false }); // 3. 리스트 갱신 await preparePageRendering({ scope: 'list', resourcePath: userCurPath, pushState: false }); } } } //////// uploadData - 드래그앤드롭 이벤트(dragover, dragleave, drop) / 파일 input change 이벤트는 pageRenderer.js의 renderContextmenu안에서 추가 let fileArr = []; let notDndArea = document.querySelector('.not-dnd-area'); let listBody = listContainer?.querySelector('.list-wrap.list-body'); let dndArea = listContainer?.querySelector('.dnd-area'); let dndAreaImage = dndArea?.querySelector('.image'); let dndAreaText = dndArea?.querySelector('.text'); let dndAreaWarn1 = dndArea?.querySelector('.warn1'); let dndAreaWarn2 = dndArea?.querySelector('.warn2'); let isDragging = false; window.addEventListener('dragover', async (e) => { e.preventDefault(); // listContariner가 화면에 표시되어 있지 않으면 리턴 if (listContainer.style.display != 'flex') return; // 갤러리 폴더 지도모드일 때 안내 표시 및 리턴 let mapContainer = listContainer?.querySelector('.map-container'); if (mapContainer.style.display == 'flex') { let notificatitonParams = { text: '지도모드에서는 드래그앤드롭 업로드 기능이 제한됩니다.' } showNotification(notificatitonParams); return; } // 페이지영역에 드래그 들어오면 notDndArea, dndArea 표시 notDndArea.style.display = 'flex'; dndArea.style.display = 'flex'; // 드래그 상태 플래그 true isDragging = true; let listBodyWidth = listBody.offsetWidth; let listBodyHeight = listBody.offsetHeight; let listScrollTop = listBody.scrollTop; let listScrollLeft = listBody.scrollLeft; let plusNum = -8; let minusNum = -4; dndArea.style.width = `${listBodyWidth + plusNum}px`; dndArea.style.height = `${listBodyHeight + plusNum}px`; dndArea.style.top = `${listScrollTop - minusNum}px`; dndArea.style.left = `${listScrollLeft - minusNum}px`; let backgroundColor, color, url, text, warn1, warn2; if (e.target.matches('.dnd-area')) { // dndArea 안으로 이동 backgroundColor = '#b5c6c3dd'; color = '#1e5149'; url = '/main/img/archive/upload_in_dnd.svg'; text = '파일을 드롭하면 업로드가 진행됩니다.'; warn1 = '폴더는 드롭해도 업로드되지 않습니다.'; warn2 = ' '; // dndAreaWarn.style.display = 'flex'; } else { // dndArea 밖으로 이동 backgroundColor = '#e9eeeddd'; color = '#a5b9b6'; url = '/main/img/archive/upload_out_dnd.svg'; text = '파일을 여기에 드래그하세요.'; warn1 = ' '; warn2 = ' '; // dndAreaWarn.style.display = 'none'; } dndArea.style.background = backgroundColor; dndArea.style.border = `2px solid ${color}`; dndAreaImage.style.backgroundImage = `url(${url})`; dndAreaText.innerHTML = text; dndAreaText.style.color = color; dndAreaWarn1.innerHTML = warn1; dndAreaWarn2.innerHTML = warn2; }) window.addEventListener('dragleave', (e) => { e.preventDefault(); if (e.target.matches('.not-dnd-area')) { if (!e.relatedTarget || e.relatedTarget == null) { // notDndArea에서 페이지영역 밖으로 나갈 때 notDndArea, dndArea 숨김 notDndArea.style.display = 'none'; dndArea.style.display = 'none'; // 드래그 상태 플래그 false isDragging = false; } } if (e.relatedTarget && e.relatedTarget.matches('.dnd-area')) { // notDndArea에서 dndArea로 들어올 때 아무 변화 없도록 리턴 return; } }) window.addEventListener('drop', async (e) => { e.preventDefault(); if (dndArea.contains(e.target)) { // dndArea 안에 드롭 const items = e.dataTransfer.items; const promises = []; const notAllowedFileArr = []; // let targetStr = ''; for (let i = 0; i < items.length; i++) { const item = items[i].webkitGetAsEntry(); if (item) { if (item.isFile) { promises.push(addFileToFileArr(item, 'file')); // targetStr += 'file/' // } else if (item.isDirectory) { // promises.push(readFolder(item, 'folder')); // targetStr += 'folder/' } } } await Promise.all(promises); let uploadDataOption = { functionId: 'dnd_file', dataType: 'file' } uploadData(fileArr, uploadDataOption); } else { // dndArea 밖에 드롭 } // notDndArea에서 페이지영역 밖으로 나갈 때 notDndArea, dndArea 숨김 notDndArea.style.display = 'none'; dndArea.style.display = 'none'; // 드래그 상태 플래그 false isDragging = false; }) // 드래그 상태에서 웹페이지 밖으로 나갈 때 notDndArea, dndArea 남아있는 경우 초기화 window.addEventListener('mousemove', () => { if (isDragging) { notDndArea.style.display = 'none'; dndArea.style.display = 'none'; isDragging = false; } }) // uploadData에 전달할 fileArr에 드롭된 파일들 담기 async function addFileToFileArr(data, dataType) { return new Promise((resolve, reject) => { data.file(file => { let dataFullPath = data.fullPath; let dataFullPathSplit = dataFullPath.split('/'); dataFullPathSplit.shift(); dataFullPath = dataFullPathSplit.join('/'); file.resourcePath = dataFullPath; file.dataType = dataType; fileArr.push(file); resolve(); }, error => { console.error("파일을 추가하는 중에 오류 발생:", error); reject(error); }); }); } // // 폴더가 드롭된 경우 폴더 내 데이터 확인 // async function readFolder(folder, dataType) { // const folderReader = folder.createReader(); // return new Promise((resolve, reject) => { // let dataList = []; // function readDataListRecursively() { // folderReader.readEntries(async (newDataList) => { // if (newDataList.length === 0) { // resolve(); // return; // } // dataList = dataList.concat(newDataList); // const promises = dataList.map(data => { // if (data.isFile) { // return addFileToFileArr(data, dataType); // } else if (data.isDirectory) { // return readFolder(data, dataType); // } // }); // await Promise.all(promises); // readDataListRecursively(); // }, error => { // console.error("폴더를 확인하는 중에 오류 발생:", error); // reject(error); // }); // } // readDataListRecursively(); // }); // } export async function uploadData(files, option) { let { functionId, dataType, addOnTarget } = option; // Viewer 권한(lev = 1 이하)인 경우 업로드 조기 차단 let userInfo = JSON.parse(vars.userInfoString || '{}'); let permission = userInfo.permission; if (permission !== undefined && permission !== null && permission <= 1) { let toggleParams = { text: '파일 업로드 권한이 없습니다.', type: 'alertModal' }; toggleModal(true, toggleParams); fileArr = []; return; } let totalSize = 0; Array.from(files).forEach(file => { totalSize += file.size; }); // 여유 공간 초과 시 제한 if (totalSize > vars.remainingSize) { let toggleParams = { text: '여유 공간이 부족합니다.', type: 'alertModal' }; toggleModal(true, toggleParams); fileArr = []; return; } // 업로드 크기 초과 시 제한 let sizeLimit = 20 * 1024 * 1024 * 1024; if (totalSize > sizeLimit) { let toggleParams = { text: '한 번에 업로드할 수 있는 최대 용량은 20 GB 입니다.', type: 'alertModal' }; toggleModal(true, toggleParams); fileArr = []; return; } let topPath = vars.lastMainTreeItem.dataset.resourcePath; //// 도면 ctb xref 관련 코드 let functionIdSplit = functionId.split('_'); // if (functionIdSplit[0] == 'addOn' && functionIdSplit[1]) { if (addOnTarget) { // 추가 파일(버전/첨부) 업로드인 경우 db에 depth4파일_attachment/version 폴더 존재 확인 후 // 없으면 생성, 있으면 그대로 진행해서 response로 'ensureAddOnFolder_success' 메시지 전달 let lastContextTargetResourcePath = vars.lastContextTarget.dataset.resourcePath; topPath = `${lastContextTargetResourcePath}_${functionIdSplit[1]}`; let ensureAddOnFolderParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, date: Date.now(), resourcePath: topPath } await axios.post(`${vars.path_name}/ensureAddOnFolder`, { params: ensureAddOnFolderParams }); } let dateArr = [], resourcePathArr = [], sizeArr = [], objectKeyArr = [], thumbnailSizeArr = [], thumbnailKeyArr = [], existingDataIdArr = [], coordArr = []; let uploadFailedFileArr = [], uploadSuccessFileArr = []; // 경로 존재 확인을 위해 resourcePathArr 저장 for (let i = 0; i < files.length; i++) { const file = files[i]; let resourcePath = `${topPath}/${file.name}`; resourcePathArr.push(resourcePath); } // resourcePathArr을 사용해서 경로 존재 확인 let checkTargetExistsResult = await checkTargetExists('file', resourcePathArr); // 이미 존재하는 경로만 existingPathArr에 저장 let existingPathArr = checkTargetExistsResult.existingPathArr; // console.log(existingPathArr); // resourcePathArr 초기화 resourcePathArr = []; let progressTitle; if (functionId == 'uploadFile' || functionId == 'dnd_file') { if (dataType == 'folder') progressTitle = '폴더 업로드'; if (dataType == 'file') progressTitle = '파일 업로드'; } else if (functionId == 'addOn_version') { progressTitle = '버전파일 추가'; } else if (functionId == 'addOn_attachment') { progressTitle = '첨부파일 추가'; } /* JS는 동기적으로 실행되고, DOM 변경이 실제로 화면에 반영(렌더링)되는 것은 현재 JS 실행이 모두 끝난 후에 (이벤트 루프에서 브라우저가 한 번 쉬는 순간)에 그려짐 그런데 confirm()은 **브라우저가 강제로 UI 스레드를 블록**해서 JS 실행도 멈추고, 렌더링도 멈춤 그래서 confirm()이 종료될 때까지 화면 변화가 적용되지 않음 이를 해결하기 위해 브라우저가 한 번 렌더링을 할 틈을 만들어줘야 함 -> await new Promise(r => requestAnimationFrame(r)); */ /* for문 안에서 toggleFileProgress를 실행하면 confirm()이 표시되기 전까지는 프로그레스 화면이 표시가 되지 않기 때문에 첫 번째 파일의 프로그레스가 표시되지 않으므로 for문 밖에서 먼저 toggleFileProgress를 실행해서 프로그레스 화면 및 첫 번째 파일의 프로그레스 표시 */ let progressData = {}; progressData.title = progressTitle; progressData.warn = true; progressData.fileName = ''; progressData.count = ''; progressData.idx = 1; await toggleFileProgress(true, progressData); await new Promise(r => requestAnimationFrame(r)); let progressSizeSpan = document.querySelector('.file-progress .size span'); progressSizeSpan.textContent = ' '; let imageExtArr = ['jpg', 'jpeg', 'png', 'webp', 'gif']; let videoExtArr = ['mp4', 'mov', 'webm']; let textExtArr = ['txt', 'md']; for (let i = 0; i < files.length; i++) { const file = files[i]; progressData = {}; progressData.title = progressTitle; progressData.warn = true; progressData.fileName = file.name; progressData.count = files.length; progressData.idx = i+1; await toggleFileProgress(true, progressData); await new Promise(r => requestAnimationFrame(r)); progressSizeSpan.textContent = ' '; let resourcePath = `${topPath}/${file.name}`; let dataId; if (existingPathArr.includes(resourcePath)) { let isUpload = confirm(`'${file.name}' 파일이 이미 존재합니다.\n기존 파일을 새 파일로 교체하시겠습니까?`); if (isUpload) dataId = getDataFromTreeObject(resourcePath, 'file').data.dataId; if (!isUpload) { if (i == files.length-1) { toggleFileProgress(false); fileArr = []; } continue; } } try { // 1) 서버에 presigned URL 요청 let date = Date.now(); let needsThumbnailExtArr = [...imageExtArr, ...videoExtArr, ...textExtArr, 'pdf']; let isLowerExt = true; let ext = splitBaseAndExt(resourcePath, isLowerExt).ext; //// 이미지, 비디오 썸네일 이미지 생성 let needsThumbnail = needsThumbnailExtArr.includes(ext); //// 이미지 썸네일 이미지 생성 // let needsThumbnail = imageExtArr.includes(ext); let thumbnailPath; if (needsThumbnail) thumbnailPath = splitBaseAndExt(resourcePath, isLowerExt).base + '.jpeg'; let generateUploadUrlParams = { resourcePath: resourcePath, date: date, needsThumbnail: needsThumbnail, thumbnailPath: thumbnailPath }; let generateUploadUrlRes = await axios.post(`${vars.path_name}/generateUploadUrl`, generateUploadUrlParams); if (generateUploadUrlRes.data.message == 'generateUploadUrl_success') { let { originUrl, objectKey, date, thumbnailUrl, thumbnailKey } = generateUploadUrlRes.data.result; // if (i === 1 || i === 2) { // throw new Error(`강제로 실패 처리: ${resourcePath}`); // } let isImage = imageExtArr.includes(ext); let isVideo = videoExtArr.includes(ext); let isText = textExtArr.includes(ext); let isPdf = ext == 'pdf'; // 이미지 경도/위도 데이터 추출 let lon = null, lat = null, height = null; if (isImage) { try { // let gpsData = await exifr.gps(file); // if (gpsData?.latitude && gpsData?.longitude) { // let lon = gpsData.longitude; // let lat = gpsData.latitude; // coordArr.push({lon, lat}); // } let parse = await exifr.parse(file); if (parse?.latitude && parse?.longitude) { lon = parse.longitude; lat = parse.latitude; height = (parse.GPSAltitude) ? parse.GPSAltitude : 0; } else { console.error(`❌ GPS 정보 없음 (${resourcePath})`); } } catch (error) { console.error(`❌ exifr.gps 실패 (${resourcePath})`); } } coordArr.push({lon, lat, height}); let thumbnailFile; if (thumbnailUrl && thumbnailKey) { progressSizeSpan.textContent = '썸네일 생성중'; let resizeTarget; //// 이미지, 비디오, 텍스트 썸네일 이미지 생성 if (isImage) resizeTarget = file; if (isVideo) { resizeTarget = await createVideoThumbnail(file, thumbnailPath); } if (isPdf) { let pdfData = await getPdfData(file); resizeTarget = await createPdfThumbnail(pdfData, 960, thumbnailPath); } if(isText) { resizeTarget = await createTextThumbnail(file, thumbnailPath); } if (resizeTarget) { // if (ext != 'pdf') { let img = new Image(); img.src = URL.createObjectURL(resizeTarget); await new Promise(r => img.onload = r); let maxSizeBytes = 100 * 1024; // 100 kb if (resizeTarget.size > maxSizeBytes) { thumbnailFile = await resizeImage(img, maxSizeBytes, thumbnailPath); } else { progressSizeSpan.textContent = ' '; thumbnailFile = resizeTarget; } await axios.put(thumbnailUrl, thumbnailFile, { headers: { 'Content-Type': thumbnailFile.type || 'application/octet-stream' } }) // } } else { alert('파일 오류로 인해 썸네일 생성이 실패했습니다.\n파일을 다시 확인해주세요.\n파일명: ' + file.name); } } // 2) presigned URL로 직접 파일 PUT 전송 await axios.put(originUrl, file, { headers: { 'Content-Type': file.type || 'application/octet-stream' }, onUploadProgress: async progress => { let totalSize = progress.total; let loadedSize = progress.loaded; let percentage = (loadedSize / totalSize) * 100; progressData.loadedSize = loadedSize; progressData.totalSize = totalSize; progressData.percentage = percentage; toggleFileProgress(true, progressData); } }); let thumbnailSize = 0; if (thumbnailFile) thumbnailSize = thumbnailFile.size; dateArr.push(date); resourcePathArr.push(resourcePath); sizeArr.push(file.size); objectKeyArr.push(objectKey); thumbnailSizeArr.push(thumbnailSize); thumbnailKeyArr.push(thumbnailKey); if (dataId) existingDataIdArr.push(dataId); uploadSuccessFileArr.push(file.name); } else if (generateUploadUrlRes.data.message == 'generateUploadUrl_failed_permission') { let toggleParams = { text: '파일 업로드 권한이 없습니다.', type: 'alertModal' }; toggleModal(true, toggleParams); throw new Error('generateUploadUrl_failed_permission'); } else { throw new Error('generateUploadUrl_failed'); } } catch (err) { console.error(`❌ 업로드 실패: ${resourcePath}`, err); uploadFailedFileArr.push(file.name); } } if (resourcePathArr.length > 0) { // resourcePathArr[0]에서 depth3 경로 추출 // -> depth3 경로로 getDataFromTreeObject 사용해서 depth3폴더 dataId 조회 // -> 파라미터에 depth3DataIdArr 추가 let depth3Path = extractPathByLength(resourcePathArr[0], 3); let dapth3DataId = getDataFromTreeObject(depth3Path, 'folder').data.dataId; let uploadDataParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, dateArr: JSON.stringify(dateArr), resourcePathArr: JSON.stringify(resourcePathArr), sizeArr: JSON.stringify(sizeArr), objectKeyArr: JSON.stringify(objectKeyArr), thumbnailSizeArr: JSON.stringify(thumbnailSizeArr), thumbnailKeyArr: JSON.stringify(thumbnailKeyArr), existingDataIdArr: JSON.stringify(existingDataIdArr), coordArr: JSON.stringify(coordArr), dataType: dataType, functionId: functionId, depth3DataIdArr: [dapth3DataId] } let uploadDataRes = await axios.post(`${vars.path_name}/uploadData`, { params: uploadDataParams }); if (uploadDataRes.data.message == 'uploadData_success') { fileArr = []; toggleContextmenu(false); toggleContextFocusBox(false); let length = JSON.parse(uploadDataParams.resourcePathArr).length; for (let i = 0; i < length ; i++) { let resourcePath = JSON.parse(uploadDataParams.resourcePathArr)[i]; let objectKey = JSON.parse(uploadDataParams.objectKeyArr)[i] let isLowerExt = true let ext = splitBaseAndExt(resourcePath, isLowerExt).ext; if (ext === 'pdf') { let pdfData = await getPdfData(files[i]); let pageCount = pdfData.numPages; if (pageCount > 10){ let thumbParam = { resourcePath : resourcePath, userInfoString : uploadDataParams.userInfoString, objectKey: objectKey, storageType : uploadDataParams.storageType, } await axios.post(`${vars.path_name}/makeThumbPdf`, { params: thumbParam }); } try { await pdfData.cleanup(); } catch {} try { await pdfData.destroy(); } catch {} } if (videoExtArr.includes(ext)) { let postProcessVideoParams = { resourcePath : resourcePath, userInfoString : uploadDataParams.userInfoString, objectKey: objectKey, storageType : uploadDataParams.storageType, } await axios.post(`${vars.path_name}/postProcessVideo`, { params: postProcessVideoParams }); } let progressData = {}; progressData.title = '업로드 마무리'; progressData.warn = true; progressData.count = length; progressData.idx = i+1; toggleFileProgress(true, progressData); await sleep(50); } } } // 업로드 마무리 프로그레스 끝나고 잠깐 대기 후 프로그레스 화면 종료되도록 setTimeout 사용 let timeoutId = setTimeout(async function() { await toggleFileProgress(false, undefined); clearTimeout(timeoutId); }, vars.progressDuration); if (uploadFailedFileArr.length == 0) { // alert('모든 파일이 정상적으로 업로드 되었습니다.'); let toggleParams = { text: '모든 파일의 업로드가 정상적으로 완료되었습니다.', type: 'alertModal', }; toggleModal(true, toggleParams); } else { // let uploadFailedList = ''; // uploadFailedFileArr.map(file => { // uploadFailedList += `${file}\n`; // }); // alert(`총 ${uploadSuccessFileArr.length+uploadFailedFileArr.length}개의 파일 중 ${uploadFailedFileArr.length}개의 파일이 업로드 실패하였습니다. // ${uploadFailedList}`); // let uploadFailedList = ''; // uploadFailedFileArr.map(file => { // uploadFailedList += `${file}
`; // }); // let toggleParams = { // text: `총 ${uploadSuccessFileArr.length+uploadFailedFileArr.length}개 중 ${uploadFailedFileArr.length}개 파일이 업로드에 실패했습니다.
${uploadFailedList}`, // type: 'alertModal', // }; // toggleModal(true, toggleParams); let toggleParams = { text: `총 ${uploadSuccessFileArr.length+uploadFailedFileArr.length}개 중 ${uploadFailedFileArr.length}개 파일이 업로드에 실패했습니다.`, type: 'alertModal', }; toggleModal(true, toggleParams); } } //////// renameTarget - 이름 변경 모달창 표시 export function openRenameModal(resourcePath, target) { // resetViewer(); let dataType, dataTypeKor; target = (target) ? target : vars.lastContextTarget; if (target.matches('.folder')) dataType = 'folder', dataTypeKor = '폴더'; if (target.matches('.file')) dataType = 'file', dataTypeKor = '파일'; let toggleParams = { title: '이름 변경', text: `변경할 ${dataTypeKor}명을 입력한 후 확인을 눌러주세요.
이름 변경 대상 : ${resourcePath}`, type: 'renameTarget', data: target, dataType: dataType, dataTypeKor: dataTypeKor, resourcePath: resourcePath }; toggleModal(true, toggleParams); } //////// renameTarget - 클라이언트 측 renameTarget export async function renameTarget(toggleParams, inputWrap) { let data = toggleParams.data; let dataType = toggleParams.dataType; let dataTypeKor = toggleParams.dataTypeKor; // let oldName = data.getElementsByClassName('name-text')[0].innerText; // let oldName = data.getElementsByClassName('name-text')[0].innerHTML; let oldName = data.getElementsByClassName('name-text')[0].textContent; let newName = (inputWrap.getElementsByTagName('input')[0].value).trim(); let dataId = data.dataset.id; let ext; if (dataTypeKor == '파일') { let split = splitBaseAndExt(oldName); ext = split.ext; oldName = `${split.base}.${ext}`; newName = `${newName}.${ext}`; } let oldPath = toggleParams.resourcePath; let topFolderPath = splitTopPathAndTargetName(oldPath).topPath; let newPath = (topFolderPath == '/') ? `/${newName}` : `${topFolderPath}/${newName}`; // 기존 경고문구 있으면 삭제 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 checkTargetExistsResult = await checkTargetExists(dataType, newPath); // 상황에 따라 경고문구 텍스트 추가 또는 renameTarget 진행 if (!newName || newName == '' || newName == null) { // 빈문자 체크 warn.innerText = `${dataTypeKor}명을 입력해주세요.`; } else if (hasSpecialChar(newName)) { // 특수문자 체크 warn.innerHTML = `다음 특수문자는 사용할 수 없습니다.
\\ / : * ? ' " \` < > | #`; } else if (checkTargetExistsResult.isExists) { // 동일이름 여부 체크 let text = `동일한 ${dataTypeKor}명이 존재합니다.`; // ** 권한 관련 if (checkTargetExistsResult.rows) { let userInfo = JSON.parse(vars.userInfoString); let changedUserPermission = changeUserPermission(userInfo.permission); let targetDataPermission = checkTargetExistsResult.rows[0].data_permission; targetDataPermission = (targetDataPermission == 0) ? 99999 : targetDataPermission; if (dataType == 'folder') { if (targetDataPermission > changedUserPermission) text = '접근이 제한되어 보이지 않는 폴더 중에\n' + text; } } warn.innerText = text; } else { // 경고문구 dom 삭제 inputWrap.removeChild(warn); // 모달창 닫기 toggleModal(false); // renameTarget 진행 // 컨트롤러 실행 let renameTargetParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, resourcePath: toggleParams.resourcePath, dataType: dataType, newName: newName, oldName: oldName, newPath: newPath, oldPath: oldPath, dataId: dataId } let renameTargetRes = await axios.post(`${vars.path_name}/renameTarget`, { params: renameTargetParams }); if (renameTargetRes.data.message == 'renameTarget_success') { console.log('renameTarget_success'); toggleContextmenu(false); // } else if (renameTargetRes.data.message == 'renameTarget_failed') { // let toggleParams = { text: '해당 폴더 내 파일이 사용 중이어서 이름을 변경할 수 없습니다.
잠시 후 다시 시도해 주세요.' }; // toggleModal(true, toggleParams); } if (renameTargetRes.data.message == 'renameTarget_failed_permission') { let params = { text: `${toggleParams.title} 실패 : 권한이 부족하여 작업이 실패했습니다.`, type: 'alertModal' }; toggleModal(true, params); } } } //////// editAuthor - 작성자 변경 모달창 표시 export function openEditAuthorModal(resourcePath, target) { target = (target) ? target : vars.lastContextTarget; let resourcePathArr = [], dataIdArr = [], prevAuthorIdArr = [], prevAuthorNmArr = []; if (vars.multiSelectListItemArr.length == 0) { resourcePathArr = [resourcePath]; // dataIdArr = [vars.lastContextTarget.dataset.id]; dataIdArr = [target.dataset.id]; let data = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject).data; prevAuthorIdArr = (data.authorId) ? [data.authorId] : [data.userId]; prevAuthorNmArr = (data.authorNm) ? [data.authorNm] : [data.userNm]; } else { vars.multiSelectListItemArr.forEach(elem => { resourcePathArr.push(elem.dataset.resourcePath); dataIdArr.push(elem.dataset.id); let data = getDataFromTreeObject(elem.dataset.resourcePath, 'file', vars.currentTreeObject).data; prevAuthorIdArr.push((data.authorId) ? data.authorId : data.userId); prevAuthorNmArr.push((data.authorNm) ? data.authorNm : data.userNm); }) } let toggleParams = { title: '작성자 변경', text: '이름 검색 후 작성자를 선택해주세요.', type: 'editAuthor', resourcePathArr: resourcePathArr, dataIdArr: dataIdArr, prevAuthorIdArr: prevAuthorIdArr, prevAuthorNmArr: prevAuthorNmArr }; toggleModal(true, toggleParams); } //////// editAuthor - 클라이언트 측 editAuthor export async function editAuthor(toggleParams) { let resourcePathArr = toggleParams.resourcePathArr; let dataIdArr = toggleParams.dataIdArr; let prevAuthorIdArr = toggleParams.prevAuthorIdArr; let prevAuthorNmArr = toggleParams.prevAuthorNmArr; let newAuthorId = toggleParams.newAuthorId; let newAuthorNm = toggleParams.newAuthorNm; let editAuthorParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, resourcePathArr: resourcePathArr, dataIdArr: dataIdArr, prevAuthorIdArr: prevAuthorIdArr, prevAuthorNmArr: prevAuthorNmArr, newAuthorId: newAuthorId, newAuthorNm: newAuthorNm } let editAuthorRes = await axios.post(`${vars.path_name}/editAuthor`, { params: editAuthorParams }); if (editAuthorRes.data.message == 'editAuthor_success') { toggleModal(false); toggleContextmenu(false); toggleContextFocusBox(false); } } export async function openDownloadModal(resourcePath) { let lastContextTarget, multiSelectListItemArr; let dataType, dataTypeKor; let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex'; if (isRecycleBinModal) { lastContextTarget = vars.lastContextTarget_bin; multiSelectListItemArr = vars.multiSelectListItemArr_bin; } else { lastContextTarget = vars.lastContextTarget; multiSelectListItemArr = vars.multiSelectListItemArr; } if (lastContextTarget.matches('.folder')) dataType = 'folder', dataTypeKor = '폴더'; if (lastContextTarget.matches('.file')) dataType = 'file', dataTypeKor = '파일'; let resourcePathArr = [], dataIdArr = []; if (multiSelectListItemArr.length == 0) { resourcePathArr = [resourcePath]; dataIdArr = [lastContextTarget.dataset.id]; } else { multiSelectListItemArr.forEach(elem => { resourcePathArr.push(elem.dataset.resourcePath); dataIdArr.push(elem.dataset.id); }) } let text; if (dataType == 'folder') { let filesCount = getDataFromTreeObject(resourcePath, 'folder').data.filesCount; text = `다음 폴더에 포함된 ${filesCount}개의 파일을 다운로드 하시겠습니까?${resourcePath}`; } else { if (resourcePathArr.length == 1) { text = `다음 파일을 다운로드 하시겠습니까?${resourcePathArr[0]}`; } else { text = `선택된 ${resourcePathArr.length}개의 파일을 다운로드 하시겠습니까?`; } } let toggleParams = { title: '다운로드', text: text, type: 'downloadTarget', resourcePathArr: resourcePathArr, dataIdArr: dataIdArr, dataType: dataType, dataTypeKor: dataTypeKor }; toggleModal(true, toggleParams); } //////// downloadTarget - 클라이언트 측 downloadTarget export async function downloadTarget(toggleParams) { let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex'; if(toggleParams.dataType !== 'folder'){ //파일인 경우 다운로드 let getDataInfoParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, dataIdArr: toggleParams.dataIdArr, isRemoved: isRecycleBinModal, debug: "'downloadTarget'에서 실행" } let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams } ); if (getDataInfoRes.data.message == 'getDataInfo_success') { let resultArr = getDataInfoRes.data.result; if (!Array.isArray(resultArr)) resultArr = [resultArr]; for (let i = 0; i < resultArr.length; i++) { let result = resultArr[i]; let objectKey = result.object_key; let pathLimit = 8; let segmentArr = []; for (let j = 0; j 0) { vars.multiSelectListItemArr.forEach(item => { if (item.classList.contains('depth5')) { let subItemData = getDataFromTreeObject(item.dataset.resourcePath, 'file', vars.currentTreeObject).data; let subCategory = subItemData.subCategory; let mainItemResourcePath = item.dataset.resourcePath.split(`_${subCategory}`)[0]; let mainItem = document.querySelector(`.list-item[data-resource-path="${mainItemResourcePath}"]`); if (!vars.multiSelectListItemArr.includes(mainItem)) relocateTargetArr.push(item); } else { relocateTargetArr.push(item); } }) } } let overviewBtn = document.querySelector('.overview-btn'); if (state == false) { if (overviewBtn) overviewBtn.classList.remove('disabled'); document.querySelectorAll('.relocate-cover').forEach(cover => { cover.style.display = 'none'; }); } if (state == true) { if (overviewBtn) overviewBtn.classList.add('disabled'); //// 커버 세팅 및 표시 // 헤더 타이틀 커버 세팅 let headerTitleCover = document.querySelector('.header-title-cover'); let headerLeftWidth = document.querySelector('.header .left').offsetWidth; headerTitleCover.style.width = `${headerLeftWidth}px`; headerTitleCover.style.left = 0; // 헤더 우측 버튼 영역 커버 세팅 let headerRightCover = document.querySelector('.header-right-cover'); let headerRightWidth = document.querySelector('.header .center .right').offsetWidth; headerRightCover.style.width = `${headerRightWidth}px`; headerRightCover.style.left = 0; // 접속 인원 영역 커버 세팅 let connectedUsersCover = document.querySelector('.connected-users-cover'); let connectedUsersWidth = document.querySelector('.connected-users').offsetWidth; connectedUsersCover.style.width = `${connectedUsersWidth}px`; // 리스트/뷰어 커버 세팅 let listViewerCover = document.querySelector('.list-viewer-cover'); let listWidth = document.querySelector('.archive-main-center').offsetWidth; let viewerWidth = document.querySelector('.archive-main-right').offsetWidth; listViewerCover.style.width = `${listWidth + viewerWidth}px`; listViewerCover.style.right = '0'; // 푸터 커버 세팅 let footerCover = document.querySelector('.footer-cover'); let footerWidth = document.querySelector('.footer').offsetWidth; let footerHeight = document.querySelector('.footer').offsetHeight; footerCover.style.width = `${footerWidth}px`; footerCover.style.height = `${footerHeight}px`; footerCover.style.bottom = 0; // 전체 커버 표시 document.querySelectorAll('.relocate-cover').forEach(cover => { cover.style.display = 'flex'; }); //// 리스트/뷰어 커버 안에 이동 기능 ui 세팅 // 이동 기능 ui가 화면 중앙에 위치하도록 컨테이너 width값 세팅 // 리스트/뷰어 커버 width값에서 좌측 트리 width값만큼 빼면 컨테이너가 화면 중앙에 위치 let container = listViewerCover.querySelector('.container'); let archiveMainLeftWidth = document.querySelector('.archive-main-left').offsetWidth; container.style.width = `calc(100% - ${archiveMainLeftWidth}px)`; // 기존 폴더 경로 추가 let oldPath = extractPathByLength(resourcePath, 3); listViewerCover.querySelector('.old-path .value').textContent = oldPath; // 선택 폴더 경로 초기화 listViewerCover.querySelector('.new-path .value').textContent = '-'; // 확인/취소 버튼 생성 let confirmBtn = document.createElement('div'); confirmBtn.classList.add('btn'); confirmBtn.classList.add('confirm-btn'); confirmBtn.innerText = '확인'; confirmBtn.addEventListener('click', function() { let oldPath = listViewerCover.querySelector('.old-path .value').textContent; let newPath = listViewerCover.querySelector('.new-path .value').textContent; let oldResourcePathArr = relocateTargetArr.map(target => target.dataset.resourcePath); let newResourcePathArr = oldResourcePathArr.map(path => path.replace(oldPath, newPath)) if (oldPath == newPath) { let toggleParams = { text: '기존 폴더 경로와 선택 폴더 경로가 동일합니다.', type: 'alertModal' }; toggleModal(true, toggleParams); } else { if (newPath == '-') { let toggleParams = { text: '파일을 이동할 폴더가 선택되지 않았습니다.', type: 'alertModal' }; toggleModal(true, toggleParams); } else { let oldFolderDataId = getDataFromTreeObject(oldPath, 'folder').data.dataId; let newFolderDataId = getDataFromTreeObject(newPath, 'folder').data.dataId; let depth3DataIdArr = [oldFolderDataId, newFolderDataId]; relocateTarget(oldResourcePathArr, newResourcePathArr, newPath, depth3DataIdArr); // toggleRelocateCover(false); } } }) let cancelBtn = document.createElement('div'); cancelBtn.classList.add('btn'); cancelBtn.classList.add('cancel-btn'); cancelBtn.innerText = '취소'; cancelBtn.addEventListener('click', function() { toggleRelocateCover(false); }) let btnWrap = listViewerCover.querySelector('.btn-wrap'); btnWrap.innerHTML = ''; btnWrap.appendChild(confirmBtn); btnWrap.appendChild(cancelBtn); } } async function relocateTarget(oldResourcePathArr, newResourcePathArr, newPath, depth3DataIdArr) { let fromPathArr = oldResourcePathArr; let toPathArr = []; // 선택된 파일 중 버전/첨부 파일이 있는 경우 버전/첨부 해제된다는 alert 표시 let containsAddOnFilie = false; for (let i = 0; i < newResourcePathArr.length; i++) { let newResourcePath = newResourcePathArr[i]; if (getDepth(newResourcePath) === 5) { containsAddOnFilie = true; // 진행할 경우 바로 이어서 목적지 폴더와 동일한 이름이 있는지 확인하기 위해 depth5 경로를 depth4 경로로 변경 let newResourcePathSplit = newResourcePath.split('/'); newResourcePathSplit.splice(4, 1); newResourcePathArr[i] = newResourcePathSplit.join('/'); } } if (containsAddOnFilie) { let text = [ '선택된 파일 중 버전/첨부 파일이 있습니다.\n\n', '버전/첨부 파일을 다른 폴더로 이동할 경우 버전/첨부가 해제되며,\n버전/첨부 파일로 되돌리려면 새로 추가해야 합니다.\n\n', '진행하시겠습니까?' ]; let confirmResult = confirm(text.join('')); if (!confirmResult) return; } // resourcePathArr을 사용해서 경로 존재 확인 let checkTargetExistsResult = await checkTargetExists('file', newResourcePathArr); // 이미 존재하는 경로만 existingPathArr에 저장 let existingPathArr = checkTargetExistsResult.existingPathArr; for (let i = 0; i < newResourcePathArr.length; i++) { let newResourcePath = newResourcePathArr[i]; if (existingPathArr.includes(newResourcePath)) { let fileName = splitTopPathAndTargetName(newResourcePath).targetName; newResourcePath = `${newPath}/${getNextFileName(Object.keys(vars.currentTreeObject.file), fileName)}`; alert('이동하려는 파일 중 일부와 동일한 이름의 파일이\n이미 목적지 폴더에 존재하여 이동이 취소되었습니다.\n\n확인 후 다시 시도해주세요.'); return; } toPathArr.push(newResourcePath); } fromPathArr = getFilteredFromPaths(fromPathArr, toPathArr); let dataIdArr = []; for (let i = 0; i < fromPathArr.length; i++) { let fromPath = fromPathArr[i]; let data = getDataFromTreeObject(fromPath, 'file').data; dataIdArr.push(data.dataId) } let relocateTargetParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, dataType: 'file', fromPathArr: fromPathArr, toPathArr: toPathArr, dataIdArr: dataIdArr, depth3DataIdArr: depth3DataIdArr } let relocateTargetRes = await axios.post(`${vars.path_name}/relocateTarget`, { params: relocateTargetParams }); if (relocateTargetRes.data.message == 'relocateTarget_success') { console.log('relocateTarget_success'); vars.lastListItem = undefined; vars.lastContextTarget = undefined; vars.lastSelectTarget = undefined; toggleRelocateCover(false); } // toPath 기준으로 fromPath 필터링 function getFilteredFromPaths(fromPathArr, toPathArr) { return fromPathArr.filter(fromPath => { // const fromName = getFileName(fromPath); // const fromBase = getNameWithoutExt(fromName); // const fromExt = getExt(fromName); const fromName = splitTopPathAndTargetName(fromPath).targetName; const fromBase = splitBaseAndExt(fromName).base; const fromExt = splitBaseAndExt(fromName).ext; return toPathArr.some(toPath => { // const toName = getFileName(toPath); // const toBase = getNameWithoutExt(toName); // const toExt = getExt(toName); const toName = splitTopPathAndTargetName(toPath).targetName; const toBase = splitBaseAndExt(toName).base; const toExt = splitBaseAndExt(toName).ext; // 확장자가 같고, 파일명이 시작 문자열로 포함되면 매칭 return toExt === fromExt && toBase.startsWith(fromBase); }); }); } // function getFileName(path) { // return path.split('/').pop(); // "파일명.확장자" // } // function getNameWithoutExt(name) { // return name.replace(/\.[^/.]+$/, ''); // 확장자 제거 // } // function getExt(name) { // return name.slice(name.lastIndexOf('.')); // ".확장자" // } } //////// removeTarget - 휴지통으로 이동 모달창 표시 export function openRemoveModal(resourcePath, target) { if (!target) { if (vars.lastContextTarget) target = vars.lastContextTarget; else target = vars.multiSelectListItemArr[0]; } let dataType, dataTypeKor; if (target.matches('.folder')) dataType = 'folder', dataTypeKor = '폴더'; if (target.matches('.file')) dataType = 'file', dataTypeKor = '파일'; let resourcePathArr = [], dataIdArr = []; if (vars.multiSelectListItemArr.length == 0) { resourcePathArr = [resourcePath]; // dataIdArr = [vars.lastContextTarget.dataset.id]; dataIdArr = [target.dataset.id]; } else { vars.multiSelectListItemArr.forEach(elem => { resourcePathArr.push(elem.dataset.resourcePath); dataIdArr.push(elem.dataset.id); }) } // vars.multiSelectListItemArr 비어있으면 resourcePath만 사용, 비어있지 않으면 vars.multiSelectListArr의 dom객체 사용해서 resourcePath 배열 추출 // vars.multiSelectListItemArr 비어있으면 text에 파일명 하나만 표시, 비어있지 않으면 여러개 표시 let text; if (dataType == 'folder') { text = `다음 폴더를 완전히 삭제하고, 폴더에 포함된 모든 파일을 휴지통으로 이동하시겠습니까?${resourcePath}`; } else { text = `다음 파일을 휴지통으로 이동하시겠습니까?${resourcePath}`; if (vars.multiSelectListItemArr.length > 1) text = `${vars.multiSelectListItemArr.length}개의 파일을 휴지통으로 이동하시겠습니까?`; } let toggleParams = { title: (dataType == 'folder') ? '폴더 삭제' : '휴지통으로 이동', text: text, type: 'removeTarget', resourcePathArr: resourcePathArr, dataIdArr: dataIdArr, dataType: dataType, dataTypeKor: dataTypeKor }; toggleModal(true, toggleParams); } //////// removeTarget - 클라이언트 측 removeTarget export async function removeTarget(toggleParams) { let depth3DataIdArr = []; if (toggleParams.dataType == 'file') { // dataType이 파일일 때 toggleParams.resourcePathArr[0]에서 depth3 경로 추출 // -> depth3 경로로 getDataFromTreeObject 사용해서 depth3폴더 dataId 조회 // -> 파라미터에 depth3DataIdArr 추가 let depth3Path = extractPathByLength(toggleParams.resourcePathArr[0], 3); let dapth3DataId = getDataFromTreeObject(depth3Path, 'folder').data.dataId; depth3DataIdArr.push(dapth3DataId); } let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex'; let removeTargetParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, resourcePathArr: toggleParams.resourcePathArr, dataIdArr: toggleParams.dataIdArr, dataType: toggleParams.dataType, isExpiredFolder: toggleParams.isExpiredFolder, depth3DataIdArr: depth3DataIdArr, isRecycleBinModal: isRecycleBinModal } let removeTargetRes = await axios.post(`${vars.path_name}/removeTarget`, { params: removeTargetParams }); if (removeTargetRes.data.message == 'removeTarget_success') { console.log('removeTarget_success'); toggleContextmenu(false); } if (removeTargetRes.data.message == 'removeTarget_failed_permission') { let params = { text: `${toggleParams.title} 실패 : 권한이 부족하여 작업이 실패했습니다.`, type: 'alertModal' }; toggleModal(true, params); } } //////// deleteTarget - 삭제 모달창 표시 export function openDeleteModal(resourcePath, target) { let lastContextTarget, multiSelectListItemArr; let dataType, dataTypeKor; let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex'; if (isRecycleBinModal) { lastContextTarget = vars.lastContextTarget_bin; multiSelectListItemArr = vars.multiSelectListItemArr_bin; } else { lastContextTarget = vars.lastContextTarget; multiSelectListItemArr = vars.multiSelectListItemArr; } target = (target) ? target : lastContextTarget; if (target.matches('.folder')) dataType = 'folder', dataTypeKor = '폴더'; if (target.matches('.file')) dataType = 'file', dataTypeKor = '파일'; let resourcePathArr = [], dataIdArr = []; if (multiSelectListItemArr.length == 0) { resourcePathArr = [resourcePath]; // dataIdArr = [vars.lastContextTarget.dataset.id]; dataIdArr = [target.dataset.id]; } else { multiSelectListItemArr.forEach(elem => { resourcePathArr.push(elem.dataset.resourcePath); dataIdArr.push(elem.dataset.id); }) } // vars.multiSelectListItemArr 비어있으면 resourcePath만 사용, 비어있지 않으면 vars.multiSelectListArr의 dom객체 사용해서 resourcePath 배열 추출 // vars.multiSelectListItemArr 비어있으면 text에 파일명 하나만 표시, 비어있지 않으면 여러개 표시 let text; if (dataType == 'folder') { text = `폴더를 완전히 삭제하시겠습니까?${resourcePath}`; } else { text = `파일을 완전히 삭제하시겠습니까?${resourcePath}`; if (multiSelectListItemArr.length > 1) text = `${multiSelectListItemArr.length}개의 파일을 완전히 삭제하시겠습니까?`; } let toggleParams = { title: (dataType == 'folder') ? '폴더 삭제' : '파일 삭제', text: text, type: 'deleteTarget', resourcePathArr: resourcePathArr, dataIdArr: dataIdArr, dataType: dataType, dataTypeKor: dataTypeKor }; toggleModal(true, toggleParams); } //////// deleteTarget - 클라이언트 측 deleteTarget export async function deleteTarget(toggleParams) { let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex'; let chunkSize = 10; let fullDataIdArr = toggleParams.dataIdArr; let progressCount = 0; let progressMax = fullDataIdArr.length; console.log('================== 삭제 시작'); let progressData = {}; progressData.title = '파일 삭제'; progressData.warn = false; progressData.count = progressMax; progressData.idx = progressCount; toggleFileProgress(true, progressData); for (let i = 0; i < fullDataIdArr.length; i += chunkSize) { let chunkDataIdArr = fullDataIdArr.slice(i, i + chunkSize); let getDataInfoParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, dataIdArr: JSON.stringify(chunkDataIdArr), isRemoved: isRecycleBinModal, debug: "'deleteTarget'에서 실행" }; let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams }); if (getDataInfoRes.data.message == 'getDataInfo_success') { console.log('================== 삭제 데이터 준비'); let resultArr = getDataInfoRes.data.result; if (!Array.isArray(resultArr)) resultArr = [resultArr]; let chunkResourcePathArr = []; let chunkObjectKeyArr = []; let chunkPreviewKeyArr = []; let chunkPopupKeyArr = []; let chunkThumbnailKeyArr = []; let currentCount = resultArr.length; for (let j = 0; j < resultArr.length; j++) { let result = resultArr[j]; chunkResourcePathArr.push(...buildResourcePathFromSegments(result)); chunkObjectKeyArr.push(result.object_key); chunkPreviewKeyArr.push(result.preview_key); chunkPopupKeyArr.push(result.popup_key); chunkThumbnailKeyArr.push(result.thumbnail_key); } let deleteTargetParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, resourcePathArr: JSON.stringify(chunkResourcePathArr), dataIdArr: JSON.stringify(chunkDataIdArr), objectKeyArr: JSON.stringify(chunkObjectKeyArr), previewKeyArr: JSON.stringify(chunkPreviewKeyArr), popupKeyArr: JSON.stringify(chunkPopupKeyArr), thumbnailKeyArr: JSON.stringify(chunkThumbnailKeyArr), dataType: toggleParams.dataType, isRecycleBinModal: isRecycleBinModal }; if (progressCount < progressMax) { // 프로그레스 표시 let progressData = {}; progressData.title = '파일 삭제'; progressData.warn = false; progressData.count = progressMax; progressData.idx = progressCount; toggleFileProgress(true, progressData); } let deleteTargetRes = await axios.post(`${vars.path_name}/deleteTarget`, deleteTargetParams); if (deleteTargetRes.data.message == 'deleteTarget_success') { // console.log('deleteTarget_success'); progressCount += currentCount; // if (progressCount < progressMax) { // // 프로그레스 표시 // let progressData = {}; // progressData.title = '파일 삭제'; // progressData.warn = false; // progressData.count = progressMax; // progressData.idx = progressCount; // toggleFileProgress(true, progressData); // } else { // console.log('================== 삭제 완료'); // // 프로그레스 종료 // toggleFileProgress(false); // } if (progressCount >= progressMax) { console.log('================== 삭제 완료'); // 프로그레스 종료 toggleFileProgress(false); toggleContextmenu(false); // resetViewer(); } } if (deleteTargetRes.data.message == 'deleteTarget_failed_permission') { toggleFileProgress(false); let params = { text: `${toggleParams.title} 실패 : 권한이 부족하여 작업이 실패했습니다.`, type: 'alertModal' }; toggleModal(true, params); } } } } export async function setDataPermission(resourcePath, targetId) { let dataType, dataTypeKor; if (vars.lastContextTarget.matches('.folder')) dataType = 'folder', dataTypeKor = '폴더'; if (vars.lastContextTarget.matches('.file')) dataType = 'file', dataTypeKor = '파일'; // ** 권한 관련 (클래스에는 포함 안될 예정) // targetId : lev_1, lev_2, lev_4, lev_8, lev_0 let targetPermission = Number(targetId.split('_')[1]); if (getDepth(resourcePath) > 1) { // depth2, depth3 폴더 권한 설정 시 타겟폴더의 권한이 부모폴더의 권한보다 낮으면 안내모달 표시 후 리턴 let parentPath = splitTopPathAndTargetName(resourcePath).topPath; let parentPermission = getDataFromTreeObject(parentPath, 'folder').data.permission; let compareTargetPermission = (targetPermission == 0) ? 99999 : targetPermission; let compareParentPermission = (parentPermission == 0) ? 99999 : parentPermission; if (compareParentPermission > compareTargetPermission) { let toggleParams = { text: '상위 폴더보다 낮은 권한으로는 설정할 수 없습니다.', type: 'alertModal' }; toggleModal(true, toggleParams); return; } } let beforePermission = getDataFromTreeObject(resourcePath, 'folder').data.permission; let setDataPermissionParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, resourcePath: resourcePath, dataId: vars.lastContextTarget.dataset.id, dataType: dataType, newPermission: targetPermission, beforePermission: beforePermission } let setDataPermissionRes = await axios.post(`${vars.path_name}/setDataPermission`, { params: setDataPermissionParams }); if (setDataPermissionRes.data.message == 'setDataPermission_success') { console.log('setDataPermission_success'); } } export async function convertPdf(resourcePath, dataId) { let getDataInfoParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, dataIdArr: [dataId], isRemoved: false, debug: "'convertPdf'에서 실행" } let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams } ); if (getDataInfoRes.data.message == 'getDataInfo_success') { let objectKey = getDataInfoRes.data.result.object_key; let dataId = getDataInfoRes.data.result.data_id; let convertPdfParams = { dataId: dataId, resourcePath: resourcePath, depth1: extractPathByLength(resourcePath, 1), depth2: extractPathByLength(resourcePath, 2), depth3: extractPathByLength(resourcePath, 3), userInfoString: vars.userInfoString, objectKey: objectKey, storageType: vars.storageType }; await axios.post(`${vars.path_name}/convertPdf`, { params: convertPdfParams }); } } //////// uploadData, downloadTarget 할 때 전체 화면 프로그레스 토글 export function toggleFileProgress(state, progressData) { let progress = document.querySelector('.file-progress'); let title = document.querySelector('.file-progress .title .title-wrap span'); let warn = document.querySelector('.file-progress .title .warn'); let fileName = document.querySelector('.file-progress .text .file-name span'); let count = document.querySelector('.file-progress .text .count span'); let size = document.querySelector('.file-progress .size span'); let percentage = document.querySelector('.file-progress .percentage span'); warn.style.display = 'none'; fileName.style.display = 'none'; count.style.display = 'none'; size.textContent = ' '; percentage.textContent = ' '; if (state == false) progress.style.display = 'none'; if (state == true) { progress.style.display = 'flex'; if (progressData) { fileName.style.display = 'flex'; count.style.display = 'flex'; title.textContent = progressData.title; if (progressData.warn) { warn.style.display = 'flex'; } else { warn.style.display = 'none'; } fileName.textContent = progressData.fileName; // count.textContent = `${progressData.count}개 중 ${progressData.idx}번째`; count.textContent = `${progressData.idx} / ${progressData.count}`; if (progressData.loadedSize && progressData.totalSize) { size.textContent = `${formatBytes(progressData.loadedSize)} / ${formatBytes(progressData.totalSize)}`; } else { size.textContent = ' '; } if (progressData.percentage) { percentage.textContent = `${progressData.percentage.toFixed(2)} %`; } else { percentage.textContent = ' '; } } else { title.textContent = '파일 업로드 준비중'; } } } // 초기화면 템플릿 파일 다운로드 export async function downloadTempFile() { let url = `https://api.digitalarchive.work/%EC%B4%88%EA%B8%B0%ED%99%94%EB%A9%B4%ED%85%9C%ED%94%8C%EB%A6%BF_v2.pptx`; // let url = `https://api.digitalarchive.work/%EC%B4%88%EA%B8%B0%ED%99%94%EB%A9%B4%ED%85%9C%ED%94%8C%EB%A6%BF_v1.pptx`; const a = document.createElement('a'); a.href = url; a.download = `초기화면템플릿_v1.pptx`; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); await sleep(300); } // MAIN_TITLE_IMAGE 업로드 export async function uploadMainTitleImage(files, uploadType) { let file = files[0]; let totalSize = 0; totalSize += file.size; // 여유 공간 초과 시 제한 if (totalSize > vars.remainingSize) { let toggleParams = { text: '여유 공간이 부족합니다.', type: 'alertModal' }; toggleModal(true, toggleParams); fileArr = []; return; } // 업로드 크기 초과 시 제한 let sizeLimit = 20 * 1024 * 1024 * 1024; if (totalSize > sizeLimit) { let toggleParams = { text: '한 번에 업로드할 수 있는 최대 용량은 20 GB 입니다.', type: 'alertMode' }; toggleModal(true, toggleParams); fileArr = []; return; } let dateArr = [], resourcePathArr = [], sizeArr = [], objectKeyArr = []; let uploadFailedFileArr = [], uploadSuccessFileArr = []; let progressTitle = '파일 업로드'; let topPath = vars.lastHeaderBtn.dataset.resourcePath; let resourcePath = `${topPath}/${file.name}`; try { if(uploadType == 'fileInput2') { let param = { userInfoString: vars.userInfoString, resourcePath: resourcePath }; let res = await axios.post(`${vars.path_name}/deleteMainTitleImage`, {params: param}); } // 1) 서버에 presigend URL 요청 let generateUploadUrlParams = { date: Date.now(), resourcePath: resourcePath }; let generateUploadUrlRes = await axios.post(`${vars.path_name}/generateUploadUrl`, generateUploadUrlParams); if(generateUploadUrlRes.data.message == 'generateUploadUrl_success') { let { originUrl, objectKey, date} = generateUploadUrlRes.data.result; // 2) presigend URL로 직접 파일 PUT 전송 await axios.put(originUrl, file, { headers: { 'Content-Type': file.type || 'application/octet-stream' }, onUploadProgress: async progress => { // let progressData = {}; // let totalSize = progress.total; // let loadedSize = progress.loaded; // let percentage = (loadedSize / totalSize) * 100; // progressData.loadedSize = loadedSize; // progressData.totalSize = totalSize; // progressData.percentage = percentage; // if (percentage != 100) progressData.state = 'working'; // if (percentage == 100) progressData.state = 'finish'; // toggleFileProgress(true, progressData); } }); dateArr.push(date); resourcePathArr.push(resourcePath); sizeArr.push(file.size); objectKeyArr.push(objectKey); } }catch(err){ console.error(`❌ 업로드 실패 : ${resourcePath}`, err); uploadFailedFileArr.push(file.name); } // 이미지 파일 db 및 로그 등록 let uploadData_titleImgParams = { userInfoString: vars.userInfoString, storageType: vars.storageType, dateArr: JSON.stringify(dateArr), resourcePathArr: JSON.stringify(resourcePathArr), sizeArr: JSON.stringify(sizeArr), objectKeyArr: JSON.stringify(objectKeyArr), dataType: 'file', activity: 'uploadData_file' } let uploadData_titleImgRes = await axios.post(`${vars.path_name}/uploadData_titleImg`, { params: uploadData_titleImgParams}); if(uploadData_titleImgRes.data.message == 'uploadData_success') { // url 받아서 가시화 let generateDownloadUrlParams = { objectKey: objectKeyArr[0], resourcePath: resourcePath }; let generateDownloadUrlRes = await axios.post(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams); let imageUrl = generateDownloadUrlRes.data.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'; } } // AI 버튼 상태 업데이트 공통 함수 export async function updateAiButtonState(dataId, state, btnType) { // 특정 dataId에 해당하는 컨테이너만 찾기 const container = document.querySelector(`.viewer-container[data-data-id="${dataId}"]`); if(!container) return; // const aiBtn = container.querySelector('.api-btn'); let aiBtn; if(btnType == 'gemini') { aiBtn = container.querySelector('.ai-btn'); }else { aiBtn = container.querySelector('.api-btn'); } if(!aiBtn) return; let aiStart = aiBtn.querySelector('.ai-start'); let aiLoading = aiBtn.querySelector('.ai-loading'); if (state === 'loading') { aiStart.style.display = 'none'; aiLoading.style.display = 'flex'; aiLoading.style.height = '100%'; aiBtn.style.pointerEvents = 'none'; } else if (state === 'initial') { aiStart.style.display = 'flex'; aiLoading.style.display = 'none'; aiBtn.style.pointerEvents = 'auto'; } }