초기 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

View File

@@ -0,0 +1,11 @@
{
"title": "DocumentSummary",
"type": "object",
"properties": {
"파일제목": { "type": "string" },
"파일내용요약": { "type": "string" }
},
"required": [
"파일제목", "파일내용요약"
]
}

View File

@@ -0,0 +1,7 @@
오타나 줄바꿈 오류가 있을 수 있으니 의미를 유추하여 정확한 정보를 추출해주세요.
정확성이 매우 중요하므로 반드시 파일에 포함된 텍스트만 사용하여 작성해주세요. 추론하거나 임의로 보충하지 마세요.
불필요한 설명 문구(예: "다음은 문서에 작성된 내용입니다", "문서 기반으로 요약해 드립니다" 등)와 특수문자(*등)는 포함하지 마세요.
한 문장이 끝나면 다음줄에서 문장을 시작해주세요. 가독성 좋게 표출해주세요.
모든 문서는 꼭 개조식으로 정리해서 표출해주세요.
1. 파일 내용 요약: 파일 내용을 요약해주세요. 반드시 한글로 작성합니다.

View File

@@ -0,0 +1,5 @@
project_name 값을 실제 프로젝트명으로 사용하고, "project_name"이라는 단어를 그대로 쓰지 마.
project_name 값이 null 이면 'err: project name is null' 이라는 단어가 출력되게 해줘
project_name에 대한 온라인 반응을 최신 보도와 공식 문서 중심으로 정리해줘. 추측은 금지야 .
아래 예제 형식처럼 글만 나오게 해줘. '결과입니다' 같은 불필요한 말은 하지마.
글 정리는 개조식으로 진행해줘.

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

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;
});
}

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);

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;
}
})
})
})

View File

@@ -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);
}

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

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

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

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];
}
}

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
views/main/jsm/main.js Normal file
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

View File

@@ -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);
}
});

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;

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();

View File

@@ -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. 첨부문서 수: 숫자만

View File

@@ -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. "첨부문서수": 찾은 첨부문서 개수를 알려주세요.
}

View File

@@ -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": [
"공문번호", "공문일자", "수신처", "수신자", "수신자약자",
"발신처", "발신자", "발신자약자", "공문제목", "공문제목요약",
"공문내용요약", "공문간연계", "공문종류", "공문유형", "첨부문서제목", "첨부문서수"
]
}

View File

@@ -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. "첨부문서수": 숫자만

View File

@@ -0,0 +1,27 @@
주의:
- **정확성이 매우 중요합니다.**
- **19.1 SDZR/AERA/RCM** 형태는 공문 번호가 아닙니다.
- 한글로 쓰라고 한 항목은 반드시 한글로 작성
- 반드시 아래 13개 항목명만 키로 사용:
"공문번호", "공문일자", "수신자", "수신자약자", "발신자", "발신자약자", "공문제목", "공문제목요약", "공문내용요약", "공문간연계", "공문종류", "공문유형", "첨부문서수"
- 키 값(항목명)에 숫자나 마크 추가 금지. 반드시 순수 텍스트(한글/영문)만 사용.
- 모르면 "확인필요", 없으면 "없음"이라고 작성
- 결과값을 **일반적인 JSON 형식**으로 그대로 출력해주세요
- 절대 ```json 등 코드블록으로 감싸지 마세요.
- 잘못된 형식이 나오면 위 조건에 맞는 JSON으로 다시 출력하세요.
{
"공문번호": "영문-숫자" 형태
"공문일자": YYYY-MM-DD
"수신자": 수신자의 직책
"수신자약자": 수신자 직책의 약자
"발신자": 발신자의 직책 (예: Project Director), 조직명은 제외
"발신자약자": 발신자 직책의 약자
"공문제목": Subject **독립된 첫 구문만**, 프로젝트 이름/설명 등 부연 문장은 제외
"공문제목요약": **한글로** 10~20자
"공문내용요약": **한글로** 간단하게
"공문간연계": 연계된 공문이 있으면 "있음", 없으면 "없음"
"공문종류": '행정/일반', '기술/성과물', ‘회의/기타’ 중 하나
"공문유형": 보고, 요청, 지시, 회신, 계약 중 하나
"첨부문서수": 숫자만
}

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

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 사용 함수 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼

View File

@@ -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 저장, 삭제 끝 🔼🔼🔼🔼🔼🔼🔼🔼🔼🔼

View File

@@ -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

View File

@@ -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
views/main/jsm/popup.js Normal file
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;
}
})
}