360 lines
12 KiB
Markdown
360 lines
12 KiB
Markdown
# 메뉴 기반 권한 시스템 가이드 (동적 화면 대응)
|
|
|
|
## 개요
|
|
|
|
**기존 메뉴 기반 권한 시스템을 유지**하면서 **동적으로 생성되는 화면에도 대응**하는 개선된 시스템입니다.
|
|
|
|
### 핵심 아이디어 💡
|
|
|
|
```
|
|
사용자가 화면 생성
|
|
↓
|
|
자동으로 메뉴 추가 (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`
|