1032 lines
42 KiB
JavaScript
1032 lines
42 KiB
JavaScript
/**
|
|
* [변경 이력 (Auto-Generated by AI)]
|
|
* - 수정일시: 2026-06-15 11:40:00
|
|
* - 수정원인: 폴더별 권한 관리 트리 노드의 data_permission 기준 정렬 요구사항 반영
|
|
* - 수정내용: getFolderPermissions API의 foldersQuery에 data_permission 컬럼을 조회하도록 SELECT절 추가
|
|
*/
|
|
const pool = require("../../db/pool.js");
|
|
const crypto = require("crypto");
|
|
|
|
const env = process.env.NODE_ENV;
|
|
const tbProject = env === 'production' ? 'tb_project' : '_test_tb_project';
|
|
const tbData = env === 'production' ? 'tb_data' : '_test_tb_data';
|
|
const tbLog = env === 'production' ? 'tb_log' : '_test_tb_log';
|
|
const tbPermission = env === 'production' ? 'tb_permission' : '_test_tb_permission';
|
|
const tbFolderPermission = env === 'production' ? 'tb_folder_permission' : '_test_tb_folder_permission';
|
|
|
|
|
|
// 감사 로그(Audit Log) 삽입 헬퍼 함수 (메인 트랜잭션에 영향을 주지 않기 위해 pool.query 사용)
|
|
async function insertAuditLog(projectId, activity, userId, userIp, detailsArray) {
|
|
try {
|
|
let targetProjectId = projectId;
|
|
if (targetProjectId === 'SYSTEM' || !targetProjectId) {
|
|
targetProjectId = null;
|
|
} else {
|
|
// 실제로 존재하는 프로젝트인지 더블 체크 (FK 에러 방지)
|
|
const checkRes = await pool.query(`SELECT 1 FROM ver4.${tbProject} WHERE project_id = $1`, [targetProjectId]);
|
|
if (checkRes.rows.length === 0) {
|
|
targetProjectId = null;
|
|
}
|
|
}
|
|
|
|
const query = `
|
|
INSERT INTO ver4.${tbLog} (project_id, activity, user_id, user_ip, log_date, path_arr)
|
|
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, $5);
|
|
`;
|
|
await pool.query(query, [
|
|
targetProjectId,
|
|
activity,
|
|
userId || 'unknown_admin',
|
|
userIp || '0.0.0.0',
|
|
detailsArray || []
|
|
]);
|
|
} catch (logErr) {
|
|
console.error("🚨 [insertAuditLog] Audit log insert failed:", logErr);
|
|
}
|
|
}
|
|
|
|
// 1. 프로젝트 관리 (Projects)
|
|
exports.getProjects = async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
SELECT
|
|
p.*,
|
|
cd.code_nm as category_nm,
|
|
COALESCE(d.total_size, 0)::BIGINT as used_bytes,
|
|
COALESCE(d.file_count, 0)::INTEGER as file_count
|
|
FROM ver4.${tbProject} p
|
|
LEFT JOIN ver4.code_detail cd ON cd.main_code = 'PROJECT_CATEGORY' AND p.category = cd.sub_code
|
|
LEFT JOIN (
|
|
SELECT project_id, SUM(COALESCE(data_size, 0)) as total_size, COUNT(*) as file_count
|
|
FROM ver4.${tbData}
|
|
WHERE is_folder = false AND (is_removed = false OR is_removed IS NULL)
|
|
GROUP BY project_id
|
|
) d ON p.project_id = d.project_id
|
|
ORDER BY p.project_id;
|
|
`;
|
|
const result = await client.query(query);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getProjects Error:", err);
|
|
res.status(500).json({ error: "프로젝트 목록 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.createProject = async (req, res) => {
|
|
const { project_id, project_nm, short_nm, category, limit_storage, is_active } = req.body;
|
|
if (!project_id || !project_nm) {
|
|
return res.status(400).json({ error: "프로젝트 ID와 명칭은 필수입니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
// 중복 체크
|
|
const dupRes = await client.query(`SELECT 1 FROM ver4.${tbProject} WHERE project_id = $1`, [project_id]);
|
|
if (dupRes.rows.length > 0) {
|
|
return res.status(400).json({ error: "이미 존재하는 프로젝트 ID입니다." });
|
|
}
|
|
|
|
// storage_byte 계산 (GB -> Bytes)
|
|
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)
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [
|
|
project_id,
|
|
project_nm,
|
|
short_nm || null,
|
|
category || null,
|
|
storage_byte,
|
|
is_active ?? true,
|
|
req.user?.user_id || 'admin'
|
|
]);
|
|
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`
|
|
]);
|
|
res.status(201).json(result.rows[0]);
|
|
|
|
} catch (err) {
|
|
console.error("createProject Error:", err);
|
|
res.status(500).json({ error: "프로젝트 생성 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.updateProject = async (req, res) => {
|
|
const { id } = req.params;
|
|
const { project_nm, short_nm, category, limit_storage, is_active } = 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
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [project_nm, short_nm || null, category || null, storage_byte, is_active, id]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(id, 'updateProject', req.user?.user_id, userIp, [
|
|
`Project Name: ${project_nm}`,
|
|
`Category: ${category}`,
|
|
`Storage limit: ${limit_storage} GB`,
|
|
`Active status: ${is_active}`
|
|
]);
|
|
res.status(200).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("updateProject Error:", err);
|
|
res.status(500).json({ error: "프로젝트 수정 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.deleteProject = async (req, res) => {
|
|
const { id } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
// [3대 삭제 제한 1] 프로젝트 사용 이력 체크
|
|
// tb_data 사용 이력 검사
|
|
const dataCountRes = await client.query(`SELECT COUNT(*) FROM ver4.${tbData} WHERE project_id = $1`, [id]);
|
|
if (parseInt(dataCountRes.rows[0].count) > 0) {
|
|
return res.status(400).json({ error: `해당 현장에 업로드된 파일 데이터(${dataCountRes.rows[0].count}건)가 존재하여 프로젝트를 삭제할 수 없습니다.` });
|
|
}
|
|
|
|
// tb_official_doc_file 사용 이력 검사
|
|
const docCountRes = await client.query("SELECT COUNT(*) FROM ver4.tb_official_doc_file WHERE project_id = $1", [id]);
|
|
if (parseInt(docCountRes.rows[0].count) > 0) {
|
|
return res.status(400).json({ error: `해당 현장에 연결된 공문서 파일(${docCountRes.rows[0].count}건)이 존재하여 프로젝트를 삭제할 수 없습니다.` });
|
|
}
|
|
|
|
// tb_banner_notice 사용 이력 검사
|
|
const noticeCountRes = await client.query("SELECT COUNT(*) FROM ver4.tb_banner_notice WHERE project_id = $1", [id]);
|
|
if (parseInt(noticeCountRes.rows[0].count) > 0) {
|
|
return res.status(400).json({ error: `해당 현장 전용 배너 공지 이력(${noticeCountRes.rows[0].count}건)이 존재하여 프로젝트를 삭제할 수 없습니다.` });
|
|
}
|
|
|
|
// 통과 시 삭제 수행 (tb_permission 등 Cascade 관계는 수동으로 정리 가능하나, 안전을 위해 권한도 함께 정리함)
|
|
await client.query(`DELETE FROM ver4.${tbPermission} WHERE project_id = $1`, [id]);
|
|
const result = await client.query(`DELETE FROM ver4.${tbProject} WHERE project_id = $1 RETURNING *`, [id]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(id, 'deleteProject', req.user?.user_id, userIp, [
|
|
`Deleted project name: ${result.rows[0].project_nm}`
|
|
]);
|
|
res.status(200).json({ message: "프로젝트가 정상적으로 삭제되었습니다." });
|
|
} catch (err) {
|
|
console.error("deleteProject Error:", err);
|
|
res.status(500).json({ error: "프로젝트 삭제 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
// 2. 프로젝트 권한 배정 (Permissions)
|
|
exports.getProjectPermissions = async (req, res) => {
|
|
const { projectId } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
SELECT pm.project_id, pm.user_id, pm.lev, u.user_nm, u.company, u.dept, u.position
|
|
FROM ver4.${tbPermission} pm
|
|
JOIN ver4.tb_user u ON pm.user_id = u.user_id
|
|
WHERE pm.project_id = $1
|
|
ORDER BY u.user_nm ASC;
|
|
`;
|
|
const result = await client.query(query, [projectId]);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getProjectPermissions Error:", err);
|
|
res.status(500).json({ error: "권한 목록 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.assignPermissions = async (req, res) => {
|
|
const { project_id, users } = req.body; // users: [{ user_id, lev }]
|
|
if (!project_id || !users || !Array.isArray(users) || users.length === 0) {
|
|
return res.status(400).json({ error: "잘못된 요청 파라미터입니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query("BEGIN");
|
|
for (const user of users) {
|
|
await client.query(`
|
|
INSERT INTO ver4.${tbPermission} (project_id, user_id, lev)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (project_id, user_id)
|
|
DO UPDATE SET lev = EXCLUDED.lev;
|
|
`, [project_id, user.user_id, user.lev]);
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(project_id, 'assignPermission', req.user?.user_id, userIp, [
|
|
`Assigned user_id: ${user.user_id}`,
|
|
`Level assigned: ${user.lev}`
|
|
]);
|
|
}
|
|
await client.query("COMMIT");
|
|
res.status(200).json({ message: "사용자가 성공적으로 현장에 배정되었습니다." });
|
|
} catch (err) {
|
|
await client.query("ROLLBACK");
|
|
console.error("assignPermissions Error:", err);
|
|
res.status(500).json({ error: "사용자 권한 배정 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.updatePermission = async (req, res) => {
|
|
const { project_id, user_id, lev } = req.body;
|
|
if (!project_id || !user_id || lev === undefined) {
|
|
return res.status(400).json({ error: "필수 파라미터가 누락되었습니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
UPDATE ver4.${tbPermission}
|
|
SET lev = $1
|
|
WHERE project_id = $2 AND user_id = $3
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [lev, project_id, user_id]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(project_id, 'updatePermission', req.user?.user_id, userIp, [
|
|
`Updated user_id: ${user_id}`,
|
|
`New level: ${lev}`
|
|
]);
|
|
res.status(200).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("updatePermission Error:", err);
|
|
res.status(500).json({ error: "권한 등급 수정 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.removePermission = async (req, res) => {
|
|
const { project_id, user_id } = req.body;
|
|
if (!project_id || !user_id) {
|
|
return res.status(400).json({ error: "필수 파라미터가 누락되었습니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
DELETE FROM ver4.${tbPermission}
|
|
WHERE project_id = $1 AND user_id = $2
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [project_id, user_id]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(project_id, 'removePermission', req.user?.user_id, userIp, [
|
|
`Removed user_id: ${user_id}`
|
|
]);
|
|
res.status(200).json({ message: "사용자 배정이 제외되었습니다." });
|
|
} catch (err) {
|
|
console.error("removePermission Error:", err);
|
|
res.status(500).json({ error: "사용자 배정 제외 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
// 3. 실시간 배너 공지 (Banners)
|
|
exports.getBanners = async (req, res) => {
|
|
const { status, fromDate, toDate } = req.query;
|
|
const client = await pool.connect();
|
|
try {
|
|
let query = `
|
|
SELECT b.*, p.project_nm, cd.code_nm as status_nm
|
|
FROM ver4.tb_banner_notice b
|
|
LEFT JOIN ver4.${tbProject} p ON b.project_id = p.project_id
|
|
LEFT JOIN ver4.code_detail cd ON b.status_code = cd.base_code
|
|
WHERE 1=1
|
|
`;
|
|
const params = [];
|
|
let paramIndex = 1;
|
|
|
|
if (status && status !== 'all') {
|
|
query += ` AND b.status_code = $${paramIndex++}`;
|
|
params.push(status);
|
|
}
|
|
if (fromDate) {
|
|
query += ` AND b.reg_date >= $${paramIndex++}`;
|
|
params.push(fromDate);
|
|
}
|
|
if (toDate) {
|
|
query += ` AND b.reg_date <= $${paramIndex++}`;
|
|
params.push(toDate);
|
|
}
|
|
|
|
query += ` ORDER BY b.banner_id DESC;`;
|
|
const result = await client.query(query, params);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getBanners Error:", err);
|
|
res.status(500).json({ error: "배너 공지 목록 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.createBanner = async (req, res) => {
|
|
const { project_id, start_date, end_date, notice_text } = req.body;
|
|
if (!start_date || !end_date || !notice_text) {
|
|
return res.status(400).json({ error: "시작일, 종료일, 공지 자막은 필수입니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
// 송출 상태 계산 (오늘 기준)
|
|
const today = new Date().toISOString().split('T')[0];
|
|
let status_code = 'NOTICE_STATUS_scheduled';
|
|
if (today >= start_date && today <= end_date) {
|
|
status_code = 'NOTICE_STATUS_active';
|
|
} else if (today > end_date) {
|
|
status_code = 'NOTICE_STATUS_expired';
|
|
}
|
|
|
|
const query = `
|
|
INSERT INTO ver4.tb_banner_notice (project_id, start_date, end_date, notice_text, status_code, reg_date)
|
|
VALUES ($1, $2, $3, $4, $5, CURRENT_DATE)
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [
|
|
project_id === 'all' || !project_id ? null : project_id,
|
|
start_date,
|
|
end_date,
|
|
notice_text,
|
|
status_code
|
|
]);
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(project_id === 'all' || !project_id ? 'SYSTEM' : project_id, 'createBanner', req.user?.user_id, userIp, [
|
|
`Banner text: ${notice_text}`,
|
|
`Period: ${start_date} ~ ${end_date}`
|
|
]);
|
|
res.status(201).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("createBanner Error:", err);
|
|
res.status(500).json({ error: "배너 공지 생성 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.stopBanner = async (req, res) => {
|
|
const { id } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
UPDATE ver4.tb_banner_notice
|
|
SET status_code = 'NOTICE_STATUS_expired'
|
|
WHERE banner_id = $1
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [id]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(result.rows[0].project_id || 'SYSTEM', 'stopBanner', req.user?.user_id, userIp, [
|
|
`Stopped banner_id: ${id}`,
|
|
`Banner text: ${result.rows[0].notice_text}`
|
|
]);
|
|
res.status(200).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("stopBanner Error:", err);
|
|
res.status(500).json({ error: "배너 송출 중지 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
// 4. 사용자 관리 (Users)
|
|
exports.getUsers = async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
SELECT u.user_id, u.user_nm, u.company, u.dept, u.position, u.group, u.is_resigned, u.create_date, cd.code_nm as group_nm
|
|
FROM ver4.tb_user u
|
|
LEFT JOIN ver4.code_detail cd ON u.group = cd.base_code
|
|
ORDER BY u.user_id;
|
|
`;
|
|
const result = await client.query(query);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getUsers Error:", err);
|
|
res.status(500).json({ error: "사용자 목록 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.getUserPermissions = async (req, res) => {
|
|
const { id } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
SELECT pm.project_id, p.project_nm, pm.lev
|
|
FROM ver4.${tbPermission} pm
|
|
JOIN ver4.${tbProject} p ON pm.project_id = p.project_id
|
|
WHERE pm.user_id = $1
|
|
ORDER BY p.project_nm ASC;
|
|
`;
|
|
const result = await client.query(query, [id]);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getUserPermissions Error:", err);
|
|
res.status(500).json({ error: "사용자 참여 현장 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.createUser = async (req, res) => {
|
|
const { user_id, user_nm, user_pw, company, dept, position, group, is_resigned } = req.body;
|
|
if (!user_id || !user_nm || !user_pw) {
|
|
return res.status(400).json({ error: "사용자 ID, 이름, 비밀번호는 필수입니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
// 중복 체크
|
|
const dupRes = await client.query("SELECT 1 FROM ver4.tb_user WHERE user_id = $1", [user_id]);
|
|
if (dupRes.rows.length > 0) {
|
|
return res.status(400).json({ error: "이미 존재하는 사용자 ID입니다." });
|
|
}
|
|
|
|
// 비밀번호 해싱 (SHA256 - GSIM 연동 상의 DB 제약 조건을 채우기 위함)
|
|
const passwordHash = crypto.createHash('sha256').update(user_pw).digest('hex');
|
|
|
|
const query = `
|
|
INSERT INTO ver4.tb_user (user_id, user_nm, user_pw, company, dept, position, "group", is_resigned, create_date)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP)
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [
|
|
user_id,
|
|
user_nm,
|
|
passwordHash,
|
|
company || null,
|
|
dept || null,
|
|
position || null,
|
|
group || null,
|
|
is_resigned ?? false
|
|
]);
|
|
|
|
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', 'createUser', req.user?.user_id, userIp, [
|
|
`Created user_id: ${user_id}`,
|
|
`User name: ${user_nm}`,
|
|
`Group: ${group}`
|
|
]);
|
|
user.user_pw = undefined; // 비밀번호 제외
|
|
res.status(201).json(user);
|
|
} catch (err) {
|
|
console.error("createUser Error:", err);
|
|
res.status(500).json({ error: "사용자 생성 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.updateUser = async (req, res) => {
|
|
const { id } = req.params;
|
|
const { user_nm, 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
|
|
]);
|
|
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, [
|
|
`Updated user_id: ${id}`,
|
|
`User name: ${user_nm}`,
|
|
`Group: ${group}`,
|
|
`Is resigned: ${is_resigned}`
|
|
]);
|
|
user.user_pw = undefined;
|
|
res.status(200).json(user);
|
|
} catch (err) {
|
|
console.error("updateUser Error:", err);
|
|
res.status(500).json({ error: "사용자 정보 수정 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.deleteUser = async (req, res) => {
|
|
const { id } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
// [3대 삭제 제한 2] 사용자 삭제 시 권한 테이블 배정 정보 체크
|
|
const permCountRes = await client.query(`SELECT COUNT(*) FROM ver4.${tbPermission} WHERE user_id = $1`, [id]);
|
|
if (parseInt(permCountRes.rows[0].count) > 0) {
|
|
return res.status(400).json({ error: `해당 사용자가 참여 중인 현장 권한(${permCountRes.rows[0].count}건)이 존재하여 계정을 삭제할 수 없습니다. 배정 해제 후 삭제 가능합니다.` });
|
|
}
|
|
|
|
const result = await client.query("DELETE FROM ver4.tb_user WHERE user_id = $1 RETURNING *", [id]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog('SYSTEM', 'deleteUser', req.user?.user_id, userIp, [
|
|
`Deleted user_id: ${id}`
|
|
]);
|
|
res.status(200).json({ message: "사용자 계정이 성공적으로 삭제되었습니다." });
|
|
} catch (err) {
|
|
console.error("deleteUser Error:", err);
|
|
res.status(500).json({ error: "사용자 삭제 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
// 5. 활동 로그 조회 (Activity Logs)
|
|
exports.getAuditLogs = async (req, res) => {
|
|
const { user_id, activity, project_nm } = req.query;
|
|
const client = await pool.connect();
|
|
try {
|
|
let query = `
|
|
SELECT l.log_id, l.log_date as clean_date, l.project_id, p.project_nm, l.user_id, l.user_ip, l.activity as clean_path, l.path_arr as criteria_info
|
|
FROM ver4.${tbLog} l
|
|
LEFT JOIN ver4.${tbProject} p ON l.project_id = p.project_id
|
|
WHERE 1=1
|
|
`;
|
|
const params = [];
|
|
let paramIndex = 1;
|
|
|
|
if (user_id) {
|
|
query += ` AND l.user_id ILIKE $${paramIndex++}`;
|
|
params.push(`%${user_id}%`);
|
|
}
|
|
if (project_nm) {
|
|
query += ` AND (p.project_nm ILIKE $${paramIndex++} OR l.project_id ILIKE $${paramIndex - 1})`;
|
|
params.push(`%${project_nm}%`);
|
|
}
|
|
if (activity && activity !== 'all') {
|
|
query += ` AND l.activity ILIKE $${paramIndex++}`;
|
|
params.push(`%${activity}%`);
|
|
}
|
|
|
|
query += ` ORDER BY l.log_id DESC LIMIT 100;`;
|
|
const result = await client.query(query, params);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getAuditLogs Error:", err);
|
|
res.status(500).json({ error: "활동 로그 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
// 6. 보관 정책 설정 (Policies)
|
|
exports.getSystemPolicy = async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const result = await client.query("SELECT * FROM ver4.tb_system_policy WHERE policy_key = 'GLOBAL_DELETE_POLICY'");
|
|
if (result.rows.length === 0) {
|
|
// 기본 레코드 없을 시 생성해서 전송
|
|
const insertRes = await client.query(`
|
|
INSERT INTO ver4.tb_system_policy (policy_key, limit_file_count, limit_days, is_active)
|
|
VALUES ('GLOBAL_DELETE_POLICY', 100, 30, FALSE)
|
|
RETURNING *;
|
|
`);
|
|
return res.status(200).json(insertRes.rows[0]);
|
|
}
|
|
res.status(200).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("getSystemPolicy Error:", err);
|
|
res.status(500).json({ error: "보관 정책 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.updateSystemPolicy = async (req, res) => {
|
|
const { limit_file_count, limit_days, is_active } = req.body;
|
|
if (limit_file_count === undefined || limit_days === undefined || is_active === undefined) {
|
|
return res.status(400).json({ error: "필수 정책 입력값이 누락되었습니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
INSERT INTO ver4.tb_system_policy (policy_key, limit_file_count, limit_days, is_active, upd_date)
|
|
VALUES ('GLOBAL_DELETE_POLICY', $1, $2, $3, CURRENT_TIMESTAMP)
|
|
ON CONFLICT (policy_key)
|
|
DO UPDATE SET limit_file_count = EXCLUDED.limit_file_count,
|
|
limit_days = EXCLUDED.limit_days,
|
|
is_active = EXCLUDED.is_active,
|
|
upd_date = CURRENT_TIMESTAMP
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [limit_file_count, limit_days, is_active]);
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog('SYSTEM', 'updateSystemPolicy', req.user?.user_id, userIp, [
|
|
`Limit file count: ${limit_file_count}`,
|
|
`Limit days: ${limit_days}`,
|
|
`Is active: ${is_active}`
|
|
]);
|
|
res.status(200).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("updateSystemPolicy Error:", err);
|
|
res.status(500).json({ error: "보관 정책 수정 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.getAutoCleanLogs = async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
SELECT log_id, clean_date, project_id, clean_path, criteria_info, result_status
|
|
FROM ver4.tb_auto_clean_log
|
|
ORDER BY log_id DESC LIMIT 100;
|
|
`;
|
|
const result = await client.query(query);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getAutoCleanLogs Error:", err);
|
|
res.status(500).json({ error: "배치 이력 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
// 7. 공통 코드 관리 (Common Codes)
|
|
exports.getCodeMasters = async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = "SELECT * FROM ver4.code_master ORDER BY main_code;";
|
|
const result = await client.query(query);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getCodeMasters Error:", err);
|
|
res.status(500).json({ error: "대분류 마스터 코드 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.createCodeMaster = async (req, res) => {
|
|
const { main_code, main_code_nm, use_yn, rmk } = req.body;
|
|
if (!main_code || !main_code_nm) {
|
|
return res.status(400).json({ error: "대분류 코드와 명칭은 필수입니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const dupRes = await client.query("SELECT 1 FROM ver4.code_master WHERE main_code = $1", [main_code]);
|
|
if (dupRes.rows.length > 0) {
|
|
return res.status(400).json({ error: "이미 존재하는 대분류 코드입니다." });
|
|
}
|
|
|
|
const query = `
|
|
INSERT INTO ver4.code_master (main_code, main_code_nm, use_yn, rmk)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [main_code, main_code_nm, use_yn ?? 'Y', rmk || null]);
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog('SYSTEM', 'createCodeMaster', req.user?.user_id, userIp, [
|
|
`Master code: ${main_code}`,
|
|
`Master code name: ${main_code_nm}`
|
|
]);
|
|
res.status(201).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("createCodeMaster Error:", err);
|
|
res.status(500).json({ error: "대분류 등록 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.updateCodeMaster = async (req, res) => {
|
|
const { code } = req.params;
|
|
const { main_code_nm, use_yn, rmk } = req.body;
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
UPDATE ver4.code_master
|
|
SET main_code_nm = $1, use_yn = $2, rmk = $3
|
|
WHERE main_code = $4
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [main_code_nm, use_yn, rmk || null, code]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog('SYSTEM', 'updateCodeMaster', req.user?.user_id, userIp, [
|
|
`Updated master code: ${code}`,
|
|
`New code name: ${main_code_nm}`,
|
|
`Use YN: ${use_yn}`
|
|
]);
|
|
res.status(200).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("updateCodeMaster Error:", err);
|
|
res.status(500).json({ error: "대분류 수정 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.deleteCodeMaster = async (req, res) => {
|
|
const { code } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
// [3대 삭제 제한 3] 대분류 삭제 시 하위 세부 코드 존재 체크
|
|
const detailCountRes = await client.query("SELECT COUNT(*) FROM ver4.code_detail WHERE main_code = $1", [code]);
|
|
if (parseInt(detailCountRes.rows[0].count) > 0) {
|
|
return res.status(400).json({ error: `해당 대분류에 기속된 세부 소분류 코드(${detailCountRes.rows[0].count}건)가 존재하여 대분류를 삭제할 수 없습니다.` });
|
|
}
|
|
|
|
const result = await client.query("DELETE FROM ver4.code_master WHERE main_code = $1 RETURNING *", [code]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog('SYSTEM', 'deleteCodeMaster', req.user?.user_id, userIp, [
|
|
`Deleted master code: ${code}`
|
|
]);
|
|
res.status(200).json({ message: "대분류 코드가 삭제되었습니다." });
|
|
} catch (err) {
|
|
console.error("deleteCodeMaster Error:", err);
|
|
res.status(500).json({ error: "대분류 삭제 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.getCodeDetails = async (req, res) => {
|
|
const { mainCode } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = "SELECT * FROM ver4.code_detail WHERE main_code = $1 ORDER BY sort_ord, sub_code;";
|
|
const result = await client.query(query, [mainCode]);
|
|
res.status(200).json(result.rows);
|
|
} catch (err) {
|
|
console.error("getCodeDetails Error:", err);
|
|
res.status(500).json({ error: "소분류 세부 코드 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.createCodeDetail = async (req, res) => {
|
|
const { main_code, sub_code, code_nm, sort_ord, use_yn, rmk } = req.body;
|
|
if (!main_code || !sub_code || !code_nm) {
|
|
return res.status(400).json({ error: "대분류, 소분류 코드 및 코드 명칭은 필수입니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const dupRes = await client.query("SELECT 1 FROM ver4.code_detail WHERE main_code = $1 AND sub_code = $2", [main_code, sub_code]);
|
|
if (dupRes.rows.length > 0) {
|
|
return res.status(400).json({ error: "해당 대분류 내에 이미 존재하는 소분류 코드입니다." });
|
|
}
|
|
|
|
// 조합 코드 (base_code) 자동 조합: main_code || '_' || sub_code
|
|
const base_code = `${main_code}_${sub_code}`;
|
|
|
|
const query = `
|
|
INSERT INTO ver4.code_detail (main_code, sub_code, base_code, code_nm, sort_ord, use_yn, rmk)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [main_code, sub_code, base_code, code_nm, sort_ord ?? 1, use_yn ?? 'Y', rmk || null]);
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog('SYSTEM', 'createCodeDetail', req.user?.user_id, userIp, [
|
|
`Master code: ${main_code}`,
|
|
`Sub code: ${sub_code}`,
|
|
`Code name: ${code_nm}`
|
|
]);
|
|
res.status(201).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("createCodeDetail Error:", err);
|
|
res.status(500).json({ error: "소분류 등록 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.updateCodeDetail = async (req, res) => {
|
|
const { mainCode, subCode } = req.params;
|
|
const { code_nm, sort_ord, use_yn, rmk } = req.body;
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
UPDATE ver4.code_detail
|
|
SET code_nm = $1, sort_ord = $2, use_yn = $3, rmk = $4
|
|
WHERE main_code = $5 AND sub_code = $6
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [code_nm, sort_ord, use_yn, rmk || null, mainCode, subCode]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog('SYSTEM', 'updateCodeDetail', req.user?.user_id, userIp, [
|
|
`Master code: ${mainCode}`,
|
|
`Sub code: ${subCode}`,
|
|
`New code name: ${code_nm}`
|
|
]);
|
|
res.status(200).json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("updateCodeDetail Error:", err);
|
|
res.status(500).json({ error: "소분류 수정 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
// 7-4. 소분류 코드 삭제
|
|
exports.deleteCodeDetail = async (req, res) => {
|
|
const { mainCode, subCode } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
DELETE FROM ver4.code_detail
|
|
WHERE main_code = $1 AND sub_code = $2
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [mainCode, subCode]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog('SYSTEM', 'deleteCodeDetail', req.user?.user_id, userIp, [
|
|
`Master code: ${mainCode}`,
|
|
`Sub code: ${subCode}`
|
|
]);
|
|
res.status(200).json({ message: "소분류 코드가 삭제되었습니다." });
|
|
} catch (err) {
|
|
console.error("deleteCodeDetail Error:", err);
|
|
res.status(500).json({ error: "소분류 삭제 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
// 2-1. Folder-Level Permissions
|
|
exports.getFolderPermissions = async (req, res) => {
|
|
const { projectId } = req.params;
|
|
const client = await pool.connect();
|
|
try {
|
|
// 1. Fetch folders (depth <= 3)
|
|
const foldersQuery = `
|
|
SELECT data_id, path1, path2, path3, data_depth, is_folder, data_permission
|
|
FROM ver4.${tbData}
|
|
WHERE project_id = $1 AND is_folder = true AND is_removed = false AND data_depth <= 3
|
|
ORDER BY path1, path2, path3;
|
|
`;
|
|
const foldersRes = await client.query(foldersQuery, [projectId]);
|
|
|
|
// 2. Fetch current folder permissions
|
|
const folderPermsQuery = `
|
|
SELECT fp.folder_permission_id, fp.folder_path_key, fp.user_id, fp.lev, u.user_nm
|
|
FROM ver4.${tbFolderPermission} fp
|
|
JOIN ver4.tb_user u ON fp.user_id = u.user_id
|
|
WHERE fp.project_id = $1;
|
|
`;
|
|
const folderPermsRes = await client.query(folderPermsQuery, [projectId]);
|
|
|
|
// 3. Fetch project users
|
|
const usersQuery = `
|
|
SELECT pm.user_id, u.user_nm, u.company, u.dept, u.position, pm.lev as project_lev
|
|
FROM ver4.${tbPermission} pm
|
|
JOIN ver4.tb_user u ON pm.user_id = u.user_id
|
|
WHERE pm.project_id = $1
|
|
ORDER BY u.user_nm ASC;
|
|
`;
|
|
const usersRes = await client.query(usersQuery, [projectId]);
|
|
|
|
res.status(200).json({
|
|
folders: foldersRes.rows,
|
|
folderPermissions: folderPermsRes.rows,
|
|
users: usersRes.rows
|
|
});
|
|
} catch (err) {
|
|
console.error("getFolderPermissions Error:", err);
|
|
res.status(500).json({ error: "폴더 권한 정보 조회 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.assignFolderPermissions = async (req, res) => {
|
|
const { project_id, folder_path_key, user_id, lev } = req.body;
|
|
if (!project_id || !folder_path_key || !user_id || lev === undefined) {
|
|
return res.status(400).json({ error: "필수 파라미터가 누락되었습니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
INSERT INTO ver4.${tbFolderPermission} (project_id, folder_path_key, user_id, lev, mod_date)
|
|
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP)
|
|
ON CONFLICT (project_id, folder_path_key, user_id)
|
|
DO UPDATE SET lev = EXCLUDED.lev, mod_date = CURRENT_TIMESTAMP
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [project_id, folder_path_key, user_id, lev]);
|
|
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(project_id, 'assignFolderPermission', req.user?.user_id, userIp, [
|
|
`Folder path: ${folder_path_key}`,
|
|
`Assigned user_id: ${user_id}`,
|
|
`Folder level assigned: ${lev}`
|
|
]);
|
|
|
|
res.status(200).json({
|
|
message: "폴더 권한이 성공적으로 부여되었습니다.",
|
|
data: result.rows[0]
|
|
});
|
|
} catch (err) {
|
|
console.error("assignFolderPermissions Error:", err);
|
|
res.status(500).json({ error: "폴더 권한 부여 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|
|
|
|
exports.removeFolderPermission = async (req, res) => {
|
|
const { project_id, folder_path_key, user_id } = req.body;
|
|
if (!project_id || !folder_path_key || !user_id) {
|
|
return res.status(400).json({ error: "필수 파라미터가 누락되었습니다." });
|
|
}
|
|
|
|
const client = await pool.connect();
|
|
try {
|
|
const query = `
|
|
DELETE FROM ver4.${tbFolderPermission}
|
|
WHERE project_id = $1 AND folder_path_key = $2 AND user_id = $3
|
|
RETURNING *;
|
|
`;
|
|
const result = await client.query(query, [project_id, folder_path_key, user_id]);
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "대상을 찾을 수 없습니다." });
|
|
}
|
|
|
|
const userIp = req.headers['cf-connecting-ip'] || req.ip || req.headers['x-forwarded-for'] || req.connection?.remoteAddress;
|
|
await insertAuditLog(project_id, 'removeFolderPermission', req.user?.user_id, userIp, [
|
|
`Folder path: ${folder_path_key}`,
|
|
`Removed user_id: ${user_id}`
|
|
]);
|
|
|
|
res.status(200).json({ message: "폴더 권한이 제거되었으며 상속 상태로 초기화되었습니다." });
|
|
} catch (err) {
|
|
console.error("removeFolderPermission Error:", err);
|
|
res.status(500).json({ error: "폴더 권한 삭제 실패" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
};
|