diff --git a/user-management/api/controllers/permissionController.js b/user-management/api/controllers/permissionController.js index 2849488..40731dc 100644 --- a/user-management/api/controllers/permissionController.js +++ b/user-management/api/controllers/permissionController.js @@ -164,6 +164,7 @@ async function getDepartmentPermissions(req, res, next) { /** * POST /api/permissions/departments/:deptId/bulk-set - 부서 권한 일괄 설정 + * 저장 후 소속 사용자의 중복 개인 레코드 정리 */ async function bulkSetDepartmentPermissions(req, res, next) { try { @@ -177,10 +178,34 @@ async function bulkSetDepartmentPermissions(req, res, next) { granted_by_id: grantedById }); + // 소속 사용자의 중복 개인 레코드 정리 + const { getPool } = require('../models/userModel'); + const db = getPool(); + const [deptUsers] = await db.query( + 'SELECT user_id FROM sso_users WHERE department_id = ?', [deptId] + ); + + // 부서가 허용한 페이지 목록 + const grantedPages = (permissions || []) + .filter(p => p.can_access) + .map(p => p.page_name); + + let syncedUsers = 0; + if (grantedPages.length > 0 && deptUsers.length > 0) { + for (const u of deptUsers) { + const [delResult] = await db.query( + `DELETE FROM user_page_permissions WHERE user_id = ? AND page_name IN (${grantedPages.map(() => '?').join(',')})`, + [u.user_id, ...grantedPages] + ); + if (delResult.affectedRows > 0) syncedUsers++; + } + } + res.json({ success: true, - message: `${result.updated_count}개의 부서 권한이 설정되었습니다`, - updated_count: result.updated_count + message: `${result.updated_count}개 부서 권한 설정 (${deptUsers.length}명 적용)`, + updated_count: result.updated_count, + synced_users: deptUsers.length }); } catch (err) { next(err); diff --git a/user-management/api/controllers/userController.js b/user-management/api/controllers/userController.js index 848744e..2b00b40 100644 --- a/user-management/api/controllers/userController.js +++ b/user-management/api/controllers/userController.js @@ -5,6 +5,7 @@ */ const userModel = require('../models/userModel'); +const permissionModel = require('../models/permissionModel'); /** * GET /api/users - 전체 사용자 목록 @@ -62,6 +63,14 @@ async function updateUser(req, res, next) { delete data.full_name; } + // 부서 변경 감지 → 개인 권한 초기화 + if (data.department_id !== undefined) { + const existingUser = await userModel.findById(userId); + if (existingUser && existingUser.department_id !== data.department_id) { + await permissionModel.clearUserPermissionsForDepartmentChange(userId); + } + } + const user = await userModel.update(userId, data); if (!user) { return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' }); diff --git a/user-management/api/models/permissionModel.js b/user-management/api/models/permissionModel.js index 291f7c3..4f22777 100644 --- a/user-management/api/models/permissionModel.js +++ b/user-management/api/models/permissionModel.js @@ -105,14 +105,39 @@ async function grantPermission({ user_id, page_name, can_access, granted_by_id, } /** - * 일괄 권한 부여 + * 일괄 권한 부여 (부서 허용 페이지는 스킵 + 기존 중복 레코드 정리) */ async function bulkGrant({ user_id, permissions, granted_by_id }) { const db = getPool(); let count = 0; + // 부서 권한 조회 + const [userRows] = await db.query( + 'SELECT department_id FROM sso_users WHERE user_id = ?', [user_id] + ); + const deptId = userRows.length > 0 ? userRows[0].department_id : null; + + const deptGranted = {}; + if (deptId) { + const [deptPerms] = await db.query( + 'SELECT page_name, can_access FROM department_page_permissions WHERE department_id = ?', + [deptId] + ); + deptPerms.forEach(p => { if (p.can_access) deptGranted[p.page_name] = true; }); + } + for (const perm of permissions) { if (!DEFAULT_PAGES[perm.page_name]) continue; + + // 부서가 허용한 페이지는 개인 레코드 삭제 (스킵) + if (deptGranted[perm.page_name]) { + await db.query( + 'DELETE FROM user_page_permissions WHERE user_id = ? AND page_name = ?', + [user_id, perm.page_name] + ); + continue; + } + await db.query( `INSERT INTO user_page_permissions (user_id, page_name, can_access, granted_by_id) VALUES (?, ?, ?, ?) @@ -126,12 +151,27 @@ async function bulkGrant({ user_id, permissions, granted_by_id }) { } /** - * 접근 권한 확인 (우선순위: 개인 > 부서 > 기본값) + * 접근 권한 확인 (우선순위: 부서 OR 개인 OR 기본값) + * 부서가 허용하면 무조건 허용 (개인이 해제 불가) */ async function checkAccess(userId, pageName) { const db = getPool(); - // 1. 명시적 개인 권한 + // 1. 부서 권한 (마스터) — 부서가 허용하면 무조건 허용 + const [userRows] = await db.query( + 'SELECT department_id FROM sso_users WHERE user_id = ?', [userId] + ); + if (userRows.length > 0 && userRows[0].department_id) { + const [deptRows] = await db.query( + 'SELECT can_access FROM department_page_permissions WHERE department_id = ? AND page_name = ?', + [userRows[0].department_id, pageName] + ); + if (deptRows.length > 0 && deptRows[0].can_access) { + return { can_access: true, reason: 'department_permission' }; + } + } + + // 2. 개인 권한 (추가 부여) const [rows] = await db.query( 'SELECT can_access FROM user_page_permissions WHERE user_id = ? AND page_name = ?', [userId, pageName] @@ -140,26 +180,9 @@ async function checkAccess(userId, pageName) { return { can_access: rows[0].can_access, reason: 'explicit_permission' }; } - // 2. 부서 권한 - const [userRows] = await db.query( - 'SELECT department_id FROM sso_users WHERE user_id = ?', - [userId] - ); - if (userRows.length > 0 && userRows[0].department_id) { - const [deptRows] = await db.query( - 'SELECT can_access FROM department_page_permissions WHERE department_id = ? AND page_name = ?', - [userRows[0].department_id, pageName] - ); - if (deptRows.length > 0) { - return { can_access: deptRows[0].can_access, reason: 'department_permission' }; - } - } - // 3. 기본 권한 const pageConfig = DEFAULT_PAGES[pageName]; - if (!pageConfig) { - return { can_access: false, reason: 'invalid_page' }; - } + if (!pageConfig) return { can_access: false, reason: 'invalid_page' }; return { can_access: pageConfig.default_access, reason: 'default_permission' }; } @@ -252,21 +275,36 @@ async function getUserPermissionsWithSource(userId) { deptPerms.forEach(p => { deptPermMap[p.page_name] = !!p.can_access; }); } - // 모든 페이지에 대해 결과 조합 + // 모든 페이지에 대해 결과 조합 (부서 OR 개인 OR 기본) const result = {}; for (const [pageName, config] of Object.entries(DEFAULT_PAGES)) { - if (pageName in userPermMap) { - result[pageName] = { can_access: userPermMap[pageName], source: 'explicit' }; - } else if (pageName in deptPermMap) { - result[pageName] = { can_access: deptPermMap[pageName], source: 'department' }; + const deptGranted = deptPermMap[pageName] === true; + + if (deptGranted) { + // 부서가 허용 → 무조건 허용, 잠금 + result[pageName] = { can_access: true, source: 'department', dept_granted: true }; + } else if (pageName in userPermMap) { + result[pageName] = { can_access: userPermMap[pageName], source: 'explicit', dept_granted: false }; } else { - result[pageName] = { can_access: config.default_access, source: 'default' }; + result[pageName] = { can_access: config.default_access, source: 'default', dept_granted: false }; } } return { permissions: result, department_id: deptId }; } +/** + * 부서 이동 시 개인 권한 초기화 — user_page_permissions 전체 삭제 + */ +async function clearUserPermissionsForDepartmentChange(userId) { + const db = getPool(); + const [result] = await db.query( + 'DELETE FROM user_page_permissions WHERE user_id = ?', + [userId] + ); + return { deleted_count: result.affectedRows }; +} + /** * 권한 삭제 (기본값으로 되돌림) */ @@ -293,5 +331,6 @@ module.exports = { setDepartmentPermission, bulkSetDepartmentPermissions, deleteDepartmentPermission, - getUserPermissionsWithSource + getUserPermissionsWithSource, + clearUserPermissionsForDepartmentChange }; diff --git a/user-management/web/index.html b/user-management/web/index.html index 3fc0023..1374a9b 100644 --- a/user-management/web/index.html +++ b/user-management/web/index.html @@ -251,6 +251,9 @@ +

+ 부서 권한 저장 시 소속 사용자에게 자동 적용됩니다 +

@@ -2001,7 +2004,7 @@ - + diff --git a/user-management/web/static/js/tkuser-users.js b/user-management/web/static/js/tkuser-users.js index 0114e0d..3bbd65b 100644 --- a/user-management/web/static/js/tkuser-users.js +++ b/user-management/web/static/js/tkuser-users.js @@ -104,7 +104,7 @@ function loadPermissionsTab() { } /* ===== Users State ===== */ -let users = [], selectedUserId = null, currentPermissions = {}, currentPermSources = {}; +let users = [], selectedUserId = null, currentPermissions = {}, currentPermSources = {}, currentDeptGranted = {}; /* ===== Users CRUD ===== */ async function loadUsers() { @@ -219,14 +219,16 @@ document.getElementById('permissionUserSelect').addEventListener('change', async async function loadUserPermissions(userId) { currentPermissions = {}; currentPermSources = {}; + currentDeptGranted = {}; const allDefs = { ...SYSTEM1_PAGES, ...SYSTEM3_PAGES, ...TKPURCHASE_PAGES, ...TKSAFETY_PAGES, ...TKUSER_PAGES }; - Object.values(allDefs).flat().forEach(p => { currentPermissions[p.key] = p.def; currentPermSources[p.key] = 'default'; }); + Object.values(allDefs).flat().forEach(p => { currentPermissions[p.key] = p.def; currentPermSources[p.key] = 'default'; currentDeptGranted[p.key] = false; }); try { const result = await api(`/permissions/users/${userId}/effective-permissions`); if (result.permissions) { for (const [pageName, info] of Object.entries(result.permissions)) { currentPermissions[pageName] = info.can_access; currentPermSources[pageName] = info.source; + currentDeptGranted[pageName] = !!info.dept_granted; } } } catch(e) { console.warn('권한 로드 실패:', e); } @@ -270,6 +272,17 @@ function renderSystemPerms(containerId, pageDef, color) { ${pages.map(p => { const checked = currentPermissions[p.key] || false; const src = currentPermSources[p.key] || 'default'; + const deptLocked = currentDeptGranted[p.key] || false; + if (deptLocked) { + return ` + `; + } return `