# 권한 그룹 기반 메뉴 필터링 가이드 > 작성일: 2025-01-27 > 파일 위치: `backend-node/src/services/adminService.ts` --- ## 📋 목차 1. [개요](#개요) 2. [메뉴 필터링 로직](#메뉴-필터링-로직) 3. [데이터베이스 구조](#데이터베이스-구조) 4. [구현 상세](#구현-상세) 5. [테스트 시나리오](#테스트-시나리오) --- ## 개요 ### ✅ 구현 완료 (2025-01-27) 사용자가 좌측 사이드바에서 볼 수 있는 메뉴는 **권한 그룹 기반**으로 필터링됩니다: 1. 사용자가 속한 권한 그룹 조회 (`authority_sub_user`) 2. 해당 권한 그룹의 메뉴 권한 확인 (`rel_menu_auth`) 3. **`read_yn = 'Y'`인 메뉴만 사이드바에 표시** --- ## 메뉴 필터링 로직 ### 흐름도 ```mermaid graph TD A[사용자 로그인] --> B{권한 그룹 조회} B -->|권한 그룹 있음| C[rel_menu_auth 조회] B -->|권한 그룹 없음| D[메뉴 없음] C --> E{read_yn = 'Y'?} E -->|Yes| F[메뉴 표시] E -->|No| G[메뉴 숨김] ``` ### 주요 단계 1. **권한 그룹 조회** ```sql SELECT DISTINCT am.objid AS role_objid, am.auth_name FROM authority_master am JOIN authority_sub_user asu ON am.objid = asu.master_objid WHERE asu.user_id = $1 AND am.status = 'active' ``` 2. **메뉴 권한 필터링** ```sql AND EXISTS ( SELECT 1 FROM rel_menu_auth rma WHERE rma.menu_objid = MENU.OBJID AND rma.auth_objid = ANY($2) -- 사용자의 권한 그룹 배열 AND rma.read_yn = 'Y' -- 읽기 권한이 있어야 함 ) ``` 3. **회사별 필터링** (기존 로직 유지) - 최고 관리자: 공통 메뉴 (`company_code = '*'`) - 회사 관리자/일반 사용자: 자기 회사 메뉴만 --- ## 데이터베이스 구조 ### 관련 테이블 ```sql -- 1. 권한 그룹 마스터 authority_master ( objid SERIAL PRIMARY KEY, auth_name VARCHAR(200), auth_code VARCHAR(100), company_code VARCHAR(50), status VARCHAR(20) ) -- 2. 권한 그룹 멤버 authority_sub_user ( objid SERIAL PRIMARY KEY, master_objid INTEGER, -- FK to authority_master user_id VARCHAR(50) -- 사용자 ID ) -- 3. 메뉴 권한 rel_menu_auth ( objid SERIAL PRIMARY KEY, menu_objid INTEGER, -- FK to menu_info auth_objid INTEGER, -- FK to authority_master create_yn VARCHAR(1), -- 생성 권한 read_yn VARCHAR(1), -- 조회 권한 ⭐ 사이드바 표시 기준 update_yn VARCHAR(1), -- 수정 권한 delete_yn VARCHAR(1), -- 삭제 권한 execute_yn VARCHAR(1), -- 실행 권한 export_yn VARCHAR(1) -- 내보내기 권한 ) -- 4. 메뉴 정보 menu_info ( objid SERIAL PRIMARY KEY, menu_name_kor VARCHAR(200), menu_url VARCHAR(500), parent_obj_id INTEGER, company_code VARCHAR(50), menu_type INTEGER, -- 0: 관리자, 1: 사용자 status VARCHAR(20) ) ``` ### 관계도 ``` user_info └─ authority_sub_user (user_id) └─ authority_master (master_objid) └─ rel_menu_auth (auth_objid) └─ menu_info (menu_objid) ``` --- ## 구현 상세 ### AdminService.getUserMenuList() **파일**: `backend-node/src/services/adminService.ts` **로직**: ```typescript static async getUserMenuList(paramMap: any): Promise { const { userId, userCompanyCode, userType, userLang = "ko" } = paramMap; // 1. 사용자가 속한 권한 그룹 조회 const userRoleGroups = await query( ` SELECT DISTINCT am.objid AS role_objid, am.auth_name FROM authority_master am JOIN authority_sub_user asu ON am.objid = asu.master_objid WHERE asu.user_id = $1 AND am.status = 'active' `, [userId] ); logger.info(`✅ 사용자 ${userId}가 속한 권한 그룹: ${userRoleGroups.length}개`); // 2. 권한 그룹 기반 메뉴 필터 조건 생성 let authFilter = ""; let queryParams: any[] = [userLang]; let paramIndex = 2; if (userRoleGroups.length > 0) { // 권한 그룹이 있는 경우: read_yn = 'Y'인 메뉴만 필터링 const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid); authFilter = ` AND EXISTS ( SELECT 1 FROM rel_menu_auth rma WHERE rma.menu_objid = MENU.OBJID AND rma.auth_objid = ANY($${paramIndex}) AND rma.read_yn = 'Y' ) `; queryParams.push(roleObjids); paramIndex++; } else { // 권한 그룹이 없는 경우: 메뉴 없음 logger.warn(`⚠️ 사용자 ${userId}는 권한 그룹이 없어 메뉴가 표시되지 않습니다.`); return []; } // 3. 회사별 필터링 조건 let companyFilter = ""; if (userType === "SUPER_ADMIN" && userCompanyCode === "*") { companyFilter = `AND MENU.COMPANY_CODE = '*'`; } else { companyFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`; queryParams.push(userCompanyCode); paramIndex++; } // 4. 메뉴 조회 쿼리 (WITH RECURSIVE) const menuList = await query( ` WITH RECURSIVE v_menu(...) AS ( SELECT ... FROM MENU_INFO MENU WHERE PARENT_OBJ_ID = 0 AND MENU_TYPE = 1 AND STATUS = 'active' ${companyFilter} ${authFilter} -- ⭐ 권한 그룹 필터 적용 UNION ALL SELECT ... FROM MENU_INFO MENU_SUB JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID WHERE MENU_SUB.STATUS = 'active' ${authFilter.replace(/MENU\\.OBJID/g, 'MENU_SUB.OBJID')} -- ⭐ 자식 메뉴에도 적용 ) SELECT ... FROM v_menu A ... ORDER BY PATH, SEQ `, queryParams ); return menuList; } ``` --- ## 테스트 시나리오 ### 시나리오 1: 최고 관리자가 권한 부여 **단계**: 1. 최고 관리자가 "테스트회사 2번"의 권한 그룹에 접속 2. "대시보드" 메뉴에 대해 `read_yn = 'Y'` 설정 3. 권한 저장 **결과**: - ✅ 해당 권한 그룹에 속한 사용자들에게 "대시보드" 메뉴가 사이드바에 표시됨 - ✅ `read_yn = 'N'`인 다른 메뉴는 표시되지 않음 **로그 확인**: ``` ✅ 사용자 user001가 속한 권한 그룹: 1개 - 권한 그룹: ["테스트회사2 관리자"] ✅ 권한 그룹 기반 메뉴 필터링 적용: 1개 그룹 ✅ 좌측 사이드바 (COMPANY_ADMIN): 회사 20 메뉴만 표시 사용자 메뉴 목록 조회 결과: 5개 ``` ### 시나리오 2: 권한 그룹이 없는 사용자 **단계**: 1. 새로운 사용자 생성 (`user002`) 2. 권한 그룹에 추가하지 않음 3. 로그인 **결과**: - ✅ 사이드바에 메뉴가 하나도 표시되지 않음 **로그 확인**: ``` ✅ 사용자 user002가 속한 권한 그룹: 0개 ⚠️ 사용자 user002는 권한 그룹이 없어 메뉴가 표시되지 않습니다. 사용자 메뉴 목록 조회 결과: 0개 ``` ### 시나리오 3: 여러 권한 그룹에 속한 사용자 **단계**: 1. 사용자 `user003`을 두 개의 권한 그룹에 추가 - 그룹 A: "대시보드" 메뉴 (`read_yn = 'Y'`) - 그룹 B: "사용자 관리" 메뉴 (`read_yn = 'Y'`) 2. 로그인 **결과**: - ✅ "대시보드"와 "사용자 관리" 메뉴가 모두 표시됨 - ✅ 두 그룹의 권한이 **OR 조건**으로 합쳐짐 **SQL 로직**: ```sql AND EXISTS ( SELECT 1 FROM rel_menu_auth rma WHERE rma.menu_objid = MENU.OBJID AND rma.auth_objid = ANY(ARRAY[그룹A_ID, 그룹B_ID]) -- OR 조건 AND rma.read_yn = 'Y' ) ``` ### 시나리오 4: 회사 관리자 vs 일반 사용자 **공통점**: - 둘 다 자기 회사 메뉴만 조회 - 권한 그룹 기반 필터링 적용 **차이점**: - **회사 관리자 (COMPANY_ADMIN)**: 권한 그룹 관리 가능 - **일반 사용자 (USER)**: 권한 그룹 관리 불가 (읽기 전용) --- ## 주의사항 ### 1. 메뉴 계층 구조 - 부모 메뉴에 `read_yn = 'Y'`가 있어야 자식 메뉴도 표시됨 - 자식 메뉴만 권한이 있어도 부모가 없으면 접근 불가 **예시**: ``` 📁 시스템 관리 (read_yn = 'N') ← 권한 없음 └─ 📄 사용자 관리 (read_yn = 'Y') ← 권한 있지만 부모가 없어서 접근 불가 ``` **해결**: - 부모 메뉴에도 `read_yn = 'Y'` 설정 필요 ### 2. 권한 그룹 상태 - `authority_master.status = 'active'`인 그룹만 적용 - 비활성화된 그룹은 멤버가 있어도 권한 없음 ### 3. 최고 관리자 예외 - 최고 관리자는 **공통 메뉴만** 조회 - 다른 회사 메뉴는 보이지 않음 - 최고 관리자도 권한 그룹에 속해야 메뉴가 보임 (일관성 유지) ### 4. 성능 고려사항 - `ANY($1)`: PostgreSQL 배열 연산자 사용으로 성능 최적화 - `EXISTS` 서브쿼리: 메뉴마다 권한 확인 - 인덱스 권장: ```sql CREATE INDEX idx_rel_menu_auth_menu ON rel_menu_auth(menu_objid); CREATE INDEX idx_rel_menu_auth_auth ON rel_menu_auth(auth_objid); CREATE INDEX idx_authority_sub_user_user ON authority_sub_user(user_id); ``` --- ## 관련 파일 - `backend-node/src/services/adminService.ts` - `getUserMenuList()` 메서드 - `backend-node/src/services/roleService.ts` - 권한 그룹 관리 - `backend-node/src/controllers/adminController.ts` - API 엔드포인트 - `frontend/contexts/MenuContext.tsx` - 프론트엔드 메뉴 Context - `frontend/lib/api/menu.ts` - 메뉴 API 클라이언트 --- ## 📝 작성자 - 작성: AI Assistant (Claude Sonnet 4.5) - 검토 필요: 백엔드 개발자, 시스템 아키텍트