ERP-node/docs/권한_그룹_메뉴_필터링_가이드.md

368 lines
9.3 KiB
Markdown

# 권한 그룹 기반 메뉴 필터링 가이드
> 작성일: 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<any[]> {
const { userId, userCompanyCode, userType, userLang = "ko" } = paramMap;
// 1. 사용자가 속한 권한 그룹 조회
const userRoleGroups = await query<any>(
`
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<any>(
`
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)
- 검토 필요: 백엔드 개발자, 시스템 아키텍트