12 KiB
12 KiB
메뉴 기반 권한 시스템 가이드 (동적 화면 대응)
개요
기존 메뉴 기반 권한 시스템을 유지하면서 동적으로 생성되는 화면에도 대응하는 개선된 시스템입니다.
핵심 아이디어 💡
사용자가 화면 생성
↓
자동으로 메뉴 추가 (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. 화면 생성 시 자동 메뉴 추가
-- 사용자가 화면 생성
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. 화면 삭제 시 자동 메뉴 비활성화
-- 화면 삭제
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: 영업팀에게 계약 관리 화면 읽기 권한 부여
-- 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: 개발팀에게 플로우 관리 전체 권한 부여
-- 플로우 관리 메뉴에 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: 권한 확인
-- '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
// 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 };
}
사용자 메뉴 렌더링
// 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 (
<nav>
{menus.map((menu) => (
<NavItem key={menu.menuObjid} menu={menu} />
))}
</nav>
);
}
버튼 권한 제어
// 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 (
<div>
<h1>계약 상세</h1>
{canUpdate && <Button>수정</Button>}
{canDelete && <Button variant="destructive">삭제</Button>}
</div>
);
}
권한 관리 UI 설계
권한 그룹 상세 페이지에서 메뉴 권한 설정
// 체크박스 그리드 형태
┌─────────────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ 메뉴 │ 생성 │ 읽기 │ 수정 │ 삭제 │ 실행 │ 내보내기│
├─────────────────┼────────┼────────┼────────┼────────┼────────┼────────┤
│ 대시보드 │ ☐ │ ☑ │ ☐ │ ☐ │ ☐ │ ☐ │
│ 계약 관리 │ ☑ │ ☑ │ ☑ │ ☐ │ ☐ │ ☑ │
│ 사용자 관리 │ ☐ │ ☑ │ ☐ │ ☐ │ ☐ │ ☐ │
│ 플로우 관리 │ ☐ │ ☑ │ ☐ │ ☐ │ ☑ │ ☐ │
└─────────────────┴────────┴────────┴────────┴────────┴────────┴────────┘
실전 시나리오
시나리오: 사용자가 "배송 현황" 화면 생성 → 권한 설정
-- 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로 회사별 메뉴 분리- 슈퍼관리자는 모든 회사 메뉴 관리
마이그레이션 실행
# 1. 권한 그룹 시스템 개선
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d ilshin < db/migrations/028_add_company_code_to_authority_master.sql
# 2. 메뉴 기반 권한 시스템 개선
docker exec -i <DB_CONTAINER_NAME> psql -U postgres -d ilshin < db/migrations/030_improve_menu_auth_system.sql
# 검증
docker exec -it <DB_CONTAINER_NAME> psql -U postgres -d ilshin -c "SELECT * FROM menu_info WHERE menu_type = 'dynamic';"
docker exec -it <DB_CONTAINER_NAME> 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를 그대로 사용하며, 새로운 컬럼만 추가됩니다.
다음 단계
- ✅ 마이그레이션 실행 (028, 030)
- 🔄 백엔드 API 구현 (권한 체크 미들웨어)
- 🔄 프론트엔드 UI 개발 (메뉴 권한 설정 그리드)
- 🔄 테스트 (영업팀 시나리오)
관련 파일
- 마이그레이션:
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