feat: 권한 탭 분리 + 부서 인원 표시 + 다수 시스템 개선

- tkuser: 권한 관리를 별도 탭으로 분리, 부서 클릭 시 소속 인원 목록 표시
- system1: 모바일 UI 개선, nginx 권한 보정, 신고 카테고리 타입 마이그레이션
- system2: 신고 상세/보고서 개선, 내 보고서 페이지 추가
- system3: 이슈 뷰/수신함/관리함 개선
- gateway: 포털 라우팅 수정
- user-management API: 부서별 권한 벌크 설정 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-23 14:12:57 +09:00
parent bf4000c4ae
commit 3cc29c03a8
37 changed files with 1751 additions and 233 deletions

View File

@@ -104,20 +104,36 @@ async function bulkGrant({ user_id, permissions, granted_by_id }) {
}
/**
* 접근 권한 확인
* 접근 권한 확인 (우선순위: 개인 > 부서 > 기본값)
*/
async function checkAccess(userId, pageName) {
const db = getPool();
// 1. 명시적 개인 권한
const [rows] = await db.query(
'SELECT can_access FROM user_page_permissions WHERE user_id = ? AND page_name = ?',
[userId, pageName]
);
if (rows.length > 0) {
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' };
@@ -125,6 +141,110 @@ async function checkAccess(userId, pageName) {
return { can_access: pageConfig.default_access, reason: 'default_permission' };
}
/**
* 부서별 페이지 권한 조회
*/
async function getDepartmentPermissions(departmentId) {
const db = getPool();
const [rows] = await db.query(
'SELECT * FROM department_page_permissions WHERE department_id = ? ORDER BY page_name',
[departmentId]
);
return rows;
}
/**
* 부서 권한 단건 UPSERT
*/
async function setDepartmentPermission({ department_id, page_name, can_access, granted_by_id }) {
const db = getPool();
const [result] = await db.query(
`INSERT INTO department_page_permissions (department_id, page_name, can_access, granted_by_id)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE can_access = VALUES(can_access), granted_by_id = VALUES(granted_by_id), granted_at = CURRENT_TIMESTAMP`,
[department_id, page_name, can_access, granted_by_id]
);
return { id: result.insertId, department_id, page_name, can_access };
}
/**
* 부서 권한 일괄 설정
*/
async function bulkSetDepartmentPermissions({ department_id, permissions, granted_by_id }) {
const db = getPool();
let count = 0;
for (const perm of permissions) {
if (!DEFAULT_PAGES[perm.page_name]) continue;
await db.query(
`INSERT INTO department_page_permissions (department_id, page_name, can_access, granted_by_id)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE can_access = VALUES(can_access), granted_by_id = VALUES(granted_by_id), granted_at = CURRENT_TIMESTAMP`,
[department_id, perm.page_name, perm.can_access, granted_by_id]
);
count++;
}
return { updated_count: count };
}
/**
* 부서 권한 삭제 (기본값으로 복귀)
*/
async function deleteDepartmentPermission(departmentId, pageName) {
const db = getPool();
const [result] = await db.query(
'DELETE FROM department_page_permissions WHERE department_id = ? AND page_name = ?',
[departmentId, pageName]
);
return result.affectedRows > 0;
}
/**
* 사용자 권한 + 출처 조회 (각 페이지별 현재 적용 권한과 출처 반환)
*/
async function getUserPermissionsWithSource(userId) {
const db = getPool();
// 개인 권한 조회
const [userPerms] = await db.query(
'SELECT page_name, can_access FROM user_page_permissions WHERE user_id = ?',
[userId]
);
const userPermMap = {};
userPerms.forEach(p => { userPermMap[p.page_name] = !!p.can_access; });
// 부서 권한 조회
const [userRows] = await db.query(
'SELECT department_id FROM sso_users WHERE user_id = ?',
[userId]
);
const deptId = userRows.length > 0 ? userRows[0].department_id : null;
const deptPermMap = {};
if (deptId) {
const [deptPerms] = await db.query(
'SELECT page_name, can_access FROM department_page_permissions WHERE department_id = ?',
[deptId]
);
deptPerms.forEach(p => { deptPermMap[p.page_name] = !!p.can_access; });
}
// 모든 페이지에 대해 결과 조합
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' };
} else {
result[pageName] = { can_access: config.default_access, source: 'default' };
}
}
return { permissions: result, department_id: deptId };
}
/**
* 권한 삭제 (기본값으로 되돌림)
*/
@@ -146,5 +266,10 @@ module.exports = {
grantPermission,
bulkGrant,
checkAccess,
deletePermission
deletePermission,
getDepartmentPermissions,
setDepartmentPermission,
bulkSetDepartmentPermissions,
deleteDepartmentPermission,
getUserPermissionsWithSource
};