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) { let { projectId, activity, userInfoString, userIp, resourcePathArr, dataIdArr, isExpiredFolder } = params; if (from) { console.log(); console.log(`=======================================`); console.log(`${from}에서 insertLog 실행 (${makePostgresTimestamp()})`); console.log(userInfoString); console.log('isExpiredFolder: ', isExpiredFolder); console.log(`=======================================`); console.log(); } if (activity == 'removeTarget_folder' && isExpiredFolder) activity = `${activity}_expired` let userInfo = JSON.parse(userInfoString); let userId = userInfo.user_id; let dateNow = makePostgresTimestamp(Date.now()); const client = await pool.connect(); try { let parsedResourcePathArr = resourcePathArr; if (typeof resourcePathArr === 'string') { try { parsedResourcePathArr = JSON.parse(resourcePathArr); } catch(e) {} } if (!Array.isArray(parsedResourcePathArr)) { parsedResourcePathArr = [parsedResourcePathArr]; } let parsedDataIdArr = dataIdArr; if (typeof dataIdArr === 'string') { try { parsedDataIdArr = JSON.parse(dataIdArr); } catch(e) {} } if (!Array.isArray(parsedDataIdArr)) { parsedDataIdArr = [parsedDataIdArr]; } let values = [ projectId, activity, userId, userIp, dateNow, parsedResourcePathArr, parsedDataIdArr ]; let queryString = ` INSERT INTO ver4.${tbLog} ( project_id, activity, user_id, user_ip, log_date, path_arr, data_id_arr ) VALUES ( $1, $2, $3, $4, $5, $6, $7 ); `; // console.log('================='); // console.log(queryString); await client.query(queryString, values); let result = { message: 'insertLog_success' }; return result; } catch(error) { console.error("insertLog err:", error); return { message: 'insertLog_failed', error: error }; } finally { client.release(); } // console.log('============================'); // console.log(projectId); // console.log(activity); // console.log(userId); // console.log(userIp); // console.log(resourcePathArr); // console.log(dataIdArr); } async function insertClickLog(params) { let { projectId, activity, userInfoString, userIp, resourcePathArr, dataIdArr } = params; let userInfo = JSON.parse(userInfoString); let userId = userInfo.user_id; let dateNow = makePostgresTimestamp(Date.now()); const client = await pool.connect(); try { let parsedResourcePathArr = resourcePathArr; if (typeof resourcePathArr === 'string') { try { parsedResourcePathArr = JSON.parse(resourcePathArr); } catch(e) {} } if (!Array.isArray(parsedResourcePathArr)) { parsedResourcePathArr = [parsedResourcePathArr]; } let parsedDataIdArr = dataIdArr; if (typeof dataIdArr === 'string') { try { parsedDataIdArr = JSON.parse(dataIdArr); } catch(e) {} } if (!Array.isArray(parsedDataIdArr)) { parsedDataIdArr = [parsedDataIdArr]; } let values = [ projectId, activity, userId, userIp, dateNow, parsedResourcePathArr, parsedDataIdArr ]; let queryString = ` INSERT INTO ver4.${tbClickLog} ( project_id, activity, user_id, user_ip, log_date, path_arr, data_id_arr ) VALUES ( $1, $2, $3, $4, $5, $6, $7 ); `; // console.log('================='); // console.log(queryString); await client.query(queryString, values); let result = { message: 'insertClickLog_success' }; return result; } catch(error) { console.error("insertClickLog err:", error); } finally { client.release(); } } /* 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 { 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 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(); // } // }