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