ERP-node/docs/권한_그룹_관리_상세_가이드.md

815 lines
22 KiB
Markdown
Raw Permalink Normal View History

# 권한 그룹 관리 시스템 상세 가이드
> 작성일: 2025-01-27
> 파일 위치: `backend-node/src/services/roleService.ts`, `backend-node/src/controllers/roleController.ts`
---
## 📋 목차
1. [권한 그룹 관리 구조](#권한-그룹-관리-구조)
2. [최고 관리자 권한](#최고-관리자-권한)
3. [회사 관리자 권한](#회사-관리자-권한)
4. [메뉴 권한 설정](#메뉴-권한-설정)
5. [멤버 관리](#멤버-관리)
6. [권한 체크 로직](#권한-체크-로직)
---
## 권한 그룹 관리 구조
### 데이터베이스 구조
```sql
-- 권한 그룹 마스터 테이블
authority_master (
objid SERIAL PRIMARY KEY, -- 권한 그룹 ID
auth_name VARCHAR(200), -- 권한 그룹명
auth_code VARCHAR(100), -- 권한 그룹 코드
company_code VARCHAR(50), -- 회사 코드 ⭐
status VARCHAR(20), -- 상태 (active/inactive)
writer VARCHAR(50), -- 작성자
regdate TIMESTAMP -- 등록일
)
-- 권한 그룹 멤버 테이블
authority_sub_user (
objid SERIAL PRIMARY KEY, -- 멤버 ID
master_objid INTEGER, -- 권한 그룹 ID (FK)
user_id VARCHAR(50), -- 사용자 ID
writer VARCHAR(50), -- 작성자
regdate TIMESTAMP -- 등록일
)
-- 메뉴 권한 테이블
rel_menu_auth (
objid SERIAL PRIMARY KEY, -- 권한 ID
menu_objid INTEGER, -- 메뉴 ID (FK)
auth_objid INTEGER, -- 권한 그룹 ID (FK)
create_yn VARCHAR(1), -- 생성 권한 (Y/N)
read_yn VARCHAR(1), -- 조회 권한 (Y/N)
update_yn VARCHAR(1), -- 수정 권한 (Y/N)
delete_yn VARCHAR(1), -- 삭제 권한 (Y/N)
execute_yn VARCHAR(1), -- 실행 권한 (Y/N) ⭐
export_yn VARCHAR(1), -- 내보내기 권한 (Y/N) ⭐
writer VARCHAR(50), -- 작성자
regdate TIMESTAMP -- 등록일
)
-- 메뉴 정보 테이블
menu_info (
objid SERIAL PRIMARY KEY, -- 메뉴 ID
menu_name_kor VARCHAR(200), -- 메뉴명 (한글)
menu_name_eng VARCHAR(200), -- 메뉴명 (영문)
menu_code VARCHAR(100), -- 메뉴 코드
menu_url VARCHAR(500), -- 메뉴 URL
menu_type INTEGER, -- 메뉴 타입
parent_obj_id INTEGER, -- 부모 메뉴 ID
seq INTEGER, -- 정렬 순서
company_code VARCHAR(50), -- 회사 코드 ⭐
status VARCHAR(20) -- 상태 (active/inactive)
)
```
### 권한 계층 구조
```
최고 관리자 (SUPER_ADMIN, company_code = "*")
├─ 모든 회사의 권한 그룹 조회/생성/수정/삭제
├─ 모든 회사의 메뉴에 대해 권한 부여 가능
└─ 다른 회사 사용자를 권한 그룹에 추가 가능
회사 관리자 (COMPANY_ADMIN, company_code = "20", "30", etc.)
├─ 자기 회사의 권한 그룹만 조회/생성/수정/삭제
├─ 자기 회사의 메뉴에 대해서만 권한 부여 가능
└─ 자기 회사 사용자만 권한 그룹에 추가 가능
일반 사용자 (USER)
└─ 권한 그룹 관리 불가 (읽기 전용)
```
---
## 최고 관리자 권한
### 1. 권한 그룹 목록 조회
**API**: `GET /api/roles`
**로직**:
```typescript
export const getRoleGroups = async (req, res) => {
const companyCode = req.query.companyCode as string | undefined;
if (isSuperAdmin(req.user)) {
// 최고 관리자: companyCode 파라미터가 있으면 해당 회사만, 없으면 전체 조회
targetCompanyCode = companyCode; // undefined면 전체 조회
} else {
// 회사 관리자: 자기 회사만 조회
targetCompanyCode = req.user?.companyCode;
}
const roleGroups = await RoleService.getRoleGroups(targetCompanyCode, search);
};
```
**데이터베이스 쿼리**:
```typescript
// RoleService.getRoleGroups()
let sql = `SELECT * FROM authority_master WHERE 1=1`;
if (companyCode) {
sql += ` AND company_code = $1`; // 특정 회사만
} else {
// 조건 없음 -> 전체 조회
}
```
**결과**:
-`companyCode` 파라미터 없음 → **모든 회사의 권한 그룹 조회**
-`companyCode = "20"`**회사 "20"의 권한 그룹만 조회**
-`companyCode = "*"`**공통 권한 그룹만 조회** (있다면)
### 2. 권한 그룹 생성
**API**: `POST /api/roles`
**로직**:
```typescript
export const createRoleGroup = async (req, res) => {
const { authName, authCode, companyCode } = req.body;
// 최고 관리자가 아닌 경우, 자기 회사에만 생성 가능
if (!isSuperAdmin(req.user) && companyCode !== req.user?.companyCode) {
return res.status(403).json({
message: "자신의 회사에만 권한 그룹을 생성할 수 있습니다",
});
}
await RoleService.createRoleGroup({
authName,
authCode,
companyCode,
writer,
});
};
```
**결과**:
- ✅ 최고 관리자는 **어떤 회사에도** 권한 그룹 생성 가능
-`companyCode = "*"` 로 공통 권한 그룹도 생성 가능
- ❌ 회사 관리자는 자기 회사에만 생성 가능
### 3. 메뉴 목록 조회 (권한 부여용)
**API**: `GET /api/roles/menus/all?companyCode=20`
**로직**:
```typescript
export const getAllMenus = async (req, res) => {
const requestedCompanyCode = req.query.companyCode as string | undefined;
let companyCode: string | undefined;
if (isSuperAdmin(req.user)) {
// 최고 관리자: 요청한 회사 코드 사용 (없으면 전체)
companyCode = requestedCompanyCode;
} else {
// 회사 관리자: 자기 회사 코드만 사용
companyCode = req.user?.companyCode;
}
const menus = await RoleService.getAllMenus(companyCode);
};
```
**데이터베이스 쿼리**:
```typescript
// RoleService.getAllMenus()
let whereConditions = ["status = 'active'"];
if (companyCode) {
// 특정 회사 메뉴만 조회 (공통 메뉴 제외)
whereConditions.push(`company_code = $1`);
} else {
// 조건 없음 -> 전체 메뉴 조회
}
const sql = `
SELECT * FROM menu_info
WHERE ${whereConditions.join(" AND ")}
ORDER BY seq, menu_name_kor
`;
```
**결과**:
-`companyCode` 없음 → **모든 회사의 모든 메뉴 조회**
-`companyCode = "20"`**회사 "20"의 메뉴만 조회**
-`companyCode = "*"`**공통 메뉴만 조회**
**⚠️ 프론트엔드 구현 주의사항:**
프론트엔드에서 권한 그룹 상세 화면 (메뉴 권한 설정)에서는 **최고 관리자가 모든 메뉴를 볼 수 있도록** `companyCode` 파라미터를 전달하지 않아야 합니다:
```typescript
// MenuPermissionsTable.tsx
const { user: currentUser } = useAuth();
const isSuperAdmin =
currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN";
// 최고 관리자: companyCode 없이 모든 메뉴 조회
// 회사 관리자: 자기 회사 메뉴만 조회
const targetCompanyCode = isSuperAdmin ? undefined : roleGroup.companyCode;
const response = await roleAPI.getAllMenus(targetCompanyCode);
```
**이렇게 하지 않으면:**
- 최고 관리자가 회사 "20"의 권한 그룹에 들어가도
- `roleGroup.companyCode` (= "20")를 API로 전달하면
- 회사 "20"의 메뉴만 보이게 됨 ❌
**올바른 동작:**
- 최고 관리자가 회사 "20"의 권한 그룹에 들어가면
- `undefined`를 API로 전달하여
- **모든 회사의 모든 메뉴**를 볼 수 있어야 함 ✅
### 4. 메뉴 권한 설정
**API**: `POST /api/roles/:id/menu-permissions`
**Request Body**:
```json
{
"permissions": [
{
"menuObjid": 101,
"createYn": "Y",
"readYn": "Y",
"updateYn": "Y",
"deleteYn": "Y",
"executeYn": "Y",
"exportYn": "Y"
},
{
"menuObjid": 102,
"createYn": "N",
"readYn": "Y",
"updateYn": "N",
"deleteYn": "N",
"executeYn": "N",
"exportYn": "N"
}
]
}
```
**로직**:
```typescript
export const setMenuPermissions = async (req, res) => {
const authObjid = parseInt(req.params.id, 10);
const { permissions } = req.body;
// 권한 그룹 조회
const roleGroup = await RoleService.getRoleGroupById(authObjid);
// 권한 체크
if (
!isSuperAdmin(req.user) &&
!canAccessCompanyData(req.user, roleGroup.companyCode)
) {
return res.status(403).json({ message: "메뉴 권한 설정 권한이 없습니다" });
}
await RoleService.setMenuPermissions(authObjid, permissions, writer);
};
```
**데이터베이스 쿼리**:
```typescript
// RoleService.setMenuPermissions()
// 1. 기존 권한 삭제
await query("DELETE FROM rel_menu_auth WHERE auth_objid = $1", [authObjid]);
// 2. 새로운 권한 삽입
const sql = `
INSERT INTO rel_menu_auth
(objid, menu_objid, auth_objid, create_yn, read_yn, update_yn, delete_yn, execute_yn, export_yn, writer, regdate)
VALUES
${values} -- (nextval('seq'), $1, $2, $3, $4, $5, $6, $7, $8, $9, NOW()), ...
`;
```
**결과**:
- ✅ 최고 관리자는 **어떤 권한 그룹에도** 메뉴 권한 설정 가능
- ✅ **모든 회사의 메뉴**에 대해 권한 부여 가능
- ✅ CRUD + Execute + Export 총 6가지 권한 설정
### 5. 멤버 관리
**API**: `PUT /api/roles/:id/members`
**Request Body**:
```json
{
"userIds": ["user001", "user002", "user003"]
}
```
**로직**:
```typescript
export const updateRoleMembers = async (req, res) => {
const masterObjid = parseInt(req.params.id, 10);
const { userIds } = req.body;
// 권한 그룹 조회
const roleGroup = await RoleService.getRoleGroupById(masterObjid);
// 권한 체크
if (
!isSuperAdmin(req.user) &&
!canAccessCompanyData(req.user, roleGroup.companyCode)
) {
return res
.status(403)
.json({ message: "권한 그룹 멤버 수정 권한이 없습니다" });
}
// 기존 멤버 조회
const existingMembers = await RoleService.getRoleMembers(masterObjid);
const existingUserIds = existingMembers.map((m) => m.userId);
// 추가할 멤버 (새로 추가된 것들)
const toAdd = userIds.filter((id) => !existingUserIds.includes(id));
// 제거할 멤버 (기존에 있었는데 없어진 것들)
const toRemove = existingUserIds.filter((id) => !userIds.includes(id));
// 추가
if (toAdd.length > 0) {
await RoleService.addRoleMembers(masterObjid, toAdd, writer);
}
// 제거
if (toRemove.length > 0) {
await RoleService.removeRoleMembers(masterObjid, toRemove, writer);
}
};
```
**결과**:
- ✅ 최고 관리자는 **어떤 회사 사용자도** 권한 그룹에 추가 가능
- ✅ 다른 회사 권한 그룹에도 멤버 추가 가능
- ⚠️ 단, 사용자 목록 API에서 최고 관리자는 필터링되므로 추가 불가
---
## 회사 관리자 권한
### 1. 권한 그룹 목록 조회
**로직**:
```typescript
if (!isSuperAdmin(req.user)) {
// 회사 관리자: 자기 회사만 조회
targetCompanyCode = req.user?.companyCode; // 강제로 자기 회사
}
```
**결과**:
- ✅ 자기 회사 (`company_code = "20"`) 권한 그룹만 조회
- ❌ 다른 회사 권한 그룹은 **절대 볼 수 없음**
### 2. 권한 그룹 생성
**로직**:
```typescript
if (!isSuperAdmin(req.user) && companyCode !== req.user?.companyCode) {
return res.status(403).json({
message: "자신의 회사에만 권한 그룹을 생성할 수 있습니다",
});
}
```
**결과**:
- ✅ 자기 회사에만 권한 그룹 생성 가능
- ❌ 다른 회사나 공통(`*`)에는 생성 불가
### 3. 메뉴 목록 조회
**로직**:
```typescript
if (!isSuperAdmin(req.user)) {
// 회사 관리자: 자기 회사 코드만 사용
companyCode = req.user?.companyCode; // 강제로 자기 회사
}
```
**데이터베이스 쿼리**:
```sql
SELECT * FROM menu_info
WHERE status = 'active'
AND company_code = '20' -- 자기 회사만
ORDER BY seq, menu_name_kor
```
**결과**:
- ✅ 자기 회사 메뉴만 조회
- ❌ 공통 메뉴(`company_code = "*"`)는 **조회되지 않음**
- ❌ 다른 회사 메뉴는 **절대 볼 수 없음**
### 4. 메뉴 권한 설정
**로직**:
```typescript
// 권한 그룹의 회사 코드 체크
if (
!isSuperAdmin(req.user) &&
!canAccessCompanyData(req.user, roleGroup.companyCode)
) {
return res.status(403).json({ message: "메뉴 권한 설정 권한이 없습니다" });
}
```
**`canAccessCompanyData` 함수**:
```typescript
export function canAccessCompanyData(
user: UserInfo | undefined,
targetCompanyCode: string
): boolean {
if (!user) return false;
if (isSuperAdmin(user)) return true; // 슈퍼관리자는 모든 회사 접근 가능
return user.companyCode === targetCompanyCode; // 자기 회사만 접근 가능
}
```
**결과**:
- ✅ 자기 회사 권한 그룹에만 메뉴 권한 설정 가능
- ✅ 자기 회사 메뉴에 대해서만 권한 부여 가능
- ❌ 다른 회사 권한 그룹이나 메뉴는 접근 불가
### 5. 멤버 관리
**로직**:
```typescript
// 권한 그룹의 회사 코드 체크
if (
!isSuperAdmin(req.user) &&
!canAccessCompanyData(req.user, roleGroup.companyCode)
) {
return res
.status(403)
.json({ message: "권한 그룹 멤버 수정 권한이 없습니다" });
}
```
**사용자 목록 조회 (Dual List Box)**:
```typescript
// adminController.getUserList()
if (req.user && req.user.companyCode !== "*") {
whereConditions.push(`company_code != '*'`); // 최고 관리자 필터링
whereConditions.push(`company_code = $1`); // 자기 회사만
}
```
**결과**:
- ✅ 자기 회사 권한 그룹에만 멤버 추가/제거 가능
- ✅ 자기 회사 사용자만 멤버로 추가 가능
- ❌ 최고 관리자는 목록에서 **절대 보이지 않음**
- ❌ 다른 회사 사용자는 **절대 볼 수 없음**
---
## 메뉴 권한 설정
### 권한 종류 (6가지)
| 권한 | 컬럼명 | 설명 | 예시 |
| ------------ | ------------ | ------------------------- | ------------------------ |
| **생성** | `create_yn` | 데이터 생성 가능 여부 | 사용자 추가, 주문 생성 |
| **조회** | `read_yn` | 데이터 조회 가능 여부 | 목록 보기, 상세 보기 |
| **수정** | `update_yn` | 데이터 수정 가능 여부 | 정보 변경, 상태 변경 |
| **삭제** | `delete_yn` | 데이터 삭제 가능 여부 | 항목 삭제, 데이터 제거 |
| **실행** | `execute_yn` | 특수 기능 실행 가능 여부 | 플로우 실행, 배치 실행 |
| **내보내기** | `export_yn` | 데이터 내보내기 가능 여부 | Excel 다운로드, PDF 출력 |
### 권한 설정 예시
#### 1. 읽기 전용 권한
```json
{
"createYn": "N",
"readYn": "Y",
"updateYn": "N",
"deleteYn": "N",
"executeYn": "N",
"exportYn": "N"
}
```
→ 조회만 가능, 수정/삭제 불가
#### 2. 전체 권한
```json
{
"createYn": "Y",
"readYn": "Y",
"updateYn": "Y",
"deleteYn": "Y",
"executeYn": "Y",
"exportYn": "Y"
}
```
→ 모든 작업 가능
#### 3. 운영자 권한
```json
{
"createYn": "Y",
"readYn": "Y",
"updateYn": "Y",
"deleteYn": "N",
"executeYn": "Y",
"exportYn": "Y"
}
```
→ 삭제 제외 모든 작업 가능
### 메뉴 권한 설정 프로세스
```mermaid
graph TD
A[권한 그룹 선택] --> B[메뉴 목록 조회]
B --> C{최고 관리자?}
C -->|Yes| D[모든 회사 메뉴 조회]
C -->|No| E[자기 회사 메뉴만 조회]
D --> F[메뉴별 권한 설정]
E --> F
F --> G[기존 권한 삭제]
G --> H[새 권한 일괄 삽입]
H --> I[완료]
```
---
## 멤버 관리
### Dual List Box 구조
```
┌─────────────────────┐ ┌─────────────────────┐
│ 사용 가능한 사용자 │ → │ 권한 그룹 멤버 │
│ │ ← │ │
│ [ ] user001 │ │ [x] user005 │
│ [ ] user002 │ │ [x] user006 │
│ [ ] user003 │ │ [x] user007 │
│ [ ] user004 │ │ │
└─────────────────────┘ └─────────────────────┘
```
### 멤버 추가/제거 로직
```typescript
// 프론트엔드에서 전송
PUT /api/roles/123/members
{
"userIds": ["user005", "user006", "user008"] // 최종 멤버 목록
}
// 백엔드 처리
const existingUserIds = ["user005", "user006", "user007"]; // 기존 멤버
const newUserIds = ["user005", "user006", "user008"]; // 요청된 멤버
const toAdd = ["user008"]; // 새로 추가 (기존에 없던 것)
const toRemove = ["user007"]; // 제거 (기존에 있었는데 없어진 것)
// 추가
INSERT INTO authority_sub_user (master_objid, user_id, writer, regdate)
VALUES (123, 'user008', 'admin', NOW())
// 제거
DELETE FROM authority_sub_user
WHERE master_objid = 123 AND user_id = 'user007'
```
### 사용자 목록 필터링
**API**: `GET /api/admin/users?companyCode=20&size=1000`
**로직**:
```typescript
// 회사 코드 필터
if (companyCode && companyCode.trim()) {
whereConditions.push(`company_code = $1`);
queryParams.push(companyCode.trim());
}
// 최고 관리자 필터링 (중요!)
if (req.user && req.user.companyCode !== "*") {
whereConditions.push(`company_code != '*'`); // 최고 관리자 숨김
}
// 검색 조건
if (search && search.trim()) {
whereConditions.push(`(
user_id ILIKE $2 OR
user_name ILIKE $2 OR
dept_name ILIKE $2
)`);
queryParams.push(`%${search.trim()}%`);
}
```
**결과**:
- ✅ 최고 관리자: 요청한 회사의 사용자 목록
- ✅ 회사 관리자: 자기 회사 사용자 목록
- ✅ 최고 관리자(`company_code = "*"`)는 **절대 목록에 표시되지 않음**
---
## 권한 체크 로직
### 1. `isSuperAdmin()`
**파일**: `backend-node/src/utils/permissionUtils.ts`
```typescript
export function isSuperAdmin(user: UserInfo | undefined): boolean {
if (!user) return false;
return user.companyCode === "*";
}
```
**사용**:
- 권한 그룹 목록 전체 조회 여부
- 다른 회사 데이터 접근 여부
- 모든 메뉴 조회 여부
### 2. `isCompanyAdmin()`
```typescript
export function isCompanyAdmin(user: UserInfo | undefined): boolean {
if (!user) return false;
return user.userType === "COMPANY_ADMIN";
}
```
**사용**:
- 권한 그룹 관리 접근 여부
- 메뉴 권한 설정 접근 여부
### 3. `canAccessCompanyData()`
```typescript
export function canAccessCompanyData(
user: UserInfo | undefined,
targetCompanyCode: string
): boolean {
if (!user) return false;
if (isSuperAdmin(user)) return true; // 슈퍼관리자는 모든 회사 접근 가능
return user.companyCode === targetCompanyCode; // 자기 회사만 접근 가능
}
```
**사용**:
- 권한 그룹 상세 조회 접근 여부
- 권한 그룹 수정/삭제 접근 여부
- 멤버 관리 접근 여부
- 메뉴 권한 설정 접근 여부
### 권한 체크 플로우
```mermaid
graph TD
A[API 요청] --> B{로그인 확인}
B -->|No| C[401 Unauthorized]
B -->|Yes| D{최고 관리자?}
D -->|Yes| E[모든 데이터 접근 허용]
D -->|No| F{회사 관리자?}
F -->|No| G[403 Forbidden]
F -->|Yes| H{자기 회사 데이터?}
H -->|No| I[403 Forbidden]
H -->|Yes| J[접근 허용]
```
---
## 💡 핵심 정리
### ✅ 최고 관리자가 할 수 있는 것
1. **권한 그룹 관리**
- ✅ 모든 회사의 권한 그룹 조회
- ✅ 어떤 회사에도 권한 그룹 생성 가능
- ✅ 다른 회사 권한 그룹 수정/삭제 가능
2. **메뉴 권한 설정**
- ✅ 모든 회사의 메뉴 조회 가능
- ✅ 어떤 권한 그룹에도 메뉴 권한 부여 가능
- ✅ 모든 메뉴에 대해 CRUD + Execute + Export 권한 설정
3. **멤버 관리**
- ✅ 어떤 회사 권한 그룹에도 멤버 추가 가능
- ✅ 다른 회사 사용자도 멤버로 추가 가능
- ⚠️ 단, 최고 관리자는 사용자 목록에서 필터링되므로 추가 불가
### ✅ 회사 관리자가 할 수 있는 것
1. **권한 그룹 관리**
- ✅ 자기 회사 권한 그룹만 조회
- ✅ 자기 회사에만 권한 그룹 생성 가능
- ✅ 자기 회사 권한 그룹만 수정/삭제 가능
2. **메뉴 권한 설정**
- ✅ 자기 회사 메뉴만 조회 가능
- ✅ 자기 회사 권한 그룹에만 메뉴 권한 부여 가능
- ✅ 자기 회사 메뉴에 대해서만 CRUD + Execute + Export 권한 설정
3. **멤버 관리**
- ✅ 자기 회사 권한 그룹에만 멤버 추가 가능
- ✅ 자기 회사 사용자만 멤버로 추가 가능
- ✅ 최고 관리자는 목록에서 절대 보이지 않음
### ❌ 제한 사항
1. **회사 관리자는 절대 할 수 없는 것**
- ❌ 다른 회사 권한 그룹 조회/수정/삭제
- ❌ 공통 메뉴(`company_code = "*"`) 조회
- ❌ 최고 관리자를 멤버로 추가
- ❌ 다른 회사 사용자를 멤버로 추가
2. **최고 관리자도 할 수 없는 것**
- ❌ 다른 최고 관리자를 권한 그룹 멤버로 추가 (사용자 목록 필터링으로 인해)
---
## 📊 권한 매트릭스
| 작업 | 최고 관리자 | 회사 관리자 | 일반 사용자 |
| ------------------ | ------------------- | ------------------------ | ----------- |
| **권한 그룹 조회** | ✅ 모든 회사 | ✅ 자기 회사만 | ❌ |
| **권한 그룹 생성** | ✅ 모든 회사 | ✅ 자기 회사만 | ❌ |
| **권한 그룹 수정** | ✅ 모든 회사 | ✅ 자기 회사만 | ❌ |
| **권한 그룹 삭제** | ✅ 모든 회사 | ✅ 자기 회사만 | ❌ |
| **메뉴 목록 조회** | ✅ 모든 회사 메뉴 | ✅ 자기 회사 메뉴만 | ❌ |
| **메뉴 권한 설정** | ✅ 모든 권한 그룹 | ✅ 자기 회사 권한 그룹만 | ❌ |
| **멤버 추가** | ✅ 모든 회사 사용자 | ✅ 자기 회사 사용자만 | ❌ |
| **멤버 제거** | ✅ 모든 회사 멤버 | ✅ 자기 회사 멤버만 | ❌ |
---
## 📝 작성자
- 작성: AI Assistant (Claude Sonnet 4.5)
- 검토 필요: 백엔드 개발자, 시스템 아키텍트
- 관련 파일:
- `backend-node/src/services/roleService.ts`
- `backend-node/src/controllers/roleController.ts`
- `backend-node/src/utils/permissionUtils.ts`
- `frontend/components/admin/RoleDetailManagement.tsx`