const pool = require("../db/pool.js"); const { getIo } = require('../socket'); const dotenv = require('dotenv'); dotenv.config(); //// s3 api 관련 const { getSignedUrl } = require('@aws-sdk/s3-request-presigner'); const { DeleteObjectCommand, PutObjectCommand, GetObjectCommand, } = require('@aws-sdk/client-s3'); //// 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 tbOfficialDocFile = env == 'production' ? 'tb_official_doc_file' : 'tb_official_doc_file'; const tbOfficialDocCompany = env == 'production' ? 'tb_official_doc_company' : 'tb_official_doc_company'; //// 큐 관련 const { convertPdfQueue, } = require('../queue.js'); // queue.js에서 행동별 Queue 객체 생성 후 사용 //// 변환 필요 확장자, 변환 불필요 확장자, 지원여부 확장자 // -> pdf 암호화 안하는 버전 - 변환 불필요 확장자에 pdf 포함 let needConvertExtArr = ['hwp', 'hwpx', 'doc', 'docx', 'xls', 'xlsx', 'xlsm', 'ppt', 'pptx', 'dwg', 'dxf', 'grm']; let notNeedConvertExtArr = ['pdf', 'ifc', 'gsim', 'mp4', 'webm', 'jpg', 'jpeg', 'png', 'txt', 'log', 'md', 'url', 'zip']; // -> 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 convertingDocPathArr = []; /************************************************************************************************************************* 유틸리티 함수 */ // storageType 에 따라 클라이언트 리턴 function getBasePrefix(pageType) { if (pageType == null || pageType == undefined) { pageType = 'officialDoc'; } return { origin: `${pageType}/origin/`, pdf: `${pageType}/pdf/`, pdf_thumb: `${pageType}/pdf_thumb/`, }; } 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; 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}`; } /************************************************************************************************************************* DB 관련 함수 */ exports.getDocSeq = async (req, res, next) => { const client = await pool.connect(); try { let queryString = `SELECT nextval('ver4.seq_tb_official_doc_file'::regclass)`; const result = await client.query(queryString); res.json({ success: true, message: '200', data: result.rows[0].nextval, }); } catch (error) { console.error('getDocSeq error: ', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } }; exports.getDocData = async (req, res, next) => { const projectId = req.baseUrl.split('/')[1]; const label = req.query.label; const client = await pool.connect(); try { let queryString = ` SELECT ROW_NUMBER() OVER (PARTITION BY doc_direction ORDER BY doc_id DESC) as rownum, doc.doc_id, doc.project_id, doc.uploader, doc.create_date, doc."permission", doc.bucket, doc.file_path, doc.ext, doc.doc_direction, doc.doc_number, doc.doc_date, doc.recipient_org, doc.recipient_org_abbr, doc.recipient_name, doc.recipient_name_abbr, doc.sender_org, doc.sender_org_abbr, doc.sender_name, doc.sender_name_abbr, doc.doc_title, doc.doc_title_summary, doc.doc_content_summary, doc.doc_related_docs, doc.doc_type, doc.doc_category, doc.attachment_title, doc.attachment_count, doc.doc_memo, doc.doc_manager, doc.doc_label, doc.mod_date, doc.mod_user_id, doc.mod_activity, doc.data_size, doc.storage_type, doc.object_key, doc.preview_key, doc.popup_key, doc.ver, doc.group_id, doc.is_aiused FROM ver4.${tbOfficialDocFile} doc WHERE project_id = $1 `; const values = [projectId]; if (label) { queryString += ` AND doc.doc_label = $2`; values.push(label); } queryString += ` ORDER BY doc.doc_id`; const result = await client.query(queryString, values); res.json({ success: true, message: 'getDocData_success', data: result.rows, }); } catch (error) { console.error('getDocData error: ', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } }; exports.getDocDataBySelected = async (req, res, next) => { const client = await pool.connect(); const { projectId, companyType, base, target, category, storageType, direction } = req.query; const docCategory = category; const docDirection = direction; try { // 카운트 쿼리 let selectCount = ` SELECT doc.doc_category, count(*) `; // 리스트 쿼리 let selectList = ` SELECT ROW_NUMBER() OVER (PARTITION BY doc_direction ORDER BY doc_id) as rownum, doc.doc_id, doc.project_id, doc.uploader, doc.create_date, doc."permission", doc.bucket, doc.file_path, doc.ext, doc.doc_direction, doc.doc_number, doc.doc_date, doc.recipient_org, doc.recipient_org_abbr, doc.recipient_name, doc.recipient_name_abbr, doc.sender_org, doc.sender_org_abbr, doc.sender_name, doc.sender_name_abbr, doc.doc_title, doc.doc_title_summary, doc.doc_content_summary, doc.doc_related_docs, doc.doc_type, doc.doc_category, doc.attachment_title, doc.attachment_count, doc.doc_memo, doc.doc_manager, doc.doc_label, doc.mod_date, doc.mod_user_id, doc.mod_activity, doc.data_size, doc.storage_type, doc.object_key, doc.preview_key, doc.popup_key, doc.ver, doc.group_id, doc.is_aiused, base.company_name AS base, target.company_name AS target `; let queryString = ` FROM ver4.${tbOfficialDocFile} doc JOIN ver4.${tbOfficialDocCompany} base ON doc.project_id = base.project_id AND base.company_type = $1 AND base.company_role = '기준' AND base.company_name = $2 JOIN ver4.${tbOfficialDocCompany} target ON doc.project_id = target.project_id AND target.company_type = $1 AND target.company_role = '상대기관' AND target.company_name = $3 WHERE doc.project_id = $4 AND ( (doc.recipient_org_abbr = base.company_name AND doc.sender_org_abbr = target.company_name) OR (doc.recipient_org_abbr = target.company_name AND doc.sender_org_abbr = base.company_name) ) AND doc.bucket = $4 AND doc.storage_type = $5 `; // 카운트 쿼리 let groupBy = ` GROUP BY doc.doc_category `; // 리스트 쿼리 // let orderBy = ` ORDER BY doc.doc_id DESC `; // 카운트 쿼리 조합 let countQuery = selectCount + queryString + groupBy; let countParams = [companyType, base, target, projectId, storageType]; const countResult = await client.query(countQuery, countParams); let listQuery = selectList + queryString; let listParams = [companyType, base, target, projectId, storageType]; let paramIndex = 6; // 1) category 있을 때만 if (docCategory && docCategory !== '') { listQuery += ` AND doc.doc_category = $${paramIndex}`; listParams.push(docCategory); paramIndex++; } // 2) direction 있을 때만(수/발신) if (docDirection && docDirection !== '') { listQuery += ` AND doc.doc_direction = $${paramIndex}`; listParams.push(docDirection); paramIndex++; } // 3) ORDER BY listQuery += orderBy; const listResult = await client.query(listQuery, listParams); res.json({ success: true, message: '200', countData: countResult.rows, listData: listResult.rows, }); } catch (error) { console.error('getData error: ', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } }; // 설정모달 회사리스트 가져오기 exports.getCompanyList = async (req, res, next) => { const client = await pool.connect(); const projectId = req.params.projectId; try { let queryString = ` SELECT project_id, company_name, company_type, company_role, company_id FROM ver4.${tbOfficialDocCompany} WHERE project_id = $1 ORDER BY company_type, company_role `; const result = await client.query(queryString, [projectId]); res.json({ success: true, message: '200', data: result.rows, }); } catch (error) { console.error('getCompanyList error', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } }; // 셀렉트 박스 회사리스트 exports.getGroupCompanyData = async (req, res) => { const client = await pool.connect(); const projectId = req.params.projectId; try { const query = ` SELECT project_id, company_name, company_type, company_role, company_id FROM ver4.${tbOfficialDocCompany} WHERE project_id = $1 ORDER BY company_type `; const result = await client.query(query, [projectId]); const companyList = result.rows; // 그룹핑 const grouped = {}; companyList.forEach((item) => { const type = item.company_type; // 발주처 / 발주처외 const role = item.company_role; // 기준 / 상대기관 const name = item.company_name; if (!grouped[type]) grouped[type] = { 기준: [], 상대기관: [] }; grouped[type][role].push({ name, id: item.company_id }); }); res.json({ success: true, message: '200', data: grouped, }); } catch (error) { console.error('getGroupCompanyData error', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } }; // 설정모달 저장버튼 : 회사 관련 exports.saveCompanyList = async (req, res, next) => { const client = await pool.connect(); const deletedCompanyList = req.body.deletedCompanyList || []; const companyList = req.body.companyList; try { // 삭제 먼저 for (let i = 0; i < deletedCompanyList.length; i++) { const item = deletedCompanyList[i]; const projectId = item.project_id; const companyId = item.company_id; const deleteQuery = ` DELETE FROM ver4.${tbOfficialDocCompany} WHERE TRIM(project_id) = $1 AND company_id = $2 `; const deleteValues = [projectId, companyId]; await client.query(deleteQuery, deleteValues); } // 그다음 수정추가 반영 for (let i = 0; i < companyList.length; i++) { const item = companyList[i]; const projectId = item.project_id; const companyId = item.company_id; const companyName = item.company_name; const companyType = item.company_type; const companyRole = item.company_role; if (!companyId || companyId === 'undefined' || companyId === 'null' || companyId.trim() === '') { // 새로 추가 const insertQuery = ` INSERT INTO ver4.${tbOfficialDocCompany} (project_id, company_name, company_type, company_role) VALUES ($1, $2, $3, $4) `; const insertValues = [projectId, companyName, companyType, companyRole]; await client.query(insertQuery, insertValues); } else { // 수정 or 기존값 존재 const updateQuery = ` INSERT INTO ver4.${tbOfficialDocCompany} (project_id, company_id, company_name, company_type, company_role) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (project_id, company_id) DO UPDATE SET company_name = EXCLUDED.company_name, company_type = EXCLUDED.company_type, company_role = EXCLUDED.company_role `; const updateValues = [projectId, companyId, companyName, companyType, companyRole]; await client.query(updateQuery, updateValues); } } res.json({ success: true, message: '200', }); } catch (error) { console.error('saveCompanyList error', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } }; // 설정모달 저장버튼 : 사용자 설정 관련 exports.saveUserSetting = async (req, res, next) => { const client = await pool.connect(); const projectId = req.baseUrl.split('/')[1]; const userInfo = req.body.userInfo ? JSON.parse(req.body.userInfo) : undefined; let userId = (userInfo) ? userInfo.user_id : undefined; const { isInstructionsChecked } = req.body; if (!userId) return; try { const queryString = ` INSERT INTO ver4.tb_user_setting (project_id, user_id, doc_option_instructions) VALUES($1, $2, $3) ON CONFLICT (project_id, user_id) DO UPDATE SET doc_option_instructions = EXCLUDED.doc_option_instructions; `; const values = [projectId, userId, isInstructionsChecked]; await client.query(queryString, values); res.json({ success: true, message: 'setting_saved', }); } catch (error) { console.error('saveUserSetting error: ', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } } exports.getUserSetting = async (req, res, next) => { const client = await pool.connect(); const projectId = req.baseUrl.split('/')[1]; const userInfo = req.query.userInfoString ? JSON.parse(req.query.userInfoString) : undefined; let userId = (userInfo) ? userInfo.user_id : undefined; if (!userId) return; try { const queryString = ` SELECT doc_option_instructions, doc_option_summary FROM ver4.tb_user_setting WHERE project_id = $1 AND user_id = $2 `; const values = [projectId, userId]; const result = await client.query(queryString, values); res.json({ success: true, data: result.rows[0], }); } catch (error) { console.error('getUserSetting error: ', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } }; /************************************************************************************************************************* 비즈니스 로직 함수 */ exports.uploadDocData = async (req, res, next) => { const projectId = req.baseUrl.split('/')[1]; const { mode, params } = req.body; params.projectId = projectId; try { if (mode === 'modal_add') { // 모달 추가모드 (파일 1개) const result = await insertDocRows(params, req); if (result !== 'insertDocRows_success') throw new Error('DB 저장 실패'); return res.status(200).json({ message: 'modal_add_success' }); } else if (mode === 'modal_edit') { // 모달 수정모드 (파일 없음) const result = await updateDocRows(params, req); if (result !== 'updateDocRows_success') throw new Error('DB 수정 실패'); return res.status(200).json({ message: 'modal_edit_success' }); } else if (mode === 'add_attach') { // 일반 첨부파일 추가 (멀티파일) const result = await insertDocRows(params, req); if (result !== 'insertDocRows_success') throw new Error('DB 저장 실패'); return res.status(200).json({ message: 'add_attach_success' }); } else if (mode === 'exist') { // 중복파일 const result = await updateExistRows(params, req); if (result !== 'updateExistRows_success') throw new Error('DB 저장 실패'); return res.status(200).json({ message: 'updateExistRows_success' }); } else { return res.status(400).json({ message: 'Invalid mode' }); } } catch (error) { console.error('uploadDocData error:', error); next(error); } }; exports.generateUploadDocUrl = async (req, res, next) => { const projectId = req.baseUrl.split('/')[1]; let pageType; let { resourcePath, date } = req.body; let bucket = projectId; let fullPath = getBasePrefix(pageType).origin + resourcePath; fullPath = fullPath.replaceAll('\\', '/'); fullPath = fullPath.replaceAll('//', '/'); let objectKey = `${fullPath}__${makeObjectKeyTimestamp(date)}`; let url; try { // S3 명령어 구성 const command = new PutObjectCommand({ Bucket: bucket, Key: objectKey, ContentType: 'application/octet-stream', }); // Presigned URL 생성 옵션 // - expiresIn: 유효 기간 (초 단위, 여기선 5분) url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 }); } catch (error) { console.error('❌ Presigned URL 생성 실패:', error); } // 클라이언트에 Presigned URL, 키, 메타데이터 반환 res.status(200).json({ message: 'generateUploadDocUrl_success', result: { url: url, objectKey: objectKey, date: date, }, }); }; exports.generateGetDocUrl = async (req, res, next) => { const projectId = req.baseUrl.split('/')[1]; let { objectKey } = req.query; let bucket = projectId; let url; try { const command = new GetObjectCommand({ Bucket: bucket, Key: objectKey, }); url = await getSignedUrl(s3, command, { expiresIn: 60 }); } catch (error) { console.error('❌ Presigned URL 가져오기 실패:', error); } res.status(200).json({ message: 'generateGetDocUrl_success', result: { url: url, objectKey: objectKey, }, }); }; exports.generateDownloadDocUrl = async (req, res, next) => { const projectId = req.baseUrl.split('/')[1]; let { objectKey, resourcePath } = req.body; let bucket = projectId; let fileName = resourcePath.split('/').pop(); try { let command = new GetObjectCommand({ Bucket: bucket, Key: objectKey, // 아래 두 옵션을 넣으면 브라우저가 파일을 바로 열지 않고 다운로드로 처리하도록 서버 응답 헤더를 강제설정할 수 있음: ResponseContentDisposition: `attachment; filename="${encodeURIComponent(fileName)}"`, ResponseCacheControl: 'no-cache', }); let url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 }); // 5분 유효 res.status(200).json({ message: 'generateDownloadDocUrl_success', url: url }); } catch (error) { console.error('❌ Download Presigned URL 생성 실패:', error); } } exports.generateDeleteDocUrl = async (req, res, next) => { const projectId = req.baseUrl.split('/')[1]; let { objectKey } = req.body; let bucket = projectId; let url; try { const command = new DeleteObjectCommand({ Bucket: bucket, Key: objectKey, }); url = await getSignedUrl(s3, command, { expiresIn: 60 }); } catch (error) { console.error('❌ Presigned URL 삭제 생성 실패:', error); } res.status(200).json({ message: 'generateDeleteDocUrl_success', result: { url: url, objectKey: objectKey, }, }); }; exports.deleteDocData = async (req, res, next) => { const projectId = req.baseUrl.split('/')[1]; const { docId, groupId, objectKey, isExists } = req.body; const bucket = projectId; try { // 공문파일인지 첨부파일인지 확인 let isOfficialDoc = !objectKey.includes('__attachment/'); // 공문파일 삭제일 때만 모든 첨부파일을 삭제 let deleteKeyArr = [objectKey]; // 중복파일인 경우에는 첨부파일 삭제 X, DB 삭제 X if (isExists) { // 중복파일이므로 MinIO에서 해당 파일 삭제는 해야함 const deleteCommand = new DeleteObjectCommand({ Bucket: bucket, Key: objectKey, }); await s3.send(deleteCommand); // 첨부파일 삭제는 건너뛰고 바로 성공 응답 return res.status(200).json({ message: 'exist_file_delete_success' }); } if (!isOfficialDoc) { // 첨부파일의 경우 const attachmentKeys = await getAttachmentObjectKeys(projectId, groupId); attachmentKeys.forEach(item => { if (item.object_key === objectKey && item.popup_key !== null) { deleteKeyArr.push(item.popup_key); } }); } else { // 공문파일의 경우 const attachmentKeys = await getAttachmentObjectKeys(projectId, groupId); attachmentKeys.forEach(item => { if (item.object_key !== objectKey) { deleteKeyArr.push(item.object_key); // 첨부파일 object_key 추가 if (item.popup_key !== null) { deleteKeyArr.push(item.popup_key); // popup_key 있으면 추가 } } }); } // 1. DB 삭제 const deleteDocRowsRes = await deleteDocRows(projectId, docId, groupId, isOfficialDoc); if (!deleteDocRowsRes.success) { return res.status(400).json({ message: 'db_delete_failed', detail: dbResult.message }); } // 2. MinIO 파일 삭제 for (let key of deleteKeyArr) { const deleteCommand = new DeleteObjectCommand({ Bucket: bucket, Key: key, }); await s3.send(deleteCommand); } // 성공 응답 return res.status(200).json({ message: 'delete_success' }); } catch (error) { console.error('❌ deleteDocData error:', error); return res.status(500).json({ message: 'delete_failed', error: error.message }); } }; exports.renameDocTarget = async(req, res) => { let { params } = req.body; let { newPath, userInfoString, docId, storageType } = params; const projectId = req.baseUrl.split('/')[1]; 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 queryString = ` UPDATE ver4.tb_official_doc_file SET file_path = $1, mod_user_id = $2, mod_date = $3 WHERE doc_id = $4 AND project_id = $5 AND bucket = $6 AND storage_type = $7; `; const result = await client.query(queryString, [newPath, userId, dateNow, docId, projectId, bucket, storageType ]); res.json({ success: true, message: 'renameDocTarget_success', data: result.rows, }); } catch (error) { console.error('renameDocTarget error', error); res.status(500).json({ success: false, message: '500', error: error.message, }); } finally { client.release(); } } async function insertDocRows(params) { const client = await pool.connect(); let result; let projectId = params.projectId; let userInfo = params.userInfoString ? JSON.parse(params.userInfoString) : undefined; let storageType = params.uploadParams?.storageType; let dateArr = params.uploadParams?.dateArr; let resourcePathArr = params.uploadParams?.resourcePathArr; let sizeArr = params.uploadParams?.sizeArr; let objectKeyArr = params.uploadParams?.objectKeyArr; let bucket = projectId; let userId = (userInfo) ? userInfo.user_id : undefined; if (!userId) return; let { doc_id, doc_direction, doc_number, doc_date, recipient_org, recipient_org_abbr, recipient_name, recipient_name_abbr, sender_org, sender_org_abbr, sender_name, sender_name_abbr, doc_title, doc_title_summary, doc_content_summary, doc_related_docs, doc_type, doc_category, attachment_title, attachment_count, doc_memo, doc_manager, doc_label, group_id, is_aiused } = params; try { let insertCols = [ 'group_id', 'project_id', 'create_date', 'uploader', 'bucket', 'file_path', 'ext', 'doc_direction', 'doc_number', 'doc_date', 'recipient_org', 'recipient_org_abbr', 'recipient_name', 'recipient_name_abbr', 'sender_org', 'sender_org_abbr', 'sender_name', 'sender_name_abbr', 'doc_title', 'doc_title_summary', 'doc_content_summary', 'doc_related_docs', 'doc_type', 'doc_category', 'attachment_title', 'attachment_count', 'doc_memo', 'doc_manager', 'doc_label', 'data_size', 'storage_type', 'object_key', 'preview_key', 'popup_key', 'is_aiused' ]; let values = []; let placeholders = []; for (let i = 0; i < dateArr.length; i++) { let createDate = makePostgresTimestamp(dateArr[i]); let resourcePath; if (resourcePathArr) resourcePath = resourcePathArr[i]; resourcePath = resourcePath.startsWith('/') ? resourcePath.slice(1) : resourcePath; let ext = (resourcePath.split('.').pop()).replace('.', '').toLowerCase(); let objectKey = null; if (objectKeyArr) objectKey = objectKeyArr[i]; let previewKey = null, popupKey = null; if (notNeedConvertExtArr.includes(ext)) { previewKey = objectKey; popupKey = objectKey; } let row = []; row.push( group_id, projectId, makePostgresTimestamp(dateArr[i]), userId, bucket, resourcePathArr[i], ext, doc_direction, doc_number, doc_date, recipient_org, recipient_org_abbr, recipient_name, recipient_name_abbr, sender_org, sender_org_abbr, sender_name, sender_name_abbr, doc_title, doc_title_summary, doc_content_summary, doc_related_docs, doc_type, doc_category, attachment_title, attachment_count, doc_memo, doc_manager, doc_label, sizeArr[i], storageType, objectKeyArr[i], previewKey, popupKey, is_aiused ); values.push(...row); const base = i * row.length; placeholders.push(`(${row.map((_, j) => `$${base + j + 1}`).join(', ')})`); } const queryString = ` INSERT INTO ver4.tb_official_doc_file (${insertCols.join(', ')}) VALUES ${placeholders.join(', ')} RETURNING *; `; await client.query(queryString, values); result = 'insertDocRows_success'; return result; } catch (error) { console.error("insertDocRows error:", error); } finally { client.release(); } }; async function updateDocRows(params) { let result; let projectId = params.projectId; let bucket = projectId; let userInfo = params.userInfoString ? JSON.parse(params.userInfoString) : undefined; let userId = (userInfo) ? userInfo.user_id : undefined; let storageType = params.uploadParams?.storageType; let activity = params.activity; let dateNow = Date.now(); let modDate = makePostgresTimestamp(dateNow); if (!userId) return; const client = await pool.connect(); let { doc_id, doc_direction, doc_number, doc_date, recipient_org, recipient_org_abbr, recipient_name, recipient_name_abbr, sender_org, sender_org_abbr, sender_name, sender_name_abbr, doc_title, doc_title_summary, doc_content_summary, doc_related_docs, doc_type, doc_category, attachment_title, attachment_count, doc_memo, doc_manager, doc_label, mod_date, mod_user_id, mod_activity } = params; const values = [ doc_direction, doc_number, doc_date, recipient_org, recipient_org_abbr, recipient_name, recipient_name_abbr, sender_org, sender_org_abbr, sender_name, sender_name_abbr, doc_title, doc_title_summary, doc_content_summary, doc_related_docs, doc_type, doc_category, attachment_title, attachment_count, doc_memo, doc_manager, doc_label, modDate, mod_user_id, mod_activity, doc_id ]; try { let queryString = ` UPDATE ver4.${tbOfficialDocFile} SET doc_direction=$1, doc_number=$2, doc_date=$3, recipient_org=$4, recipient_org_abbr=$5, recipient_name=$6, recipient_name_abbr=$7, sender_org=$8, sender_org_abbr=$9, sender_name=$10, sender_name_abbr=$11, doc_title=$12, doc_title_summary=$13, doc_content_summary=$14, doc_related_docs=$15, doc_type=$16, doc_category=$17, attachment_title=$18, attachment_count=$19, doc_memo=$20, doc_manager=$21, doc_label=$22, mod_date=$23, mod_user_id=$24, mod_activity=$25 WHERE doc_id=$26 `; await client.query(queryString, values); result = 'updateDocRows_success'; return result; } catch (error) { console.error("updateDocRows error:", error); } finally { client.release(); } } async function updateExistRows(params) { let result; let projectId = params.projectId; let bucket = projectId; let userInfo = params.userInfoString ? JSON.parse(params.userInfoString) : undefined; let userId = (userInfo) ? userInfo.user_id : undefined; let storageType = params.uploadParams?.storageType; let activity = params.activity; let dateNow = Date.now(); let modDate = makePostgresTimestamp(dateNow); if (!userId) return; const client = await pool.connect(); let { doc_id, mod_date, mod_user_id, mod_activity, object_key, preview_key, popup_key } = params; const values = [ modDate, userId, mod_activity, object_key, preview_key, popup_key, doc_id ]; try { let queryString = ` UPDATE ver4.${tbOfficialDocFile} SET mod_date=$1, mod_user_id=$2, mod_activity=$3, object_key=$4, preview_key=$5, popup_key=$6 WHERE doc_id=$7 `; await client.query(queryString, values); result = 'updateExistRows_success'; return result; } catch (error) { console.error("updateExistRows error:", error); } finally { client.release(); } } async function deleteDocRows(projectId, docId, groupId, isOfficialDoc) { const client = await pool.connect(); try { let queryString; let values; if (isOfficialDoc) { // 공문이면 group_id로 첨부파일까지 전체 삭제 queryString = ` DELETE FROM ver4.${tbOfficialDocFile} WHERE project_id = $1 AND group_id = $2 `; values = [projectId, groupId]; } else { // 첨부파일이면 doc_id로 해당 파일만 삭제 queryString = ` DELETE FROM ver4.${tbOfficialDocFile} WHERE project_id = $1 AND doc_id = $2 `; values = [projectId, docId]; } await client.query(queryString, values); return { success: true }; } catch (error) { console.error('deleteDocRows error:', error); return { success: false, message: 'DB 삭제 오류', error }; } finally { client.release(); } } exports.convertDocPdf = async(req, res) => { const projectId = req.baseUrl.split('/')[1]; let bucket = projectId; let userIp = req.ip; let { params } = req.body; let { storageType, objectKey, resourcePath, docId, userInfoString } = params; //// 배열에 현재 변환중인 파일 경로 추가 convertingDocPathArr.push(resourcePath); //// 변환 시작 socket 전송 let resultData = { resourcePath: resourcePath, convertingDocPathArr: convertingDocPathArr }; let io = getIo(); io.emit('convertDoc_start', resultData); let command = new GetObjectCommand({ Bucket: bucket, Key: objectKey, }); let url = await getSignedUrl(s3, command, { expiresIn: 60 * 5 }); // 5분 유효 // 대기 중인 작업 수 확인 let waitingCount = await convertPdfQueue.getWaitingCount(); // 작업 시작자(initiator) 결정 let initiator = `DEV_LOCAL_${JSON.parse(params.userInfoString).user_id}`; if (env == 'production') { if (deploymentType == 'ONPREMISE') initiator = 'HYHC_ONPREMISE'; if (deploymentType == 'CLOUD') initiator = `AWS_CLOUD_${cloudType}`; } let type = 'officialDoc'; let dataId = docId; // add() 함수로 큐에 새로운 작업을 추가 // queue.add(name, data, options); // name: 작업의 이름 // data: 작업에 대한 데이터, 작업이 처리될 때 필요한 데이터나 인자를 포함. 이 파라미터는 객체 형태로 전달됨 // option: 작업에 대한 옵션(선택적) const job = await convertPdfQueue.add( `'${initiator}'에서 문서를 PDF로 변환`, { resourcePath, url, objectKey, bucket, storageType, dataId, projectId, userInfoString, userIp, initiator, type }, // { jobId: `pdf-${Date.now()}` } // ← 원하는 ID 값 지정 ); res.status(200).json({ jobId: job.id, waiting: waitingCount }); // queue.js에서 변환 완료/실패 후 소켓으로 convertPdf_success/convertPdf_failed 이벤트 전송 } async function getAttachmentObjectKeys(projectId, groupId) { const client = await pool.connect(); try { const queryString = ` SELECT object_key, popup_key FROM ver4.${tbOfficialDocFile} WHERE project_id = $1 AND group_id = $2 AND object_key LIKE '%__attachment/%' `; const result = await client.query(queryString, [projectId, groupId]); return result.rows.map(row => ({ object_key: row.object_key, popup_key: row.popup_key })); } catch (error) { console.error('❌ getAttachmentObjectKeys error:', error); return []; } finally { client.release(); } } exports.getExtractKey = async (req, res) => { // const apiKey = 'sk-a518fc3f1955915440d402f95de982de'; // IP 기반 호출 const apiKey = 'sk-dc2c9312ac5538366bb61d4dee3d5832'; // 도메인 기반 호출 if (apiKey) { res.status(200).json({ message: 'getExtractKey_success', apiKey: apiKey, }); } else { res.status(400).json({ message: '🚫 API 키가 없습니다.', }); } } exports.checkDocTargetExists = async (req, res) => { const projectId = req.baseUrl.split('/')[1]; let { params } = req.body; params.projectId = projectId; let checkDocTargetExistsActionResult = await checkDocTargetExistsAction(params); if (checkDocTargetExistsActionResult.message == 'checkDocTargetExistsAction_success') { res.status(200).json({ message: 'checkDocTargetExists_success', rows: checkDocTargetExistsActionResult.results }); } } async function checkDocTargetExistsAction(params) { let { projectId, storageType, docLabel, resourcePathArr } = params; let bucket = projectId; resourcePathArr = JSON.parse(resourcePathArr); const client = await pool.connect(); try { let results = []; for (const path of resourcePathArr) { const queryString = ` SELECT doc_id, project_id, bucket, file_path, ext, doc_label, storage_type, object_key, preview_key, popup_key, group_id FROM ver4.tb_official_doc_file WHERE file_path = $1 AND project_id = $2 AND bucket = $3 AND storage_type = $4 AND doc_label = $5 `; const { rows } = await client.query(queryString, [path, projectId, bucket, storageType, docLabel]); if (rows.length != 0) results.push({ rows: rows }); } return { message: 'checkDocTargetExistsAction_success', results }; } catch (error) { console.error("checkDocTargetExistsAction err: ", error); } finally { client.release(); } } exports.docGeminiAiAction = async(req, res) => { if (!req.files) { console.log('파일없음') return res.status(400).send("파일이 없습니다."); } const inputFile = req.files['input_file'][0]; // input_file const promptFile = req.files['prompt_file'][0]; // prompt_file const schemaFile = req.files['schema_file'][0]; // schema_file if (!inputFile || !promptFile || !schemaFile) return res.status(400).send('파일 없음'); // console.log(inputFile, promptFile, schemaFile); try { // 1. 파일 버퍼 가져오기 및 인코딩 const inputFileBuffer = inputFile.buffer; const base64Pdf = inputFileBuffer.toString('base64'); // 이미지테스트 // const base64Image = inputFileBuffer.toString('base64'); const promptFileBuffer = promptFile.buffer; const promptText = promptFileBuffer.toString('utf-8'); // 2. 스키마 json 파싱 const schemaBuffer = schemaFile.buffer; const schemaText = schemaBuffer.toString('utf8'); const schema = JSON.parse(schemaText); if (!inputFileBuffer || !promptFileBuffer) { return res.status(400).send('파일을 처리하는데 오류가 발생했습니다.'); } // 3. Gemini API 요청 const { GoogleGenerativeAI } = require('@google/generative-ai'); const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' }); const result = await model.generateContent({ contents: [ // contents 배열로 감싸기 { parts: [ { text: promptText }, // 텍스트(프롬프트) 부분 { inline_data: { // pdf(파일) 부분 mime_type: 'application/pdf', data: base64Pdf, // mime_type: 'image/jpeg', // 이미지테스트 //image/png // data: base64Image, // 이미지테스트 }, }, ], }, ], generationConfig: { responseMimeType: 'application/json', responseSchema: schema, }, }); const json = await result.response.text(); res.status(200).json({ data: json, }) // 예상 토큰수 확인 const countResult = await model.countTokens({ contents: [ { role: 'user', parts: [ { text: promptText }, { inline_data: { // pdf(파일) 부분 mime_type: 'application/pdf', data: base64Pdf, // mime_type: 'image/jpeg', // 이미지테스트 //image/png // data: base64Image, // 이미지테스트 }, } ] } ] }); console.log(">> 예상 사용 토큰 수:", countResult.totalTokens); } catch(err) { // console.error(err); if(err.status == 429) { console.error("🚫 사용량 초과! 잠시 후 다시 시도하세요."); } else { throw err; } } }