초기 PM 소스 전체 업로드

This commit is contained in:
koj729
2026-06-12 17:14:03 +09:00
commit 4e33c9a02a
1769 changed files with 377797 additions and 0 deletions
@@ -0,0 +1,11 @@
{
"title": "DocumentSummary",
"type": "object",
"properties": {
"파일제목": { "type": "string" },
"파일내용요약": { "type": "string" }
},
"required": [
"파일제목", "파일내용요약"
]
}
@@ -0,0 +1,7 @@
오타나 줄바꿈 오류가 있을 수 있으니 의미를 유추하여 정확한 정보를 추출해주세요.
정확성이 매우 중요하므로 반드시 파일에 포함된 텍스트만 사용하여 작성해주세요. 추론하거나 임의로 보충하지 마세요.
불필요한 설명 문구(예: "다음은 문서에 작성된 내용입니다", "문서 기반으로 요약해 드립니다" 등)와 특수문자(*등)는 포함하지 마세요.
한 문장이 끝나면 다음줄에서 문장을 시작해주세요. 가독성 좋게 표출해주세요.
모든 문서는 꼭 개조식으로 정리해서 표출해주세요.
1. 파일 내용 요약: 파일 내용을 요약해주세요. 반드시 한글로 작성합니다.
@@ -0,0 +1,5 @@
project_name 값을 실제 프로젝트명으로 사용하고, "project_name"이라는 단어를 그대로 쓰지 마.
project_name 값이 null 이면 'err: project name is null' 이라는 단어가 출력되게 해줘
project_name에 대한 온라인 반응을 최신 보도와 공식 문서 중심으로 정리해줘. 추측은 금지야 .
아래 예제 형식처럼 글만 나오게 해줘. '결과입니다' 같은 불필요한 말은 하지마.
글 정리는 개조식으로 진행해줘.
+503
View File
@@ -0,0 +1,503 @@
import { vars } from './variable.js';
// 파일 사이즈 변환
export function formatBytes(bytes) {
const abs = Math.abs(bytes);
let size, unit;
if (abs < 1024) {
size = addCommasToNumber(bytes);
unit = ' Bytes';
} else if (abs < 1048576) {
size = addCommasToNumber((bytes / 1024).toFixed(2));
unit = ' KB';
} else if (abs < 1073741824) {
size = addCommasToNumber((bytes / 1048576).toFixed(2));
unit = ' MB';
} else if (abs < 1099511627776) {
size = addCommasToNumber((bytes / 1073741824).toFixed(2));
unit = ' GB';
} else {
size = addCommasToNumber((bytes / 1099511627776).toFixed(2));
unit = ' TB';
}
if (size != 0) {
let decimalPointNum = size.split('.')[1];
if (decimalPointNum) {
// 소수점 아래 숫자가 00인 경우
if (decimalPointNum == '00') size = size.split('.')[0];
// 소수점 아래 두번째 숫자가 0인 경우
else if (decimalPointNum[1] == '0') size = `${size.split('.')[0]}.${decimalPointNum[0]}`;
}
}
return size + unit;
}
// 1000 단위 콤마 추가
// export function addCommasToNumber(num) {
// return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
// }
export function addCommasToNumber(num) {
const isNegative = Number(num) < 0;
const parts = Math.abs(Number(num)).toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
const formatted = parts.join('.');
return isNegative ? `-${formatted}` : formatted;
}
export function getDepth(path) {
path = path.startsWith('/') ? path.slice(1) : path;
let pathSplit = path.split('/');
let result = pathSplit.length;
return result;
}
export function updatePartialPath(sourcePath, targetPath) {
let sourcePathSplit = sourcePath.split('/');
let targetPathSplit = targetPath.split('/');
for (let i = 0; i < sourcePathSplit.length; i++) {
targetPathSplit[i] = sourcePathSplit[i];
}
return targetPathSplit.join('/');
}
// 파일명, 확장자 분리
export function splitBaseAndExt(target, isLowerExt) {
let targetSplit = target.split('.');
let ext = targetSplit.pop();
if (isLowerExt) ext = ext.toLowerCase();
let base = targetSplit.join('.');
return { ext: ext, base: base }
}
// 선택한 대상이 포함된 상위 폴더 경로 추출
export function splitTopPathAndTargetName(resourcePath) {
let resourcePathSplit = resourcePath.split('/');
let targetName = resourcePathSplit.pop();
let topPath = resourcePathSplit.join('/');
if (!topPath || topPath == '' || topPath == null) topPath = '/';
return { targetName: targetName, topPath: topPath };
}
export function extractPathByLength(target, count) {
if (!Array.isArray(target)) {
target = target.split('/');
}
const parts = target.filter(Boolean).slice(0, count);
return '/' + parts.join('/');
}
// 특수문자를 체크하는 정규표현식
export function hasSpecialChar(target) {
// const pattern = /[\/\\;:?`'"<>|#%&*]/;
const pattern = /[\/\\:*?'"`<>|#]/;
return pattern.test(target);
}
// 동일한 폴더명/파일명 존재 여부 확인
export function existEqualName(type, comparisonPath, targetName) {
let result;
if (comparisonPath != '' && comparisonPath != undefined) {
result = getTypeDataFromFolderData(type, comparisonPath)[targetName];
} else {
// result = vars.currentTreeObject[type][targetName];
result = vars.allTreeObject[type][targetName];
}
// 헤더 버튼 이름 변경 시 페이지 버튼과 이름 같은 경우 변경 불가
let pageBtnNameArr = ['과업개요', '공문'];
if (comparisonPath == '/' && result == undefined) result = pageBtnNameArr.includes(targetName);
return result;
// folderData에서 타입(폴더/파일)과 경로를 사용해서 해당 경로의 타입별 데이터 가져오기
function getTypeDataFromFolderData(type, path) {
let data;
if (type == 'folder') data = vars.allTreeObject[type];
if (type == 'file') data = vars.currentTreeObject[type];
let pathSplit = path.split('/');
pathSplit.shift();
// if (pathSplit[0] == '') return data;
for (let i = 0; i < pathSplit.length; i++) {
if (data[pathSplit[i]]) {
if (type == 'folder') data = data[pathSplit[i]]['child'][type];
if (type == 'file') data = data;
} else {
data = data;
}
}
return data;
}
}
export function getNextFileName(nameArr, newName) {
const dotIdx = newName.lastIndexOf('.');
const base = dotIdx !== -1 ? newName.slice(0, dotIdx) : newName;
const ext = dotIdx !== -1 ? newName.slice(dotIdx) : '';
const set = new Set(nameArr);
let maxNum = 0;
const regex = new RegExp(`^${escapeRegExp(base)}(?: \\((\\d+)\\))?${escapeRegExp(ext)}$`);
for (const name of set) {
const match = name.match(regex);
if (match) {
const num = match[1] ? parseInt(match[1], 10) : 0;
if (num > maxNum) maxNum = num;
}
}
if (maxNum === 0 && !set.has(newName)) return newName;
return `${base} (${maxNum + 1})${ext}`;
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}
export function getDataFromTreeObject(path, type, data) {
let parentData;
if (!data) data = vars.allTreeObject;
let pathSplit = path.split('/');
pathSplit.shift();
for (let i = 0; i < pathSplit.length; i++) {
if (i != pathSplit.length - 1) {
if (data['folder'][pathSplit[i]]) {
data = data['folder'][pathSplit[i]]['child'];
}
} else {
if (type == 'folder') {
parentData = data;
data = data['folder'][pathSplit[i]];
}
if (type == 'file') {
if (data['file'][pathSplit[i]]) {
parentData = data;
data = data['file'][pathSplit[i]];
}
}
}
}
return { parentData: parentData, data: data };
}
export function getCurrentTreeObject(path) {
let data = vars.allTreeObject;
let pathSplit = path.split('/');
pathSplit.shift();
for (let i = 0; i < pathSplit.length; i++) {
if (i != pathSplit.length - 1) {
if (data['folder'][pathSplit[i]]) {
data = data['folder'][pathSplit[i]]['child'];
}
} else {
if (data['folder'][pathSplit[i]]) data = data['folder'][pathSplit[i]]['child'];
}
}
return data;
}
export function targetFocus(target, behavior = 'smooth') {
target.scrollIntoView({
behavior: behavior, // 부드럽게
block: 'center', // 화면 중앙에 위치하도록 (start, end, nearest 도 가능)
inline: 'center'
});
target.focus();
}
export function pxToRem(px) {
let remPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
return px / remPx;
}
export function buildResourcePathFromSegments(rows) {
if (!Array.isArray(rows)) rows = [rows];
let result = [];
rows.map(row => {
let resourcePath = '/';
let maxDepth = 8;
let arr = [];
for (let i = 0; i < maxDepth; i++) {
let num = i+1;
let segment = row[`path${num}`];
if (segment) arr.push(segment);
}
if (arr.length != 0) resourcePath += arr.join('/');
result.push(resourcePath);
})
return result;
}
export function headerBtnForceClick(headerBtn) {
headerBtn.click();
}
export function treeBtnForceClick(treeBtn) {
treeBtn.click();
}
export function closeInitProgress() {
document.querySelector('.init-progress').style.display = 'none';
}
export async function getMyDownloadList(){
let res = await axios.get(`${vars.path_name}/getMyDownloadList`, {params : {user_id : JSON.parse(vars.userInfoString).user_id}});
// console.log(res.data);
if(res.data.length > 0){
if(document.querySelector('.download-btn .download-reddot')){
document.querySelector('.download-btn .download-reddot').style.display = 'block';
}
}else{
if(document.querySelector('.download-btn .download-reddot')){
document.querySelector('.download-btn .download-reddot').style.display = 'none';
}
}
let ul = document.querySelector('.download-modal__body_list');
ul.innerHTML = '';
for(let i = 0; i < res.data.length; i++){
let li = document.createElement('li');
li.classList.add('download-modal__body_list_item');
let expireDate = new Date(res.data[i].expire_date);
let now = new Date();
let yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
let state = (expireDate < yesterday)? `<img class="--button__xsmall-icon --disable" src="/main/img/archive/icon__download--111.svg" alt="/" />`: (!res.data[i].made)?`준비중`:`<img class="--button__xsmall-icon downloadFolder__btn__icon" src="/main/img/archive/icon__download--111.svg" alt="/" />`
let expire =(state == '준비중')?state :(expireDate < yesterday)? '만료됨': formatDateTime(expireDate);
li.innerHTML = `
<img class="--icon" src="/main/img/header/header-folder-icon.svg" alt="/" />
<div class="download-modal__body_list_item_text">
<h4 class="download-modal__body_list_item_text_title">
${res.data[i].name}
</h4>
<p class="download-modal__body_list_item_text_subtitle">
${res.data[i].project_nm}${res.data[i].path}
</p>
</div>
<h6 class="download-modal__body_list_item_time">
${expire}
</h6>
<div class="download-modal__body_list_item_status">${state}</div>
`;
ul.appendChild(li);
li.querySelector('.downloadFolder__btn__icon')?.addEventListener('click', () => {
window.location.href = res.data[i].url;
})
}
function formatDateTime(date) {
const padZero = (num) => String(num).padStart(2, '0');
const year = date.getFullYear();
const month = padZero(date.getMonth() + 1); // getMonth()는 0부터 시작하므로 +1
const day = padZero(date.getDate());
const hours = padZero(date.getHours());
const minutes = padZero(date.getMinutes());
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
}
export async function openNewWindowViewer() {
let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex';
let lastListItem = (isRecycleBinModal) ? vars.lastListItem_bin : vars.lastListItem;
let resourcePath = lastListItem.dataset.resourcePath;
let dataId = lastListItem.dataset.id;
let isLowerExt = true, ext = splitBaseAndExt(resourcePath, isLowerExt).ext;
let thumbnail_key = getDataFromTreeObject(resourcePath, 'file', vars.currentTreeObject).data?.thumbnailKey;
//Presigned URL
let PresignedUrl = undefined;
let getDataInfoParams = {
userInfoString: vars.userInfoString,
storageType: vars.storageType,
dataIdArr: [dataId],
isRemoved: isRecycleBinModal,
debug: "'우측 미리보기 새 창으로 열기 버튼 클릭 이벤트'에서 실행"
}
let getDataInfoRes = await axios.post(`${vars.path_name}/getDataInfo`, { params: getDataInfoParams } );
if (getDataInfoRes.data.message == 'getDataInfo_success') {
let objectKey = getDataInfoRes.data.result.popup_key;
if(objectKey == undefined || objectKey == `` || objectKey == null){
return;
}
let generateDownloadUrlParams = {
objectKey: objectKey,
resourcePath: resourcePath
}
let generateDownloadUrlRes = await axios.post(`${vars.path_name}/generateDownloadUrl`, generateDownloadUrlParams);
if (generateDownloadUrlRes.data.message == 'generateDownloadUrl_success') {
PresignedUrl = generateDownloadUrlRes.data.url;
}
}
//Presigned URL end
thumbnail_key = encodeURIComponent(thumbnail_key);
resourcePath = encodeURIComponent(resourcePath);
let fullPath = encodeURIComponent(PresignedUrl);
// 뷰어 기본 width값은 현재 화면 width값의 절반
let denominator = 2;
// url 뷰어는 현재 화면 width값과 동일하게 설정
if (ext == 'url') denominator = 1;
let width = screen.width / denominator;
let height = screen.height;
let left = screen.width * 2 + 10;
let open_ext = `pdf`;
switch(ext){
case 'pdf' :
case 'hwp' :
case 'hwpx' :
case 'xls' :
case 'xlsm' :
case 'ppt' :
case 'pptx' :
case 'doc' :
case 'docx' :
case 'dwg' :
case 'dxf' :
case 'grm' :
open_ext = 'pdf';
break
case 'gsim' :
open_ext = 'gsim';
break
case 'ifc' :
open_ext = 'ifc';
break
case 'png' :
case 'jpg' :
case 'jpeg' :
case 'webp' :
case 'gif' :
open_ext = 'png';
break
case 'mp4' :
case 'mov' :
case 'webm' :
open_ext = 'mp4';
break
case 'log' :
case 'txt' :
case 'md' :
open_ext = 'txt';
break
case 'url' :
open_ext = 'url';
break;
case 'zip' :
open_ext = 'zip';
break
case 'glb':
case 'gltf':
case 'obj':
case 'stl':
case 'fbx':
case '3dm':
open_ext = 'glb';
break;
case 'html':
open_ext = 'html';
break;
}
//presigned url은 ext를 읽을수 없엉...
const jsonData = JSON.stringify({$ext : open_ext});
const encodedData = btoa(jsonData);
// 3d모델뷰어 썸네일 생성을 위해 dataId, path_name 추가
// let popup = window.open(`/popup?path=${fullPath}&data=${encodedData}`, '', `width=${width}, height=${height}, left=${left},`);
let popup = window.open(`/popup?thumbnail_key=${thumbnail_key}&path_name=${vars.path_name}&resourcePath=${resourcePath}&dataId=${dataId}&path=${fullPath}&data=${encodedData}`, '', `width=${width}, height=${height}, left=${left},`);
}
export function initFileAreaUI(fileAreaMode) {
let listContainer = document.querySelector('.archive-main .archive-main-center .list-container');
let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex';
if (isRecycleBinModal) listContainer = document.querySelector('.recycle-bin-modal .recycle-bin-wrap .list-container');
let listHeader = listContainer.querySelector('.list-header');
let listBody = listContainer.querySelector('.list-body');
let listBodyItemWrap = listBody.querySelector('.list-item-wrap');
let mapContainer = listBody.querySelector('.map-container');
let archiveMainRight = document.querySelector('.archive-main-right');
let viewerHeader = archiveMainRight.querySelector('.viewer-header');
if (fileAreaMode == 'map') {
listHeader.style.display = 'none';
listBody.style.height = '100%';
listBody.style.overflowY = 'hidden';
listBodyItemWrap.style.display = 'none';
if (mapContainer) mapContainer.style.display = 'flex';
archiveMainRight.style.minWidth = '36rem';
archiveMainRight.style.maxWidth = '36rem';
archiveMainRight.style.height = 'calc(100% - 2.75rem)';
archiveMainRight.style.position = 'absolute';
archiveMainRight.style.right = '0.625rem';
archiveMainRight.style.top = '0.5rem';
archiveMainRight.style.background = '#fffa';
archiveMainRight.style.backdropFilter = 'blur(0.2rem)';
archiveMainRight.style.webkitBackdropFilter = 'blur(0.2rem)';
archiveMainRight.style.borderRadius = '0.25rem';
archiveMainRight.style.boxShadow = '#ccc 0 0 0rem 0.0625rem, #0c0c0db3 0rem 0.5rem 1rem -0.25rem';
viewerHeader.style.borderRadius = '0.25rem 0.25rem 0 0';
} else {
listHeader.style.display = 'flex';
listBody.style.height = 'calc(100% - 2.25rem)';
listBody.style.overflowY = 'scroll';
listBodyItemWrap.style.display = 'flex';
if (mapContainer) mapContainer.style.display = 'none';
archiveMainRight.style.minWidth = '41rem';
archiveMainRight.style.maxWidth = '41rem';
archiveMainRight.style.height = '100%';
archiveMainRight.style.position = 'relative';
archiveMainRight.style.right = 'unset';
archiveMainRight.style.top = 'unset';
archiveMainRight.style.background = '#fff';
archiveMainRight.style.backdropFilter = 'none';
archiveMainRight.style.webkitBackdropFilter = 'none';
archiveMainRight.style.borderRadius = 'unset';
archiveMainRight.style.boxShadow = 'none';
viewerHeader.style.borderRadius = 'unset';
}
}
export function syncGroupStyle() {
let listContainer = document.querySelector('.archive-main .archive-main-center .list-container');
let isRecycleBinModal = document.querySelector('.recycle-bin-modal').style.display == 'flex';
if (isRecycleBinModal) listContainer = document.querySelector('.recycle-bin-modal .recycle-bin-wrap .list-container');
listContainer.querySelectorAll('.main-list-item').forEach(item => {
if (listContainer.querySelectorAll(`.list-item.selected[data-main-file-name="${item.dataset.mainFileName}"]`).length > 0) {
listContainer.querySelectorAll(`.list-item[data-main-file-name="${item.dataset.mainFileName}"]`).forEach(item => {
item.classList.add('group-style');
item.classList.remove('non-selected');
})
} else {
listContainer.querySelectorAll(`.list-item[data-main-file-name="${item.dataset.mainFileName}"]`).forEach(item => {
item.classList.remove('group-style');
})
}
})
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+348
View File
@@ -0,0 +1,348 @@
import { vars } from './variable.js';
import {
targetFocus,
syncGroupStyle
} from './common.js';
import {
resetViewer,
} from './pageRenderer.js';
let archiveMain = document.querySelector('.archive-main');
let viewerContainer = archiveMain?.querySelector('.archive-main-right .viewer-container');
addFileDragEvent(document.querySelector('.archive-main-center .list-container .list-body'), 'file-area');
addFileDragEvent(document.querySelector('.recycle-bin-modal .list-container .list-body'), 'recycle-bin');
export function addFileDragEvent(listBody, target) {
let draggingStartTarget, draggingEndTarget;
let draggingStart = false, isDragging = false;
let isCtrl = false, isShift = false;
let processedItems = new Set();
let originalSelection = new Set();
let multiSelectBox = document.createElement('div');
multiSelectBox.style.position = 'absolute';
multiSelectBox.style.border = '1px solid rgba(0, 120, 215, 1)';
multiSelectBox.style.background = 'rgba(0, 120, 215, 0.2)';
multiSelectBox.style.zIndex = '9999';
multiSelectBox.style.pointerEvents = 'none';
multiSelectBox.style.display = 'none';
listBody.appendChild(multiSelectBox);
vars.preventNextClick = false;
let startX = 0;
let startY = 0;
let prevClientX = 0;
let prevClientY = 0;
let autoScrollDirection = null;
let autoScrollFrameId = null;
const scrollSpeed = 8;
const scrollMargin = 36;
function autoScrollLoop() {
if (!autoScrollDirection) return;
// 더 작으면 느리고 부드러움
if (autoScrollDirection === 'down') {
listBody.scrollTop += scrollSpeed;
} else if (autoScrollDirection === 'up') {
listBody.scrollTop -= scrollSpeed;
}
autoScrollFrameId = requestAnimationFrame(autoScrollLoop);
}
listBody.addEventListener('pointerdown', (e) => {
// 마우스 좌클릭이 아닌 경우 리턴
if (e.button !== 0) return;
// 갤러리 폴더 지도화면인 경우 리턴
if (target == 'file-area' && listBody.querySelector('.map-container').style.display === 'flex') return;
draggingStartTarget = e.target;
draggingStart = true;
isDragging = false;
isCtrl = e.ctrlKey;
isShift = e.shiftKey;
processedItems.clear();
if (target == 'file-area') originalSelection = new Set(vars.multiSelectListItemArr);
if (target == 'recycle-bin') originalSelection = new Set(vars.multiSelectListItemArr_bin);
vars.preventNextClick = false;
// if (isCtrl) {
// vars.lastListItem = e.target.closest('.list-item');
// } else {
// listBody.querySelectorAll('.list-item').forEach(listItem => {
// listItem.classList.remove('selected');
// listItem.classList.remove('group-style');
// })
// vars.multiSelectListItemArr = [];
// }
const dragStartListItem = e.target.closest('.list-item');
if (dragStartListItem && !isShift) {
if (target == 'file-area') vars.lastListItem = dragStartListItem;
if (target == 'recycle-bin') vars.lastListItem_bin = dragStartListItem;
}
if (!isCtrl) {
// Ctrl이 아닐 경우 기존 선택 모두 해제
listBody.querySelectorAll('.list-item').forEach(listItem => {
listItem.classList.remove('selected');
listItem.classList.remove('group-style');
});
if (target == 'file-area') vars.multiSelectListItemArr = [];
if (target == 'recycle-bin') vars.multiSelectListItemArr_bin = [];
}
const bodyRect = listBody.getBoundingClientRect();
startX = e.clientX + listBody.scrollLeft - bodyRect.left;
startY = e.clientY + listBody.scrollTop - bodyRect.top;
prevClientX = e.clientX;
prevClientY = e.clientY;
Object.assign(multiSelectBox.style, {
left: `${startX}px`,
top: `${startY}px`,
width: `0px`,
height: `0px`,
display: 'none'
});
});
listBody.addEventListener('pointermove', (e) => {
// 마우스 좌클릭이 아닌 경우 리턴
if (e.buttons !== 1) return;
// list-body에서 마우스 드래그가 시작된게 아닌 경우 리턴
if (!draggingStart) return;
if (e.clientX === prevClientX && e.clientY === prevClientY) return;
prevClientX = e.clientX;
prevClientY = e.clientY;
const listBodyRect = listBody.getBoundingClientRect();
const currX = e.clientX + listBody.scrollLeft - listBodyRect.left;
const currY = e.clientY + listBody.scrollTop - listBodyRect.top;
const dx = currX - startX;
const dy = currY - startY;
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
isDragging = true;
vars.preventNextClick = true;
const left = Math.min(startX, currX);
const top = Math.min(startY, currY);
const width = Math.abs(dx);
const height = Math.abs(dy);
Object.assign(multiSelectBox.style, {
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
display: 'block'
});
const boxRect = multiSelectBox.getBoundingClientRect();
listBody.querySelectorAll('.list-item .wrap').forEach(elem => {
const rect = elem.getBoundingClientRect();
const overlaps = !(
rect.right < boxRect.left ||
rect.left > boxRect.right ||
rect.bottom < boxRect.top ||
rect.top > boxRect.bottom
);
let listItem = elem.parentElement;
if (isCtrl) {
if (overlaps && !processedItems.has(listItem)) {
processedItems.add(listItem);
// ✅ 토글: 원래 선택되어 있었으면 해제, 아니면 선택
if (originalSelection.has(listItem)) {
listItem.classList.remove('selected');
listItem.classList.remove('group-style');
if (target == 'file-area') vars.multiSelectListItemArr = vars.multiSelectListItemArr.filter(item => item !== listItem);
if (target == 'recycle-bin') vars.multiSelectListItemArr_bin = vars.multiSelectListItemArr_bin.filter(item => item !== listItem);
} else {
listItem.classList.add('selected');
listItem.classList.add('group-style');
if (target == 'file-area') vars.multiSelectListItemArr.push(listItem);
if (target == 'recycle-bin') vars.multiSelectListItemArr_bin.push(listItem);
}
}
// ✅ 박스에서 벗어난 항목 복원
if (!overlaps && processedItems.has(listItem)) {
processedItems.delete(listItem);
if (originalSelection.has(listItem)) {
listItem.classList.add('selected');
listItem.classList.add('group-style');
if (target == 'file-area') {
if (!vars.multiSelectListItemArr.includes(listItem)) vars.multiSelectListItemArr.push(listItem);
}
if (target == 'recycle-bin') {
if (!vars.multiSelectListItemArr_bin.includes(listItem)) vars.multiSelectListItemArr_bin.push(listItem);
}
} else {
listItem.classList.remove('selected');
listItem.classList.remove('group-style');
if (target == 'file-area') vars.multiSelectListItemArr = vars.multiSelectListItemArr.filter(item => item !== listItem);
if (target == 'recycle-bin') vars.multiSelectListItemArr_bin = vars.multiSelectListItemArr_bin.filter(item => item !== listItem);
}
}
} else {
listItem.classList.toggle('selected', overlaps);
listItem.classList.toggle('group-style', overlaps);
if (target == 'file-area') {
if (overlaps && !vars.multiSelectListItemArr.includes(listItem)) vars.multiSelectListItemArr.push(listItem);
}
if (target == 'recycle-bin') {
if (overlaps && !vars.multiSelectListItemArr_bin.includes(listItem)) vars.multiSelectListItemArr_bin.push(listItem);
}
}
});
if (e.clientY > listBodyRect.bottom - scrollMargin) {
if (autoScrollDirection !== 'down') {
cancelAnimationFrame(autoScrollFrameId);
autoScrollDirection = 'down';
autoScrollLoop();
}
} else if (e.clientY < listBodyRect.top + scrollMargin) {
if (autoScrollDirection !== 'up') {
cancelAnimationFrame(autoScrollFrameId);
autoScrollDirection = 'up';
autoScrollLoop();
}
} else {
if (autoScrollDirection !== null) {
cancelAnimationFrame(autoScrollFrameId);
autoScrollDirection = null;
}
}
}
});
document.addEventListener('pointerup', async (e) => {
// list-body에서 마우스 드래그가 시작된게 아닌 경우 리턴
if (!draggingStart) return;
if (target == 'file-area') {
if (vars.lastHeaderBtn) targetFocus(vars.lastHeaderBtn);
if (vars.lastMainTreeItem) targetFocus(vars.lastMainTreeItem);
}
if (isShift && isDragging) {
const dragStartListItem = draggingStartTarget.closest('.list-item');
if (dragStartListItem) {
if (target == 'file-area') vars.lastListItem = dragStartListItem;
if (target == 'recycle-bin') vars.lastListItem_bin = dragStartListItem;
}
}
multiSelectBox.style.display = 'none';
draggingEndTarget = e.target;
draggingStart = false;
isDragging = false;
isCtrl = false;
isShift = false;
// 드래그가 아이템 바깥 영역에서만 진행되고, 드래그에 포함된 아이템이 하나도 없을 때 뷰어 초기화
if (
(
(draggingStartTarget.matches('.list-body') && draggingEndTarget.matches('.list-body'))
|| (draggingStartTarget.matches('.list-item-wrap') && draggingEndTarget.matches('.list-item-wrap'))
)
&&
(
(target == 'file-area' && vars.multiSelectListItemArr.length == 0)
|| (target == 'recycle-bin' && vars.multiSelectListItemArr_bin.length == 0)
)
)
{
// 모든 아이템에서 non-selected 클래스 삭제
document.querySelectorAll('.grid-item').forEach(gridItem => {
gridItem.classList.remove('non-selected')
})
// if (viewerContainer.style.display == 'flex') resetViewer();
resetViewer();
vars.lastContextTarget = undefined;
if (target == 'file-area') {
vars.lastListGroupTarget = undefined;
vars.lastListItem = undefined;
// vars.lastContextTarget = undefined;
}
if (target == 'recycle-bin') {
vars.lastListGroupTarget_bin = undefined;
vars.lastListItem_bin = undefined;
// vars.lastContextTarget_bin = undefined;
}
// userSelected 삭제
let me = JSON.parse(vars.userInfoString);
me.selected = undefined;
vars.socket.emit('fileSelect',{me : me});
} else {
if (target == 'file-area') {
// 모든 아이템에 non-selected 클래스 추가
document.querySelectorAll('.grid-item').forEach(gridItem => {
gridItem.classList.add('non-selected')
})
}
}
// 드래그 시작한 대상과 종료된 대상이 다르면 뷰어 초기화
if (draggingStartTarget != draggingEndTarget) {
if (target == 'file-area') {
vars.lastListItem = undefined;
vars.lastListGroupTarget = undefined;
}
if (target == 'recycle-bin') {
vars.lastListItem_bin = undefined;
vars.lastListGroupTarget_bin = undefined;
}
if (viewerContainer.style.display == 'flex') resetViewer();
}
// 드래그가 끝나고 pointerup 이벤트가 발생하는 시점에 selected 클래스가 있는 listItem만 필터링
if (target == 'file-area') vars.multiSelectListItemArr = vars.multiSelectListItemArr.filter(item => item.classList.contains('selected'));
if (target == 'recycle-bin') vars.multiSelectListItemArr_bin = vars.multiSelectListItemArr_bin.filter(item => item.classList.contains('selected'));
// vars.multiSelectListItemArr/vars.multiSelectListItemArr_bin에 포함된 아이템에서 non-selected 클래스 삭제
if (target == 'file-area') {
vars.multiSelectListItemArr.forEach(item => {
item.classList.remove('non-selected');
})
}
if (target == 'recycle-bin') {
vars.multiSelectListItemArr_bin.forEach(item => {
item.classList.remove('non-selected');
})
}
await syncGroupStyle();
// 드래그 시작한 대상의 부모요소와 종료된 대상의 부모요소가 다르면 뷰어 초기화
// if (draggingStartTarget.parentElement != draggingEndTarget.parentElement) {
// if (viewerContainer.style.display == 'flex') resetViewer();
// }
if (target == 'file-area') vars.lastSelectTarget = vars.multiSelectListItemArr[0];
if (target == 'recycle-bin') vars.lastSelectTarget_bin = vars.multiSelectListItemArr_bin[0];
cancelAnimationFrame(autoScrollFrameId);
autoScrollDirection = null;
});
}
+59
View File
@@ -0,0 +1,59 @@
import { vars } from './variable.js';
import * as socketManager from './socketManager.js';
import * as eventManager from './eventManager.js';
import * as fileDrag from './fileDrag.js';
import {
preparePageRendering,
renderSizeBar,
renderLog
} from './pageRenderer.js';
export let isLoaded = false;
export function loadArchive() {
isLoaded = true;
}
//// 뷰어 요약 ai버튼 삭제
// document.querySelector('.ai-btn').remove();
//// archive controller에 useEncrypt 변수 설정
// let setUseEncryptRes = await axios.put(`${vars.path_name}/setUseEncrypt`, {useEncrypt: vars.convertOption.useEncrypt});
let pageRanderingOption = {
isInit: true,
// favorite: 즐겨찾기 한 경로
scope: 'headerBtn'
}
await preparePageRendering(pageRanderingOption);
//// 폴더 사용 용량 표시
await renderSizeBar();
//// 활동 정보
let isInit = true;
await renderLog(isInit);
// ol.map = undefined;
// ol.map = new ol.Map({
// target: 'map-container',
// layers: [vars.baseLayer],
// // layers: [vars.roadLayer, vars.satelliteLayer, vars.hybridLayer],
// view: new ol.View({
// projection: 'EPSG:3857',
// center: ol.proj.fromLonLat([127.8, 35.9]),
// zoom: 7,
// constrainResolution: true,
// smoothResolutionConstraint: true
// })
// });
// 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);
+241
View File
@@ -0,0 +1,241 @@
import { vars } from './variable.js';
import { renderLog } from './pageRenderer.js';
const modal = document.querySelector('.log-filter');
const customDisplay = modal.querySelector('.custom-select-display');
const customList = modal.querySelector('.custom-select-list');
const userSelect = document.querySelector('#log-user-select');
// 기본 로그필터 정보 불러오기
export async function getDefaultFilter() {
// 해당 프로젝트에 존재하는 모든 사용자
let userLogRes = await axios.get(`${vars.path_name}/getUserLog`);
if (userLogRes.data.message == 'getUserLog_success') {
const userLogDataArr = Object.values(userLogRes.data.logData);
// user_id -> user_nm 매핑 (중복 제거)
const userMap = new Map();
userLogDataArr.forEach(data => {
if(!userMap.has(data.user_id)) {
userMap.set(data.user_id, data.user_nm);
}
});
// selectbox 사용자 리스트 추가
userSelect.innerHTML = '';
userSelect.insertAdjacentHTML('beforeend', '<option value="allUser">모든 사용자</option>');
// Map -> Array로 변환 후 user_id로 정렬
const sortedUsers = [...userMap.entries()].sort((a, b) => {
const userIdA = a[0];
const userIdB = b[0];
return userIdA.localeCompare(userIdB, 'ko-KR');
});
// 정렬된 사용자 추가
sortedUsers.forEach(([userId, userNm]) => {
let option = document.createElement('option');
option.value = userId;
option.textContent = `${userId} (${userNm})`;
userSelect.appendChild(option);
});
// custom select 리스트에도 반영
customList.innerHTML = '';
Array.from(userSelect.options).forEach(opt => {
const li = document.createElement('li');
li.dataset.value = opt.value;
li.textContent = opt.textContent;
customList.appendChild(li);
});
// 기본값으로 설정
customDisplay.textContent = "모든 사용자";
userSelect.value = "allUser";
customList.style.display = "none"; // 혹시 열려있으면 닫기
}
// 활동유형 초기화
let activityCheckbox = modal.querySelectorAll('.body .log-activity label input[type="checkbox"]');
activityCheckbox.forEach(checkbox => {
checkbox.checked = true;
})
// 활동로그에 존재하는 '오늘 ~ 일주일' 간의 로그 기록
let logRes = await axios.get(`${vars.path_name}/getLog`);
if (logRes.data.message == 'getLog_success') {
const logDataArr = Object.values(logRes.data.logData);
// 기본 활동시간 - 오늘 ~ 일주일로 설정
const dateInputs = modal.querySelectorAll('.log-date .log-date-wrap input[type="date"]');
const toDateStr = (d) => d.toISOString().slice(0, 10);
const today = new Date();
const weekAgo = new Date(today)
weekAgo.setDate(today.getDate() - 7);
let startDate = toDateStr(weekAgo);
let endDate = toDateStr(today);
// 달력창 UI
dateInputs[0].value = startDate;
dateInputs[1].value = endDate;
return { logDataArr }
}
}
// custom select 동작
customDisplay.addEventListener('click', () => {
customList.style.display = customList.style.display === 'flex' ? 'none': 'flex';
});
// 항목 선택 시
customList.addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
const value = e.target.dataset.value;
const text = e.target.textContent;
// 표시 UI 업데이트
customDisplay.textContent = text;
// 실제 select 값도 변경
userSelect.value = value;
// 리스트 닫기
customList.style.display = 'none';
}
})
// 로그필터 저장 버튼 클릭 이벤트
modal.querySelector('.foot ._button-medium').addEventListener('click', async() => {
// date 불러오기
let { startDate, endDate } = getLogFilterDate();
// user 불러오기
let { user } = getLogFilterUser();
// activity 불러오기
let { activity } = getLogFilterActivity();
// 서버로 전달한 파라미터
const logFilterParams = {
startDate: startDate,
endDate: endDate,
user: user,
activity: activity
};
let filterLogRes = await axios.get(`${vars.path_name}/getFilterLog`, { params: logFilterParams });
if (filterLogRes.data.message == 'getFilterLog_success') {
const isInit = true;
const addFooterLog = false;
const archiveModalLogList = document.querySelector('.archive-modal > .wrap .modal-wrap .modal-body .log-wrap .log-item-wrap.log-body');
archiveModalLogList.scrollTop = 0;
renderLog(isInit, addFooterLog, filterLogRes.data.logData)
}
});
document.querySelector('.archive-modal').addEventListener('click', async(e) => {
if (modal.style.display == 'flex') {
// 로그 필터 켜져있을 때에만 이벤트 작동
if (e.target.classList.contains('custom-select-display') || e.target.classList.contains('custom-select-list') || e.target.closest('.custom-select-list')) {
// 사용자 셀렉트 박스 또는 옵션 영역 클릭했을 때
} else {
// 사용자 셀렉트 박스 또는 옵션 영역을 제외한 나머지 모달 영역 클릭했을 때
customList.style.display = 'none';
}
}
})
// date 불러오기
function getLogFilterDate() {
// 날짜 선택 (start, end)
const dateInputs = modal.querySelectorAll('.log-date .log-date-wrap input[type="date"]');
let startDate = dateInputs[0].value; // 시작일
let endDate = dateInputs[1].value; // 종료일
return { startDate, endDate };
}
// user 불러오기
function getLogFilterUser() {
// 선택한 사용자 user에 추가
const userSelect = modal.querySelector('.body .log-user select');
const selectValue = userSelect.value;
let user = [];
if(selectValue == 'allUser') {
// 전체 사용자 선택
user = Array.from(userSelect.options)
.map(opt => opt.value)
.filter(value => value !== 'allUser' && value !== '');
} else {
user.push(selectValue);
}
return { user };
}
// activity 불러오기
function getLogFilterActivity() {
// 체크한 활동유형 activity에 추가
const activityMapping = {
'uploadData_file' : ['uploadData_file', 'addOn_version', 'addOn_attachment'],
'renameTarget' : ['renameTarget_folder', 'renameTarget_file'],
'removeTarget' : ['removeTarget_folder', 'removeTarget_file', 'removeTarget_folder_expired'],
'downloadTarget': ['downloadTarget_folder', 'downloadTarget_file'],
'addPermission': ['addPermission_worker', 'addPermission_viewer', 'addPermission_subMaster', 'addPermission_securityWorker'],
'deletePermission': ['deletePermission_worker', 'deletePermission_viewer', 'deletePermission_subMaster', 'deletePermission_securityWorker']
}
const activity = Array.from(modal.querySelectorAll('.body .log-activity label input[type="checkbox"]:checked')).flatMap(check => activityMapping[check.value] || [check.value]);
return { activity };
}
// 초기화 / 전체해제 버튼 클릭 이벤트
const logFilterBtn = modal.querySelectorAll('._button-xsmall');
logFilterBtn.forEach(btn => {
btn.addEventListener('click', (e) => {
if(e.target.classList.contains('reset')) {
// 활동시간 초기화- '오늘~일주일'
const dateInputs = modal.querySelectorAll('.log-date .log-date-wrap input[type="date"]');
const toDateStr = (d) => d.toISOString().slice(0, 10);
const today = new Date();
const weekAgo = new Date(today);
weekAgo.setDate(today.getDate() - 7);
let startDate = toDateStr(weekAgo);
let endDate = toDateStr(today);
dateInputs[0].value = startDate;
dateInputs[1].value = endDate;
// 사용자 초기화 - '모든 사용자'
customDisplay.textContent = '모든 사용자';
userSelect.value = 'allUser';
customList.style.display = 'none';
}
const inputs = modal.querySelectorAll('.body .log-activity label input[type="checkbox"]');
inputs.forEach(input => {
if(e.target.classList.contains('reset')) {
input.checked = true;
} else if (e.target.classList.contains('select-all')) {
input.checked = true;
} else if (e.target.classList.contains('clear-all')) {
input.checked = false;
}
})
})
})
@@ -0,0 +1,32 @@
import { vars } from './variable.js';
import { toggleContextmenu, toggleContextFocusBox } from './eventManager.js'
export async function mgmtFunc_changeProjectState(toggleParams) {
if (toggleParams.type == 'changeProjectState') toggleParams.targetColumn = 'is_active';
await axios.post('/common/mgmtFunc_updateProject', { params: toggleParams });
}
export async function mgmtFunc_changeBannerNotice(toggleParams) {
if (toggleParams.type == 'changeBannerNotice') toggleParams.targetColumn = 'banner_notice';
await axios.post('/common/mgmtFunc_updateProject', { params: toggleParams });
}
export async function mgmtFunc_resetConvert(resourcePath, dataId) {
let resetConvertParams = {
resourcePath: resourcePath,
dataId: dataId
}
let resetConvertRes = await axios.post(`${vars.path_name}/mgmtFunc_resetConvert`, { params: resetConvertParams });
if (resetConvertRes.data.message == 'mgmtFunc_resetConvert_success') {
toggleContextmenu(false);
toggleContextFocusBox(false);
}
}
export async function mgmtFunc_addClickLog(params) {
// params.dataId -> 컨트롤러에서 숫자형으로 형변환해서 db추가
// project_id, activity, user_id, user_ip는 컨트롤러에서 params에 추가 (user_id는 params.userInfoString 사용)
let addClickLogRes = await axios.post(`${vars.path_name}/mgmtFunc_addClickLog`, { params: params });
// console.log(addClickLogRes.data.message);
}
+72
View File
@@ -0,0 +1,72 @@
let archiveManualSwiper = undefined;
export async function renderManual() {
await createManualSwiperArea();
archiveManualSwiper = new Swiper('.modal-body > .manual-wrap .manual-swiper', {
slidesPerView: 1,
spaceBetween: 0,
centeredSlides: true,
speed: 400,
loop: true,
navigation: {
nextEl: '.swiper.manual-swiper .button-next',
prevEl: '.swiper.manual-swiper .button-prev',
},
pagination: {
el: '.swiper.manual-swiper .swiper-pagination',
clickable: true,
renderBullet: function (index, className) {
return '<span class="' + className + '">' + (index + 1) + "</span>";
},
},
});
}
/** swiper area
* [div] manual-wrap
* [div] manual-swiper (v) <-- swiper
* [ul] manual-swiper-wrap <-- swiper-wrapper
* [li] manual-swiper-item <-- swiper-slide
* [img] manual-img
* [div] button-prev
* [div] button-next
* [div] swiper-pagination
*/
export async function createManualSwiperArea() {
const manualWrap = document.querySelector('.modal-body > .manual-wrap');
manualWrap.innerHTML = ''; // swiper 초기화
const manualSwiper = document.createElement('div');
manualSwiper.classList.add('swiper', 'manual-swiper');
const manualSwiperWrap = document.createElement('ul');
manualSwiperWrap.classList.add('swiper-wrapper', 'manual-swiper-wrap');
manualSwiper.appendChild(manualSwiperWrap);
manualWrap.appendChild(manualSwiper);
// swiper 동적 생성
for (let i = 0; i < 5; i++) {
const manualSwiperItem = document.createElement('li');
manualSwiperItem.classList.add('swiper-slide', 'manual-swiper-item');
manualSwiperItem.dataset.save = true;
manualSwiperItem.dataset.fileName = `${[i+1]}.jpg`;
const manualImg = document.createElement('img');
manualImg.classList.add('manual-img');
manualImg.src = `/main/img/manual/${[i+1]}.jpg`;
manualSwiperItem.appendChild(manualImg);
manualSwiperWrap.appendChild(manualSwiperItem);
}
// 이전버튼, 다음버튼, 페이지네이션버튼 생성
const swiperButtonPrev = document.createElement('div');
swiperButtonPrev.classList.add('button-prev');
const swiperButtonNext = document.createElement('div');
swiperButtonNext.classList.add('button-next');
const swiperPagination = document.createElement('div');
swiperPagination.classList.add('swiper-pagination');
manualSwiper.appendChild(swiperButtonPrev);
manualSwiper.appendChild(swiperButtonNext);
manualSwiper.appendChild(swiperPagination);
}
File diff suppressed because it is too large Load Diff
+754
View File
@@ -0,0 +1,754 @@
import { PMTiles } from 'https://esm.sh/pmtiles@3.0.7';
const MAX_LEVEL = 20;
const CLICK_HIGHLIGHT_OUTER_WIDTH = 10; // 바깥쪽 흰색 테두리 두께
const CLICK_HIGHLIGHT_INNER_WIDTH = 5; // 안쪽 선 두께
const HOVER_HIGHLIGHT_OUTER_WIDTH = 6; // 바깥쪽 흰색 테두리 두께
const HOVER_HIGHLIGHT_INNER_WIDTH = 2; // 안쪽 선 두께
// 선택/호버 상태 추적
let selectedFeature = null;
let hoveredFeature = null;
let selectLayer = null;
let hoverLayer = null;
// 현재 선택된 feature 저장
let currentSelectedFeature = null;
// 선택된 feature의 레이어 키 추가
let currentSelectedLayerKey = null;
// ---- OpenLayers VectorTile Layer ----
class OLVectorLayer {
constructor(olMap, layerId) {
this.olMap = olMap;
this.layerId = layerId;
this.type = null;
this.templateUrl = null;
this.pmtilesInstance = null;
this.maxLevel = MAX_LEVEL;
this.vectorLayer = null;
}
async setTileSource(source, type, zoomRange = null) {
this.type = type;
if (type === 'pbf') {
this.templateUrl = source;
try {
const baseUrl = source.replace('/{z}/{x}/{y}.pbf', '');
const metadataUrl = `${baseUrl}/metadata.json`;
const response = await fetch(metadataUrl);
const metadata = await response.json();
if (metadata.maxzoom) {
this.maxLevel = parseInt(metadata.maxzoom);
}
} catch (error) {
console.warn(`[${this.layerId}] Could not read metadata.json`);
}
this.vectorLayer = new ol.layer.VectorTile({
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
url: source,
maxZoom: this.maxLevel
}),
style: styleFunction,
renderMode: 'vector',
zIndex: 1,
// 줌 범위 설정
minZoom: zoomRange ? zoomRange.min : undefined,
maxZoom: zoomRange ? zoomRange.max : undefined
});
this.olMap.addLayer(this.vectorLayer);
const zoomInfo = zoomRange ? ` (zoom ${zoomRange.min}-${zoomRange.max})` : '';
console.log(`[${this.layerId}] PBF Vector layer initialized${zoomInfo}`);
} else if (type === 'pmtiles') {
this.pmtilesInstance = source;
try {
const metadata = await this.pmtilesInstance.getMetadata();
if (metadata.maxzoom) {
this.maxLevel = parseInt(metadata.maxzoom);
}
// console.log(`[${this.layerId}] PMTiles metadata:`, metadata);
} catch (error) {
console.warn(`[${this.layerId}] Could not read PMTiles metadata`);
}
this.vectorLayer = new ol.layer.VectorTile({
source: new ol.source.VectorTile({
format: new ol.format.MVT(),
tileUrlFunction: (tileCoord) => {
const z = tileCoord[0];
const x = tileCoord[1];
const y = tileCoord[2];
return `pmtiles://${this.layerId}/${z}/${x}/${y}`;
},
tileLoadFunction: async (tile, url) => {
const urlParts = url.split('/');
const z = parseInt(urlParts[urlParts.length - 3]);
const x = parseInt(urlParts[urlParts.length - 2]);
const y = parseInt(urlParts[urlParts.length - 1]);
// console.log(`Loading PMTiles tile: ${z}/${x}/${y}`);
try {
const tileData = await this.pmtilesInstance.getZxy(z, x, y);
if (tileData && tileData.data) {
// console.log(`PMTiles tile ${z}/${x}/${y} loaded, size: ${tileData.data.byteLength} bytes`);
const format = new ol.format.MVT();
const tileGrid = ol.tilegrid.createXYZ({maxZoom: this.maxLevel});
const tileExtent = tileGrid.getTileCoordExtent([z, x, y]);
const features = format.readFeatures(tileData.data, {
extent: tileExtent,
featureProjection: 'EPSG:3857'
});
// console.log(`PMTiles tile ${z}/${x}/${y} features: ${features.length}`);
if (tile.setFeatures) {
tile.setFeatures(features);
} else {
tile.setLoader(() => Promise.resolve(features));
}
} else {
// console.log(`PMTiles tile ${z}/${x}/${y} - no data`);
if (tile.setFeatures) {
tile.setFeatures([]);
} else {
tile.setLoader(() => Promise.resolve([]));
}
}
} catch (error) {
// console.error(`Failed to load PMTiles tile ${z}/${x}/${y}:`, error);
if (tile.setFeatures) {
tile.setFeatures([]);
} else {
tile.setLoader(() => Promise.resolve([]));
}
}
},
maxZoom: this.maxLevel
}),
style: styleFunction,
zIndex: 1,
updateWhileAnimating: false,
updateWhileInteracting: false,
renderBuffer: 50,
declutter: false,
// 줌 범위 설정
minZoom: zoomRange ? zoomRange.min : undefined,
maxZoom: zoomRange ? zoomRange.max : undefined
});
this.olMap.addLayer(this.vectorLayer);
// this.pmtilesInstance = source;
// try {
// const metadata = await this.pmtilesInstance.getMetadata();
// if (metadata.maxzoom) this.maxLevel = parseInt(metadata.maxzoom, 10);
// } catch (e) {
// console.warn(`[${this.layerId}] Could not read PMTiles metadata`);
// }
// // PMTiles가 512 기준이면 512 권장
// const tileSize = 256;
// const tileGrid = ol.tilegrid.createXYZ({
// maxZoom: this.maxLevel,
// tileSize
// });
// // 기존 styleFunction 그대로 사용
// const styleFn = styleFunction;
// // 벡터 → 래스터 변환용 레이어
// this.rasterLayer = new ol.layer.Tile({
// source: new ol.source.TileImage({
// tileGrid,
// tileUrlFunction: (tileCoord) => {
// const [z, x, y] = tileCoord;
// return `pmtiles://${this.layerId}/${z}/${x}/${y}`;
// },
// tileLoadFunction: async (imageTile, url) => {
// const parts = url.split('/');
// const z = parseInt(parts[parts.length - 3], 10);
// const x = parseInt(parts[parts.length - 2], 10);
// const y = parseInt(parts[parts.length - 1], 10);
// // 타일 extent (EPSG:3857)
// const tileExtent = tileGrid.getTileCoordExtent([z, x, y]);
// const resolution = tileGrid.getResolution(z);
// // 해상도/부하 트레이드오프
// const pixelRatio = 1; // 필요시 window.devicePixelRatio
// const canvas = document.createElement('canvas');
// canvas.width = tileSize * pixelRatio;
// canvas.height = tileSize * pixelRatio;
// const ctx = canvas.getContext('2d');
// // ★ extent 반영하여 벡터 컨텍스트 생성
// const vectorCtx = ol.render.toContext(ctx, {
// size: [tileSize * pixelRatio, tileSize * pixelRatio],
// pixelRatio,
// extent: tileExtent // ← 중요: 지도좌표 → 픽셀 변환 기준
// });
// try {
// const tileData = await this.pmtilesInstance.getZxy(z, x, y);
// if (tileData && tileData.data) {
// // MVT 파싱 (extent는 featureProjection과 동일 좌표계여야 함)
// const format = new ol.format.MVT();
// const features = format.readFeatures(tileData.data, {
// extent: tileExtent, // EPSG:3857
// featureProjection: 'EPSG:3857'
// });
// // zIndex 반영을 위해 (feature, style) 쌍 정렬
// const drawQueue = [];
// for (const f of features) {
// let s = styleFn ? styleFn(f, resolution) : null;
// if (!s) continue;
// const styles = Array.isArray(s) ? s : [s];
// for (const one of styles) {
// if (one && typeof one.getZIndex === 'function') {
// const zi = one.getZIndex() ?? 0;
// drawQueue.push({ f, style: one, z: zi });
// }
// }
// }
// drawQueue.sort((a, b) => a.z - b.z);
// for (const { f, style } of drawQueue) {
// vectorCtx.drawFeature(f, style);
// }
// } else {
// // 데이터 없음 → 투명 타일
// ctx.clearRect(0, 0, canvas.width, canvas.height);
// }
// imageTile.getImage().src = canvas.toDataURL('image/png');
// } catch (err) {
// console.warn(`PMTiles raster tile load failed ${z}/${x}/${y}`, err);
// const empty = document.createElement('canvas');
// empty.width = tileSize;
// empty.height = tileSize;
// imageTile.getImage().src = empty.toDataURL();
// }
// },
// // crossOrigin: 'anonymous',
// }),
// zIndex: 100000,
// minZoom: zoomRange ? zoomRange.min : undefined,
// maxZoom: zoomRange ? zoomRange.max : undefined
// });
// this.olMap.addLayer(this.rasterLayer);
const zoomInfo = zoomRange ? ` (zoom ${zoomRange.min}-${zoomRange.max})` : '';
console.log(`[${this.layerId}] PMTiles Vector layer initialized${zoomInfo}`);
try {
const metadata = await this.pmtilesInstance.getMetadata();
// PMTiles metadata에서 bounds 정보 확인
if (metadata && (metadata.bounds || metadata.antimeridian_adjusted_bounds || metadata.center)) {
let extent;
let bounds;
if (metadata.bounds) bounds = metadata.bounds;
if (metadata.antimeridian_adjusted_bounds) bounds = metadata.antimeridian_adjusted_bounds.split(',');
if (bounds) {
// bounds: [minLon, minLat, maxLon, maxLat]
const [minLon, minLat, maxLon, maxLat] = bounds;
const bottomLeft = ol.proj.fromLonLat([minLon, minLat]);
const topRight = ol.proj.fromLonLat([maxLon, maxLat]);
extent = [bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]];
} else if (metadata.center) {
// center와 zoom을 이용한 대략적인 영역 계산
const [centerLon, centerLat, centerZoom] = metadata.center;
const centerPoint = ol.proj.fromLonLat([centerLon, centerLat]);
const resolution = this.olMap.getView().getResolutionForZoom(centerZoom || 10);
const size = 1000; // 대략적인 크기
extent = [
centerPoint[0] - size * resolution,
centerPoint[1] - size * resolution,
centerPoint[0] + size * resolution,
centerPoint[1] + size * resolution
];
}
let pmtilesFitBtn = document.querySelector('.map-container .control-btn-wrap .pmtiles-fit-btn');
if (extent) {
if (ol.map.getOverlays().getLength() == 0) {
this.olMap.getView().fit(extent, {
padding: [50, 50, 50, 50],
// duration: 500,
// constrainResolution: true,
maxZoom: 18
});
// console.log(`[${this.layerId}] PMTiles layer fitted to bounds`);
}
pmtilesFitBtn.style.display = 'flex';
pmtilesFitBtn.addEventListener('click', async() => {
this.olMap.getView().fit(extent, {
padding: [50, 50, 50, 50],
// duration: 500,
// constrainResolution: true,
maxZoom: 18
});
})
}
}
} catch (error) {
console.warn(`[${this.layerId}] Could not fit PMTiles bounds:`, error);
}
}
}
// GeoJSON/KML용 벡터 소스 설정 메서드
async setVectorSource(url, type, zoomRange = null) {
this.type = type;
if (type === 'geojson') {
console.log(`[${this.layerId}] Loading GeoJSON from: ${url}`);
this.vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({
url: url,
format: new ol.format.GeoJSON()
}),
style: styleFunction,
zIndex: 1,
updateWhileAnimating: false,
updateWhileInteracting: false,
renderBuffer: 50,
declutter: false,
// 줌 범위 설정
minZoom: zoomRange ? zoomRange.min : undefined,
maxZoom: zoomRange ? zoomRange.max : undefined
});
this.olMap.addLayer(this.vectorLayer);
const zoomInfo = zoomRange ? ` (zoom ${zoomRange.min}-${zoomRange.max})` : '';
console.log(`[${this.layerId}] GeoJSON Vector layer initialized${zoomInfo}`);
// GeoJSON 로드 완료 이벤트 처리
const source = this.vectorLayer.getSource();
source.once('change', () => {
if (source.getState() === 'ready') {
const features = source.getFeatures();
const featureCount = features.length;
console.log(`[${this.layerId}] GeoJSON loaded: ${featureCount} features`);
// feature 수에 따른 경고
if (featureCount > 10000) {
console.warn(`⚠️ Large dataset detected (${featureCount} features). Performance may be affected.`);
}
// 로드된 features가 있으면 해당 영역으로 자동 줌 (단독 레이어인 경우만)
if (features.length > 0 && !zoomRange) {
const extent = source.getExtent();
this.olMap.getView().fit(extent, {
padding: [50, 50, 50, 50],
maxZoom: 18
});
}
}
});
} else if (type === 'kml') {
console.log(`[${this.layerId}] Loading KML from: ${url}`);
this.vectorLayer = new ol.layer.Vector({
source: new ol.source.Vector({
url: url,
format: new ol.format.KML({
extractStyles: false,
showPointNames: false
})
}),
style: styleFunction,
zIndex: 1,
updateWhileAnimating: false,
updateWhileInteracting: false,
renderBuffer: 50,
declutter: false,
// 줌 범위 설정
minZoom: zoomRange ? zoomRange.min : undefined,
maxZoom: zoomRange ? zoomRange.max : undefined
});
this.olMap.addLayer(this.vectorLayer);
const zoomInfo = zoomRange ? ` (zoom ${zoomRange.min}-${zoomRange.max})` : '';
console.log(`[${this.layerId}] KML Vector layer initialized${zoomInfo}`);
// KML 로드 완료 이벤트 처리
const source = this.vectorLayer.getSource();
source.once('change', () => {
if (source.getState() === 'ready') {
const features = source.getFeatures();
console.log(`[${this.layerId}] KML loaded: ${features.length} features`);
if (features.length > 0 && !zoomRange) {
const extent = source.getExtent();
this.olMap.getView().fit(extent, {
padding: [50, 50, 50, 50],
maxZoom: 18
});
}
}
});
}
}
clear() {
if (this.vectorLayer) {
this.olMap.removeLayer(this.vectorLayer);
this.vectorLayer = null;
console.log(`[${this.layerId}] Layer cleared`);
}
}
}
// ---- 통합 멀티 레이어 관리자 (OpenLayers) ----
export class OLMultiLayerManager {
constructor(olMap) {
this.olMap = olMap;
this.layerMap = new Map();
this.pmtilesInstances = new Map();
console.log('OLMultiLayerManager initialized');
}
async toggleLayer(layerId, format) {
const key = `${format}_${layerId}`;
if (this.layerMap.has(key)) {
// 레이어 제거
// PMTiles 제거 시 연결된 detail GeoJSON도 함께 제거
const layer = this.layerMap.get(key);
layer.clear();
this.layerMap.delete(key);
if (this.pmtilesInstances.has(key)) {
this.pmtilesInstances.delete(key);
}
console.log(`[${key}] Layer removed`);
return false;
} else {
// 레이어 추가 로직
if (format === 'pbf') {
const layer = new OLVectorLayer(this.olMap, key);
const templateUrl = `http://172.16.41.52:3003/vector_tile_pbf/${layerId}/{z}/{x}/{y}.pbf`;
await layer.setTileSource(templateUrl, 'pbf');
this.layerMap.set(key, layer);
} else if (format === 'pmtiles') {
// console.log(`🚀 Creating hybrid PMTiles + GeoJSON layers for: ${layerId}`);
// // 1. PMTiles 레이어 추가 (zoom 0-17만 표시)
// const pmtilesLayer = new OLVectorLayer(this.olMap, key);
// const pmtilesPath = `http://172.16.41.52:3003/vector_tile_pmtiles/${layerId}.pmtiles`;
// const pmtiles = new PMTiles(pmtilesPath);
// this.pmtilesInstances.set(key, pmtiles);
// await pmtilesLayer.setTileSource(pmtiles, 'pmtiles', { min: 0, max: 18.1 });
// this.layerMap.set(key, pmtilesLayer);
// console.log(`✅ [${key}] PMTiles layer added (zoom 0-17)`);
// // 2. GeoJSON 레이어 추가 (zoom 18-24만 표시)
// const detailGeojsonKey = `geojson_detail_${layerId}`;
// const detailGeojsonLayer = new OLVectorLayer(this.olMap, detailGeojsonKey);
// const geojsonUrl = `http://172.16.41.52:3003/vector_tile_geojson/${layerId}.geojson`;
// await detailGeojsonLayer.setVectorSource(geojsonUrl, 'geojson', { min: 17.9, max: 24 });
// this.layerMap.set(detailGeojsonKey, detailGeojsonLayer);
// console.log(`✅ [${detailGeojsonKey}] Detail GeoJSON layer added (zoom 18-24)`);
// console.log(`🎯 Hybrid layer setup complete. PMTiles will show at zoom 0-17, GeoJSON at zoom 18+`);
const pmtilesLayer = new OLVectorLayer(this.olMap, key);
// const pmtilesPath = `http://172.16.41.52:3003/vector_tile_pmtiles/${layerId}.pmtiles`;
let pmtilesPath = `https://gsim-model.digitalarchive.work/pmtiles/vector/${layerId}.pmtiles`;
if (layerId == 'testbim') pmtilesPath = `https://gsim-model.digitalarchive.work/pmtiles/vector/dsdj2.pmtiles`;
// let mapName = layerId;
// if (layerId == 'testbim') mapName = 'dsdj2';
// let pmtilesPath = `https://gsim-model.digitalarchive.work/pmtiles/vector/${mapName}.pmtiles`;
let checkUrlExistsResult = await checkUrlExists(pmtilesPath);
if (checkUrlExistsResult == true) {
const pmtiles = new PMTiles(pmtilesPath);
this.pmtilesInstances.set(key, pmtiles);
await pmtilesLayer.setTileSource(pmtiles, 'pmtiles');
this.layerMap.set(key, pmtilesLayer);
} else {
let pmtilesFitBtn = document.querySelector('.map-container .control-btn-wrap .pmtiles-fit-btn');
pmtilesFitBtn.style.display = 'none';
return;
}
} else if (format === 'geojson') {
const layer = new OLVectorLayer(this.olMap, key);
const geojsonUrl = `http://172.16.41.52:3003/vector_tile_geojson/${layerId}.geojson`;
await layer.setVectorSource(geojsonUrl, 'geojson');
this.layerMap.set(key, layer);
} else if (format === 'kml') {
const layer = new OLVectorLayer(this.olMap, key);
const kmlUrl = `http://172.16.41.52:3003/vector_tile_kml/${layerId}.kml`;
await layer.setVectorSource(kmlUrl, 'kml');
this.layerMap.set(key, layer);
}
// console.log(`[${key}] Layer added`);
return true;
}
}
clearAll() {
this.layerMap.forEach(layer => layer.clear());
this.layerMap.clear();
this.pmtilesInstances.clear();
console.log('All OpenLayers layers cleared');
}
getActiveLayers() {
return Array.from(this.layerMap.keys());
}
}
// ---- 피처별 스타일 함수 ----
function styleFunction(feature, resolution) {
const props = feature.getProperties();
const entityType = props?.EntityType;
let color = props.Color || '#ff0000';
const constWidth = props.constWidth || 0;
const ltscale = props.ltscale || 1.0;
// let dashPattern = props.dashPattern;
// if (dashPattern && typeof dashPattern === 'string') {
// try {
// // "[0.5,-0.25]" → [0.5, -0.25]
// dashPattern = JSON.parse(dashPattern);
// } catch (e) {
// console.warn('dashPattern 파싱 실패:', dashPattern, e);
// dashPattern = null;
// }
// }
// ⭐ constWidth에 따른 선 두께 결정
let strokeWidth = 1; // 기본값
// if (constWidth === 0) {
// strokeWidth = 1;
// } else if (constWidth === 1) {
// strokeWidth = 5;
// } else {
// // 향후 다른 값 대비
// // strokeWidth = 2 + (constWidth * 2);
// // strokeWidth = 1 + (constWidth * 5);
// strokeWidth = constWidth * 5;
// }
// if (constWidth === 0) {
// strokeWidth = 1;
// } else {
// // 향후 다른 값 대비
// // strokeWidth = 2 + (constWidth * 2);
// // strokeWidth = 1 + (constWidth * 5);
// strokeWidth = constWidth * 10;
// }
// if (constWidth === 0) {
// strokeWidth = 1;
// } else if (constWidth > 0 && constWidth <= 1) {
// strokeWidth = constWidth * 10;
// } else {
// strokeWidth = constWidth * 5;
// }
// if (constWidth === 0) {
// strokeWidth = 1;
// } else {
// console.log(constWidth);
// strokeWidth = constWidth*2;
// }
// // ⭐ dashPattern을 OpenLayers lineDash로 변환
// let lineDash = null;
// if (dashPattern && Array.isArray(dashPattern) && dashPattern.length > 0) {
// // const zoomFactor = Math.min(1 / resolution, 1.0);
// const scaleFactor = 10; // 조정 가능 (0.3 ~ 1.0 추천)
// lineDash = dashPattern.map(value =>
// Math.abs(value) * ltscale * scaleFactor
// );
// }
// 투명도 처리
const hexToRgba = (hex, alpha = 1) => {
if (!hex || hex.length < 7) return `rgba(255, 0, 0, ${alpha})`;
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
};
const geomType = feature.getGeometry().getType();
if (entityType) {
if (entityType === 'TEXT_POLYGON' || entityType === 'LINETYPE_PATTERN_TEXT') {
return new ol.style.Style({
fill: new ol.style.Fill({
color: hexToRgba(color, 1.0)
}),
zIndex: 40
});
} else if (entityType === 'LINETYPE_PATTERN_SHAPE') {
return new ol.style.Style({
fill: new ol.style.Fill({
color: hexToRgba(color, 0.4)
}),
stroke: new ol.style.Stroke({
color: hexToRgba(color, 1.0),
width: strokeWidth,
// lineDash: lineDash,
lineCap: 'butt',
lineJoin: 'miter'
}),
zIndex: 20
});
} else if (entityType === 'HATCH') {
return new ol.style.Style({
fill: new ol.style.Fill({
color: hexToRgba(color, 0.4)
}),
stroke: new ol.style.Stroke({
color: hexToRgba(color, 1.0),
width: strokeWidth,
// lineDash: lineDash,
lineCap: 'butt',
lineJoin: 'miter'
}),
zIndex: 10
});
} else if (entityType === 'SOLID_LINE_POLYGON' || entityType === 'DASHED_LINE_POLYGON') {
return new ol.style.Style({
fill: new ol.style.Fill({
color: hexToRgba(color, 1)
}),
stroke: new ol.style.Stroke({
color: hexToRgba(color, 1.0),
width: strokeWidth,
// lineDash: lineDash,
lineCap: 'butt',
lineJoin: 'miter'
}),
zIndex: 10
});
} else if (entityType === 'LINETYPE_PATTERN_SHAPE_LINE') {
return new ol.style.Style({
stroke: new ol.style.Stroke({
color: hexToRgba(color, 1.0),
width: strokeWidth,
// lineDash: lineDash,
lineCap: 'butt',
lineJoin: 'miter'
}),
zIndex: 30
});
} else if (entityType === 'LWPOLYLINE' || entityType === 'LINE') {
return new ol.style.Style({
stroke: new ol.style.Stroke({
color: hexToRgba(color, 1.0),
width: strokeWidth,
// lineDash: lineDash,
lineCap: 'butt',
lineJoin: 'miter'
}),
zIndex: 30
});
}
} else {
if (geomType === 'Polygon' || geomType === 'MultiPolygon') {
return new ol.style.Style({
fill: new ol.style.Fill({
color: hexToRgba(color, 0.4)
}),
stroke: new ol.style.Stroke({
color: hexToRgba(color, 1.0),
width: strokeWidth,
// lineDash: lineDash,
lineCap: 'butt',
lineJoin: 'miter'
}),
zIndex: 20
});
} else if (geomType === 'LineString' || geomType === 'MultiLineString') {
// let strokeWidth, color;
// if (props.DIVI == '주곡선') {
// strokeWidth = 2;
// color = '#cccccc';
// } else {
// strokeWidth = 1;
// color = '#777777';
// }
return new ol.style.Style({
stroke: new ol.style.Stroke({
color: hexToRgba(color, 1.0),
width: strokeWidth,
// lineDash: lineDash,
lineCap: 'butt',
lineJoin: 'miter'
}),
zIndex: 30
});
} else if (geomType === 'Point' || geomType === 'MultiPoint') {
return new ol.style.Style({
image: new ol.style.Circle({
radius: 4,
fill: new ol.style.Fill({
color: hexToRgba(color, 1.0)
})
}),
zIndex: 30
});
}
}
}
// async function checkUrlExists(url) {
// try {
// const res = await fetch(url, { method: "HEAD" });
// if (res.ok) {
// return true; // 200~299 → 정상 URL
// }
// return false; // 404, 403 등
// } catch (e) {
// return false; // 네트워크 오류, CORS 오류 포함
// }
// }
async function checkUrlExists(url) {
try {
const res = await fetch(url, { method: "HEAD" });
// 200~299
return res.ok;
} catch (e) {
// 여기서 swallow(삼키기)
return false;
}
}
File diff suppressed because it is too large Load Diff
+710
View File
@@ -0,0 +1,710 @@
import { typeStep2Kor } from '../main.js';
let projectTypeBtn;
let projectTypeCapsule;
let projectTypeList = null;
let projectStepList = null;
let mainLayerState = null; // 메인 레이어 상태
// 프로젝트 설정 저장 버튼 클릭 이벤트
document.getElementById('project-save-btn').addEventListener('click', async () => {
const projectSaveBtn = document.getElementById('project-save-btn');
const projectStepBtn = document.getElementById('project-step-btn');
const projectNameInput = document.getElementById('project-name-input');
if (projectSaveBtn.textContent === '저장') {
// type
let typeVal = projectTypeBtn.children[0].textContent;
let typeValResult = stepTypeEng(typeVal);
// step
let stepVal = projectStepBtn.children[0].textContent;
let stepValResult = stepTypeEng(stepVal);
// project_name
let nameVal = projectNameInput.value;
let projectInfoParams = {
projectId : vars.project.project_id,
category : vars.project.category,
project_type : typeValResult,
step : stepValResult,
project_nm : nameVal,
}
let res = await axios.post('/common/updateProjectInfo', {params: projectInfoParams});
if (res.data.message == 'updateProjectInfo_success') {
// 수정된 프로젝트 정보 가져와서 전역변수에 저장
let projectRes = await axios.get('/common/getProject');
for (let project of projectRes.data.data) {
vars.allProject[project.project_id] = project;
}
vars.project = vars.allProject[vars.project_id];
setReadOnlyMode(vars.project.category);
}
}
});
// 프로젝트 설정 취소 클릭 이벤트
document.querySelector('#project-cancel-btn').addEventListener('click', () => {
const projectSaveBtn = document.getElementById('project-save-btn');
if (projectSaveBtn.textContent === '저장') projectSaveBtn.textContent = '수정';
const list = document.querySelector('#project-type-wrap-overseas .project-type__list');
list.classList.remove('--project-list__open');
setReadOnlyMode(vars.project.category);
});
export function initProjectSetting(param) {
const projectSaveBtn = document.getElementById('project-save-btn');
const projectCancelBtn = document.getElementById('project-cancel-btn');
const projectLocationBtn = document.getElementById('project-location-btn');
let projectTypeWrap = document.getElementById('project-type-wrap');
let projectTypeWrapOverseas = document.getElementById('project-type-wrap-overseas');
if (param === undefined) {
// overseas, bim 아닌 경우
projectTypeWrap.style.display = 'none';
projectTypeWrapOverseas.style.display = 'none';
projectTypeBtn = document.getElementById('project-type-btn');
projectTypeCapsule = document.getElementById('project-type-capsule');
} else {
if (param === 'overseas') {
// overseas
projectTypeWrap.style.display = 'none';
projectTypeWrapOverseas.style.display = 'flex';
projectTypeBtn = document.getElementById('project-type-btn-overseas');
projectTypeCapsule = document.getElementById('project-type-capsule-overseas');
} else {
//bim
projectTypeWrap.style.display = 'flex';
projectTypeWrapOverseas.style.display = 'none';
projectTypeBtn = document.getElementById('project-type-btn');
projectTypeCapsule = document.getElementById('project-type-capsule');
}
adjustSelectBoxWidth(projectTypeBtn);
}
setReadOnlyMode(param);
// 권한이 있는 경우 저장 버튼 이벤트 추가
if (vars.permission.checkPermission('change-project-btn')) {
// 기존 리스너 제거 후 추가 (중복 방지)
projectSaveBtn.textContent = '수정';
projectSaveBtn.removeEventListener('click', handleProjectSaveClick);
projectSaveBtn.addEventListener('click', () => handleProjectSaveClick(param));
} else {
// 권한 없으면 버튼 제거
projectSaveBtn.remove();
projectCancelBtn.remove();
projectLocationBtn.remove();
}
// 이벤트 리스너 초기화
initEventListeners();
}
function handleProjectSaveClick(param) {
const projectSaveBtn = document.getElementById('project-save-btn');
if (projectSaveBtn.textContent === '저장') {
// 저장 버튼 클릭 : 읽기 모드로 전환
projectSaveBtn.textContent = '수정';
setReadOnlyMode(param);
} else {
// 수정 버튼 클릭: 편집 모드로 전환
projectSaveBtn.textContent = '저장';
setEditMode(param);
}
}
// 읽기 모드 : 관리자 이상이면서 읽기모드 or 일반유저
function setReadOnlyMode(param) {
const projectStepBtn = document.getElementById('project-step-btn');
const projectStepCapsule = document.getElementById('project-step-capsule');
const projectNameView = document.getElementById('project-name-view');
const projectNameInput = document.getElementById('project-name-input');
const projectSaveBtn = document.getElementById('project-save-btn');
const projectCancelBtn = document.getElementById('project-cancel-btn');
// step - 버튼 숨기고 캡슐 표시
projectStepBtn.style.display = 'none';
projectStepCapsule.style.display = 'flex';
projectStepCapsule.innerHTML = typeStep2Kor(vars.project.step);
projectStepCapsule.className = `project-step-capsule --step-capsule__${vars.project.step}`;
// type - 버튼 숨기고 캡슐 표시
projectTypeBtn.style.display = 'none';
projectTypeCapsule.style.display = 'flex';
projectTypeCapsule.innerHTML = typeStep2Kor(vars.project.project_type);
// name - input 숨기고 view 표시
projectNameInput.style.display = 'none';
projectNameView.style.display = 'flex';
projectNameView.innerHTML = vars.project.short_nm;
if(param !== 'overseas' && param !== 'bimproject') {
projectStepCapsule.style.display = 'none';
projectTypeCapsule.style.display = 'none';
projectNameView.innerHTML = vars.project.project_nm;
projectSaveBtn.style.display = 'none';
projectCancelBtn.style.display = 'none';
}
// location - 관리자이상:버튼 o , 관리자이하: 버튼x
if (vars.permission.checkPermission('change-project-btn')) {
document.querySelector('#project-location-btn').style.display = 'flex';
}
const lat = document.querySelector('.archive-modal .wrap .modal-wrap .modal-body .project-setting-wrap .project-location-wrap .project-location-lat');
lat.innerHTML =`<div class="project-location-lat">위도 ${vars.project.lat}</div>`
const lon = document.querySelector('.archive-modal .wrap .modal-wrap .modal-body .project-setting-wrap .project-location-wrap .project-location-lon');
lon.innerHTML =`<div class="project-location-lon">경도 ${vars.project.lon}</div>`
}
// 편집모드 : 관리자 이상이면서 편집모드
function setEditMode(param) {
const projectStepBtn = document.getElementById('project-step-btn');
const projectStepCapsule = document.getElementById('project-step-capsule');
const projectNameView = document.getElementById('project-name-view');
const projectNameInput = document.getElementById('project-name-input');
// step - 캡슐 숨기고 버튼 표시
projectStepCapsule.style.display = 'none';
projectStepBtn.style.display = 'flex';
projectStepBtn.innerHTML = `
<h5 class="project-step__label --step__${vars.project.step}">${typeStep2Kor(vars.project.step)}</h5>
<i class="project-step__icon"></i>
`;
// type - 캡슐 숨기고 버튼 표시
projectTypeCapsule.style.display = 'none';
projectTypeBtn.style.display = 'flex';
projectTypeBtn.innerHTML = `
<h5 class="project-type__label --type__${vars.project.project_type}">${typeStep2Kor(vars.project.project_type)}</h5>
<i class="project-type__icon"></i>
`;
// name - view 숨기고 input 표시
projectNameView.style.display = 'none';
projectNameInput.style.display = 'flex';
projectNameInput.value = projectNameView.textContent;
if(param !== 'overseas' && param !== 'bimproject') {
projectStepBtn.style.display = 'none';
projectTypeBtn.style.display = 'none';
}
// location - 수정 버튼, 위경도 위치 표시
document.querySelector('#project-location-btn').style.display = 'flex';
const lat = document.querySelector('.archive-modal .wrap .modal-wrap .modal-body .project-setting-wrap .project-location-wrap .project-location-lat');
lat.innerHTML =`<div class="project-location-lat">위도 ${vars.project.lat}</div>`
const lon = document.querySelector('.archive-modal .wrap .modal-wrap .modal-body .project-setting-wrap .project-location-wrap .project-location-lon');
lon.innerHTML =`<div class="project-location-lon">경도 ${vars.project.lon}</div>`
}
// 접속 인원 모달 - 프로젝트 type, step 변경 이벤트
function initEventListeners() {
// project-type-btn 클릭 이벤트
if (projectTypeBtn) {
// 기존 이벤트 제거 후 추가 (중복 방지)
const newBtn = projectTypeBtn.cloneNode(true);
projectTypeBtn.parentNode.replaceChild(newBtn, projectTypeBtn);
projectTypeBtn = newBtn;
projectTypeList = projectTypeBtn.parentElement.querySelector('.project-type__list');
projectTypeBtn.addEventListener('click', (e) => {
e.stopPropagation();
projectTypeList?.classList.toggle('--project-list__open');
});
}
// project-step-btn 클릭 이벤트
const projectStepBtn = document.getElementById('project-step-btn');
if (projectStepBtn) {
projectStepList = document.querySelector('.project-step__list');
projectStepBtn.addEventListener('click', (e) => {
e.stopPropagation();
projectStepList?.classList.toggle('--project-list__open');
});
}
// modal-wrap 클릭 이벤트
if (projectTypeBtn) {
const modalWrap = projectTypeBtn.closest('.modal-wrap');
if (modalWrap) {
modalWrap.addEventListener('click', (e) => {
projectTypeList?.classList.remove('--project-list__open');
projectStepList?.classList.remove('--project-list__open');
});
}
}
// project-type__list_item 클릭 이벤트
if (projectTypeList) {
projectTypeList.querySelectorAll('.project-type__list_item').forEach(item => {
item.addEventListener('click', async(e) => {
e.stopPropagation();
projectTypeList.classList.remove('--project-list__open');
let type = e.target.classList[1].split('__')[1];
let kor = e.target.textContent;
projectTypeBtn.innerHTML = `
<h5 class="project-type__label --type__${type}">${kor}</h5>
<i class="project-type__icon"></i>`;
});
});
}
// project-step__list_item 클릭 이벤트
if (projectStepList) {
projectStepList.querySelectorAll('.project-step__list_item').forEach(item => {
item.addEventListener('click', async(e) => {
e.stopPropagation();
projectStepList.classList.remove('--project-list__open');
let step = e.target.classList[1].split('__')[1];
let btn = document.getElementById('project-step-btn');
let kor = typeStep2Kor(step);
if (btn) {
btn.innerHTML = `
<h5 class="project-step__label --step__${step}">${kor}</h5>
<i class="project-step__icon"></i>`;
}
});
});
}
}
function stepTypeEng(param) {
let result = param;
switch(param) {
case '완료':
result = 'done';
break;
case '진행':
result = 'active';
break;
case '중지':
result = 'stop';
break;
case '대기':
result = 'wait';
break;
case '시공':
result = 'construction';
break;
case '설계':
result = 'design';
break;
case '제안':
result = 'surgest';
break;
case '연구':
result = 'research';
break;
case '지원':
result = 'support';
break;
case '센터':
result = 'center';
break;
case '측량':
result = 'survey';
break;
case 'MP (기본계획)':
result = 'MP';
break;
case 'DD (실시설계)':
result = 'DD';
break;
case 'FS (타당성조사)':
result = 'FS';
break;
case 'PD (기본설계)':
result = 'PD';
break;
case 'DS (설계감리)':
result = 'DS';
break;
case 'CS (시공감리)':
result = 'CS';
break;
case 'PMC (실시설계)':
result = 'PMC';
break;
case 'IDC (타당성조사)':
result = 'IDC';
break;
case 'DR (설계검토)':
result = 'DR';
break;
case 'BD (수주영업)':
result = 'BD';
break;
case 'ETC (기타)':
result = 'ETC';
break;
default:
result = '없음';
break;
}
return result;
}
// 셀렉트박스 크기 조정
function adjustSelectBoxWidth(button) {
if (!button) return;
const list = button.parentElement.querySelector('.project-type__list');
if (!list) return;
// canvas를 사용한 텍스트 너비 측정
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
// 현재 폰트 스타일 가져오기
const computedStyle = window.getComputedStyle(button);
context.font =`${computedStyle.fontSize} ${computedStyle.fontFamily}`;
let maxWidth = 0;
const items = list.querySelectorAll('.project-type__list_item');
items.forEach(item => {
const text = item.textContent.trim();
const metrics = context.measureText(text);
const width = metrics.width;
if (width > maxWidth) {
maxWidth = width;
}
});
// 적용
if (maxWidth > 0) {
const finalWidth = maxWidth + 25;
// 버튼에 적용
button.style.minWidth = `${finalWidth}px`;
button.style.textAlign = 'center'; // 가운데 정렬
button.style.justifyContent = 'center'; // flex인 경우
button.style.display = 'flex'; // flex 적용
button.style.alignItems = 'center'; // 세로 가운데 정렬
// 캡슐에 적용
const capsule = button.parentElement.querySelector('.--type-capsule');
if (capsule) {
capsule.style.minWidth = `${finalWidth}px`;
capsule.style.textAlign = 'center'; // 가운데 정렬
capsule.style.justifyContent = 'center'; // flex인 경우
capsule.style.display = 'flex'; // flex 적용
capsule.style.alignItems = 'center'; // 세로 가운데 정렬
}
// 리스트에 적용
list.style.minWidth = `${finalWidth}px`;
// 리스트 글씨 정렬
items.forEach(item => {
item.style.paddingLeft = '5px';
item.style.paddingRight = '5px';
})
}
}
// 프로젝트 위치 수정 버튼 클릭 이벤트
document.getElementById('project-location-btn').addEventListener('click', () => {
document.querySelector('.project-location-modal').style.display = 'flex';
openLocationModal();
})
// 지도 모달창 닫기 버튼, 취소 버튼 클릭 이벤트
document.querySelector('.project-location-modal .modal-wrap .modal-head .close').addEventListener('click', () => {
closeLocationModal();
});
document.querySelector('.project-location-modal .modal-wrap .modal-foot .button').addEventListener('click', () => {
closeLocationModal();
});
// 기본 지도 모달창 버튼 클릭 이벤트
document.querySelector('.project-location-modal .modal-wrap .modal-body .map-wrap .xs-button').addEventListener('click', () => {
document.querySelector('.project-location-modal .modal-wrap .modal-body .map-wrap .base-map').style.display = 'flex';
});
// 기본 지도 모달창 닫기 버튼 클릭 이벤트
document.querySelector('.project-location-modal .modal-wrap .modal-body .map-wrap .base-map .text-wrap .close').addEventListener('click', () => {
document.querySelector('.project-location-modal .modal-wrap .modal-body .map-wrap .base-map').style.display = 'none';
});
// 지도 모달창 저장 버튼 클릭 이벤트
document.querySelector('.project-location-modal .modal-wrap .modal-foot .primary-button').addEventListener('click', async() => {
if(!ol.locationMap || !ol.locationMap.clickCoord) {
alert('위치를 선택해주세요');
return;
}
let updateLocationParams = {
projectId: vars.project.project_id,
category: vars.project.category,
project_nm: vars.project.project_nm,
lon: ol.locationMap.clickCoord.lon,
lat: ol.locationMap.clickCoord.lat
};
let res = await axios.post('/common/updateLocationInfo', {params: updateLocationParams});
if (res.data.message == 'updateLocationInfo_success') {
vars.project.lon = res.data.data.lon;
vars.project.lat = res.data.data.lat;
closeLocationModal();
const lat = document.querySelector('.archive-modal .wrap .modal-wrap .modal-body .project-setting-wrap .project-location-wrap .project-location-lat');
lat.innerHTML = `<div class="project-location-lat">위도 ${res.data.data.lat}</div>`;
const lon = document.querySelector('.archive-modal .wrap .modal-wrap .modal-body .project-setting-wrap .project-location-wrap .project-location-lon');
lon.innerHTML = `<div class="project-location-lon">경도 ${res.data.data.lon}</div>`;
}
})
// 프로젝트 위치 수정
function openLocationModal() {
// 메인 지도 레이어 상태 백업
if (vars.roadLayer && vars.satelliteLayer && vars.hybridLayer) {
mainLayerState = {
road: vars.roadLayer.getVisible(),
satellite: vars.satelliteLayer.getVisible(),
hybrid: vars.hybridLayer.getVisible()
};
}
initLocationMap();
// 기존 좌표가 있을 때만 블루 마커 생성
if (vars.project.lon && vars.project.lat) {
if (!ol.locationMap.blueMarkerOverlay) {
// 블루 마커 (저장된 위치)
let blueMarker = document.createElement('div');
blueMarker.style.width = '1rem';
blueMarker.style.height = '1rem';
blueMarker.style.backgroundColor = '#0D8DF2';
blueMarker.style.borderRadius = '50%';
blueMarker.style.border = '0.1875rem solid #fff';
blueMarker.style.boxShadow = '0 0.25rem 0.5rem rgba(0, 0, 0, 0.05)';
ol.locationMap.blueMarkerOverlay = new ol.Overlay({
element: blueMarker,
positioning: 'center-center',
stopEvent: false,
zIndex: 9998
});
ol.locationMap.addOverlay(ol.locationMap.blueMarkerOverlay);
}
if (!ol.locationMap.blueLabelOverlay) {
let blueLabel = document.createElement('div');
ol.locationMap.blueLabelOverlay = new ol.Overlay({
element: blueLabel,
positioning: 'bottom-center',
offset: [0, -10],
stopEvent: false,
zIndex: 9998,
});
ol.locationMap.addOverlay(ol.locationMap.blueLabelOverlay);
}
const position3857 = ol.proj.fromLonLat([vars.project.lon, vars.project.lat]);
ol.locationMap.blueMarkerOverlay.setPosition(position3857);
ol.locationMap.blueLabelOverlay.setPosition(position3857);
ol.locationMap.blueLabelOverlay.element.innerHTML = `
<div style="background: #fff; color: #111; padding: 0.25rem 0.5rem; border-radius: 0.25rem; border: 0.1875rem solid #0D8DF2; box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05); font-size: 0.875rem; font-weight: 500; line-height: 1.25rem; letter-spacing: -0.0175rem;">
저장된 위치 <br>
경도 : ${vars.project.lon}<br>
위도 : ${vars.project.lat}
</div>
`;
}
}
// 지도 초기화
function initLocationMap() {
// 위치 모달 지도만 삭제
if (ol.locationMap) {
ol.locationMap.getOverlays()?.clear();
ol.locationMap.getLayers()?.forEach(l => {
const src = l.getSource?.();
if (src && src instanceof ol.source.Vector) src.clear(true);
});
ol.locationMap.getLayers()?.clear();
if (typeof ol.locationMap.dispose === 'function') {
ol.locationMap.dispose();
}
ol.locationMap = null;
}
// 위치 설정 모달용 임시 레이어 생성
const tempRoadLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: vars.roadLayer.getSource().getUrls()[0]
}),
visible: mainLayerState?.road ?? true
});
const tempSatelliteLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: vars.satelliteLayer.getSource().getUrls()[0]
}),
visible: mainLayerState?.satellite ?? false
});
const tempHybridLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: vars.hybridLayer.getSource().getUrls()[0]
}),
visible: mainLayerState?.hybrid ?? false
});
ol.locationMap = new ol.Map({
target: 'map-location',
layers: [tempRoadLayer, tempSatelliteLayer, tempHybridLayer],
view: new ol.View({
projection: 'EPSG:3857',
center: ol.proj.fromLonLat([127.8, 35.9]),
zoom: 7,
constrainResolution: true,
smoothResolutionConstraint: true
})
});
// 임시 레이어 참조 저장
ol.locationMap.tempRoadLayer = tempRoadLayer;
ol.locationMap.tempSatelliteLayer = tempSatelliteLayer;
ol.locationMap.tempHybridLayer = tempHybridLayer;
// proj4 설정
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);
// 레드 마커 생성
let marker = document.createElement('div');
marker.classList.add('marker');
marker.style.width = '1rem';
marker.style.height = '1rem';
marker.style.backgroundColor = '#F21D0D';
marker.style.borderRadius = '50%';
marker.style.border = '0.1875rem solid #fff';
marker.style.boxShadow = '0 0.25rem 0.5rem rgba(0, 0, 0, 0.05)';
ol.locationMap.redMarkerOverlay = new ol.Overlay({
element: marker,
positioning: 'center-center',
stopEvent: false,
zIndex: 9999
});
ol.locationMap.addOverlay(ol.locationMap.redMarkerOverlay);
let label = document.createElement('div');
ol.locationMap.redLabelOverlay = new ol.Overlay({
element: label,
positioning: 'bottom-center',
offset: [0, -10],
stopEvent: false,
zIndex: 9999,
});
ol.locationMap.addOverlay(ol.locationMap.redLabelOverlay);
// 클릭 이벤트
ol.locationMap.on('click', function(e) {
const lonLat = ol.proj.toLonLat(e.coordinate);
const lon = lonLat[0].toFixed(6);
const lat = lonLat[1].toFixed(6);
ol.locationMap.clickCoord = {
lon: Number(lon),
lat: Number(lat),
coordinate3857: e.coordinate
};
ol.locationMap.redMarkerOverlay.setPosition(e.coordinate);
ol.locationMap.redLabelOverlay.element.innerHTML = `
<div style="background: #fff; color: #111; padding: 0.25rem 0.5rem; border-radius: 0.25rem; border: 0.1875rem solid #F21D0D; box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.05); font-size: 0.875rem; font-weight:500; line-height: 1.25rem; letter-spacing: -0.0175rem;">
선택한 위치 <br>
경도 : ${lon} <br>
위도 : ${lat}
</div>
`;
ol.locationMap.redLabelOverlay.setPosition(e.coordinate);
});
}
// 기본지도 타입 라디오 버튼 클릭 이벤트
document.querySelectorAll('.project-location-modal input[name="location-base-map"]').forEach(radio => {
radio.addEventListener('change', function() {
if (!ol.locationMap) return;
const type = this.value;
// 위치 설정 모달의 임시 레이어만 변경
ol.locationMap.tempRoadLayer.setVisible(false);
ol.locationMap.tempSatelliteLayer.setVisible(false);
ol.locationMap.tempHybridLayer.setVisible(false);
switch(type) {
case 'road':
ol.locationMap.tempRoadLayer.setVisible(true);
break;
case 'hybrid':
ol.locationMap.tempHybridLayer.setVisible(true);
break;
case 'satellite':
ol.locationMap.tempSatelliteLayer.setVisible(true);
break;
}
});
});
// 모달 닫기 함수
function closeLocationModal() {
document.querySelector('.project-location-modal').style.display = 'none';
if (ol.locationMap) {
ol.locationMap.setTarget(null);
if (typeof ol.locationMap.dispose === 'function') {
ol.locationMap.dispose();
}
ol.locationMap = null;
}
// 메인 지도 레이어 상태 복원
if (mainLayerState) {
vars.roadLayer.setVisible(mainLayerState.road);
vars.satelliteLayer.setVisible(mainLayerState.satellite);
vars.hybridLayer.setVisible(mainLayerState.hybrid);
mainLayerState = null;
}
}
File diff suppressed because it is too large Load Diff
+663
View File
@@ -0,0 +1,663 @@
import { vars } from './variable.js';
//설정창 close 25-08-18 권한 설정 모달 사용 중에 모달이 꺼지는일이 많아 주석처리로 꺼지는 기능을 막음
// document.querySelector('.permission-modal').addEventListener('click', (e) => {
// if (e.target.className.includes('permission-modal')) document.querySelector('.permission-modal').style.display = 'none';
// })
document.querySelector('.permission-modal img[alt="닫기"]').addEventListener('click', () => {
// 이름 검색 결과창 초기화
searchResultInit();
document.querySelector('.permission-modal').style.display = 'none';
})
//설정창 저장
document.getElementById('permission-submit').addEventListener('click', () => {
if(vars.permissionList.changed && Object.keys(vars.permissionList.changed).length > 0){
if (confirm(`${Object.keys(vars.permissionList.changed).length}개의 변경사항을 저장하시겠습니까?`)) {
upsertAuthList();
}
} else {
searchResultInit();
document.querySelector('.permission-modal').style.display = 'none';
}
});
//설정창 취소
document.getElementById('permission-cancel').addEventListener('click', () => {
if (vars.permissionList.changed && Object.keys(vars.permissionList.changed).length > 0) {
if (!confirm(`${Object.keys(vars.permissionList.changed).length}개의 변경사항을 저장하지 않고 취소하시겠습니까?`)) {
return;
}
}
// 이름 검색 결과창 초기화
searchResultInit();
document.querySelector('.permission-modal').style.display = 'none';
});
//tab 변경
document.querySelectorAll('.permission-modal input[type="radio"]').forEach(ele => {
ele.addEventListener('click', (e) => {
document.querySelector('.permission-modal .modal-wrap .left .search-result-container').style.display = 'none';
document.querySelector('.permission-modal .modal-wrap .left ul').style.display = 'block';
let id = e.target.id;
let tab = document.querySelector('.permission-modal .select-list-' + id);
drawUserPermissionList(vars.permissionList, id);
[...tab.parentElement.children].forEach(element => {
if (element.classList.contains('select-list')) element.style.display = 'none';
});
tab.style.display = 'block';
})
});
export async function getList() {
let param = {
// project_id : JSON.parse(localStorage.getItem('data')).id
project_id: vars.project_id
}
let res = await axios.get('/auth/getMemberList', { params: param });
return res.data;
}
// 유저권한설정창
export async function drawUserPermissionList(list, rightId = 'sub-master') {
document.querySelector('.permission-modal .left ul').innerHTML = '';
document.querySelectorAll(`.permission-modal .select-list ul`).forEach(ul => ul.innerHTML = '');
// DB에 저장횐 company-list가 6개 외에 더 있어서 회사 리스트 고정
const companyList = [ '한맥기술', '삼안', '장헌산업', `(주)장헌` , '피티씨' , '한라산업개발', '바론컨설턴트' , '기타'];
// 기존 권한을 가진 인원(key-value)들을 배열화
const permission = Object.values(list.changePermission);
try {
// changePermission에는 user_id - lev만 있기때문에 배열을 DB로 보내 유저 정보를 가져온다
const usersInfoRes = await axios.post('/auth/getPermissionUserInfo', {permission : permission});
const userInfoArr = usersInfoRes.data.result;
const rightUl = document.querySelector(`.permission-modal .select-list.select-list-${rightId} ul`);
userInfoArr.forEach(user => {
if (findPermission(user.user_id, permission) !== rightId) return;
if (rightUl.querySelector(`#_${user.user_id}`)) return;
// 유저 리스트 DOM 생성
const userLi = drawUserList(user, rightId, permission, true);
rightUl.appendChild(userLi);
// 클릭 이벤트
bindUserLiClick(userLi, user, rightId, permission, true);
});
// 유저 수 체크
checkPermissionUserNum(rightId);
} catch (err) {
console.error('getPermissionUserInfo', err);
}
// 회사 리스트 생성
companyList.forEach(company => {
const companyTitle = drawCompanyTitle(company);
const companyToggleImg = companyTitle.querySelector('.company-toggle img');
// 회사 리스트 클릭시 부서명 생성
companyTitle.addEventListener('click', async () => {
let deptTitle = companyTitle.nextElementSibling;
// 부서명 최초 1회 DOM 생성 이후에 토글
if (!deptTitle || !deptTitle.classList.contains('dept-container')) {
companyToggleImg.src = '/main/img/permission/down.svg';
deptTitle = document.createElement('ul');
deptTitle.className = 'dept-container _scrollbar';
deptTitle.dataset.company = company;
companyTitle.insertAdjacentElement('afterend', deptTitle);
try {
// 회사명을 통해 부서리스트 조회
const deptRes = await axios.get('/auth/getDeptList', { params: { company } });
if (deptRes.data.message === '200') {
const deptList = deptRes.data.result;
// 한글 정렬
deptList.sort((a, b) => {
if (!a.dept || !b.dept) return 0;
return a.dept.localeCompare(b.dept, 'ko');
});
// 부서 리스트 생성
for (const depts of deptList) {
if (!depts.dept) continue;
const deptLi = drawDeptTitle(company, depts.dept);
const deptToggleImg = deptLi.querySelector('.dept-toggle img');
const deptCheckBox = deptLi.querySelector('input[type="checkbox"]');
const deptToggelWrap = deptLi.querySelector('.toggle-wrap');
// 부서 개별 체크 박스 change 이벤트
deptCheckBox.addEventListener('change', async (e) => {
const isChecked = deptCheckBox.checked;
const rightUl = document.querySelector(`.permission-modal .right .select-list.select-list-${rightId} ul`);
try {
// 회사명 - 부서를 통해 부서의 유저를 전부 조회
const userRes = await axios.get('/auth/getUserList', { params: { company, dept: depts.dept }});
const userList = userRes.data.result;
// 부서 전체 선택시에 이미 권한이 있어 권한을 부여할 수 없는 인원 수를 체크하는 배열
const diffGroupArr = [];
// 유저 리스트 생성
for (let user of userList) {
const existingRight = rightUl.querySelector(`#_${user.user_id}`);
const existingLeft = document.querySelector(`.permission-modal .left ul li#_${user.user_id} .user-wrap`);
const lev = findPermission(user.user_id, Object.values(vars.permissionList.changePermission));
// 전체 선택시
if (isChecked) {
// 이미 오른쪽(권한이 있던)에 있는 인원과 권한이 다른 인원은 continue로 건너뛰기
if (existingRight) continue;
if (lev && lev !== rightId){
diffGroupArr.push(user);
continue;
}
// 부관리자 10명 제한
if (rightId === 'sub-master' && rightUl.children.length >= 10) {
updateCheckboxState(user.company, user.dept);
return alert('부관리자는 최대 10명까지 선택 가능합니다.');
}
// 전체선택으로 권한 변경된 유저 권한 변경
changePermission(user.user_id, rightId, undefined);
// 오른쪽 리스트에 생성
const copy = drawUserList(user, rightId, permission, true);
rightUl.appendChild(copy);
// 클릭이벤트 추카
bindUserLiClick(copy, user, rightId, permission, true);
// 왼쪽 유저리스트에 권한 뱃지 생성
if (existingLeft) {
const roleDiv = createRoleDiv(rightId);
existingLeft.insertAdjacentHTML('beforeend', roleDiv);
existingLeft.classList.add('selected_' + rightId);
existingLeft.style.display = 'flex';
}
} else {
// 전체 선택해제
if (existingRight) {
// 오른쪽 DOM 삭제와 권한 해제
existingRight.remove();
changePermission(user.user_id, undefined, rightId);
}
// 왼쪽 유저 리스트에 권한 뱃지 삭제
existingLeft?.querySelector(`.user-permission-${rightId}`)?.remove();
existingLeft?.classList.remove(`selected_${rightId}`);
}
}
// 체크 박스 상태 변화 감지
updateCheckboxState(company, depts.dept);
if (diffGroupArr.length > 0){
alert(`이미 권한을 가진 ${diffGroupArr.length}명은 ${{'sub-master' : '부관리자', 'security-worker' : '보안참여자', 'worker' : '일반참여자', 'viewer' : '참관자'}[rightId]} 권한을 부여할 수 없습니다.`);
deptCheckBox.checked = true;
}
} catch (err) {
console.error('getUserList Error (checkbox): ', err);
}
});
// 체크 박스 상태 변화 감지
updateCheckboxState(company, depts.dept);
// 부서명 클릭 이벤트
deptToggelWrap.addEventListener('click', async () => {
let userTitle = deptLi.nextElementSibling;
// 유저 최초 1회 생성 이후에 토글
if (!userTitle || !userTitle.classList.contains('user-container')) {
deptToggleImg.src = '/main/img/permission/down.svg';
userTitle = document.createElement('ul');
userTitle.className = 'user-container _scrollbar';
deptLi.insertAdjacentElement('afterend', userTitle);
// 회사명,부서명으로 유저리스트 조회
const userRes = await axios.get('/auth/getUserList', { params: { company, dept: depts.dept }});
if (userRes.data.message === '200') {
const userList = userRes.data.result;
// 유저리스트 생성
userList.forEach(user => {
// 유저 DOM 생성
const userLi = drawUserList(user, rightId, permission);
userTitle.appendChild(userLi);
// 유저 클릭 이벤트
bindUserLiClick(userLi, user, rightId, permission);
});
}
}
// 유저 토글 처리
userTitle.style.display = userTitle.style.display === 'block' ? 'none' : 'block';
deptToggleImg.src = userTitle.style.display === 'block' ? '/main/img/permission/down.svg' : '/main/img/permission/up.svg';
});
}
}
} catch (error) {
console.error('getDeptList Error', error);
}
}
// 부서 토글처리
deptTitle.style.display = deptTitle.style.display === 'block' ? 'none' : 'block';
companyToggleImg.src = deptTitle.style.display === 'block' ? '/main/img/permission/down.svg' : '/main/img/permission/up.svg';
});
});
}
// 유저 검색 기능 엔터 이벤트
document.getElementById('permission-search').addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
// input 값 추출 이후 DOM 생성
const keyword = e.target.value.trim().toLowerCase();
searchResultUser(keyword);
}
});
// 유저 검색 기능 클릭 이벤트
document.querySelector('.permission-modal .modal-wrap .left .search-box .wrap .search-image').addEventListener('click', () => {
const keyword = document.querySelector('.permission-modal .modal-wrap .left .search-input').value.trim().toLowerCase();
searchResultUser(keyword);
});
// 유저 검색 기능(엔터, 검색바 클릭)
function searchResultUser(keyword) {
// 프로젝트 관리자를 제외하고 검색하기 위해 필터링
const users = vars.permissionList.all.filter(user => user.user_id !== vars.project.user_id);
const searchResult = document.querySelector('.permission-modal .modal-wrap .left .search-result-container');
const permissionLeftUl = document.querySelector('.permission-modal .modal-wrap .left ul');
const permission = Object.values(vars.permissionList.changePermission);
const rightId = document.querySelector('.permission-modal .modal-wrap .tab-input:checked').id;
const positionMap = {'회장': 1, '부회장':2, '사장':3, '상임고문':4, '기술위원':5, '부사장':6, '고문':7, '전무이사':8, '수석연구원':9, '상무이사':10, '이사':11, '책임연구원':12, '부장':13, '차장':14, '선임연구원':15, '과장':16, '연구원':17, '대리':18, '사원':19};
if (keyword !== '') {
// 이름으로 필터링 이후 직급으로 정렬
const result = users.filter(user => user.user_nm?.toLowerCase().includes(keyword)).sort((a,b) => {
const aPosition = positionMap[a.position] ?? 99;
const bPosition = positionMap[b.position] ?? 99;
if(aPosition !== bPosition) return aPosition - bPosition;
return a.user_id.localeCompare(b.user_id);
})
if (result.length === 0) {
searchResult.style.display = 'none';
permissionLeftUl.style.display = 'block';
return alert('일치하는 사용자가 없습니다.');
}
searchResult.innerHTML = '';
// 조회된 유저 있을때
result.forEach(user => {
//유저 DOM 생성
const userLi = drawUserList(user, rightId, permission);
searchResult.appendChild(userLi);
//클릭이벤트
bindUserLiClick(userLi, user, rightId, permission, false);
});
// 검색결과창 띄우기
searchResult.style.display = 'block';
permissionLeftUl.style.display = 'none';
} else {
// 빈 값 입력시 검색결과창 내리기
searchResult.style.display = 'none';
permissionLeftUl.style.display = 'block';
}
}
// 유저 검색결과 초기화(닫기버튼, X버튼 클릭시 실행)
function searchResultInit(){
const searchResult = document.querySelector('.search-result-container');
searchResult.innerHTML = '';
searchResult.style.display = 'none';
document.querySelector('.permission-modal .modal-wrap .left ul').style.display = 'block';
document.querySelector('.permission-modal .left .search-input').value = '';
}
// 회사명 DOM 생성
function drawCompanyTitle(company){
if(company == '(주)장헌') company = '장헌';
if(company == '피티씨') company = 'PTC';
const companyTitle = document.createElement('li');
companyTitle.className = 'company-title';
companyTitle.innerHTML += `
<img class="" src="/main/img/permission/s-icon__${company}-large-logo.svg" alt="" />
<div class="toggle-wrap">
<span class="company-name">${(company == '기타')? '' :"(주)"}${company}</span>
<div class="company-toggle">
<img src="/main/img/permission/up.svg" alt="방향표">
</div>
</div>
`
document.querySelector('.permission-modal .left ul').append(companyTitle);
return companyTitle;
}
// 부서명 DOM 생성
function drawDeptTitle(company, dept){
const deptLi = document.createElement('li');
deptLi.className = 'dept-title';
deptLi.dataset.company = company;
deptLi.dataset.dept = dept;
deptLi.innerHTML = `
<label class="custom-checkbox">
<input type="checkbox">
<span class="checkmark"></span>
</label>
<div class="toggle-wrap">
<span class="dept-name">${dept}</span>
<div class="dept-toggle">
<img src="/main/img/permission/up.svg" alt="방향표">
</div>
</div>
`;
document.querySelector(`.permission-modal .left ul .dept-container[data-company="${company}"]`).appendChild(deptLi)
return deptLi;
}
// 유저리스트 DOM 생성
function drawUserList(user, rightId, permissionList, isRightSide = false) {
const li = document.createElement('li');
li.className = 'user-li';
li.id = `_${user.user_id}`;
li.dataset.company = user.company;
li.dataset.dept = user.dept;
const userWrap = document.createElement('div');
userWrap.className = `user-wrap${isRightSide ? ` selected_${rightId} _scrollbar` : ''}`;
userWrap.innerHTML += `
<div class="user-img">
<img draggable="false" src="http://erp.${(user.company === '장헌산업' ? 'jangheon' : 'hanmaceng')}.co.kr/erpphoto/${user.user_id}.jpg" alt="이미지" onerror="this.onerror=null; this.src='/main/img/archive/empty-profile.svg'">
</div>
<div class="user-info">
<div class="user-path">
<img class="user-logo" src="/main/img/permission/s-icon__${(user.company == '피티씨')?'PTC':user.company}-small-logo.svg" alt="" />
<span class="user-team">${user.dept}</span>
</div>
<div class="user-top">
<span class="user-name">${user.user_nm}</span>
<span class="user-position">${user.position ?? ''}</span>
</div>
</div>
`;
// 유저 권한 확인
const lev = findPermission(user.user_id, Object.values(vars.permissionList.changePermission));
const role = isRightSide ? rightId : lev;
// 유저권한 생성
if (role) {
const roleDiv = createRoleDiv(role);
userWrap.insertAdjacentHTML('beforeend', roleDiv);
if (!isRightSide && lev !== rightId) userWrap.classList.add('disabled');
if (!isRightSide) userWrap.classList.add(`selected_${lev}`);
}
li.appendChild(userWrap);
return li;
}
// 유저 권한 뱃지 생성
function createRoleDiv(role){
return `<div class="user-permission-${role}">
<h6>${{'sub-master' : '부관리', 'security-worker' : '보안', 'worker' : '일반', 'viewer' : '참관'}[role]}</h6>
</div>`;
}
// 유저 클릭이벤트 바인딩
function bindUserLiClick(userLi, user, rightId, permissionList, isRightSide = false) {
const userWrap = userLi.querySelector('.user-wrap');
const rightUl = document.querySelector(`.permission-modal .select-list.select-list-${rightId} ul`);
userLi.addEventListener('click', () => {
const rightUser = rightUl.querySelector(`li#_${user.user_id}`);
const leftUserWrap = document.querySelector(`.permission-modal .left ul li#_${user.user_id} .user-wrap`);
const leftUserPermission = leftUserWrap?.querySelector(`.user-permission-${rightId}`);
if (userWrap.classList.contains('disabled')) return alert('다른 그룹에 등록된 인원입니다.');
const alreadySelected = userWrap.classList.contains(`selected_${rightId}`);
// 이미 권한을 가진 인원 클릭했을때 해제 이벤트
if (alreadySelected) {
changePermission(user.user_id, undefined, rightId);
// 오른쪽 유저 삭제(오른쪽 리스트 유저 삭제)
rightUser.remove();
// 왼쪽 유저 삭제(뱃지삭제 -> userWrap 클래스 삭제) 리스트가 펼쳐지지않아 아직 DOM 생성전에는 삭제가 불가능하기 때문에 옵셔널체이닝 추가
leftUserPermission?.remove();
leftUserWrap?.classList.remove(`selected_${rightId}`);
} else {
// 권한 부여 이벤트
if (rightId === 'sub-master' && rightUl.children.length >= 10) return alert('부관리자는 최대 10명까지 선택 가능합니다.');
changePermission(user.user_id, rightId, undefined);
userWrap.classList.add(`selected_${rightId}`);
userWrap.insertAdjacentHTML('beforeend', createRoleDiv(rightId));
// 왼쪽 유저리스트 클릭시 오른쪽에 복사하여 생성+이벤트 추가
const copy = drawUserList(user, rightId, permissionList, true);
rightUl.appendChild(copy);
bindUserLiClick(copy, user, rightId, permissionList, true);
}
updateCheckboxState(user.company, user.dept);
// 유저 수 조회 이후 갱신
checkPermissionUserNum(rightId);
});
}
document.querySelector('.permission-modal #help-btn').addEventListener('click', () => toggleHelp(true));
document.querySelector('.permission-modal .modal-permission-help .modal-permission-help__head img').addEventListener('click', ()=>toggleHelp(false));
function toggleHelp(show){
const helpModal = document.querySelector('.permission-modal .modal-permission-help');
const modalBack = document.querySelector('.permission-modal .modal-background');
helpModal.style.display = show ? 'block' : 'none';
modalBack.style.display = show ? 'block' : 'none';
}
// 권한을 통해 유저 수 체크
function checkPermissionUserNum(rightId){
// 오른쪽 리스트에 몇명이 있는지 체크
const userNum = document.querySelector(`.permission-modal .modal-wrap .right .select-list.select-list-${rightId} ul`).children.length;
// 유저 수를 나타낼 dom
const headerUserNum = document.querySelector('.permission-modal .modal-header .header-user-num-wrap .user-num');
// 부관리자 10명이 넘을때 강조효과
let text = `현재 ${userNum}`;
let color = '#111';
if(rightId === 'sub-master'){
text += ' (최대 10명)';
if(userNum >= 10) color = '#F21D0D';
}
headerUserNum.innerText = text;
headerUserNum.style.color = color;
}
// 체크박스 DOM, 회사명, 부서를 통해 체크박스 상태 반영 함수
async function updateCheckboxState(company, dept){
const deptUsers = document.querySelectorAll(`.permission-modal .modal-wrap .right ul li[data-company="${company}"][data-dept="${dept}"]`);
const checkbox = document.querySelector(`.permission-modal .modal-wrap .left li[data-company="${company}"][data-dept="${dept}"] input[type=checkbox]`);
const checkmark = document.querySelector(`.permission-modal .modal-wrap .left li[data-company="${company}"][data-dept="${dept}"] label .checkmark`);
const rightId = document.querySelector('.permission-modal .modal-wrap .tab-input:checked').id;
const key = `${company}_${dept}`;
//permissionList.deptUserCount와 현재 권한 부여된 유저의 길이를 비교해 체크박스 상태변화
if(vars.permissionList.deptUserCount[key]){
if(!checkbox) return;
if(deptUsers.length === 0){
checkbox.checked = false;
checkmark.classList.remove('indeterminate');
} else if(deptUsers.length === vars.permissionList.deptUserCount[key]) {
checkbox.checked = true;
checkmark.classList.remove('indeterminate');
} else {
checkbox.checked = false;
checkmark.classList.add('indeterminate');
}
}
checkPermissionUserNum(rightId);
}
function findPermission(id, permissionList) {
for (let i = 0; i < permissionList.length; i++) {
if (permissionList[i].user_id.toUpperCase() == id.toUpperCase()) return permissionList[i].lev;
}
return undefined;
}
function getUserInfo(id) {
for (let i = 0; i < vars.permissionList.all.length; i++) {
if (vars.permissionList.all[i].user_id == id) return vars.permissionList.all[i];
}
}
function changePermission(id, group, rightId) {
if (!vars.permissionList.changed) vars.permissionList.changed = [];
if (!vars.permissionList.changePermission) vars.permissionList.changePermission = {};
id = id.toUpperCase();
/*
vars.permissionList.changed => 변화객체
vars.permissionList.permission => 원본 permission
vars.permissionList.changePermission => 변화후 permission
*/
if (group) {
//그룹에 넣을때 key, value 형태로 저장
vars.permissionList.changePermission[id] = {user_id : id, lev : group}
} else {
//그룹에서 제외
delete vars.permissionList.changePermission[id];
}
// 기존 permission에서 이전 Lev 가져오기
const original = vars.permissionList.permission?.find(p => p.user_id.toUpperCase() === id);
const originalLev = original?.lev;
// 기존 permission과 새로운 permission을 비교해 변화 감지
if(group === originalLev || (!group && !originalLev)){
delete vars.permissionList.changed[id];
return;
}
// 변화감지하여 변화객체 저장 (중복성 피하기 위해)
vars.permissionList.changed[id] = {user_id : id, lev : group, before : originalLev, user_nm : getUserInfo(id)?.user_nm}
}
async function upsertAuthList() {
//lev undefined는 deleteAuthList
let upsertArr = [];
let deleteArr = [];
//key-value 형태로 만들었던 changed를 배열로 전환
const changedArr = Object.values(vars.permissionList.changed);
if (changedArr.length > 0) {
for (let i = 0; i < changedArr.length; i++) {
if (changedArr[i].lev == undefined) {
deleteArr.push(changedArr[i]);
} else {
upsertArr.push(changedArr[i]);
}
}
let upsertMessage = (upsertArr.length > 0) ? false : true;
let deleteMessage = (deleteArr.length > 0) ? false : true;
console.log(upsertArr);
console.log(deleteArr);
if (upsertArr.length > 0) {
let upsertRes = await axios.post('/auth/upsertPermission', { project_id: vars.project_id, targetArr: upsertArr, userInfoString: vars.userInfoString });
if (upsertRes.data.message == 'upsert success'){
const logs = upsertRes.data.logs;
if(logs.length > 0){
try{ // upsert 이후 로그 추가
const logRes = await axios.post(`${vars.path_name}/addPermissionLog`, {logs});
if(logRes.data.message === 'addPermissionLog success') upsertMessage = true;
} catch(err) {
console.error('upsert addPermissionLog Error', err);
}
}
}
}
if (deleteArr.length > 0) {
let deleteRes = await axios.post('/auth/deletePermission', { project_id: vars.project_id, targetArr: deleteArr, userInfoString: vars.userInfoString });
if (deleteRes.data.message == 'delete success'){
const logs = deleteRes.data.logs;
if(logs.length > 0){
try{ // delete 이후 로그 추가
const logRes = await axios.post(`${vars.path_name}/addPermissionLog`, {logs});
if(logRes.data.message === 'addPermissionLog success') deleteMessage = true;
} catch(err) {
console.error('delete addPermssionLog Error', err);
}
}
}
}
if (upsertMessage && deleteMessage) {
alert(`${Object.keys(vars.permissionList.changed).length}개의 변경사항이 저장되었습니다.`);
}
}
searchResultInit();
document.querySelector('.permission-modal').style.display = 'none';
}
export class Permission {
constructor(){
//permission 정의
this.permissionDef = {
1535 : '개발자',
255 : '관리자',
191 : '부관리자',
15 : '보안참여자',
7 : '일반참여자',
1 : '참관자'
}
//기능 정의 -'기능명': 허용 권한 code -> ex) 'headerPlusBtn' : 191
this.funcDef = {
'dev-menu' : 1535,
'permission-btn' : 191,
'overview-left-edit' : 191,
'overview-middle-edit' : 191,
'overview-task-history-add' : 191,
'overview-task-history-save' : 191,
'overview-task-history-delete' : 191,
'overview-schedule-add' : 7,
'overview-schedule-edit' : 7,
'overview-issue-edit' : 191,
'header-menu-add' : 191,
'memo-ai' : 7,
'memo-edit' : 7,
'memo-text' : 7,
'set-user-permission' : 191,
'context-menu-viewer' : 7,
'context-menu-sub-master' : 191,
'add-badge-master' : 255,
'add-badge-sub-master' : 191,
'add-badge-security-worker': 15,
'add-badge-worker': 7,
'add-badge-viewer': 1,
'convert-btn-viewer': 7,
'project-inactive-sign' : 1535,
'createFolder-gallery' : 1535,
'download-folder' : 255,
'change-project-btn' : 255,
}
this.user = (vars.userInfoString)?JSON.parse(vars.userInfoString) : undefined;
this.permission = this.user.permission;
this.permissionString = this._toPermissionString();
}
_toPermissionString(){
return this.permissionDef[`${this.permission}`];
}
checkPermission(func){
return this.permission >= this.funcDef[func];
}
}
+213
View File
@@ -0,0 +1,213 @@
let path_name = decodeURIComponent(window.location.pathname);
let project_id = path_name.split('/')[1];
//////// pm-bcmf 연결용 테스트 코드 - project_id가 pm-bcmf일 때 project_id는 dsdj2, path_name은 /dsdj2/archive로 변경
if (project_id == 'pm-bcmf') {
project_id = 'dsdj2';
path_name = '/dsdj2/archive';
}
//세종안성(sjas)(gtb.) and 청용천교(cheongyong)(cheongyong.) 연결용 테스트 코드
if(project_id == '' || project_id == undefined) {
if(window.location.origin.includes('gtb.')){
project_id = 'sjas';
path_name = '/sjas/archive';
}/* else if(window.location.origin.includes('cheongyong.')){
project_id = 'cheongyong';
path_name = '/cheongyong/archive';
} */
}
export const vars = {
// 아카이브 라우터 연결을 위한 변수
path_name: path_name,
// 프로젝트 id
project_id: project_id,
// 오브젝트 스토리지 타입 (ONPREMISE / CLOUD)
storageType: undefined,
// 클라우드 타입 (ONPREMISE / CLOUD)
cloudType: undefined,
// 버킷 (프로젝트 id와 동일)
bucket: project_id,
// 현재 로그인 한 유저 정보
userInfoString: undefined,
// 현재 로그인 한 유저의 permission 관리 객체
permission: undefined,
// 현재 로그인 한 유저의 마우스커서 정보를 담는 객체
cursors: {},
// 전체 프로젝트 정보
allProject: {},
// 현재 프로젝트 정보
project: undefined,
// 최상위 폴더 사용 용량 / 남은 용량 / 저장 가능한 최대 용량 (50 GB) -> DB에서 가져오기 / 폴더별 사이즈 (아카이브, 공문, 과업개요, GSIM 모델)
bucketSize: undefined, remainingSize: undefined, bucketMaxSize: 50*1024*1024*1024, foldersSize: undefined,
// 최상위 폴더 아래에 있는 모든 폴더/파일 데이터
allTreeObject: undefined,
// 현재 선택된 폴더 아래에 있는 모든 폴더/파일 데이터
currentTreeObject: undefined,
// 휴지통에 있는 모든 파일 데이터
recycleBinObject: undefined,
// 마지막으로 선택한 header-btn/tree-item/list-item -> 스타일 변경에 사용 - changeListItemStyle(listItem)
lastHeaderBtn: undefined, lastMainTreeItem: undefined, lastModalTreeItem: undefined, lastListItem: undefined, lastListGroupTarget: undefined,
// lastMainTreeDepth2Item: undefined, lastModalTreeDepth2Item: undefined,
// lastMainTreeDepth3Item: undefined, lastModalTreeDepth3Item: undefined,
// 마지막으로 우클릭한 타겟 -> 컨텍스트 메뉴에 사용
lastContextTarget: undefined,
// 마지막으로 선택한 header/tree/list item
lastSelectTarget: undefined,
// 드래그가 시작된 list-item
dragStartListItem: undefined,
// 클릭, 더블클릭 구분을 위한 시간
clickDuration: 500,
// 업로드, 다운로드 완료 후 프로그레스 종료 전 완료 후 잠깐 대기하는 시간
progressDuration: 500,
// 현재 변환중인 파일 정보를 저장하는 배열
convertingDataArr: [],
// 현재 요약중인 파일 정보를 저장하는 배열
summarizeAiDataArr: [],
// file area list에서 다중 선택된 item 배열
multiSelectListItemArr: [],
// 휴지통에서 사용할 lastListItem, lastListGroupTarget, lastContextTarget, lastSelectTarget, multiSelectListItemArr
// 휴지통에서 다중 선택된 item 배열
lastListItem_bin: undefined,
lastListGroupTarget_bin: undefined,
lastContextTarget_bin: undefined,
lastSelectTarget_bin: undefined,
multiSelectListItemArr_bin: [],
// 로그 데이터 -> 활동 정보 activity-info-item(로그 데이터)에 사용 (활동 정보 아이템 클릭 시 상세 정보 표시에 사용)
logData: undefined,
// 로그 모달창이 열렸는지 안열렸는지 판단하는 변수 -> 최초에만 false이고 한 번이라도 열리면 true로 유지
isLogModalOpened: false,
// 새로 추가되는 로그 아이템을 저장하는 배열
newLogItemArr: [],
// 일반아이템리스트/휴지통아이템리스트 구분에 사용
listItemWrap: document.querySelector('.archive-main-center .list-container .list-body .list-item-wrap'),
// 미리보기 뷰어
viewer: undefined,
// 메모 임시 저장
tempMemo: [],
// 파일 리스트 sort용 현재 컬럼, 현재 정렬순
curSortCol : 'name',
curSortOrder : 'asc',
// 휴지통 모달창 sort용 현재 컬럼, 현재 정렬순
curSortCol_bin : 'remove-date',
curSortOrder_bin : 'desc',
// 리스트/그리드 파일 목록에서 파일 선택했을 때 뷰어에 연결중 표시에 사용할 시간
viewerConnectingTime: 0, viewerConnectingTimeValue: 700,
// 트리 영역에서 마지막으로 선택한 폴더 타입
lastFolderType: undefined,
// 컨트롤 박스에서 마지막으로 선택한 파일 영역 모드
// lastFileAreaMode: undefined,
lastFileAreaMode: 'list',
// 지도 모드 (normal <-> edit)
mapMode: 'normal',
// 배경지도 url (일반, 위성, 하이브리드)
road: 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&hl=ko&&apistyle=s.e:l.i|p.v:off',
satellite: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&hl=ko&apistyle=s.e:l.i|p.v:off',
hybrid: 'https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&hl=ko&apistyle=s.e:l.i|p.v:off',
// 클러스터 거리 (픽셀 기준)
clusterDistance: 1,
// 마지막으로 선택한 클러스터
lastSelectCluster: undefined,
// 모든 오버레이를 표시하는 줌 레벨
allOverlayVisibleZoom: 19,
// 클러스터 레이어, 포인트 레이어
olClusterLayer: undefined,
olPointLayer: undefined,
listOrder: [],
countdownTimer: undefined,
}
// 오픈레이어스 타일 레이어
vars.baseLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: vars.road,
crossOrigin: 'anonymous',
transition: 0
})
});
vars.roadLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: vars.road,
crossOrigin: 'anonymous',
transition: 0
}),
visible: true
});
vars.satelliteLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: vars.satellite,
crossOrigin: 'anonymous',
transition: 0
}),
visible: false
});
vars.hybridLayer = new ol.layer.Tile({
source: new ol.source.XYZ({
url: vars.hybrid,
crossOrigin: 'anonymous',
transition: 0
}),
visible: false
});
// 구성 popup ver 위한 전역 함수
// vars.navigateToPath = function(path) {
// const resourcePath = `/${path}`;
// // 헤더 버튼 클릭 (탭 선택)
// const tabPath = resourcePath.split('/')[1]; // '모델뷰어테스트/모델/3dm' -> '모델뷰어테스트'
// const headerBtn = document.querySelector(`[data-resource-path="/${tabPath}"]`);
// if (headerBtn) headerBtn.click();
// // 트리 아이템 클릭 (폴더 선택)
// setTimeout(() => {
// const treeItem = document.querySelector(`.tree-item-wrap[data-resource-path="${resourcePath}"] .tree-item`);
// if (treeItem) treeItem.click();
// }, 500);
// }
window.vars = vars;
+856
View File
@@ -0,0 +1,856 @@
import { AntiDebug } from '../../anti_debugging.js';
let antiDebug = new AntiDebug();
import { vars } from './archive/variable.js';
import { Permission } from './archive/userPermission.js';
import { docVars } from './officialDoc/docVariable.js';
import { overviewVars } from './overview/overviewVariable.js';
import {getMyDownloadList} from './archive/common.js';
// import iconv from 'https://cdn.jsdelivr.net/npm/iconv-lite@0.6.3/lib/index.min.js';
import { initProjectSetting } from './archive/projectSetting.js';
document.addEventListener('DOMContentLoaded', async function() {
//// .env파일에 저장된 배포 타입, 클라우드 타입 가져와서 전역변수에 storageType, cloudType 으로 저장
let getEnvDataRes = await axios.get('/common/getEnvData');
vars.storageType = getEnvDataRes.data.deploymentType;
vars.cloudType = getEnvDataRes.data.cloudType;
vars.serviceName = getEnvDataRes.data.serviceName;
docVars.storageType = getEnvDataRes.data.deploymentType;
//// 프로젝트 정보 가져와서 전역변수에 저장
let projectRes = await axios.get('/common/getProject');
for (let project of projectRes.data.data) {
vars.allProject[project.project_id] = project;
}
vars.project = vars.allProject[vars.project_id];
//// 타이틀 설정
await setTitle();
//// 프로젝트별 최대 용량 설정
vars.bucketMaxSize = (vars.project) ? parseInt(vars.project.storage_byte) * 1024 * 1024 * 1024 : 0;
//// 유저 정보 객체 가져와서 스트링으로 전역 변수에 저장
let authStatusRes = await axios.get('/auth/status',{ params: { project_id: vars.project_id } });
let userInfo = authStatusRes.data.user;
vars.userInfoString = JSON.stringify(userInfo);
//////// pm-bcmf 연결용 테스트 코드 - BCMF 유저 정보 생성, 타이틀 커서 스타일 변경, 자동 경로 이동용 변수 설정
let getBcmfUrlQueryRes = await axios.get('/auth/getBcmfUrlQuery');
if (getBcmfUrlQueryRes.data.bcmfId && getBcmfUrlQueryRes.data.bcmfId.includes('bcmf-')) {
// BCMF 유저 정보 생성
let permission = (getBcmfUrlQueryRes.data.bcmfId.split('-')[1] == 'viewer') ? 1 : 7
userInfo = {
user_id: getBcmfUrlQueryRes.data.bcmfId,
user_nm: "BCMF",
company: "BCMF",
dept: "대산당진 2공구",
position: "",
group: "bcmf",
is_resigned: false,
permission: permission,
bookmark: null,
}
vars.userInfoString = JSON.stringify(userInfo);
// 타이틀에 마우스 올렸을 때 기본 커서로 표시
document.querySelector('body > .header .left .title').style.cursor = 'default';
// bcmf에서 iframe으로 pm 화면 연결하는 경우 헤더, 푸터 숨김
document.querySelector('body > .header').style.display = 'none';
document.querySelector('body > .footer').style.display = 'none';
// 헤더가 숨겨져 있으므로 높이에서 footer height(2.25rem)만 빼고, top은 0으로 설정
document.querySelectorAll('body > .main').forEach(main => {
main.style.height = '100dvh';
main.style.top = '0';
})
// 시작경로를 파라미터로 전달한 경우 해당 경로로 자동 이동을 위한 변수 설정
if (getBcmfUrlQueryRes.data.startPath) {
let startPath = getBcmfUrlQueryRes.data.startPath;
let startPathSplit = startPath.split('/');
vars.startPathDepth1 = startPathSplit[0];
vars.startPathDepth2 = startPathSplit[1];
vars.startPathDepth3 = startPathSplit[2];
}
}
if (checkProjectInactive()) setProjectInactive('lock');
// vars.allProject에서 각 프로젝트별 배너 공지(banner_notice) 사용해서 배너 공지 표시/해제 설정
toggleBannerNoticeArea();
//// 접속 인원 모달창에 프로젝트 담당자 표시
// let projectManagerName = document.querySelector('.archive-modal .modal-header > .title .right-wrap .project-manager-name');
let projectManagerName = document.querySelector('.archive-modal .modal-wrap .modal-body .connected-users-wrap .project-setting-wrap .project-manager-wrap .project-manager-name');
projectManagerName.textContent = (vars.project) ? `${vars.project.user_nm} ${vars.project.position}` : '-';
if (vars.project && !vars.project.user_nm) projectManagerName.textContent = '-';
// 유저 정보로 permission 객체 생성
vars.permission = new Permission();
//permission 객체 수정 못하도록 freeze
Object.freeze(vars.permission);
Object.freeze(Object.prototype);
// 조건 위치를 permission 객체 함수를 사용하기 위해 객체 생성 이후로 위치 조정 main.js/33 -> main.js/47
// 개발자 계정인 경우
if (vars.permission.checkPermission('project-inactive-sign')) {
// 프로젝트가 비활성화 상태이면 project-inactive-sign on
if (!vars.project.is_active) {
document.querySelector('.project-inactive-sign').style.display = 'flex';
}
}
//// 각 페이지별 index.js 임포트
const archiveIndex = await import(`./archive/index.js`);
if (!archiveIndex.isLoaded) {
archiveIndex.loadArchive();
}
// 과업개요 index.js 임포트
if(vars.project && vars.project.overview) await import(`./overview/index.js`);
//// 유저 권한에 따라 초기 dom객체 표시 or 삭제
await removeDomByPermission(userInfo);
//// 컴포넌트 초기 설정
await componentsInit();
//// 실시간 마우스 표시
document.addEventListener('mousemove', throttle((event) => {
vars.socket.emit('setMouseInfo', {
x: event.clientX / window.innerWidth,
y: event.clientY / window.innerHeight,
});
}, 10));
vars.socket.on('setMouse', (data) => {
// 자신의 커서는 생성하거나 업데이트하지 않음
if (data.clientId === vars.socket.id) {
return;
}
if (!vars.cursors[data.clientId]) {
// 새로운 클라이언트 ID에 대한 커서 생성
let cursor = document.createElement('div');
cursor.className = 'cursor';
let wrap = document.createElement('div');
wrap.className = 'wrap';
let userInfo = data;
let userColor = data.userColor;
let userText = document.createElement('div');
userText.className = 'user-text';
userText.textContent = `${userInfo.user_nm} ${userInfo.position}`;
// userText.style.color = userColor;
userText.style.background = userColor;
let cursorSvg = `
<svg width="30" height="30" viewBox="0 0 50 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.6279 2L14.611 43.1659L26.1686 30.0568L43.605 31.2398L14.6279 2Z"
stroke="#cccccc" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.7041 4.61523L15.6894 40.3646L25.7263 28.9804L40.8684 30.0077L15.7041 4.61523Z"
fill="${userColor}" stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
</svg>
`;
// let cursorSvg = `
// <svg width="30" height="30" viewBox="0 0 50 52" fill="none" xmlns="http://www.w3.org/2000/svg">
// <path d="M15.7041 4.61523L15.6894 40.3646L25.7263 28.9804L40.8684 30.0077L15.7041 4.61523Z"
// fill="${userColor}"/>
// </svg>
// `;
cursor.innerHTML = cursorSvg;
wrap.appendChild(userText);
cursor.appendChild(wrap);
document.body.appendChild(cursor);
vars.cursors[data.clientId] = cursor;
vars.cursors[data.clientId].id = data.clientId;
vars.cursors[data.clientId].timer = setTimeout(() => {
if (vars.cursors[clientId]) {
vars.cursors[clientId].remove();
delete vars.cursors[clientId];
}
}, 3000);
}
// 커서 위치 업데이트
vars.cursors[data.clientId].style.left = (data.x * window.innerWidth - 7) + 'px';
vars.cursors[data.clientId].style.top = data.y * window.innerHeight + 'px';
//타이머 재설정
if (vars.cursors[data.clientId].timer) {
clearTimeout(vars.cursors[data.clientId].timer);
}
vars.cursors[data.clientId].timer = setTimeout(() => {
if (vars.cursors[data.clientId]) {
vars.cursors[data.clientId].remove();
delete vars.cursors[data.clientId];
}
}, 3000);
});
vars.socket.on('userDisconnected', (clientId) => {
// 연결이 끊긴 클라이언트의 커서 제거
if (vars.cursors[clientId]) {
vars.cursors[clientId].remove();
delete vars.cursors[clientId];
}
});
save_user();
});
// ** 권한 관련
async function removeDomByPermission(userInfo) {
if (userInfo.permission != undefined || userInfo.permission != null || userInfo.permission != '') {
// 기능별 권한이 변경될 경우를 고려해서 각자 개별 분리 시켜주었습니다.
// 개발자도구 모달
const devMenuModal = document.querySelector('.dev-menu-modal');
// 개발자도구 모달은 키는 함수가 따로 있기 때문에 권한이 없다면 DOM 삭제만 진행
if(devMenuModal){
if(!vars.permission.checkPermission('dev-menu')) devMenuModal.remove();
}
// 헤더 폴더 추가 버튼 (부관리자 이상)
const headerMenuAddBtn = document.querySelector('.menu-add');
if(headerMenuAddBtn){
if(vars.permission.checkPermission('set-user-permission')){
headerMenuAddBtn.style.setProperty('display', 'flex', 'important');
} else {
headerMenuAddBtn.remove();
}
}
// 유저 권한 설정 버튼 (부관리자 이상)
const setUserPermissionBtn = document.querySelector('.set-user-permission-btn');
if(setUserPermissionBtn){
if(vars.permission.checkPermission('set-user-permission')){
setUserPermissionBtn.style.setProperty('display', 'flex', 'important');
} else {
setUserPermissionBtn.remove();
}
}
// 과업개요 왼쪽 영역 수정 버튼 (부관리자 이상)
const overviewSectionLeftBtn = document.querySelector('.overview .section-left-edit');
if(overviewSectionLeftBtn){
if(vars.permission.checkPermission('overview-left-edit')){
overviewSectionLeftBtn.style.setProperty('display', 'flex', 'important');
} else {
overviewSectionLeftBtn.remove();
}
}
// 과업개요 가운데 영역 수정 버튼 (부관리자 이상)
const overviewSectionMiddleBtn = document.querySelector('.overview .section-middle-edit');
if(overviewSectionMiddleBtn){
if(vars.permission.checkPermission('overview-middle-edit')){
overviewSectionMiddleBtn.style.setProperty('display', 'flex', 'important');
} else {
overviewSectionMiddleBtn.remove();
}
}
// 과업개요 주요 현안 및 이슈 수정 버튼 (부관리자 이상)
const overviewIssueBtn = document.querySelector('.overview .issue-edit');
if(overviewIssueBtn){
if(vars.permission.checkPermission('overview-issue-edit')){
overviewIssueBtn.style.setProperty('display', 'flex', 'important');
} else {
overviewIssueBtn.remove();
}
}
// 과업개요 - 과업기간 - 변동내역 모달 - 리스트 추가 버튼 (부관리자 이상)
const overviewTaskHistoryAddBtn = document.querySelector('.overview-modal .task-history-add');
if(overviewTaskHistoryAddBtn){
if(vars.permission.checkPermission('overview-task-history-add')){
overviewTaskHistoryAddBtn.style.setProperty('display', 'flex', 'important');
} else {
overviewTaskHistoryAddBtn.remove();
}
}
// 과업개요 - 과업기간 - 변동내역 모달 - 저장 버튼 (부관리자 이상)
const overviewTaskHistorySaveBtn = document.querySelector('.overview-modal .task-history-save');
if(overviewTaskHistorySaveBtn){
if(vars.permission.checkPermission('overview-task-history-save')){
overviewTaskHistorySaveBtn.style.setProperty('display', 'flex', 'important');
} else {
overviewTaskHistorySaveBtn.remove();
}
}
// 과업개요 - 주요일정 - 일정 추가버튼 (일반 참여자 이상)
const overviewScheduleAddBtn = document.querySelector('.overview .add-schedule-btn');
if(overviewScheduleAddBtn){
if(vars.permission.checkPermission('overview-schedule-add')){
overviewScheduleAddBtn.style.setProperty('display', 'flex', 'important');
} else {
overviewScheduleAddBtn.remove();
}
}
// 아카이브 - 메모 - AI버튼 (일반 참여자 이상)
const memoAiBtn = document.querySelector('.memo .ai-btn');
if(memoAiBtn){
if(vars.permission.checkPermission('memo-ai')){
memoAiBtn.style.setProperty('display', 'flex', 'important');
} else {
memoAiBtn.remove();
}
}
// 아카이브 - 메모 - 수정버튼 (일반 참여자 이상)
const memoEditBtn = document.querySelector('.memo .edit-btn');
if(memoEditBtn){
if(vars.permission.checkPermission('memo-edit')){
memoEditBtn.style.setProperty('display', 'flex', 'important');
} else {
memoEditBtn.remove();
}
}
// 폴더 다운로드 버튼 - 관리자 이상
const downloadFolderBtn = document.querySelector('.header .download-btn');
if(downloadFolderBtn){
if(vars.permission.checkPermission('download-folder')){
await getMyDownloadList();
downloadFolderBtn.style.setProperty('display', 'flex', 'important');
} else {
downloadFolderBtn.remove();
}
}
// 프로젝트 step, type, name 변경 - 관리자 이상
const projectStepBtn = document.getElementById('project-step-btn');
const projectStepCapsule = document.getElementById('project-step-capsule');
const projectTypeBtn = document.getElementById('project-type-btn');
const projectTypeCapsule = document.getElementById('project-type-capsule');
if(projectStepBtn && projectStepCapsule && projectTypeBtn && projectTypeCapsule) {
if(vars.project.category === 'overseas') {
initProjectSetting('overseas');
} else if (vars.project.category === 'bimproject') {
initProjectSetting('bimproject');
} else {
initProjectSetting();
}
}
// if(projectStepBtn && projectStepCapsule && projectTypeBtn && projectTypeCapsule && (window.location.href.toLowerCase().includes('bim.') || window.location.href.toLowerCase().includes('overseas.'))){
// if(projectStepBtn && projectStepCapsule && projectTypeBtn && projectTypeCapsule && (window.location.href.toLowerCase().includes('172.') || window.location.href.toLowerCase().includes('overseas.'))){
// initProjectSetting();
// }
//#임시# 프로젝트 step, type 변경 - 관리자 이상
//step
// const projectStepBtn = document.getElementById('project-step-btn');
// const projectStepCapsule = document.getElementById('project-step-capsule');
// if(projectStepBtn && projectStepCapsule && (window.location.href.toLowerCase().includes('bim.') || window.location.href.toLowerCase().includes('overseas.'))){
// if(vars.permission.checkPermission('change-project-step')) {
// projectStepCapsule.remove();
// projectStepBtn.innerHTML = `
// <h5 class="project-step__label --step__${vars.project.step}">${typeStep2Kor(vars.project.step)}</h5>
// <i class="project-step__icon"></i>`;
// } else {
// projectStepBtn.remove();
// projectStepCapsule.innerHTML = typeStep2Kor(vars.project.step);
// projectStepCapsule.classList.add(`--step-capsule__${vars.project.step}`);
// }
// }else{
// projectStepCapsule.remove();
// projectStepBtn.remove();
// }
// //type
// const projectTypeBtn = document.getElementById('project-type-btn');
// const projectTypeCapsule = document.getElementById('project-type-capsule');
// if(projectTypeBtn && projectTypeCapsule && window.location.href.toLowerCase().includes('bim.')){
// if(vars.permission.checkPermission('change-project-type')) {
// projectTypeCapsule.remove();
// projectTypeBtn.innerHTML = `
// <h5 class="project-type__label --type__${vars.project.project_type}">${typeStep2Kor(vars.project.project_type)}</h5>
// <i class="project-type__icon"></i>`;
// } else {
// projectTypeBtn.remove();
// projectTypeCapsule.innerHTML = typeStep2Kor(vars.project.project_type);
// }
// }else{
// projectTypeCapsule.remove();
// projectTypeBtn.remove();
// }
// //// 권한이 개발자인 경우
// document.querySelectorAll('.permission-min-dev').forEach(elem => {
// if (vars.permission.checkPermission('dev-menu')) { // 나중에 개발자 코드 변경될 예정 // Permission Class Test 250621
// if (elem.classList.contains('dev-menu-modal')) return; // 개발자 메뉴 모달은 바로 flex로 설정할 필요 없으므로 제외
// elem.style.setProperty('display', 'flex', 'important');
// } else {
// elem.remove();
// }
// })
//// 권한이 부관리자 이상인 경우
// document.querySelectorAll('.permission-min-sub-master').forEach(elem => {
// if (vars.permission.checkPermission('permission-btn') || vars.permission.checkPermission('overview-left-edit')) {
// elem.style.setProperty('display', 'flex', 'important');
// } else {
// elem.remove();
// }
// })
// //// 권한이 보안참여자 미만인 경우
// document.querySelectorAll('.permission-min-security-worker').forEach(elem => {
// if (userInfo.permission < 15) {
// elem.remove();
// } else {
// elem.style.setProperty('display', 'flex', 'important');
// }
// })
// //// 권한이 일반참여자 미만인 경우
// document.querySelectorAll('.permission-min-worker').forEach(elem => {
// // 현재 main.html에서 .ai-btn, .edit-btn (메모 ai, 수정 버튼)클래스에 permission-min-worker 추가
// if (userInfo.permission < 7) {
// elem.remove();
// } else {
// elem.style.setProperty('display', 'flex', 'important');
// }
// })
}
}
//type, step 한글화
export function typeStep2Kor(param){
let result = undefined;
switch(param){
case 'done':
result = '완료';
break;
case 'active':
result = '진행';
break;
case 'stop':
result = '중지';
break;
case 'wait':
result = '대기';
break;
case 'construction':
result = '시공';
break;
case 'design':
result = '설계';
break;
case 'surgest':
result = '제안';
break;
case 'research':
result = '연구';
break;
case 'support':
result = '지원';
break;
case 'center':
result = '센터';
break;
case 'survey':
result = '측량';
break;
// case 'ETC':
// result = '기타';
// break;
case 'MP':
result = 'MP (기본계획)';
break;
case 'DD':
result = 'DD (실시설계)';
break;
case 'FS':
result = 'FS (타당성조사)';
break;
case 'PD':
result = 'PD (기본설계)';
break;
case 'DS':
result = 'DS (설계감리)';
break;
case 'CS':
result = 'CS (시공감리)';
break;
case 'PMC':
result = 'PMC (실시설계)';
break;
case 'IDC':
result = 'IDC (타당성조사)';
break;
case 'DR':
result = 'DR (설계검토)';
break;
case 'ETC':
result = 'ETC (기타)';
break;
default:
result = '없음';
break;
}
return result;
}
async function setTitle() {
// 타이틀 카테고리 이미지 및 프로젝트명 설정 후 마르퀴효과 적용
// document.querySelector('.header .left .title .marquee-track .category').src = `/main/img/header/header-logo-${vars.project.category}.svg`;
let category = '-';
let projectNm = '-';
if (vars.project) {
category = vars.project.category.toUpperCase();
projectNm = vars.project.project_nm;
}
document.querySelector('.header .left .title .marquee-track .category').innerHTML = category;
document.querySelector('.header .left .title .marquee-track .project-name').innerHTML = projectNm;
if (vars.project) await startMarquee('title');
}
async function componentsInit() {
// 헤더 - 프로젝트에 포함되지 않은 계정인 경우 헤더 버튼 삭제
// 헤더 - 프로젝트 db에서 과업개요, 공문, 위치기반모델 사용 여부에 따라 헤더 버튼 삭제
// 푸터 - 프로젝트에 포함되지 않은 계정인 경우 푸터 저장공간/활동로그 삭제
// 프로젝트가 비활성화 상태면 리턴
if (checkProjectInactive()) return;
let overviewMain = document.querySelector('.overview-main');
let overviewBtn = document.querySelector('.header .overview-btn');
let overviewModalWrapper = document.querySelector('.overview-modal-wrapper');
let officialDocMain = document.querySelector('.official-doc-main');
let officialDocBtn = document.querySelector('.header .official-doc-btn');
let officialDocModalBackground = document.querySelector('.official-doc-modal-background');
let officialDocModalAll = document.querySelectorAll('.official-doc-modal');
let modelBtn = document.querySelector('.header .model-btn');
let mainNotice = document.querySelector('.main-notice');
let userInfo = JSON.parse(vars.userInfoString);
if ((userInfo.permission == null && typeof userInfo.permission != 'number')) {
// 프로젝트에 포함되어 있지 않은 사용자는 프로젝트 비활성화
setProjectInactive('permission');
} else {
// ** 권한 관련
// 참관자 권한일 때 활동로그 삭제
// if (userInfo.permission == 1) logWrap.remove();
// 프로젝트 db에서 과업개요, 공문, 위치기반모델 사용 여부에 따라 버튼 삭제
if (vars.project.overview) {
overviewBtn.style.display = 'flex';
} else {
if (overviewMain) overviewMain.remove();
if (overviewBtn) overviewBtn.remove();
if (overviewModalWrapper) overviewModalWrapper.remove();
}
if (vars.project.official_doc) {
officialDocBtn.style.display = 'flex';
} else {
if (officialDocMain) officialDocMain.remove();
if (officialDocBtn) officialDocBtn.remove();
if (officialDocModalBackground) officialDocModalBackground.remove();
if (officialDocModalAll.length != 0) {
officialDocModalAll.forEach(elem => {
elem.remove();
})
}
}
if (vars.project.gsim) modelBtn.style.display = 'flex';
else modelBtn.remove();
}
}
async function startMarquee(type) {
let trackContainer;
if (type == 'title') trackContainer = document.querySelector('body > .header .left .title');
if (type == 'bannerNotice') trackContainer = document.querySelector('body > .banner-notice-area .content');
let originalTrack = trackContainer.querySelector('.marquee-track');
if(window.location.origin.includes('gtb.')){
// gtb 임시코드
// document.querySelectorAll('.marquee-track-wrap .pm, .marquee-track-wrap .symbol, .marquee-track-wrap .category').forEach(e=>e.style.display='none');
originalTrack.querySelector('.pm').style.display = 'none';
originalTrack.querySelectorAll('.symbol').forEach(e => e.style.display = 'none');
originalTrack.querySelector('.category').style.display = 'none';
}
const createMarqueeInstance = async (container, originalTrack) => {
const wrap = document.createElement('div');
wrap.classList.add('marquee-track-wrap');
const cloneTrack1 = originalTrack.cloneNode(true);
const cloneTrack2 = originalTrack.cloneNode(true);
wrap.appendChild(originalTrack.cloneNode(true));
wrap.appendChild(cloneTrack1);
wrap.appendChild(cloneTrack2);
container.innerHTML = '';
container.appendChild(wrap);
const setMarquee = (entries, wrap) => {
const pixelsPerSecond = 40;
const firstTrack = wrap.querySelector('.marquee-track:first-child');
if (!firstTrack) return;
const trackWidth = firstTrack.getBoundingClientRect().width;
if (trackWidth === 0) return;
const duration = trackWidth / pixelsPerSecond;
wrap.style.setProperty('--duration', `${duration}s`);
wrap.style.setProperty('--translateX', `-${trackWidth}px`);
};
// 초기 마르퀴 설정
// setTimeout(async function () {
// await setMarquee(null, wrap);
// }, 500);
// 타이틀/배너 공지 크기 변화 감지되면 다시 마르퀴 설정
setTimeout(async function () {
let observer = new ResizeObserver(async (entries, observer) => {
await setMarquee(entries, wrap);
});
observer.observe(container);
}, 500);
//// 초기 마르퀴 설정 안해도 정상 작동?
};
createMarqueeInstance(trackContainer, originalTrack);
}
//user정보 저장
function save_user() {
if (vars.lastHeaderBtn) {
let me = JSON.parse(vars.userInfoString);
me.project_id = vars.project_id;
me.userColor = getRandomColor();
me.curPath = vars.lastHeaderBtn.dataset.resourcePath;
me.origin = window.location.origin;
vars.socket.emit('setUser', { me: me, stat: 'first', origin: window.location.origin});
}
}
export function checkProjectInactive() {
// vars.project.is_active는 프로젝트가 활성화 되어 있으면 true, 비활성화 되어 있으면 false
// checkProjectInactive() 함수는 프로젝트가 비활성화 상태인지를 체크하는 함수이므로
// 활성화 되어 있으면 true, 비활성화 되어 있으면 false를 리턴해야됨 (vars.project.is_active와 반대)
// 비활성화 된 프로젝트 배열 안에 현재 프로젝트 아이디가 포함되어 있으면 비활성화 상태(true)로 리턴
let result = (vars.project) ? !vars.project.is_active : false;
// 관리자 계정인 경우 무조건 활성화 상태(false)로 리턴
if (JSON.parse(vars.userInfoString).group == 'dev') result = false;
return result;
}
export async function toggleBannerNoticeArea() {
let userInfo = JSON.parse(vars.userInfoString);
let userId = userInfo.user_id;
// if (userId.includes('bcmf-')) {
// } else {
let bannerNoticeArea = document.querySelector('body > .banner-notice-area');
let bannerNoticeContent = bannerNoticeArea.querySelector('.content');
let header = document.querySelector('body > .header');
let mainAll = document.querySelectorAll('body > .main');
bannerNoticeArea.style.justifyContent = 'center';
bannerNoticeContent.innerHTML = '';
let textWrap = document.createElement('div');
textWrap.classList.add('text-wrap');
let text = document.createElement('div');
text.classList.add('text');
textWrap.appendChild(text);
bannerNoticeContent.appendChild(textWrap);
let noticeText = (vars.project) ? vars.project.banner_notice : '';
if (noticeText == '' || noticeText == undefined || noticeText == null) {
bannerNoticeArea.style.display = 'none';
text.innerHTML = '';
header.style.top = '0';
mainAll.forEach(main => {
if (userId.includes('bcmf-')) {
// bcmf에서 iframe으로 pm 화면 연결하는 경우 헤더, 푸터가 숨겨져 있으므로 높이는 100dvh로, top은 0으로 설정
main.style.height = '100dvh';
main.style.top = '0';
} else {
main.style.height = 'calc(100dvh - 2.25rem - 2.25rem)';
main.style.top = '2.25rem';
}
})
} else {
bannerNoticeArea.style.display = 'flex';
text.innerHTML = noticeText;
header.style.top = '2.25rem';
mainAll.forEach(main => {
if (userId.includes('bcmf-')) {
// bcmf에서 iframe으로 pm 화면 연결하는 경우 헤더, 푸터가 숨겨져 있으므로 높이에서 banner-notice-area height(2.25rem) 빼고, top은 2.25rem으로 설정
main.style.height = 'calc(100dvh - 2.25rem)';
main.style.top = '2.25rem';
} else {
main.style.height = 'calc(100dvh - 2.25rem - 2.25rem - 2.25rem)';
main.style.top = '4.5rem';
}
})
if (textWrap.getBoundingClientRect().width > bannerNoticeArea.getBoundingClientRect().width - 10) {
textWrap.classList.add('marquee-track');
bannerNoticeArea.style.justifyContent = 'flex-start';
await startMarquee('bannerNotice');
}
}
// }
}
export function setProjectInactive(type, text) {
// 프로젝트 비활성화 된 경우 초기 프로그레스 종료
document.querySelector('.init-progress').style.display = 'none';
let archiveMain = document.querySelector('.archive-main');
let permissionModal = document.querySelector('.permission-modal');
let overviewMain = document.querySelector('.overview-main');
let overviewBtn = document.querySelector('.header .overview-btn');
let overviewModalWrapper = document.querySelector('.overview-modal-wrapper');
let officialDocMain = document.querySelector('.official-doc-main');
let officialDocBtn = document.querySelector('.header .official-doc-btn');
let officialDocModalBackground = document.querySelector('.official-doc-modal-background');
let officialDocModalAll = document.querySelectorAll('.official-doc-modal');
let headerCenterLeft = document.querySelector('.header > .center .left.wrap');
let headerCenterRight = document.querySelector('.header > .center .right.wrap');
let sizeWrap = document.querySelector('.footer .size-wrap');
let logWrap = document.querySelector('.footer .log-wrap');
let setUserPermissionBtn = document.querySelector('.archive-modal .set-user-permission-btn');
let mainNotice = document.querySelector('.main-notice');
if (archiveMain) archiveMain.remove();
if (permissionModal) permissionModal.remove();
if (overviewMain) overviewMain.remove();
if (overviewBtn) overviewBtn.remove();
if (overviewModalWrapper) overviewModalWrapper.remove();
if (officialDocMain) officialDocMain.remove();
if (officialDocBtn) officialDocBtn.remove();
if (officialDocModalBackground) officialDocModalBackground.remove();
if (officialDocModalAll.length != 0) {
officialDocModalAll.forEach(elem => {
elem.remove();
})
}
if (headerCenterLeft) headerCenterLeft.remove();
if (headerCenterRight) headerCenterRight.remove();
if (sizeWrap) sizeWrap.remove();
if (logWrap) logWrap.remove();
if (setUserPermissionBtn) setUserPermissionBtn.remove();
let mainNoticeText = '';
if (type == 'permission') {
// mainNoticeText = `
// <div>${JSON.parse(vars.userInfoString).user_nm} 님은 ${vars.project.project_nm} 프로젝트에 포함되어 있지 않습니다.</div>
// <div>프로젝트 참가 신청 또는 문의사항은 프로젝트 담당자에게 연락해 주시기 바랍니다.</div>
// <div>프로젝트 담당자 : ${vars.project.user_nm} ${vars.project.position}</div>
// `;
mainNoticeText = `
<div>${JSON.parse(vars.userInfoString).user_nm} 님은 프로젝트에 포함되어 있지 않습니다.</div>
`;
}
if (type == 'lock') {
if (text) {
mainNoticeText = text;
} else {
mainNoticeText = `
<div>프로젝트가 비활성화 상태입니다.</div>
`;
}
}
mainNotice.style.display = 'flex';
mainNotice.innerHTML = mainNoticeText;
}
export function getRandomColor() {
let r, g, b;
let brightness;
while (true) {
r = Math.floor(Math.random() * 256);
g = Math.floor(Math.random() * 256);
b = Math.floor(Math.random() * 256);
// 밝기 계산 (0 ~ 255)
brightness = (r * 299 + g * 587 + b * 114) / 1000;
// 너무 밝거나 어두운 색상 필터링
if (brightness > 0 && brightness < 150) {
// 각 색상 채널의 차이가 너무 작으면 필터링
if (Math.abs(r - g) > 20 || Math.abs(r - b) > 20 || Math.abs(g - b) > 20) {
break; // 조건을 만족하면 루프 종료
}
}
}
// 16진수 문자열로 변환
const hexR = r.toString(16).padStart(2, '0');
const hexG = g.toString(16).padStart(2, '0');
const hexB = b.toString(16).padStart(2, '0');
return `#${hexR}${hexG}${hexB}`;
}
// 임시 스로틀
export function throttle(func, limit) {
let lastFunc;
let lastRan;
return function (...args) {
const context = this;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function () {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
window.dtest = async function() {
let res = await axios.post(`${vars.path_name}/dtest`);
console.log(res.data.message);
}
window.otest = async function() {
let res = await axios.post(`${vars.path_name}/otest`);
console.log(res.data.message);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
import { vars } from '../archive/variable.js';
import { docVars } from './docVariable.js';
import { renderDocViewer } from './docPageRenderer.js';
let socket = vars.socket;
// 변환시작, 변환완료 등 상태에 따른 실시간 UI 변경 처리는 socket.on에서 처리
// 서버에 현재 변환중인 파일 경로를 보내는 등 로직 처리는 working
//// 변환 시작
socket.on('convertDoc_start', (resultData) => {
let resourcePath = resultData.resourcePath;
let state = document.querySelector(`.attach-item[data-resource-path="${resourcePath}"] .state`);
if (!state) return;
state.classList.remove('convert');
state.classList.add('working');
let convertBtnText = state.querySelector('.convert-btn-text');
if (convertBtnText) convertBtnText.innerHTML = '변환중';
});
//// 변환 완료
socket.on('convertPdf_success', async (resultData) => {
let resourcePath = resultData.resourcePath;
docVars.convertingPathArr = resultData.convertingPathArr;
let state = document.querySelector(`.attach-item[data-resource-path="${resourcePath}"] .state`);
if (!state) return;
state.classList.remove('working');
state.classList.add('viewable');
let stateText = state.querySelector('.state-text');
if (stateText) stateText.innerHTML = '열람가능';
let convertBtn = state.querySelector('.convert-btn');
if (convertBtn) convertBtn.remove();
if (docVars.lastClickedListTarget) {
let lastClickedListTargetPath = docVars.lastClickedListTarget.dataset.resourcePath;
if (resourcePath == lastClickedListTargetPath) renderDocViewer(resourcePath, resultData.dataId);
}
});
+63
View File
@@ -0,0 +1,63 @@
let PathNameWindow = decodeURIComponent(window.location.pathname); // /gsimteam/archive
let path_name = PathNameWindow.replace('archive', 'officialDoc'); // /gsimteam/officialDoc
let project_id = PathNameWindow.split('/')[1]; // gsimteam
export const docVars = {
// 라우터 연결을 위한 변수
path_name: path_name,
// 프로젝트 id
project_id: project_id,
// 오브젝트 스토리지 타입 (ONPREMISE / CLOUD)
storageType: undefined,
// 버킷 (프로젝트 id와 동일)
bucket: project_id,
// 마지막으로 클릭한 타켓
lastClickedListTarget: undefined,
// 마지막으로 우클릭한 타겟 -> 컨텍스트 메뉴에 사용
lastContextTarget: undefined,
// 로그 데이터 -> 활동 정보 activity-info-item(로그 데이터)에 사용 (활동 정보 아이템 클릭 시 상세 정보 표시에 사용)
logData: undefined,
// 변환중인 파일 경로를 저장하는 배열
workingFileArr: [],
// 미리보기 뷰어
viewer: undefined,
// 수신/발신
direction: undefined,
allRecipientListByDirection: undefined,
allSendingListByDirection: undefined,
// 파일 리스트 sort용 현재 컬럼, 현재 정렬순
curSortCol : 'number',
curSortOrder : 'asc',
//
allDocData: [],
allDocOfficialData: [],
allDocAttachData: [],
currentDroppedFiles: [],
currentDocId: null,
currentGroupId: null,
selectedDoc: null,
selectedAttachItem: null,
selectParams: {
typeOptions: '', // depth1: 발주처/외
baseOptions: '', // depth2: 기준
targetOptions: '', // depth3: 상대기관
},
groupCompanyData: {},
originCompanyList: [],
};
// window.docVars = docVars;
+12
View File
@@ -0,0 +1,12 @@
import * as docSocketManager from './docSocketManager.js';
import { initDocSlider, initCategoryClicked, initCustomSelectBoxes, activateSelectUI, showInstructions } from './docPageRenderer.js';
// 셀렉트박스 클릭 이벤트
await initCustomSelectBoxes();
activateSelectUI();
// 카테고리 클릭 이벤트
initCategoryClicked();
// 화면 위아래 슬라이드
initDocSlider();
// 설정모달: 초기화면 도움말
showInstructions();
@@ -0,0 +1,21 @@
반드시 아래 지침을 따를 것:
- **공문에 실제로 쓰인 텍스트만** 사용해. 추정 금지.
- 모르면 "확인필요", 없으면 "없음"이라고 써.
- 한글로 쓰라고 한 항목은 반드시 한글로 써.
1. 공문 번호: Ref. No. (reference no.는 12번 항목)
2. 공문 일자: 날짜
3. 수신처: 대표 기관 이름 하나만 (예:Seoyoung Engineering Co., Ltd)
4. 수신자: 받는 사람 직책 (예: Project Director)
5. 수신자 약자: (예: Project Director → PD)
6. 발신처: 대표 기관 이름
7. 발신자: 보낸 사람 직책(예: Team Leader)
8. 발신자 약자: (예: Team Leader → TL)
9. 공문 제목: Subject 가장 위에 한 줄만 (bold체)
10. 공문 제목 요약: 한글로 10~20자
11. 공문 내용 요약: 한글로 간단하게
12. 공문간 연계: 관련된 다른 공문의 번호 (reference no.)
13. 공문 종류: [행정/일반:(직원, 파견, 동원, 조직, 비용, 계약 등), 기술/성과물:(일정 협의, 작업계획, 성과물 제출, 기술적 업무 회의, 성과물 전달 등), 회의/기타:(회의록 등 위에 내용 이외의 것) 중 하나
14. 공문 유형: [보고:사실과 계획 일방적으로 통보, 요청:회신 또는 행동 부탁, 지시:권한 있는 주체가 수행을 명령, 회신:기존 공무에 대해 응답하거나 의견을 제공, 계약:계약조건 변경과 관련] 중 하나
15. 첨부문서 제목: Enclosures: 목록 전체
16. 첨부문서 수: 숫자만
@@ -0,0 +1,33 @@
아래 문서에서 정보를 추출하여 JSON 객체로만 출력해주세요.
- 절대 ```json 등 코드블록으로 감싸지 마세요.
- 불필요한 설명 금지, 추론 금지, 파일에 있는 내용만 사용
- 값이 없는 항목은 "없음"으로 표기
- 숫자 값은 숫자 그대로
- 결과값을 **일반적인 JSON 형식**으로 그대로 출력해주세요.
- 이스케이프 문자 (예: \", \n 등)는 사용하지 말고, 결과값은 **그대로** JSON 형태로 출력해주세요.
{
1. "공문번호": 공문 번호로 Ref. No.를 의미합니다. 없는 경우는 없음으로 표기해주세요. (예시: "Ref. No. SYJV-250031")
2. "공문일자": 공문에 적혀 있는 날짜입니다. 번역하지 않고 그대로 표기해주세요. (예시: "Mar / 28 / 2025")
3. "수신처": 공문을 받는 사람이 속한 조직명 (예시: "Department of Public Works and Highways")
4. "수신자": 공문을 받은 사람의 직책 (예시: "Project Director")
5. "수신자약자": 수신자 직책 약자 (예시: "PD")
6. "발신처": 공문을 보낸 사람이 속한 조직명 (예시: "SEOYOUNG JOINT VENTURE")
7. "발신자": 공문을 보낸 사람의 직책 (예시: "Team Leader")
8. "발신자약자": 발신자 직책 약자 (예시: "TL")
9. "공문제목": 공문의 제목으로 SUBJECT 의미합니다. 적당한 길이로 끊어야 하는데 윗 문장이 프로젝트 이름으로 판단되는 경우, 9.1 프로젝트 항목을 신설해 리턴 (예시: "Submission of Comment Matrix for Design Deliverable")
10. "공문제목요약": 공문 제목을 10~20자 사이로 요약해주세요. 반드시 한글로 작성합니다.
11. "공문내용요약": 공문 내용을 요약해주세요. 반드시 한글로 작성합니다.
12. "공문간연계": 연계된 공문이 있으면 공문번호를 알려주세요. 공문번호만 필요합니다. 없는 경우는 없음으로 표기해주세요.
13. "공문종류": 공문 종류는 공문의 내용을 분석해서 다음 3가지 중 반드시 하나를 선택합니다.
* 행정/일반 – 인사, 파견, 조직, 비용(예산), 계약 등 경영/행정 관련
* 기술/성과물 – 일정 협의, 작업계획, 성과물 제출, 기술적 업무 회의, 성과물 전달 등
* 회의/기타 – 회의록 등 위에 내용 이외의 것
14. "공문유형": 공문 유형은 공문의 내용을 분석해서 다음의 5가지 중 반드시 하나를 선택합니다.
* 보고 : 완료된 사실이나 계획을 일방적으로 알리는 공문
* 요청 : 상대방의 행동 또는 답변을 유도하는 공문
* 지시 : 권한 있는 주체가 수행을 명령하는 공문
* 회신 : 기존 공무에 대해 응답하거나 의견을 제공하는 공문
* 계약 : 계약조건 변경과 관련된 공식 공문
15. "첨부문서제목": 공문의 첨부 문서는 Enclosures: 를 의미합니다. 없는 경우는 없음으로 표기해주세요. (예시: "1. Comment Matrix_4.4.2 Draft Detailed Engineer Design Report (Section A)" )
16. "첨부문서수": 찾은 첨부문서 개수를 알려주세요.
}
@@ -0,0 +1,33 @@
{
"title": "DocumentSummary",
"type": "object",
"properties": {
"공문번호": { "type": "string" },
"공문일자": { "type": "string" },
"수신처": { "type": "string" },
"수신자": { "type": "string" },
"수신자약자": { "type": "string" },
"발신처": { "type": "string" },
"발신자": { "type": "string" },
"발신자약자": { "type": "string" },
"공문제목": { "type": "string" },
"공문제목요약": { "type": "string" },
"공문내용요약": { "type": "string" },
"공문간연계": { "type": "string" },
"공문종류": {
"type": "string",
"enum": ["행정/일반", "기술/성과물", "회의/기타"]
},
"공문유형": {
"type": "string",
"enum": ["보고", "요청", "지시", "회신", "계약"]
},
"첨부문서제목": { "type": "string" },
"첨부문서수": { "type": "integer" }
},
"required": [
"공문번호", "공문일자", "수신처", "수신자", "수신자약자",
"발신처", "발신자", "발신자약자", "공문제목", "공문제목요약",
"공문내용요약", "공문간연계", "공문종류", "공문유형", "첨부문서제목", "첨부문서수"
]
}
@@ -0,0 +1,22 @@
주의:
- **정확성이 매우 중요합니다.**
- **19.1 SDZR/AERA/RCM** 형태는 공문 번호가 아닙니다.
- 한글로 쓰라고 한 항목은 반드시 한글로 작성
- 반드시 아래 **1~13번 항목만** 출력
- 모르면 "확인필요", 없으면 "없음"이라고 작성
- 결과값을 **일반적인 JSON 형식**으로 그대로 출력해주세요
- 절대 ```json 등 코드블록으로 감싸지 마세요.
1. "공문번호": "영문-숫자" 형태
2. "공문일자": YYYY-MM-DD
3. "수신자": 수신자의 직책
4. "수신자약자": 수신자 직책의 약자
5. "발신자": 발신자의 직책 (예: Project Director), 조직명은 제외
6. "발신자약자": 발신자 직책의 약자
7. "공문제목": Subject **독립된 첫 구문만**, 프로젝트 이름/설명 등 부연 문장은 제외
8. "공문제목요약": **한글로** 10~20자
9. "공문내용요약": **한글로** 간단하게
10. "공문간연계": 연계된 공문이 있으면 "있음", 없으면 "없음"
11. "공문종류": '행정/일반', '기술/성과물', ‘회의/기타’ 중 하나
12. "공문유형": 보고, 요청, 지시, 회신, 계약 중 하나
13. "첨부문서수": 숫자만
@@ -0,0 +1,27 @@
주의:
- **정확성이 매우 중요합니다.**
- **19.1 SDZR/AERA/RCM** 형태는 공문 번호가 아닙니다.
- 한글로 쓰라고 한 항목은 반드시 한글로 작성
- 반드시 아래 13개 항목명만 키로 사용:
"공문번호", "공문일자", "수신자", "수신자약자", "발신자", "발신자약자", "공문제목", "공문제목요약", "공문내용요약", "공문간연계", "공문종류", "공문유형", "첨부문서수"
- 키 값(항목명)에 숫자나 마크 추가 금지. 반드시 순수 텍스트(한글/영문)만 사용.
- 모르면 "확인필요", 없으면 "없음"이라고 작성
- 결과값을 **일반적인 JSON 형식**으로 그대로 출력해주세요
- 절대 ```json 등 코드블록으로 감싸지 마세요.
- 잘못된 형식이 나오면 위 조건에 맞는 JSON으로 다시 출력하세요.
{
"공문번호": "영문-숫자" 형태
"공문일자": YYYY-MM-DD
"수신자": 수신자의 직책
"수신자약자": 수신자 직책의 약자
"발신자": 발신자의 직책 (예: Project Director), 조직명은 제외
"발신자약자": 발신자 직책의 약자
"공문제목": Subject **독립된 첫 구문만**, 프로젝트 이름/설명 등 부연 문장은 제외
"공문제목요약": **한글로** 10~20자
"공문내용요약": **한글로** 간단하게
"공문간연계": 연계된 공문이 있으면 "있음", 없으면 "없음"
"공문종류": '행정/일반', '기술/성과물', ‘회의/기타’ 중 하나
"공문유형": 보고, 요청, 지시, 회신, 계약 중 하나
"첨부문서수": 숫자만
}
+9
View File
@@ -0,0 +1,9 @@
import { getData } from './overviewDataManager.js';
await getData();
// overviewCommon.js : 아래에 있는 js에서 사용하는 함수들을 모아놓은 js
// overviewDataManager.js : 과업개요 db 통신 추가/수정/삭제
// overviewModalManager.js : 과업개요 모달 인터랙션 js
// overviewPageRenderer.js : 과업개요 페이지를 그리는 랜더링 js
// overviewVariable.js : 과업개요에서 사용 되는 전역변수 js
+514
View File
@@ -0,0 +1,514 @@
import { vars } from '../archive/variable.js';
import { overviewVars } from './overviewVariable.js';
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 공용 함수 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// 공동도급 형식 자르기
export function splitStr(data) {
if (data === null || data === undefined) return;
if (data.includes('^&')) {
const splitStr = data.split('^&');
return splitStr;
}
}
// 날짜 포맷팅
export function sliceDate(date){
if(!date) return null;
const formattedDate = date.slice(0,4) + '-' + date.slice(4,6) + '-' + date.slice(6,8);
return formattedDate;
}
export function overviewVarsInit() {
if(overviewVars.sectionTabData)overviewVars.sectionTabData = JSON.parse(JSON.stringify(overviewVars.originalSectionTabData))
overviewVars.deleteTaskHistory = [];
if (!arraysEqual(overviewVars.filesArr, overviewVars.originalFilesArr)) overviewVars.filesArr = [...overviewVars.originalFilesArr];
if (!arraysEqual(overviewVars.filesSizeArr, overviewVars.originalFilesSizeArr))overviewVars.filesSizeArr = [...overviewVars.originalFilesSizeArr];
if (!arraysEqual(overviewVars.filesNameArr, overviewVars.originalFilesNameArr)) overviewVars.filesNameArr = [...overviewVars.originalFilesNameArr];
overviewDropboxInit();
}
function arraysEqual(a, b) {
if (!Array.isArray(a) || !Array.isArray(b)) return false;
if (a.length !== b.length) return false;
return a.every((value, index) => value === b[index]);
}
export function setOverviewType(){
// overseas분기용
if(!window.location.host.toLowerCase().includes('overseas.')){
document.querySelectorAll('.only-overseas').forEach(ele => {
ele.remove();
})
overviewVars.overseas = false;
} else {
document.querySelectorAll('.no-overseas').forEach(ele => {
ele.remove();
});
overviewVars.overseas = true;
}
}
//🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 공용 함수 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Section-Left 사용 함수 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// 이미지 중복 이름, 확장자 체크 함수
export function checkDuplicatesFileNameAndExt(files){
const duplicateNameArr = [];
const notAllowExtArr = [];
const acceptExt = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
files.forEach(file => {
const fileExt = file.name.split('.').pop().toLowerCase();
if(!acceptExt.includes(fileExt)){
notAllowExtArr.push(file);
return;
}
const isDuplicateFile = overviewVars.filesArr.some(f => f.name === file.name);
const isDuplicateFileName = overviewVars.filesNameArr.includes('overview/' + file.name);
const isDuplicateFileSize = overviewVars.filesSizeArr.some(f => f.size === file.size);
if(!isDuplicateFile && !isDuplicateFileName && !isDuplicateFileSize){
overviewVars.filesArr.push(file);
overviewVars.filesNameArr.push(file.name);
if(overviewVars.filesSizeArr[0] === 0 ) overviewVars.filesSizeArr = [];
overviewVars.filesSizeArr.push(file.size);
} else {
duplicateNameArr.push(file);
}
});
if(duplicateNameArr.length > 0 || notAllowExtArr.length > 0){
alert(`다음 확장자만 등록할 수 있습니다 : \n- ${acceptExt}\n \n` + '그 외에 확장자 또는 중복된 파일은 등록할 수 없습니다.');
}
}
// DropBox에 파일 리스트 생성 함수
export function displayFileName(){
const dropbox = document.querySelector('.overview-modal.section-left .dropbox');
if(overviewVars.filesNameArr.length > 0 ){
dropbox.innerHTML = '';
dropbox.style.backgroundColor = '#fff'
dropbox.style.alignItems = 'stretch';
dropbox.style.justifyContent = 'normal';
}
overviewVars.filesNameArr.forEach(file => {
const originalName = file;
const displayName = file.replace('overview/', '');
const wrapDiv = document.createElement('div');
wrapDiv.classList.add('file-wrap-div');
wrapDiv.dataset.filepath = file;
const iconImg = document.createElement('img');
iconImg.src = '/main/img/overview/icon-imageFile.svg';
iconImg.classList.add('dropbox-file-img');
const fileName = document.createElement('h3');
fileName.innerText = displayName;
fileName.style.width = '95%';
const button = document.createElement('button');
button.classList.add('xs-btn');
button.addEventListener('click', async ()=> {
const fileIndex = overviewVars.filesArr.findIndex(f => f.name === displayName);
if(fileIndex !== -1) overviewVars.filesArr.splice(fileIndex, 1);
const nameIndex = overviewVars.filesNameArr.findIndex(f => f === file);
// 배열 삭제 전 지워야될 key 값 저장
let key = overviewVars.filesNameArr[nameIndex];
if(nameIndex !== -1){
overviewVars.filesNameArr.splice(nameIndex, 1);
overviewVars.filesSizeArr.splice(nameIndex, 1);
}
// object-storage에 저장된 file만 s3 api 이용해서 삭제
if(document.querySelector(`.overview .swiper-slide[data-filepath="${file}"]`)){
overviewVars.deleteImgArr.push(key);
}
overviewDropboxInit()
wrapDiv.remove();
});
const buttonimg = document.createElement('img');
buttonimg.src = '/main/img/overview/icon-close-111.svg';
button.appendChild(buttonimg);
wrapDiv.append(iconImg, fileName, button);
dropbox.appendChild(wrapDiv);
});
}
export function overviewDropboxInit(){
if(overviewVars.filesNameArr.length === 0){
const dropbox = document.querySelector('.overview-modal.section-left .dropbox');
dropbox.innerHTML = `<div class="dropbox-top">
<img class="icon" src="/main/img/overview/icon-imageFile.svg"
alt="icon-dropfile">
<p>
여기로 파일을 드래그하거나 <br>
<strong style="border-bottom: 0.063rem solid #777;">우측 상단에서 이미지 파일을
업로드하세요.</strong>
</p>
</div>`
dropbox.style.justifyContent = 'center';
dropbox.style.alignItems = 'center';
dropbox.style.backgroundColor = '#eee';
}
}
// 접근용 presignedUrl 발급
export async function getPresignedURL(key) {
const res = await axios.get(`/${vars.project_id}/overview/generateGetImgUrl`, { params: { key: key } });
const url = res.data;
return url;
};
// 삭제용 presignedUrl 발급 이후에 minIO 삭제요청
export async function generateDeleteImgUrl(key) {
if (key === undefined) return;
try {
const res = await axios.delete(`/${vars.project_id}/overview/generateDeleteImgUrl`, { data: { key } });
let { url } = res.data;
await axios.delete(url)
return true;
} catch (err) {
console.error(err);
}
}
// 업로드용 presignedURL 발급
export async function uploadImgData(file) {
try {
const presignedUrlRes = await axios.post(`/${vars.project_id}/overview/generateUploadUrl`, { fileName: file.name });
let { url, key } = presignedUrlRes.data;
const bucketRes = await axios.put(url, file, { headers: { 'Content-type': file.type || 'application/octet-stream' } });
if (bucketRes.status == 200) {
return key;
}
} catch (error) {
console.error('이미지 업로드 실패: ', error)
}
}
//🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Section-Left 사용 함수 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Section-Middle 사용 함수 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// 공동도급표 구분자 추가 함수
export function addJointContractDelimiter() {
let representativeCompany = document.querySelector('.overview-modal .joint-contract-company-name')?.innerText;
let companyName = '';
document.querySelectorAll('.overview-modal .joint-contract-company-name').forEach((name) => {
companyName += name.innerText.trim().replace(/,/g, '') + '^&';
if (companyName === '^&') companyName = '';
});
let companyShares = '';
document.querySelectorAll('.overview-modal .joint-contract-company-shares').forEach((shares) => {
companyShares += shares.innerText.trim().replace(/,/g, '') + '^&';
if (companyShares === '^&') companyShares = '';
});
let contractKrw = '';
document.querySelectorAll('.overview-modal .joint-contract-krw').forEach((krw) => {
contractKrw += krw.innerText.trim().replace(/,/g, '') + '^&';
if (contractKrw === '^&') contractKrw = '';
});
let contractUsd = '';
document.querySelectorAll('.overview-modal .joint-contract-usd').forEach((usd) => {
contractUsd += usd.innerText.trim().replace(/,/g, '') + '^&';
if (contractUsd === '^&') contractUsd = '';
});
return { representativeCompany, companyName, companyShares, contractKrw, contractUsd }
}
// 공동도급 문자열 입력 제한 함수
export function restrictToNumber(element, event) {
let text = element.innerText;
const shares = element.classList.contains('joint-contract-company-shares');
const payment = element.classList.contains('joint-contract-krw') || element.classList.contains('joint-contract-usd');
// 지분율 소수점 허용
if (shares) {
text = text.replace(/[^0-9.]/g, '');
// 소수점 중복 제한 (숫자, .)
const parts = text.split('.');
let integerPart = parts[0].replace(/^0+(?=\d)/,'');
if(integerPart === '') integerPart = '0'
if (parts.length > 1) {
text = integerPart + '.' + parts.slice(1).join('');
} else {
text = integerPart;
}
element.innerText = text;
} else {
// 문자열 제한 (오직 숫자만)
text = text.replace(/[^0-9]/g, '');
}
// 계약금 , 생성
if (payment) {
//계약금 ,(콤마) 생성
if (isNaN(parseFloat(text))) {
element.innerText = '';
} else {
element.innerText = parseFloat(text).toLocaleString();
}
if (!(event.data >= '0' && event.data <= '9')) showToolTip(element, '숫자만 입력 가능합니다.');
} else {
element.innerText = text;
if (!((event.data >= '0' && event.data <= '9') || event.data == '.')) showToolTip(element, '숫자와 .만 입력 가능합니다.');
}
// 커서 맨끝으로 이동시키기
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(element);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
// 문자열 입력했을때 툴팁
export function showToolTip(element, message) {
let tooltip = document.querySelector('.overview-modal-tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.className = 'overview-modal-tooltip';
document.body.appendChild(tooltip);
}
if (tooltip.dataset.active === 'true') return;
tooltip.textContent = message;
tooltip.style.opacity = '0.8';
tooltip.style.visibility = 'visible';
tooltip.dataset.active = 'true';
const rect = element.getBoundingClientRect();
tooltip.style.left = `${rect.left + window.scrollX + (rect.width / 2) - (tooltip.offsetWidth / 2)}px`;
tooltip.style.top = `${rect.bottom + window.scrollY + 5}px`;
setTimeout(() => {
tooltip.style.opacity = '0';
tooltip.style.visibility = 'hidden';
tooltip.dataset.active = 'false'
}, 1000);
}
// 공동도급 지분율,계약금 계산
export function calculateTotalJointContract(element) {
let totalValue = 0;
let className = element.className;
if (className === 'joint-contract-company-name' || className === 'total-joint-contract-company-name' || className === 'total-joint-contract-company-shares' || className === 'total-joint-contract-krw'|| className === 'total-joint-contract-usd' || className === 'sticky') return;
document.querySelectorAll(`.overview-modal .${className}`).forEach(joint => {
let value = parseFloat(joint.innerText.replaceAll(',', '').trim());
if (isNaN(value)) value = 0;
totalValue += value;
});
if (isNaN(totalValue)) totalValue = 0;
if (className === 'joint-contract-company-shares') {
// 지분율 100%를 넘겼을 때 강제로 남은 지분율만큼만 입력되도록
if(totalValue > 100){
const excess = totalValue - 100;
let current = parseFloat(element.innerText);
element.innerText = current-excess;
document.querySelector(`.overview-modal .total-${className}`).innerText = 100;
showToolTip(element, '지분율은 100%를 넘길 수 없습니다.');
return;
}
document.querySelector(`.overview-modal .total-${className}`).innerText = totalValue;
} else {
document.querySelector(`.overview-modal .total-${className}`).innerText = totalValue.toLocaleString();
}
}
export function restrictDuplicatedTabName(){
// 중복 탭 이름 제한
const tabs = document.querySelectorAll('.overview-modal .section-tabs .tab');
const tabValues = [];
for(let tab of tabs){
const input = tab.querySelector('input');
const val = input ? input.value.trim() : '';
if(tabValues.includes(val)){
alert('중복된 탭 이름이 있습니다.')
return false;
}
tabValues.push(val);
}
return true;
}
//🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Section-Middle 사용 함수 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Section-Right 사용 함수 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
export function formatHour(hour) {
const normalized = (hour + 24) % 24;
const h = Math.floor(normalized); // 정수 시간
const m = Math.round((normalized - h) * 60); // 소수점 -> 분
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}
export function parseTimeToDecimal(timeStr) {
const [h, m] = timeStr.split(':').map(Number);
return h + (m / 60);
}
// 1시간당 1.75rem 기준으로 위치 계산
export function calculateRemPosition(currentTime) {
const currentHour = parseInt(currentTime.split(':')[0]);
const currentMinute = parseInt(currentTime.split(':')[1]);
const hourRem = 1.75
const minuteRem = 1.75 / 60;
const moveHour = currentHour * hourRem;
const moveMinute = currentMinute * minuteRem;
// left 0으로 놓았을때 00:00에 위치하도록 보정
const moveRem = moveHour + moveMinute - 3.5;
return moveRem
}
// 타임존 계산을 통해 어느 장소에서든 동일한 결과값을 보장
export function getTimeToTimeZone(timeZone) {
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false,
timeZone: timeZone
});
return formatter.format(now);
}
// 연속일정 Color 변경 함수 --> 연속일정에 백그라운드 컬러를 그대로 넣었다간 일정이 보여지지 않아 투명도를 높인 색깔로 변경하여 연속일정에 적용
export function changeColor(color) {
let dataColor = null;
switch (color) {
case "#F21D0D": dataColor = "#FEE9E7"; break;
case "#B92ED1": dataColor = "#F8EBFB"; break;
case "#6D3DC2": dataColor = "#F1ECF9"; break;
case "#0D8DF2": dataColor = "#E7F4FE"; break;
case "#4DB251": dataColor = "#EEF8EE"; break;
case "#FFBF00": dataColor = "#FFF9E6"; break;
case "#A0705F": dataColor = "#F6F1EF"; break;
case "#7F7F7F": dataColor = "#F3F3F3"; break;
case "#688897": dataColor = "#F0F4F5"; break;
default: dataColor = "#FFFFFF";
}
return dataColor;
}
// 주요일정 모달 작동시 현재시간 적용
export function setDefaultScheduleTime() {
const now = new Date();
const yyyy = now.getFullYear();
const mm = (now.getMonth() + 1).toString().padStart(2, '0');
const dd = now.getDate().toString().padStart(2, '0');
const hh = now.getHours().toString().padStart(2, '0');
const min = now.getMinutes().toString().padStart(2, '0');
const today = `${yyyy}-${mm}-${dd}`;
const timeNow = `${hh}:${min}`;
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
const hh2 = oneHourLater.getHours().toString().padStart(2, '0');
const min2 = oneHourLater.getMinutes().toString().padStart(2, '0');
const timeLater = `${hh2}:${min2}`;
document.querySelector('.startDate').value = today;
document.querySelector('.endDate').value = today;
document.querySelector('.startTime').value = timeNow;
document.querySelector('.endTime').value = timeLater;
document.querySelector('.startTime').disabled = false
document.querySelector('.endTime').disabled = false
};
export function setIssueEditMode(isEdit){
const editBtn = document.querySelector('.overview .xs-btn-type.issue-edit');
const saveBtn = document.querySelector('.overview .xs-btn-type.issue-save');
const cancleBtn = document.querySelector('.overview .xs-btn-type.issue-cancle');
const issueMessage = document.querySelector('.overview .box.issue .wrap .message')
const issueContent = document.querySelector('.overview .box.issue .wrap .issue-content');
editBtn.style.display = isEdit ? 'none' : 'block';
saveBtn.style.display = isEdit ? 'block' : 'none';
cancleBtn.style.display = isEdit ? 'block' : 'none';
issueContent.disabled = !isEdit;
issueContent.style.color = isEdit ? '#ff3d00' : '#111';
if(isEdit){
issueMessage.classList.add('active');
issueMessage.innerText = '내용을 작성/수정한 후 저장 버튼을 눌러주세요';
issueMessage.style.color = '#111';
} else {
issueMessage.classList.remove('active');
issueMessage.innerText = '수정 버턴을 눌러 내용을 작성/수정할 수 있습니다.';
issueMessage.style.color = '#777';
}
if(isEdit && issueContent.value.length > 0) issueMessage.style.color = '#ddd';
}
export function applyUtcOffsetTime(offset){
const now = new Date();
const hourOffset = Math.trunc(offset/3600);
let minuteOffset = 0;
if(offset % 3600 !==0){
minuteOffset = (offset/3600) - hourOffset;
if(minuteOffset == 0.75) minuteOffset = 45;
if(minuteOffset == 0.5) minuteOffset = 30;
if(minuteOffset == 0.25) minuteOffset = 15;
}
const hours = now.getUTCHours() + hourOffset;
const minute = now.getUTCMinutes() + minuteOffset;
const date = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), hours, minute))
return `${date.getUTCHours().toString().padStart(2,'0')}:${date.getUTCMinutes().toString().padStart(2,'0')}`;
}
export function fillPartialBlock(block, fraction, isStart) {
if (!block || fraction === 0) return;
const halfBlock = block.querySelectorAll('.timetable-half-block');
const quarterBlock = block.querySelectorAll('.timetable-quarter-block');
if(isStart){
if(fraction == 0.25){
quarterBlock[1].classList.add('work-hour');
halfBlock[1].classList.add('work-hour');
}
if(fraction == 0.5)halfBlock[1].classList.add('work-hour');
if(fraction == 0.75)quarterBlock[3].classList.add('work-hour');
} else {
if(fraction == 0.25)quarterBlock[0].classList.add('work-hour');
if(fraction == 0.5)halfBlock[0].classList.add('work-hour');
if(fraction == 0.75){
halfBlock[0].classList.add('work-hour');
quarterBlock[2].classList.add('work-hour');
}
}
}
//🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Section-Right 사용 함수 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
@@ -0,0 +1,453 @@
import { vars } from '../archive/variable.js';
import { checkProjectInactive } from '../main.js';
import { drawList, generateCalendar, startTimer, updateTimeBar, makeTaskHistory } from './overviewPageRenderer.js';
import { fileDragAndDrop } from './overviewModalManager.js';
import { addJointContractDelimiter, restrictDuplicatedTabName, uploadImgData, setIssueEditMode, generateDeleteImgUrl, setOverviewType } from './overviewCommon.js';
import { headerBtnForceClick, closeInitProgress } from '../archive/common.js';
import { overviewVars } from './overviewVariable.js';
// overviewDataManager.js는 db 통신처럼 데이터가 실질적으로 저장되고 삭제되는 함수 및 dom 조작을 모아놓은 js이다.
// getData는 tb_overview에 데이터를 가져와서 데이터를 함수들에게 뿌려주는 역할을 한다.
export async function getData() {
if (checkProjectInactive()) return;
try {
const res = await axios.get(`/${vars.project_id}/overview/getData`, {
params: { projectId: vars.project_id },
});
if (res.data.success) {
const data = res.data.data;
await setOverviewType();
await drawList(data);
//위치도 개요도 드래그앤 드롭
fileDragAndDrop();
// 현재 날짜로 달력 생성
generateCalendar(overviewVars.currentYear, overviewVars.currentMonth);
// 교류시간 동작
if(overviewVars.overseas)startTimer(data[0]?.nation_nm);
//////// pm-bcmf 연결용 테스트 코드 - 파라미터로 전달받은 시작경로로 화면 전환 (depth1)
if (vars.startPathDepth1) {
let headerBtn = document.querySelector(`body > .header .center .left.wrap .menu-tab .btn[data-resource-path="/${vars.startPathDepth1}"]`);
if (headerBtn) await headerBtnForceClick(headerBtn);
}
// 과업개요 생성 후
if (!vars.startPathDepth1 && !vars.startPathDepth2 && !vars.startPathDepth3) {
// // 시작경로가 없으면 초기 프로그레스 종료
// console.log('******** 과업개요 렌더링 후 초기 프로그레스 종료 (시작경로 없음)');
await closeInitProgress();
} else {
if (document.querySelectorAll('body > .header .center .menu-tab .folder-btn').length == 0) {
// // 시작경로가 있는데 헤더폴더버튼이 없으면 초기 프로그레스 종료
// console.log('******** 과업개요 렌더링 후 초기 프로그레스 종료 (시작경로 있음, 헤더폴더버튼 없음)');
await closeInitProgress();
} else {
if (JSON.parse(vars.userInfoString).user_id.includes('bcmf-') && vars.project_id == 'dsdj2') {
// 시작경로, 헤더폴더버튼이 있고 현재 계정이 bcmf계정이고 현재 프로젝트가 대산당진2공구인 경우 과업개요 숨김
document.querySelector('.overview-main').style.display = 'none';
} else {
// 시작경로, 헤더폴더버튼이 있고 현재 계정이 bcmf계정이 아니거나 현재 프로젝트가 대산당진2공구가 아닌 경우 과업개요 숨김
// console.log('******** 과업개요 렌더링 후 초기 프로그레스 종료 (현재 계정이 bcmf계정이 아니거나 현재 프로젝트가 대산당진2공구가 아님)');
// console.log(`현재 계정: ${JSON.parse(vars.userInfoString).user_id}`);
// console.log(`현재 프로젝트: ${vars.project_id}`);
closeInitProgress();
}
}
}
} else {
console.error("getData error");
}
} catch (error) {
console.error("getData error", error);
}
}
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Section-Left 저장 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// Section-left 저장
document.querySelector('.overview-modal.section-left .section-left-save')?.addEventListener('click', async () => {
const modal = document.querySelector('.overview-modal.section-left');
const modalWrapper = document.querySelector('.overview-modal-wrapper');
// // 시설규모 탭 제목만 입력 후 저장할때 경고문
// for(const key in overviewVars.sectionTabData){
// if (overviewVars.sectionTabData[key].length === 0) return alert('시설규모 탭에 입력되지 않은 항목이 있습니다. 작성 후 저장해 주세요.');
// }
// // 중복된 탭 이름 사용하지 못하게
// const duplicatedTabName = restrictDuplicatedTabName();
// if(!duplicatedTabName) return;
let locationImgKey;
let originFileSize;
try{
// 이미지 파일 업로드
for(let i = 0; overviewVars.filesArr.length > i; i++){
await uploadImgData(overviewVars.filesArr[i], overviewVars.filesArr.type);
}
// 이미지 파일 objectKey 형식으로 변환
for(let j = 0; overviewVars.filesNameArr.length > j; j++){
if(!overviewVars.filesNameArr[j].includes('overview/')) overviewVars.filesNameArr[j] = 'overview/'+ overviewVars.filesNameArr[j];
}
// JSON 문자화 시킨다음에 db에 삽입
locationImgKey = JSON.stringify(overviewVars.filesNameArr);
originFileSize = JSON.stringify(overviewVars.filesSizeArr);
// 파일 사이즈가 빈 배열일때 기본값 설정
if(originFileSize === '[]') originFileSize = '[0]';
} catch (err) {
console.error(err);
}
if(overviewVars.deleteImgArr.length > 0){
for(let i = 0; overviewVars.deleteImgArr.length > i; i++){
await generateDeleteImgUrl(overviewVars.deleteImgArr[i]);
}
await axios.post(`/${vars.project_id}/overview/updateOverviewImgData`, { projectId: vars.project_id, locationImgKey : locationImgKey, originFileSize : originFileSize });
}
const projectId = vars.project_id;
const businessPurpose = modal.querySelector('.business-purpose').value;
const performanceArea = modal.querySelector('.performance-area').value;
const referenceArea = modal.querySelector('.reference-area').value;
const facilityOverview = modal.querySelector('.facility-overview').value;
let data = {projectId, businessPurpose, performanceArea, referenceArea, facilityOverview, originFileSize, locationImgKey };
//overseas용 분기
if(!overviewVars.overseas){
data.nation = modal.querySelector('.nation').value;
data.continent = modal.querySelector('.continent').value;
} else {
data.nation = modal.querySelector('.nation').innerText;
data.continent = modal.querySelector('.continent').innerText;
}
// minIO에 저장, DB 저장이 완료되면 minIO에 기존에 있던 사진을 삭제
try {
const res = await axios.post(`/${projectId}/overview/saveSectionLeftData`, data);
if (res.data.message === '200') {
try{
// 탭만 입력했을때 기본값 세팅
for (const title in overviewVars.sectionTabData) {
if (!overviewVars.sectionTabData[title] || overviewVars.sectionTabData[title].length === 0) {
// 기본값 세팅
overviewVars.sectionTabData[title] = [ { key: '', value: '', id: null, projectId } ];
}
}
await upsertSectionTabData();
const secondRes = await axios.post(`/${projectId}/overview/saveSectionLeftTabData`, overviewVars.sectionTabData);
if (secondRes.data.message === '200') overviewVars.sectionTabData = {};
} catch(err){
console.error(err, 'sectionLeftTabData 저장 오류')
}
alert('저장이 완료되었습니다.');
modal.style.display = 'none';
modalWrapper.style.display = 'none';
getData();
updateTimeBar();
} else {
alert('파일 저장에 실패하였습니다.');
}
}catch(err){
console.error('S3 이미지 삭제 오류 : ' , err);
}
});
async function upsertSectionTabData() {
const deleteArr = [];
// 섹션별 비교
for (let sectionName in overviewVars.originalSectionTabData) {
const originalArr = overviewVars.originalSectionTabData[sectionName];
const currentArr = overviewVars.sectionTabData[sectionName] || [];
// currentArr에 없는 항목만 삭제 대상으로
const toDelete = originalArr.filter(item => {
return !currentArr.some(c => c.id === item.id);
});
if (toDelete.length > 0) {
deleteArr.push(...toDelete);
}
}
// 삭제 API 호출
try {
const deleteRes = await axios.delete(`/${vars.project_id}/overview/deleteCellData`, { data: { deleteArr: deleteArr }});
if(deleteRes.data.message === 200)console.log('overviewSectionLeftCellData 삭제 완료');
} catch (err) {
console.error('삭제 실패', cell.id, err);
}
}
// 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Section-Left 저장 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Section-Middle 저장, 삭제 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// section-middle 저장
document.querySelector('.overview-modal.section-middle .btn-active')?.addEventListener('click', async () => {
const modal = document.querySelector('.overview-modal.section-middle');
const modalWrapper = document.querySelector('.overview-modal-wrapper');
const projectId = vars.project_id;
const abbreviatedName = modal.querySelector('.abbreviated-name').value;
const taskNmEn = modal.querySelector('.task-nm-en').value;
const taskPurpose = modal.querySelector('.task-purpose').value;
const orderSizeUsd = modal.querySelector('.order-size-usd').value;
const orderSizeKrw = modal.querySelector('.order-size-krw').value;
const clientOrigin = modal.querySelector('.client-origin').value;
const financial = modal.querySelector('.financial').value;
const financialCountry = modal.querySelector('.financial-country').value;
const selectionMethod = modal.querySelector('.selection-method').value;
const supportDepartment = modal.querySelector('.support-department').value;
const supportManagerNm = modal.querySelector('.support-manager-nm').value;
const completionDate = modal.querySelector('.completion-date').value;
let data = {projectId, abbreviatedName, taskNmEn, taskPurpose, orderSizeUsd, orderSizeKrw, clientOrigin, financial, financialCountry, selectionMethod, supportDepartment, supportManagerNm, completionDate};
// overseas용 분기
if(!overviewVars.overseas){
data.projectNo = modal.querySelector('.project-no').value
data.taskType = modal.querySelector('.task-type').value
data.bid = modal.querySelector('.bid').value
data.relativeClient = modal.querySelector('.client').value
data.department = modal.querySelector('.department').value
data.taskNmKr = modal.querySelector('.task-nm-kr').value;
data.scheduledCommencementDate = modal.querySelector('.scheuled-commencement-date').value;
data.contractPeriod = modal.querySelector('.contract-period').value;
data.projectManagerNm = modal.querySelector('.projectmanager-nm').value;
data.managerNm = modal.querySelector('.manager-nm').value;
data.contractDate = modal.querySelector('.contract-date').value;
data.commencementDate = modal.querySelector('.commencement-date').value;
data.originalCompletionDate = modal.querySelector('.scheduled-completion-date').value;
// 공동도급 데이터 DB 삽입을 위한 식별자(^&)추가
const jointContract = addJointContractDelimiter();
data.jointContractComapnyName = jointContract.companyName;
data.jointContractShares = jointContract.companyShares;
data.jointContractKrw = jointContract.contractKrw;
data.jointContractUsd = jointContract.contractUsd;
data.representativeCompany = jointContract.representativeCompany;
} else {
data.projectNo = modal.querySelector('.project-no').innerText;
data.taskType = modal.querySelector('.task-type').innerText;
data.bid = modal.querySelector('.bid').innerText;
data.relativeClient = modal.querySelector('.client').innerText;
data.department = modal.querySelector('.department').innerText;
data.taskNmKr = modal.querySelector('.task-nm-kr').innerText;
data.scheduledCommencementDate = modal.querySelector('.scheuled-commencement-date').innerText;
data.contractPeriod = modal.querySelector('.contract-period').innerText.replace('개월', '');
data.projectManagerNm = modal.querySelector('.projectmanager-nm').innerText;
data.managerNm = modal.querySelector('.manager-nm').innerText;
data.contractDate = modal.querySelector('.contract-date').innerText.replaceAll('-', '');
data.commencementDate = modal.querySelector('.commencement-date').innerText.replaceAll('-', '');
data.originalCompletionDate = modal.querySelector('.scheduled-completion-date').innerText.replaceAll('-', '');
// 공동도급 데이터 DB 삽입을 위한 식별자(^&)추가
const jointContract = addJointContractDelimiter();
data.jointContractComapnyName = jointContract.companyName;
data.jointContractShares = jointContract.companyShares;
data.jointContractKrw = jointContract.contractKrw;
data.jointContractUsd = jointContract.contractUsd;
data.representativeCompany = jointContract.representativeCompany;
}
try {
const res = await axios.post(`/${projectId}/overview/saveSectionMiddleData`, data);
if (res.data.message = '200') {
alert('저장이 완료되었습니다.');
modal.style.display = 'none';
modalWrapper.style.display = 'none';
getData();
updateTimeBar();
} else {
alert('오류가 발생하였습니다.');
}
} catch (err) {
console.log(err);
}
});
// Section-Middle - 과업중지 이력 저장
document.querySelector('.overview-modal.task-period .modal-footer .btn-active')?.addEventListener('click', async () => {
const liList = document.querySelectorAll('.overview-modal.task-period .overview-modal-body ul li input');
const modal = document.querySelector('.overview-modal.task-period');
const modalWrapper = document.querySelector('.overview-modal-wrapper');
const saveLiArr = [];
if(overviewVars.deleteTaskHistory.length > 0)await deleteTaskHistory();
for (let i = 0; i < liList.length; i += 6) {
// 과업중지이력의 Value가 전부 미입력 상태일때는 저장하지 않는다.
const values = [liList[i].value, liList[i+1].value, liList[i+2].value, liList[i+3].value, liList[i+4].value, liList[i+5].value];
if(values.every(value => value === ''))continue;
saveLiArr.push({
projectId: vars.project_id,
order: liList[i].value,
suspensionDate: liList[i + 1].value,
suspensionReason: liList[i + 2].value,
resumptionDate: liList[i + 3].value,
consultationContent: liList[i + 4].value,
changeDate: liList[i + 5].value
});
}
try {
const res = await axios.post(`/${vars.project_id}/overview/saveTaskHistoryData`, saveLiArr);
if (res.data.message === '200') {
alert('과업중지 이력이 저장되었습니다.');
makeTaskHistory();
modal.style.display = 'none';
modalWrapper.style.display = 'none';
}
}
catch (err) {
alert('과업중지 이력 저장에 실패하였습니다.');
}
});
export async function deleteTaskHistory() {
if(overviewVars.deleteTaskHistory.length > 0){
try{
const res = await axios.delete(`/${vars.project_id}/overview/deleteTaskPeriodData`, { data: { deleteArr : overviewVars.deleteTaskHistory } });
if(res.data.message === '200') overviewVars.deleteTaskHistory = [];
} catch(err) {
console.error(err, '과업중지이력 삭제 실패');
}
}
}
// 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Section-Middle 저장, 삭제 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Section-Right 저장, 삭제 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// Section-Right calendar 주요일정 저장
document.querySelector('.overview-modal.schedule .schedule-save')?.addEventListener('click', async () => {
const modal = document.querySelector('.overview-modal.schedule');
const modalWrapper = document.querySelector('.overview-modal-wrapper');
const projectId = vars.project_id;
const country = document.querySelector('.overview-modal.schedule input[type=radio]:checked').value;
const color = document.querySelector('.overview-modal .color .label-color.on').dataset.color;
const title = document.querySelector('.overview-modal.schedule .schedule-title').value;
const content = document.querySelector('.overview-modal.schedule .schedule-content').value;
const startDate = document.querySelector('.overview-modal.schedule .startDate').value;
const startTime = document.querySelector('.overview-modal.schedule .startTime').value;
const endDate = document.querySelector('.overview-modal.schedule .endDate').value;
const endTime = document.querySelector('.overview-modal.schedule .endTime').value;
const scheduleId = document.querySelector('.overview-modal.schedule').dataset.scheduleId;
if (!title) return alert('일정의 제목을 입력해주세요.')
// 주요일정 하루종일 기능
const alldayChecked = document.querySelector('.overview-modal.schedule .custom-checkbox.all-day input').checked;
const startDateTimeStr = `${startDate}T${startTime}`;
const endDateTimeStr = `${endDate}T${endTime}`;
if(alldayChecked === true){
//날짜 유효성 검증
const startDay = new Date(startDate);
const endDay = new Date(endDate);
if(startDay > endDay) return alert('시작일은 종료일보다 늦을 수 없습니다. 다시 설정해주십시오.');
} else {
//날짜 + 시간 유효성 검증
const startDateTime = new Date(startDateTimeStr);
const endDateTime = new Date(endDateTimeStr);
if (startDateTime > endDateTime)return alert('시작일은 종료일보다 늦을 수 없습니다. 다시 설정해주십시오.');
}
let data = { scheduleId, projectId, country, color, title, content, startDateTimeStr, endDateTimeStr }
// 기존일정 id 삽입
if (scheduleId !== '') data.scheduleId = scheduleId;
try {
const res = await axios.post(`/${projectId}/overview/saveScheduleData`, data);
if (res.data.message == '200') {
modal.style.display = 'none';
modalWrapper.style.display = 'none';
alert('일정 저장이 완료되었습니다.');
generateCalendar(overviewVars.currentYear, overviewVars.currentMonth);
}
} catch (err) {
console.log(err);
}
});
// Section-Right calendar 주요일정 삭제
document.querySelector('.overview-modal.schedule .schedule-delete')?.addEventListener('click', async () => {
if (confirm('일정을 삭제하시겠습니까?')) {
const modal = document.querySelector('.overview-modal.schedule');
const modalWrapper = document.querySelector('.overview-modal-wrapper');
const scheduleId = document.querySelector('.overview-modal.schedule').dataset.scheduleId;
const res = await axios.delete(`/${vars.project_id}/overview/deleteScheduleData`, { data: { scheduleId } });
if (res.data.message = '200') {
alert('일정이 삭제되었습니다.');
modal.style.display = 'none';
modalWrapper.style.display = 'none';
generateCalendar(overviewVars.currentYear, overviewVars.currentMonth);
} else {
alert('삭제에 실패하였습니다.')
}
}
});
// 주요 현안 및 이슈
document.querySelector('.overview .xs-btn-type.issue-edit')?.addEventListener('click', () => {
setIssueEditMode(true)
});
// 메모 입력시 내용의 길이에 따라
document.querySelector('.overview .box.issue .wrap .issue-content')?.addEventListener('input', () => {
const issueMessage = document.querySelector('.overview .box.issue .wrap .message')
const issueContent = document.querySelector('.overview .box.issue .wrap .issue-content');
if(issueContent.value.length > 0){
issueMessage.style.color = '#ddd';
} else {
issueMessage.style.color = '#111';
}
});
document.querySelector('.overview .xs-btn-type.issue-save')?.addEventListener('click', async () => {
const projectId = vars.project_id;
const issueContent = document.querySelector('.overview .box.issue .wrap .issue-content');
const issueData = issueContent.value;
let data = {projectId, issueData}
try{
const result = await axios.post(`/${projectId}/overview/saveIssueData`, data);
if(result.data.message === '200'){
alert('주요 현안 및 이슈 저장 성공');
setIssueEditMode(false)
overviewVars.issueData = issueData;
}
} catch (err) {
console.error(err)
}
});
document.querySelector('.overview .xs-btn-type.issue-cancle')?.addEventListener('click', () => {
const issueContent = document.querySelector('.overview .box.issue .wrap .issue-content');
issueContent.value = overviewVars.issueData;
setIssueEditMode(false);
});
// 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Section-Right 저장, 삭제 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
@@ -0,0 +1,765 @@
import { vars } from '../archive/variable.js';
import { displayFileName, checkDuplicatesFileNameAndExt , restrictToNumber, calculateTotalJointContract, showToolTip, overviewVarsInit } from './overviewCommon.js';
import { makeTaskHistory } from './overviewPageRenderer.js';
import { overviewVars } from './overviewVariable.js';
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Overview 모달 공통 사용 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// 모달CLSOE
const modalNames = ['facility-size', 'section-left', 'section-middle', 'schedule-list', 'schedule', 'task-period'];
modalNames.forEach(name => {
const modal = document.querySelector(`.overview-modal.${name}`);
if (!modal) return;
const closeBtn = modal.querySelector('.overview-modal-header--end img');
const footerCloseBtn = modal.querySelector('.modal-footer .btn-close');
closeBtn?.addEventListener('click', () => {
modal.style.display = 'none';
document.querySelector('.overview-modal-wrapper').style.display = 'none';
// 변수 초기화
overviewVarsInit();
});
footerCloseBtn?.addEventListener('click', () => {
modal.style.display = 'none';
document.querySelector('.overview-modal-wrapper').style.display = 'none';
// 변수 초기화
overviewVarsInit();
});
});
// 모달 영역 외에 클릭했을때 모달 꺼지는 이벤트
// document.querySelector('.overview-modal-wrapper')?.addEventListener('pointerdown', (e) => {
// document.querySelector('.overview-modal-wrapper').style.display = 'none';
// document.querySelectorAll('.overview-modal').forEach(modal => {
// if (!modal.contains(e.target)) {
// modal.style.display = 'none';
// }
// });
// });
// 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Overview 모달 공통 사용 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Secion-Left 모달 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// 위치도/개요도 드래그앤드롭
export function fileDragAndDrop() {
const dropbox = document.querySelector('.overview-modal .dropbox');
dropbox.addEventListener('dragenter', (e) => {
e.preventDefault();
dropbox.style.background = '#999';
});
dropbox.addEventListener('dragover', (e) => {
e.preventDefault();
dropbox.style.background = '#999';
});
dropbox.addEventListener('dragleave', (e) => {
e.preventDefault();
dropbox.style.background = '#e6e8ea';
});
dropbox.addEventListener('drop', (e) => {
e.preventDefault();
dropbox.style.background = '#e6e8ea';
const files = Array.from(e.dataTransfer.files);
// 중복 이름과 확장자 체크
checkDuplicatesFileNameAndExt(files);
displayFileName();
});
};
// section-left 모달 열기
document.querySelector('.overview .box-header--right.section-left-edit')?.addEventListener('click', () => {
const modal = document.querySelector('.overview-modal.section-left');
const modalWrapper = document.querySelector('.overview-modal-wrapper');
modal.querySelector('.business-purpose').value = document.querySelector('.overview .box-body.business-purpose').innerText;
modal.querySelector('.performance-area').value = document.querySelector('.overview .box-body.location-map-detail .performance-area').innerText;
modal.querySelector('.reference-area').value = document.querySelector('.overview .box-body.location-map-detail .reference-area').innerText;
modal.querySelector('.facility-overview').value = document.querySelector('.overview .box-body.facility-size .facility-overview').innerText;
//overseas용 분기
if(!overviewVars.overseas){
modal.querySelector('.nation').value = document.querySelector('.overview .box-body.location-map-detail .nation-nm').innerText;
modal.querySelector('.continent').value = document.querySelector('.overview .box-body.location-map-detail .continent').innerText;
} else {
modal.querySelector('.nation').innerText = document.querySelector('.overview .box-body.location-map-detail .nation-nm').innerText;
modal.querySelector('.continent').innerText = document.querySelector('.overview .box-body.location-map-detail .continent').innerText;
}
overviewVarsInit();
if(overviewVars.filesNameArr.length > 0) displayFileName();
modal.querySelectorAll('input[type=file]').forEach(input => { input.value = ''; })
makeTabCell(overviewVars.originalSectionTabData);
// 토글 처리
if (modal.style.display === 'block') {
modal.style.display = 'none';
return;
}
modal.style.display = 'block';
modalWrapper.style.display = 'block';
});
// section-left 모달 시설규모 탭,셀 생성 함수
export function makeTabCell(data) {
const tabContainer = document.querySelector('.section-tabs');
const contentContainer = document.querySelector('.section-content');
// 가로 스크롤 이벤트
tabContainer.addEventListener('wheel', (e) => {
e.preventDefault();
tabContainer.scrollLeft += e.deltaY;
});
tabContainer.innerHTML = '';
contentContainer.innerHTML = '';
// 데이터 없을 때 기본값
if (Object.keys(data).length === 0) {
data = { '': [{ key: '', value: '', id: '' }] };
}
const titles = Object.keys(data);
titles.forEach((title, index) => {
const tab = createTab(title, index === 0);
bindTabEvents(tab, contentContainer);
tabContainer.appendChild(tab);
});
// 탭 추가 버튼
const addBtn = document.createElement('button');
addBtn.classList.add('xs-btn', 'add');
addBtn.innerHTML = `<img class="icon" src="/main/img/overview/icon-add.svg" alt="icon-add">`;
tabContainer.appendChild(addBtn);
addBtn.addEventListener('click', () => {
const newTab = createTab('', true);
bindTabEvents(newTab, contentContainer, true);
tabContainer.insertBefore(newTab, addBtn);
newTab.click();
});
// 첫번째 탭 클릭
if (titles.length > 0) {
tabContainer.querySelector('.tab')?.click();
}
}
// 탭 생성
function createTab(title, isActive) {
const tab = document.createElement('div');
tab.className = 'tab';
if (isActive) tab.classList.add('click-on');
const input = document.createElement('input');
input.className = 'tab-style-input';
input.type = 'text';
input.value = title;
input.placeholder = '탭 제목 입력';
const closeBtn = document.createElement('button');
closeBtn.className = 'xs-btn';
closeBtn.innerHTML = `<img class="icon" src="/main/img/overview/icon-close-111.svg">`;
tab.appendChild(input);
tab.appendChild(closeBtn);
return tab;
}
// 탭 클릭, 제목입력, 삭제버튼 이벤트
function bindTabEvents(tab, contentContainer, isNew = false) {
const input = tab.querySelector('input');
const closeBtn = tab.querySelector('button');
// 탭 클릭 이벤트
tab.addEventListener('click', () => {
document.querySelectorAll('.section-tabs .tab').forEach(t => t.classList.remove('click-on'));
tab.classList.add('click-on');
renderCells(input.value.trim(), contentContainer);
});
// 탭 제목 input 변경 이벤트
let previousSection = input.value.trim();
input.addEventListener('input', () => {
const section = String(input.value.trim());
// 탭 제목이 빈값 일때 삭제 처리
if (section === '') {
if (previousSection && overviewVars.sectionTabData[previousSection]) delete overviewVars.sectionTabData[previousSection];
showToolTip(input, '탭 제목이 없어 셀 내용이 초기화되었습니다.')
// 셀 초기화
document.querySelectorAll('.section-content .section-content--cell').forEach((cell, index) => {
if(index === 0){
cell.querySelectorAll('input').forEach(input => input.value = '');
} else{
// 셀 추가 버튼을 건너뜀
if(cell.className === 'section-content--cell add-cell-btn') return;
cell.remove();
}
});
previousSection = '';
return;
}
// 탭 제목이 같을때 처리 막기
if (section !== previousSection && overviewVars.sectionTabData[section]){
showToolTip(input, '이미 존재하는 탭 이름입니다. 다른 이름을 입력해주세요.')
input.value = previousSection;
return;
}
// 탭 이름이 변경될때
if (previousSection && previousSection !== section) {
if (overviewVars.sectionTabData[previousSection]) {
overviewVars.sectionTabData[section] = overviewVars.sectionTabData[previousSection];
delete overviewVars.sectionTabData[previousSection];
} else {
overviewVars.sectionTabData[section] = [];
}
} else if (!overviewVars.sectionTabData[section]) {
overviewVars.sectionTabData[section] = [];
}
previousSection = section;
});
// 탭 삭제 이벤트
closeBtn.addEventListener('click', async (e) => {
e.stopPropagation();
const title = input.value;
const tabLength = document.querySelectorAll('.section-tabs .tab').length;
const tabInput = document.querySelector('.section-tabs .tab input');
const cell = document.querySelectorAll('.section-content--cell input');
const emptyTab = !tabInput?.value.trim();
const emptyCell = Array.from(cell).every(input => !input.value.trim());
// 탭이 한 개 남았고 공백일 경우 삭제 막기
if (tabLength === 1 && emptyTab && emptyCell) return;
try {
if (tabLength === 1) {
delete overviewVars.sectionTabData[title];
document.querySelector('.section-tabs .tab input').value = '';
document.querySelectorAll('.section-content .section-content--cell').forEach((cell, index) => {
if(index === 0){
cell.querySelectorAll('input').forEach(input => input.value = '');
} else{
// 셀 추가 버튼을 건너뜀
if(cell.className === 'section-content--cell add-cell-btn') return;
cell.remove();
}
});
} else {
delete overviewVars.sectionTabData[title];
tab.remove();
const allTabs = document.querySelectorAll('.section-tabs .tab');
const lastTab = allTabs[allTabs.length - 1];
if (lastTab) lastTab.click();
}
} catch (err) {
console.error(err);
}
});
}
// 셀 렌더딩
function renderCells(title, container) {
container.innerHTML = '';
let cells = overviewVars.sectionTabData[title] || [{ key: '', value: '', id: '' }];
// 타이틀만 있고 셀의 내용이 없을때 기본값 설정
if(cells.length === 0) cells = [{ key: '', value: '', id: '' }];
cells.forEach(cell => {
const row = createCell(cell);
bindCellEvents(row, title);
container.appendChild(row);
});
if (!container._hasInputEvent) {
container.addEventListener('input', (e) => {
if (!e.target.matches('input')) return;
const section = document.querySelector('.tab.click-on input').value.trim();
if (section === '') {
container.querySelectorAll('.section-content--cell input').forEach(cell => {
cell.value = ''
});
showToolTip(e.target, '탭의 제목을 먼저 입력해주세요')
return
}
const keyInputs = container.querySelectorAll('input.short-input');
const valueInputs = container.querySelectorAll('input.middle-input');
const keyValueList = [];
for (let i = 0; i < keyInputs.length; i++) {
const key = keyInputs[i].value;
const value = valueInputs[i] ? valueInputs[i].value : '';
const id = keyInputs[i].dataset.id ? parseInt(keyInputs[i].dataset.id) : null;
const projectId = vars.project_id;
keyValueList.push({ key, value, id, projectId });
}
overviewVars.sectionTabData[section] = keyValueList;
});
container._hasInputEvent = true;
}
// 셀 추가 버튼
const addCell = document.createElement('div');
addCell.className = 'section-content--cell add-cell-btn';
addCell.innerHTML = `
<img class="icon" src="/main/img/overview/icon-add-primary.svg" alt="icon-add-primary">
<h3>셀추가</h3>`;
addCell.addEventListener('click', () => {
const newCell = createCell({ key: '', value: '', id: null });
bindCellEvents(newCell, title);
container.insertBefore(newCell, addCell);
});
container.appendChild(addCell);
}
// 셀 생성
function createCell(cell) {
const row = document.createElement('div');
row.className = 'section-content--cell';
row.innerHTML = `
<input class="short-input" type="text" value="${cell.key ?? ''}" data-id="${cell.id ?? ''}" placeholder="셀 제목 입력">
<input class="middle-input" type="text" value="${cell.value ?? ''}" data-id="${cell.id ?? ''}" placeholder="셀 내용 입력">
`;
const btn = document.createElement('button');
btn.classList.add('xs-btn', 'cell-remove');
btn.innerHTML = `<img src="/main/img/overview/icon-close-111.svg">`;
row.appendChild(btn);
return row;
}
// 셀 삭제 버튼 이벤트 추가
function bindCellEvents(row, title) {
const btn = row.querySelector('.cell-remove');
const cellId = row.querySelector('input')?.dataset.id;
btn.addEventListener('click', async () => {
try {
// 기본생성된 셀 이벤트에서 title이 들어오지 않는 경우가 있어서 값을 직접 넣어줌
title = document.querySelector('.section-tabs .tab.click-on input').value;
const cells = document.querySelectorAll('.section-content--cell');
if (cellId) {
// DB 삭제 성공 시 sectionTabData 업데이트
const index = overviewVars.sectionTabData[title].findIndex(item => item.id == cellId);
if (index !== -1) overviewVars.sectionTabData[title].splice(index, 1);
} else {
// 탭 입력이 되지않고 sectionTabData에 data가 없을때 row 삭제처리
if(!overviewVars.sectionTabData[title]){
if(cells.length > 2){
return row.remove();
} else {
return;
}
}
const index = overviewVars.sectionTabData[title].findIndex(item => item.key === row.querySelector('.short-input')?.value && item.value === row.querySelector('.middle-input')?.value);
if(index !== -1) overviewVars.sectionTabData[title].splice(index, 1);
}
// 셀이 하나뿐이면 value 초기화, 아니면 삭제
if (cells.length === 2) {
row.querySelectorAll('input').forEach(input => input.value = '');
} else {
row.remove();
}
} catch (err) {
console.error(err);
}
});
}
// Section-left 시설규모 모달 OPEN
document.querySelector('.overview .box-header--right.facility-size button')?.addEventListener('click', async () => {
const modalWrapper = document.querySelector('.overview-modal-wrapper');
const modal = document.querySelector('.overview-modal.facility-size');
//토글
if (modal.style.display === 'block') {
modal.style.display = 'none';
modalWrapper.style.display = 'none';
return;
}
try {
const res = await axios.get(`/${vars.project_id}/overview/getFacilitySizeData`, { params: { projectId: vars.project_id } });
if (res.data.message === '200') {
const modalBody = modal.querySelector('.overview-modal-body');
modalBody.innerHTML = '';
const dataArr = res.data.data;
if (dataArr.length === 0) {
modalBody.innerHTML = `<img src="/main/img/overview/non-facilitySize-image.svg">`
}
let currentTitle = '';
for (let i = 0; i < dataArr.length; i++) {
if (dataArr[i].title !== currentTitle) {
currentTitle = dataArr[i].title;
const boxHeader = document.createElement('div');
boxHeader.className = 'box-header';
const h3 = document.createElement('h3');
h3.textContent = currentTitle;
boxHeader.appendChild(h3);
modalBody.appendChild(boxHeader);
}
const typeWrap = document.createElement('div');
typeWrap.className = 'type--wrap';
const left = document.createElement('div');
left.className = 'type--wrap-left';
const p = document.createElement('p');
p.textContent = dataArr[i].key;
left.appendChild(p);
const right = document.createElement('div');
right.className = 'type--wrap-right';
const h4 = document.createElement('h4');
h4.textContent = dataArr[i].value;
right.appendChild(h4);
typeWrap.appendChild(left);
typeWrap.appendChild(right);
modalBody.appendChild(typeWrap);
}
}
modal.style.display = 'block';
modalWrapper.style.display = 'block';
} catch (err) {
console.log(err);
}
});
// section-left 위치도/개요도 이미지 저장
document.querySelector('.overview-modal.section-left .overview-img-file-input')?.addEventListener('change', () => {
const files = Array.from(document.querySelector('.overview-modal.section-left .overview-img-file-input').files);
// 중복 이름과 확장자 체크
checkDuplicatesFileNameAndExt(files);
// 등록된 파일의 이름을 추가
displayFileName();
// 초기화
document.querySelector('.overview-modal.section-left .overview-img-file-input').value = '';
});
// 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Secion-Left 모달 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Secion-Middle 모달 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// section-middle 모달 열기
document.querySelector('.overview .box-header--right.section-middle-edit')?.addEventListener('click', () => {
const modal = document.querySelector('.overview-modal.section-middle');
const modalWrapper = document.querySelector('.overview-modal-wrapper');
modal.querySelector('.abbreviated-name').value = document.querySelector('.overview .abbreviated-name').innerText;
modal.querySelector('.task-nm-en').value = document.querySelector('.overview .task-nm-en').innerText;
modal.querySelector('.task-purpose').value = document.querySelector('.overview .task-purpose').innerText;
modal.querySelector('.order-size-usd').value = document.querySelector('.overview .order-size-usd').innerText;
modal.querySelector('.order-size-krw').value = document.querySelector('.overview .order-size-krw').innerText;
modal.querySelector('.client-origin').value = document.querySelector('.overview .client-origin').innerText;
modal.querySelector('.financial').value = document.querySelector('.financial').innerText;
modal.querySelector('.financial-country').value = document.querySelector('.overview .financial-country').innerText;
modal.querySelector('.selection-method').value = document.querySelector('.overview .selection-method').innerText;
modal.querySelector('.overview-table-container').innerHTML = document.querySelector('.overview .overview-table-container ').innerHTML;
modal.querySelector('.support-department').value = document.querySelector('.overview .support-department').innerText;
modal.querySelector('.support-manager-nm').value = document.querySelector('.overview .support-manager-nm').innerText;
modal.querySelector('.completion-date').value = document.querySelector('.overview .completion-date').innerText;
// overseas용 분기
if(!overviewVars.overseas){
modal.querySelector('.project-no').value = document.querySelector('.overview .project-no').innerText;
modal.querySelector('.task-type').value = document.querySelector('.overview .task-type').innerText;
modal.querySelector('.bid').value = document.querySelector('.overview .bid').innerText;
modal.querySelector('.client').value = document.querySelector('.overview .client').innerText;
modal.querySelector('.department').value = document.querySelector('.overview .department').innerText;
modal.querySelector('.currency-code').value = document.querySelector('.overview .currency-code').innerText;
modal.querySelector('.task-nm-kr').value = document.querySelector('.overview .task-nm-kr').innerText;
modal.querySelector('.contract-period').value = document.querySelector('.overview .contract-period').innerText;
modal.querySelector('.projectmanager-nm').value = document.querySelector('.overview .projectmanager-nm').innerText;
modal.querySelector('.manager-nm').value = document.querySelector('.overview .manager-nm').innerText;
modal.querySelector('.contract-date').value = document.querySelector('.overview .contract-date').innerText;
modal.querySelector('.commencement-date').value = document.querySelector('.overview .commencement-date').innerText;
modal.querySelector('.scheduled-completion-date').value = document.querySelector('.overview .scheduled-completion-date').innerText;
modal.querySelector('.scheuled-commencement-date').value = document.querySelector('.overview .scheuled-commencement-date').innerText;
modal.querySelectorAll('.joint-contract-company-name').forEach(company => { company.contentEditable = true; company.style.color = '#FF3D00'; })
// 공동도급 수정, 색상변화, 숫자 제한
modal.querySelectorAll('.joint-contract-company-shares, .joint-contract-krw, .joint-contract-usd').forEach(joint => {
joint.contentEditable = true;
joint.style.color = '#FF3D00';
joint.addEventListener('input', (event) => {
restrictToNumber(joint, event);
calculateTotalJointContract(joint);
});
});
} else {
modal.querySelector('.project-no').innerText = document.querySelector('.overview .project-no').innerText;
modal.querySelector('.task-type').innerText = document.querySelector('.overview .task-type').innerText;
modal.querySelector('.bid').innerText = document.querySelector('.overview .bid').innerText;
modal.querySelector('.client').innerText = document.querySelector('.overview .client').innerText;
modal.querySelector('.department').innerText = document.querySelector('.overview .department').innerText;
modal.querySelector('.currency-code').innerText = document.querySelector('.overview .currency-code').innerText;
modal.querySelector('.task-nm-kr').innerText = document.querySelector('.overview .task-nm-kr').innerText;
modal.querySelector('.contract-period').innerText = document.querySelector('.overview .contract-period').innerText;
modal.querySelector('.projectmanager-nm').innerText = document.querySelector('.overview .projectmanager-nm').innerText;
modal.querySelector('.manager-nm').innerText = document.querySelector('.overview .manager-nm').innerText;
modal.querySelector('.contract-date').innerText = document.querySelector('.overview .contract-date').innerText;
modal.querySelector('.commencement-date').innerText = document.querySelector('.overview .commencement-date').innerText;
modal.querySelector('.scheduled-completion-date').innerText = document.querySelector('.overview .scheduled-completion-date').innerText;
modal.querySelector('.scheuled-commencement-date').innerText = document.querySelector('.overview .scheuled-commencement-date').innerText;
}
const jointTable = modal.querySelector('.overview-table-container')
// 탭 컨테이너 가로스크롤 휠 이벤트 추가
jointTable.addEventListener('wheel', (e)=> {
e.preventDefault();
jointTable.scrollLeft += e.deltaY;
});
// 토글 처리
if (modal.style.display === 'block') {
modal.style.display = 'none';
return;
}
modal.style.display = 'block';
modalWrapper.style.display = 'block';
});
// section-middle 과업기간 모달 열기
document.querySelector('.overview .box-header--right.task-period')?.addEventListener('click', async () => {
const modal = document.querySelector('.overview-modal.task-period');
const modalWrapper = document.querySelector('.overview-modal-wrapper');
modal.querySelector('.contract-date').innerText = document.querySelector('.overview .contract-date').innerText;
modal.querySelector('.commencement-date').innerText = document.querySelector('.overview .commencement-date').innerText;
modal.querySelector('.scheduled-completion-date').innerText = document.querySelector('.overview .scheduled-completion-date').innerText;
modal.querySelector('.completion-date').innerText = document.querySelector('.overview .completion-date').innerText;
// 과업 중지 이력 생성
makeTaskHistory();
// 토글 처리
if (modal.style.display === 'block') {
modal.style.display = 'none';
return;
}
modal.style.display = 'block';
modalWrapper.style.display = 'block';
});
// section-middle 과업중지 이력 리스트 추가
document.querySelector('.overview-modal.task-period .box-header--right button')?.addEventListener('click', () => {
const ul = document.querySelector('.overview-modal.task-period .overview-modal-body ul');
const li = document.createElement('li');
// 차수
const numberDiv = document.createElement('div');
numberDiv.className = 'work-list-number';
const orderInput = document.createElement('input');
orderInput.type = 'text';
orderInput.className = 'order work-list-number';
numberDiv.appendChild(orderInput);
// 중지일자
const dateDiv = document.createElement('div');
dateDiv.className = 'work-list-date';
const dateInput = document.createElement('input');
dateInput.className = 'suspension-date work-list-date';
dateInput.type = 'date';
dateDiv.appendChild(dateInput);
// 중지사유
const reasonDiv = document.createElement('div');
reasonDiv.className = 'work-list-script';
const reasonInput = document.createElement('input');
reasonInput.className = 'suspension-reason work-list-script';
reasonInput.type = 'text';
reasonDiv.appendChild(reasonInput);
// 재개일자
const redateDiv = document.createElement('div');
redateDiv.className = 'work-list-redate';
const redateInput = document.createElement('input');
redateInput.className = 'resumption-date work-list-redate';
redateInput.type = 'date';
redateDiv.appendChild(redateInput);
// 협의내용
const detailDiv = document.createElement('div');
detailDiv.className = 'work-list-detail';
const consultInput = document.createElement('input');
consultInput.className = 'consuletation-content work-list-detail';
consultInput.type = 'text';
detailDiv.appendChild(consultInput);
// 변경 일자
const changeDiv = document.createElement('div');
changeDiv.className = 'work-list-changedate';
const changeInput = document.createElement('input');
changeInput.className = 'change-date work-list-changedate';
changeInput.type = 'date';
changeDiv.appendChild(changeInput);
const btn = document.createElement('button');
btn.className = 'xs-btn-type';
const img = document.createElement('img');
img.className = 'icon';
img.src = '/main/img/overview/icon-close-111.svg';
img.alt = 'icon-close-111';
btn.appendChild(img);
// 과업 중지이력 리스트 개별 삭제버튼
btn.addEventListener('click', async (e) => {
li.remove();
});
li.append(numberDiv, dateDiv, reasonDiv, redateDiv, detailDiv, changeDiv, btn);
ul.appendChild(li);
});
if(!overviewVars.overseas){
// section-middle 공동도급 열 추가
document.querySelector('.overview-modal.section-middle .joint-contract-add')?.addEventListener('click', () => {
const table = document.querySelector('.overview-modal.section-middle .overview-table-container table');
const headerRow = table.querySelector('thead tr');
const currentCols = headerRow.querySelectorAll('th').length;
// 총계와 주관사
const newIndex = currentCols - 1;
document.querySelector('.overview-modal .total-joint-contract-company-name').innerText = newIndex;
const newTh = document.createElement('th');
if (newIndex < 2) {
newTh.innerText = '주관사';
} else {
newTh.innerText = `공동도급${newIndex - 1}`;
}
headerRow.appendChild(newTh);
const tbodyRows = table.querySelectorAll('tbody tr');
const defaultValues = ['회사명', '0', '0', '0'];
const classList = ['joint-contract-company-name', 'joint-contract-company-shares', 'joint-contract-krw', 'joint-contract-usd'];
tbodyRows.forEach((row, idx) => {
const td = document.createElement('td');
td.className = classList[idx];
td.innerText = defaultValues[idx];
td.contentEditable = true;
td.style.color = '#FF3D00';
// 공동도급 숫자 제한
if (idx > 0) {
td.addEventListener('input', (event) => {
restrictToNumber(td, event);
calculateTotalJointContract(td);
});
}
row.appendChild(td);
});
});
// section-middle 공동도급 열 삭제
document.querySelector('.overview-modal.section-middle .joint-contract-delete')?.addEventListener('click', () => {
const table = document.querySelector('.overview-modal.section-middle .overview-table-container table');
const theadRow = table.querySelector('thead tr');
const ths = theadRow.querySelectorAll('th');
// 주관사까지 삭제했을때 계산할 값이 없으므로 초기화
if (ths.length === 3){
document.querySelector('.overview-modal .total-joint-contract-company-name').innerText = 0;
document.querySelectorAll('.overview-modal .total-joint-contract-company-shares, .overview-modal .total-joint-contract-krw, .overview-modal .total-joint-contract-usd').forEach(el => el.innerText = 0);
}
if (ths.length <= 2) {
alert("삭제할 수 있는 열이 없습니다.");
return;
}
// 마지막 th 삭제
theadRow.removeChild(ths[ths.length - 1]);
// tbody의 마지막 td 삭제
const tbodyRows = table.querySelectorAll('tbody tr');
tbodyRows.forEach(row => {
const tds = row.querySelectorAll('td');
if (tds.length > 2) {
row.removeChild(tds[tds.length - 1]);
}
});
// 삭제시에 총계 계산
document.querySelectorAll('.overview-modal.section-middle .overview-table-container td').forEach(td => {
calculateTotalJointContract(td);
});
// 총 회사 수 업데이트
document.querySelector('.overview-modal .total-joint-contract-company-name').innerText = ths.length - 3;
});
}
// 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Secion-Middle 모달 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
//🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽 Secion-Right 모달 시작 🔽🔽🔽🔽🔽🔽🔽🔽🔽🔽
// section-right 캘린더 주요일정 하루종일 버튼
document.querySelector('.overview-modal.schedule .custom-checkbox.all-day')?.addEventListener('change', () => {
const allDay = document.querySelector('.overview-modal.schedule .custom-checkbox.all-day input');
const startTime = document.querySelector('.overview-modal.schedule .startTime');
const endTime = document.querySelector('.overview-modal.schedule .endTime');
const isChecked = allDay.checked;
startTime.disabled = isChecked;
endTime.disabled = isChecked;
if (isChecked) {
startTime.value = '';
endTime.value = '';
} else {
startTime.value = '00:00';
endTime.value = '23:59';
}
});
// section-right 주요일정 Color 클릭 효과
document.querySelectorAll('.overview-modal .color .label-color')?.forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.overview-modal .color .label-color').forEach(label => {
label.classList.remove('on');
});
e.target.classList.add('on');
});
});
// 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼 Secion-Right 모달 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
export const overviewVars = {
// 시설규모 탭안에 셀요소들을 담아놓은 객체
sectionTabData : {},
originalSectionTabData : {},
// 캘린더 현재년도
currentYear : new Date().getFullYear(),
// 캘린더 현재월
currentMonth : new Date().getMonth(),
// 공동도급 정렬 배열
companyArr : [],
// 이미지 파일들을 담은 전역변수
filesArr : [],
// 이미지 파일이름만을 담은 전역변수
filesNameArr : [],
// 이미지 파일들의 size를 담은 전역변수
filesSizeArr : [],
originalFilesArr : [],
originalFilesNameArr : [],
originalFilesSizeArr : [],
deleteImgArr : [],
// DB에 있는 주요 현안 및 이슈 담은 전역변수
issueData : {},
// 삭제할 과업중지 이력을 모아놓을 배열
deleteTaskHistory : [],
}
// window.overviewVars = overviewVars;
+574
View File
@@ -0,0 +1,574 @@
//형태는 ?path=fullPath(ex.http://gsim.hanmac..../1.jpg)&data=eadNadfl
//data는 json({key:value})을 stringify한 후에 btoa로 base64로 변경해서 달기.
//ex)
//const jsonData = JSON.stringify({ key1: "value1", key2: "value2" });
//const encodedData = btoa(jsonData);
//?path=${fullPath}&data=${encodedData}
//넘어간 data는 모두 표출됨, 숨길 data는 key 앞에 $붙여주기(type, password는 그냥써도 무조건 표출X)
//받는 형태는 pdf, img(png, jpg, jpeg, panorama), mp4, gsim(gsimViewer로 redirect), ifc, 미지원 6가지
const searchParam = new URLSearchParams(window.location.search);
//serchParam 비워주기 - debug off
// window.history.replaceState(null, null, '/')
const fullPath = searchParam.get('path');
let ext = fullPath.toLowerCase().split('.').pop();//가장 마지막.이후 확장자
const dataA = searchParam.get('data');
// title filename으로 변경
document.title = fullPath.split('/').pop();
let data = (dataA)?JSON.parse((dataA)?decodeURIComponent(escape(atob(dataA))):'{}'):undefined;
//data로 ext 넘어오면 무조건 변경
ext = data.$ext;
// 🚩3d모델뷰어 썸네일 생성을 위해 아래의 변수 추가
const thumbnail_key = decodeURIComponent(searchParam.get('thumbnail_key'));
const path_name = searchParam.get('path_name');
const resourcePath = decodeURIComponent(searchParam.get('resourcePath'));
const dataId = searchParam.get('dataId');
if(data && Object.keys(data).length>0 && (data.$type == 'text'|| data.type == 'text')){
//type text로 넘어왔을때 무조건 text뷰어로 연결
_openText(fullPath);
}else{
switch(ext){
case 'pdf':
_openPdf(fullPath,data);
break;
case 'mp4':
case 'mov':
case 'webm':
_openVideo(fullPath);
break;
case 'png':
case 'jpg':
case 'jpeg':
case 'webp':
case 'gif':
if(data && Object.keys(data).length>0 && (data.$type == 'panorama'||data.type == 'panorama')){
// data 있으면서 type이 panorama인 경우
_openPano(fullPath);
}else{
_openImage(fullPath);
}
break;
case 'gsim':
// gsim viewer 주소로 변경 필요
window.location.href = `/libs/gsimViewer/gsimViewer.html?${searchParam.toString()}`;
break;
case 'ifc':
_openIfc(fullPath, thumbnail_key, resourcePath, dataId, path_name);
break;
case 'txt':
case 'log':
case 'md':
_openText(fullPath);
break;
case 'url':
_openUrl(fullPath);
break;
case 'zip':
_openZip(fullPath);
break;
case 'glb':
case 'gltf':
case 'obj':
case 'stl':
case 'fbx':
case '3dm':
_open3d(fullPath, thumbnail_key, resourcePath, dataId, path_name);
break;
case 'html':
_openHtml(fullPath);
break;
}
if(data && Object.keys(data).length>0){
let keys = Object.keys(data).filter(key=>key.indexOf('$') != 0);
if(keys.length > 0){
_drawMeta(data);
}
}
}
function _openText(path){
// ext를 매개변수로 받아도 md파일이 txt로 넘어오기때문에 경로에서 직접 ext 추출
let ext = path.split('.').pop();
ext = ext.split('%')[0];
fetch(path).then(res => res.text()).then(data=>{
let pre;
// md 파일 확장자 일때 파싱 후 HTML 삽입
if(ext === 'md'){
pre = document.createElement('div');
pre.classList.add('markdown-wrap');
// mermaid 시퀀스 다이어 그램 변환
const renderer = new marked.Renderer();
const originalCode = renderer.code.bind(renderer);
renderer.code = (code) => {
if(code.lang === 'mermaid') return `<pre class="mermaid">${code.text}</pre>`
return originalCode(code);
}
marked.setOptions({ gfm: true, breaks: true, renderer});
const div = document.createElement('div');
div.classList.add('markdown-body');
div.innerHTML = marked.parse(data);
// mermaid 초기화 및 생성
const mermaidEls = div.querySelectorAll('.mermaid');
if(mermaidEls.length > 0){
mermaid.initialize({ startOnLoad: false });
mermaid.init(undefined, mermaidEls);
}
pre.appendChild(div);
} else {
pre = document.createElement('pre');
pre.style.width = '100%';
pre.style.height = '100%';
pre.style.objectFit = 'contain';
pre.textContent = data;
}
document.getElementById('popup_viewer').appendChild(pre);
if(ext === 'md')hljs.highlightAll();
}).catch(err=>console.error('파일 로드 실패', err));
}
function _openZip(path){
fetch(path).then(async data=>{
let zblob = await data.blob();
const zip = new JSZip();
await zip.loadAsync(zblob);
let folderText = ``;
let fileText = ``;
zip.forEach((relativePath, zipEntry) => {
let slashIdx = relativePath.indexOf('/');
if(slashIdx == -1 || slashIdx == relativePath.length -1){
if(zipEntry.dir){
folderText += `(폴더) ${zipEntry.name.split('/')[0]} \n`;
}else{
fileText += `(파일) ${zipEntry.name} \n`
}
}
});
const pre = document.createElement('pre');
pre.style.width = '100%';
pre.style.height = '100%';
pre.style.objectFit = 'contain';
pre.textContent = `${folderText}${(folderText == ``)?'':'\n'}${fileText}`;
document.getElementById('popup_viewer').appendChild(pre);
})
}
function _openUrl(path){
fetch(path).then(res => res.text()).then(data=>{
let url = data.split('URL=')[1];
let iframe = document.createElement('iframe');
iframe.src = url;
iframe.style.width = '100%'; // 컨테이너에 맞게 너비 설정
iframe.style.height = '100%'; // 컨테이너에 맞게 높이 설정
iframe.style.border = 'none'; // 테두리 제거 (선택 사항)
document.getElementById('popup_viewer').appendChild(iframe);
}).catch(err=>console.error('파일 로드 실패', err));
}
function _openHtml(path){
fetch(path).then(res => res.text()).then(data=>{
let iframe = document.createElement('iframe');
iframe.srcdoc = data;
iframe.style.width = '100%'; // 컨테이너에 맞게 너비 설정
iframe.style.height = '100%'; // 컨테이너에 맞게 높이 설정
iframe.style.border = 'none'; // 테두리 제거 (선택 사항)
document.getElementById('popup_viewer').appendChild(iframe);
}).catch(err=>console.error('파일 로드 실패', err));
}
// function _openPdf(path,data){
// let pdf_options = {
// url: path,
// initialPage: 1,
// };
// if(data && Object.keys(data).length > 0 && (data.$password || data.password)){
// pdf_options.password = (data.$password)?data.$password:data.password;
// }
// let iframe = document.createElement('iframe');
// iframe.src = `/libs/pdfViewer/web/viewer.html`;
// document.getElementById('popup_viewer').appendChild(iframe);
// iframe.addEventListener('load', () => {
// // pdf 실행 시 무조건 1페이지부터 보이도록 기존 pdf 히스토리 삭제
// try {
// let appWin = iframe.contentWindow;
// // PDF.js의 기본 히스토리 키
// appWin.localStorage.removeItem('pdfjs.history');
// // 또는 여러 키 삭제
// Object.keys(appWin.localStorage).forEach(k => {
// if (k.startsWith('pdfjs.history') || k.startsWith('pdfjs.preferences')) {
// appWin.localStorage.removeItem(k);
// }
// });
// } catch (e) { /* ignore */ }
// let app = document.querySelector('#popup_viewer iframe').contentWindow.PDFViewerApplication;
// app.pdfCursorTools._handTool.activate();
// app.open(pdf_options);
// iframe.width = '100%';
// iframe.height = '100%';
// });
// }
function _openPdf(path, data) {
let pdf_options = {
url: path,
initialPage: 1,
};
if (data && Object.keys(data).length > 0 && (data.$password || data.password)) {
pdf_options.password = (data.$password) ? data.$password : data.password;
}
let iframe = document.createElement('iframe');
iframe.src = `/libs/pdfViewer/web/viewer.html`;
document.getElementById('popup_viewer').appendChild(iframe);
iframe.addEventListener('load', () => {
try {
let appWin = iframe.contentWindow;
// 히스토리 삭제
appWin.localStorage.removeItem('pdfjs.history');
Object.keys(appWin.localStorage).forEach(k => {
if (k.startsWith('pdfjs.history') || k.startsWith('pdfjs.preferences')) {
appWin.localStorage.removeItem(k);
}
});
let app = appWin.PDFViewerApplication;
let isRendering = false;
let lastScale = 1;
app.initializedPromise.then(() => {
const PDFPageView = appWin.PDFPageView;
// draw 함수를 완전히 오버라이드
const originalDraw = PDFPageView.prototype.draw;
PDFPageView.prototype.draw = function() {
// 고해상도 설정
this.outputScale = {
sx: (window.devicePixelRatio || 1) * 2,
sy: (window.devicePixelRatio || 1) * 2
};
return originalDraw.call(this);
};
// reset 함수도 오버라이드하여 캔버스 완전 삭제
const originalReset = PDFPageView.prototype.reset;
PDFPageView.prototype.reset = function() {
// 캔버스 완전히 제거
if (this.canvas) {
if (this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
}
this.canvas.width = 0;
this.canvas.height = 0;
this.canvas = null;
}
// 렌더 태스크 취소
if (this.renderTask) {
this.renderTask.cancel();
this.renderTask = null;
}
return originalReset.call(this);
};
if (app.pdfViewer) {
app.pdfViewer.textLayerMode = 1;
app.pdfViewer.useOnlyCssZoom = false;
}
// 확대 시 완전 재렌더링
app.eventBus.on('scalechanging', function(evt) {
if (isRendering) return;
const newScale = evt.scale;
const scaleDiff = Math.abs(newScale - lastScale);
// 스케일 변화가 있을 때만 재렌더링
if (scaleDiff > 0.01) {
isRendering = true;
setTimeout(() => {
if (app.pdfViewer && app.pdfViewer._pages) {
app.pdfViewer._pages.forEach(pageView => {
// 1. 렌더 태스크 강제 취소
if (pageView.renderTask) {
pageView.renderTask.cancel();
pageView.renderTask = null;
}
// 2. 캔버스 완전 제거
if (pageView.canvas) {
const parent = pageView.canvas.parentNode;
if (parent) {
parent.removeChild(pageView.canvas);
}
pageView.canvas.width = 0;
pageView.canvas.height = 0;
pageView.canvas = null;
}
// 3. viewport 업데이트
if (pageView.pdfPage) {
pageView.viewport = pageView.pdfPage.getViewport({
scale: newScale,
rotation: pageView.rotation || 0
});
}
// 4. 페이지 완전 리셋
pageView.reset();
// 5. 새로 그리기
pageView.draw();
});
}
lastScale = newScale;
isRendering = false;
}, 200); // 딜레이 증가
}
});
// 스크롤/페이지 변경 시에도 재렌더링
app.eventBus.on('updateviewarea', function(evt) {
if (isRendering) return;
// 현재 보이는 페이지만 재렌더링
const visiblePages = app.pdfViewer._getVisiblePages();
if (visiblePages && visiblePages.views) {
visiblePages.views.forEach(view => {
if (view && view.div && view.div.classList.contains('pdfPage')) {
const pageView = app.pdfViewer._pages[view.id - 1];
if (pageView && !pageView.renderTask) {
pageView.draw();
}
}
});
}
});
});
app.pdfCursorTools._handTool.activate();
app.open(pdf_options);
} catch (e) {
console.error('PDF 설정 오류:', e);
}
iframe.width = '100%';
iframe.height = '100%';
});
}
function _openVideo(path){
const popupVideo = document.createElement('video');
popupVideo.autoplay = true;
popupVideo.muted = true;
popupVideo.playsInline = true;
popupVideo.controls = true;
popupVideo.crossOrigin = 'anonymous';
const sourceElement = document.createElement('source');
sourceElement.src = path;
popupVideo.style.width = '100%';
popupVideo.style.height = '100%';
popupVideo.style.objectFit = 'contain';
document.getElementById('popup_viewer').appendChild(popupVideo);
popupVideo.appendChild(sourceElement);
}
function _openPano(path){
let panorama = pannellum.viewer('popup_viewer',{
"type": "equirectangular",
"panorama": path,
"autoLoad": true,
"compass":false,
})
}
function _openImage(path){
const img = document.createElement('img');
img.src = path;
document.getElementById('popup_viewer').appendChild(img);
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'contain';
img.addEventListener('click',()=>{
document.getElementById('large-img-container').style.display = 'block';
centerImage();
})
const container = document.getElementById('large-img-container');
const image = document.getElementById('large-image');
const zoomInfo = document.getElementById('large-img-zoomInfo');
image.src = path;
let isDragging = false;
let startX, startY;
let translateX = 0;
let translateY = 0;
let scale = 1;
// 드래그 시작
function dragStart(e) {
if (e.target !== image) return;
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
// 드래그 중일 때 transition 제거
image.style.transition = 'none';
}
// 드래그 중
function drag(e) {
if (!isDragging) return;
e.preventDefault();
// 현재 마우스 위치에서 시작 위치를 빼서 이동 거리 계산
translateX = e.clientX - startX;
translateY = e.clientY - startY;
updateImageTransform();
}
// 드래그 종료
function dragEnd() {
if (!isDragging) return;
isDragging = false;
// 드래그 종료 시 부드러운 transition 효과 복원
image.style.transition = 'transform 0.1s ease-out';
}
// 확대/축소
function zoom(e) {
e.preventDefault();
// 현재 이미지의 실제 위치와 크기
const imageRect = image.getBoundingClientRect();
// 마우스 포인터의 화면상 좌표
const mouseX = e.clientX;
const mouseY = e.clientY;
// 마우스 포인터의 이미지상 상대 좌표
const mouseImageX = mouseX - imageRect.left;
const mouseImageY = mouseY - imageRect.top;
// 이전 스케일 저장
const prevScale = scale;
// 새로운 스케일 계산
const delta = Math.max(-1, Math.min(1, e.wheelDelta || -e.detail));
const scaleFactor = 0.1;
if (delta > 0) {
scale = Math.min(4, scale + scaleFactor);
} else {
scale = Math.max(0.1, scale - scaleFactor);
}
// 스케일 변화율
const scaleRatio = scale / prevScale;
// 새로운 translate 값 계산
// 마우스 포인터 위치는 고정되어야 하므로, 스케일 변화에 따른 위치 조정
translateX = mouseX - (((mouseX - translateX) * scaleRatio));
translateY = mouseY - (((mouseY - translateY) * scaleRatio));
updateImageTransform();
updateZoomInfo();
}
// 이미지 transform 업데이트
function updateImageTransform() {
// transform의 원점을 이미지의 중심으로 설정
const originX = image.offsetWidth / 2;
const originY = image.offsetHeight / 2;
image.style.transformOrigin = `${originX}px ${originY}px`;
image.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}
// 줌 정보 업데이트
function updateZoomInfo() {
zoomInfo.textContent = `${Math.round(scale * 100)}%`;
}
// 이미지 초기 위치 설정
function centerImage() {
translateX = (container.offsetWidth - image.offsetWidth) / 2;
translateY = (container.offsetHeight - image.offsetHeight) / 2;
updateImageTransform();
}
// 이벤트 리스너 등록
container.addEventListener('mousedown', dragStart);
window.addEventListener('mousemove', drag);
window.addEventListener('mouseup', dragEnd);
container.addEventListener('wheel', zoom);
container.addEventListener('contextmenu',(e)=>{
e.preventDefault();
container.style.display = 'none';
})
// 더블클릭으로 원래 크기로 복원
container.addEventListener('dblclick', () => {
scale = 1;
updateZoomInfo();
});
}
function _openIfc(path, thumbnail_key, resourcePath, dataId, path_name){
const iframe = document.createElement('iframe');
iframe.onload = () => {
iframe.contentWindow.postMessage({ path, thumbnail_key, resourcePath, dataId, path_name }, '*'); // path 값을 iframe에 전달
};
iframe.src = `/libs/ifcViewer/index.html`;
iframe.width = '100%';
iframe.height = '100%';
document.getElementById('popup_viewer').appendChild(iframe);
}
function _open3d(path, thumbnail_key, resourcePath, dataId, path_name){
const iframe = document.createElement('iframe');
iframe.onload = () => {
iframe.contentWindow.postMessage({ path, thumbnail_key, resourcePath, dataId, path_name }, '*'); // path 값을 iframe에 전달
};
iframe.src = `/libs/3dViewer/index.html`;
iframe.width = '100%';
iframe.height = '100%';
document.getElementById('popup_viewer').appendChild(iframe);
}
function _drawMeta(data){
let container = document.getElementById('meta-data');
container.style.display = 'block';
Object.keys(data).forEach(item=>{
if(!item.startsWith('$') && item != 'type' && item != 'password'){
let line = `<span class="key">${item}</span> : <span>${data[item]}</span><br>`;
container.innerHTML += line;
}
})
}