# 메뉴 기반 권한 시스템 가이드 (동적 화면 대응) ## 개요 **기존 메뉴 기반 권한 시스템을 유지**하면서 **동적으로 생성되는 화면에도 대응**하는 개선된 시스템입니다. ### 핵심 아이디어 💡 ``` 사용자가 화면 생성 ↓ 자동으로 메뉴 추가 (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 ( ); } ``` ### 버튼 권한 제어 ```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 (

계약 상세

{canUpdate && } {canDelete && }
); } ``` --- ## 권한 관리 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 psql -U postgres -d ilshin < db/migrations/028_add_company_code_to_authority_master.sql # 2. 메뉴 기반 권한 시스템 개선 docker exec -i psql -U postgres -d ilshin < db/migrations/030_improve_menu_auth_system.sql # 검증 docker exec -it psql -U postgres -d ilshin -c "SELECT * FROM menu_info WHERE menu_type = 'dynamic';" docker exec -it 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`