1061 lines
40 KiB
JavaScript
1061 lines
40 KiB
JavaScript
import { vars } from '../archive/variable.js';
|
|
import { docVars } from './docVariable.js';
|
|
import { checkProjectInactive } from '../main.js';
|
|
import { openDocAiChoiceModal, openDocUpsertModal } from './docModalManager.js';
|
|
import { drawDocList, itemBoxSelected, renderDocViewer, toggleDocContextmenu } from './docPageRenderer.js';
|
|
import { splitBaseAndExt, hasSpecialChar } from '../archive/common.js';
|
|
import { resetViewer } from '../archive/pageRenderer.js';
|
|
|
|
let docMain = document.querySelector('.official-doc-main'); // 화면
|
|
let notDndArea = document.querySelector('.not-dnd-area');
|
|
let docMainList = document.querySelector('.official-doc-main .official-doc-list-container');
|
|
let docDndArea = document.querySelector('.official-doc-main .official-doc-list-container .official-doc-main-body .doc-dnd-area');
|
|
let docDndImage = docDndArea?.querySelector('.doc-img');
|
|
let docDndText = docDndArea?.querySelector('.doc-text');
|
|
let isDragging = false;
|
|
|
|
let isAiUsed = true; // 기본값은 true로 설정
|
|
// AI 사용 여부를 설정하는 함수
|
|
export function setAiUsed(value) {
|
|
isAiUsed = value;
|
|
}
|
|
// AI 사용 여부를 가져오는 함수
|
|
export function getAiUsed() {
|
|
return isAiUsed;
|
|
}
|
|
|
|
// dragover: 드래그 된 요소가 droppable 요소 위에 있는 동안 지속적으로 발생
|
|
docMain?.addEventListener('dragover', async (e) => {
|
|
const types = e.dataTransfer.types;
|
|
toggleDocContextmenu('.official-doc-contextmenu', false);
|
|
// 파일이 아닌 항목에 대해서는 dnd 안되게 // 글자가 dnd 되는 문제 때문에
|
|
if (types && types.includes('Files')) {
|
|
e.preventDefault();
|
|
|
|
// 페이지영역에 드래그 들어오면 notDndArea, docDndArea 표시
|
|
// notDndArea.style.display = 'flex';
|
|
docDndArea.style.display = 'flex';
|
|
// 드래그 상태 플래그 true
|
|
isDragging = true;
|
|
|
|
// dnd 영역 크기
|
|
let docMainListWidth = docMainList.offsetWidth;
|
|
let docMainListHeight = docMainList.offsetHeight;
|
|
let docMainListTop = docMainList.offsetTop;
|
|
docDndArea.style.width = `${docMainListWidth}px`;
|
|
docDndArea.style.minHeight = `${docMainListHeight}px`;
|
|
docDndArea.style.top = `${docMainListTop}px`;
|
|
|
|
if (e.target.matches('.doc-dnd-area')) {
|
|
// dndArea 안으로 이동
|
|
docDndArea.style.background = '#b5c6c3dd';
|
|
docDndArea.style.border = '2px solid #1e5149';
|
|
// docDndImage.style.backgroundImage = "url('/main/img/archive/upload_in_dnd.svg')";
|
|
docDndText.textContent = '파일을 여기에 드롭하세요.';
|
|
docDndText.style.color = '#1e5149';
|
|
} else {
|
|
// dndArea 밖으로 이동
|
|
docDndArea.style.background = '#e9eeeddd';
|
|
docDndArea.style.border = '2px solid #1e5149';
|
|
// docDndImage.style.backgroundImage = "url('/main/img/archive/upload_out_dnd.svg')";
|
|
docDndText.textContent = '파일을 여기에 드래그하세요.';
|
|
docDndText.style.color = '#a5b9b6';
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
});
|
|
// dragleave: 드래그된 요소가 droppable 요소에서 벗어날 때 발생
|
|
docMain?.addEventListener('dragleave', async (e) => {
|
|
e.preventDefault();
|
|
if (e.target.matches('.doc-dnd-area')) {
|
|
// notDndArea에서 페이지영역 밖으로 나갈 때 notDndArea, dndArea 숨김
|
|
// notDndArea.style.display = 'none';
|
|
docDndArea.style.display = 'none';
|
|
// 드래그 상태 플래그 false
|
|
isDragging = false;
|
|
}
|
|
});
|
|
// 드래그 상태에서 웹페이지 밖으로 나갈 때 notDndArea, dndArea 남아있는 경우 초기화
|
|
docMain?.addEventListener('mousemove', () => {
|
|
if (isDragging) {
|
|
// notDndArea.style.display = 'none';
|
|
docDndArea.style.display = 'none';
|
|
isDragging = false;
|
|
}
|
|
});
|
|
// drop: 드래그된 요소가 droppable 요소에 놓일 때 발생
|
|
docDndArea?.addEventListener('drop', async (e) => {
|
|
e.preventDefault();
|
|
const droppedFiles = Array.from(e.dataTransfer.files);
|
|
|
|
if (!droppedFiles.length) return;
|
|
if (droppedFiles.length > 1) {
|
|
alert('파일은 하나씩 추가해주세요.');
|
|
return;
|
|
}
|
|
|
|
let resourcePathArr = [];
|
|
// 경로 존재 확인을 위해 resourcePathArr 저장
|
|
for (let i = 0; i < droppedFiles.length; i++) {
|
|
const file = droppedFiles[i];
|
|
let resourcePath = `/${file.name}`;
|
|
resourcePathArr.push(resourcePath);
|
|
}
|
|
|
|
// resourcePathArr을 사용해서 경로 존재 확인
|
|
let checkTargetExistsResult = await checkDocTargetExists('official', resourcePathArr);
|
|
|
|
// 이미 존재하는 경로만 existingPathArr에 저장
|
|
let existingPathArr = checkTargetExistsResult.existingPathArr;
|
|
|
|
if (existingPathArr.length != 0) {
|
|
// 중복파일 있음
|
|
for (let i = 0; i < droppedFiles.length; i++) {
|
|
const file = droppedFiles[i];
|
|
let resourcePath = `/${file.name}`;
|
|
let docTitle = docVars.allDocData?.find((doc) => doc.file_path === resourcePath)?.doc_title;
|
|
let docDirection = docVars.allDocData?.find((doc) => doc.file_path === resourcePath)?.doc_direction;
|
|
let recipientOrg = docVars.allDocData?.find((doc) => doc.file_path === resourcePath)?.recipient_org;
|
|
let senderOrg = docVars.allDocData?.find((doc) => doc.file_path === resourcePath)?.sender_org;
|
|
|
|
let objectKey;
|
|
let isExists = false;
|
|
if (existingPathArr.includes(resourcePath)) {
|
|
let isUpload = confirm(`'${file.name}' 파일이 이미 존재합니다.\n기존 파일을 새 파일로 교체하시겠습니까?\n[파일 제목] ${docTitle}\n[수신처] ${recipientOrg}\n[발신처] ${senderOrg}\n[위치] '${docDirection}' 공문 리스트`);
|
|
if (isUpload) objectKey = checkTargetExistsResult.existingObjectKey.keyMap[resourcePath];
|
|
if (!isUpload) continue;
|
|
}
|
|
if (objectKey) {
|
|
isExists = true;
|
|
const docId = checkTargetExistsResult.existingDocId.keyMap[resourcePath];
|
|
await axios.post(`${docVars.path_name}/deleteDocData`, { docId, objectKey, isExists });
|
|
}
|
|
}
|
|
|
|
const uploadResult = await uploadDocFile(droppedFiles, 'exist');
|
|
const newObjectKey = uploadResult.uploadParams.objectKeyArr[0];
|
|
|
|
const params = {};
|
|
for (let i = 0; i < droppedFiles.length; i++) {
|
|
const file = droppedFiles[i];
|
|
let resourcePath = `/${file.name}`;
|
|
const docId = checkTargetExistsResult.existingDocId.keyMap[resourcePath];
|
|
params[`doc_id`] = docId;
|
|
params[`object_key`] = newObjectKey;
|
|
params[`preview_key`] = newObjectKey;
|
|
params[`popup_key`] = newObjectKey;
|
|
}
|
|
|
|
await upsertDocData(params, 'exist');
|
|
} else {
|
|
// 중복파일 없음
|
|
const droppedFileName = droppedFiles[0].name;
|
|
const droppedFileExt = droppedFileName.split('.').pop().toLowerCase();
|
|
|
|
if (droppedFileExt != 'pdf') {
|
|
alert('PDF 파일만 지원합니다.');
|
|
return;
|
|
}
|
|
|
|
// pdf 10장 넘으면 리턴
|
|
let pageCount = await checkPdfPageCount(droppedFiles[0]);
|
|
if (pageCount >= 10) {
|
|
if (confirm('11장 이상의 PDF는 AI 요약을 지원하지 않습니다.\n사용자 입력모달로 이동하시겠습니까?')) {
|
|
setAiUsed(false);
|
|
openDocUpsertModal({}, droppedFiles, droppedFileName, 'input');
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
// 공문 파일
|
|
for (const file of droppedFiles) {
|
|
formData.append('input_file', file);
|
|
}
|
|
// 프롬프트 파일
|
|
try {
|
|
const res = await fetch('/main/jsm/officialDoc/prompt/해외공문 프롬프트250901(한글)_json.txt');
|
|
const blob = await res.blob();
|
|
const promptFile = new File([blob], '해외공문 프롬프트250901(한글)_json.txt', { type: 'text/plain' });
|
|
formData.append('prompt_file', promptFile);
|
|
} catch (error) {
|
|
console.error('프롬프트 파일 로드 실패:', error);
|
|
return;
|
|
}
|
|
// 스키마 파일
|
|
try {
|
|
const res = await fetch('/main/jsm/officialDoc/prompt/default_schema.json');
|
|
const blob = await res.blob();
|
|
const schemaFile = new File([blob], 'default_schema.json', { type: 'application/json' });
|
|
formData.append('schema_file', schemaFile);
|
|
} catch (error) {
|
|
console.error('스키마 파일 로드 실패:', error);
|
|
return;
|
|
}
|
|
|
|
// AI 선택 모달 열기 > 선택 완료되면 requestToAiWithChoice 호출
|
|
openDocAiChoiceModal((selectedAiType) => {
|
|
requestToAiWithChoice(formData, droppedFiles, selectedAiType);
|
|
});
|
|
}
|
|
|
|
// notDndArea에서 페이지영역 밖으로 나갈 때 notDndArea, dndArea 숨김
|
|
notDndArea.style.display = 'none';
|
|
docDndArea.style.display = 'none';
|
|
// 드래그 상태 플래그 false
|
|
isDragging = false;
|
|
});
|
|
|
|
async function checkPdfPageCount(file) {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
const loadingTask = window.pdfjsLib.getDocument({ data: new Uint8Array(arrayBuffer) });
|
|
const pdf = await loadingTask.promise;
|
|
return pdf.numPages;
|
|
}
|
|
|
|
async function requestToAiWithChoice(formData, droppedFiles, aiType) {
|
|
const files = formData.get('input_file');
|
|
const fileName = files ? files.name : '파일 없음';
|
|
|
|
try {
|
|
showFullScreenLoading(); // 로딩 화면 보이기
|
|
|
|
// AI 사용 안함
|
|
if (aiType == 'aiNotUse') {
|
|
hideFullScreenLoading();
|
|
setAiUsed(false);
|
|
openDocUpsertModal({}, droppedFiles, fileName, 'input');
|
|
return;
|
|
}
|
|
|
|
const apiKey = await getExtractKey(); // AICell에서 발급해준 apiKey
|
|
|
|
const url = {
|
|
outerAi: 'https://gateway.hmac.kr/general/outer/e2e',
|
|
innerAi: 'https://gateway.hmac.kr/extract/inner/structured',
|
|
};
|
|
|
|
const res = await fetch(url[aiType], {
|
|
method: 'POST',
|
|
headers: {
|
|
'x-API-key': apiKey,
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
const resData = await res.json();
|
|
|
|
// AICell API(outerAi, innerAi)의 경우, 응답받은 request_id를 통해 데이터 풀링
|
|
const requestId = resData.request_id;
|
|
if (!requestId) throw new Error('request_id 없음');
|
|
|
|
// api에 polling 요청
|
|
timeoutPolling(
|
|
() => checkAiProgress(requestId, droppedFiles, fileName, aiType),
|
|
5000, // 5초 간격
|
|
60, // 최대 60번 > 5분
|
|
droppedFiles,
|
|
fileName
|
|
);
|
|
} catch (err) {
|
|
handleServerError(droppedFiles, fileName); // 서버 에러 대비 사용자입력모달
|
|
}
|
|
}
|
|
|
|
async function checkAiProgress(requestId, droppedFiles, fileName, aiType) {
|
|
try {
|
|
const apiKey = await getExtractKey();
|
|
// aiType으로 데이터 풀링할 url (outerAi = general, innerAi = extract)
|
|
const url = aiType === 'outerAi' ? `https://gateway.hmac.kr/general/progress/${requestId}` : `https://gateway.hmac.kr/extract/progress/${requestId}`;
|
|
const res = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'x-API-key': apiKey,
|
|
},
|
|
});
|
|
// console.log(`요청: /extract/progress/${requestId} → status: ${res.status}`);
|
|
|
|
if (!res.ok) {
|
|
// 404 등 실패 시, polling 종료
|
|
handleServerError(droppedFiles, fileName); // 서버 에러 대비 사용자입력모달
|
|
throw new Error('__STOP_POLLING__'); // polling 종료를 위한 에러 던지기
|
|
}
|
|
|
|
const data = await res.json();
|
|
// console.log('요약 진행 상태 응답: ', data);
|
|
|
|
// final_result가 최종 ai공문요약데이터
|
|
// 결과가 완료되지 않아도 (data.status 200 || data.final_result) 조건을 충족하기 때문에
|
|
// 결과 생성 전에는 다음 polling으로 넘김
|
|
if (data.final_result == '작업이 진행 중입니다. 결과는 아직 생성되지 않았습니다.') return;
|
|
|
|
// 결과에 에러 떴을 때
|
|
if (data.final_result?.error) {
|
|
handleServerError(droppedFiles, fileName); // 서버 에러 대비 사용자입력모달
|
|
throw new Error('__STOP_POLLING__'); // polling 종료를 위한 에러 던지기
|
|
}
|
|
|
|
// 성공 시 polling 중단하고 콜백 실행
|
|
if (data.status === 200 || data.final_result) {
|
|
// console.log('최최종: ', data.final_result[0]);
|
|
setAiUsed(true);
|
|
let generatedData = data.final_result.generated;
|
|
if (aiType === 'outerAi') generatedData = JSON.parse(data.final_result.generated);
|
|
if (aiType === 'innerAi') generatedData = data.final_result[0].generated;
|
|
openDocUpsertModal(generatedData, droppedFiles, fileName);
|
|
hideFullScreenLoading(); // 로딩 화면 숨기기
|
|
throw new Error('__STOP_POLLING__'); // 재귀 중단용 에러
|
|
}
|
|
} catch (error) {
|
|
if (error.message === '__STOP_POLLING__') {
|
|
throw error; // 재귀 멈추기 위한 예외는 다시 던짐
|
|
} else {
|
|
console.error('polling 중 에러: ', error);
|
|
handleServerError(droppedFiles, fileName); // 서버 에러 대비 사용자입력모달
|
|
throw new Error('__STOP_POLLING__'); // 재귀 중단용 에러
|
|
}
|
|
}
|
|
}
|
|
|
|
export const timeoutPolling = (func, timeout, maxAttempts = -1, droppedFiles = [], fileName = '') => {
|
|
if (maxAttempts === 0) {
|
|
handleServerError(droppedFiles, fileName); // 서버 에러 대비 사용자입력모달
|
|
return;
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
try {
|
|
await func();
|
|
} catch (error) {
|
|
if (error.message === '__STOP_POLLING__') return; // 성공적 중단
|
|
console.error(error);
|
|
}
|
|
|
|
timeoutPolling(func, timeout, maxAttempts - 1, droppedFiles, fileName);
|
|
}, timeout);
|
|
};
|
|
|
|
// 서버 에러 대비 사용자입력모달
|
|
const handleServerError = (droppedFiles, fileName) => {
|
|
hideFullScreenLoading();
|
|
if (confirm('서버와의 연결이 불안정합니다.\n사용자 입력모달로 이동하시겠습니까?')) {
|
|
setAiUsed(false);
|
|
openDocUpsertModal({}, droppedFiles, fileName, 'input');
|
|
}
|
|
};
|
|
|
|
async function getExtractKey() {
|
|
try {
|
|
const res = await axios.post(`${docVars.path_name}/getExtractKey`);
|
|
|
|
if (res.data.message === 'getExtractKey_success') {
|
|
const apiKey = res.data.apiKey;
|
|
return apiKey;
|
|
} else {
|
|
throw new Error('getExtractKey 실패');
|
|
}
|
|
} catch (error) {
|
|
console.error('getExtractKey error: ', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ai 응답 기다리면서 뜨는 로딩 화면
|
|
function showFullScreenLoading() {
|
|
document.querySelector('.doc-ai-loading').style.display = 'flex';
|
|
}
|
|
function hideFullScreenLoading() {
|
|
document.querySelector('.doc-ai-loading').style.display = 'none';
|
|
}
|
|
|
|
export async function uploadDocFile(files, mode) {
|
|
let dateArr = [],
|
|
resourcePathArr = [],
|
|
sizeArr = [],
|
|
objectKeyArr = [],
|
|
uploadSuccessFileArr = [],
|
|
uploadFailedFileArr = [];
|
|
|
|
let resourcePath;
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
if (mode === 'modal_add') {
|
|
resourcePath = `/${file.name}`;
|
|
} else if (mode === 'add_attach') {
|
|
const parentPathLi = docVars.lastClickedListTarget.closest('li');
|
|
const parentPath = parentPathLi.dataset.resourcePath;
|
|
resourcePath = `${parentPath}__attachment/${file.name}`;
|
|
} else if (mode === 'exist') {
|
|
resourcePath = `/${file.name}`;
|
|
}
|
|
|
|
try {
|
|
// 1) 서버에 presigned URL 요청
|
|
let axiosParams = {
|
|
resourcePath: resourcePath,
|
|
date: Date.now(),
|
|
};
|
|
let generateUploadUrlRes = await axios.post(`${docVars.path_name}/generateUploadDocUrl`, axiosParams);
|
|
if (generateUploadUrlRes.data.message == 'generateUploadDocUrl_success') {
|
|
let { url, objectKey, date } = generateUploadUrlRes.data.result;
|
|
|
|
// 2) presigned URL로 직접 파일 PUT 전송
|
|
await axios.put(url, file, {
|
|
headers: {
|
|
'Content-Type': file.type || 'application/octet-stream',
|
|
},
|
|
});
|
|
|
|
dateArr.push(date);
|
|
resourcePathArr.push(resourcePath);
|
|
sizeArr.push(file.size);
|
|
objectKeyArr.push(objectKey);
|
|
uploadSuccessFileArr.push(file.name);
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ 업로드 실패: ${resourcePath}`, error);
|
|
uploadFailedFileArr.push(file.name);
|
|
}
|
|
}
|
|
|
|
// 업로드 정보 리턴
|
|
return {
|
|
success: resourcePathArr.length > 0,
|
|
uploadParams: {
|
|
storageType: docVars.storageType,
|
|
dateArr: dateArr,
|
|
resourcePathArr: resourcePathArr,
|
|
sizeArr: sizeArr,
|
|
objectKeyArr: objectKeyArr,
|
|
},
|
|
fileArrReset: true,
|
|
uploadFailedFileArr,
|
|
uploadSuccessFileArr,
|
|
};
|
|
}
|
|
|
|
// DB 저장
|
|
export async function upsertDocData(formData, mode) {
|
|
try {
|
|
if (mode === 'modal_edit') {
|
|
formData.userInfoString = vars.userInfoString;
|
|
if (!formData.doc_id && docVars.currentDocId) {
|
|
formData.doc_id = docVars.currentDocId;
|
|
}
|
|
} else if (mode === 'exist') {
|
|
formData.userInfoString = vars.userInfoString;
|
|
}
|
|
|
|
const res = await axios.post(`${docVars.path_name}/uploadDocData`, { params: formData, mode });
|
|
|
|
if (res.status != 200) {
|
|
console.warn(`⚠️ ${mode} 실패`, res.data);
|
|
}
|
|
|
|
await syncDocInfo(['official', 'attach', null]);
|
|
await getDocDataBySelected();
|
|
} catch (error) {
|
|
console.error(`❌ ${mode} 요청 중 오류:`, error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function syncDocInfo(labels = null) {
|
|
try {
|
|
const labelArray = Array.isArray(labels) ? labels : [labels];
|
|
|
|
const resPromises = labelArray.map((label) => {
|
|
return axios.get(`${docVars.path_name}/getDocData`, {
|
|
params: label ? { label } : {},
|
|
});
|
|
});
|
|
const resData = await Promise.all(resPromises);
|
|
|
|
const result = {};
|
|
resData.forEach((res, index) => {
|
|
if (res.data.message === 'getDocData_success') {
|
|
const label = labelArray[index];
|
|
if (label === 'attach') {
|
|
result.allDocAttachData = res.data.data;
|
|
} else {
|
|
result.allDocData = res.data.data;
|
|
}
|
|
}
|
|
});
|
|
|
|
// 전역 변수에 저장
|
|
if (result.allDocOfficialData) docVars.allDocOfficialData = result.allDocOfficialData;
|
|
if (result.allDocAttachData) docVars.allDocAttachData = result.allDocAttachData;
|
|
if (result.allDocData) docVars.allDocData = result.allDocData;
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('syncDocInfo err ', error);
|
|
}
|
|
}
|
|
|
|
export async function getDocDataBySelected(direction) {
|
|
// console.log('선택된 파라미터:', JSON.stringify(docVars.selectParams, null, 2));
|
|
|
|
const docCategory = document.querySelector('.official-doc-category.official-doc-tab-on h6')?.innerText.trim();
|
|
const categoryValue = docCategory === '전체보기' ? '' : docCategory;
|
|
|
|
try {
|
|
const params = {
|
|
projectId: docVars.project_id,
|
|
companyType: docVars.selectParams.typeOptions,
|
|
base: docVars.selectParams.baseOptions,
|
|
target: docVars.selectParams.targetOptions,
|
|
category: categoryValue,
|
|
storageType: docVars.storageType,
|
|
direction: direction,
|
|
};
|
|
|
|
const res = await axios.get(`${docVars.path_name}/getDocDataBySelected`, {
|
|
params: params,
|
|
});
|
|
|
|
if (res.data.success) {
|
|
if (direction == '' || direction == undefined || direction == null) {
|
|
docVars.allDocOfficialData = res.data.listData;
|
|
await drawDocList(docVars.allDocOfficialData);
|
|
getDocCount(res.data.countData);
|
|
}
|
|
if (direction == '수신') {
|
|
docVars.allRecipientListByDirection = res.data.listData
|
|
await drawDocList(docVars.allRecipientListByDirection, direction);
|
|
}
|
|
if (direction == '발신') {
|
|
docVars.allSendingListByDirection = res.data.listData
|
|
await drawDocList(docVars.allSendingListByDirection, direction);
|
|
}
|
|
} else {
|
|
console.error('getDocDataBySelected error');
|
|
}
|
|
} catch (error) {
|
|
console.error('getDocDataBySelected error', error);
|
|
}
|
|
}
|
|
|
|
export function getDocCount(data) {
|
|
const categoryButtons = document.querySelectorAll('.official-doc-category');
|
|
|
|
// 화면 카테고리에 있는 분류들 이름 모아놓기
|
|
const categoryNamesOnPage = [];
|
|
for (let i = 0; i < categoryButtons.length; i++) {
|
|
const categoryText = categoryButtons[i].querySelector('.category-text');
|
|
categoryNamesOnPage.push(categoryText.innerText);
|
|
}
|
|
|
|
// 전체 카운트 계산용
|
|
let totalCnt = 0;
|
|
for (let i = 0; i < data.length; i++) {
|
|
totalCnt += parseInt(data[i].count);
|
|
}
|
|
|
|
for (let i = 0; i < categoryButtons.length; i++) {
|
|
const categoryText = categoryButtons[i].querySelector('.category-text');
|
|
const categoryName = categoryText.innerText;
|
|
const categoryCount = categoryButtons[i].querySelector('.category-count');
|
|
|
|
if (categoryName === '전체보기') {
|
|
categoryCount.innerText = `(${totalCnt})`;
|
|
} else {
|
|
let count = 0;
|
|
for (let j = 0; j < data.length; j++) {
|
|
if (data[j].doc_category === categoryName) {
|
|
count = data[j].count;
|
|
break;
|
|
}
|
|
}
|
|
categoryCount.innerText = `(${count})`;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function convertDocPdf(resourcePath, docId) {
|
|
const selectedDoc = docVars.allDocData.find((doc) => doc.doc_id === docId);
|
|
let objectKey = selectedDoc.object_key;
|
|
|
|
let convertPdfParams = {
|
|
userInfoString: vars.userInfoString,
|
|
storageType: vars.storageType,
|
|
objectKey: objectKey,
|
|
resourcePath: resourcePath,
|
|
docId: docId,
|
|
};
|
|
let convertPdfRes = await axios.post(`${docVars.path_name}/convertDocPdf`, { params: convertPdfParams });
|
|
// console.log(convertPdfRes);
|
|
}
|
|
|
|
// 셀렉트박스 회사리스트 DB에서 가져오기
|
|
export async function getGroupCompanyData() {
|
|
try {
|
|
const res = await axios.get(`${docVars.path_name}/getGroupCompanyData`, docVars.project_id);
|
|
if (res.data.success) {
|
|
docVars.groupCompanyData = res.data.data;
|
|
} else {
|
|
console.error('getGroupCompanyData err');
|
|
}
|
|
} catch (error) {
|
|
console.error('getGroupCompanyData err: ', error);
|
|
}
|
|
}
|
|
// console.log(docVars.groupCompanyData);
|
|
// docVars.groupCompanyData res 예시
|
|
// 발주처 :
|
|
// 기준 : ['SY_JV']
|
|
// 상대기관 : ['DPWH']
|
|
// 발주처외 :
|
|
// 기준 : ['삼안']
|
|
// 상대기관 : (4) ['서영', '진우', '경동', '한국수출입은행']
|
|
|
|
|
|
/* 컨텍스트메뉴 */
|
|
// 수정 // 수정모달 오픈
|
|
document.querySelector('#renameTargetDoc')?.addEventListener('click', () => {
|
|
const selectedDoc = docVars.allDocOfficialData.find((doc) => doc.doc_id === docVars.currentDocId);
|
|
|
|
if (!selectedDoc) {
|
|
console.error('DB에 없는 데이터');
|
|
return;
|
|
}
|
|
|
|
const transformed = {
|
|
공문아이디: selectedDoc.doc_id,
|
|
수신처: selectedDoc.recipient_org,
|
|
수신처약자: selectedDoc.recipient_org_abbr,
|
|
수신자: selectedDoc.recipient_name,
|
|
수신자약자: selectedDoc.recipient_name_abbr,
|
|
발신처: selectedDoc.sender_org,
|
|
발신처약자: selectedDoc.sender_org_abbr,
|
|
발신자: selectedDoc.sender_name,
|
|
발신자약자: selectedDoc.sender_name_abbr,
|
|
doc_direction: selectedDoc.doc_direction,
|
|
공문번호: selectedDoc.doc_number,
|
|
공문유형: selectedDoc.doc_type,
|
|
공문종류: selectedDoc.doc_category,
|
|
공문제목: selectedDoc.doc_title,
|
|
공문제목요약: selectedDoc.doc_title_summary,
|
|
공문간연계: selectedDoc.doc_related_docs,
|
|
공문일자: selectedDoc.doc_date,
|
|
첨부문서수: selectedDoc.attachment_count,
|
|
첨부문서제목: selectedDoc.attachment_title,
|
|
공문내용요약: selectedDoc.doc_content_summary,
|
|
doc_manager: selectedDoc.doc_manager,
|
|
doc_memo: selectedDoc.doc_memo,
|
|
file_path: selectedDoc.file_path,
|
|
project_id: selectedDoc.project_id,
|
|
};
|
|
|
|
openDocUpsertModal(transformed, null, null, 'edit');
|
|
});
|
|
|
|
// 첨부파일 추가
|
|
document.querySelector('.official-doc-contextmenu-item.add-on-file.attach')?.addEventListener('change', async (event) => {
|
|
let files = Array.from(event.target.files); // FileList를 배열로 변환
|
|
|
|
const selectedDocElement = document.querySelector('li.main-file-container > div.official-doc-file-info.item-selected')?.parentElement;
|
|
const currentGroupId = selectedDocElement.dataset.groupId;
|
|
|
|
const formData = {
|
|
doc_label: 'attach',
|
|
group_id: currentGroupId,
|
|
};
|
|
|
|
let resourcePathArr = [];
|
|
// 경로 존재 확인을 위해 resourcePathArr 저장
|
|
for (let i = 0; i < files.length; i++) {
|
|
const officialFilePath = docVars.lastClickedListTarget.closest('li').dataset.resourcePath;
|
|
const attachPrefix = `${officialFilePath}__attachment/`;
|
|
const file = files[i];
|
|
let resourcePath = `${attachPrefix}${file.name}`;
|
|
resourcePathArr.push(resourcePath);
|
|
}
|
|
|
|
// resourcePathArr을 사용해서 경로 존재 확인
|
|
let checkTargetExistsResult = await checkDocTargetExists('attach', resourcePathArr);
|
|
|
|
// 이미 존재하는 경로만 existingPathArr에 저장
|
|
let existingPathArr = checkTargetExistsResult.existingPathArr;
|
|
|
|
// resourcePathArr 초기화
|
|
resourcePathArr = [];
|
|
|
|
if (files.length > 0) {
|
|
const updatedFiles = [];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
let file = files[i];
|
|
const officialFilePath = docVars.lastClickedListTarget.closest('li').dataset.resourcePath;
|
|
const attachPrefix = `${officialFilePath}__attachment/`;
|
|
let resourcePath = `${attachPrefix}${file.name}`;
|
|
let docDirection = docVars.allDocData?.find((doc) => doc.file_path === resourcePath)?.doc_direction;
|
|
let recipientOrg = docVars.allDocData?.find((doc) => doc.file_path === resourcePath)?.recipient_org;
|
|
let senderOrg = docVars.allDocData?.find((doc) => doc.file_path === resourcePath)?.sender_org;
|
|
|
|
let objectKey;
|
|
if (existingPathArr.includes(resourcePath)) {
|
|
let isUpload = confirm(`'${file.name}' 파일이 이미 존재합니다.\n기존 파일을 새 파일로 교체하시겠습니까?`);
|
|
if (isUpload) objectKey = checkTargetExistsResult.existingObjectKey.keyMap[resourcePath];
|
|
resetViewer();
|
|
if (!isUpload) continue;
|
|
}
|
|
if (objectKey) {
|
|
const docId = checkTargetExistsResult.existingDocId.keyMap[resourcePath];
|
|
await axios.post(`${docVars.path_name}/deleteDocData`, { docId, objectKey });
|
|
}
|
|
|
|
updatedFiles.push(file);
|
|
}
|
|
|
|
if (updatedFiles.length === 0) {
|
|
alert('업로드할 파일이 없습니다.');
|
|
return;
|
|
}
|
|
|
|
const uploadResult = await uploadDocFile(updatedFiles, 'add_attach');
|
|
|
|
if (!uploadResult.success) {
|
|
alert('파일 업로드에 실패했습니다.');
|
|
return;
|
|
}
|
|
|
|
formData.userInfoString = vars.userInfoString;
|
|
formData.uploadParams = uploadResult.uploadParams;
|
|
formData.uploadSuccessFileArr = uploadResult.uploadSuccessFileArr;
|
|
formData.uploadFailedFileArr = uploadResult.uploadFailedFileArr;
|
|
|
|
await upsertDocData(formData, 'add_attach');
|
|
} else {
|
|
alert('파일없음');
|
|
}
|
|
});
|
|
|
|
// 첨부파일 이름 변경
|
|
let isModalOpen = false;
|
|
document.querySelector('#renameAttachTargetDoc')?.addEventListener('click', () => {
|
|
if (checkProjectInactive()) return;
|
|
openDocRenameModal();
|
|
});
|
|
|
|
function openDocRenameModal() {
|
|
if (isModalOpen) return;
|
|
resetRenameModalContent();
|
|
isModalOpen = true;
|
|
|
|
const modal = document.querySelector('.official-doc-modal.official-doc-context-modal');
|
|
const modalBackground = document.querySelector('.official-doc-modal-background');
|
|
modal.style.display = 'flex';
|
|
modalBackground.style.display = 'flex';
|
|
|
|
let selectedDoc = docVars.allDocData.find((doc) => doc.doc_id === docVars.currentDocId);
|
|
|
|
const modalHeader = document.querySelector('.official-doc-modal.official-doc-context-modal .official-doc-modal-header');
|
|
const title = document.createElement('h3');
|
|
title.textContent = '이름 변경';
|
|
modalHeader.appendChild(title);
|
|
|
|
const modalBody = document.querySelector('.official-doc-modal.official-doc-context-modal .official-doc-modal-body');
|
|
const text = document.createElement('div');
|
|
text.classList.add('text-wrap');
|
|
text.innerHTML = `변경할 파일명을 입력한 후 확인을 눌러주세요.<br>이름 변경 대상 : ${selectedDoc.file_path}`;
|
|
modalBody.appendChild(text);
|
|
|
|
const oldName = selectedDoc.file_path.split('/').pop().split('.')[0];
|
|
const inputWrap = document.createElement('div');
|
|
inputWrap.classList.add('input-wrap');
|
|
const input = document.createElement('input');
|
|
input.type = 'text';
|
|
input.value = oldName;
|
|
input.spellcheck = false;
|
|
|
|
inputWrap.appendChild(input);
|
|
modalBody.appendChild(inputWrap);
|
|
input.focus();
|
|
|
|
let confirmBtn = document.querySelector('.official-doc-modal.official-doc-context-modal .official-doc-positive-btn');
|
|
let cancelBtn = document.querySelector('.official-doc-modal.official-doc-context-modal .official-doc-negative-btn');
|
|
|
|
const removeEventListeners = (oldChild) => {
|
|
const newChild = oldChild.cloneNode(true);
|
|
oldChild.parentNode.replaceChild(newChild, oldChild);
|
|
return newChild;
|
|
};
|
|
|
|
confirmBtn = removeEventListeners(confirmBtn);
|
|
cancelBtn = removeEventListeners(cancelBtn);
|
|
|
|
confirmBtn.addEventListener('click', () => {
|
|
handleRenameDoc(selectedDoc, inputWrap);
|
|
});
|
|
|
|
cancelBtn.addEventListener('click', () => {
|
|
closeDocRenameModal();
|
|
});
|
|
}
|
|
|
|
function closeDocRenameModal() {
|
|
const modal = document.querySelector('.official-doc-modal.official-doc-context-modal');
|
|
const modalBackground = document.querySelector('.official-doc-modal-background');
|
|
modal.style.display = 'none';
|
|
modalBackground.style.display = 'none';
|
|
|
|
resetRenameModalContent();
|
|
isModalOpen = false;
|
|
}
|
|
|
|
function resetRenameModalContent() {
|
|
const modalHeader = document.querySelector('.official-doc-modal.official-doc-context-modal .official-doc-modal-header');
|
|
const modalBody = document.querySelector('.official-doc-modal.official-doc-context-modal .official-doc-modal-body');
|
|
const textWrap = modalBody.querySelector('.text-wrap');
|
|
const inputWrap = modalBody.querySelector('.input-wrap');
|
|
|
|
if (modalHeader) modalHeader.innerHTML = '';
|
|
if (modalBody) modalBody.innerHTML = '';
|
|
if (textWrap) textWrap.innerHTML = '';
|
|
if (inputWrap) inputWrap.innerHTML = '';
|
|
}
|
|
|
|
async function handleRenameDoc(data, inputWrap) {
|
|
let oldName = data.file_path.split('/').pop();
|
|
let newName = inputWrap.getElementsByTagName('input')[0].value.trim();
|
|
let docId = data.doc_id;
|
|
|
|
let ext;
|
|
let split = splitBaseAndExt(oldName);
|
|
ext = split.ext;
|
|
oldName = `${split.base}.${ext}`;
|
|
newName = `${newName}.${ext}`;
|
|
|
|
let oldPath = data.file_path;
|
|
let parts = oldPath.split('/');
|
|
let topFolderPath = '/' + parts[1];
|
|
let newPath = topFolderPath == '/' ? `/${newName}` : `${topFolderPath}/${newName}`;
|
|
|
|
// 기존 경고문구 있으면 삭제
|
|
if (document.querySelector('.official-doc-modal.official-doc-context-modal .input-wrap .warn')) {
|
|
inputWrap.removeChild(document.querySelector('.official-doc-modal.official-doc-context-modal .input-wrap .warn'));
|
|
}
|
|
|
|
// 경고문구 dom 생성
|
|
let warn = document.createElement('div');
|
|
warn.classList.add('warn');
|
|
warn.style.top = `${inputWrap.offsetHeight}px`;
|
|
inputWrap.appendChild(warn);
|
|
|
|
let checkTargetExistsResult = await checkDocTargetExists('attach', newPath);
|
|
|
|
// 상황에 따라 경고문구 텍스트 추가 또는 renameTarget 진행
|
|
if (!newName || newName == '' || newName == null) {
|
|
// 빈문자 체크
|
|
warn.innerText = `파일명을 입력해주세요.`;
|
|
} else if (hasSpecialChar(newName)) {
|
|
// 특수문자 체크
|
|
warn.innerHTML = `다음 특수문자는 사용할 수 없습니다.<br>\\ / : * ? ' " \` < > | #`;
|
|
} else if (checkTargetExistsResult.isExists) {
|
|
// 동일이름 여부 체크
|
|
let text = `동일한 파일명이 존재합니다.`;
|
|
warn.innerText = text;
|
|
} else {
|
|
if (inputWrap.contains(warn)) {
|
|
inputWrap.removeChild(warn);
|
|
}
|
|
closeDocRenameModal();
|
|
|
|
let renameTargetParams = {
|
|
userInfoString: vars.userInfoString,
|
|
storageType: vars.storageType,
|
|
resourcePath: data.file_path,
|
|
newName: newName,
|
|
oldName: oldName,
|
|
newPath: newPath,
|
|
oldPath: oldPath,
|
|
docId: docId,
|
|
};
|
|
|
|
let li = docVars.lastClickedListTarget.closest('li');
|
|
let res = await axios.post(`${docVars.path_name}/renameDocTarget`, { params: renameTargetParams });
|
|
if (res.data.message == 'renameDocTarget_success') {
|
|
await syncDocInfo(['official', 'attach', null]);
|
|
await getDocDataBySelected();
|
|
let newLi = document.querySelector(`li[data-doc-id="${li.dataset.docId}"]`);
|
|
if (newLi) {
|
|
itemBoxSelected(newLi);
|
|
await renderDocViewer(newLi.dataset.resourcePath, newLi.dataset.docId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 삭제
|
|
async function handleDeleteDoc(selectedDoc) {
|
|
const docId = selectedDoc.doc_id;
|
|
const groupId = selectedDoc.group_id;
|
|
const objectKey = selectedDoc.object_key;
|
|
let fileName = selectedDoc?.file_path.split('/').pop();
|
|
const docTitle = selectedDoc?.doc_title;
|
|
const docLabel = selectedDoc?.doc_label;
|
|
|
|
if (confirm(`정말 삭제하시겠습니까?\n\n[파일 이름] ${fileName}\n[파일 제목] ${docTitle}`)) {
|
|
try {
|
|
const res = await axios.post(`${docVars.path_name}/deleteDocData`, { docId, groupId, objectKey });
|
|
if (res.data.message === 'delete_success') {
|
|
alert('삭제가 완료되었습니다.');
|
|
await syncDocInfo(['official', 'attach', null]);
|
|
await getDocDataBySelected();
|
|
docVars.selectedDoc = null;
|
|
// 미리보기 영역 초기화
|
|
document.querySelector('.official-doc-main .official-doc-preview .official-doc-preview--container').style.display = 'none';
|
|
document.querySelector('.official-doc-main .official-doc-preview .official-doc-preview--notice').style.display = 'flex';
|
|
} else {
|
|
alert('삭제에 실패했습니다.');
|
|
}
|
|
} catch (error) {
|
|
console.error('삭제 요청 중 오류 발생:', error);
|
|
alert('삭제 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 삭제 버튼 클릭 이벤트
|
|
async function setupDeleteDocButtons() {
|
|
if (checkProjectInactive()) return;
|
|
|
|
const buttons = document.querySelectorAll('.official-doc-contextmenu-item.deleteTargetDoc');
|
|
buttons.forEach((btn) => {
|
|
btn.addEventListener('click', async () => {
|
|
await syncDocInfo(['official', 'attach', null]);
|
|
const selectedDoc = docVars.allDocData.find((doc) => doc.doc_id === docVars.currentDocId);
|
|
if (selectedDoc) {
|
|
await handleDeleteDoc(selectedDoc);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
setupDeleteDocButtons();
|
|
|
|
// 다운로드
|
|
async function handleDownloadDoc() {
|
|
const selectedDoc = docVars.allDocData.find((doc) => doc.doc_id === docVars.currentDocId);
|
|
|
|
if (selectedDoc) {
|
|
let resourcePath = selectedDoc.file_path;
|
|
let objectKey = selectedDoc.object_key;
|
|
|
|
let generateDownloadUrlParams = {
|
|
objectKey: objectKey,
|
|
resourcePath: resourcePath,
|
|
};
|
|
|
|
try {
|
|
let generateDownloadUrlRes = await axios.post(`${docVars.path_name}/generateDownloadDocUrl`, generateDownloadUrlParams);
|
|
if (generateDownloadUrlRes.data.message === 'generateDownloadDocUrl_success') {
|
|
let url = generateDownloadUrlRes.data.url;
|
|
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = resourcePath.split('/').pop();
|
|
a.style.display = 'none';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 300)); // 딜레이
|
|
}
|
|
} catch (error) {
|
|
console.error('다운로드 URL 생성 중 오류 발생:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 다운로드 버튼 클릭 이벤트 함수
|
|
function setupDownloadButton() {
|
|
if (checkProjectInactive()) return;
|
|
|
|
document.querySelectorAll('.official-doc-contextmenu-item.downloadTargetDoc').forEach((btn) => {
|
|
btn.addEventListener('click', handleDownloadDoc);
|
|
});
|
|
}
|
|
setupDownloadButton();
|
|
|
|
// 이미 존재하는 파일인지 확인
|
|
async function checkDocTargetExists(docLabel, resourcePathArr) {
|
|
let result = {};
|
|
if (!Array.isArray(resourcePathArr)) resourcePathArr = [resourcePathArr];
|
|
|
|
let checkTargetExistsParams = {
|
|
storageType: docVars.storageType,
|
|
docLabel: docLabel,
|
|
resourcePathArr: JSON.stringify(resourcePathArr),
|
|
};
|
|
let checkTargetExistsRes = await axios.post(`${docVars.path_name}/checkDocTargetExists`, { params: checkTargetExistsParams });
|
|
if (checkTargetExistsRes.data.message == 'checkDocTargetExists_success') {
|
|
let rows = checkTargetExistsRes.data.rows;
|
|
|
|
result.existingPathArr = [];
|
|
|
|
if (rows.length == 0) {
|
|
result.isExists = false;
|
|
} else {
|
|
result.rows = rows;
|
|
result.isExists = true;
|
|
result.existingPathArr = filterFilePath(rows);
|
|
result.existingObjectKey = filterObjectKey(rows);
|
|
result.existingDocId = filterDocId(rows);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function filterFilePath(rows) {
|
|
if (!Array.isArray(rows)) rows = [rows];
|
|
|
|
let result = [];
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
for (let j = 0; j < rows[i].rows.length; j++) {
|
|
result.push(rows[i].rows[j].file_path);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function filterObjectKey(rows) {
|
|
if (!Array.isArray(rows)) rows = [rows];
|
|
|
|
let result = [];
|
|
result.keyMap = {};
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
for (let j = 0; j < rows[i].rows.length; j++) {
|
|
const file_path = rows[i].rows[j].file_path;
|
|
const object_key = rows[i].rows[j].object_key;
|
|
result.keyMap[file_path] = object_key;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function filterDocId(rows) {
|
|
if (!Array.isArray(rows)) rows = [rows];
|
|
|
|
let result = [];
|
|
result.keyMap = {};
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
for (let j = 0; j < rows[i].rows.length; j++) {
|
|
const file_path = rows[i].rows[j].file_path;
|
|
const doc_id = rows[i].rows[j].doc_id;
|
|
result.keyMap[file_path] = doc_id;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|