Files
PM_test/controllers/archiveController.js
2026-06-19 17:58:47 +09:00

4403 lines
156 KiB
JavaScript

const path = require('path');
const pool = require("../db/pool.js");
const multer = require('multer');
const archiver = require('archiver');
const util = require('util');
const { exec, execSync } = require('child_process');
const execPromise = util.promisify(exec);
const { getIo } = require('../socket.js');
// const { userInfo } = require('os');
// const { query } = require('winston');
// const { GoogleGenerativeAI } = require("@google/genai");
const pdfParse = require('pdf-parse');
const { encode } = require('gpt-tokenizer');
const dotenv = require('dotenv');
dotenv.config();
//// s3 api 관련
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
const {
ListObjectsV2Command,
DeleteObjectCommand,
CopyObjectCommand,
HeadObjectCommand,
PutObjectCommand,
GetObjectCommand,
ListBucketsCommand
} = require('@aws-sdk/client-s3');
const serviceName = process.env.SERVICE_NAME;
//// env의 DEPLOYMENT_TYPE에 따라 클라이언트 설정
const onPremiseClient = require('../config/onPremiseClient.js');
const cloudClient = require('../config/cloudClient.js');
const storageClients = {
'ONPREMISE': onPremiseClient,
'CLOUD': cloudClient
}
const deploymentType = process.env.DEPLOYMENT_TYPE;
const s3 = storageClients[deploymentType];
const cloudType = process.env.CLOUD_TYPE;
//// env의 NODE_ENV에 따라 DB 테이블 이름 설정
const env = process.env.NODE_ENV;
const tbLog = env === 'production' ? 'tb_log' : '_test_tb_log';
const tbData = env == 'production'? 'tb_data':'_test_tb_data';
const tbClickLog = env == 'production'? 'tb_click_log':'_test_tb_click_log';
const tbProject = env === 'production' ? 'tb_project' : '_test_tb_project';
const tbPermission = env === 'production' ? 'tb_permission' : '_test_tb_permission';
const tbFolderPermission = env === 'production' ? 'tb_folder_permission' : '_test_tb_folder_permission';
// 테스트
// const tbLog = env == 'production'? 'tb_log':'tb_log';
// const tbData = env == 'production'? 'tb_data':'tb_data';
// const tbClickLog = env == 'production'? 'tb_click_log':'tb_click_log';
//// 큐 관련
const { redisConnection } = require('../config/redis.js');
const {
convertPdfQueue,
zipFolderQueue,
thumbQueue,
postProcessVideoQueue,
summarizeAIQueue,
summarizeAPIQueue
} = require('../queue.js'); // queue.js에서 행동별 Queue 객체 생성 후 사용
const { application } = require('express');
//// 변환 필요 확장자, 변환 불필요 확장자, 지원여부 확장자
// -> pdf 암호화 안하는 버전 - 변환 불필요 확장자에 pdf 포함
let needConvertExtArr = ['hwp', 'hwpx', 'doc', 'docx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'dwg', 'dxf', 'grm'];
let notNeedConvertExtArr = ['pdf', 'gsim', 'ifc', 'png', 'jpg', 'jpeg', 'webp', 'gif', 'mp4', 'mov', 'webm', 'txt', 'log', 'md', 'url', 'zip', 'glb', 'gltf', 'obj', 'stl', 'fbx', '3dm', 'html'];
// -> pdf 암호화 하는 버전 - 변환 필요 확장자에 pdf 포함
// let needConvertExtArr = ['pdf', 'hwp', 'hwpx', 'doc', 'docx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'dwg', 'dxf'];
// let notNeedConvertExtArr = ['mp4', 'jpg', 'jpeg', 'png'];
let supportedExtArr = [...needConvertExtArr, ...notNeedConvertExtArr];
//// 현재 변환중인 파일 정보를 저장하는 배열
let convertingDataArr = [];
//// 현재 AI 요약중인 파일 정보를 저장하는 배열
let summarizeAiDataArr = [];
// 🔻🔻🔻🔻🔻🔻🔻🔻 유틸리티 함수 시작 🔻🔻🔻🔻🔻🔻🔻🔻
// storageType 에 따라 클라이언트 리턴
function getS3(storageType) {
return storageType === 'Cloud'
? cloudClient
: onPremiseClient; // 기본은 minIO
}
function getBasePrefix(pageType) {
return {
origin: `${pageType}/origin/`,
pdf: `${pageType}/pdf/`,
pdf_thumb: `${pageType}/pdf_thumb/`,
thumbnail: `${pageType}/thumbnail/`,
}
}
function getDepth(path) {
path = path.startsWith('/') ? path.slice(1) : path;
let pathSplit = path.split('/');
let result = pathSplit.length;
return result;
}
function getPathArray(path) {
let result = Array(8).fill(null);
let pathSplit = path.replace(/\/$/, '').split('/');
for(let i = 0; i < pathSplit.length; i++) {
result[i] = pathSplit[i];
}
return result;
}
function getPathSegment(path, num) {
path = path.startsWith('/') ? path.slice(1) : path;
let pathSplit = path.split('/');
return pathSplit[num-1];
}
function makeObjectKeyTimestamp(date) {
if (date) date = new Date(date);
else date = new Date(Date.now());
// Intl API로 현재 시스템 타임존 확인
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// KST 변환이 필요한 경우에만 UTC + 9시간 적용
const isUtcOrNonSeoul = !timeZone || timeZone !== 'Asia/Seoul';
date = isUtcOrNonSeoul ? new Date(date.getTime() + 9 * 60 * 60 * 1000) : date;
let YY = String(date.getFullYear()).substring(2, 4);
let MM = String(date.getMonth() + 1).padStart(2, '0');
let DD = String(date.getDate()).padStart(2, '0');
let HH = String(date.getHours()).padStart(2, '0');
let mm = String(date.getMinutes()).padStart(2, '0');
let ss = String(date.getSeconds()).padStart(2, '0');
let SSS = String(date.getMilliseconds()).padStart(3, '0');
return `${YY}${MM}${DD}-${HH}${mm}${ss}-${SSS}`;
}
function makePostgresTimestamp(date) {
if (date) date = new Date(date);
else date = new Date(Date.now());
// Intl API로 현재 시스템 타임존 확인
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// KST 변환이 필요한 경우에만 UTC + 9시간 적용
const isUtcOrNonSeoul = !timeZone || timeZone !== 'Asia/Seoul';
date = isUtcOrNonSeoul ? new Date(date.getTime() + 9 * 60 * 60 * 1000) : date;
// console.log('@@@@@@@@@@@@@@');
// console.log(date);
// console.log(timeZone);
// console.log(isUtcOrNonSeoul);
// console.log(date);
let YYYY = date.getFullYear();
let MM = String(date.getMonth() + 1).padStart(2, '0');
let DD = String(date.getDate()).padStart(2, '0');
let HH = String(date.getHours()).padStart(2, '0');
let mm = String(date.getMinutes()).padStart(2, '0');
let ss = String(date.getSeconds()).padStart(2, '0');
let SSS = String(date.getMilliseconds()).padStart(3, '0');
return `${YYYY}-${MM}-${DD} ${HH}:${mm}:${ss}.${SSS}`;
}
function getFileNameFromKey(key) {
let keySplit1 = key?.split('/')[key.split('/').length-1];
let keySplit2 = keySplit1?.split('__')[0];
let keySplit3 = keySplit2?.split('.');
keySplit3?.pop();
let fileName = keySplit3?.join('.');
return fileName;
}
function getPermissionString(permission) {
let strings = {
1: '참관자',
// 2: '',
4: '일반참여자',
8: '보안참여자',
0: '부관리자',
}
return strings[`${permission}`];
}
// 🔺🔺🔺🔺🔺🔺🔺🔺 유틸리티 함수 끝 🔺🔺🔺🔺🔺🔺🔺🔺
// 🔻🔻🔻🔻🔻🔻🔻🔻 DB 관련 함수 시작 🔻🔻🔻🔻🔻🔻🔻🔻
async function selectData(projectId, storageType, userInfo, resourcePath) {
let userId = (userInfo)?userInfo.user_id:undefined;
if (!userId) return;
let permission = (userInfo)?userInfo.permission:undefined;
let depth = getDepth(resourcePath);
const client = await pool.connect();
try {
////////////////////////////////////////////
//// 민홍이형 파라미터 바인딩 작업
let values = [];
let paramCounter = 1;
// let queryString = `
// SELECT
// d.data_id, d.project_id, d.user_id, d.create_date, d.data_permission, d.bucket, d.is_folder, d.is_removed, d.data_depth,
// d.ext, d.path1, d.path2, d.path3, d.path4, d.path5, d.path6, d.path7, d.path8, d.mod_date, d.mod_user_id, d.mod_activity,
// d.data_size, d.memo, d.storage_type, d.object_key, d.preview_key, d.popup_key, d.ver, d.folder_type, d.thumbnail_key,
// d.lon, d.lat, d.height, d.author_id, d.author_nm,
// CASE
// WHEN u.is_resigned = TRUE THEN u.user_nm || '(퇴사자)'
// ELSE u.user_nm
// END AS user_nm,
// u.company, u.dept, u.position, u.user_pw, u.group, u.bookmark
// FROM ver4.${tbData} d
// INNER JOIN ver4.tb_user u
// ON d.user_id = u.user_id
// WHERE d.project_id = $${paramCounter++}
// AND d.bucket = $${paramCounter++}
// AND d.storage_type = $${paramCounter++}
// AND d.is_removed = false`;
let queryString = `
SELECT
d.data_id, d.project_id, d.user_id, d.create_date, d.data_permission, d.bucket, d.is_folder, d.is_removed, d.data_depth,
d.ext, d.path1, d.path2, d.path3, d.path4, d.path5, d.path6, d.path7, d.path8, d.mod_date, d.mod_user_id, d.mod_activity,
d.data_size, d.memo, d.storage_type, d.object_key, d.preview_key, d.popup_key, d.ver, d.folder_type, d.thumbnail_key,
d.lon, d.lat, d.height, d.author_id, d.author_nm, d.last_folder_act_date,
CASE
WHEN u.is_resigned = TRUE THEN u.user_nm || '(퇴사자)'
ELSE u.user_nm
END AS user_nm,
u.company, u.dept, u.position, u.user_pw, u.group, u.bookmark,
CASE
WHEN mod_u.is_resigned = TRUE THEN mod_u.user_nm || '(퇴사자)'
ELSE mod_u.user_nm
END AS mod_user_nm
FROM ver4.${tbData} d
INNER JOIN ver4.tb_user u
ON d.user_id = u.user_id
LEFT JOIN ver4.tb_user mod_u
ON d.mod_user_id = mod_u.user_id
WHERE d.project_id = $${paramCounter++}
AND d.bucket = $${paramCounter++}
AND d.storage_type = $${paramCounter++}
AND d.is_removed = false`;
values.push(projectId);
values.push(projectId);
values.push(storageType);
if (resourcePath == '') {
// resourcePath가 ''인 경우 -> 헤더 버튼에 표시할 depth가 1인 폴더만 조회
queryString += `
AND d.data_depth = $${paramCounter++}`;
values.push(depth);
} else {
// resourcePath가 ''이 아닌 경우 -> 해당 resourcePath의 아래 depth 폴더를 조회해야 하므로 depth에 1을 더해서 사용
queryString += `
AND d.data_depth = $${paramCounter++}`;
values.push(depth + 1);
if (depth + 1 >= 2) {
for (let i = 0; i < depth; i++) {
let num = i + 1;
queryString += `
AND d.path${num} = $${paramCounter++}`;
values.push(getPathSegment(resourcePath, num));
}
}
}
queryString += `
AND ((d.data_permission+32) & COALESCE(
(SELECT lev FROM ver4.${tbFolderPermission}
WHERE project_id = d.project_id
AND user_id = $${paramCounter++}
AND folder_path_key = CASE
WHEN d.data_depth = 1 THEN d.path1
WHEN d.data_depth = 2 THEN CONCAT(d.path1, '/', d.path2)
ELSE CONCAT(d.path1, '/', d.path2, '/', d.path3)
END),
(SELECT lev FROM ver4.${tbPermission}
WHERE project_id = d.project_id
AND user_id = $${paramCounter++}),
$${paramCounter++}::integer
)) <> 0`;
values.push(userId);
values.push(userId);
values.push(parseInt(permission) || 0);
queryString += `
ORDER BY path1, path2, path3, path4, path5, path6, path7, path8;`;
////////////////////////////////////////////
//// chat gpt 중복 row 필터
// let values = [];
// let paramCounter = 1;
// let innerQuery = `
// SELECT
// d.data_id, d.project_id, d.user_id, d.create_date, d.data_permission, d.bucket, d.is_folder, d.is_removed, d.data_depth,
// d.ext, d.path1, d.path2, d.path3, d.path4, d.path5, d.path6, d.path7, d.path8, d.mod_date, d.mod_user_id, d.mod_activity,
// d.data_size, d.memo, d.storage_type, d.object_key, d.preview_key, d.popup_key, d.ver,
// u.user_nm, u.company, u.dept, u.position, u.user_pw, u.group, u.bookmark,
// ROW_NUMBER() OVER (
// PARTITION BY d.path1, d.path2, d.path3, d.path4, d.path5, d.path6, d.path7, d.path8
// ORDER BY d.mod_date DESC
// ) AS row_num
// FROM ver4.${tbData} d
// INNER JOIN ver4.tb_user u
// ON d.user_id = u.user_id
// WHERE d.project_id = $${paramCounter++}
// AND d.bucket = $${paramCounter++}
// AND d.storage_type = $${paramCounter++}
// AND d.is_removed = false`;
// values.push(projectId);
// values.push(projectId); // bucket이 projectId로 들어오는 구조라고 가정
// values.push(storageType);
// if (resourcePath == '') {
// innerQuery += `
// AND d.data_depth = $${paramCounter++}`;
// values.push(depth);
// } else {
// innerQuery += `
// AND d.data_depth = $${paramCounter++}`;
// values.push(depth + 1);
// if (depth + 1 >= 2) {
// for (let i = 0; i < depth; i++) {
// let num = i + 1;
// innerQuery += `
// AND d.path${num} = $${paramCounter++}`;
// values.push(getPathSegment(resourcePath, num));
// }
// }
// }
// // 권한 조건
// innerQuery += `
// AND ((d.data_permission + 32) & `;
// if (permission) {
// innerQuery += `$${paramCounter++}`;
// values.push(permission);
// } else {
// innerQuery += `(SELECT lev FROM ver4.${tbPermission} WHERE project_id = $${paramCounter++} AND user_id = $${paramCounter++})`;
// values.push(projectId);
// values.push(userId);
// }
// innerQuery += `) <> 0`;
// // 최종적으로 row_num = 1만 필터링
// let queryString = `
// SELECT * FROM (
// ${innerQuery}
// ) AS sub
// WHERE sub.row_num = 1
// ORDER BY sub.path1, sub.path2, sub.path3, sub.path4, sub.path5, sub.path6, sub.path7, sub.path8;
// `;
////////////////////////////////////////////
// console.log('==============');
// console.log(queryString);
// console.log('Values:', values);
let result = await client.query(queryString, values);
// console.log('@@@@');
// console.log(result.rows);
return result.rows;
} catch(error) {
console.error("selectData err:", error);
} finally {
client.release();
}
}
async function selectRemovedData(projectId, storageType, userInfo) {
let userId = (userInfo)?userInfo.user_id:undefined;
if (!userId) return;
let permission = (userInfo)?userInfo.permission:undefined;
const client = await pool.connect();
try {
let values = [];
let paramCounter = 1;
let queryString = `
SELECT
d.data_id, d.project_id, d.user_id, d.create_date, d.data_permission, d.bucket, d.is_folder, d.is_removed, d.data_depth,
d.ext, d.path1, d.path2, d.path3, d.path4, d.path5, d.path6, d.path7, d.path8, d.mod_date, d.mod_user_id, d.mod_activity,
d.data_size, d.memo, d.storage_type, d.object_key, d.preview_key, d.popup_key, d.ver, d.folder_type, d.thumbnail_key,
d.lon, d.lat, d.height, d.author_id, d.author_nm,
CASE
WHEN u.is_resigned = TRUE THEN u.user_nm || '(퇴사자)'
ELSE u.user_nm
END AS user_nm,
u.company, u.dept, u.position, u.user_pw, u.group, u.bookmark,
CASE
WHEN mod_u.is_resigned = TRUE THEN mod_u.user_nm || '(퇴사자)'
ELSE mod_u.user_nm
END AS mod_user_nm
FROM ver4.${tbData} d
INNER JOIN ver4.tb_user u
ON d.user_id = u.user_id
LEFT JOIN ver4.tb_user mod_u
ON d.mod_user_id = mod_u.user_id
WHERE d.project_id = $${paramCounter++}
AND d.bucket = $${paramCounter++}
AND d.storage_type = $${paramCounter++}
AND d.is_removed = true
AND d.is_folder = false`;
values.push(projectId);
values.push(projectId);
values.push(storageType);
queryString += `
AND ((d.data_permission+32) & COALESCE(
(SELECT lev FROM ver4.${tbFolderPermission}
WHERE project_id = d.project_id
AND user_id = $${paramCounter++}
AND folder_path_key = CASE
WHEN d.data_depth = 1 THEN d.path1
WHEN d.data_depth = 2 THEN CONCAT(d.path1, '/', d.path2)
ELSE CONCAT(d.path1, '/', d.path2, '/', d.path3)
END),
(SELECT lev FROM ver4.${tbPermission}
WHERE project_id = d.project_id
AND user_id = $${paramCounter++}),
$${paramCounter++}::integer
)) <> 0`;
values.push(userId);
values.push(userId);
values.push(parseInt(permission) || 0);
queryString += `
ORDER BY path1, path2, path3, path4, path5, path6, path7, path8;`;
// console.log('==================');
// console.log(queryString);
// console.log(values);
let result = await client.query(queryString, values);
return result.rows;
} catch(error) {
console.error("selectRemovedData err:", error);
} finally {
client.release();
}
}
async function selectLog(projectId, params) {
const client = await pool.connect();
try {
let queryString = `
SELECT
l.log_id, l.project_id, l.activity, l.user_id, l.user_ip, l.log_date, l.path_arr, l.data_id_arr,
u.company, u.dept, u.position, u.user_pw, u.group, u.bookmark, u.user_nm
FROM ver4.${tbLog} l
INNER JOIN ver4.tb_user u
ON l.user_id = u.user_id
WHERE l.project_id = '${projectId}'`
if(params != undefined) {
// 특정 유저를 선택한 경우 removeTarget_folder_expired 제거
const isAllUser = params.user && params.user.length > 1;
let activityArr = params.activity ? [...params.activity] : null;
if (activityArr) {
if (!isAllUser) {
activityArr = activityArr.filter(a => a !== 'removeTarget_folder_expired');
}
if (activityArr.length > 0) {
const activityList = activityArr.map(a => `'${a}'`).join(',');
queryString += `
AND l.activity IN (${activityList})
`;
}
} else {
queryString += `
AND 1 = 0
`
}
if(params.user) {
let userArr = [...params.user];
// allUser인 경우 '-'도 포함
if (isAllUser) {
if (!userArr.includes('-')) {
userArr.push('-');
}
}
const userList = userArr.map(u => `'${u}'`).join(',');
queryString += `
AND l.user_id IN (${userList})
`;
}
if(params.startDate) {
queryString += `
AND l.log_date >= '${params.startDate} 00:00:00.000'
`
}
if(params.endDate) {
// 다음날 자정까지
queryString += `
AND l.log_date <= '${params.endDate} 23:59:59.999'
`
}
} else {
let today = new Date();
let weekAgo = new Date(today);
weekAgo.setDate(today.getDate() - 7);
// YYYY-MM-DD 형식
const toDateStr = (d) => {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
let startDate = toDateStr(weekAgo);
let endDate = toDateStr(today);
// let today = makePostgresTimestamp(todayEnd);
// let weekAgo = makePostgresTimestamp(weekAgoStart);
queryString += `
AND l.log_date >= '${startDate} 00:00:00.000'
AND l.log_date <= '${endDate} 23:59:59.999'
`
}
queryString += `
ORDER BY l.log_id DESC
LIMIT 500;
`;
// console.log('============');
// console.log(queryString);
let result = await client.query(queryString);
// console.log('@@@@');
// console.log(result.rows);
// console.log(result.rows.length);
let logData = {};
result.rows.forEach(row => {
logData[row.log_id] = row;
})
return logData;
} catch(error) {
console.error("selectLog err:", error);
} finally {
client.release();
}
}
async function selectUserLog(projectId) {
const client = await pool.connect();
try {
let queryString = `
SELECT
l.log_id, l.project_id, l.activity, l.user_id, l.user_ip, l.log_date, l.path_arr, l.data_id_arr,
u.company, u.dept, u.position, u.user_pw, u.group, u.bookmark, u.user_nm
FROM ver4.${tbLog} l
INNER JOIN ver4.tb_user u
ON l.user_id = u.user_id
WHERE l.project_id = '${projectId}'
ORDER BY l.log_id DESC
`
// console.log('============');
// console.log(queryString);
let result = await client.query(queryString);
// console.log('@@@@');
// console.log(result.rows);
// console.log(result.rows.length);
let logData = {};
result.rows.forEach(row => {
logData[row.log_id] = row;
})
return logData;
} catch(error) {
console.error("selectUserLog err:", error);
} finally {
client.release();
}
}
// async function selectLog(projectId) {
// const client = await pool.connect();
// try {
// let queryString = `
// SELECT
// l.log_id, l.project_id, l.activity, l.user_id, l.user_ip, l.log_date, l.path_arr, l.data_id_arr,
// CASE
// WHEN u.is_resigned = TRUE THEN u.user_nm || '(퇴사자)'
// ELSE u.user_nm
// END AS user_nm,
// u.company, u.dept, u.position, u.user_pw, u.group, u.bookmark
// FROM ver4.${tbLog} l
// INNER JOIN ver4.tb_user u
// ON l.user_id = u.user_id
// WHERE l.project_id = '${projectId}'
// ORDER BY l.log_id DESC
// LIMIT 500;`;
// // console.log('============');
// // console.log(queryString);
// let result = await client.query(queryString);
// // console.log('@@@@');
// // console.log(result.rows);
// let logData = {};
// result.rows.forEach(row => {
// logData[row.log_id] = row;
// })
// return logData;
// } catch(error) {
// console.error("selectLog err:", error);
// } finally {
// client.release();
// }
// }
async function userLog(projectId) {
const client = await pool.connect();
try {
let queryString = `
SELECT
l.log_id, l.project_id, l.activity, l.user_id, l.user_ip, l.log_date, l.path_arr, l.data_id_arr,
CASE
WHEN u.is_resigned = TRUE THEN u.user_nm || '(퇴사자)'
ELSE u.user_nm
END AS user_nm,
u.company, u.dept, u.position, u.user_pw, u.group, u.bookmark
FROM ver4.${tbLog} l
INNER JOIN ver4.tb_user u
ON l.user_id = u.user_id
WHERE l.project_id = '${projectId}'
ORDER BY l.log_id DESC;
`;
let result = await client.query(queryString);
let logData = {};
result.rows.forEach(row => {
logData[row.log_id] = row;
})
return logData;
} catch(error) {
console.error("filterUserLog err:", error);
} finally {
client.release();
}
}
async function insertData(params) {
let {
projectId, userInfoString, storageType, dateArr, resourcePathArr, sizeArr, objectKeyArr,
thumbnailSizeArr, thumbnailKeyArr, coordArr, dataType, dataPermission, folderType, activity
} = params;
let bucket = projectId;
let userInfo = JSON.parse(userInfoString);
let userId = userInfo.user_id;
if (dataPermission == undefined) dataPermission = 1;
const client = await pool.connect();
try {
let isFolder = true;
if (dataType == 'file') isFolder = false;
let values = [];
let placeholders = [];
for (let i = 0; i < dateArr.length; i++) {
let createDate = makePostgresTimestamp(dateArr[i]);
let resourcePath;
if (resourcePathArr && resourcePathArr.length != 0) resourcePath = resourcePathArr[i];
resourcePath = resourcePath.startsWith('/') ? resourcePath.slice(1) : resourcePath;
let depth = getDepth(resourcePath);
let ext = null;
if (dataType == 'file') ext = (resourcePath.split('.').pop()).replace('.', '').toLowerCase();
let dataSize = 0;
if (sizeArr && sizeArr.length != 0) dataSize = sizeArr[i];
let objectKey = null;
if (objectKeyArr && objectKeyArr.length != 0) objectKey = objectKeyArr[i];
let previewKey = null, popupKey = null;
if (!needConvertExtArr.includes(ext)) {
previewKey = objectKey;
popupKey = objectKey;
}
// let previewKey = objectKey;
// let popupKey = objectKey;
let thumbnailSize = 0;
if (thumbnailSizeArr && thumbnailSizeArr.length != 0) thumbnailSize = thumbnailSizeArr[i];
let thumbnailKey = null;
if (thumbnailKeyArr && thumbnailKeyArr.length != 0) thumbnailKey = thumbnailKeyArr[i];
let lon = null, lat = null, height = null;
if (coordArr && coordArr.length != 0) {
lon = coordArr[i]?.lon;
lat = coordArr[i]?.lat;
height = coordArr[i]?.height;
}
let lastFolderActDate = null;
if (activity == 'createFolder' && depth == 3) lastFolderActDate = makePostgresTimestamp(Date.now());
values.push(
projectId, userId, createDate, dataPermission, bucket, isFolder, depth, ext, dataSize,
storageType, objectKey, previewKey, popupKey, folderType, thumbnailSize, thumbnailKey, lon, lat,
height, lastFolderActDate, ...getPathArray(resourcePath)
);
let idx = i * 28;
placeholders.push(`
($${idx+1}, $${idx+2}, $${idx+3}, $${idx+4}, $${idx+5}, $${idx+6}, $${idx+7}, $${idx+8}, $${idx+9},
$${idx+10}, $${idx+11}, $${idx+12}, $${idx+13}, $${idx+14}, $${idx+15}, $${idx+16}, $${idx+17}, $${idx+18},
$${idx+19}, $${idx+20}, $${idx+21}, $${idx+22}, $${idx+23}, $${idx+24}, $${idx+25}, $${idx+26}, $${idx+27}, $${idx+28})
`);
}
let queryString = `
INSERT INTO ver4.${tbData} (
project_id, user_id, create_date, data_permission, bucket, is_folder, data_depth, ext, data_size,
storage_type, object_key, preview_key, popup_key, folder_type, thumbnail_size, thumbnail_key, lon, lat,
height, last_folder_act_date, path1, path2, path3, path4, path5, path6, path7, path8
) VALUES ${placeholders.join(',')}
RETURNING data_id;
`;
// console.log('@@@@@@@@@@@@@@@@@');
// console.log(queryString);
// console.log('');
// console.log('!!!!!!!!!!!!!!!!!');
// console.log(values);
let { rows } = await client.query(queryString, values);
let result = { message: 'insertData_success', rows: rows };
return result;
} catch(error) {
console.error("insertData err:", error);
return { message: 'insertData_failed', error: error };
} finally {
client.release();
}
}
async function insertLog(params, from) {
// Synchronous database inserts for activity log are now handled asynchronously
// by the activityLogger middleware via BullMQ.
return { message: 'insertLog_success' };
}
async function insertClickLog(params) {
// Synchronous database inserts for click log are now handled asynchronously
// by the activityLogger middleware via BullMQ.
return { message: 'insertClickLog_success' };
}
/*
updateRows
이름변경
다른 폴더로 이동
휴지통에 버리기
등록자 변경?
권한 변경
*/
async function updateDataRename(params) {
let { projectId, userInfoString, storageType, resourcePath, activity, newName, oldName } = params;
let bucket = projectId;
let userInfo = JSON.parse(userInfoString);
let userId = userInfo.user_id;
let dateNow = makePostgresTimestamp(Date.now());
let depth = getDepth(resourcePath);
const client = await pool.connect();
try {
let segment1 = getPathSegment(resourcePath, depth);
let values = [];
let paramCounter = 1; // 파라미터 번호($1, $2, ...)
let queryString = `
UPDATE ver4.${tbData}
SET
path${depth} = REPLACE(path${depth}, $${paramCounter++}, $${paramCounter++}),
mod_date = CASE
WHEN path${depth} = $${paramCounter++} AND data_depth = $${paramCounter++} THEN $${paramCounter++}
ELSE mod_date
END,
mod_user_id = CASE
WHEN path${depth} = $${paramCounter++} AND data_depth = $${paramCounter++} THEN $${paramCounter++}
ELSE mod_user_id
END,
mod_activity = CASE
WHEN path${depth} = $${paramCounter++} AND data_depth = $${paramCounter++} THEN $${paramCounter++}
ELSE mod_activity
END
WHERE is_removed = false
AND project_id = $${paramCounter++}
AND bucket = $${paramCounter++}
AND storage_type = $${paramCounter++}`;
console.log(queryString);
values.push(oldName); // $1
values.push(newName); // $2
values.push(segment1); // $3 (mod_date, mod_user_id, mod_activity의 WHEN 절에서 사용)
values.push(depth); // $4 (mod_date, mod_user_id, mod_activity의 WHEN 절에서 data_depth와 비교)
values.push(dateNow); // $5
values.push(segment1); // $6 (segment1이 다시 사용되지만, 다른 위치이므로 새로운 파라미터 번호 할당)
values.push(depth); // $7 (depth가 다시 사용되지만, 다른 위치이므로 새로운 파라미터 번호 할당)
values.push(userId); // $8
values.push(segment1); // $9 (segment1이 다시 사용되지만, 다른 위치이므로 새로운 파라미터 번호 할당)
values.push(depth); // $10 (depth가 다시 사용되지만, 다른 위치이므로 새로운 파라미터 번호 할당)
values.push(activity); // $11
values.push(projectId); // $12
values.push(bucket); // $13
values.push(storageType); // $14
for (let i = 0; i < depth; i++) {
let num = i + 1;
let segment2 = getPathSegment(resourcePath, num);
if (num === 4) {
queryString += `
AND path${num} IN ($${paramCounter++}, $${paramCounter++}, $${paramCounter++})`;
values.push(segment2);
values.push(`${segment2}_version`);
values.push(`${segment2}_attachment`);
} else {
queryString += `
AND path${num} = $${paramCounter++}`;
values.push(segment2);
}
}
queryString += `;`;
// console.log('========================');
// console.log(queryString);
// console.log('Values:', values); // 생성된 파라미터 값
await client.query(queryString, values);
let result = { message: 'updateDataRename_success' };
return result;
} catch(error) {
console.error(`updateDataRename error:`, error);
} finally {
client.release();
}
}
async function updateDataAuthor(params) {
let { userInfoString, dataIdArr, newAuthorId, newAuthorNm, activity } = params;
let userInfo = JSON.parse(userInfoString);
let userId = userInfo.user_id;
let dateNow = makePostgresTimestamp(Date.now());
const client = await pool.connect();
try {
let placeholders = dataIdArr.map((_, index) => `$${index + 6}`).join(', ');
let queryString = `
UPDATE ver4.${tbData}
SET
author_id = $1,
author_nm = $2,
mod_date = $3,
mod_user_id = $4,
mod_activity = $5
WHERE is_removed = false
AND data_id IN (${placeholders});
`;
let values = [newAuthorId, newAuthorNm, dateNow, userId, activity, ...dataIdArr];
await client.query(queryString, values);
let result = { message: 'updateDataAuthor_success' };
return result;
} catch(error) {
console.error("updateDataAuthor err:", error);
}finally {
client.release();
}
}
async function updateDataRelocate(params) {
let { projectId, userInfoString, storageType, fromPathArr, toPathArr, dataIdArr, activity } = params;
let bucket = projectId;
let userInfo = JSON.parse(userInfoString);
let userId = userInfo.user_id;
let dateNow = makePostgresTimestamp(Date.now());
const getPathSegments = (path) => path.split('/').filter(Boolean);
const extractFileName = (path4) => {
const match = path4.match(/^(.*?)(?:_attachment|_version)?$/);
return match ? match[1] : path4;
};
const getNewSegment = (oldSegment, fromBase, toBase) => {
if (oldSegment === fromBase) return toBase;
if (oldSegment === `${fromBase}_attachment`) return `${toBase}_attachment`;
if (oldSegment === `${fromBase}_version`) return `${toBase}_version`;
return oldSegment;
};
const client = await pool.connect();
try {
await client.query('BEGIN');
for (let i = 0; i < fromPathArr.length; i++) {
let from = getPathSegments(fromPathArr[i]);
let to = getPathSegments(toPathArr[i]);
if (from.length == 5 && to.length == 4) {
// depth5 추가(버전/첨부) 파일을 depth4로 이동하는 경우
let dataId = dataIdArr[i];
for (let j = 0; j < 4; j++) {
to.push(null);
}
let query = `
UPDATE ver4.${tbData}
SET
data_depth = $1,
path1 = $2,
path2 = $3,
path3 = $4,
path4 = $5,
path5 = $6,
path6 = $7,
path7 = $8,
path8 = $9,
mod_date = $10,
mod_user_id = $11,
mod_activity = $12
WHERE is_removed = false
AND project_id = $13
AND bucket = $14
AND storage_type = $15
AND data_id = $16
`;
let values = [4, ...to, dateNow, userId, activity, projectId, bucket, storageType, dataId];
await client.query(query, values);
} else {
// depth4 파일을 depth4 그대로 이동하는 경우
let fromBase = extractFileName(from[3]); // depth4
let toBase = extractFileName(to[3]); // depth4
let baseUpdate = async (path4, depth) => {
let fromPath = [...from];
let toPath = [...to];
// path4 치환
fromPath[3] = path4;
toPath[3] = getNewSegment(path4, fromBase, toBase);
let values = [
toPath[0], toPath[1], toPath[2], toPath[3],
path4, dateNow,
path4, userId,
path4, activity,
projectId, bucket, storageType
];
let query = `
UPDATE ver4.${tbData}
SET
path1 = $1,
path2 = $2,
path3 = $3,
path4 = $4,
mod_date = CASE WHEN path4 = $5 AND data_depth = ${depth} THEN $6 ELSE mod_date END,
mod_user_id = CASE WHEN path4 = $7 AND data_depth = ${depth} THEN $8 ELSE mod_user_id END,
mod_activity = CASE WHEN path4 = $9 AND data_depth = ${depth} THEN $10 ELSE mod_activity END
WHERE is_removed = false
AND project_id = $11
AND bucket = $12
AND storage_type = $13
AND path1 = $14
AND path2 = $15
AND path3 = $16
AND path4 = $17
`;
values.push(fromPath[0], fromPath[1], fromPath[2], fromPath[3]);
await client.query(query, values);
};
// depth4 원본 파일 이동
await baseUpdate(from[3], 4);
// depth4 _attachment 이동 (있을 경우만)
await baseUpdate(`${fromBase}_attachment`, 4);
// depth4 _version 이동 (있을 경우만)
await baseUpdate(`${fromBase}_version`, 4);
// depth5 첨부/버전 내부 파일 이동
let attachmentTypes = ['_attachment', '_version'];
for (let suffix of attachmentTypes) {
let original = `${fromBase}${suffix}`;
let renamed = `${toBase}${suffix}`;
let values = [
to[0], to[1], to[2], renamed,
original, dateNow,
original, userId,
original, activity,
projectId, bucket, storageType,
from[0], from[1], from[2], original
];
let query = `
UPDATE ver4.${tbData}
SET
path1 = $1,
path2 = $2,
path3 = $3,
path4 = $4,
path5 = path5,
mod_date = CASE WHEN path4 = $5 AND data_depth = 5 THEN $6 ELSE mod_date END,
mod_user_id = CASE WHEN path4 = $7 AND data_depth = 5 THEN $8 ELSE mod_user_id END,
mod_activity = CASE WHEN path4 = $9 AND data_depth = 5 THEN $10 ELSE mod_activity END
WHERE is_removed = false
AND project_id = $11
AND bucket = $12
AND storage_type = $13
AND path1 = $14
AND path2 = $15
AND path3 = $16
AND path4 = $17
`;
await client.query(query, values);
}
}
}
await client.query('COMMIT');
return { message: 'updateDataRelocate_success' };
} catch (err) {
await client.query('ROLLBACK');
console.error('updateDataRelocate error:', err);
throw err;
} finally {
client.release();
}
}
async function updateDataRemove(params) {
let { projectId, userInfoString, storageType, resourcePathArr, activity } = params;
let bucket = projectId;
let userInfo = JSON.parse(userInfoString);
let userId = userInfo.user_id;
let dateNow = makePostgresTimestamp(Date.now());
const client = await pool.connect();
try {
await client.query('BEGIN');
let values = [];
let paramCounter = 1;
let matchingConditionsArray = [];
for (const resourcePath of resourcePathArr) {
let depth = getDepth(resourcePath);
let condition = `data_depth >= $${paramCounter++}`;
values.push(depth);
for (let i = 0; i < depth; i++) {
let num = i + 1;
let segment = getPathSegment(resourcePath, num);
if (num === 4) {
condition += ` AND path${num} IN ($${paramCounter++}, $${paramCounter++}, $${paramCounter++})`;
values.push(segment);
values.push(`${segment}_version`);
values.push(`${segment}_attachment`);
} else {
condition += ` AND path${num} = $${paramCounter++}`;
values.push(segment);
}
}
matchingConditionsArray.push(`(${condition})`);
}
let matchingConditions = matchingConditionsArray.join(' OR ');
// UPDATE 쿼리 (RETURNING data_id)
const updateQueryString = `
UPDATE ver4.${tbData}
SET
is_removed = true,
mod_date = $${paramCounter++},
mod_user_id = $${paramCounter++},
mod_activity = $${paramCounter++}
WHERE data_id IN (
SELECT data_id
FROM ver4.${tbData}
WHERE is_removed = false
AND project_id = $${paramCounter++}
AND bucket = $${paramCounter++}
AND storage_type = $${paramCounter++}
AND (${matchingConditions})
)
RETURNING data_id;
`;
values.push(dateNow, userId, activity, projectId, bucket, storageType);
const updateResult = await client.query(updateQueryString, values);
const updatedDataIds = updateResult.rows.map(row => row.data_id);
if (updatedDataIds.length > 0) {
// DELETE 쿼리 (is_folder = true인 것만)
const deleteQueryString = `
DELETE FROM ver4.${tbData}
WHERE data_id = ANY($1) AND is_folder = true;
`;
await client.query(deleteQueryString, [updatedDataIds]);
}
await client.query('COMMIT');
return { message: 'updateDataRemove_success' };
} catch (error) {
await client.query('ROLLBACK');
console.error(`updateDataRemove error:`, error);
throw error;
} finally {
client.release();
}
}
// async function updateDataRemove(params) {
// let { projectId, userInfoString, storageType, resourcePathArr, activity } = params;
// let bucket = projectId;
// let userInfo = JSON.parse(userInfoString);
// let userId = userInfo.user_id;
// let dateNow = makePostgresTimestamp(Date.now());
// const client = await pool.connect();
// try {
// let values = [];
// let paramCounter = 1;
// let matchingConditionsArray = [];
// for (const resourcePath of resourcePathArr) {
// let depth = getDepth(resourcePath);
// let condition = `data_depth >= $${paramCounter++}`;
// values.push(depth); // data_depth 파라미터 추가
// for (let i = 0; i < depth; i++) {
// let num = i + 1;
// let segment = getPathSegment(resourcePath, num);
// if (num === 4) {
// condition += ` AND path${num} IN ($${paramCounter++}, $${paramCounter++}, $${paramCounter++})`;
// values.push(segment);
// values.push(`${segment}_version`);
// values.push(`${segment}_attachment`);
// } else {
// condition += ` AND path${num} = $${paramCounter++}`;
// values.push(segment);
// }
// }
// matchingConditionsArray.push(`(${condition})`);
// }
// let matchingConditions = matchingConditionsArray.join(' OR ');
// let queryString = `
// WITH matching_rows AS (
// SELECT data_id
// FROM ver4.${tbData}
// WHERE is_removed = false
// AND project_id = $${paramCounter++}
// AND bucket = $${paramCounter++}
// AND storage_type = $${paramCounter++}
// AND (${matchingConditions})
// )
// UPDATE ver4.${tbData} AS t
// SET
// is_removed = true,
// mod_date = $${paramCounter++},
// mod_user_id = $${paramCounter++},
// mod_activity = $${paramCounter++}
// FROM matching_rows m
// WHERE t.data_id = m.data_id;
// `;
// values.push(projectId);
// values.push(bucket);
// values.push(storageType);
// values.push(dateNow);
// values.push(userId);
// values.push(activity);
// // console.log('======================');
// // console.log(queryString);
// // console.log(values);
// await client.query(queryString, values);
// let result = { message: 'updateDataRemove_success' };
// return result;
// } catch (error) {
// console.error(`updateDataRemove error:`, error);
// } finally {
// client.release();
// }
// }
async function updateDataPermission(params) {
let { projectId, userInfoString, storageType, resourcePath, dataId, activity, dataType, newPermission } = params;
let bucket = projectId;
let userInfo = JSON.parse(userInfoString);
let userId = userInfo.user_id;
let dateNow = makePostgresTimestamp(Date.now());
let depth = getDepth(resourcePath);
const client = await pool.connect();
try {
let segment1 = getPathSegment(resourcePath, depth);
let queryString = `
UPDATE ver4.${tbData}
SET
data_permission = ${newPermission},
mod_date = CASE
WHEN path${depth} = '${segment1}' AND data_depth = ${depth} THEN '${makePostgresTimestamp(dateNow)}'
ELSE mod_date
end,
mod_user_id = CASE
WHEN path${depth} = '${segment1}' AND data_depth = ${depth} THEN '${userId}'
ELSE mod_user_id
END,
mod_activity = CASE
WHEN path${depth} = '${segment1}' AND data_depth = ${depth} THEN '${activity}'
ELSE mod_activity
END
WHERE is_removed = false
AND project_id = '${projectId}'
AND bucket = '${bucket}'
AND storage_type = '${storageType}'`;
for (let i = 0; i < depth; i++) {
let num = i + 1;
let segment2 = getPathSegment(resourcePath, num);
if (num === 4) {
queryString += `
AND path${num} IN ('${segment2}', '${segment2}_version', '${segment2}_attachment')`;
} else {
queryString += `
AND path${num} = '${segment2}'`;
}
}
queryString += `;`;
await client.query(queryString);
let result = { message: 'updateDataPermission_success' };
return result;
} catch (error) {
console.error(`updateDataPermission error:`, error);
} finally {
client.release();
}
}
async function updateDataPosition(params) {
let { dataId, lon, lat } = params;
const client = await pool.connect();
try {
let queryString = `
UPDATE ver4.${tbData}
SET
lon = ${lon},
lat = ${lat}
WHERE data_id = ${dataId}
AND is_removed = false;`;
await client.query(queryString);
let result = { message: 'updateDataPosition_success' };
return result;
} catch (error) {
console.error(`updateDataPosition error:`, error);
} finally {
client.release();
}
}
async function updateLastFolderActDate(arr, inputSeconds) {
let dataIdString = `(${arr.join(',')})`;
let dateNow = Date.now();
let date = makePostgresTimestamp(dateNow);
if (inputSeconds) date = makePostgresTimestamp(new Date(dateNow - (15 * 24 * 60 * 60 * 1000) + (inputSeconds * 1000)));
const client = await pool.connect();
try {
let queryString = `
UPDATE ver4.${tbData}
SET last_folder_act_date = '${date}'
WHERE data_id IN ${dataIdString};
`;
// console.log('@@@@@@@@@@@@@@@@@@@@@@');
// console.log(queryString);
await client.query(queryString);
let result = { message: 'updateLastFolderActDate_success' };
return result;
} catch (err) {
console.error('renewExpiryDate error:', err);
} finally {
client.release();
}
}
async function deleteData(params) {
let { dataIdArr } = params;
const client = await pool.connect();
try {
await client.query('BEGIN');
let placeholders = dataIdArr.map((_, idx) => `$${idx + 1}`).join(', ');
let queryString = `
DELETE FROM ver4.${tbData}
WHERE data_id IN (${placeholders})
`;
await client.query(queryString, dataIdArr);
await client.query('COMMIT');
return { message: 'deleteData_success' };
} catch (err) {
await client.query('ROLLBACK');
console.error('deleteData error:', err);
throw err;
} finally {
client.release();
}
}
//// chat gpt 코드
async function checkTargetExistsAction(params) {
let { projectId, storageType, dataType, resourcePathArr } = params;
let bucket = projectId;
let isFolder = (dataType === 'folder');
resourcePathArr = JSON.parse(resourcePathArr);
const MAX_DEPTH = 8;
const client = await pool.connect();
try {
const pathTuples = [];
for (const path of resourcePathArr) {
const depth = getDepth(path);
const segments = [];
for (let i = 0; i < depth; i++) {
segments.push(getPathSegment(path, i + 1));
}
if (depth === 4) {
['', '_version', '_attachment'].forEach(suffix => {
const padded = [...segments];
padded[3] = padded[3] + suffix;
while (padded.length < MAX_DEPTH) padded.push(null);
pathTuples.push([depth, ...padded]);
});
} else {
while (segments.length < MAX_DEPTH) segments.push(null);
pathTuples.push([depth, ...segments]);
}
}
const colNames = ['data_depth'];
for (let i = 1; i <= MAX_DEPTH; i++) {
colNames.push(`path${i}`);
}
const bindValues = [];
let bindIndex = 1;
const valueRows = pathTuples.map(tuple => {
const rowPlaceholders = tuple.map((_, idx) => {
const typeCast = (idx === 0) ? '::int' : '::text';
return `$${bindIndex++}${typeCast}`;
});
bindValues.push(...tuple);
return `(${rowPlaceholders.join(', ')})`;
});
// NULL-safe 비교 조건 생성
const joinConditions = colNames.map(c => `t.${c} IS NOT DISTINCT FROM v.${c}`).join(' AND ');
// console.log('##################################');
// console.log(valueRows);
if (valueRows.length === 0) {
return {
message: 'checkTargetExistsAction_success',
rows: []
};
}
const query = `
SELECT
t.data_id, t.project_id, t.user_id, t.create_date, t.data_permission, t.bucket, t.is_folder, t.is_removed, t.data_depth,
ext, t.path1, t.path2, t.path3, t.path4, t.path5, t.path6, t.path7, t.path8, t.mod_date, t.mod_user_id, t.mod_activity,
data_size, t.memo, t.storage_type, t.object_key, t.preview_key, t.popup_key, t.ver
FROM ver4.${tbData} t
JOIN (
VALUES ${valueRows.join(',\n')}
) AS v(${colNames.join(', ')})
ON ${joinConditions}
WHERE t.project_id = $${bindIndex++}::text
AND t.bucket = $${bindIndex++}::text
AND t.storage_type = $${bindIndex++}::text
AND t.is_folder = $${bindIndex++}::bool
AND t.is_removed = false::bool
`;
bindValues.push(projectId, bucket, storageType, isFolder);
const { rows } = await client.query(query, bindValues);
return {
message: 'checkTargetExistsAction_success',
rows
};
} catch (error) {
console.error("checkTargetExistsAction err:", error);
} finally {
client.release();
}
}
async function cleanUpExistingData(existingDataIdArr) {
let existingDataIdString;
if (existingDataIdArr.length == 1) existingDataIdString = existingDataIdArr[0];
if (existingDataIdArr.length > 1) existingDataIdString = existingDataIdArr.join(',');
const client = await pool.connect();
try {
await client.query('BEGIN');
// object_key, preview_key, popup_key select 조회 후 row 삭제
let selectQueryString = `
SELECT object_key, preview_key, popup_key, thumbnail_key
FROM ver4.${tbData}
WHERE data_id in (${existingDataIdString});
`;
let deleteQueryString = `
DELETE FROM ver4.${tbData}
WHERE data_id IN (${existingDataIdString});
`;
// console.log('@@@@@@@@@@@@@ selectQueryString');
// console.log(selectQueryString);
// console.log('############ deleteQueryString');
// console.log(deleteQueryString);
let { rows } = await client.query(selectQueryString);
await client.query(deleteQueryString);
await client.query('COMMIT');
return {
message: 'cleanUpExistingData_success',
rows: rows
}
} catch(error) {
console.error("cleanUpExistingData err:", error);
} finally {
client.release();
}
}
async function getFilesCount(projectId, storageType, resourcePath) {
let depth = getDepth(resourcePath);
const client = await pool.connect();
try {
//// chat gpt 코드 - path1부터 path8까지 전체비교해서 중복되는 row 있는 경우 한 개만 카운트
// 1. 내부 서브쿼리 생성 (ROW_NUMBER로 path 중복 제거)
let innerQuery = `
SELECT
ROW_NUMBER() OVER (
PARTITION BY path1, path2, path3, path4, path5, path6, path7, path8
ORDER BY mod_date DESC
) AS row_num
FROM ver4.${tbData}
WHERE project_id = '${projectId}'
AND bucket = '${projectId}'
AND storage_type = '${storageType}'
AND is_folder = false
AND is_removed = false
AND data_depth >= 4
`;
for (let i = 0; i < depth; i++) {
let num = i + 1;
innerQuery += ` AND path${num} = '${getPathSegment(resourcePath, num)}'`;
}
// 2. 바깥쪽 쿼리에서 row_num = 1만 카운트
let queryString = `
SELECT count(*)
FROM (
${innerQuery}
) AS sub
WHERE sub.row_num = 1;
`;
////////////////////////
// console.log('==================');
// console.log(queryString);
let { rows } = await client.query(queryString);
return rows[0].count;
} catch (error) {
console.error("getFilesCount err:", error);
} finally {
client.release();
}
}
// async function getObjectKeyTimestamp(params) {
// const client = await pool.connect();
// try {
// let queryString = `
// SELECT object_key
// FROM ver4.${tbData}
// WHERE data_id = '${params.dataId}';
// `;
// let { rows } = await client.query(queryString);
// let objectKey = rows[0].object_key;
// let timestamp = objectKey.split('__')[1];
// return { rows: rows[0], objectKey: objectKey, timestamp: timestamp };
// } catch(error) {
// console.error("getFilesCount err:", error)
// } finally {
// client.release();
// }
// }
async function ensureAddOnFolderAction(params) {
let resourcePath = params.resourcePath;
let path1 = getPathSegment(resourcePath, 1);
let path2 = getPathSegment(resourcePath, 2);
let path3 = getPathSegment(resourcePath, 3);
let path4 = getPathSegment(resourcePath, 4);
const client = await pool.connect();
try {
let values = [];
let paramCounter = 1;
let queryString = `
SELECT
data_id, project_id, user_id, create_date, data_permission, bucket, is_folder, is_removed, data_depth,
ext, path1, path2, path3, path4, path5, path6, path7, path8, mod_date, mod_user_id, mod_activity,
data_size, memo, storage_type, object_key, preview_key, popup_key, ver
FROM ver4.${tbData}
WHERE project_id = $${paramCounter++}
AND bucket = $${paramCounter++}
AND storage_type = $${paramCounter++}
AND is_folder = true
AND is_removed = false
AND data_depth = $${paramCounter++}
AND path1 = $${paramCounter++}
AND path2 = $${paramCounter++}
AND path3 = $${paramCounter++}
AND path4 = $${paramCounter++};`;
values.push(params.projectId); // $1
values.push(params.projectId); // $2 (bucket 값)
values.push(params.storageType); // $3
values.push(4); // $4 (data_depth 값)
values.push(path1); // $5
values.push(path2); // $6
values.push(path3); // $7
values.push(path4); // $8
// console.log('========================');
// console.log(queryString);
// console.log('Values:', values);
let { rows } = await client.query(queryString, values);
if (rows[0]) {
return 'check_addOnFolder_success';
} else {
let result = await insertData(params);
return 'create_addOnFolder_success';
}
} catch(error) {
console.error("ensureAddOnFolderAction err:", error)
} finally {
client.release();
}
}
async function getDataInfoAction(params) {
console.log('');
console.log('===========================');
console.log(`async function getDataInfoAction (${makePostgresTimestamp()})`);
console.log(params.debug);
console.log('===========================');
console.log('');
let { projectId, storageType, dataIdArr, isRemoved } = params;
let bucket = projectId;
const client = await pool.connect();
try {
if (!Array.isArray(dataIdArr) || dataIdArr.length === 0) return [];
let dataIdPlaceholders = dataIdArr.map((_, i) => `$${i + 1}`).join(', ');
let bindStartIndex = dataIdArr.length + 1;
let queryString = `
SELECT
data_id, project_id, user_id, create_date, data_permission, bucket, is_folder, is_removed, data_depth,
ext, path1, path2, path3, path4, path5, path6, path7, path8, mod_date, mod_user_id, mod_activity,
data_size, memo, storage_type, object_key, preview_key, popup_key, thumbnail_key, ver, lon, lat, height, ai_summary
FROM ver4.${tbData}
WHERE project_id = $${bindStartIndex}
AND bucket = $${bindStartIndex + 1}
AND storage_type = $${bindStartIndex + 2}
AND is_removed = $${bindStartIndex + 3}
AND data_id IN (${dataIdPlaceholders});
`;
let values = [...dataIdArr, projectId, bucket, storageType, isRemoved];
let { rows } = await client.query(queryString, values);
return rows;
} catch(error) {
console.error("getDataInfoAction err:", error)
} finally {
client.release();
}
}
async function getFolderSizeAction(projectId, storageType) {
let result = [];
const client = await pool.connect();
try {
let queryString = `
SELECT
COALESCE(archive.recycle_bin_size, 0) AS "recycle_bin/size",
COALESCE(archive.recycle_bin_count, 0) AS "recycle_bin/count",
COALESCE(archive.data_size, 0) AS "archive/origin/size",
COALESCE(archive.data_count, 0) AS "archive/origin/count",
COALESCE(archive.popup_size, 0) AS "archive/pdf/size",
COALESCE(archive.popup_count, 0) AS "archive/pdf/count",
COALESCE(archive.preview_size, 0) AS "archive/pdf_thumb/size",
COALESCE(archive.preview_count, 0) AS "archive/pdf_thumb/count",
COALESCE(official_doc.data_size, 0) AS "official_doc/origin/size",
COALESCE(official_doc.data_count, 0) AS "official_doc/origin/count",
COALESCE(official_doc.popup_size, 0) AS "official_doc/pdf/size",
COALESCE(official_doc.popup_count, 0) AS "official_doc/pdf/count",
COALESCE(overview.data_size, 0) AS "overview/size",
COALESCE(overview.data_count, 0) AS "overview/count"
FROM
(
SELECT
SUM(CASE WHEN is_removed = true THEN data_size ELSE 0 END) AS recycle_bin_size,
COUNT(CASE WHEN is_removed = true THEN 1 END) AS recycle_bin_count,
SUM(CASE WHEN is_removed = false THEN data_size ELSE 0 END) AS data_size,
COUNT(CASE WHEN is_removed = false THEN 1 END) AS data_count,
SUM(popup_size) AS popup_size,
COUNT(CASE WHEN popup_size > 0 THEN 1 END) AS popup_count,
SUM(preview_size) AS preview_size,
COUNT(CASE WHEN preview_size > 0 THEN 1 END) AS preview_count
FROM ver4.${tbData}
WHERE project_id = '${projectId}'
AND bucket = '${projectId}'
AND storage_type = '${storageType}'
AND is_folder = false
) AS archive,
(
SELECT
SUM(data_size) AS data_size,
COUNT(CASE WHEN data_size > 0 THEN 1 END) AS data_count,
SUM(popup_size) AS popup_size,
COUNT(CASE WHEN popup_size > 0 THEN 1 END) AS popup_count
FROM ver4.tb_official_doc_file
WHERE project_id = '${projectId}'
AND bucket = '${projectId}'
AND storage_type = '${storageType}'
) AS official_doc,
(
SELECT
SUM(value::bigint) AS data_size,
COUNT(CASE WHEN value::bigint > 0 THEN 1 END) AS data_count
FROM ver4.tb_overview
LEFT JOIN LATERAL json_array_elements_text(data_size::json) AS t(value) ON true
WHERE project_id = '${projectId}' AND data_size IS NOT NULL
) AS overview
`;
let { rows } = await client.query(queryString);
let keys = Object.keys(rows[0]);
let tempArr = [];
for(let i = 0; i < keys.length; i++) {
let key = keys[i];
let splitKey = key.split('/');
splitKey.pop();
tempArr.push(splitKey.join('/'));
}
tempArr = new Set(tempArr);
tempArr = Array.from(tempArr);
for(let i = 0; i < tempArr.length; i++) {
let item = tempArr[i];
let obj = {};
obj['key'] = item;
obj['size'] = rows[0][`${item}/size`];
obj['count'] = rows[0][`${item}/count`];
result.push(obj);
}
return result;
} catch(error) {
console.error("getFolderSizeAction err:", error);
return [];
} finally {
client.release();
}
}
async function selectControlBoxPosition(userId) {
const client = await pool.connect();
try {
let queryString = `
SELECT user_id, user_nm, company, dept, position, floating_box_position
FROM ver4.tb_user
WHERE user_id = $1
`;
let values = [userId];
let result = await client.query(queryString, values);
return result.rows;
} catch(error) {
console.error("selectControlBoxPosition err:", error);
} finally {
client.release();
}
}
async function updateControlBoxPosition(params) {
let { userId, positionData } = params;
const client = await pool.connect();
try {
let queryString = `
UPDATE ver4.tb_user
SET floating_box_position = $1
WHERE user_id = $2
`;
let values = [positionData, userId];
await client.query(queryString, values);
let result = { message: 'updateControlBoxPosition_success' };
return result;
} catch(error) {
console.error("updateControlBoxPosition err:", error);
} finally {
client.release();
}
}
// 🔺🔺🔺🔺🔺🔺🔺🔺 DB 관련 함수 끝 🔺🔺🔺🔺🔺🔺🔺🔺
// 🔻🔻🔻🔻🔻🔻🔻🔻 비즈니스 로직 함수 시작 🔻🔻🔻🔻🔻🔻🔻🔻
async function buildTreeObject(projectId, storageType, userInfo, resourcePath) {
let result = { folder: {}, file: {} };
resourcePath = resourcePath.startsWith('/') ? resourcePath.slice(1) : resourcePath;
try {
let folder = {}, file = {};
let dataRows = await selectData(projectId, storageType, userInfo, resourcePath);
// let removedDataRows = await selectRemovedData(projectId, storageType, userInfo, resourcePath);
for (let i = 0; i < dataRows.length; i++) {
let row = dataRows[i];
let dataDepth = row.data_depth;
let newResourcePath = '';
for(let j = 0; j < dataDepth; j++) {
newResourcePath += '/' + row[`path${j+1}`];
}
newResourcePath = newResourcePath.replaceAll('//', '/');
let filesCount = 0;
if (dataDepth <= 3) {
filesCount = await getFilesCount(projectId, storageType, newResourcePath);
}
if (row.is_folder) {
let child = (dataDepth >= 2) ? await buildTreeObject(projectId, storageType, userInfo, newResourcePath) : { folder: {}, file: {} };
// depth4 추가 폴더dp 파일이 비어있는 경우 해당 row는 treeObject에서 제외
if (dataDepth == 4 && Object.values(child.folder).length == 0 && Object.values(child.file).length == 0) {
continue;
}
folder[row[`path${dataDepth}`]] = {
type: 'folder',
name: row[`path${dataDepth}`],
// child: (dataDepth >= 2) ? await buildTreeObject(projectId, storageType, userInfo, newResourcePath) : { folder: {}, file: {} },
child: child,
filesCount: filesCount,
permission: row.data_permission,
userId: row.user_id,
userNm: row.user_nm,
company: row.company,
dept: row.dept,
position: row.position,
createDate: row.create_date,
size: row.data_size,
memo: row.memo,
resourcePath: newResourcePath,
depth: dataDepth,
modDate: row.mod_date,
modUserId: row.mod_user_id,
modUserNm: row.mod_user_nm,
modActivity: row.mod_activity,
storageType: row.storage_type,
bucket: row.bucket,
objectKey: row.object_key,
dataId: Number(row.data_id),
folderType: row.folder_type,
lastFolderActDate: row.last_folder_act_date
}
} else {
if (dataDepth == getDepth(newResourcePath)) {
// 지원여부, 변환필요, 변환완료 변수 생성
let isSupported = false, needConvert = false, isConverted = false;
// 현재 파일 확장자 변수 생성
let ext = (row.ext).toLowerCase();
// 현재 파일 확장자가 지원여부 확장자 배열에 포함되어 있는지 확인해서 isSupported에 저장
if (supportedExtArr.includes(ext)) isSupported = true;
// 현재 파일 확장자가 변환필요 확장자 배열에 포함되어 있는지 확인해서 needConvert에 저장
if (needConvertExtArr.includes(ext)) needConvert = true;
// 변환이 필요한 확장자일 때
if (needConvert) {
// object_key, preview_key, popup_key에서 파일 이름 추출 후 비교해서
// object_keyd의 파일이름과 preview_key, popup_key 각각의 파일이름이 동일하면 isConverted true로 설정
// let objectKeyFileName = getFileNameFromKey(row.object_key);
// let previewKeyFileName = getFileNameFromKey(row.preview_key);
// let popupKeyFileName = getFileNameFromKey(row.popup_key);
// if (objectKeyFileName == previewKeyFileName && objectKeyFileName == popupKeyFileName) {
if (row.preview_key && row.popup_key) {
isConverted = true;
} else {
isConverted = false;
}
}
let subCategory, mainFileName;
if (row.data_depth == 4) mainFileName = row.path4;
if (row.data_depth == 5) {
let path4Split = row.path4.split('_');
subCategory = path4Split.pop();
mainFileName = path4Split.join('_');
}
file[row[`path${dataDepth}`]] = {
type: 'file',
name: row[`path${dataDepth}`],
isSupported: isSupported,
needConvert: needConvert,
isConverted: isConverted,
permission: row.data_permission,
userId: row.user_id,
userNm: row.user_nm,
company: row.company,
dept: row.dept,
position: row.position,
createDate: row.create_date,
size: row.data_size,
memo: row.memo,
resourcePath: newResourcePath,
ext: row.ext,
depth: dataDepth,
modDate: row.mod_date,
modUserId: row.mod_user_id,
modUserNm: row.mod_user_nm,
modActivity: row.mod_activity,
storageType: row.storage_type,
bucket: row.bucket,
objectKey: row.object_key,
previewKey: row.preview_key,
thumbnailKey: row.thumbnail_key,
dataId: Number(row.data_id),
lon: row.lon,
lat: row.lat,
height: row.height,
authorId: row.author_id,
authorNm: row.author_nm,
mainFileName: mainFileName,
subCategory: subCategory
}
}
}
result = { folder: folder, file: file };
}
} catch (err) {
console.log('---------------- buildTreeObject 실패');
console.log(err);
}
return await result;
}
async function buildRecycleBinObject(projectId, storageType, userInfo) {
let result = { recycleBin: {} };
try {
// let recycleBin = {};
let removedDataRows = await selectRemovedData(projectId, storageType, userInfo);
for (let i = 0; i < removedDataRows.length; i++) {
let row = removedDataRows[i];
let dataDepth = row.data_depth;
let newResourcePath = '';
for(let j = 0; j < dataDepth; j++) {
newResourcePath += '/' + row[`path${j+1}`];
}
newResourcePath = newResourcePath.replaceAll('//', '/');
// 지원여부, 변환필요, 변환완료 변수 생성
let isSupported = false, needConvert = false, isConverted = false;
// 현재 파일 확장자 변수 생성
let ext = (row.ext).toLowerCase();
// 현재 파일 확장자가 지원여부 확장자 배열에 포함되어 있는지 확인해서 isSupported에 저장
if (supportedExtArr.includes(ext)) isSupported = true;
// 현재 파일 확장자가 변환필요 확장자 배열에 포함되어 있는지 확인해서 needConvert에 저장
if (needConvertExtArr.includes(ext)) needConvert = true;
// 변환이 필요한 확장자일 때
if (needConvert) {
// object_key, preview_key, popup_key에서 파일 이름 추출 후 비교해서
// object_keyd의 파일이름과 preview_key, popup_key 각각의 파일이름이 동일하면 isConverted true로 설정
// let objectKeyFileName = getFileNameFromKey(row.object_key);
// let previewKeyFileName = getFileNameFromKey(row.preview_key);
// let popupKeyFileName = getFileNameFromKey(row.popup_key);
// if (objectKeyFileName == previewKeyFileName && objectKeyFileName == popupKeyFileName) {
if (row.preview_key && row.popup_key) {
isConverted = true;
} else {
isConverted = false;
}
}
result.recycleBin[`${row[`path${dataDepth}`]}___[recycle-bin]___${i}`] = {
type: 'file',
name: row[`path${dataDepth}`],
isSupported: isSupported,
needConvert: needConvert,
isConverted: isConverted,
permission: row.data_permission,
userId: row.user_id,
userNm: row.user_nm,
company: row.company,
dept: row.dept,
position: row.position,
createDate: row.create_date,
size: row.data_size,
memo: row.memo,
resourcePath: newResourcePath,
ext: row.ext,
depth: dataDepth,
modDate: row.mod_date,
modUserId: row.mod_user_id,
modUserNm: row.mod_user_nm,
modActivity: row.mod_activity,
storageType: row.storage_type,
bucket: row.bucket,
objectKey: row.object_key,
previewKey: row.preview_key,
dataId: Number(row.data_id),
authorId: row.author_id,
authorNm: row.author_nm,
}
}
// result.recycleBin = recycleBin;
} catch {
console.log(111);
console.log('---------------- buildRecycleBinObject 실패');
}
return await result;
}
async function deleteTargetAction(keyArr, bucket) {
for (let key of keyArr) {
const command = new DeleteObjectCommand({
Bucket: bucket,
Key: key,
});
await s3.send(command);
// console.log(`⚠ 삭제중 - ${key}`);
// socket 보내서 클라이언트에서 몇 개중에 몇개 삭제됐는지 프로그레스 표시?
}
}
// 🔺🔺🔺🔺🔺🔺🔺🔺 비즈니스 로직 함수 끝 🔺🔺🔺🔺🔺🔺🔺🔺
// 🔻🔻🔻🔻🔻🔻🔻🔻 클라이언트 요청 핸들러 시작 🔻🔻🔻🔻🔻🔻🔻🔻
exports.getTreeObject = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.query;
let { userInfoString, storageType, resourcePath } = params;
let userInfo = JSON.parse(userInfoString);
resourcePath = resourcePath.startsWith('/') ? resourcePath.slice(1) : resourcePath;
// if (resourcePath == '/') resourcePath = '';
let currentTreeObject = await buildTreeObject(projectId, storageType, userInfo, resourcePath);
console.log('');
console.log(`---------------- buildTreeObject 성공 - ${userInfo.user_nm} ${userInfo.position} (projectId: ${projectId} / resourcePath: '${resourcePath}')`);
console.log('');
let message = 'getTreeObject_success';
if (!currentTreeObject) message = 'getTreeObject_failed';
res.status(200).json({
message: message,
currentTreeObject: currentTreeObject,
// allTreeObject: allTreeObject,
convertingDataArr: convertingDataArr,
});
}
exports.getRecycleBinObject = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.query;
let { userInfoString, storageType, resourcePath } = params;
let userInfo = JSON.parse(userInfoString);
let recycleBinObject = await buildRecycleBinObject(projectId, storageType, userInfo);
console.log('');
console.log(`---------------- getRecycleBinObject 성공 - ${userInfo.user_nm} ${userInfo.position} (projectId: ${projectId} / resourcePath: '${resourcePath}')`);
console.log('');
let message = 'getRecycleBinObject_success';
if (!recycleBinObject) message = 'getRecycleBinObject_failed';
res.status(200).json({
message: message,
recycleBinObject: recycleBinObject
});
}
exports.getFolderSize = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.query;
let { storageType } = params;
try {
let getFolderSizeResult = await getFolderSizeAction(projectId, storageType);
res.status(200).json({
message: 'getFolderSize_success',
result: getFolderSizeResult || []
});
} catch(error) {
console.error("exports.getFolderSize err:", error);
res.status(200).json({
message: 'getFolderSize_failed',
result: []
});
}
}
exports.getLog = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let selectLogResult = await selectLog(projectId);
res.status(200).json({
message: 'getLog_success',
logData: selectLogResult,
});
}
exports.getUserLog = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let selectLogResult = await selectUserLog(projectId);
res.status(200).json({
message: 'getUserLog_success',
logData: selectLogResult,
});
}
exports.getFilterLog = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
const params = req.query;
let selectLogResult = await selectLog(projectId, params);
res.status(200).json({
message: 'getFilterLog_success',
logData: selectLogResult,
});
}
exports.checkTargetExists = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
let checkTargetExistsActionResult = await checkTargetExistsAction(params);
if (checkTargetExistsActionResult.message == 'checkTargetExistsAction_success') {
res.status(200).json({
message: 'checkTargetExists_success',
rows: checkTargetExistsActionResult.rows
});
}
}
exports.createFolder = async (req, res) => {
try {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
let activity = 'createFolder';
let folderType = params.folderType;
if (folderType) activity = `${activity}-${folderType}`;
params.activity = activity;
let insertDataResult = await insertData(params);
if (insertDataResult && insertDataResult.message == 'insertData_success') {
let dataIdArr = [];
for (let i = 0; i < insertDataResult.rows.length; i++) {
let row = insertDataResult.rows[i];
dataIdArr.push(row.data_id);
}
params.dataIdArr = dataIdArr;
params.userIp = req.ip;
let insertLogResult = await insertLog(params);
if (insertLogResult && insertLogResult.message == 'insertLog_success') {
let resultData = {
message: 'createFolder_success',
projectId: projectId,
activity: activity,
resourcePath: params.resourcePathArr[0]
};
let io = getIo();
io.emit('createFolder_success', resultData);
return res.status(200).json({
message: 'createFolder_success',
});
}
}
res.status(500).json({
message: 'createFolder_failed',
error: '폴더 생성 중 오류가 발생했습니다.'
});
} catch (error) {
console.error("createFolder error:", error);
res.status(500).json({
message: 'createFolder_failed',
error: error.message
});
}
}
exports.generateUploadUrl = async (req, res, next) => {
const projectId = req.baseUrl.split('/')[1];
const pageType = req.baseUrl.split('/')[2];
let { resourcePath, date, needsThumbnail, thumbnailPath } = req.body;
let bucket = projectId;
// 실시간 권한 조회 검사 (Viewer 권한자 업로드 차단)
const userGroup = req.user?.group;
let userPermission = null;
if (userGroup === 'super' || userGroup === 'dev' || userGroup === 'USER_GROUP_super') {
userPermission = 1535;
} else if (req.user?.user_id) {
const client = await pool.connect();
try {
const queryStr = `
SELECT
CASE
WHEN EXISTS (SELECT 1 FROM ver4.${tbProject} WHERE project_id = $1 AND user_id = $2)
THEN 255
ELSE (
SELECT lev
FROM ver4.${tbPermission}
WHERE project_id = $1 AND user_id = $2
)
END as lev
`;
const checkRes = await client.query(queryStr, [projectId, req.user.user_id]);
userPermission = checkRes.rows[0]?.lev || null;
} catch (dbErr) {
console.error("❌ [Upload Permission Check] DB Error:", dbErr);
userPermission = null;
} finally {
client.release();
}
}
if (userPermission === null || userPermission <= 1) {
return res.status(200).json({
message: 'generateUploadUrl_failed_permission',
error: '파일 업로드 권한이 없습니다.'
});
}
// ONPREMISE(MinIO) 환경에서 버킷이 유실되거나 자동 생성되지 않은 경우를 대비한 동적 생성 처리
if (deploymentType === 'ONPREMISE') {
try {
const { CreateBucketCommand, HeadBucketCommand } = require('@aws-sdk/client-s3');
try {
await s3.send(new HeadBucketCommand({ Bucket: bucket }));
} catch (headErr) {
// NoSuchBucket 등 에러(404) 발생 시 생성 시도
if (headErr.name === 'NotFound' || headErr.$metadata?.httpStatusCode === 404) {
console.log(`[generateUploadUrl] 🚀 Bucket '${bucket}' not found on MinIO. Creating...`);
await s3.send(new CreateBucketCommand({ Bucket: bucket }));
console.log(`[generateUploadUrl] ✔️ Bucket '${bucket}' created successfully.`);
} else {
throw headErr;
}
}
} catch (bucketErr) {
console.error(`[generateUploadUrl] ⚠️ Failed to verify/create MinIO bucket '${bucket}':`, bucketErr);
}
}
let originFullPath = getBasePrefix(pageType).origin + resourcePath;
originFullPath = originFullPath.replaceAll('\\', '/');
originFullPath = originFullPath.replaceAll('//', '/');
let objectKey = `${originFullPath}__${makeObjectKeyTimestamp(date)}`;
let originUrl;
try {
const command = new PutObjectCommand({
Bucket: bucket,
Key: objectKey,
ContentType: 'application/octet-stream'
});
originUrl = await getSignedUrl(s3, command, { expiresIn: 60 * 60 * 24 });
} catch (error) {
console.error('❌ Upload Presigned URL 생성 실패:', error);
}
let thumbnailUrl = null, thumbnailKey = null;
if (needsThumbnail) {
let thumbnailFullPath = getBasePrefix(pageType).thumbnail + thumbnailPath;
thumbnailFullPath = thumbnailFullPath.replaceAll('\\', '/');
thumbnailFullPath = thumbnailFullPath.replaceAll('//', '/');
thumbnailKey = `${thumbnailFullPath}__${makeObjectKeyTimestamp(date)}`;
try {
const command = new PutObjectCommand({
Bucket: bucket,
Key: thumbnailKey,
ContentType: 'application/octet-stream'
});
thumbnailUrl = await getSignedUrl(s3, command, { expiresIn: 60 * 60 * 24 });
} catch (error) {
console.error('❌ Upload Presigned URL 생성 실패:', error);
}
}
res.status(200).json({
message: 'generateUploadUrl_success',
result: {
originUrl: originUrl,
objectKey: objectKey,
date: date,
thumbnailUrl: thumbnailUrl,
thumbnailKey: thumbnailKey
}
});
}
exports.uploadData = async (req, res, next) => {
const projectId = req.baseUrl.split('/')[1];
// 실시간 권한 조회 검사 (Viewer 권한자 업로드 차단)
const userGroup = req.user?.group;
let userPermission = null;
if (userGroup === 'super' || userGroup === 'dev' || userGroup === 'USER_GROUP_super') {
userPermission = 1535;
} else if (req.user?.user_id) {
const client = await pool.connect();
try {
const queryStr = `
SELECT
CASE
WHEN EXISTS (SELECT 1 FROM ver4.${tbProject} WHERE project_id = $1 AND user_id = $2)
THEN 255
ELSE (
SELECT lev
FROM ver4.${tbPermission}
WHERE project_id = $1 AND user_id = $2
)
END as lev
`;
const checkRes = await client.query(queryStr, [projectId, req.user.user_id]);
userPermission = checkRes.rows[0]?.lev || null;
} catch (dbErr) {
console.error("❌ [UploadData Permission Check] DB Error:", dbErr);
userPermission = null;
} finally {
client.release();
}
}
if (userPermission === null || userPermission <= 1) {
return res.status(200).json({
message: 'uploadData_failed_permission',
error: '파일 업로드 권한이 없습니다.'
});
}
let { params } = req.body;
params.projectId = projectId;
params.dateArr = JSON.parse(params.dateArr);
params.resourcePathArr = JSON.parse(params.resourcePathArr);
params.sizeArr = JSON.parse(params.sizeArr);
params.objectKeyArr = JSON.parse(params.objectKeyArr);
params.thumbnailSizeArr = JSON.parse(params.thumbnailSizeArr);
params.thumbnailKeyArr = JSON.parse(params.thumbnailKeyArr);
params.existingDataIdArr = JSON.parse(params.existingDataIdArr);
params.coordArr = JSON.parse(params.coordArr);
let activity = `uploadData_${params.dataType}`;
if (params.functionId.includes('addOn_')) activity = params.functionId;
params.activity = activity;
let insertDataResult = await insertData(params);
if (insertDataResult.message == 'insertData_success') {
let dataIdArr = [];
for (let i = 0; i < insertDataResult.rows.length; i++) {
let row = insertDataResult.rows[i];
dataIdArr.push(row.data_id);
}
params.dataIdArr = dataIdArr;
params.userIp = req.ip;
let updateLastFolderActDateResult = await updateLastFolderActDate(params.depth3DataIdArr);
// if (updateLastFolderActDateResult.message == 'updateLastFolderActDate_success') {
// }
let insertLogResult = await insertLog(params);
if (insertLogResult.message == 'insertLog_success') {
// PPT/PPTX 파일 업로드 즉시 PDF 변환 처리
for (let i = 0; i < insertDataResult.rows.length; i++) {
try {
let row = insertDataResult.rows[i];
let resourcePath = params.resourcePathArr[i];
let ext = (resourcePath.split('.').pop()).replace('.', '').toLowerCase();
if (['ppt', 'pptx'].includes(ext)) {
let dataId = row.data_id;
let objectKey = params.objectKeyArr[i];
let storageType = params.storageType;
let userInfoString = params.userInfoString;
let userIp = req.ip;
let bucket = projectId;
let command = new GetObjectCommand({
Bucket: bucket,
Key: objectKey,
});
let url = await getSignedUrl(s3, command, { expiresIn: 60 * 60 * 6 }); // 유효시간 6시간
let initiator = `DEV_LOCAL_${JSON.parse(userInfoString).user_id}`;
let type = 'archive';
if (env == 'production') {
if (deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE';
if (deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`;
}
const job = await convertPdfQueue.add(
`'${initiator}'에서 문서를 PDF로 변환`,
{ resourcePath, url, objectKey, bucket, storageType, dataId, projectId, userInfoString, userIp, initiator, type, serviceName }
);
let resourcePathClean = resourcePath.startsWith('/') ? resourcePath.slice(1) : resourcePath;
let pathArray = getPathArray(resourcePathClean);
convertingDataArr.push({
dataId: dataId,
resourcePath: resourcePath,
depth1: pathArray[0],
depth2: pathArray[1],
depth3: pathArray[2],
jobId: job.id
});
// 변환 시작 소켓 전송
let startEventData = { projectId: projectId, resourcePath: resourcePath, convertingDataArr: convertingDataArr };
let io = getIo();
io.emit('convert_start', startEventData);
console.log(`[PPT Auto-Convert] queued job ${job.id} for dataId ${dataId} (${resourcePath})`);
}
} catch (autoErr) {
console.error(`[PPT Auto-Convert Failed] index ${i}:`, autoErr);
}
}
let resultData = {
message: 'uploadData_success',
projectId: projectId,
activity: activity,
resourcePathArr: params.resourcePathArr
};
if (params.existingDataIdArr.length != 0) {
// params.existingDataIdArr에 들어있는 중복 파일 id를 사용해서
// cleanUpExistingData에서 중복 파일 row를 조회한 뒤 row 삭제하고, 조회한 row 결과 반환
let cleanUpExistingDataResult = await cleanUpExistingData(params.existingDataIdArr);
if (cleanUpExistingDataResult.message == 'cleanUpExistingData_success') {
// 반환한 row에 담긴 object_key, preview_key, popup_key를 keyArr 배열에 담고 중복키 제거한 뒤
// keyArr 배열 사용해서 오브젝트 스토리지에서 파일 삭제
let keyArr = [];
for (let row of cleanUpExistingDataResult.rows) {
if (row.object_key) keyArr.push(row.object_key);
if (row.preview_key) keyArr.push(row.preview_key);
if (row.popup_key) keyArr.push(row.popup_key);
if (row.thumbnail_key) keyArr.push(row.thumbnail_key);
}
keyArr = [...new Set(keyArr)];
await deleteTargetAction(keyArr, projectId);
}
}
let io = getIo();
io.emit('uploadData_success', resultData);
res.status(200).json({
message: 'uploadData_success',
});
}
}
}
exports.ensureAddOnFolder = async (req, res, next) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
params.dateArr = [params.date];
params.resourcePathArr = [params.resourcePath];
params.dataType = 'folder';
let result = await ensureAddOnFolderAction(params);
if (result == 'check_addOnFolder_success' || result == 'create_addOnFolder_success') {
res.status(200).json({
message: 'ensureAddOnFolder_success',
});
}
}
exports.renameTarget = async(req, res) => {
let { params } = req.body;
let permission = JSON.parse(params.userInfoString).permission;
let depth = getDepth(params.resourcePath);
if ((depth == 1 && permission < 191) || (depth >= 2 && permission < 7)) {
res.status(200).json({
message: 'renameTarget_failed_permission',
});
} else {
const projectId = req.baseUrl.split('/')[1];
params.projectId = projectId;
let activity = `renameTarget_${params.dataType}`;
params.activity = activity;
let updateDataRenameResult = await updateDataRename(params);
if (updateDataRenameResult.message == 'updateDataRename_success') {
params.userIp = req.ip;
params.resourcePathArr = [params.oldPath, params.newPath];
params.dataIdArr = [params.dataId];
let insertLogResult = await insertLog(params);
if (insertLogResult.message == 'insertLog_success') {
let resultData = {
message: `renameTarget_success`,
projectId: projectId,
activity: activity,
oldPath: params.oldPath,
newPath: params.newPath
};
let io = getIo();
io.emit('renameTarget_success', resultData);
res.status(200).json({
message: 'renameTarget_success',
});
}
}
}
}
exports.editAuthor = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
params.activity = 'editAuthor';
let updateDataAuthorResult = await updateDataAuthor(params);
if (updateDataAuthorResult.message == 'updateDataAuthor_success') {
let originalResourcePathArr = params.resourcePathArr;
let newResourcePathArr = [];
for (let i = 0; i < originalResourcePathArr.length; i++) {
let resourcePath = originalResourcePathArr[i];
let prevAuthorId = params.prevAuthorIdArr[i];
let prevAuthorNm = params.prevAuthorNmArr[i];
let obj = {
resourcePath: resourcePath,
prevAuthorId: prevAuthorId,
prevAuthorNm: prevAuthorNm,
newAuthorId: params.newAuthorId,
newAuthorNm: params.newAuthorNm
};
newResourcePathArr.push(obj);
}
params.resourcePathArr = newResourcePathArr;
params.userIp = req.ip;
let insertLogResult = await insertLog(params);
if (insertLogResult.message == 'insertLog_success') {
params.message = 'editAuthor_success';
params.originalResourcePathArr = originalResourcePathArr;
let resultData = params;
let io = getIo();
io.emit('editAuthor_success', resultData);
res.status(200).json({
message: 'editAuthor_success',
});
}
}
}
exports.getDataInfo = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
if (typeof params.dataIdArr === 'string') params.dataIdArr = JSON.parse(params.dataIdArr);
console.log('');
console.log('@@@@@@@@@@@@@@@@@@@@@@@@@@@');
console.log(`exports.getDataInfo (${makePostgresTimestamp()})`);
console.log(params.debug);
console.log('@@@@@@@@@@@@@@@@@@@@@@@@@@@');
console.log('');
try {
let result = await getDataInfoAction(params);
if (result && result.length == 1) result = result[0];
res.status(200).json({
message: 'getDataInfo_success',
result: result || null,
});
} catch (err) {
console.error("exports.getDataInfo err:", err);
res.status(500).json({
message: 'getDataInfo_error',
error: err.message
});
}
}
exports.generateDownloadUrl = async (req, res, next) => {
const projectId = req.baseUrl.split('/')[1];
let { objectKey, resourcePath, isThumbnail } = req.body;
let bucket = projectId;
let fileName = resourcePath.split('/').pop();
let ext = (objectKey.split('.').pop().toLowerCase()).split('__')[0];
try {
let isInlineType = ['jpg', 'jpeg', 'png', 'webp', 'gif']; // 웹 이미지 확장자
let responseContentDispositionString, responseContentTypeString;
if (isInlineType.includes(ext)) {
responseContentDispositionString = `attachment; filename="${encodeURIComponent(fileName)}"`;
responseContentTypeString = getMimeType(ext);
} else {
responseContentDispositionString = `attachment; filename="${encodeURIComponent(fileName)}"`;
responseContentTypeString = 'application/octet-stream';
}
let command = new GetObjectCommand({
Bucket: bucket,
Key: objectKey,
// 아래 두 옵션을 넣으면 브라우저가 파일을 바로 열지 않고 다운로드로 처리하도록 서버 응답 헤더를 강제설정할 수 있음:
ResponseContentDisposition: responseContentDispositionString,
ResponseContentType: responseContentTypeString,
ResponseCacheControl: 'no-cache',
});
let time = 60 * 60 * 24; // 유효시간: 60분(1시간) * 24 = 24시간 하루
if (isThumbnail) time = 60 * 60 * 24; // 유효시간: 60분(1시간) * 24 = 24시간 하루
let url = await getSignedUrl(s3, command, { expiresIn: time });
res.status(200).json({
message: 'generateDownloadUrl_success',
url: url
});
} catch (error) {
console.error('❌ Download Presigned URL 생성 실패:', error);
}
function getMimeType(ext) {
const mimeMap = {
gif: 'image/gif',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
webp: 'image/webp'
};
return mimeMap[ext] || 'application/octet-stream';
}
}
exports.downloadTarget = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
let activity = `downloadTarget_${params.dataType}`;
params.activity = activity;
params.userIp = req.ip;
let insertLogResult = await insertLog(params);
if (insertLogResult.message == 'insertLog_success') {
let io = getIo();
io.emit('downloadTarget_success', params);
res.status(200).json({
message: 'downloadTarget_success',
});
}
}
exports.relocateTarget = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
let activity = `relocateTarget_${params.dataType}`;
params.activity = activity;
let updateDataRelocateResult = await updateDataRelocate(params);
if (updateDataRelocateResult.message == 'updateDataRelocate_success') {
params.userIp = req.ip;
let fromPathArr = params.fromPathArr;
let toPathArr = params.toPathArr;
let resourcePathArr = [];
for (let i = 0; i < fromPathArr.length; i++) {
let obj = {};
obj.from = fromPathArr[i];
obj.to = toPathArr[i];
resourcePathArr.push(obj);
}
params.resourcePathArr = resourcePathArr;
let updateLastFolderActDateResult = await updateLastFolderActDate(params.depth3DataIdArr);
// if (updateLastFolderActDateResult.message == 'updateLastFolderActDate_success') {
// }
let insertLogResult = await insertLog(params);
if (insertLogResult.message == 'insertLog_success') {
let resultData = {
message: `relocateTarget_success`,
projectId: projectId,
activity: activity,
resourcePathArr: params.resourcePathArr,
userInfoString: params.userInfoString
};
let io = getIo();
io.emit('relocateTarget_success', resultData);
res.status(200).json({
message: 'relocateTarget_success',
});
}
}
}
exports.removeTarget = async(req, res) => {
try {
let { params } = req.body;
let permission = JSON.parse(params.userInfoString).permission;
let depth = getDepth(params.resourcePathArr[0]);
let isRecycleBinModal = params.isRecycleBinModal;
const isExpiredFolder = params.isExpiredFolder === true;
if (isExpiredFolder) {
// 자동 기한 만료 삭제의 경우 안전 검증
if (depth !== 3 || params.dataType !== 'folder') {
return res.status(400).json({
message: 'removeTarget_failed',
error: '보존 정책 자동 삭제는 3단계 폴더만 대상이 될 수 있습니다.'
});
}
const client = await pool.connect();
try {
// 1. 최신 시스템 정책 조회
const policyRes = await client.query("SELECT * FROM ver4.tb_system_policy WHERE policy_key = 'GLOBAL_DELETE_POLICY'");
if (policyRes.rows.length === 0 || !policyRes.rows[0].is_active) {
return res.status(400).json({
message: 'removeTarget_failed',
error: '자동 삭제 정책이 비활성화 상태입니다.'
});
}
const { limit_file_count, limit_days } = policyRes.rows[0];
// 2. 해당 폴더의 실제 정보 조회 (최종 활동 시각 확인)
const resourcePath = params.resourcePathArr[0];
const projectId = req.baseUrl.split('/')[1];
const folderQuery = `
SELECT last_folder_act_date
FROM ver4.${tbData}
WHERE project_id = $1 AND is_folder = true AND data_depth = 3 AND is_removed = false
AND path1 = $2 AND path2 = $3 AND path3 = $4;
`;
const folderRes = await client.query(folderQuery, [
projectId,
getPathSegment(resourcePath, 1),
getPathSegment(resourcePath, 2),
getPathSegment(resourcePath, 3)
]);
if (folderRes.rows.length === 0) {
return res.status(404).json({
message: 'removeTarget_failed',
error: '해당 폴더를 찾을 수 없거나 이미 삭제되었습니다.'
});
}
const lastFolderActDate = new Date(folderRes.rows[0].last_folder_act_date);
const expiryDate = new Date(lastFolderActDate.getTime() + limit_days * 24 * 60 * 60 * 1000);
if (expiryDate > new Date()) {
return res.status(400).json({
message: 'removeTarget_failed',
error: `해당 폴더는 아직 만료 기한이 지나지 않았습니다. (남은 기한 검증 실패)`
});
}
// 3. 하위 파일 개수 계산
const filesCount = await getFilesCount(projectId, params.storageType || 'ONPREMISE', resourcePath);
if (Number(filesCount) >= limit_file_count) {
return res.status(400).json({
message: 'removeTarget_failed',
error: `해당 폴더의 파일 개수가 기준(${limit_file_count}개) 이상입니다. (파일 개수 검증 실패)`
});
}
} catch (err) {
console.error("isExpiredFolder verification error:", err);
return res.status(500).json({
message: 'removeTarget_failed',
error: '만료 폴더 검증 처리 중 오류가 발생했습니다.'
});
} finally {
client.release();
}
}
if (!isExpiredFolder && !isRecycleBinModal && ((depth == 1 && permission < 191) || (depth >= 2 && permission < 7))) {
return res.status(200).json({
message: 'removeTarget_failed_permission',
});
} else {
const projectId = req.baseUrl.split('/')[1];
params.projectId = projectId;
let activity = `removeTarget_${params.dataType}`;
params.activity = activity;
let updateDataRemoveResult = await updateDataRemove(params);
if (updateDataRemoveResult && updateDataRemoveResult.message == 'updateDataRemove_success') {
params.userIp = req.ip;
if (params.dataType == 'file') {
let updateLastFolderActDateResult = await updateLastFolderActDate(params.depth3DataIdArr);
}
let insertLogResult = await insertLog(params);
if (insertLogResult && insertLogResult.message == 'insertLog_success') {
let resultData = {
message: `removeTarget_success`,
projectId: projectId,
activity: activity,
resourcePathArr: params.resourcePathArr,
userInfoString: params.userInfoString,
isExpiredFolder: params.isExpiredFolder
};
let io = getIo();
io.emit('removeTarget_success', resultData);
return res.status(200).json({
message: 'removeTarget_success',
});
}
}
res.status(500).json({
message: 'removeTarget_failed',
error: '대상 제거 중 오류가 발생했습니다.'
});
}
} catch (error) {
console.error("removeTarget error:", error);
res.status(500).json({
message: 'removeTarget_failed',
error: error.message
});
}
}
exports.deleteTarget = async(req, res) => {
let params = req.body;
let permission = JSON.parse(params.userInfoString).permission;
let depth = getDepth(params.resourcePathArr[0]);
let isRecycleBinModal = params.isRecycleBinModal;
if (!isRecycleBinModal && (depth == 1 && permission < 191) || (depth >= 2 && permission < 7)) {
res.status(200).json({
message: 'deleteTarget_failed_permission',
});
} else {
const projectId = req.baseUrl.split('/')[1];
params.projectId = projectId;
params.resourcePathArr = JSON.parse(params.resourcePathArr);
params.dataIdArr = JSON.parse(params.dataIdArr);
params.objectKeyArr = JSON.parse(params.objectKeyArr);
params.previewKeyArr = JSON.parse(params.previewKeyArr);
params.popupKeyArr = JSON.parse(params.popupKeyArr);
params.thumbnailKeyArr = JSON.parse(params.thumbnailKeyArr);
let activity = `deleteTarget_${params.dataType}`;
params.activity = activity;
// 모든 키를 담는 keyArr 배열 생성
let keyArr = [...params.objectKeyArr, ...params.previewKeyArr, ...params.popupKeyArr, ...params.thumbnailKeyArr];
// 중복된 키 제거
keyArr = [...new Set(keyArr)];
// null, '', undefined 등 불필요한 값 제거
keyArr = keyArr.filter(key =>
typeof key === 'string' ? key.trim() !== '' : Boolean(key)
);
let deleteDataResult = await deleteData(params);
if (deleteDataResult.message == 'deleteData_success') {
// 완전 삭제는 DB삭제 후 오브젝트 스토리지에서 삭제하는데,
// DB에서 삭제되면 더 이상 조회되지 않아 파일 관련 기능을 사용할 수 없으므로
// 오브젝트 스토리지에서 삭제하기 전에 소켓으로 미리 갱신
let successResultData = {
message: `deleteTarget_success`,
projectId: projectId,
activity: activity,
userInfoString: params.userInfoString
};
let io = getIo();
io.emit('deleteTarget_success', successResultData);
try {
await deleteTargetAction(keyArr, projectId);
console.log('❗❗ 오브젝트 스토리지 삭제 완료');
// 로그 추가
res.status(200).json({
message: 'deleteTarget_success',
});
} catch (err) {
console.error('deleteTarget error:', err);
throw err;
}
}
}
}
exports.setDataPermission = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
let activity = `setDataPermission_${params.dataType}`;
params.activity = activity;
let updateDataPermissionResult = await updateDataPermission(params);
if (updateDataPermissionResult.message == 'updateDataPermission_success') {
params.resourcePathArr = [{
resourcePath: params.resourcePath,
beforePermission: getPermissionString(params.beforePermission),
newPermission: getPermissionString(params.newPermission),
}];
params.dataIdArr = [params.dataId];
params.userIp = req.ip;
let insertLogResult = await insertLog(params);
if (insertLogResult.message == 'insertLog_success') {
let resultData = {
message: `setDataPermission_success`,
projectId: projectId,
activity: activity,
resourcePath: params.resourcePath,
userInfoString: params.userInfoString,
beforePermission: params.beforePermission,
newPermission: params.newPermission,
};
let io = getIo();
io.emit('setDataPermission_success', resultData);
res.status(200).json({
message: 'setDataPermission_success',
});
}
}
}
exports.editPosition = async(req, res) => {
let { params } = req.body;
let updateDataPositionResult = await updateDataPosition(params);
if (updateDataPositionResult.message == 'updateDataPosition_success') {
res.status(200).json({
message: 'editPosition_success',
});
}
}
exports.renewExpiryDate = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
let inputSeconds = (params.inputSeconds) ? params.inputSeconds : undefined;
let updateLastFolderActDateResult = await updateLastFolderActDate(params.depth3DataIdArr, inputSeconds);
if (updateLastFolderActDateResult.message == 'updateLastFolderActDate_success') {
let resultData = {
message: `renewExpiryDate_success`,
projectId: projectId,
resourcePath: params.resourcePath,
dataId: params.dataId,
userInfoString: params.userInfoString,
inputSeconds: inputSeconds
}
let io = getIo();
io.emit('renewExpiryDate_success', resultData);
res.status(200).json({
message: 'renewExpiryDate_success',
});
}
}
//pdf_thumb
exports.makeThumbPdf = async(req, res)=>{
const projectId = req.baseUrl.split('/')[1];
let bucket = projectId;
let userIp = req.ip;
let { resourcePath, userInfoString, objectKey, storageType } = req.body.params;
let dataId = undefined;
let command = new GetObjectCommand({
Bucket: bucket,
Key: objectKey,
});
const client = await pool.connect();
try{
let queryString = `select data_id from ver4.${tbData} where project_id = $1 and object_key = $2`;
let Res = await client.query(queryString, [projectId, objectKey]);
dataId = Res.rows[0].data_id;
let url = await getSignedUrl(s3, command, { expiresIn: 60 * 60 * 15 }); // 유효시간: 15분
let waitingCount = await thumbQueue.getWaitingCount();
let initiator = `DEV_LOCAL_${JSON.parse(userInfoString).user_id}`;
let type = 'archive';
if (env == 'production') {
if (deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE';
if (deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`;
}
const job = await thumbQueue.add(
`'${initiator}'에서 PDF 업로드(Thumb생성)`,
{ resourcePath, url, objectKey, bucket, storageType, dataId, projectId, userInfoString, userIp, initiator, type },
);
res.status(200).json({
jobId: job.id,
waiting: waitingCount
});
}catch(err){
console.error(err);
res.status(500).json({
message : 'makeThumb Error',
});
}finally{
client.release();
}
}
exports.convertPdf = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let bucket = projectId;
let userIp = req.ip;
let { params } = req.body;
let { dataId, resourcePath, depth1, depth2, depth3, userInfoString, objectKey, storageType } = params;
let command = new GetObjectCommand({
Bucket: bucket,
Key: objectKey,
});
let url = await getSignedUrl(s3, command, { expiresIn: 60 * 60 * 6 }); // 유효시간: 6시간
let waitingCount = await convertPdfQueue.getWaitingCount();
let initiator = `DEV_LOCAL_${JSON.parse(userInfoString).user_id}`;
let type = 'archive';
if (env == 'production') {
if (deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE';
if (deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`;
}
const job = await convertPdfQueue.add(
`'${initiator}'에서 문서를 PDF로 변환`,
{ resourcePath, url, objectKey, bucket, storageType, dataId, projectId, userInfoString, userIp, initiator, type, serviceName },
// { jobId: `pdf-${Date.now()}` } // ← 원하는 ID 값 지정
);
res.status(200).json({
jobId: job.id,
waiting: waitingCount
});
//// 배열에 현재 변환중인 파일 정보 추가
convertingDataArr.push({
dataId: dataId,
resourcePath: resourcePath,
depth1: depth1,
depth2: depth2,
depth3: depth3,
jobId: job.id
})
//// 변환 시작 socket 전송
let resultData = { projectId: projectId, resourcePath: resourcePath, convertingDataArr: convertingDataArr };
let io = getIo();
io.emit('convert_start', resultData);
// queue.js에서 변환 완료/실패 후 소켓으로 convertPdf_success/convertPdf_failed 이벤트 전송
// console.log('@@@@@@@@@@@@@@');
// console.log(convertingDataArr);
}
exports.addConvetPdfLog = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
let { userInfoString, resourcePath, dataId } = params
//// 배열에서 파일 정보 삭제
convertingDataArr = convertingDataArr.filter(data => data.dataId !== dataId);
const waiting = await convertPdfQueue.getWaitingCount();
const active = await convertPdfQueue.getActiveCount();
if (waiting == 0 && active == 0 && convertingDataArr.length != 0) {
convertingDataArr = [];
}
let insertLogResult = await insertLog(params, 'addConvertPdfLog');
if (insertLogResult.message == 'insertLog_success') {
let resultData = {
message: 'addConvetPdfLog_success',
projectId: projectId,
userInfoString: userInfoString,
resourcePath: resourcePath,
dataId: dataId,
convertingDataArr: convertingDataArr
};
let io = getIo();
io.emit('addConvetPdfLog_success', resultData);
res.status(200).json({
message: 'addConvetPdfLog_success',
});
}
}
exports.removeConvertingData = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
let { resourcePath, dataId, userInfoString, stdout } = params;
//// 배열에서 파일 정보 삭제
convertingDataArr = convertingDataArr.filter(data => data.dataId !== dataId);
const waiting = await convertPdfQueue.getWaitingCount();
const active = await convertPdfQueue.getActiveCount();
if (waiting == 0 && active == 0 && convertingDataArr.length != 0) {
convertingDataArr = [];
}
let resultData = {
message: 'removeConvertingData_success',
projectId: projectId,
convertingDataArr: convertingDataArr,
resourcePath: resourcePath,
dataId: dataId,
userInfoString: userInfoString,
stdout: stdout || ''
};
let io = getIo();
io.emit('removeConvertingData_success', resultData);
res.status(200).json({
message: 'removeConvertingData_success',
resultData: resultData
});
}
exports.postProcessVideo = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { resourcePath, userInfoString, objectKey, storageType } = req.body.params;
let bucket = projectId;
let userIp = req.ip;
let dataId = undefined;
const client = await pool.connect();
try {
let queryString = `select data_id from ver4.${tbData} where project_id = $1 and object_key = $2`;
let result = await client.query(queryString, [projectId, objectKey]);
dataId = result.rows[0].data_id;
let waitingCount = await postProcessVideoQueue.getWaitingCount();
let initiator = `DEV_LOCAL_${JSON.parse(userInfoString).user_id}`;
let type = 'archive';
if (env == 'production') {
if (deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE';
if (deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`;
}
const job = await postProcessVideoQueue.add(
`'${initiator}'에서 동영상 후처리`,
{ resourcePath, objectKey, bucket, storageType, dataId, projectId, userInfoString, userIp, initiator, type },
);
res.status(200).json({
jobId: job.id,
waiting: waitingCount
});
} catch (err) {
console.error(err);
res.status(500).json({
message : 'postProcessVideo Error',
});
} finally {
client.release();
}
}
exports.requestResetViewer = async(req, res) => {
}
exports.updateMemoInfo = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
const { userInfoString, params} = req.body;
let userInfo = JSON.parse(userInfoString);
let userId = (userInfo)? userInfo.user_id : undefined;
if(!userId) return;
let parts = params.resourcePath.split('/').filter(Boolean);
const client = await pool.connect();
try {
let string = [];
for(let i=0; i<parts.length; i++) {
let escapeParts = parts[i].replaceAll("'", "''");
string.push(` and path${i+1} = '${escapeParts}'`);
}
let escapeMemo = params.memo.replaceAll("'", "''");
let queryString = `
update ver4.${tbData}
set memo = '${escapeMemo}'
where project_id = '${projectId}'
`;
string.forEach(str => {
queryString += str;
})
let result = client.query(queryString);
let resultData = {
projectId: projectId,
resourcePath: params.resourcePath,
memo: params.memo,
}
let io = getIo();
io.emit('saveMemo_success', resultData);
res.status(200).json({
message: 'updateMemo_success'
});
}catch(error) {
console.error('selectData err:', error);
}finally {
client.release();
}
}
exports.getMemoInfo = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
const { userInfoString, resourcePath, dataId } = req.query;
let userInfo = JSON.parse(userInfoString);
let userId = (userInfo)? userInfo.user_id : undefined;
if(!userId) return;
let parts = resourcePath.split('/').filter(Boolean);
const client = await pool.connect();
try {
// let string;
// for(let i=0; i<parts.length; i++) {
// parts[i] = parts[i].replace(/'/g, "\\'");
// string = `and path${i+1} = '${parts[i]}'`;
// }
// let queryString = `
// select memo
// from ver4.${tbData}
// where project_id = '${projectId}'
// ${string}
// `;
// let result = await client.query(queryString);
// res.status(200).json({
// message: 'selectMemo_success',
// result: result.rows[0]
// });
const queryParams = [projectId];
queryParams.push(dataId);
let paramIndex = 3;
let conditionParts = [];
for (let i = 0; i < parts.length; i++) {
conditionParts.push(`path${i+1} = $${paramIndex}`);
queryParams.push(parts[i]);
paramIndex++;
}
let queryString = `
SELECT memo
FROM ver4.${tbData}
WHERE project_id = $1
AND data_id = $2
${conditionParts.length > 0 ? 'AND ' + conditionParts.join(' AND ') : ''}
`;
let result = await client.query(queryString, queryParams);
res.status(200).json({
message: 'selectMemo_success',
result: result.rows[0]
});
}catch(error) {
console.error('selectData err:', error);
}finally {
client.release();
}
}
// main title img 관련
exports.uploadData_titleImg = async(req, res, next) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
params.projectId = projectId;
params.dateArr = JSON.parse(params.dateArr);
params.resourcePathArr = JSON.parse(params.resourcePathArr);
params.sizeArr = JSON.parse(params.sizeArr);
params.objectKeyArr = JSON.parse(params.objectKeyArr);
let activity = `uploadData_${params.dataType}`;
let insertDataResult = await insertData(params);
if (insertDataResult.message == 'insertData_success') {
let dataIdArr = [];
for (let i = 0; i < insertDataResult.rows.length; i++) {
let row = insertDataResult.rows[i];
dataIdArr.push(row.data_id);
}
params.dataIdArr = dataIdArr;
params.userIp = req.ip;
let insertLogResult = await insertLog(params);
if (insertLogResult.message == 'insertLog_success') {
let resultData = {
message: 'uploadData_success',
projectId: projectId,
activity: activity,
resourcePathArr: params.resourcePathArr
};
let io = getIo();
io.emit('uploadData_success', resultData);
res.status(200).json({
message: 'uploadData_success',
});
}
}
}
exports.generateImageUrl = async (req, res, next) => {
const projectId = req.baseUrl.split('/')[1];
const pageType = req.baseUrl.split('/')[2];
let { resourcePath } = req.query;
let bucket = projectId;
let fullPath = resourcePath;
fullPath = fullPath.replaceAll('\\', '/');
fullPath = fullPath.replaceAll('//', '/');
fullPath = fullPath.split('/')[1];
const client = await pool.connect();
try {
let queryString = `select object_key from ver4.${tbData}
where project_id = '${projectId}'
and path1 = $1
and data_depth = 2
and is_folder = false
and is_removed = false`
let {rows} = await client.query(queryString, [fullPath]);
let objectKey = rows[0].object_key
try {
const command = new GetObjectCommand({
Bucket: bucket,
Key: objectKey,
ContentType: 'application/octet-stream'
});
url = await getSignedUrl(s3, command, { expiresIn: 60 * 60 * 15 }); // 유효시간: 15분
}catch(error) {
console.error('❌ Image Presigned URL 생성 실패:', error)
}
res.status(200).json({
message: 'generateImageUrl_success',
result: {
url: url
}
});
}catch(error) {
console.error('generateImageUrl err:', error);
}finally {
client.release();
}
}
exports.isMainTitleImage = async(req, res) => {
const client = await pool.connect();
try {
const projectId = req.baseUrl.split('/')[1];
let { resourcePath } = req.query;
resourcePath = resourcePath.replace('/', '');
let queryString = `
select path2
from ver4.${tbData}
where project_id = '${projectId}'
and path1 = $1
and data_depth = 2
and is_folder = false
and is_removed = false;
`
let result = await client.query(queryString, [resourcePath]);
res.status(200).json({
message: 'isMainTitleImage_true',
result: result.rows[0]
})
}catch(error) {
console.error('isMainTitleImage err:', error);
}finally {
client.release();
}
}
exports.deleteMainTitleImage = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
let { userInfoString, resourcePath } = params;
let userInfo = JSON.parse(userInfoString);
let userId = (userInfo)? userInfo.user_id : '-';
let dateNow = makePostgresTimestamp(Date.now());
resourcePath = resourcePath.split('/')[1];
const client = await pool.connect();
try {
// DB 삭제
let queryString = `
update ver4.${tbData} set
is_removed = true,
mod_date = $1,
mod_user_id = $2,
mod_activity = $3
where project_id = '${projectId}'
and path1 = $4
and is_folder = false
and data_depth = 2`
;
let values = [dateNow, userId, 'removeTarget_file', resourcePath];
let result = await client.query(queryString, values);
res.status(200).json({
message: 'deleteMainTitleImage_success',
})
}catch(error) {
console.error('deleteMainTitleImage err:', error);
}finally {
client.release();
}
}
exports.mgmtFunc_resetConvert = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
let { resourcePath, dataId } = params;
for (const data of convertingDataArr) {
if (Number(data.dataId) === Number(dataId)) {
try {
const job = await convertPdfQueue.getJob(data.jobId);
if (job) {
await job.remove();
console.log(`[Cancel] Successfully removed job ${data.jobId} from convert-pdf queue`);
}
} catch (jobErr) {
console.error('[Cancel] Error removing job:', jobErr);
}
}
}
//// 배열에서 파일 정보 삭제
convertingDataArr = convertingDataArr.filter(data => Number(data.dataId) !== Number(dataId));
const waiting = await convertPdfQueue.getWaitingCount();
const active = await convertPdfQueue.getActiveCount();
if (waiting == 0 && active == 0 && convertingDataArr.length != 0) {
convertingDataArr = [];
}
let resultData = {
message: 'mgmtFunc_resetConvert_success',
projectId: projectId,
convertingDataArr: convertingDataArr,
resourcePath: resourcePath,
dataId: dataId
};
let io = getIo();
io.emit('mgmtFunc_resetConvert_success', resultData);
res.status(200).json({
message: 'mgmtFunc_resetConvert_success',
resultData: resultData
});
}
exports.mgmtFunc_addClickLog = async (req, res) => {
try {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
let userIp = req.ip;
params.projectId = projectId;
params.userIp = userIp;
let insertClickLogResult = await insertClickLog(params);
if (insertClickLogResult && insertClickLogResult.message == 'insertClickLog_success') {
res.status(200).json({
message: 'insertClickLog_success',
});
} else {
res.status(200).json({
message: 'insertClickLog_failed',
});
}
} catch (err) {
console.error("exports.mgmtFunc_addClickLog err:", err);
res.status(500).json({
message: 'mgmtFunc_addClickLog_error',
error: err.message
});
}
}
exports.getControlBoxPosition = async (req, res) => {
let { userId } = req.query;
let selectControlBoxPositionResult = await selectControlBoxPosition(userId);
res.status(200).json({
message: 'getControlBoxPosition_success',
result: selectControlBoxPositionResult[0]
});
}
exports.setControlBoxPosition = async (req, res) => {
let { params } = req.body;
let updateControlBoxPositionResult = await updateControlBoxPosition(params);
if (updateControlBoxPositionResult.message == 'updateControlBoxPosition_success') {
res.status(200).json({
message: 'setControlBoxPosition_success',
});
}
}
// 🔺🔺🔺🔺🔺🔺🔺🔺 클라이언트 요청 핸들러 끝 🔺🔺🔺🔺🔺🔺🔺🔺
exports.dtest = async (req, res) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
const selectQueryString = `
SELECT *
FROM ver4.${tbData}
WHERE is_folder = false
AND popup_key IS NOT NULL
AND popup_size = '0'
AND LOWER(ext) IN (
'hwp', 'hwpx', 'doc', 'docx',
'xls', 'xlsx', 'xlsm',
'ppt', 'pptx',
'dwg', 'dxf', 'grm'
);
`;
const result = await client.query(selectQueryString);
const { rows } = result;
const BATCH_SIZE = 200;
// rows를 200개씩 끊어서 처리
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
const batch = rows.slice(i, i + BATCH_SIZE);
const updates = [];
// 이 배치 내에서 S3 순차 호출
for (const row of batch) {
const command = new HeadObjectCommand({
Bucket: row.bucket,
Key: row.popup_key,
});
const response = await s3.send(command);
updates.push({
dataId: row.data_id,
size: response.ContentLength,
});
}
// CASE WHEN 쿼리 생성
const caseStatements = updates
.map(u => `WHEN data_id = ${u.dataId} THEN '${u.size}'`)
.join('\n');
const idList = updates.map(u => u.dataId).join(', ');
const updateQueryString = `
UPDATE ver4.${tbData}
SET popup_size = CASE
${caseStatements}
ELSE popup_size
END
WHERE data_id IN (${idList});
`;
await client.query(updateQueryString);
}
await client.query('COMMIT');
res.status(200).json({
message: 'qwe',
});
} catch (error) {
await client.query('ROLLBACK');
console.error('qwe error:', error);
res.status(500).json({ error: 'Failed to update popup_size' });
} finally {
client.release();
}
};
exports.otest = async (req, res) => {
const client = await pool.connect();
try {
await client.query('BEGIN');
const selectQueryString = `
SELECT *
FROM ver4.tb_official_doc_file
WHERE popup_key IS NOT NULL
AND popup_size = '0'
AND LOWER(ext) IN (
'hwp', 'hwpx', 'doc', 'docx',
'xls', 'xlsx', 'xlsm',
'ppt', 'pptx',
'dwg', 'dxf', 'grm'
);
`;
const result = await client.query(selectQueryString);
const { rows } = result;
const BATCH_SIZE = 200;
// rows를 200개씩 끊어서 처리
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
const batch = rows.slice(i, i + BATCH_SIZE);
const updates = [];
// 이 배치 내에서 S3 순차 호출
for (const row of batch) {
const command = new HeadObjectCommand({
Bucket: row.bucket,
Key: row.popup_key,
});
const response = await s3.send(command);
updates.push({
docId: row.doc_id,
size: response.ContentLength,
});
}
// CASE WHEN 쿼리 생성
const caseStatements = updates
.map(u => `WHEN doc_id = ${u.docId} THEN '${u.size}'`)
.join('\n');
const idList = updates.map(u => u.docId).join(', ');
const updateQueryString = `
UPDATE ver4.tb_official_doc_file
SET popup_size = CASE
${caseStatements}
ELSE popup_size
END
WHERE doc_id IN (${idList});
`;
await client.query(updateQueryString);
}
await client.query('COMMIT');
res.status(200).json({
message: 'asd',
});
} catch (error) {
await client.query('ROLLBACK');
console.error('asd error:', error);
res.status(500).json({ error: 'Failed to update popup_size' });
} finally {
client.release();
}
};
// gemini AI
exports.summarizeAI = multer({ storage: multer.memoryStorage() });
exports.summarizeAI_action = async(req, res) => {
let { projectId , objectKey, userInfoString, resourcePath, storageType, dataId, type} = req.query;
let bucket = projectId;
let userIp = req.ip;
let initiator = `DEV_LOCAL_${JSON.parse(userInfoString).user_id}`;
if (env == 'production') {
if (deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE';
if (deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`;
}
const files = req.files.prompt_file[0];
const prompt = files.buffer.toString('utf-8');
let job;
if(type == 'outer') {
// 파일 다운로드 Presigned URL
let command = new GetObjectCommand({
Bucket: bucket,
Key: objectKey,
});
let url = await getSignedUrl(s3, command, { expiresIn: 60 * 60 * 15 }); // 유효시간 : 15분
job = await summarizeAPIQueue.add(
`'${initiator}'에서 파일 AI 요약`,
{ prompt, url, resourcePath, initiator, storageType, dataId, projectId, bucket, userInfoString, userIp, type }
)
} else {
job = await summarizeAIQueue.add(
`'${initiator}'에서 파일 AI 요약`,
{ prompt, resourcePath, initiator, storageType, dataId, projectId, bucket, userInfoString, userIp, type }
)
}
//// 배열에 현재 요약중인 파일 정보 추가
summarizeAiDataArr.push({
projectId: projectId,
dataId: dataId,
resourcePath: resourcePath,
jobId: job.id,
type: type,
});
// 요약 시작 socket 전송
let resultData = { summarizeAiDataArr };
let io = getIo();
io.emit('summarize_start', resultData);
res.status(200).json({
message: 'summarizeAI_success'
});
};
// exports.summarizeAI = multer({ storage: multer.memoryStorage() });
// exports.summarizeAI_action = async(req, res) => {
// let { projectId, objectKey, storageType, userInfoString, resourcePath, dataId } = req.query;
// let bucket = projectId;
// let userIp = req.ip;
// const files = req.files.prompt_file[0];
// // 파일 다운로드 Presigned URL
// let command = new GetObjectCommand({
// Bucket: bucket,
// Key: objectKey,
// });
// let url = await getSignedUrl(s3, command, { expiresIn: 60 * 15 }); // 유효시간: 15분
// if(type == 'gemini') {
// const promptFile = files.find(f => /\.txt$/i.test(f.originalname) || f.mimetype === 'text/plain');
// const prompt = promptFile ? promptFile.buffer.toString('utf-8') : '';
// const jsonFile = files.find(f => /\.json$/i.test(f.originalname) || f.mimetype === 'application/json');
// if (jsonFile) {
// try {
// const json = JSON.parse(jsonFile.buffer.toString('utf-8'));
// } catch (e) {
// console.error('❌ JSON 파싱 실패:', e);
// return res.status(400).json({ message: 'invalid JSON file'});
// }
// }
// let initiator = `DEV_LOCAL_${JSON.parse(userInfoString).user_id}`;
// if(env == 'production') {
// if (deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE';
// if (deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`;
// };
// const job = await summarizeAIQueue.add(
// `'${initiator}'에서 PDF 파일 AI 요악`,
// { prompt, url, resourcePath, initiator, storageType, dataId, projectId, bucket, userInfoString, userIp, json }
// );
// res.status(200).json({
// jobId: job.id,
// data: job.data,
// });
// //// 배열에 현재 요약중인 파일 정보 추가
// summarizeAiDataArr.push({
// dataId: dataId,
// resourcePath: resourcePath,
// depth1,
// depth2,
// depth3,
// jobId: job.id
// });
// }else {
// const prompt = files.buffer.toString('utf-8');
// let initiator = `DEV_LOCAL_${JSON.parse(userInfoString).user_id}`;
// if (env == 'production') {
// if(deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE';
// if(deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`;
// }
// const job = await summarizeAPIQueue.add(
// `'${initiator}'에서 파일 AI 요약`,
// { prompt, url, resourcePath, initiator, storageType, dataId, projectId, bucket, userInfoString, userIp }
// );
// res.status(200).json({
// message: 'summarizeAI_success',
// });
// //// 배열에 현재 요약중인 파일 정보 추가
// summarizeAiDataArr.push({
// dataId: dataId,
// resourcePath: resourcePath
// });
// // }
// // 요약 시작 socket 전송
// let resultData = { resourcePath: resourcePath, summarizeAiDataArr };
// let io = getIo();
// io.emit('summarize_start', resultData);
// }
// ai 요약 로그 추가
exports.addSummarizeAiLog = async(req, res) => {
// let { addSummarizeAiLogParams } = req;
let { projectId, userInfoString, resourcePath, dataId, text, isState, type } = req;
let data = req;
for(let i = 0; i < summarizeAiDataArr.length; i++){
if(summarizeAiDataArr[i].dataId == dataId){
summarizeAiDataArr.splice(i, 1);
i--;
}
}
if(isState) {
let insertLogResult = await insertLog(data, 'addSummarizeAiLog');
if (insertLogResult.message == 'insertLog_success') {
// DB에 ai_summary 및 memo 텍스트 영속적 저장
const client = await pool.connect();
try {
const tbDataName = process.env.NODE_ENV === 'production' ? 'tb_data' : '_test_tb_data';
await client.query(
`UPDATE ver4.${tbDataName} SET ai_summary = $1, memo = $1 WHERE data_id = $2`,
[text, dataId]
);
} catch (dbErr) {
console.error('❌ Failed to update ai_summary/memo in DB:', dbErr);
} finally {
client.release();
}
resultData = {
message: 'addSummarizeAiLog_success',
projectId: projectId,
userInfoString: userInfoString,
resourcePath: resourcePath,
dataId: dataId,
summarizeAiDataArr: summarizeAiDataArr,
text: text,
type: type
};
}
let io = getIo();
io.emit('addSummarizeAiLog_success', resultData);
}
}
// ai 상태 조회용
exports.summarizeState = async(req, res) => {
const { resourcePath, dataId } = req.query;
const [dataInfo] = summarizeAiDataArr.filter(
it => it.resourcePath === resourcePath && String(it.dataId) == String(dataId)
);
res.json({
message: 'summarizeState_success',
dataInfo: dataInfo
});
};
//folder download
exports.downloadzip = async(req, res)=>{
const projectId = req.baseUrl.split('/')[1];
let bucket = projectId;
let userIp = req.ip;
let {resourcePath, userInfoString, storageType} = req.query;
let result = {};
let pathArr = resourcePath[0].split('/');
pathArr.shift();
let depth = pathArr.length;
let logParam = {
projectId:projectId,
activity : 'downloadTarget_folder',
userInfoString : userInfoString,
userIp : userIp,
resourcePathArr : resourcePath,
dataIdArr : []
};
await insertLog(logParam);
const client = await pool.connect();
try{
let queryString = `select * from ver4.${tbData}
where project_id = '${projectId}'
and is_folder = false
and is_removed = false `;
for(let i = 1; i < depth+1; i++){
queryString += ` and path${i} = '${pathArr[i-1]}' `
}
queryString += `order by path1, path2, path3`;
let {rows} = await client.query(queryString);
let folderName = pathArr.pop();
let jsonDepth = 0;
for(let i = 0 ; i < rows.length; i++){
let command = new GetObjectCommand({
Bucket : bucket,
Key : rows[i].object_key
})
let url = await getSignedUrl(s3, command, { expiresIn: 60 * 60 * 24 }); // 유효시간: 24시간
if(!result[folderName]) result[folderName] = {};
if(rows[i][`path${depth+1}`] !== null && rows[i][`path${depth+1}`] !== undefined){
if(jsonDepth < 1) jsonDepth = 1;
if(!result[folderName][rows[i][`path${depth+1}`]]) result[folderName][rows[i][`path${depth+1}`]] = {};
}
if(rows[i][`path${depth+2}`] !== null && rows[i][`path${depth+2}`] !== undefined){
if(jsonDepth < 2) jsonDepth = 2;
if(!result[folderName][rows[i][`path${depth+1}`]][rows[i][`path${depth+2}`]]) result[folderName][rows[i][`path${depth+1}`]][rows[i][`path${depth+2}`]] = {};
}else if(rows[i][`path${depth+1}`] !== null && rows[i][`path${depth+1}`] !== undefined){
result[folderName][rows[i][`path${depth+1}`]] = url;
}
if(rows[i][`path${depth+3}`] !== null && rows[i][`path${depth+3}`] !== undefined){
if(jsonDepth < 3) jsonDepth = 3;
if(!result[folderName][rows[i][`path${depth+1}`]][rows[i][`path${depth+2}`]][rows[i][`path${depth+3}`]]) result[folderName][rows[i][`path${depth+1}`]][rows[i][`path${depth+2}`]][rows[i][`path${depth+3}`]] = {};
}else if(rows[i][`path${depth+2}`] !== null && rows[i][`path${depth+2}`] !== undefined){
result[folderName][rows[i][`path${depth+1}`]][rows[i][`path${depth+2}`]] = url;
}
if(rows[i][`path${depth+4}`] !== null && rows[i][`path${depth+4}`] !== undefined){
if(jsonDepth < 4) jsonDepth = 4;
if(!result[folderName][rows[i][`path${depth+1}`]][rows[i][`path${depth+2}`]][rows[i][`path${depth+3}`]][rows[i][`path${depth+4}`]]) result[folderName][rows[i][`path${depth+1}`]][rows[i][`path${depth+2}`]][rows[i][`path${depth+3}`]][rows[i][`path${depth+4}`]] = url;
}else if(rows[i][`path${depth+3}`] !== null && rows[i][`path${depth+3}`] !== undefined){
result[folderName][rows[i][`path${depth+1}`]][rows[i][`path${depth+2}`]][rows[i][`path${depth+3}`]] = url;
}
}
let insertQuery = `
insert into ver4.tb_download_folder (user_id, project_id, path, name)
values ($1, $2, $3, $4)
`
await client.query(insertQuery, [JSON.parse(userInfoString).user_id, projectId, resourcePath[0], folderName]);
}catch(err){
console.error('downloadZip query 중 error 발생: '+err);
}finally{
client.release();
}
let initiator = `DEV_LOCAL_${JSON.parse(userInfoString).user_id}`;
let type = 'archive';
if (env == 'production') {
if (deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE';
if (deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`;
}
const job = await zipFolderQueue.add(
`'${initiator}'에서 download 요청(zip생성)`,{
json : result,
userInfoString : userInfoString,
resourcePath: resourcePath,
bucket : projectId,
storageType : storageType,
initiator : initiator,
}
);
res.status(200).json({
jobId: job.id,
});
}
exports.getMyDownloadList = async(req, res) => {
const { user_id } = req.query;
const client = await pool.connect();
try{
let queryString = `
select *, (select project_nm from ver4.tb_project where project_id = ver4.tb_download_folder.project_id) project_nm from ver4.tb_download_folder where user_id = $1 and (expire_date > now() - interval '1 day' OR made = false) order by expire_date
`
let result = await client.query(queryString, [user_id]);
res.status(200).json(result.rows);
}catch(error){
console.error('getMyDownloadList query 중 error 발생: '+error);
res.status(500).json({ error: error.message, message: 'getMyDownloadList_failed' });
}finally{
client.release();
}
}
function parseJsonToFlatArray(jsonData) {
const fileList = [];
function traverse(node, currentPath) {
for (const key in node) {
const value = node[key];
if (typeof value === 'object' && value !== null) {
const nextPath = `${currentPath}/${key}`;
traverse(value, nextPath);
} else if (typeof value === 'string') {
fileList.push({
filename: key,
path: currentPath,
url: value
});
}
}
}
if (jsonData && typeof jsonData === 'object') {
traverse(jsonData, '');
} else {
console.error("유효한 JSON 데이터 객체가 아닙니다.");
}
return fileList;
}
// 유저 권한 로그 추가
exports.addPermissionLog = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { logs } = req.body;
// logs안에 insertLog에서 쓰는 params에 형태로 그대로 들어있어 req.body에서 꺼낸 후 for문으로 내부함수 insertLog 호출
try {
for(let i = 0; logs.length > i; i++){
await insertLog(logs[i]);
}
// 로그 삽입 완료 이후 이벤트 클라이언트 소켓에 전달
let resultData = {
message: 'addPermissionLog_success',
projectId: projectId,
};
let io = getIo();
io.emit('addPermissionLog_success', resultData);
res.status(200).json({message : 'addPermissionLog success'});
} catch(err) {
res.status(500).json({message : 'addPermissionLog failed'});
console.error('addPermissionLog Error', err);
}
}
// 이전 파일 썸네일 추가 (임시)
exports.getNullThumbnailDataInfo = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let result = await getNullThumbnailDataInfoAction(projectId);
res.status(200).json({
message: 'getNullThumbnailDataInfo_success',
result: result,
});
}
// 이전 파일 썸네일 추가 -select
async function getNullThumbnailDataInfoAction(projectId) {
let bucket = projectId;
const client = await pool.connect();
try {
// let queryString = `
// SELECT DISTINCT f.*
// FROM ver4.${tbData} as f
// JOIN ver4.${tbData} as g
// ON g.project_id = f.project_id
// AND g.bucket = f.bucket
// AND g.data_depth = 3
// AND g.folder_type = 'gallery'
// AND g.path1 = f.path1
// AND g.path2 = f.path2
// AND g.path3 = f.path3
// WHERE f.project_id = '${projectId}'
// AND f.bucket = '${bucket}'
// AND f.is_folder = false
// AND f.is_folder = false
// AND f.is_removed = false
// AND f.data_depth = 4
// AND f.ext in ('png', 'jpg', 'jpeg', 'webp', 'gif', 'mp4', 'webm', 'mov')
// AND f.thumbnail_key is null;
// `
let queryString = `
SELECT f.*
FROM ver4.${tbData} as f
WHERE f.project_id = $1
AND f.is_folder = false
AND f.is_removed = false
AND f.data_depth >= 4
AND f.ext in ('png', 'jpg', 'jpeg', 'webp', 'mp4', 'webm', 'mov', 'pdf', 'hwp', 'hwpx', 'doc', 'docx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'dwg', 'dxf', 'grm', 'txt', 'md', 'gif')
AND f.preview_key is not null
AND f.popup_key is not null
AND f.thumbnail_key is null;
`;
// let queryString = `
// SELECT f.*
// FROM ver4.${tbData} as f
// WHERE f.project_id = $1
// AND f.bucket = $2
// AND f.is_folder = false
// AND f.is_removed = false
// AND f.data_depth = 4
// AND f.ext in ('png', 'jpg', 'jpeg', 'webp', 'gif', 'mp4', 'webm', 'mov', 'pdf')
// AND f.thumbnail_key is null;
// `;
let values = [projectId];
let { rows } = await client.query(queryString, values)
// let { rows } = await client.query(queryString, [projectId, bucket]);
return rows;
}catch(err) {
console.error('getNullThumbnailDataInfoAction err:', err)
}finally {
client.release();
}
}
// 이전 파일 썸네일 추가 - update
exports.updateThumbnailInfo = async (req, res) => {
const projectId = req.baseUrl.split('/')[1];
let result = await updateThumbnailInfoAction(projectId, req.body);
res.status(200).json(result);
}
async function updateThumbnailInfoAction(projectId, params) {
let bucket = projectId;
const client = await pool.connect();
try {
const {
data_id,
thumbnail_key,
thumbnail_size,
lon,
lat,
height,
mod_user_id
} = params;
let queryString = `
UPDATE ver4.${tbData}
SET
thumbnail_key = $1,
thumbnail_size = $2,
lon = $3,
lat = $4,
height = $5,
mod_user_id = $6
WHERE data_id = $7
AND project_id = $8
AND bucket = $9
RETURNING data_id, thumbnail_key, thumbnail_size;
`;
let values = [thumbnail_key, thumbnail_size, lon, lat, height, mod_user_id, data_id, projectId, bucket];
let { rows } = await client.query(queryString, values);
if (rows.length > 0) {
return {
message: 'updateThumbnailInfo_success',
result: rows[0],
}
}else {
return { message: 'updateThumbnailInfo_not_found' };
}
}catch(err) {
console.error('updateThumbnailInfoAction err:', err);
return {
message: 'updateThumbnailInfo_error',
error: err.message
};
} finally {
client.release();
}
}
// 삭제예정(2025.10.31): 이호성
// exports.get3dViewerThumbUrl = async(req, res, next) => {
// const dataId = req.query.dataId;
// const client = await pool.connect();
// try {
// let queryString = `
// SELECT t.*
// FROM ver4.${tbData} as t
// WHERE t.data_id = $1
// `;
// let values = [dataId];
// let { rows } = await client.query(queryString, values);
// if (!rows.length) {
// return res.status(404).json({ message: 'Not found', dataId });
// }
// const { thumbnail_key, path1, path2, path3, path4, path5 } = rows[0];
// let resourcePath = path.posix.join(path1, path2, path3, path4);
// if (path5) resourcePath = resourcePath + '/' + path5;
// const basename = path.posix.basename(resourcePath);
// const dirname = path.posix.dirname(resourcePath);
// const fileNameWithoutExt = path.posix.parse(basename).name;
// const thumbnailPath = path.posix.join(dirname, `${fileNameWithoutExt}.jpeg`);
// res.json({ thumbnail_key, resourcePath, thumbnailPath });
// } catch(err) {
// console.error('get3dViewerThumbUrl err:', err)
// next(err)
// } finally {
// client.release();
// }
// }