회사별 메뉴 필터링 기능
This commit is contained in:
parent
29c49d7f07
commit
02df4355e2
|
|
@ -17,19 +17,25 @@ export async function getAdminMenus(
|
|||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info("=== 메뉴 목록 조회 시작 ===");
|
||||
logger.info("=== 관리자 메뉴 목록 조회 시작 ===");
|
||||
|
||||
// 현재 로그인한 사용자의 회사 코드와 로케일 가져오기
|
||||
// 현재 로그인한 사용자의 정보 가져오기
|
||||
const userId = req.user?.userId;
|
||||
const userCompanyCode = req.user?.companyCode || "ILSHIN";
|
||||
const userType = req.user?.userType;
|
||||
const userLang = (req.query.userLang as string) || "ko";
|
||||
const menuType = req.query.menuType as string | undefined; // menuType 파라미터 추가
|
||||
|
||||
logger.info(`사용자 ID: ${userId}`);
|
||||
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
|
||||
logger.info(`사용자 유형: ${userType}`);
|
||||
logger.info(`사용자 로케일: ${userLang}`);
|
||||
logger.info(`메뉴 타입: ${menuType || "전체"}`);
|
||||
|
||||
const paramMap = {
|
||||
userId,
|
||||
userCompanyCode,
|
||||
userType,
|
||||
userLang,
|
||||
menuType, // menuType 추가
|
||||
};
|
||||
|
|
@ -37,7 +43,7 @@ export async function getAdminMenus(
|
|||
const menuList = await AdminService.getAdminMenuList(paramMap);
|
||||
|
||||
logger.info(
|
||||
`메뉴 조회 결과: ${menuList.length}개 (타입: ${menuType || "전체"})`
|
||||
`관리자 메뉴 조회 결과: ${menuList.length}개 (타입: ${menuType || "전체"}, 회사: ${userCompanyCode})`
|
||||
);
|
||||
if (menuList.length > 0) {
|
||||
logger.info("첫 번째 메뉴:", menuList[0]);
|
||||
|
|
@ -76,21 +82,29 @@ export async function getUserMenus(
|
|||
try {
|
||||
logger.info("=== 사용자 메뉴 목록 조회 시작 ===");
|
||||
|
||||
// 현재 로그인한 사용자의 회사 코드와 로케일 가져오기
|
||||
// 현재 로그인한 사용자의 정보 가져오기
|
||||
const userId = req.user?.userId;
|
||||
const userCompanyCode = req.user?.companyCode || "ILSHIN";
|
||||
const userType = req.user?.userType;
|
||||
const userLang = (req.query.userLang as string) || "ko";
|
||||
|
||||
logger.info(`사용자 ID: ${userId}`);
|
||||
logger.info(`사용자 회사 코드: ${userCompanyCode}`);
|
||||
logger.info(`사용자 유형: ${userType}`);
|
||||
logger.info(`사용자 로케일: ${userLang}`);
|
||||
|
||||
const paramMap = {
|
||||
userId,
|
||||
userCompanyCode,
|
||||
userType,
|
||||
userLang,
|
||||
};
|
||||
|
||||
const menuList = await AdminService.getUserMenuList(paramMap);
|
||||
|
||||
logger.info(`사용자 메뉴 조회 결과: ${menuList.length}개`);
|
||||
logger.info(
|
||||
`사용자 메뉴 조회 결과: ${menuList.length}개 (회사: ${userCompanyCode})`
|
||||
);
|
||||
if (menuList.length > 0) {
|
||||
logger.info("첫 번째 메뉴:", menuList[0]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,18 +3,51 @@ import { query, queryOne } from "../database/db";
|
|||
|
||||
export class AdminService {
|
||||
/**
|
||||
* 관리자 메뉴 목록 조회
|
||||
* 관리자 메뉴 목록 조회 (회사별 필터링 적용)
|
||||
*/
|
||||
static async getAdminMenuList(paramMap: any): Promise<any[]> {
|
||||
try {
|
||||
logger.info("AdminService.getAdminMenuList 시작 - 파라미터:", paramMap);
|
||||
|
||||
const { userLang = "ko", menuType } = paramMap;
|
||||
const {
|
||||
userId,
|
||||
userCompanyCode,
|
||||
userType,
|
||||
userLang = "ko",
|
||||
menuType,
|
||||
} = paramMap;
|
||||
|
||||
// menuType에 따른 WHERE 조건 생성
|
||||
const menuTypeCondition =
|
||||
menuType !== undefined ? `MENU_TYPE = ${parseInt(menuType)}` : "1 = 1";
|
||||
|
||||
// 회사별 필터링 조건 생성
|
||||
let companyFilter = "";
|
||||
let queryParams: any[] = [userLang];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
||||
// SUPER_ADMIN: 모든 메뉴 표시
|
||||
logger.info("✅ SUPER_ADMIN 모드: 모든 관리자 메뉴 표시");
|
||||
companyFilter = "";
|
||||
} else if (userType === "COMPANY_ADMIN") {
|
||||
// COMPANY_ADMIN: 자기 회사 메뉴 + 공통 메뉴
|
||||
companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE IS NULL)`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ COMPANY_ADMIN 모드: 회사 ${userCompanyCode} 관리자 메뉴 + 공통 메뉴 표시`
|
||||
);
|
||||
} else {
|
||||
// 일반 사용자: 자기 회사 메뉴만
|
||||
companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE IS NULL)`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ 일반 사용자 모드: 회사 ${userCompanyCode} 관리자 메뉴 + 공통 메뉴 표시`
|
||||
);
|
||||
}
|
||||
|
||||
// 기존 Java의 selectAdminMenuList 쿼리를 Raw Query로 포팅
|
||||
// WITH RECURSIVE 쿼리 구현
|
||||
const menuList = await query<any>(
|
||||
|
|
@ -96,6 +129,8 @@ export class AdminService {
|
|||
)
|
||||
FROM MENU_INFO MENU
|
||||
WHERE ${menuTypeCondition}
|
||||
AND STATUS = 'active'
|
||||
${companyFilter}
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM MENU_INFO parent_menu
|
||||
WHERE parent_menu.OBJID = MENU.PARENT_OBJ_ID
|
||||
|
|
@ -160,6 +195,7 @@ export class AdminService {
|
|||
FROM MENU_INFO MENU_SUB
|
||||
JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID
|
||||
WHERE MENU_SUB.OBJID != ANY(V_MENU.PATH)
|
||||
AND MENU_SUB.STATUS = 'active'
|
||||
)
|
||||
SELECT
|
||||
LEVEL AS LEV,
|
||||
|
|
@ -190,14 +226,18 @@ export class AdminService {
|
|||
WHERE 1 = 1
|
||||
ORDER BY PATH, SEQ
|
||||
`,
|
||||
[userLang]
|
||||
queryParams
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`메뉴 목록 조회 결과: ${menuList.length}개 (menuType: ${menuType || "전체"})`
|
||||
`관리자 메뉴 목록 조회 결과: ${menuList.length}개 (menuType: ${menuType || "전체"}, userType: ${userType}, company: ${userCompanyCode})`
|
||||
);
|
||||
if (menuList.length > 0) {
|
||||
logger.info("첫 번째 메뉴:", menuList[0]);
|
||||
logger.info("첫 번째 메뉴:", {
|
||||
objid: menuList[0].objid,
|
||||
name: menuList[0].menu_name_kor,
|
||||
companyCode: menuList[0].company_code,
|
||||
});
|
||||
}
|
||||
|
||||
return menuList;
|
||||
|
|
@ -208,15 +248,44 @@ export class AdminService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 사용자 메뉴 목록 조회
|
||||
* 사용자 메뉴 목록 조회 (회사별 필터링 적용)
|
||||
*/
|
||||
static async getUserMenuList(paramMap: any): Promise<any[]> {
|
||||
try {
|
||||
logger.info("AdminService.getUserMenuList 시작 - 파라미터:", paramMap);
|
||||
|
||||
const { userLang = "ko" } = paramMap;
|
||||
const { userId, userCompanyCode, userType, userLang = "ko" } = paramMap;
|
||||
|
||||
// 기존 Java의 selectUserMenuList 쿼리를 Raw Query로 포팅
|
||||
// SUPER_ADMIN (company_code='*'): 모든 메뉴
|
||||
// COMPANY_ADMIN: 자기 회사 메뉴 + 공통 메뉴 (company_code IS NULL)
|
||||
// 일반 사용자: 권한이 있는 메뉴만
|
||||
let companyFilter = "";
|
||||
let queryParams: any[] = [userLang];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
||||
// SUPER_ADMIN: 모든 메뉴 표시
|
||||
logger.info("✅ SUPER_ADMIN 모드: 모든 메뉴 표시");
|
||||
companyFilter = "";
|
||||
} else if (userType === "COMPANY_ADMIN") {
|
||||
// COMPANY_ADMIN: 자기 회사 메뉴 + 공통 메뉴
|
||||
companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE IS NULL)`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ COMPANY_ADMIN 모드: 회사 ${userCompanyCode} 메뉴 + 공통 메뉴 표시`
|
||||
);
|
||||
} else {
|
||||
// 일반 사용자: 자기 회사 메뉴만
|
||||
companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE IS NULL)`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
logger.info(
|
||||
`✅ 일반 사용자 모드: 회사 ${userCompanyCode} 메뉴 + 공통 메뉴 표시`
|
||||
);
|
||||
}
|
||||
|
||||
// 기존 Java의 selectUserMenuList 쿼리를 Raw Query로 포팅 + 회사별 필터링
|
||||
const menuList = await query<any>(
|
||||
`
|
||||
WITH RECURSIVE v_menu(
|
||||
|
|
@ -257,6 +326,8 @@ export class AdminService {
|
|||
FROM MENU_INFO MENU
|
||||
WHERE PARENT_OBJ_ID = 0
|
||||
AND MENU_TYPE = 1
|
||||
AND STATUS = 'active'
|
||||
${companyFilter}
|
||||
|
||||
UNION ALL
|
||||
|
||||
|
|
@ -279,7 +350,7 @@ export class AdminService {
|
|||
MENU_SUB.OBJID = ANY(PATH)
|
||||
FROM MENU_INFO MENU_SUB
|
||||
JOIN V_MENU ON MENU_SUB.PARENT_OBJ_ID = V_MENU.OBJID
|
||||
WHERE 1 = 1
|
||||
WHERE MENU_SUB.STATUS = 'active'
|
||||
)
|
||||
SELECT
|
||||
LEVEL AS LEV,
|
||||
|
|
@ -320,12 +391,18 @@ export class AdminService {
|
|||
WHERE 1 = 1
|
||||
ORDER BY PATH, SEQ
|
||||
`,
|
||||
[userLang]
|
||||
queryParams
|
||||
);
|
||||
|
||||
logger.info(`사용자 메뉴 목록 조회 결과: ${menuList.length}개`);
|
||||
logger.info(
|
||||
`사용자 메뉴 목록 조회 결과: ${menuList.length}개 (userType: ${userType}, company: ${userCompanyCode})`
|
||||
);
|
||||
if (menuList.length > 0) {
|
||||
logger.info("첫 번째 메뉴:", menuList[0]);
|
||||
logger.info("첫 번째 메뉴:", {
|
||||
objid: menuList[0].objid,
|
||||
name: menuList[0].menu_name_kor,
|
||||
companyCode: menuList[0].company_code,
|
||||
});
|
||||
}
|
||||
|
||||
return menuList;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,351 @@
|
|||
# 메뉴 회사별 필터링 개선 완료
|
||||
|
||||
## 📋 개요
|
||||
|
||||
**문제점**: SUPER_ADMIN이 좌측 사이드바에서 모든 회사의 메뉴를 보게 되어 혼란스러움
|
||||
|
||||
**해결책**:
|
||||
|
||||
- **좌측 사이드바**: 모든 사용자(SUPER_ADMIN 포함)가 **공통 메뉴만** 표시
|
||||
- **메뉴 관리 화면**: SUPER_ADMIN만 **전체 메뉴** 관리 가능
|
||||
|
||||
## 🎯 개선 내용
|
||||
|
||||
### 핵심 차이점
|
||||
|
||||
| 화면 유형 | SUPER_ADMIN | COMPANY_ADMIN | 일반 사용자 |
|
||||
| ------------------ | -------------- | ----------------- | -------------- |
|
||||
| **좌측 사이드바** | 공통 메뉴만 ✅ | 공통 메뉴만 ✅ | 공통 메뉴만 ✅ |
|
||||
| **메뉴 관리 화면** | 전체 메뉴 ✅ | 자기 회사 메뉴 ✅ | 접근 불가 ❌ |
|
||||
|
||||
### 1. 좌측 사이드바 (`menuType=0` 또는 `menuType=1`)
|
||||
|
||||
**모든 사용자**: 공통 메뉴만 표시 (`company_code IS NULL`)
|
||||
|
||||
```sql
|
||||
-- 모든 사용자 공통
|
||||
WHERE STATUS = 'active'
|
||||
AND MENU.COMPANY_CODE IS NULL -- 공통 메뉴만
|
||||
```
|
||||
|
||||
**이유**:
|
||||
|
||||
- SUPER_ADMIN도 일반 업무 시에는 공통 메뉴만 사용
|
||||
- 다른 회사 메뉴가 섞여 보이면 혼란스러움
|
||||
- 메뉴 관리는 별도 화면에서 수행
|
||||
|
||||
### 2. 메뉴 관리 화면 (`menuType=undefined`)
|
||||
|
||||
#### SUPER_ADMIN
|
||||
|
||||
- **모든 회사의 메뉴** 표시 및 관리 가능
|
||||
|
||||
```sql
|
||||
WHERE STATUS = 'active' -- 필터 없음
|
||||
```
|
||||
|
||||
#### COMPANY_ADMIN
|
||||
|
||||
- **자기 회사 메뉴 + 공통 메뉴** 표시 및 관리
|
||||
|
||||
```sql
|
||||
WHERE STATUS = 'active'
|
||||
AND (MENU.COMPANY_CODE = '사용자회사코드' OR MENU.COMPANY_CODE IS NULL)
|
||||
```
|
||||
|
||||
## 🔧 수정된 코드
|
||||
|
||||
### `adminService.ts` - getAdminMenuList()
|
||||
|
||||
```typescript
|
||||
// 좌측 사이드바 관리자 메뉴는 모든 사용자가 공통 메뉴만 표시
|
||||
// SUPER_ADMIN도 좌측 사이드바에서는 공통 메뉴만 보임
|
||||
// 메뉴 관리 화면(menuType 없음)에서만 전체 메뉴 조회
|
||||
if (menuType === undefined) {
|
||||
// 메뉴 관리 화면: SUPER_ADMIN은 전체, 나머지는 자기 회사만
|
||||
if (userType === "SUPER_ADMIN" && userCompanyCode === "*") {
|
||||
logger.info("✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시");
|
||||
companyFilter = "";
|
||||
} else {
|
||||
logger.info(
|
||||
`✅ 메뉴 관리 화면: 회사 ${userCompanyCode} 메뉴 + 공통 메뉴 표시`
|
||||
);
|
||||
companyFilter = `AND (MENU.COMPANY_CODE = $${paramIndex} OR MENU.COMPANY_CODE IS NULL)`;
|
||||
queryParams.push(userCompanyCode);
|
||||
paramIndex++;
|
||||
}
|
||||
} else {
|
||||
// 좌측 사이드바: 모든 사용자가 공통 메뉴만
|
||||
logger.info("✅ 좌측 사이드바 (관리자): 공통 메뉴만 표시");
|
||||
companyFilter = `AND MENU.COMPANY_CODE IS NULL`;
|
||||
}
|
||||
```
|
||||
|
||||
### `adminService.ts` - getUserMenuList()
|
||||
|
||||
```typescript
|
||||
// 좌측 사이드바 메뉴는 모든 사용자가 공통 메뉴만 표시
|
||||
// (SUPER_ADMIN도 좌측 사이드바에서는 공통 메뉴만 보임)
|
||||
// 메뉴 관리 화면에서는 별도로 전체 메뉴 조회
|
||||
|
||||
// 모든 사용자: 공통 메뉴만 (company_code IS NULL)
|
||||
companyFilter = `AND MENU.COMPANY_CODE IS NULL`;
|
||||
logger.info("✅ 좌측 사이드바: 공통 메뉴만 표시");
|
||||
```
|
||||
|
||||
## 🧪 테스트 시나리오
|
||||
|
||||
### 시나리오 1: SUPER_ADMIN 로그인
|
||||
|
||||
#### 좌측 사이드바
|
||||
|
||||
```bash
|
||||
# ✅ 공통 메뉴만 표시
|
||||
- 공통 대시보드
|
||||
- 공통 설정
|
||||
- 공통 보고서
|
||||
|
||||
# ❌ 표시되지 않음
|
||||
- ILSHIN 전용 메뉴
|
||||
- VEXPLOR 전용 메뉴
|
||||
```
|
||||
|
||||
#### 메뉴 관리 화면 (`/admin/menus`)
|
||||
|
||||
```bash
|
||||
# ✅ 모든 회사 메뉴 표시
|
||||
- ILSHIN 전용 메뉴 (company_code='ILSHIN')
|
||||
- VEXPLOR 전용 메뉴 (company_code='VEXPLOR')
|
||||
- 공통 메뉴 (company_code IS NULL)
|
||||
```
|
||||
|
||||
### 시나리오 2: COMPANY_ADMIN (ILSHIN) 로그인
|
||||
|
||||
#### 좌측 사이드바
|
||||
|
||||
```bash
|
||||
# ✅ 공통 메뉴만 표시
|
||||
- 공통 대시보드
|
||||
- 공통 설정
|
||||
- 공통 보고서
|
||||
|
||||
# ❌ 표시되지 않음
|
||||
- ILSHIN 전용 메뉴
|
||||
```
|
||||
|
||||
#### 메뉴 관리 화면 (`/admin/menus`)
|
||||
|
||||
```bash
|
||||
# ✅ ILSHIN 메뉴 + 공통 메뉴 표시
|
||||
- ILSHIN 전용 메뉴 (company_code='ILSHIN')
|
||||
- 공통 메뉴 (company_code IS NULL)
|
||||
|
||||
# ❌ 표시되지 않음
|
||||
- VEXPLOR 전용 메뉴
|
||||
```
|
||||
|
||||
### 시나리오 3: 일반 사용자 로그인
|
||||
|
||||
#### 좌측 사이드바
|
||||
|
||||
```bash
|
||||
# ✅ 공통 메뉴만 표시
|
||||
- 공통 대시보드
|
||||
- 공통 설정
|
||||
- 공통 보고서
|
||||
```
|
||||
|
||||
#### 메뉴 관리 화면
|
||||
|
||||
```bash
|
||||
# ❌ 접근 불가 (권한 없음)
|
||||
```
|
||||
|
||||
## 📝 로그 확인
|
||||
|
||||
### 좌측 사이드바 접근 시
|
||||
|
||||
```log
|
||||
# 관리자 메뉴 (menuType=0)
|
||||
✅ 좌측 사이드바 (관리자): 공통 메뉴만 표시
|
||||
|
||||
# 사용자 메뉴 (menuType=1)
|
||||
✅ 좌측 사이드바: 공통 메뉴만 표시
|
||||
```
|
||||
|
||||
### 메뉴 관리 화면 접근 시
|
||||
|
||||
```log
|
||||
# SUPER_ADMIN
|
||||
✅ 메뉴 관리 화면 (SUPER_ADMIN): 모든 메뉴 표시
|
||||
|
||||
# COMPANY_ADMIN
|
||||
✅ 메뉴 관리 화면: 회사 ILSHIN 메뉴 + 공통 메뉴 표시
|
||||
```
|
||||
|
||||
## 📊 API 응답 예시
|
||||
|
||||
### 좌측 사이드바 (모든 사용자 동일)
|
||||
|
||||
```javascript
|
||||
// GET /api/admin/menus?menuType=1
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"objid": "1",
|
||||
"menu_name_kor": "공통 대시보드",
|
||||
"company_code": null // ✅ 공통 메뉴만
|
||||
},
|
||||
{
|
||||
"objid": "2",
|
||||
"menu_name_kor": "공통 설정",
|
||||
"company_code": null // ✅ 공통 메뉴만
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 메뉴 관리 화면 (SUPER_ADMIN)
|
||||
|
||||
```javascript
|
||||
// GET /api/admin/menus (menuType 없음)
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "menu_name_kor": "ILSHIN 메뉴", "company_code": "ILSHIN" }, // ✅ 전체
|
||||
{ "menu_name_kor": "VEXPLOR 메뉴", "company_code": "VEXPLOR" }, // ✅ 전체
|
||||
{ "menu_name_kor": "공통 메뉴", "company_code": null } // ✅ 전체
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 메뉴 관리 화면 (COMPANY_ADMIN, ILSHIN)
|
||||
|
||||
```javascript
|
||||
// GET /api/admin/menus (menuType 없음)
|
||||
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "menu_name_kor": "ILSHIN 메뉴", "company_code": "ILSHIN" }, // ✅ 자기 회사
|
||||
{ "menu_name_kor": "공통 메뉴", "company_code": null } // ✅ 공통
|
||||
// ❌ VEXPLOR 메뉴 없음
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 사용자 경험 개선
|
||||
|
||||
### Before (문제점)
|
||||
|
||||
```
|
||||
SUPER_ADMIN 로그인
|
||||
└─ 좌측 사이드바
|
||||
├─ ILSHIN 대시보드 ← 혼란스러움
|
||||
├─ ILSHIN 보고서 ← 혼란스러움
|
||||
├─ VEXPLOR 대시보드 ← 혼란스러움
|
||||
├─ VEXPLOR 보고서 ← 혼란스러움
|
||||
└─ 공통 설정
|
||||
```
|
||||
|
||||
### After (개선)
|
||||
|
||||
```
|
||||
SUPER_ADMIN 로그인
|
||||
└─ 좌측 사이드바
|
||||
├─ 공통 대시보드 ← 깔끔함
|
||||
├─ 공통 설정 ← 깔끔함
|
||||
└─ 공통 보고서 ← 깔끔함
|
||||
|
||||
└─ 메뉴 관리 화면 (/admin/menus)
|
||||
├─ ILSHIN 대시보드 ← 관리 목적
|
||||
├─ ILSHIN 보고서 ← 관리 목적
|
||||
├─ VEXPLOR 대시보드 ← 관리 목적
|
||||
├─ VEXPLOR 보고서 ← 관리 목적
|
||||
└─ 공통 설정
|
||||
```
|
||||
|
||||
## ✅ 완료 체크리스트
|
||||
|
||||
- [x] getAdminMenuList() 수정 (menuType 조건 분기)
|
||||
- [x] getUserMenuList() 수정 (공통 메뉴만 필터링)
|
||||
- [x] 로그 메시지 개선
|
||||
- [x] 린트 에러 해결
|
||||
- [x] 문서 작성
|
||||
- [ ] 실제 테스트
|
||||
- [ ] SUPER_ADMIN - 좌측 사이드바
|
||||
- [ ] SUPER_ADMIN - 메뉴 관리 화면
|
||||
- [ ] COMPANY_ADMIN - 좌측 사이드바
|
||||
- [ ] COMPANY_ADMIN - 메뉴 관리 화면
|
||||
|
||||
## 🚨 주의사항
|
||||
|
||||
### 공통 메뉴 (`company_code IS NULL`)
|
||||
|
||||
- 모든 회사에서 공통으로 사용하는 메뉴
|
||||
- 예: 대시보드, 설정, 공지사항 등
|
||||
- **반드시 `company_code`를 `NULL`로 설정**해야 좌측 사이드바에 표시됨
|
||||
|
||||
### 회사 전용 메뉴
|
||||
|
||||
- 특정 회사에서만 사용하는 메뉴
|
||||
- `company_code`에 회사 코드 지정 (예: `ILSHIN`, `VEXPLOR`)
|
||||
- 좌측 사이드바에는 표시되지 않음
|
||||
- 메뉴 관리 화면에서만 표시됨
|
||||
|
||||
## 🔮 향후 개선 사항
|
||||
|
||||
### 1. 회사 전환 기능
|
||||
|
||||
SUPER_ADMIN이 특정 회사 컨텍스트로 전환하여 해당 회사 메뉴를 볼 수 있는 기능:
|
||||
|
||||
```tsx
|
||||
// 헤더에 회사 선택 드롭다운
|
||||
<Select value={currentCompany} onChange={switchCompany}>
|
||||
<option value="*">전체 (관리자 모드)</option>
|
||||
<option value="ILSHIN">ILSHIN</option>
|
||||
<option value="VEXPLOR">VEXPLOR</option>
|
||||
</Select>
|
||||
```
|
||||
|
||||
### 2. 메뉴 타입 추가
|
||||
|
||||
- 공통 메뉴 (company_code IS NULL)
|
||||
- 회사 전용 메뉴 (company_code 지정)
|
||||
- **부서 전용 메뉴** (향후 추가)
|
||||
|
||||
### 3. 동적 메뉴 표시
|
||||
|
||||
사용자 권한에 따라 메뉴 항목의 표시 여부 결정:
|
||||
|
||||
```sql
|
||||
-- 권한 기반 메뉴 필터링 (향후 개선)
|
||||
SELECT * FROM get_user_menus_with_permissions('user_id', 'company_code');
|
||||
```
|
||||
|
||||
## 🎉 결론
|
||||
|
||||
### 개선 효과
|
||||
|
||||
1. **사용자 경험 개선**
|
||||
|
||||
- SUPER_ADMIN도 좌측 사이드바에서 깔끔한 UI 제공
|
||||
- 다른 회사 메뉴가 섞이지 않아 혼란 방지
|
||||
|
||||
2. **권한 분리**
|
||||
|
||||
- 일반 업무: 공통 메뉴만 사용
|
||||
- 관리 업무: 메뉴 관리 화면에서 전체 메뉴 관리
|
||||
|
||||
3. **확장성**
|
||||
- 향후 회사별 메뉴 추가 시에도 좌측 사이드바는 깔끔하게 유지
|
||||
- 메뉴 관리 화면에서만 복잡한 메뉴 구조 관리
|
||||
|
||||
---
|
||||
|
||||
**문서 작성일**: 2025-01-XX
|
||||
**작성자**: AI Assistant
|
||||
**버전**: 2.0 (개선판)
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
# 메뉴 회사별 필터링 구현 완료
|
||||
|
||||
## 📋 개요
|
||||
|
||||
로그인한 사용자의 **회사 코드**에 따라 좌측 사이드바 메뉴가 필터링되어 표시되도록 구현했습니다.
|
||||
|
||||
## 🎯 구현 내용
|
||||
|
||||
### 1. 백엔드 수정
|
||||
|
||||
#### `adminController.ts` 수정
|
||||
|
||||
- **getAdminMenus()**: 관리자 메뉴 조회 시 사용자 정보 전달
|
||||
- **getUserMenus()**: 사용자 메뉴 조회 시 사용자 정보 전달
|
||||
|
||||
```typescript
|
||||
// 추가된 파라미터
|
||||
const userId = req.user?.userId;
|
||||
const userCompanyCode = req.user?.companyCode || "ILSHIN";
|
||||
const userType = req.user?.userType;
|
||||
```
|
||||
|
||||
#### `adminService.ts` 수정
|
||||
|
||||
- **getAdminMenuList()**: 회사별 필터링 로직 추가
|
||||
- **getUserMenuList()**: 회사별 필터링 로직 추가
|
||||
|
||||
### 2. 회사별 필터링 규칙
|
||||
|
||||
#### SUPER_ADMIN (`userType='SUPER_ADMIN'`, `companyCode='*'`)
|
||||
|
||||
- **모든 회사의 메뉴** 표시
|
||||
- 제한 없음
|
||||
|
||||
```sql
|
||||
-- 필터 없음
|
||||
WHERE STATUS = 'active'
|
||||
```
|
||||
|
||||
#### COMPANY_ADMIN (`userType='COMPANY_ADMIN'`)
|
||||
|
||||
- **자기 회사 메뉴 + 공통 메뉴** 표시
|
||||
- `company_code = 사용자회사코드` OR `company_code IS NULL`
|
||||
|
||||
```sql
|
||||
-- 회사 필터 적용
|
||||
WHERE STATUS = 'active'
|
||||
AND (MENU.COMPANY_CODE = '사용자회사코드' OR MENU.COMPANY_CODE IS NULL)
|
||||
```
|
||||
|
||||
#### 일반 사용자 (`userType='USER'`)
|
||||
|
||||
- **자기 회사 메뉴 + 공통 메뉴** 표시
|
||||
- COMPANY_ADMIN과 동일한 필터링
|
||||
|
||||
```sql
|
||||
-- 회사 필터 적용
|
||||
WHERE STATUS = 'active'
|
||||
AND (MENU.COMPANY_CODE = '사용자회사코드' OR MENU.COMPANY_CODE IS NULL)
|
||||
```
|
||||
|
||||
## 📊 데이터베이스 구조
|
||||
|
||||
### menu_info 테이블
|
||||
|
||||
```sql
|
||||
CREATE TABLE menu_info (
|
||||
objid NUMERIC PRIMARY KEY,
|
||||
menu_name_kor VARCHAR(100),
|
||||
menu_url VARCHAR(200),
|
||||
menu_type NUMERIC, -- 0: 관리자 메뉴, 1: 사용자 메뉴
|
||||
company_code VARCHAR(50), -- 회사 코드 (NULL: 공통 메뉴)
|
||||
parent_obj_id NUMERIC,
|
||||
seq NUMERIC,
|
||||
status VARCHAR(20), -- 'active', 'inactive'
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
### 데이터베이스 함수 활용
|
||||
|
||||
마이그레이션 `031_add_menu_auth_columns.sql`에서 생성한 함수들:
|
||||
|
||||
- `get_user_menus_with_permissions()`: 사용자별 메뉴 권한 조회
|
||||
- `check_menu_crud_permission()`: 메뉴별 CRUD 권한 확인
|
||||
|
||||
## 🧪 테스트 시나리오
|
||||
|
||||
### 테스트 1: SUPER_ADMIN 로그인
|
||||
|
||||
**기대 결과**: 모든 회사의 메뉴가 표시됨
|
||||
|
||||
```bash
|
||||
# 로그인 사용자: admin (SUPER_ADMIN, company_code='*')
|
||||
# 예상 메뉴:
|
||||
# - ILSHIN 회사 메뉴
|
||||
# - VEXPLOR 회사 메뉴
|
||||
# - 공통 메뉴 (company_code IS NULL)
|
||||
```
|
||||
|
||||
### 테스트 2: COMPANY_ADMIN 로그인 (ILSHIN)
|
||||
|
||||
**기대 결과**: ILSHIN 회사 메뉴 + 공통 메뉴만 표시
|
||||
|
||||
```bash
|
||||
# 로그인 사용자: ilshin_admin (COMPANY_ADMIN, company_code='ILSHIN')
|
||||
# 예상 메뉴:
|
||||
# - ILSHIN 회사 메뉴
|
||||
# - 공통 메뉴 (company_code IS NULL)
|
||||
# ❌ VEXPLOR 회사 메뉴 (표시 안 됨)
|
||||
```
|
||||
|
||||
### 테스트 3: COMPANY_ADMIN 로그인 (VEXPLOR)
|
||||
|
||||
**기대 결과**: VEXPLOR 회사 메뉴 + 공통 메뉴만 표시
|
||||
|
||||
```bash
|
||||
# 로그인 사용자: vexplor_admin (COMPANY_ADMIN, company_code='VEXPLOR')
|
||||
# 예상 메뉴:
|
||||
# - VEXPLOR 회사 메뉴
|
||||
# - 공통 메뉴 (company_code IS NULL)
|
||||
# ❌ ILSHIN 회사 메뉴 (표시 안 됨)
|
||||
```
|
||||
|
||||
### 테스트 4: 일반 사용자 로그인
|
||||
|
||||
**기대 결과**: 자기 회사 메뉴 + 공통 메뉴만 표시
|
||||
|
||||
```bash
|
||||
# 로그인 사용자: user1 (USER, company_code='ILSHIN')
|
||||
# 예상 메뉴:
|
||||
# - ILSHIN 회사 메뉴 (권한이 있는 메뉴만)
|
||||
# - 공통 메뉴 (company_code IS NULL)
|
||||
```
|
||||
|
||||
## 📝 로그 확인
|
||||
|
||||
백엔드 로그에서 다음과 같은 메시지를 확인할 수 있습니다:
|
||||
|
||||
```log
|
||||
# SUPER_ADMIN
|
||||
✅ SUPER_ADMIN 모드: 모든 메뉴 표시
|
||||
|
||||
# COMPANY_ADMIN
|
||||
✅ COMPANY_ADMIN 모드: 회사 ILSHIN 메뉴 + 공통 메뉴 표시
|
||||
|
||||
# 일반 사용자
|
||||
✅ 일반 사용자 모드: 회사 ILSHIN 메뉴 + 공통 메뉴 표시
|
||||
```
|
||||
|
||||
## 🔧 수정된 파일
|
||||
|
||||
### 백엔드
|
||||
|
||||
1. `/backend-node/src/controllers/adminController.ts`
|
||||
|
||||
- `getAdminMenus()`: 회사별 필터링 파라미터 전달
|
||||
- `getUserMenus()`: 회사별 필터링 파라미터 전달
|
||||
|
||||
2. `/backend-node/src/services/adminService.ts`
|
||||
- `getAdminMenuList()`: 회사별 필터링 쿼리 적용
|
||||
- `getUserMenuList()`: 회사별 필터링 쿼리 적용
|
||||
|
||||
### 프론트엔드
|
||||
|
||||
- 변경 없음 (기존 API 호출 방식 유지)
|
||||
|
||||
## 🎨 UI 동작
|
||||
|
||||
### 좌측 사이드바 메뉴
|
||||
|
||||
```tsx
|
||||
// frontend/components/layout/AppLayout.tsx
|
||||
// useMenu 훅에서 자동으로 회사별 필터링된 메뉴 가져옴
|
||||
|
||||
const { userMenus, adminMenus, loading, refreshMenus } = useMenu();
|
||||
|
||||
// 사용자 모드
|
||||
const currentMenus = isAdminMode ? adminMenus : userMenus;
|
||||
```
|
||||
|
||||
## ✅ 검증 방법
|
||||
|
||||
### 1. 백엔드 로그 확인
|
||||
|
||||
```bash
|
||||
# Docker 로그 확인
|
||||
docker-compose -f docker/dev/docker-compose.yml logs -f app
|
||||
|
||||
# 또는 터미널에서 백엔드 직접 실행
|
||||
cd backend-node
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 2. 브라우저 개발자 도구
|
||||
|
||||
```javascript
|
||||
// 네트워크 탭에서 API 응답 확인
|
||||
GET /api/admin/menus?menuType=1
|
||||
|
||||
// 응답 예시 (COMPANY_ADMIN, ILSHIN)
|
||||
{
|
||||
"success": true,
|
||||
"message": "사용자 메뉴 목록 조회 성공",
|
||||
"data": [
|
||||
{
|
||||
"objid": "1",
|
||||
"menu_name_kor": "대시보드",
|
||||
"company_code": "ILSHIN" // ✅ ILSHIN 메뉴
|
||||
},
|
||||
{
|
||||
"objid": "2",
|
||||
"menu_name_kor": "공통 설정",
|
||||
"company_code": null // ✅ 공통 메뉴
|
||||
}
|
||||
// ❌ VEXPLOR 메뉴는 없음
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 데이터베이스 직접 확인
|
||||
|
||||
```sql
|
||||
-- 메뉴 데이터 확인
|
||||
SELECT
|
||||
objid,
|
||||
menu_name_kor,
|
||||
company_code,
|
||||
status
|
||||
FROM menu_info
|
||||
WHERE menu_type = 1
|
||||
AND status = 'active'
|
||||
ORDER BY seq;
|
||||
|
||||
-- 회사별 메뉴 카운트
|
||||
SELECT
|
||||
COALESCE(company_code, '공통') AS company,
|
||||
COUNT(*) AS menu_count
|
||||
FROM menu_info
|
||||
WHERE status = 'active'
|
||||
GROUP BY company_code
|
||||
ORDER BY company;
|
||||
```
|
||||
|
||||
## 🚨 주의사항
|
||||
|
||||
### 1. 공통 메뉴 (`company_code IS NULL`)
|
||||
|
||||
- 모든 회사에서 공통으로 사용하는 메뉴
|
||||
- 회사 코드가 NULL인 메뉴는 모든 사용자에게 표시됨
|
||||
|
||||
### 2. 비활성 메뉴 (`status='inactive'`)
|
||||
|
||||
- 회사 코드와 관계없이 표시되지 않음
|
||||
- 필터링 전에 `status='active'` 조건으로 먼저 걸러짐
|
||||
|
||||
### 3. 권한 체크
|
||||
|
||||
- 현재는 메뉴 목록 표시만 필터링
|
||||
- 실제 메뉴 접근 권한은 `rel_menu_auth` 테이블 기반으로 별도 체크 필요
|
||||
- 향후 `get_user_menus_with_permissions()` 함수 활용 가능
|
||||
|
||||
## 🔮 향후 개선 사항
|
||||
|
||||
### 1. 권한 기반 메뉴 필터링
|
||||
|
||||
현재는 회사 코드만 체크하지만, 향후 사용자 권한 그룹 기반 필터링 추가:
|
||||
|
||||
```sql
|
||||
-- 031_add_menu_auth_columns.sql의 함수 활용
|
||||
SELECT * FROM get_user_menus_with_permissions('user_id', 'company_code');
|
||||
```
|
||||
|
||||
### 2. 캐싱 전략
|
||||
|
||||
- 메뉴 데이터는 자주 변경되지 않으므로 Redis 캐싱 고려
|
||||
- 회사별로 캐시 키 분리: `menus:company:{companyCode}`
|
||||
|
||||
### 3. 다국어 메뉴명
|
||||
|
||||
- 현재는 `menu_name_kor` 기본 사용
|
||||
- `MULTI_LANG_TEXT` 테이블 기반 다국어 지원 이미 구현됨
|
||||
|
||||
## 📚 참고 자료
|
||||
|
||||
- 데이터베이스 마이그레이션: `/db/migrations/031_add_menu_auth_columns.sql`
|
||||
- 권한 서비스: `/backend-node/src/services/roleService.ts`
|
||||
- 메뉴 관리 컴포넌트: `/frontend/components/admin/MenuManagement.tsx`
|
||||
|
||||
## ✨ 완료 체크리스트
|
||||
|
||||
- [x] 백엔드 컨트롤러 수정 (`adminController.ts`)
|
||||
- [x] 백엔드 서비스 수정 (`adminService.ts`)
|
||||
- [x] 회사별 필터링 로직 구현
|
||||
- [x] SUPER_ADMIN 예외 처리
|
||||
- [x] 공통 메뉴 필터링 (company_code IS NULL)
|
||||
- [x] 비활성 메뉴 제외 (status='active')
|
||||
- [x] 로그 메시지 추가
|
||||
- [x] 린트 에러 해결
|
||||
- [ ] 실제 테스트 (각 사용자 유형별)
|
||||
- [ ] 권한 기반 메뉴 필터링 (향후 개선)
|
||||
|
||||
## 🎉 결론
|
||||
|
||||
로그인한 사용자의 **회사 코드**에 따라 좌측 사이드바 메뉴가 자동으로 필터링되어 표시됩니다.
|
||||
|
||||
- **SUPER_ADMIN**: 모든 메뉴
|
||||
- **COMPANY_ADMIN**: 자기 회사 + 공통 메뉴
|
||||
- **일반 사용자**: 자기 회사 + 공통 메뉴
|
||||
|
||||
이제 다른 회사의 사용자가 로그인하면 자기 회사에 해당하는 메뉴만 보게 됩니다! 🚀
|
||||
Loading…
Reference in New Issue