/** * 사용자 관리 컨트롤러 * * 사용자 CRUD 및 상태 관리 기능을 제공하는 컨트롤러 * * @author TK-FB-Project * @since 2025-12-11 */ const bcrypt = require('bcrypt'); const { ValidationError, ForbiddenError, NotFoundError, ConflictError, DatabaseError } = require('../utils/errors'); const { asyncHandler } = require('../middlewares/errorHandler'); const logger = require('../utils/logger'); /** * 관리자 권한 확인 헬퍼 함수 */ const checkAdminPermission = (user) => { if (!user || !['admin', 'system'].includes(user.access_level)) { throw new ForbiddenError('관리자 권한이 필요합니다'); } }; /** * 모든 사용자 조회 */ const getAllUsers = asyncHandler(async (req, res) => { checkAdminPermission(req.user); logger.info('사용자 목록 조회 요청', { requestedBy: req.user?.username }); const { getDb } = require('../dbPool'); const db = await getDb(); try { const query = ` SELECT u.user_id, u.username, u.name, u.email, u.role_id, r.name as role, u._access_level_old as access_level, u.is_active, u.worker_id, w.worker_name, w.department_id, d.department_name, u.created_at, u.updated_at, u.last_login_at as last_login FROM users u LEFT JOIN roles r ON u.role_id = r.id LEFT JOIN workers w ON u.worker_id = w.worker_id LEFT JOIN departments d ON w.department_id = d.department_id ORDER BY u.created_at DESC `; const [users] = await db.execute(query); logger.info('사용자 목록 조회 성공', { count: users.length }); res.json({ success: true, data: users, message: '사용자 목록 조회 성공' }); } catch (error) { logger.error('사용자 목록 조회 실패', { error: error.message }); throw new DatabaseError('사용자 목록을 조회하는데 실패했습니다'); } }); /** * 특정 사용자 조회 */ const getUserById = asyncHandler(async (req, res) => { const { id } = req.params; if (!id || isNaN(id)) { throw new ValidationError('유효하지 않은 사용자 ID입니다'); } logger.info('사용자 조회 요청', { userId: id }); const { getDb } = require('../dbPool'); const db = await getDb(); try { const query = ` SELECT user_id, username, name, email, phone, role, access_level, is_active, created_at, updated_at, last_login FROM users WHERE user_id = ? `; const [users] = await db.execute(query, [id]); if (users.length === 0) { throw new NotFoundError('사용자를 찾을 수 없습니다'); } logger.info('사용자 조회 성공', { userId: id, username: users[0].username }); res.json({ success: true, data: users[0], message: '사용자 조회 성공' }); } catch (error) { if (error instanceof NotFoundError) { throw error; } logger.error('사용자 조회 실패', { userId: id, error: error.message }); throw new DatabaseError('사용자를 조회하는데 실패했습니다'); } }); /** * 새 사용자 생성 */ const createUser = asyncHandler(async (req, res) => { checkAdminPermission(req.user); const { username, name, email, phone, role, password } = req.body; logger.info('사용자 생성 요청', { username, name, role }); // 필수 필드 검증 if (!username || !name || !role || !password) { throw new ValidationError('필수 필드가 누락되었습니다', { required: ['username', 'name', 'role', 'password'], received: { username, name, role, password: '***' } }); } // 사용자명 유효성 검증 if (username.length < 3 || username.length > 20) { throw new ValidationError('사용자명은 3-20자 사이여야 합니다'); } // 비밀번호 유효성 검증 if (password.length < 6) { throw new ValidationError('비밀번호는 최소 6자 이상이어야 합니다'); } // 권한 레벨 검증 const validRoles = ['admin', 'group_leader', 'worker']; if (!validRoles.includes(role)) { throw new ValidationError('유효하지 않은 권한입니다', { valid: validRoles, received: role }); } const { getDb } = require('../dbPool'); const db = await getDb(); try { // 사용자명 중복 확인 const checkQuery = 'SELECT user_id FROM users WHERE username = ?'; const [existing] = await db.execute(checkQuery, [username]); if (existing.length > 0) { throw new ConflictError('이미 존재하는 사용자명입니다'); } // 비밀번호 해시화 const hashedPassword = await bcrypt.hash(password, 10); // 사용자 생성 const insertQuery = ` INSERT INTO users (username, name, email, phone, role, access_level, password_hash, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 1, NOW()) `; const [result] = await db.execute(insertQuery, [ username, name, email || null, phone || null, role, role, // access_level을 role과 동일하게 설정 hashedPassword ]); logger.info('사용자 생성 성공', { userId: result.insertId, username, name, role, createdBy: req.user.username }); res.status(201).json({ success: true, data: { user_id: result.insertId }, message: '사용자가 성공적으로 생성되었습니다' }); } catch (error) { if (error instanceof ConflictError) { throw error; } logger.error('사용자 생성 실패', { username, error: error.message }); throw new DatabaseError('사용자를 생성하는데 실패했습니다'); } }); /** * 사용자 정보 수정 */ const updateUser = asyncHandler(async (req, res) => { checkAdminPermission(req.user); const { id } = req.params; const { username, name, email, role, role_id, password, worker_id } = req.body; if (!id || isNaN(id)) { throw new ValidationError('유효하지 않은 사용자 ID입니다'); } logger.info('사용자 수정 요청', { userId: id, body: req.body }); // 최소 하나의 수정 필드가 필요 if (!username && !name && email === undefined && !role && !role_id && !password && worker_id === undefined) { throw new ValidationError('수정할 필드가 없습니다'); } const { getDb } = require('../dbPool'); const db = await getDb(); try { // 사용자 존재 확인 const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?'; const [existing] = await db.execute(checkQuery, [id]); if (existing.length === 0) { throw new NotFoundError('사용자를 찾을 수 없습니다'); } if (existing[0].is_active === 0) { throw new ValidationError('비활성화된 사용자는 수정할 수 없습니다'); } // 업데이트할 필드들 const updates = []; const values = []; if (username) { if (username.length < 3 || username.length > 20) { throw new ValidationError('사용자명은 3-20자 사이여야 합니다'); } // 사용자명 중복 확인 (자신 제외) const dupQuery = 'SELECT user_id FROM users WHERE username = ? AND user_id != ?'; const [duplicate] = await db.execute(dupQuery, [username, id]); if (duplicate.length > 0) { throw new ConflictError('이미 존재하는 사용자명입니다'); } updates.push('username = ?'); values.push(username); } if (name) { updates.push('name = ?'); values.push(name); } if (email !== undefined) { updates.push('email = ?'); values.push(email || null); } // role_id 또는 role 문자열 처리 if (role_id) { // role_id가 유효한지 확인 const [roleCheck] = await db.execute('SELECT id, name FROM roles WHERE id = ?', [role_id]); if (roleCheck.length === 0) { throw new ValidationError('유효하지 않은 역할 ID입니다'); } updates.push('role_id = ?'); values.push(role_id); logger.info('role_id로 역할 변경', { userId: id, role_id, role_name: roleCheck[0].name }); } else if (role) { // role 문자열을 role_id로 변환 (하위 호환성) const roleNameMap = { 'admin': 'Admin', 'system': 'System Admin', 'user': 'User', 'guest': 'Guest', 'group_leader': 'User', // 임시 매핑 'worker': 'User' // 임시 매핑 }; const roleName = roleNameMap[role.toLowerCase()] || role; const [roleCheck] = await db.execute('SELECT id FROM roles WHERE name = ?', [roleName]); if (roleCheck.length === 0) { throw new ValidationError(`유효하지 않은 권한입니다: ${role}`); } updates.push('role_id = ?'); values.push(roleCheck[0].id); logger.info('role 문자열로 역할 변경', { userId: id, role, role_id: roleCheck[0].id }); } if (password) { if (password.length < 6) { throw new ValidationError('비밀번호는 최소 6자 이상이어야 합니다'); } const hashedPassword = await bcrypt.hash(password, 10); updates.push('password = ?'); values.push(hashedPassword); } // worker_id 업데이트 (null도 허용 - 연결 해제) if (worker_id !== undefined) { if (worker_id !== null) { // worker_id가 유효한지 확인 const [workerCheck] = await db.execute('SELECT worker_id, worker_name FROM workers WHERE worker_id = ?', [worker_id]); if (workerCheck.length === 0) { throw new ValidationError('유효하지 않은 작업자 ID입니다'); } logger.info('작업자 연결', { userId: id, worker_id, worker_name: workerCheck[0].worker_name }); } else { logger.info('작업자 연결 해제', { userId: id }); } updates.push('worker_id = ?'); values.push(worker_id); } updates.push('updated_at = NOW()'); values.push(id); const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE user_id = ?`; logger.info('실행할 UPDATE 쿼리', { query: updateQuery, values }); await db.execute(updateQuery, values); logger.info('사용자 수정 성공', { userId: id, username: existing[0].username, updatedFields: Object.keys(req.body), updatedBy: req.user.username }); res.json({ success: true, data: { user_id: id }, message: '사용자 정보가 성공적으로 수정되었습니다' }); } catch (error) { if (error instanceof NotFoundError || error instanceof ValidationError || error instanceof ConflictError) { throw error; } logger.error('사용자 수정 실패', { userId: id, error: error.message, stack: error.stack }); throw new DatabaseError('사용자 정보를 수정하는데 실패했습니다'); } }); /** * 사용자 상태 변경 (활성화/비활성화) */ const updateUserStatus = asyncHandler(async (req, res) => { checkAdminPermission(req.user); const { id } = req.params; const { is_active } = req.body; if (!id || isNaN(id)) { throw new ValidationError('유효하지 않은 사용자 ID입니다'); } if (is_active === undefined || ![0, 1, true, false].includes(is_active)) { throw new ValidationError('유효하지 않은 활성 상태 값입니다'); } const activeValue = is_active === true || is_active === 1 ? 1 : 0; // 자기 자신 비활성화 방지 if (parseInt(id) === req.user.user_id && activeValue === 0) { throw new ValidationError('자기 자신을 비활성화할 수 없습니다'); } logger.info('사용자 상태 변경 요청', { userId: id, is_active: activeValue }); const { getDb } = require('../dbPool'); const db = await getDb(); try { // 사용자 존재 확인 const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?'; const [users] = await db.execute(checkQuery, [id]); if (users.length === 0) { throw new NotFoundError('사용자를 찾을 수 없습니다'); } // 상태 변경이 필요한지 확인 if (users[0].is_active === activeValue) { const status = activeValue === 1 ? '활성' : '비활성'; throw new ValidationError(`사용자가 이미 ${status} 상태입니다`); } const query = 'UPDATE users SET is_active = ?, updated_at = NOW() WHERE user_id = ?'; await db.execute(query, [activeValue, id]); const statusText = activeValue === 1 ? '활성화' : '비활성화'; logger.info(`사용자 ${statusText} 성공`, { userId: id, username: users[0].username, newStatus: activeValue, updatedBy: req.user.username }); res.json({ success: true, data: { user_id: id, is_active: activeValue }, message: `사용자가 성공적으로 ${statusText}되었습니다` }); } catch (error) { if (error instanceof NotFoundError || error instanceof ValidationError) { throw error; } logger.error('사용자 상태 변경 실패', { userId: id, error: error.message }); throw new DatabaseError('사용자 상태를 변경하는데 실패했습니다'); } }); /** * 사용자 삭제 (Soft Delete) */ const deleteUser = asyncHandler(async (req, res) => { checkAdminPermission(req.user); const { id } = req.params; if (!id || isNaN(id)) { throw new ValidationError('유효하지 않은 사용자 ID입니다'); } // 자기 자신 삭제 방지 if (req.user && req.user.user_id == id) { throw new ValidationError('자기 자신은 삭제할 수 없습니다'); } logger.info('사용자 삭제 요청', { userId: id }); const { getDb } = require('../dbPool'); const db = await getDb(); try { // 사용자 존재 확인 const checkQuery = 'SELECT user_id, username, is_active FROM users WHERE user_id = ?'; const [users] = await db.execute(checkQuery, [id]); if (users.length === 0) { throw new NotFoundError('사용자를 찾을 수 없습니다'); } if (users[0].is_active === 0) { throw new ValidationError('이미 비활성화된 사용자입니다'); } // Soft Delete (is_active = 0) const query = 'UPDATE users SET is_active = 0, updated_at = NOW() WHERE user_id = ?'; await db.execute(query, [id]); logger.info('사용자 비활성화 성공', { userId: id, username: users[0].username, deletedBy: req.user.username }); res.json({ success: true, data: { user_id: id }, message: '사용자가 성공적으로 비활성화되었습니다' }); } catch (error) { if (error instanceof NotFoundError || error instanceof ValidationError) { throw error; } logger.error('사용자 비활성화 실패', { userId: id, error: error.message }); throw new DatabaseError('사용자를 비활성화하는데 실패했습니다'); } }); /** * 사용자 영구 삭제 (Hard Delete) */ const permanentDeleteUser = asyncHandler(async (req, res) => { checkAdminPermission(req.user); const { id } = req.params; if (!id || isNaN(id)) { throw new ValidationError('유효하지 않은 사용자 ID입니다'); } // 자기 자신 삭제 방지 if (req.user && req.user.user_id == id) { throw new ValidationError('자기 자신은 삭제할 수 없습니다'); } logger.info('사용자 영구 삭제 요청', { userId: id }); const { getDb } = require('../dbPool'); const db = await getDb(); try { // 사용자 존재 확인 const checkQuery = 'SELECT user_id, username FROM users WHERE user_id = ?'; const [users] = await db.execute(checkQuery, [id]); if (users.length === 0) { throw new NotFoundError('사용자를 찾을 수 없습니다'); } const username = users[0].username; // 관련 데이터 삭제 (외래 키 제약 조건 때문에 순서 중요) // 1. 로그인 로그 삭제 await db.execute('DELETE FROM login_logs WHERE user_id = ?', [id]); // 2. 페이지 접근 권한 삭제 await db.execute('DELETE FROM user_page_access WHERE user_id = ?', [id]); // 3. 사용자 삭제 await db.execute('DELETE FROM users WHERE user_id = ?', [id]); logger.info('사용자 영구 삭제 성공', { userId: id, username: username, deletedBy: req.user.username }); res.json({ success: true, data: { user_id: id }, message: `사용자 "${username}"이(가) 영구적으로 삭제되었습니다` }); } catch (error) { if (error instanceof NotFoundError || error instanceof ValidationError) { throw error; } logger.error('사용자 영구 삭제 실패', { userId: id, error: error.message }); throw new DatabaseError('사용자를 영구 삭제하는데 실패했습니다'); } }); /** * 사용자의 페이지 접근 권한 조회 */ const getUserPageAccess = asyncHandler(async (req, res) => { const { id } = req.params; if (!id || isNaN(id)) { throw new ValidationError('유효하지 않은 사용자 ID입니다'); } logger.info('사용자 페이지 권한 조회 요청', { userId: id }); const { getDb } = require('../dbPool'); const db = await getDb(); try { // 권한 조회: user_page_access에 명시적 권한이 있으면 사용, 없으면 is_default_accessible 사용 const query = ` SELECT p.id as page_id, p.page_key, p.page_name, p.page_path, p.category, p.is_default_accessible, COALESCE(upa.can_access, p.is_default_accessible) as can_access FROM pages p LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ? ORDER BY p.category, p.display_order `; const [pageAccess] = await db.execute(query, [id]); logger.info('사용자 페이지 권한 조회 성공', { userId: id, pageCount: pageAccess.length }); res.json({ success: true, data: { pageAccess }, message: '페이지 권한 조회 성공' }); } catch (error) { logger.error('사용자 페이지 권한 조회 실패', { userId: id, error: error.message }); throw new DatabaseError('페이지 권한을 조회하는데 실패했습니다'); } }); /** * 사용자의 페이지 접근 권한 업데이트 */ const updateUserPageAccess = asyncHandler(async (req, res) => { checkAdminPermission(req.user); const { id } = req.params; const { pageAccess } = req.body; if (!id || isNaN(id)) { throw new ValidationError('유효하지 않은 사용자 ID입니다'); } if (!Array.isArray(pageAccess)) { throw new ValidationError('pageAccess는 배열이어야 합니다'); } logger.info('사용자 페이지 권한 업데이트 요청', { userId: id, pageCount: pageAccess.length, updatedBy: req.user.username }); const { getDb } = require('../dbPool'); const db = await getDb(); try { // 트랜잭션 시작 await db.query('START TRANSACTION'); // 기존 권한 삭제 await db.execute('DELETE FROM user_page_access WHERE user_id = ?', [id]); // 새 권한 삽입 if (pageAccess.length > 0) { const values = pageAccess.map(p => [id, p.page_id, p.can_access]); const placeholders = values.map(() => '(?, ?, ?)').join(', '); const flatValues = values.flat(); await db.execute( `INSERT INTO user_page_access (user_id, page_id, can_access) VALUES ${placeholders}`, flatValues ); } // 커밋 await db.query('COMMIT'); logger.info('사용자 페이지 권한 업데이트 성공', { userId: id, pageCount: pageAccess.length, updatedBy: req.user.username }); res.json({ success: true, data: { user_id: id }, message: '페이지 권한이 성공적으로 업데이트되었습니다' }); } catch (error) { // 롤백 await db.query('ROLLBACK'); logger.error('사용자 페이지 권한 업데이트 실패', { userId: id, error: error.message }); throw new DatabaseError('페이지 권한을 업데이트하는데 실패했습니다'); } }); /** * 사용자 비밀번호 초기화 (000000) */ const resetUserPassword = asyncHandler(async (req, res) => { checkAdminPermission(req.user); const { id } = req.params; if (!id || isNaN(id)) { throw new ValidationError('유효하지 않은 사용자 ID입니다'); } const { getDb } = require('../dbPool'); const db = await getDb(); try { // 사용자 존재 확인 const [existing] = await db.execute('SELECT user_id, username FROM users WHERE user_id = ?', [id]); if (existing.length === 0) { throw new NotFoundError('사용자를 찾을 수 없습니다'); } // 비밀번호를 000000으로 초기화 const hashedPassword = await bcrypt.hash('000000', 10); await db.execute( 'UPDATE users SET password = ?, password_changed_at = NULL, updated_at = NOW() WHERE user_id = ?', [hashedPassword, id] ); logger.info('사용자 비밀번호 초기화 성공', { userId: id, username: existing[0].username, resetBy: req.user.username }); res.json({ success: true, message: '비밀번호가 000000으로 초기화되었습니다' }); } catch (error) { if (error instanceof NotFoundError || error instanceof ValidationError) { throw error; } logger.error('비밀번호 초기화 실패', { userId: id, error: error.message }); throw new DatabaseError('비밀번호 초기화에 실패했습니다'); } }); module.exports = { getAllUsers, getUserById, createUser, updateUser, updateUserStatus, deleteUser, permanentDeleteUser, getUserPageAccess, updateUserPageAccess, resetUserPassword };