각 회사별 데이터 분리
This commit is contained in:
317
docs/권한_그룹_시스템_설계.md
Normal file
317
docs/권한_그룹_시스템_설계.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# 권한 그룹 시스템 설계 (RBAC)
|
||||
|
||||
## 개요
|
||||
|
||||
회사 내에서 **역할 기반 접근 제어(RBAC - Role-Based Access Control)**를 통해 세밀한 권한 관리를 제공합니다.
|
||||
|
||||
## 기존 시스템 분석
|
||||
|
||||
### 현재 테이블 구조
|
||||
|
||||
#### 1. `authority_master` - 권한 그룹 마스터
|
||||
|
||||
```sql
|
||||
CREATE TABLE authority_master (
|
||||
objid NUMERIC PRIMARY KEY,
|
||||
auth_name VARCHAR, -- 권한 그룹 이름 (예: "영업팀 권한", "개발팀 권한")
|
||||
auth_code VARCHAR, -- 권한 코드 (예: "SALES_TEAM", "DEV_TEAM")
|
||||
writer VARCHAR,
|
||||
regdate TIMESTAMP,
|
||||
status VARCHAR
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. `authority_sub_user` - 권한 그룹 멤버
|
||||
|
||||
```sql
|
||||
CREATE TABLE authority_sub_user (
|
||||
objid NUMERIC PRIMARY KEY,
|
||||
master_objid NUMERIC, -- authority_master.objid 참조
|
||||
user_id VARCHAR, -- user_info.user_id 참조
|
||||
writer VARCHAR,
|
||||
regdate TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### 3. `rel_menu_auth` - 메뉴 권한 매핑
|
||||
|
||||
```sql
|
||||
CREATE TABLE rel_menu_auth (
|
||||
objid NUMERIC,
|
||||
menu_objid NUMERIC, -- menu_info.objid 참조
|
||||
auth_objid NUMERIC, -- authority_master.objid 참조
|
||||
writer VARCHAR,
|
||||
regdate TIMESTAMP,
|
||||
create_yn VARCHAR, -- 생성 권한 (Y/N)
|
||||
read_yn VARCHAR, -- 조회 권한 (Y/N)
|
||||
update_yn VARCHAR, -- 수정 권한 (Y/N)
|
||||
delete_yn VARCHAR -- 삭제 권한 (Y/N)
|
||||
);
|
||||
```
|
||||
|
||||
## 개선 사항
|
||||
|
||||
### 1. 회사별 권한 그룹 지원
|
||||
|
||||
**현재 문제점:**
|
||||
|
||||
- `authority_master` 테이블에 `company_code` 컬럼이 없음
|
||||
- 모든 회사가 권한 그룹을 공유하게 됨
|
||||
|
||||
**해결 방안:**
|
||||
|
||||
```sql
|
||||
-- 마이그레이션 028
|
||||
ALTER TABLE authority_master ADD COLUMN company_code VARCHAR(20);
|
||||
CREATE INDEX idx_authority_master_company ON authority_master(company_code);
|
||||
|
||||
-- 기존 데이터 마이그레이션 (기본값 설정)
|
||||
UPDATE authority_master SET company_code = 'ILSHIN' WHERE company_code IS NULL;
|
||||
```
|
||||
|
||||
### 2. 권한 레벨과 권한 그룹의 차이
|
||||
|
||||
| 구분 | 권한 레벨 (userType) | 권한 그룹 (authority_master) |
|
||||
| ---------- | -------------------------------- | ------------------------------ |
|
||||
| **목적** | 시스템 레벨 권한 | 메뉴별 세부 권한 |
|
||||
| **범위** | 전역 (시스템 전체) | 회사별 (회사 내부) |
|
||||
| **관리자** | 최고 관리자 (SUPER_ADMIN) | 회사 관리자 (COMPANY_ADMIN) |
|
||||
| **예시** | SUPER_ADMIN, COMPANY_ADMIN, USER | "영업팀", "개발팀", "관리자팀" |
|
||||
|
||||
### 3. 2단계 권한 체계
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1단계: 권한 레벨 (userType) │
|
||||
│ - SUPER_ADMIN: 모든 회사 관리, DDL 실행 │
|
||||
│ - COMPANY_ADMIN: 자기 회사 관리, 권한 그룹 생성 │
|
||||
│ - USER: 자기 회사 데이터 조회/수정 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2단계: 권한 그룹 (authority_master) │
|
||||
│ - 회사 내부에서 메뉴별 세부 권한 설정 │
|
||||
│ - 생성(C), 조회(R), 수정(U), 삭제(D) 권한 제어 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 사용 시나리오
|
||||
|
||||
### 시나리오 1: 영업팀 권한 그룹
|
||||
|
||||
**요구사항:**
|
||||
|
||||
- 영업팀은 고객 관리, 계약 관리 메뉴만 접근 가능
|
||||
- 고객 정보는 조회/수정 가능하지만 삭제 불가
|
||||
- 계약은 생성/조회/수정 가능
|
||||
|
||||
**구현:**
|
||||
|
||||
```sql
|
||||
-- 1. 권한 그룹 생성
|
||||
INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status)
|
||||
VALUES (nextval('seq_authority'), '영업팀 권한', 'SALES_TEAM', 'COMPANY_1', 'active');
|
||||
|
||||
-- 2. 사용자 추가
|
||||
INSERT INTO authority_sub_user (objid, master_objid, user_id)
|
||||
VALUES
|
||||
(nextval('seq_auth_sub'), 1, 'user1'),
|
||||
(nextval('seq_auth_sub'), 1, 'user2');
|
||||
|
||||
-- 3. 메뉴 권한 설정
|
||||
-- 고객 관리 메뉴
|
||||
INSERT INTO rel_menu_auth (menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn)
|
||||
VALUES (100, 1, 'N', 'Y', 'Y', 'N');
|
||||
|
||||
-- 계약 관리 메뉴
|
||||
INSERT INTO rel_menu_auth (menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn)
|
||||
VALUES (101, 1, 'Y', 'Y', 'Y', 'N');
|
||||
```
|
||||
|
||||
### 시나리오 2: 개발팀 권한 그룹
|
||||
|
||||
**요구사항:**
|
||||
|
||||
- 개발팀은 모든 기술 메뉴 접근 가능
|
||||
- 프로젝트, 코드 관리 메뉴는 모든 권한 보유
|
||||
- 시스템 설정은 조회만 가능
|
||||
|
||||
**구현:**
|
||||
|
||||
```sql
|
||||
-- 1. 권한 그룹 생성
|
||||
INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status)
|
||||
VALUES (nextval('seq_authority'), '개발팀 권한', 'DEV_TEAM', 'COMPANY_1', 'active');
|
||||
|
||||
-- 2. 메뉴 권한 설정
|
||||
INSERT INTO rel_menu_auth (menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn)
|
||||
VALUES
|
||||
(200, 2, 'Y', 'Y', 'Y', 'Y'), -- 프로젝트 관리 (모든 권한)
|
||||
(201, 2, 'Y', 'Y', 'Y', 'Y'), -- 코드 관리 (모든 권한)
|
||||
(202, 2, 'N', 'Y', 'N', 'N'); -- 시스템 설정 (조회만)
|
||||
```
|
||||
|
||||
## 구현 단계
|
||||
|
||||
### Phase 1: 데이터베이스 마이그레이션
|
||||
|
||||
- [ ] `authority_master`에 `company_code` 추가
|
||||
- [ ] 기존 데이터 마이그레이션
|
||||
- [ ] 인덱스 생성
|
||||
|
||||
### Phase 2: 백엔드 API
|
||||
|
||||
- [ ] 권한 그룹 CRUD API
|
||||
- `GET /api/admin/roles` - 회사별 권한 그룹 목록
|
||||
- `POST /api/admin/roles` - 권한 그룹 생성
|
||||
- `PUT /api/admin/roles/:id` - 권한 그룹 수정
|
||||
- `DELETE /api/admin/roles/:id` - 권한 그룹 삭제
|
||||
- [ ] 권한 그룹 멤버 관리 API
|
||||
- `GET /api/admin/roles/:id/members` - 멤버 목록
|
||||
- `POST /api/admin/roles/:id/members` - 멤버 추가
|
||||
- `DELETE /api/admin/roles/:id/members/:userId` - 멤버 제거
|
||||
- [ ] 메뉴 권한 매핑 API
|
||||
- `GET /api/admin/roles/:id/menu-permissions` - 메뉴 권한 목록
|
||||
- `PUT /api/admin/roles/:id/menu-permissions` - 메뉴 권한 설정
|
||||
|
||||
### Phase 3: 프론트엔드 UI
|
||||
|
||||
- [ ] 권한 그룹 관리 페이지 (`/admin/roles`)
|
||||
- 권한 그룹 목록 (회사별 필터링)
|
||||
- 권한 그룹 생성/수정/삭제
|
||||
- [ ] 권한 그룹 상세 페이지 (`/admin/roles/:id`)
|
||||
- 멤버 관리 (사용자 추가/제거)
|
||||
- 메뉴 권한 설정 (CRUD 권한 토글)
|
||||
- [ ] 사용자 관리 페이지 연동
|
||||
- 사용자별 권한 그룹 할당
|
||||
|
||||
### Phase 4: 권한 체크 로직
|
||||
|
||||
- [ ] 미들웨어 개선
|
||||
- 권한 레벨 체크 (기존)
|
||||
- 권한 그룹 체크 (신규)
|
||||
- 메뉴별 CRUD 권한 체크 (신규)
|
||||
- [ ] 프론트엔드 가드
|
||||
- 메뉴 표시/숨김
|
||||
- 버튼 활성화/비활성화
|
||||
|
||||
## 권한 체크 플로우
|
||||
|
||||
```
|
||||
사용자 요청
|
||||
↓
|
||||
1. 인증 체크 (로그인 여부)
|
||||
↓
|
||||
2. 권한 레벨 체크 (userType)
|
||||
- SUPER_ADMIN: 모든 접근 허용
|
||||
- COMPANY_ADMIN: 자기 회사만
|
||||
- USER: 권한 그룹 체크로 이동
|
||||
↓
|
||||
3. 권한 그룹 체크 (authority_sub_user)
|
||||
- 사용자가 속한 권한 그룹 조회
|
||||
↓
|
||||
4. 메뉴 권한 체크 (rel_menu_auth)
|
||||
- 요청한 메뉴에 대한 권한 확인
|
||||
- CRUD 권한 체크
|
||||
↓
|
||||
5. 접근 허용/거부
|
||||
```
|
||||
|
||||
## 예상 UI 구조
|
||||
|
||||
### 권한 그룹 관리 페이지
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 권한 그룹 관리 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ [회사 선택: COMPANY_1 ▼] [검색: ____] [+ 그룹 생성] │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌───────────────┬──────────┬──────────┬────────┐ │
|
||||
│ │ 권한 그룹명 │ 코드 │ 멤버 수 │ 액션 │ │
|
||||
│ ├───────────────┼──────────┼──────────┼────────┤ │
|
||||
│ │ 영업팀 권한 │ SALES │ 5명 │ [수정] │ │
|
||||
│ │ 개발팀 권한 │ DEV │ 8명 │ [수정] │ │
|
||||
│ │ 관리자팀 │ ADMIN │ 2명 │ [수정] │ │
|
||||
│ └───────────────┴──────────┴──────────┴────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 권한 그룹 상세 페이지
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 영업팀 권한 (SALES_TEAM) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 【 멤버 관리 】 │
|
||||
│ [+ 멤버 추가] │
|
||||
│ ┌──────────┬──────────┬────────┐ │
|
||||
│ │ 사용자 ID │ 이름 │ 액션 │ │
|
||||
│ ├──────────┼──────────┼────────┤ │
|
||||
│ │ user1 │ 김철수 │ [제거] │ │
|
||||
│ │ user2 │ 이영희 │ [제거] │ │
|
||||
│ └──────────┴──────────┴────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 【 메뉴 권한 설정 】 │
|
||||
│ ┌─────────────┬────┬────┬────┬────┐ │
|
||||
│ │ 메뉴 │ 생성│ 조회│ 수정│ 삭제│ │
|
||||
│ ├─────────────┼────┼────┼────┼────┤ │
|
||||
│ │ 고객 관리 │ □ │ ☑ │ ☑ │ □ │ │
|
||||
│ │ 계약 관리 │ ☑ │ ☑ │ ☑ │ □ │ │
|
||||
│ │ 매출 분석 │ □ │ ☑ │ □ │ □ │ │
|
||||
│ └─────────────┴────┴────┴────┴────┘ │
|
||||
│ [저장] [취소] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 마이그레이션 계획
|
||||
|
||||
### 028_add_company_code_to_authority_master.sql
|
||||
|
||||
```sql
|
||||
-- 권한 그룹 테이블에 회사 코드 추가
|
||||
ALTER TABLE authority_master ADD COLUMN IF NOT EXISTS company_code VARCHAR(20);
|
||||
|
||||
-- 인덱스 생성
|
||||
CREATE INDEX IF NOT EXISTS idx_authority_master_company ON authority_master(company_code);
|
||||
|
||||
-- 기존 데이터 마이그레이션
|
||||
UPDATE authority_master
|
||||
SET company_code = 'ILSHIN'
|
||||
WHERE company_code IS NULL;
|
||||
|
||||
-- NOT NULL 제약 조건 추가
|
||||
ALTER TABLE authority_master ALTER COLUMN company_code SET NOT NULL;
|
||||
ALTER TABLE authority_master ALTER COLUMN company_code SET DEFAULT 'ILSHIN';
|
||||
|
||||
-- 주석 추가
|
||||
COMMENT ON COLUMN authority_master.company_code IS '회사 코드 (회사별 권한 그룹 격리)';
|
||||
```
|
||||
|
||||
## 참고 사항
|
||||
|
||||
### 권한 우선순위
|
||||
|
||||
1. **SUPER_ADMIN**: 모든 권한 (권한 그룹 체크 생략)
|
||||
2. **COMPANY_ADMIN**: 회사 내 모든 권한 (권한 그룹 체크 생략)
|
||||
3. **USER**: 권한 그룹에 따른 메뉴별 권한
|
||||
|
||||
### 권한 그룹 vs 권한 레벨
|
||||
|
||||
- **권한 레벨**: 사용자 등록 시 최초 1회 설정 (최고 관리자가 변경)
|
||||
- **권한 그룹**: 회사 관리자가 자유롭게 생성/관리, 사용자는 여러 그룹에 속할 수 있음
|
||||
|
||||
### 보안 고려사항
|
||||
|
||||
- 회사 관리자는 자기 회사의 권한 그룹만 관리 가능
|
||||
- 최고 관리자는 모든 회사의 권한 그룹 관리 가능
|
||||
- 권한 그룹 삭제 시 연결된 사용자/메뉴 권한도 함께 삭제 (CASCADE)
|
||||
|
||||
## 다음 단계
|
||||
|
||||
1. **마이그레이션 028 실행** → `company_code` 추가
|
||||
2. **백엔드 API 개발** → 권한 그룹 CRUD
|
||||
3. **프론트엔드 UI 개발** → 권한 그룹 관리 페이지
|
||||
4. **권한 체크 로직 통합** → 미들웨어 개선
|
||||
|
||||
이 설계를 구현하시겠습니까?
|
||||
307
docs/권한_시스템_마이그레이션_완료.md
Normal file
307
docs/권한_시스템_마이그레이션_완료.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# 권한 시스템 마이그레이션 완료 보고서
|
||||
|
||||
## 실행 완료 ✅
|
||||
|
||||
날짜: 2025-10-27
|
||||
대상 데이터베이스: `plm` (39.117.244.52:11132)
|
||||
|
||||
---
|
||||
|
||||
## 실행된 마이그레이션
|
||||
|
||||
### 1. **028_add_company_code_to_authority_master.sql** ✅
|
||||
|
||||
**목적**: 권한 그룹 시스템 개선 (회사별 격리)
|
||||
|
||||
**주요 변경사항**:
|
||||
|
||||
- `authority_master.company_code` 컬럼 추가 (회사별 권한 그룹 격리)
|
||||
- 외래 키 제약 조건 추가 (`authority_sub_user` ↔ `authority_master`, `user_info`)
|
||||
- 권한 요약 뷰 생성 (`v_authority_group_summary`)
|
||||
- 유틸리티 함수 생성 (`get_user_authority_groups`)
|
||||
|
||||
### 2. **031_add_menu_auth_columns.sql** ✅
|
||||
|
||||
**목적**: 메뉴 기반 권한 시스템 개선 (동적 화면 대응)
|
||||
|
||||
**주요 변경사항**:
|
||||
|
||||
- `menu_info.screen_code`, `menu_info.menu_code` 컬럼 추가
|
||||
- `rel_menu_auth.execute_yn`, `rel_menu_auth.export_yn` 컬럼 추가
|
||||
- 화면 생성 시 자동 메뉴 추가 트리거 (`auto_create_menu_for_screen`)
|
||||
- 화면 삭제 시 자동 메뉴 비활성화 트리거 (`auto_deactivate_menu_for_screen`)
|
||||
- 권한 체크 함수 (`check_menu_crud_permission`)
|
||||
- 사용자 메뉴 조회 함수 (`get_user_menus_with_permissions`)
|
||||
- 권한 요약 뷰 (`v_menu_permission_summary`)
|
||||
|
||||
---
|
||||
|
||||
## 현재 데이터베이스 구조
|
||||
|
||||
### 1. 권한 그룹 시스템
|
||||
|
||||
#### `authority_master` (권한 그룹)
|
||||
|
||||
```
|
||||
objid | NUMERIC | 권한 그룹 ID (PK)
|
||||
auth_name | VARCHAR(50) | 권한 그룹 이름
|
||||
auth_code | VARCHAR(50) | 권한 그룹 코드
|
||||
company_code | VARCHAR(20) | 회사 코드 ⭐ (회사별 격리)
|
||||
status | VARCHAR(20) | 활성/비활성
|
||||
```
|
||||
|
||||
#### `authority_sub_user` (권한 그룹 멤버)
|
||||
|
||||
```
|
||||
master_objid | NUMERIC | 권한 그룹 ID (FK)
|
||||
user_id | VARCHAR(50) | 사용자 ID (FK)
|
||||
```
|
||||
|
||||
#### 현재 권한 그룹 현황
|
||||
|
||||
- COMPANY_1: 2개 그룹
|
||||
- COMPANY_2: 2개 그룹
|
||||
- COMPANY_3: 7개 그룹
|
||||
- COMPANY_4: 2개 그룹
|
||||
- ILSHIN: 3개 그룹
|
||||
|
||||
### 2. 메뉴 권한 시스템
|
||||
|
||||
#### `menu_info` (메뉴 정보)
|
||||
|
||||
```
|
||||
objid | NUMERIC | 메뉴 ID (PK)
|
||||
menu_name_kor | VARCHAR(64) | 메뉴 이름 (한글)
|
||||
menu_name_eng | VARCHAR(64) | 메뉴 이름 (영어)
|
||||
menu_code | VARCHAR(50) | 메뉴 코드 ⭐ (신규)
|
||||
menu_url | VARCHAR(256) | 메뉴 URL
|
||||
menu_type | NUMERIC | 메뉴 타입 (0=일반, 1=시스템, 2=동적생성 ⭐)
|
||||
screen_code | VARCHAR(50) | 화면 코드 ⭐ (동적 메뉴 연동)
|
||||
company_code | VARCHAR(50) | 회사 코드
|
||||
parent_obj_id | NUMERIC | 부모 메뉴 ID
|
||||
seq | NUMERIC | 정렬 순서
|
||||
status | VARCHAR(32) | 상태
|
||||
```
|
||||
|
||||
#### `rel_menu_auth` (메뉴별 권한)
|
||||
|
||||
```
|
||||
menu_objid | NUMERIC | 메뉴 ID (FK)
|
||||
auth_objid | NUMERIC | 권한 그룹 ID (FK)
|
||||
create_yn | VARCHAR(50) | 생성 권한
|
||||
read_yn | VARCHAR(50) | 읽기 권한
|
||||
update_yn | VARCHAR(50) | 수정 권한
|
||||
delete_yn | VARCHAR(50) | 삭제 권한
|
||||
execute_yn | CHAR(1) | 실행 권한 ⭐ (신규)
|
||||
export_yn | CHAR(1) | 내보내기 권한 ⭐ (신규)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 자동화 기능
|
||||
|
||||
### 1. 화면 생성 시 자동 메뉴 추가 🤖
|
||||
|
||||
```sql
|
||||
-- 사용자가 화면 생성
|
||||
INSERT INTO screen_definitions (screen_name, screen_code, company_code, ...)
|
||||
VALUES ('계약 관리', 'SCR_CONTRACT', 'ILSHIN', ...);
|
||||
|
||||
-- ↓ 트리거 자동 실행 ↓
|
||||
|
||||
-- menu_info에 자동 추가됨!
|
||||
-- menu_type = 2 (동적 생성)
|
||||
-- screen_code = 'SCR_CONTRACT'
|
||||
-- menu_url = '/screen/SCR_CONTRACT'
|
||||
```
|
||||
|
||||
### 2. 화면 삭제 시 자동 메뉴 비활성화 🤖
|
||||
|
||||
```sql
|
||||
-- 화면 삭제
|
||||
UPDATE screen_definitions
|
||||
SET is_active = 'D'
|
||||
WHERE screen_code = 'SCR_CONTRACT';
|
||||
|
||||
-- ↓ 트리거 자동 실행 ↓
|
||||
|
||||
-- 메뉴 비활성화됨!
|
||||
UPDATE menu_info
|
||||
SET status = 'inactive'
|
||||
WHERE screen_code = 'SCR_CONTRACT';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 사용 가이드
|
||||
|
||||
### 1. 권한 그룹 생성
|
||||
|
||||
```sql
|
||||
-- 예: ILSHIN 회사의 "개발팀" 권한 그룹 생성
|
||||
INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status, writer, regdate)
|
||||
VALUES (nextval('seq_authority_master'), '개발팀', 'DEV_TEAM', 'ILSHIN', 'active', 'admin', NOW());
|
||||
```
|
||||
|
||||
### 2. 권한 그룹에 멤버 추가
|
||||
|
||||
```sql
|
||||
-- 예: '개발팀'에 사용자 'dev1' 추가
|
||||
INSERT INTO authority_sub_user (master_objid, user_id)
|
||||
VALUES (
|
||||
(SELECT objid FROM authority_master WHERE auth_code = 'DEV_TEAM' AND company_code = 'ILSHIN'),
|
||||
'dev1'
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 메뉴 권한 설정
|
||||
|
||||
```sql
|
||||
-- 예: '개발팀'에게 특정 메뉴의 CRUD 권한 부여
|
||||
INSERT INTO rel_menu_auth (menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, execute_yn, export_yn, writer)
|
||||
VALUES (
|
||||
1005, -- 메뉴 ID
|
||||
(SELECT objid FROM authority_master WHERE auth_code = 'DEV_TEAM' AND company_code = 'ILSHIN'),
|
||||
'Y', 'Y', 'Y', 'Y', 'Y', 'N', -- CRUD + Execute 권한
|
||||
'admin'
|
||||
);
|
||||
```
|
||||
|
||||
### 4. 사용자 권한 확인
|
||||
|
||||
```sql
|
||||
-- 예: 'dev1' 사용자가 메뉴 1005를 수정할 수 있는지 확인
|
||||
SELECT check_menu_crud_permission('dev1', 1005, 'update');
|
||||
-- 결과: TRUE 또는 FALSE
|
||||
|
||||
-- 예: 'dev1' 사용자가 접근 가능한 모든 메뉴 조회
|
||||
SELECT * FROM get_user_menus_with_permissions('dev1', 'ILSHIN');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 다음 단계
|
||||
|
||||
### 1. 백엔드 API 구현
|
||||
|
||||
**필요한 API**:
|
||||
|
||||
- `GET /api/roles/:id/menu-permissions` - 권한 그룹의 메뉴 권한 조회
|
||||
- `POST /api/roles/:id/menu-permissions` - 메뉴 권한 설정
|
||||
- `GET /api/users/menus` - 현재 사용자가 접근 가능한 메뉴 목록
|
||||
- `POST /api/menu-permissions/check` - 특정 메뉴에 대한 권한 확인
|
||||
|
||||
**구현 파일**:
|
||||
|
||||
- `backend-node/src/services/RoleService.ts`
|
||||
- `backend-node/src/controllers/roleController.ts`
|
||||
- `backend-node/src/middleware/permissionMiddleware.ts`
|
||||
|
||||
### 2. 프론트엔드 UI 개발
|
||||
|
||||
**필요한 페이지/컴포넌트**:
|
||||
|
||||
1. **권한 그룹 상세 페이지** (`/admin/roles/[id]`)
|
||||
|
||||
- 기본 정보 (이름, 코드, 회사)
|
||||
- 멤버 관리 (Dual List Box) ✅ 이미 구현됨
|
||||
- **메뉴 권한 설정** (체크박스 그리드) ⬅️ 신규 개발 필요
|
||||
|
||||
2. **메뉴 권한 설정 그리드**
|
||||
|
||||
```
|
||||
┌─────────────────┬────────┬────────┬────────┬────────┬────────┬────────┐
|
||||
│ 메뉴 │ 생성 │ 읽기 │ 수정 │ 삭제 │ 실행 │ 내보내기│
|
||||
├─────────────────┼────────┼────────┼────────┼────────┼────────┼────────┤
|
||||
│ 대시보드 │ ☐ │ ☑ │ ☐ │ ☐ │ ☐ │ ☐ │
|
||||
│ 계약 관리 │ ☑ │ ☑ │ ☑ │ ☐ │ ☐ │ ☑ │
|
||||
│ 사용자 관리 │ ☐ │ ☑ │ ☐ │ ☐ │ ☐ │ ☐ │
|
||||
└─────────────────┴────────┴────────┴────────┴────────┴────────┴────────┘
|
||||
```
|
||||
|
||||
3. **네비게이션 메뉴** (사용자별 권한 필터링)
|
||||
|
||||
- `get_user_menus_with_permissions` 함수 활용
|
||||
- 읽기 권한이 있는 메뉴만 표시
|
||||
|
||||
4. **버튼/액션 권한 제어**
|
||||
- 생성 버튼: `can_create`
|
||||
- 수정 버튼: `can_update`
|
||||
- 삭제 버튼: `can_delete`
|
||||
- 실행 버튼: `can_execute` (플로우, DDL)
|
||||
- 내보내기 버튼: `can_export`
|
||||
|
||||
**구현 파일**:
|
||||
|
||||
- `frontend/components/admin/RoleDetailManagement.tsx` (메뉴 권한 탭 추가)
|
||||
- `frontend/components/admin/MenuPermissionGrid.tsx` (신규)
|
||||
- `frontend/lib/api/role.ts` (메뉴 권한 API 추가)
|
||||
- `frontend/hooks/useMenuPermission.ts` (신규)
|
||||
|
||||
### 3. 테스트 시나리오
|
||||
|
||||
**시나리오 1: 영업팀 권한 설정**
|
||||
|
||||
1. 영업팀 권한 그룹 생성
|
||||
2. 멤버 추가 (3명)
|
||||
3. 메뉴 권한 설정:
|
||||
- 대시보드: 읽기만
|
||||
- 계약 관리: CRUD + 내보내기
|
||||
- 플로우 관리: 읽기 + 실행
|
||||
4. 영업팀 사용자로 로그인하여 검증
|
||||
|
||||
**시나리오 2: 동적 화면 생성 및 권한 설정**
|
||||
|
||||
1. "배송 현황" 화면 생성
|
||||
2. 자동으로 메뉴 추가 확인
|
||||
3. 영업팀에게 읽기 권한 부여
|
||||
4. 영업팀 사용자 로그인하여 메뉴 표시 확인
|
||||
|
||||
---
|
||||
|
||||
## 주의사항
|
||||
|
||||
### 1. 기존 데이터 호환성
|
||||
|
||||
- 기존 `menu_info` 테이블 구조는 그대로 유지
|
||||
- 새로운 컬럼만 추가되어 기존 데이터에 영향 없음
|
||||
|
||||
### 2. 권한 타입 매핑
|
||||
|
||||
- `menu_type`이 `numeric`에서 `VARCHAR`로 변경되지 않음 (기존 구조 유지)
|
||||
- `menu_type = 2`가 동적 생성 메뉴를 의미
|
||||
|
||||
### 3. 데이터 마이그레이션 불필요
|
||||
|
||||
- 기존 권한 데이터는 그대로 유지
|
||||
- 새로운 권한 그룹은 수동으로 설정 필요
|
||||
|
||||
---
|
||||
|
||||
## 검증 체크리스트
|
||||
|
||||
- [x] `authority_master.company_code` 컬럼 존재 확인
|
||||
- [x] `menu_info.screen_code`, `menu_info.menu_code` 컬럼 존재 확인
|
||||
- [x] `rel_menu_auth.execute_yn`, `rel_menu_auth.export_yn` 컬럼 존재 확인
|
||||
- [x] 트리거 함수 생성 확인 (`auto_create_menu_for_screen`, `auto_deactivate_menu_for_screen`)
|
||||
- [x] 권한 체크 함수 생성 확인 (`check_menu_crud_permission`)
|
||||
- [x] 사용자 메뉴 조회 함수 생성 확인 (`get_user_menus_with_permissions`)
|
||||
- [x] 권한 요약 뷰 생성 확인 (`v_menu_permission_summary`)
|
||||
- [ ] 백엔드 API 구현
|
||||
- [ ] 프론트엔드 UI 구현
|
||||
- [ ] 테스트 시나리오 실행
|
||||
|
||||
---
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- `docs/메뉴_기반_권한_시스템_가이드.md` - 사용자 가이드
|
||||
- `docs/권한_체계_가이드.md` - 3단계 권한 체계 개요
|
||||
- `db/migrations/028_add_company_code_to_authority_master.sql` - 권한 그룹 마이그레이션
|
||||
- `db/migrations/031_add_menu_auth_columns.sql` - 메뉴 권한 마이그레이션
|
||||
|
||||
---
|
||||
|
||||
## 문의사항
|
||||
|
||||
기술적 문의사항이나 추가 기능 요청은 개발팀에 문의하세요.
|
||||
589
docs/권한_체계_가이드.md
Normal file
589
docs/권한_체계_가이드.md
Normal file
@@ -0,0 +1,589 @@
|
||||
# 3단계 권한 체계 가이드
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [권한 체계 개요](#권한-체계-개요)
|
||||
2. [권한 레벨 상세](#권한-레벨-상세)
|
||||
3. [데이터베이스 설정](#데이터베이스-설정)
|
||||
4. [백엔드 구현](#백엔드-구현)
|
||||
5. [프론트엔드 구현](#프론트엔드-구현)
|
||||
6. [실무 예제](#실무-예제)
|
||||
7. [FAQ](#faq)
|
||||
|
||||
---
|
||||
|
||||
## 권한 체계 개요
|
||||
|
||||
### 3단계 권한 구조
|
||||
|
||||
```
|
||||
┌────────────────────┬──────────────┬─────────────────┬────────────────────────┐
|
||||
│ 권한 레벨 │ company_code │ user_type │ 접근 범위 │
|
||||
├────────────────────┼──────────────┼─────────────────┼────────────────────────┤
|
||||
│ 최고 관리자 │ * │ SUPER_ADMIN │ ✅ 전체 회사 데이터 │
|
||||
│ (Super Admin) │ │ │ ✅ DDL 실행 권한 │
|
||||
│ │ │ │ ✅ 회사 생성/삭제 │
|
||||
│ │ │ │ ✅ 시스템 설정 │
|
||||
├────────────────────┼──────────────┼─────────────────┼────────────────────────┤
|
||||
│ 회사 관리자 │ 20 │ COMPANY_ADMIN │ ✅ 자기 회사 데이터 │
|
||||
│ (Company Admin) │ │ │ ✅ 회사 사용자 관리 │
|
||||
│ │ │ │ ✅ 회사 설정 변경 │
|
||||
│ │ │ │ ❌ DDL 실행 불가 │
|
||||
│ │ │ │ ❌ 타회사 접근 불가 │
|
||||
├────────────────────┼──────────────┼─────────────────┼────────────────────────┤
|
||||
│ 일반 사용자 │ 20 │ USER │ ✅ 자기 회사 데이터 │
|
||||
│ (User) │ │ │ ❌ 사용자 관리 불가 │
|
||||
│ │ │ │ ❌ 설정 변경 불가 │
|
||||
└────────────────────┴──────────────┴─────────────────┴────────────────────────┘
|
||||
```
|
||||
|
||||
### 핵심 원칙
|
||||
|
||||
1. **company_code = "\*"** → 전체 시스템 접근 (슈퍼관리자 전용)
|
||||
2. **company_code = "특정코드"** → 해당 회사만 접근
|
||||
3. **user_type** → 회사 내 권한 레벨 결정
|
||||
|
||||
---
|
||||
|
||||
## 권한 레벨 상세
|
||||
|
||||
### 1️⃣ 슈퍼관리자 (SUPER_ADMIN)
|
||||
|
||||
**조건:**
|
||||
|
||||
- `company_code = '*'`
|
||||
- `user_type = 'SUPER_ADMIN'`
|
||||
|
||||
**권한:**
|
||||
|
||||
- ✅ 모든 회사 데이터 조회/수정
|
||||
- ✅ DDL 실행 (CREATE TABLE, ALTER TABLE 등)
|
||||
- ✅ 회사 생성/삭제
|
||||
- ✅ 시스템 설정 변경
|
||||
- ✅ 모든 사용자 관리
|
||||
- ✅ 코드 관리, 템플릿 관리 등 전역 설정
|
||||
|
||||
**사용 사례:**
|
||||
|
||||
- 시스템 전체 관리자
|
||||
- 데이터베이스 스키마 변경
|
||||
- 새로운 회사 추가
|
||||
- 전사 공통 설정 관리
|
||||
|
||||
**계정 예시:**
|
||||
|
||||
```sql
|
||||
INSERT INTO user_info (user_id, user_name, company_code, user_type)
|
||||
VALUES ('super_admin', '시스템 관리자', '*', 'SUPER_ADMIN');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2️⃣ 회사 관리자 (COMPANY_ADMIN)
|
||||
|
||||
**조건:**
|
||||
|
||||
- `company_code = '특정 회사 코드'` (예: '20')
|
||||
- `user_type = 'COMPANY_ADMIN'`
|
||||
|
||||
**권한:**
|
||||
|
||||
- ✅ 자기 회사 데이터 조회/수정
|
||||
- ✅ 자기 회사 사용자 관리 (추가/수정/삭제)
|
||||
- ✅ 자기 회사 설정 변경
|
||||
- ✅ 자기 회사 대시보드/화면 관리
|
||||
- ❌ DDL 실행 불가
|
||||
- ❌ 타 회사 데이터 접근 불가
|
||||
- ❌ 시스템 전역 설정 변경 불가
|
||||
|
||||
**사용 사례:**
|
||||
|
||||
- 각 회사의 IT 관리자
|
||||
- 회사 내 사용자 계정 관리
|
||||
- 회사별 커스터마이징 설정
|
||||
|
||||
**계정 예시:**
|
||||
|
||||
```sql
|
||||
INSERT INTO user_info (user_id, user_name, company_code, user_type)
|
||||
VALUES ('company_admin_20', '회사20 관리자', '20', 'COMPANY_ADMIN');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3️⃣ 일반 사용자 (USER)
|
||||
|
||||
**조건:**
|
||||
|
||||
- `company_code = '특정 회사 코드'` (예: '20')
|
||||
- `user_type = 'USER'`
|
||||
|
||||
**권한:**
|
||||
|
||||
- ✅ 자기 회사 데이터 조회/수정
|
||||
- ✅ 자신이 만든 화면/대시보드 관리
|
||||
- ❌ 사용자 관리 불가
|
||||
- ❌ 회사 설정 변경 불가
|
||||
- ❌ 타 회사 데이터 접근 불가
|
||||
|
||||
**사용 사례:**
|
||||
|
||||
- 일반 업무 사용자
|
||||
- 데이터 입력/조회
|
||||
- 개인 대시보드 생성
|
||||
|
||||
**계정 예시:**
|
||||
|
||||
```sql
|
||||
INSERT INTO user_info (user_id, user_name, company_code, user_type)
|
||||
VALUES ('user_kim', '김철수', '20', 'USER');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 데이터베이스 설정
|
||||
|
||||
### 마이그레이션 실행
|
||||
|
||||
```bash
|
||||
# 권한 체계 마이그레이션 실행
|
||||
psql -U postgres -d your_database -f db/migrations/026_add_user_type_hierarchy.sql
|
||||
```
|
||||
|
||||
### 주요 변경사항
|
||||
|
||||
1. **코드 테이블 업데이트:**
|
||||
|
||||
- `ADMIN` → `COMPANY_ADMIN` 으로 변경
|
||||
- `SUPER_ADMIN` 신규 추가
|
||||
|
||||
2. **PostgreSQL 함수 추가:**
|
||||
|
||||
- `is_super_admin(user_id)` - 슈퍼관리자 확인
|
||||
- `is_company_admin(user_id, company_code)` - 회사 관리자 확인
|
||||
- `can_access_company_data(user_id, company_code)` - 데이터 접근 권한
|
||||
|
||||
3. **권한 뷰 생성:**
|
||||
- `v_user_permissions` - 사용자별 권한 요약
|
||||
|
||||
---
|
||||
|
||||
## 백엔드 구현
|
||||
|
||||
### 1. 권한 체크 유틸리티 사용
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isSuperAdmin,
|
||||
isCompanyAdmin,
|
||||
isAdmin,
|
||||
canExecuteDDL,
|
||||
canAccessCompanyData,
|
||||
canManageUsers,
|
||||
} from "../utils/permissionUtils";
|
||||
|
||||
// 슈퍼관리자 확인
|
||||
if (isSuperAdmin(req.user)) {
|
||||
// 전체 데이터 조회
|
||||
}
|
||||
|
||||
// 회사 데이터 접근 권한 확인
|
||||
if (canAccessCompanyData(req.user, targetCompanyCode)) {
|
||||
// 해당 회사 데이터 조회
|
||||
}
|
||||
|
||||
// 사용자 관리 권한 확인
|
||||
if (canManageUsers(req.user, targetCompanyCode)) {
|
||||
// 사용자 추가/수정/삭제
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 미들웨어 사용
|
||||
|
||||
```typescript
|
||||
import {
|
||||
requireSuperAdmin,
|
||||
requireAdmin,
|
||||
requireCompanyAccess,
|
||||
requireUserManagement,
|
||||
requireDDLPermission,
|
||||
} from "../middleware/permissionMiddleware";
|
||||
|
||||
// 슈퍼관리자 전용 엔드포인트
|
||||
router.post(
|
||||
"/api/admin/ddl/execute",
|
||||
authenticate,
|
||||
requireDDLPermission,
|
||||
ddlController.execute
|
||||
);
|
||||
|
||||
// 관리자 전용 엔드포인트 (슈퍼관리자 + 회사관리자)
|
||||
router.get(
|
||||
"/api/admin/users",
|
||||
authenticate,
|
||||
requireAdmin,
|
||||
userController.getUserList
|
||||
);
|
||||
|
||||
// 회사 데이터 접근 체크
|
||||
router.get(
|
||||
"/api/data/:companyCode/orders",
|
||||
authenticate,
|
||||
requireCompanyAccess,
|
||||
orderController.getOrders
|
||||
);
|
||||
|
||||
// 사용자 관리 권한 체크
|
||||
router.post(
|
||||
"/api/admin/users/:companyCode",
|
||||
authenticate,
|
||||
requireUserManagement,
|
||||
userController.createUser
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 서비스 레이어 구현
|
||||
|
||||
```typescript
|
||||
// ❌ 잘못된 방법 - 하드코딩된 회사 코드
|
||||
async getOrders(companyCode: string) {
|
||||
return query("SELECT * FROM orders WHERE company_code = $1", [companyCode]);
|
||||
}
|
||||
|
||||
// ✅ 올바른 방법 - 권한 체크 포함
|
||||
async getOrders(user: PersonBean, companyCode: string) {
|
||||
// 권한 확인
|
||||
if (!canAccessCompanyData(user, companyCode)) {
|
||||
throw new Error("해당 회사 데이터에 접근할 권한이 없습니다.");
|
||||
}
|
||||
|
||||
// 슈퍼관리자는 모든 데이터 조회 가능
|
||||
if (isSuperAdmin(user)) {
|
||||
if (companyCode === "*") {
|
||||
return query("SELECT * FROM orders"); // 전체 조회
|
||||
}
|
||||
}
|
||||
|
||||
// 일반 사용자/회사 관리자는 자기 회사만
|
||||
return query("SELECT * FROM orders WHERE company_code = $1", [companyCode]);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 프론트엔드 구현
|
||||
|
||||
### 1. 사용자 타입 정의
|
||||
|
||||
```typescript
|
||||
// frontend/types/user.ts
|
||||
export interface UserInfo {
|
||||
userId: string;
|
||||
userName: string;
|
||||
companyCode: string;
|
||||
userType: string; // 'SUPER_ADMIN' | 'COMPANY_ADMIN' | 'USER'
|
||||
isSuperAdmin?: boolean;
|
||||
isCompanyAdmin?: boolean;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 권한 기반 UI 렌더링
|
||||
|
||||
```tsx
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
|
||||
function AdminPanel() {
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* 슈퍼관리자만 표시 */}
|
||||
{user?.isSuperAdmin && (
|
||||
<Button onClick={handleDDLExecution}>DDL 실행</Button>
|
||||
)}
|
||||
|
||||
{/* 관리자만 표시 (슈퍼관리자 + 회사관리자) */}
|
||||
{user?.isAdmin && (
|
||||
<Button onClick={handleUserManagement}>사용자 관리</Button>
|
||||
)}
|
||||
|
||||
{/* 모든 사용자 표시 */}
|
||||
<Button onClick={handleDataView}>데이터 조회</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 권한 체크 Hook
|
||||
|
||||
```typescript
|
||||
// frontend/hooks/usePermissions.ts
|
||||
export function usePermissions() {
|
||||
const { user } = useAuth();
|
||||
|
||||
return {
|
||||
isSuperAdmin: user?.isSuperAdmin ?? false,
|
||||
isCompanyAdmin: user?.isCompanyAdmin ?? false,
|
||||
isAdmin: user?.isAdmin ?? false,
|
||||
canExecuteDDL: user?.isSuperAdmin ?? false,
|
||||
canManageUsers: user?.isAdmin ?? false,
|
||||
canAccessCompany: (companyCode: string) => {
|
||||
if (user?.isSuperAdmin) return true;
|
||||
return user?.companyCode === companyCode;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 사용 예시
|
||||
function DataTable({ companyCode }: { companyCode: string }) {
|
||||
const { canAccessCompany } = usePermissions();
|
||||
|
||||
if (!canAccessCompany(companyCode)) {
|
||||
return <div>접근 권한이 없습니다.</div>;
|
||||
}
|
||||
|
||||
return <Table data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 실무 예제
|
||||
|
||||
### 예제 1: 주문 데이터 조회
|
||||
|
||||
**시나리오:**
|
||||
|
||||
- 슈퍼관리자: 모든 회사의 주문 조회
|
||||
- 회사20 관리자: 회사20의 주문만 조회
|
||||
- 회사20 사용자: 회사20의 주문만 조회
|
||||
|
||||
**백엔드 구현:**
|
||||
|
||||
```typescript
|
||||
// orders.service.ts
|
||||
export class OrderService {
|
||||
async getOrders(user: PersonBean, companyCode?: string) {
|
||||
let sql = "SELECT * FROM orders WHERE 1=1";
|
||||
const params: any[] = [];
|
||||
|
||||
// 슈퍼관리자가 아닌 경우 회사 필터 적용
|
||||
if (!isSuperAdmin(user)) {
|
||||
sql += " AND company_code = $1";
|
||||
params.push(user.companyCode);
|
||||
} else if (companyCode && companyCode !== "*") {
|
||||
// 슈퍼관리자가 특정 회사를 지정한 경우
|
||||
sql += " AND company_code = $1";
|
||||
params.push(companyCode);
|
||||
}
|
||||
|
||||
return query(sql, params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**프론트엔드 구현:**
|
||||
|
||||
```tsx
|
||||
function OrderList() {
|
||||
const { user } = useAuth();
|
||||
const [selectedCompany, setSelectedCompany] = useState(user?.companyCode);
|
||||
|
||||
// 슈퍼관리자는 회사 선택 가능
|
||||
const showCompanySelector = user?.isSuperAdmin;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showCompanySelector && (
|
||||
<Select value={selectedCompany} onChange={setSelectedCompany}>
|
||||
<option value="*">전체 회사</option>
|
||||
<option value="20">회사 20</option>
|
||||
<option value="30">회사 30</option>
|
||||
</Select>
|
||||
)}
|
||||
|
||||
<OrderTable companyCode={selectedCompany} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 예제 2: 사용자 관리
|
||||
|
||||
**시나리오:**
|
||||
|
||||
- 슈퍼관리자: 모든 회사의 사용자 관리
|
||||
- 회사20 관리자: 회사20 사용자만 관리
|
||||
- 회사20 사용자: 사용자 관리 불가
|
||||
|
||||
**백엔드 구현:**
|
||||
|
||||
```typescript
|
||||
// users.controller.ts
|
||||
router.post("/api/admin/users", authenticate, async (req, res) => {
|
||||
const { companyCode, userId, userName } = req.body;
|
||||
|
||||
// 권한 확인
|
||||
if (!canManageUsers(req.user, companyCode)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: "사용자 관리 권한이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 슈퍼관리자가 아닌 경우, 자기 회사만 가능
|
||||
if (!isSuperAdmin(req.user) && companyCode !== req.user.companyCode) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: "다른 회사의 사용자를 생성할 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 생성
|
||||
await UserService.createUser({ companyCode, userId, userName });
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 예제 3: DDL 실행 (테이블 생성)
|
||||
|
||||
**시나리오:**
|
||||
|
||||
- 슈퍼관리자만 DDL 실행 가능
|
||||
- 다른 모든 사용자는 차단
|
||||
|
||||
**백엔드 구현:**
|
||||
|
||||
```typescript
|
||||
// ddl.controller.ts
|
||||
router.post(
|
||||
"/api/admin/ddl/execute",
|
||||
authenticate,
|
||||
requireDDLPermission, // 슈퍼관리자 체크 미들웨어
|
||||
async (req, res) => {
|
||||
const { sql } = req.body;
|
||||
|
||||
// 추가 보안 검증
|
||||
if (!canExecuteDDL(req.user)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: "DDL 실행 권한이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// DDL 실행
|
||||
await query(sql);
|
||||
|
||||
// 감사 로그 기록
|
||||
await AuditService.logDDL({
|
||||
userId: req.user.userId,
|
||||
sql,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
**프론트엔드 구현:**
|
||||
|
||||
```tsx
|
||||
function DDLExecutor() {
|
||||
const { user } = useAuth();
|
||||
|
||||
// 슈퍼관리자가 아니면 컴포넌트 자체를 숨김
|
||||
if (!user?.isSuperAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>DDL 실행 (슈퍼관리자 전용)</h2>
|
||||
<textarea placeholder="SQL 입력" />
|
||||
<Button onClick={handleExecute}>실행</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q1: 기존 ADMIN 계정은 어떻게 되나요?
|
||||
|
||||
**A:** 마이그레이션 스크립트가 자동으로 처리합니다:
|
||||
|
||||
- `company_code = '*'`인 ADMIN → `SUPER_ADMIN`으로 변경
|
||||
- `company_code = '특정코드'`인 ADMIN → `COMPANY_ADMIN`으로 변경
|
||||
|
||||
### Q2: 슈퍼관리자 계정은 몇 개가 적절한가요?
|
||||
|
||||
**A:** 보안상 최소 1개, 최대 2-3개를 권장합니다. 모든 DDL 실행이 감사 로그에 기록되므로 책임 추적이 가능합니다.
|
||||
|
||||
### Q3: 회사 관리자가 다른 회사 데이터를 조회하려면?
|
||||
|
||||
**A:** 불가능합니다. 회사 간 데이터 격리가 필수입니다. 필요시 슈퍼관리자에게 요청하거나, API 통합 기능을 사용해야 합니다.
|
||||
|
||||
### Q4: USER가 COMPANY_ADMIN으로 승격하려면?
|
||||
|
||||
**A:**
|
||||
|
||||
1. 슈퍼관리자 또는 해당 회사의 관리자가 처리
|
||||
2. `UPDATE user_info SET user_type = 'COMPANY_ADMIN' WHERE user_id = 'xxx'`
|
||||
3. 사용자 재로그인 필요
|
||||
|
||||
### Q5: 회사 코드 '\*'의 의미는?
|
||||
|
||||
**A:** 와일드카드로, "모든 회사"를 의미합니다. 슈퍼관리자 전용 코드이며, 일반 회사 코드로는 사용할 수 없습니다.
|
||||
|
||||
### Q6: 권한 체크는 어디서 해야 하나요?
|
||||
|
||||
**A:**
|
||||
|
||||
- **백엔드 (필수)**: 미들웨어 + 서비스 레이어 모두
|
||||
- **프론트엔드 (선택)**: UI 렌더링 최적화용 (보안 목적 아님)
|
||||
|
||||
### Q7: 테이블에 회사 필터링을 추가하려면?
|
||||
|
||||
**A:**
|
||||
|
||||
1. 테이블에 `company_code` 컬럼 추가
|
||||
2. `backend-node/src/services/dataService.ts`의 `COMPANY_FILTERED_TABLES` 배열에 테이블명 추가
|
||||
3. 자동으로 회사 필터링 적용됨
|
||||
|
||||
---
|
||||
|
||||
## 체크리스트
|
||||
|
||||
### 새로운 엔드포인트 추가 시
|
||||
|
||||
- [ ] 적절한 권한 미들웨어 적용 (`requireSuperAdmin`, `requireAdmin` 등)
|
||||
- [ ] 서비스 레이어에서 `canAccessCompanyData()` 체크
|
||||
- [ ] 감사 로그 기록 (중요 작업의 경우)
|
||||
- [ ] 프론트엔드 UI에 권한 기반 렌더링 적용
|
||||
- [ ] 에러 메시지에 필요한 권한 레벨 명시
|
||||
|
||||
### 새로운 테이블 생성 시
|
||||
|
||||
- [ ] `company_code` 컬럼 추가 (회사별 데이터인 경우)
|
||||
- [ ] `COMPANY_FILTERED_TABLES` 배열에 등록
|
||||
- [ ] 인덱스 생성: `CREATE INDEX ON table_name(company_code)`
|
||||
- [ ] Row Level Security 정책 고려 (선택사항)
|
||||
|
||||
---
|
||||
|
||||
## 참고 파일
|
||||
|
||||
- 마이그레이션: `/db/migrations/026_add_user_type_hierarchy.sql`
|
||||
- 권한 유틸: `/backend-node/src/utils/permissionUtils.ts`
|
||||
- 미들웨어: `/backend-node/src/middleware/permissionMiddleware.ts`
|
||||
- 타입 정의: `/backend-node/src/types/auth.ts`
|
||||
- 인증 서비스: `/backend-node/src/services/authService.ts`
|
||||
416
docs/리소스_기반_권한_시스템_가이드.md
Normal file
416
docs/리소스_기반_권한_시스템_가이드.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# 리소스 기반 권한 시스템 가이드
|
||||
|
||||
## 개요
|
||||
|
||||
동적으로 화면과 테이블을 생성하는 Low-Code 플랫폼에 맞춘 **리소스 기반 권한 시스템**입니다.
|
||||
|
||||
전통적인 "메뉴" 개념 대신, **"리소스 타입"**(화면, 테이블, 플로우 등)에 대한 **세밀한 CRUD 권한**을 관리합니다.
|
||||
|
||||
## 왜 메뉴 기반이 아닌가?
|
||||
|
||||
### 문제점
|
||||
|
||||
- 현재 시스템은 **동적으로 화면(`screen_definitions`)을 생성**
|
||||
- 사용자가 **DDL을 실행하여 테이블을 동적으로 생성**
|
||||
- **메뉴는 고정되어 있지 않음** (사용자가 생성한 화면 = 새로운 "메뉴")
|
||||
|
||||
### 해결책
|
||||
|
||||
- **리소스 타입** (SCREEN, TABLE, FLOW, DASHBOARD 등) 기반 권한
|
||||
- **특정 리소스 ID** 또는 **전체 타입**에 대한 권한 부여
|
||||
- **6가지 세밀한 권한**: Create, Read, Update, Delete, Execute, Export
|
||||
|
||||
---
|
||||
|
||||
## 시스템 구조
|
||||
|
||||
### 1. 리소스 타입 (`resource_types`)
|
||||
|
||||
| type_code | type_name | description |
|
||||
| --------- | --------- | ------------------------------ |
|
||||
| SCREEN | 화면 | 동적으로 생성된 화면 |
|
||||
| TABLE | 테이블 | 동적으로 생성된 데이터 테이블 |
|
||||
| FLOW | 플로우 | 데이터 플로우 |
|
||||
| DASHBOARD | 대시보드 | 대시보드 |
|
||||
| REPORT | 리포트 | 리포트 |
|
||||
| API | API | 외부 API 호출 |
|
||||
| FILE | 파일 | 파일 업로드/다운로드 |
|
||||
| SYSTEM | 시스템 | 시스템 설정 (SUPER_ADMIN 전용) |
|
||||
|
||||
### 2. 권한 그룹 (`authority_master`)
|
||||
|
||||
기존 테이블 활용 (회사별 격리 지원):
|
||||
|
||||
- `objid`: 권한 그룹 ID
|
||||
- `auth_name`: 권한 그룹 이름 (예: "영업팀", "개발팀")
|
||||
- `auth_code`: 권한 그룹 코드
|
||||
- `company_code`: 회사 코드
|
||||
- `status`: 활성/비활성
|
||||
|
||||
### 3. 리소스별 권한 (`resource_permissions`)
|
||||
|
||||
| 컬럼 | 타입 | 설명 |
|
||||
| ------------- | ------------ | --------------------------------- |
|
||||
| role_group_id | INTEGER | 권한 그룹 ID (FK) |
|
||||
| resource_type | VARCHAR(50) | 리소스 타입 (SCREEN, TABLE 등) |
|
||||
| resource_id | VARCHAR(255) | 특정 리소스 ID (**NULL = 전체**) |
|
||||
| can_create | BOOLEAN | 생성 권한 |
|
||||
| can_read | BOOLEAN | 읽기 권한 |
|
||||
| can_update | BOOLEAN | 수정 권한 |
|
||||
| can_delete | BOOLEAN | 삭제 권한 |
|
||||
| can_execute | BOOLEAN | 실행 권한 (플로우 실행, DDL 실행) |
|
||||
| can_export | BOOLEAN | 내보내기 권한 |
|
||||
|
||||
**핵심**: `resource_id`가 **NULL**이면 해당 타입 **전체**에 대한 권한
|
||||
|
||||
### 4. 사용자별 직접 권한 (`user_resource_permissions`)
|
||||
|
||||
권한 그룹 외에 **개별 사용자에게 직접 권한** 부여 가능 (보조적 사용)
|
||||
|
||||
---
|
||||
|
||||
## 권한 체크 로직
|
||||
|
||||
### 우선순위
|
||||
|
||||
1. **SUPER_ADMIN** (`company_code = '*'`, `user_type = 'SUPER_ADMIN'`)
|
||||
|
||||
- 모든 권한 (무조건 TRUE)
|
||||
|
||||
2. **COMPANY_ADMIN** (`user_type = 'COMPANY_ADMIN'`)
|
||||
|
||||
- 자기 회사 모든 리소스 권한 (단, `SYSTEM` 타입 제외)
|
||||
|
||||
3. **권한 그룹 기반 권한** (`authority_sub_user` → `resource_permissions`)
|
||||
|
||||
- 사용자가 속한 권한 그룹의 권한
|
||||
|
||||
4. **개별 권한** (`user_resource_permissions`)
|
||||
- 사용자에게 직접 부여된 권한
|
||||
|
||||
**최종 판정**: `권한 그룹 권한 OR 개별 권한` (하나라도 TRUE이면 허용)
|
||||
|
||||
---
|
||||
|
||||
## 사용 예시
|
||||
|
||||
### 예시 1: 영업팀에게 모든 화면 읽기 권한 부여
|
||||
|
||||
```sql
|
||||
-- 1. 영업팀 권한 그룹 ID 조회
|
||||
SELECT objid FROM authority_master
|
||||
WHERE auth_code = 'SALES_TEAM' AND company_code = 'ILSHIN';
|
||||
-- 결과: objid = 1001
|
||||
|
||||
-- 2. 화면(SCREEN) 전체에 대한 읽기 권한 부여
|
||||
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_read, created_by)
|
||||
VALUES (1001, 'SCREEN', NULL, TRUE, 'admin');
|
||||
-- ^^^^ ^^^^ NULL = 모든 화면
|
||||
```
|
||||
|
||||
### 예시 2: 특정 화면에만 수정 권한 부여
|
||||
|
||||
```sql
|
||||
-- 특정 화면 ID: 'SCR_SALES_REPORT' (screen_definitions.screen_code)
|
||||
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_read, can_update, created_by)
|
||||
VALUES (1001, 'SCREEN', 'SCR_SALES_REPORT', TRUE, TRUE, 'admin');
|
||||
-- ^^^^^^^^^^^^^^^^^ 특정 화면만
|
||||
```
|
||||
|
||||
### 예시 3: 테이블 CRUD 권한 부여 (삭제 제외)
|
||||
|
||||
```sql
|
||||
-- 모든 테이블에 대해 CRU (Create, Read, Update) 권한 부여
|
||||
INSERT INTO resource_permissions (
|
||||
role_group_id, resource_type, resource_id,
|
||||
can_create, can_read, can_update, can_delete,
|
||||
created_by
|
||||
)
|
||||
VALUES (1001, 'TABLE', NULL, TRUE, TRUE, TRUE, FALSE, 'admin');
|
||||
```
|
||||
|
||||
### 예시 4: 플로우 실행 권한 부여
|
||||
|
||||
```sql
|
||||
-- 특정 플로우만 실행 가능
|
||||
INSERT INTO resource_permissions (
|
||||
role_group_id, resource_type, resource_id,
|
||||
can_read, can_execute,
|
||||
created_by
|
||||
)
|
||||
VALUES (1001, 'FLOW', '29', TRUE, TRUE, 'admin');
|
||||
-- ^^ flow_definition.id
|
||||
```
|
||||
|
||||
### 예시 5: 개별 사용자에게 직접 권한 부여
|
||||
|
||||
```sql
|
||||
-- 'john.doe' 사용자에게 시스템 설정 읽기 권한
|
||||
INSERT INTO user_resource_permissions (
|
||||
user_id, resource_type, resource_id, can_read, created_by
|
||||
)
|
||||
VALUES ('john.doe', 'SYSTEM', NULL, TRUE, 'admin');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 백엔드 API 사용법
|
||||
|
||||
### 1. 권한 체크 함수
|
||||
|
||||
```sql
|
||||
-- 사용자 'john.doe'가 화면 'SCR_SALES_REPORT'를 읽을 수 있는지 확인
|
||||
SELECT check_user_resource_permission('john.doe', 'SCREEN', 'SCR_SALES_REPORT', 'read');
|
||||
-- 결과: TRUE 또는 FALSE
|
||||
|
||||
-- 테이블 'contract_mgmt'를 삭제할 수 있는지 확인
|
||||
SELECT check_user_resource_permission('john.doe', 'TABLE', 'contract_mgmt', 'delete');
|
||||
```
|
||||
|
||||
### 2. 접근 가능한 리소스 목록 조회
|
||||
|
||||
```sql
|
||||
-- 사용자 'john.doe'가 읽을 수 있는 모든 화면 목록
|
||||
SELECT * FROM get_user_accessible_resources('john.doe', 'SCREEN', 'read');
|
||||
|
||||
-- 결과 예시:
|
||||
-- resource_id | can_create | can_read | can_update | can_delete | can_execute | can_export
|
||||
-- ------------+------------+----------+------------+------------+-------------+-----------
|
||||
-- * | FALSE | TRUE | FALSE | FALSE | FALSE | FALSE
|
||||
-- SCR_SALES | FALSE | TRUE | TRUE | FALSE | FALSE | TRUE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 프론트엔드 통합
|
||||
|
||||
### React Hook 예시
|
||||
|
||||
```typescript
|
||||
// hooks/usePermission.ts
|
||||
import { useState, useEffect } from "react";
|
||||
import { checkResourcePermission } from "@/lib/api/permission";
|
||||
|
||||
export function usePermission(
|
||||
resourceType: string,
|
||||
resourceId: string | null,
|
||||
permissionType: "create" | "read" | "update" | "delete" | "execute" | "export"
|
||||
) {
|
||||
const [hasPermission, setHasPermission] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPermission = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await checkResourcePermission({
|
||||
resourceType,
|
||||
resourceId,
|
||||
permissionType,
|
||||
});
|
||||
setHasPermission(response.success && response.data?.hasPermission);
|
||||
} catch (error) {
|
||||
console.error("권한 확인 오류:", error);
|
||||
setHasPermission(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkPermission();
|
||||
}, [resourceType, resourceId, permissionType]);
|
||||
|
||||
return { hasPermission, isLoading };
|
||||
}
|
||||
```
|
||||
|
||||
### 컴포넌트에서 사용
|
||||
|
||||
```tsx
|
||||
// components/ScreenDetail.tsx
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export function ScreenDetail({ screenCode }: { screenCode: string }) {
|
||||
const { hasPermission: canUpdate } = usePermission(
|
||||
"SCREEN",
|
||||
screenCode,
|
||||
"update"
|
||||
);
|
||||
const { hasPermission: canDelete } = usePermission(
|
||||
"SCREEN",
|
||||
screenCode,
|
||||
"delete"
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{screenCode}</h1>
|
||||
{canUpdate && <Button>수정</Button>}
|
||||
{canDelete && <Button variant="destructive">삭제</Button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 실전 시나리오
|
||||
|
||||
### 시나리오 1: 영업팀 권한 설정
|
||||
|
||||
**요구사항**:
|
||||
|
||||
- 모든 화면 조회 가능
|
||||
- 계약 테이블(`contract_mgmt`) CRUD 전체
|
||||
- 영업 플로우만 실행 가능
|
||||
- 데이터 내보내기 가능
|
||||
|
||||
```sql
|
||||
-- 영업팀 ID: 1001
|
||||
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_create, can_read, can_update, can_delete, can_execute, can_export, created_by)
|
||||
VALUES
|
||||
-- 모든 화면 읽기
|
||||
(1001, 'SCREEN', NULL, FALSE, TRUE, FALSE, FALSE, FALSE, FALSE, 'admin'),
|
||||
-- 계약 테이블 CRUD
|
||||
(1001, 'TABLE', 'contract_mgmt', TRUE, TRUE, TRUE, TRUE, FALSE, TRUE, 'admin'),
|
||||
-- 영업 플로우 실행
|
||||
(1001, 'FLOW', 'sales_flow', FALSE, TRUE, FALSE, FALSE, TRUE, FALSE, 'admin');
|
||||
```
|
||||
|
||||
### 시나리오 2: 읽기 전용 사용자
|
||||
|
||||
**요구사항**:
|
||||
|
||||
- 모든 리소스 읽기만 가능
|
||||
- 수정/삭제/생성 불가
|
||||
|
||||
```sql
|
||||
-- 읽기 전용 권한 그룹 생성
|
||||
INSERT INTO authority_master (objid, auth_name, auth_code, company_code, status, writer, regdate)
|
||||
VALUES (nextval('seq_authority_master'), '읽기 전용', 'READ_ONLY', 'ILSHIN', 'active', 'admin', NOW());
|
||||
|
||||
-- 권한 부여
|
||||
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_read, created_by)
|
||||
SELECT
|
||||
(SELECT objid FROM authority_master WHERE auth_code = 'READ_ONLY' AND company_code = 'ILSHIN'),
|
||||
type_code,
|
||||
NULL,
|
||||
TRUE,
|
||||
'admin'
|
||||
FROM resource_types
|
||||
WHERE type_code != 'SYSTEM'; -- 시스템 제외
|
||||
```
|
||||
|
||||
### 시나리오 3: 개발팀 (DDL 실행 권한)
|
||||
|
||||
**요구사항**:
|
||||
|
||||
- 테이블 생성/삭제 가능 (DDL 실행)
|
||||
- 모든 화면 CRUD
|
||||
- 플로우 생성/실행
|
||||
|
||||
```sql
|
||||
-- 개발팀 ID: 1002
|
||||
INSERT INTO resource_permissions (role_group_id, resource_type, resource_id, can_create, can_read, can_update, can_delete, can_execute, created_by)
|
||||
VALUES
|
||||
-- 화면 CRUD
|
||||
(1002, 'SCREEN', NULL, TRUE, TRUE, TRUE, TRUE, FALSE, 'admin'),
|
||||
-- 테이블 CRUD + 실행(DDL)
|
||||
(1002, 'TABLE', NULL, TRUE, TRUE, TRUE, TRUE, TRUE, 'admin'),
|
||||
-- 플로우 CRUD + 실행
|
||||
(1002, 'FLOW', NULL, TRUE, TRUE, TRUE, TRUE, TRUE, 'admin');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 실행
|
||||
|
||||
```bash
|
||||
# Docker Compose 환경
|
||||
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d ilshin < db/migrations/028_add_company_code_to_authority_master.sql
|
||||
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d ilshin < db/migrations/029_create_resource_based_permission_system.sql
|
||||
|
||||
# 검증
|
||||
docker exec -it <DB_CONTAINER_NAME> psql -U postgres -d ilshin -c "SELECT * FROM resource_types;"
|
||||
docker exec -it <DB_CONTAINER_NAME> psql -U postgres -d ilshin -c "SELECT * FROM v_role_permissions_summary;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 추가 기능 확장 아이디어
|
||||
|
||||
### 1. 시간 기반 권한
|
||||
|
||||
```sql
|
||||
ALTER TABLE resource_permissions ADD COLUMN valid_from TIMESTAMP;
|
||||
ALTER TABLE resource_permissions ADD COLUMN valid_until TIMESTAMP;
|
||||
```
|
||||
|
||||
### 2. 조건부 권한 (Row-Level Security)
|
||||
|
||||
```sql
|
||||
-- 예: 자신이 생성한 데이터만 수정 가능
|
||||
ALTER TABLE resource_permissions ADD COLUMN row_condition TEXT;
|
||||
-- 'created_by = :user_id'
|
||||
```
|
||||
|
||||
### 3. 권한 요청/승인 워크플로우
|
||||
|
||||
```sql
|
||||
CREATE TABLE permission_requests (
|
||||
request_id SERIAL PRIMARY KEY,
|
||||
user_id VARCHAR(50),
|
||||
resource_type VARCHAR(50),
|
||||
resource_id VARCHAR(255),
|
||||
permission_type VARCHAR(20),
|
||||
reason TEXT,
|
||||
status VARCHAR(20), -- 'pending', 'approved', 'rejected'
|
||||
approved_by VARCHAR(50),
|
||||
approved_date TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q1: 메뉴 기반 권한과 무엇이 다른가요?
|
||||
|
||||
**A**: 메뉴는 고정된 화면을 가정하지만, 이 시스템은 사용자가 **동적으로 생성한 화면/테이블**에도 권한을 부여할 수 있습니다. 예를 들어, 사용자 A가 "계약 관리" 화면을 생성하면, 권한 그룹 B에게 그 화면의 읽기 권한을 즉시 부여할 수 있습니다.
|
||||
|
||||
### Q2: `resource_id`가 NULL인 경우와 특정 ID인 경우의 차이는?
|
||||
|
||||
**A**:
|
||||
|
||||
- `resource_id = NULL`: **해당 타입의 모든 리소스**에 대한 권한
|
||||
- `resource_id = 'SCR_001'`: **특정 리소스만** 권한
|
||||
|
||||
예: `(SCREEN, NULL, read)` = 모든 화면 읽기
|
||||
예: `(SCREEN, 'SCR_001', read)` = SCR_001 화면만 읽기
|
||||
|
||||
### Q3: 권한 그룹과 개별 권한의 우선순위는?
|
||||
|
||||
**A**: **OR 연산**입니다. 권한 그룹에서 허용되거나, 개별 권한에서 허용되면 최종적으로 허용됩니다.
|
||||
|
||||
### Q4: COMPANY_ADMIN은 왜 SYSTEM 타입 권한이 없나요?
|
||||
|
||||
**A**: SYSTEM 타입은 **시스템 전체 설정**(예: 회사 생성/삭제, 전체 사용자 관리)이므로 SUPER_ADMIN만 접근 가능합니다.
|
||||
|
||||
### Q5: 동적으로 생성된 화면의 `resource_id`는 무엇인가요?
|
||||
|
||||
**A**: `screen_definitions.screen_code`를 사용합니다. 예: `'SCR_CONTRACT_MGMT'`
|
||||
|
||||
### Q6: 플로우의 `resource_id`는?
|
||||
|
||||
**A**: `flow_definition.id` (숫자)를 문자열로 변환하여 사용합니다. 예: `'29'`
|
||||
|
||||
---
|
||||
|
||||
## 관련 파일
|
||||
|
||||
- **마이그레이션**: `db/migrations/028_add_company_code_to_authority_master.sql`
|
||||
- **마이그레이션**: `db/migrations/029_create_resource_based_permission_system.sql`
|
||||
- **백엔드 서비스**: `backend-node/src/services/RoleService.ts`
|
||||
- **프론트엔드 API**: `frontend/lib/api/role.ts`
|
||||
- **권한 체계 가이드**: `docs/권한_체계_가이드.md`
|
||||
359
docs/메뉴_기반_권한_시스템_가이드.md
Normal file
359
docs/메뉴_기반_권한_시스템_가이드.md
Normal file
@@ -0,0 +1,359 @@
|
||||
# 메뉴 기반 권한 시스템 가이드 (동적 화면 대응)
|
||||
|
||||
## 개요
|
||||
|
||||
**기존 메뉴 기반 권한 시스템을 유지**하면서 **동적으로 생성되는 화면에도 대응**하는 개선된 시스템입니다.
|
||||
|
||||
### 핵심 아이디어 💡
|
||||
|
||||
```
|
||||
사용자가 화면 생성
|
||||
↓
|
||||
자동으로 메뉴 추가 (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`
|
||||
Reference in New Issue
Block a user