각 회사별 데이터 분리

This commit is contained in:
kjs
2025-10-27 16:40:59 +09:00
parent 783ce5594e
commit 29c49d7f07
59 changed files with 8698 additions and 585 deletions

View 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. **권한 체크 로직 통합** → 미들웨어 개선
이 설계를 구현하시겠습니까?

View 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` - 메뉴 권한 마이그레이션
---
## 문의사항
기술적 문의사항이나 추가 기능 요청은 개발팀에 문의하세요.

View 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`

View 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`

View 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`