/** * [변경 이력 (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, overview } = 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, overview, create_date) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 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', 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`, `Overview enabled: ${overview !== false}` ]); 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, 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, overview = $6 WHERE project_id = $7 RETURNING *; `; 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: "대상을 찾을 수 없습니다." }); } 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}`, `Overview enabled: ${overview}` ]); 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, user_pw, company, dept, position, group, is_resigned } = req.body; const client = await pool.connect(); try { 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; 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) { 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}` ]); 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); 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(); } };