Files
PM_test/controllers/officialDocController.js
2026-06-12 17:14:03 +09:00

1362 lines
46 KiB
JavaScript

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