ERP-node/docs/메뉴_기반_권한_시스템_가이드.md

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_codeauthority_master.company_code로 회사별 격리됩니다.

Q4: 기존 메뉴 시스템과 호환되나요?

A: 완전히 호환됩니다. 기존 menu_inforel_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