815 lines
22 KiB
Markdown
815 lines
22 KiB
Markdown
|
|
# 권한 그룹 관리 시스템 상세 가이드
|
||
|
|
|
||
|
|
> 작성일: 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`
|