한글뷰어 기능수정

This commit is contained in:
koj729
2026-06-18 08:52:23 +09:00
parent cb0c42fbeb
commit 9268e4e6bc
38 changed files with 2544 additions and 211 deletions

View File

@@ -76,7 +76,7 @@ exports.getProjects = async (req, res) => {
};
exports.createProject = async (req, res) => {
const { project_id, project_nm, short_nm, category, limit_storage, is_active } = req.body;
const { project_id, project_nm, short_nm, category, limit_storage, is_active, overview } = req.body;
if (!project_id || !project_nm) {
return res.status(400).json({ error: "프로젝트 ID와 명칭은 필수입니다." });
}
@@ -93,8 +93,8 @@ exports.createProject = async (req, res) => {
const storage_byte = limit_storage ? parseInt(limit_storage) * 1024 * 1024 * 1024 : 0;
const query = `
INSERT INTO ver4.${tbProject} (project_id, project_nm, short_nm, category, storage_byte, is_active, user_id, create_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, CURRENT_TIMESTAMP)
INSERT INTO ver4.${tbProject} (project_id, project_nm, short_nm, category, storage_byte, is_active, user_id, overview, create_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP)
RETURNING *;
`;
const result = await client.query(query, [
@@ -104,13 +104,15 @@ exports.createProject = async (req, res) => {
category || null,
storage_byte,
is_active ?? true,
req.user?.user_id || 'admin'
req.user?.user_id || 'admin',
overview !== false // 기본값 true
]);
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog(project_id, 'createProject', req.user?.user_id, userIp, [
`Project Name: ${project_nm}`,
`Category: ${category}`,
`Storage limit: ${limit_storage} GB`
`Storage limit: ${limit_storage} GB`,
`Overview enabled: ${overview !== false}`
]);
res.status(201).json(result.rows[0]);
@@ -124,18 +126,18 @@ exports.createProject = async (req, res) => {
exports.updateProject = async (req, res) => {
const { id } = req.params;
const { project_nm, short_nm, category, limit_storage, is_active } = req.body;
const { project_nm, short_nm, category, limit_storage, is_active, overview } = req.body;
const client = await pool.connect();
try {
const storage_byte = limit_storage ? parseInt(limit_storage) * 1024 * 1024 * 1024 : 0;
const query = `
UPDATE ver4.${tbProject}
SET project_nm = $1, short_nm = $2, category = $3, storage_byte = $4, is_active = $5
WHERE project_id = $6
SET project_nm = $1, short_nm = $2, category = $3, storage_byte = $4, is_active = $5, overview = $6
WHERE project_id = $7
RETURNING *;
`;
const result = await client.query(query, [project_nm, short_nm || null, category || null, storage_byte, is_active, id]);
const result = await client.query(query, [project_nm, short_nm || null, category || null, storage_byte, is_active, overview, id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
@@ -144,7 +146,8 @@ exports.updateProject = async (req, res) => {
`Project Name: ${project_nm}`,
`Category: ${category}`,
`Storage limit: ${limit_storage} GB`,
`Active status: ${is_active}`
`Active status: ${is_active}`,
`Overview enabled: ${overview}`
]);
res.status(200).json(result.rows[0]);
} catch (err) {
@@ -517,37 +520,62 @@ exports.createUser = async (req, res) => {
exports.updateUser = async (req, res) => {
const { id } = req.params;
const { user_nm, company, dept, position, group, is_resigned } = req.body;
const { user_nm, user_pw, company, dept, position, group, is_resigned } = req.body;
const client = await pool.connect();
try {
const query = `
UPDATE ver4.tb_user
SET user_nm = $1, company = $2, dept = $3, position = $4, "group" = $5, is_resigned = $6
WHERE user_id = $7
RETURNING *;
`;
const result = await client.query(query, [
user_nm,
company || null,
dept || null,
position || null,
group || null,
is_resigned,
id
]);
let result;
if (user_pw && user_pw.trim() !== '') {
const passwordHash = crypto.createHash('sha256').update(user_pw).digest('hex');
const query = `
UPDATE ver4.tb_user
SET user_nm = $1, user_pw = $2, company = $3, dept = $4, position = $5, "group" = $6, is_resigned = $7
WHERE user_id = $8
RETURNING *;
`;
result = await client.query(query, [
user_nm,
passwordHash,
company || null,
dept || null,
position || null,
group || null,
is_resigned,
id
]);
} else {
const query = `
UPDATE ver4.tb_user
SET user_nm = $1, company = $2, dept = $3, position = $4, "group" = $5, is_resigned = $6
WHERE user_id = $7
RETURNING *;
`;
result = await client.query(query, [
user_nm,
company || null,
dept || null,
position || null,
group || null,
is_resigned,
id
]);
}
if (result.rows.length === 0) {
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
}
const user = result.rows[0];
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
await insertAuditLog('SYSTEM', 'updateUser', req.user?.user_id, userIp, [
const details = [
`Updated user_id: ${id}`,
`User name: ${user_nm}`,
`Group: ${group}`,
`Is resigned: ${is_resigned}`
]);
];
if (user_pw && user_pw.trim() !== '') {
details.push("Password was updated");
}
await insertAuditLog('SYSTEM', 'updateUser', req.user?.user_id, userIp, details);
user.user_pw = undefined;
res.status(200).json(user);
} catch (err) {
@@ -671,6 +699,13 @@ exports.updateSystemPolicy = async (req, res) => {
`Limit days: ${limit_days}`,
`Is active: ${is_active}`
]);
try {
const { getIo } = require('../../socket.js');
const io = getIo();
io.emit('updateSystemPolicy_success', result.rows[0]);
} catch (socketErr) {
console.error("Failed to emit updateSystemPolicy_success:", socketErr.message);
}
res.status(200).json(result.rows[0]);
} catch (err) {
console.error("updateSystemPolicy Error:", err);

View File

@@ -2581,6 +2581,64 @@ exports.uploadData = async (req, res, next) => {
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,
@@ -2881,8 +2939,81 @@ exports.removeTarget = async(req, res) => {
let permission = JSON.parse(params.userInfoString).permission;
let depth = getDepth(params.resourcePathArr[0]);
let isRecycleBinModal = params.isRecycleBinModal;
const isExpiredFolder = params.isExpiredFolder === true;
if (!isRecycleBinModal && (depth == 1 && permission < 191) || (depth >= 2 && permission < 7)) {
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',
});
@@ -3221,7 +3352,7 @@ exports.addConvetPdfLog = async(req, res) => {
exports.removeConvertingData = async(req, res) => {
const projectId = req.baseUrl.split('/')[1];
let { params } = req.body;
let { resourcePath, dataId, userInfoString } = params;
let { resourcePath, dataId, userInfoString, stdout } = params;
//// 배열에서 파일 정보 삭제
convertingDataArr = convertingDataArr.filter(data => data.dataId !== dataId);
@@ -3238,7 +3369,8 @@ exports.removeConvertingData = async(req, res) => {
convertingDataArr: convertingDataArr,
resourcePath: resourcePath,
dataId: dataId,
userInfoString: userInfoString
userInfoString: userInfoString,
stdout: stdout || ''
};
let io = getIo();
@@ -4363,6 +4495,7 @@ async function updateThumbnailInfoAction(projectId, params) {
}
}
// 삭제예정(2025.10.31): 이호성
// exports.get3dViewerThumbUrl = async(req, res, next) => {
// const dataId = req.query.dataId;