ERP-node/docs/메뉴_기반_권한_시스템_가이드.md

360 lines
12 KiB
Markdown
Raw Permalink Normal View History

2025-10-27 16:40:59 +09:00
# 메뉴 기반 권한 시스템 가이드 (동적 화면 대응)
## 개요
**기존 메뉴 기반 권한 시스템을 유지**하면서 **동적으로 생성되는 화면에도 대응**하는 개선된 시스템입니다.
### 핵심 아이디어 💡
```
사용자가 화면 생성
자동으로 메뉴 추가 (menu_info)
권한 관리자가 메뉴 권한 설정 (rel_menu_auth)
사용자는 "메뉴"로만 권한 확인 (직관적!)
```
---
## 시스템 구조
### 1. `menu_info` (메뉴 정보)
| 컬럼 | 타입 | 설명 |
| ---------------- | ------------ | ------------------------------------------------------------------ |
| objid | INTEGER | 메뉴 ID (PK) |
| menu_name | VARCHAR(100) | 메뉴 이름 |
| menu_code | VARCHAR(50) | 메뉴 코드 |
| menu_url | VARCHAR(255) | 메뉴 URL |
| **menu_type** | VARCHAR(20) | **'static'**(고정 메뉴) 또는 **'dynamic'**(화면 생성 시 자동 추가) |
| **screen_code** | VARCHAR(50) | 동적 메뉴인 경우 `screen_definitions.screen_code` |
| **company_code** | VARCHAR(20) | 회사 코드 (회사별 메뉴 격리) |
| parent_objid | INTEGER | 부모 메뉴 ID (계층 구조) |
| is_active | BOOLEAN | 활성/비활성 |
### 2. `rel_menu_auth` (메뉴별 권한)
| 컬럼 | 타입 | 설명 |
| -------------- | ------- | ----------------------------------------- |
| menu_objid | INTEGER | 메뉴 ID (FK) |
| auth_objid | INTEGER | 권한 그룹 ID (FK) |
| **create_yn** | CHAR(1) | 생성 권한 ('Y'/'N') |
| **read_yn** | CHAR(1) | 읽기 권한 ('Y'/'N') |
| **update_yn** | CHAR(1) | 수정 권한 ('Y'/'N') |
| **delete_yn** | CHAR(1) | 삭제 권한 ('Y'/'N') |
| **execute_yn** | CHAR(1) | 실행 권한 ('Y'/'N') - 플로우 실행, DDL 등 |
| **export_yn** | CHAR(1) | 내보내기 권한 ('Y'/'N') |
---
## 자동화 기능 🤖
### 1. 화면 생성 시 자동 메뉴 추가
```sql
-- 사용자가 화면 생성
INSERT INTO screen_definitions (screen_name, screen_code, company_code, ...)
VALUES ('계약 관리', 'SCR_CONTRACT', 'ILSHIN', ...);
-- ↓ 트리거가 자동 실행 ↓
-- menu_info에 자동 추가됨!
-- menu_name = '계약 관리'
-- menu_code = 'SCR_CONTRACT'
-- menu_url = '/screen/SCR_CONTRACT'
-- menu_type = 'dynamic'
-- company_code = 'ILSHIN'
```
### 2. 화면 삭제 시 자동 메뉴 비활성화
```sql
-- 화면 삭제
UPDATE screen_definitions
SET is_active = 'D'
WHERE screen_code = 'SCR_CONTRACT';
-- ↓ 트리거가 자동 실행 ↓
-- 해당 메뉴도 비활성화됨!
UPDATE menu_info
SET is_active = FALSE
WHERE screen_code = 'SCR_CONTRACT';
```
---
## 사용 예시
### 예시 1: 영업팀에게 계약 관리 화면 읽기 권한 부여
```sql
-- 1. 계약 관리 메뉴 ID 조회 (화면 생성 시 자동으로 추가됨)
SELECT objid FROM menu_info
WHERE menu_code = 'SCR_CONTRACT';
-- 결과: objid = 1005
-- 2. 영업팀 권한 그룹 ID 조회
SELECT objid FROM authority_master
WHERE auth_code = 'SALES_TEAM' AND company_code = 'ILSHIN';
-- 결과: objid = 1001
-- 3. 읽기 권한 부여
INSERT INTO rel_menu_auth (menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, writer)
VALUES (1005, 1001, 'N', 'Y', 'N', 'N', 'admin');
```
### 예시 2: 개발팀에게 플로우 관리 전체 권한 부여
```sql
-- 플로우 관리 메뉴에 CRUD + 실행 권한
INSERT INTO rel_menu_auth (menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, execute_yn, writer)
VALUES (
(SELECT objid FROM menu_info WHERE menu_code = 'MENU_FLOW_MGMT'),
(SELECT objid FROM authority_master WHERE auth_code = 'DEV_TEAM'),
'Y', 'Y', 'Y', 'Y', 'Y', 'admin'
);
```
### 예시 3: 권한 확인
```sql
-- 'john.doe' 사용자가 계약 관리 메뉴를 읽을 수 있는지 확인
SELECT check_user_menu_permission('john.doe', 1005, 'read');
-- 결과: TRUE 또는 FALSE
-- 'john.doe' 사용자가 접근 가능한 모든 메뉴 조회
SELECT * FROM get_user_accessible_menus('john.doe', 'ILSHIN');
```
---
## 프론트엔드 통합
### React Hook
```typescript
// hooks/useMenuPermission.ts
import { useState, useEffect } from "react";
import { checkMenuPermission } from "@/lib/api/menu";
export function useMenuPermission(
menuObjid: number,
permissionType: "create" | "read" | "update" | "delete" | "execute" | "export"
) {
const [hasPermission, setHasPermission] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const checkPermission = async () => {
try {
const response = await checkMenuPermission(menuObjid, permissionType);
setHasPermission(response.success && response.data?.hasPermission);
} catch (error) {
console.error("권한 확인 오류:", error);
setHasPermission(false);
} finally {
setIsLoading(false);
}
};
checkPermission();
}, [menuObjid, permissionType]);
return { hasPermission, isLoading };
}
```
### 사용자 메뉴 렌더링
```tsx
// components/Navigation.tsx
import { useEffect, useState } from "react";
import { getUserAccessibleMenus } from "@/lib/api/menu";
import { useAuth } from "@/hooks/useAuth";
export function Navigation() {
const { user } = useAuth();
const [menus, setMenus] = useState([]);
useEffect(() => {
const loadMenus = async () => {
if (!user) return;
const response = await getUserAccessibleMenus(
user.userId,
user.companyCode
);
if (response.success) {
setMenus(response.data);
}
};
loadMenus();
}, [user]);
return (
<nav>
{menus.map((menu) => (
<NavItem key={menu.menuObjid} menu={menu} />
))}
</nav>
);
}
```
### 버튼 권한 제어
```tsx
// components/ContractDetail.tsx
import { useMenuPermission } from "@/hooks/useMenuPermission";
export function ContractDetail({ menuObjid }: { menuObjid: number }) {
const { hasPermission: canUpdate } = useMenuPermission(menuObjid, "update");
const { hasPermission: canDelete } = useMenuPermission(menuObjid, "delete");
return (
<div>
<h1>계약 상세</h1>
{canUpdate && <Button>수정</Button>}
{canDelete && <Button variant="destructive">삭제</Button>}
</div>
);
}
```
---
## 권한 관리 UI 설계
### 권한 그룹 상세 페이지에서 메뉴 권한 설정
```tsx
// 체크박스 그리드 형태
┌─────────────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ 메뉴 │ 생성 │ 읽기 │ 수정 │ 삭제 │ 실행 │ 내보내기│
├─────────────────┼────────┼────────┼────────┼────────┼────────┼────────┤
│ 대시보드 │ ☐ │ ☑ │ ☐ │ ☐ │ ☐ │ ☐ │
│ 계약 관리 │ ☑ │ ☑ │ ☑ │ ☐ │ ☐ │ ☑ │
│ 사용자 관리 │ ☐ │ ☑ │ ☐ │ ☐ │ ☐ │ ☐ │
│ 플로우 관리 │ ☐ │ ☑ │ ☐ │ ☐ │ ☑ │ ☐ │
└─────────────────┴────────┴────────┴────────┴────────┴────────┴────────┘
```
---
## 실전 시나리오
### 시나리오: 사용자가 "배송 현황" 화면 생성 → 권한 설정
```sql
-- 1단계: 사용자가 화면 생성
INSERT INTO screen_definitions (screen_name, screen_code, company_code, created_by)
VALUES ('배송 현황', 'SCR_DELIVERY', 'ILSHIN', 'admin');
-- 2단계: 트리거가 자동으로 메뉴 추가 (자동!)
-- menu_info에 'SCR_DELIVERY' 메뉴가 자동 생성됨
-- 3단계: 권한 관리자가 영업팀에게 읽기 권한 부여
INSERT INTO rel_menu_auth (
menu_objid,
auth_objid,
read_yn,
export_yn,
writer
)
VALUES (
(SELECT objid FROM menu_info WHERE menu_code = 'SCR_DELIVERY'),
(SELECT objid FROM authority_master WHERE auth_code = 'SALES_TEAM'),
'Y',
'Y',
'admin'
);
-- 4단계: 영업팀 사용자가 로그인하면 "배송 현황" 메뉴가 보임!
SELECT * FROM get_user_accessible_menus('sales_user', 'ILSHIN');
```
---
## 장점
### ✅ 사용자 친화적
- **"메뉴" 개념으로 권한 관리** (직관적)
- 기존 시스템과 동일한 UI/UX
### ✅ 자동화
- 화면 생성 시 **자동으로 메뉴 추가**
- 화면 삭제 시 **자동으로 메뉴 비활성화**
### ✅ 세밀한 권한
- 메뉴별 **6가지 권한** (Create, Read, Update, Delete, Execute, Export)
- 권한 그룹 단위 관리
### ✅ 회사별 격리
- `menu_info.company_code`로 회사별 메뉴 분리
- 슈퍼관리자는 모든 회사 메뉴 관리
---
## 마이그레이션 실행
```bash
# 1. 권한 그룹 시스템 개선
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d ilshin < db/migrations/028_add_company_code_to_authority_master.sql
# 2. 메뉴 기반 권한 시스템 개선
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d ilshin < db/migrations/030_improve_menu_auth_system.sql
# 검증
docker exec -it <DB_CONTAINER_NAME> psql -U postgres -d ilshin -c "SELECT * FROM menu_info WHERE menu_type = 'dynamic';"
docker exec -it <DB_CONTAINER_NAME> psql -U postgres -d ilshin -c "SELECT * FROM v_menu_auth_summary;"
```
---
## FAQ
### Q1: 동적 메뉴와 정적 메뉴의 차이는?
**A**:
- **정적 메뉴** (`menu_type='static'`): 수동으로 추가한 고정 메뉴 (예: 대시보드, 사용자 관리)
- **동적 메뉴** (`menu_type='dynamic'`): 화면 생성 시 자동 추가된 메뉴
### Q2: 화면을 삭제하면 메뉴도 삭제되나요?
**A**: 메뉴는 **삭제되지 않고 비활성화**(`is_active=FALSE`)됩니다. 나중에 복구 가능합니다.
### Q3: 같은 화면에 대해 회사마다 다른 권한을 설정할 수 있나요?
**A**: 네! `menu_info.company_code``authority_master.company_code`로 회사별 격리됩니다.
### Q4: 기존 메뉴 시스템과 호환되나요?
**A**: 완전히 호환됩니다. 기존 `menu_info``rel_menu_auth`를 그대로 사용하며, 새로운 컬럼만 추가됩니다.
---
## 다음 단계
1. ✅ 마이그레이션 실행 (028, 030)
2. 🔄 백엔드 API 구현 (권한 체크 미들웨어)
3. 🔄 프론트엔드 UI 개발 (메뉴 권한 설정 그리드)
4. 🔄 테스트 (영업팀 시나리오)
---
## 관련 파일
- **마이그레이션**: `db/migrations/028_add_company_code_to_authority_master.sql`
- **마이그레이션**: `db/migrations/030_improve_menu_auth_system.sql`
- **백엔드 서비스**: `backend-node/src/services/RoleService.ts`
- **프론트엔드 API**: `frontend/lib/api/role.ts`