플랜별 메뉴 접근 제한 설계
개요
요금제 시스템 구현 설계의 일환으로, 가입 플랜에 따라 특정 메뉴 접근을 제한하는 메커니즘을 정의한다. 전체 이용요금 및 과금 정책과 연계되며, 플랜 코드(PLAN_FREE, PLAN_BASIC, PLAN_STD, PLAN_SPC)를 기준으로 동작한다. 설계 원문
권위 소스: Menu.visible_tiers
기존 LicenseMenuMapping 별도 테이블 대신 Menu.visible_tiers JSONB 필드를 단일 권위 소스로 사용한다. 설계 원문
| 방식 | 설명 |
|---|---|
기존 (LicenseMenuMapping) | 별도 테이블 JOIN 필요, 동기화 이슈 발생 가능 |
신규 (Menu.visible_tiers) | 메뉴와 허용 플랜 정보가 한 곳에 집중 → 동기화 이슈 없음, JOIN 불필요 |
기본값: ["PLAN_FREE", "PLAN_BASIC", "PLAN_STD", "PLAN_SPC"]
플랜별 메뉴 허용 규칙
| 메뉴 그룹 | PLAN_FREE | PLAN_BASIC | PLAN_STD | PLAN_SPC |
|---|---|---|---|---|
| 급여관리 (4개) | ✔ | ✔ | ✔ | ✔ |
| 이러닝센터 (5개) | ✘ | ✘ | ✔ | ✔ |
| 전자근로계약서 | ✘ | ✘ | ✔ | ✔ |
| 나머지 전체 | ✔ | ✔ | ✔ | ✔ |
- 이러닝/전자근로계약서 메뉴의
visible_tiers→["PLAN_STD", "PLAN_SPC"] - 그 외 전체 메뉴 →
["PLAN_FREE", "PLAN_BASIC", "PLAN_STD", "PLAN_SPC"]
이용요금(과금) 정책 스펙과 함께 참고할 것. 설계 원문
미들웨어 연동 구조
메모리 캐시 전략
매 요청마다 DB를 조회하는 오버헤드를 피하기 위해 앱 시작 시 Menu.visible_tiers 데이터를 메모리에 로드한다.
# {menu_url: set(plan_codes)} — 해당 메뉴에 접근 가능한 플랜 목록
_MENU_PLAN_CACHE: dict[str, set[str]] = {}| 시점 | 동작 |
|---|---|
| 앱 시작 | Menu 테이블 전체 조회 → 캐시 1회 로드 |
| 라이센스 메뉴 수정 시 | 캐시 즉시 갱신 |
| 일반 API 요청 | DB 조회 없이 딕셔너리 lookup |
라이센스 메뉴 관리 페이지 전환
기존 license-menus/page.tsx는 LicenseMenuMapping 테이블을 참조하고 있으나, Menu.visible_tiers를 직접 읽고 쓰도록 전환한다.
| 작업 | 변경 내용 |
|---|---|
| GET | Menu 테이블에서 visible_tiers 조회 |
| PUT | Menu.visible_tiers 업데이트 + 메모리 캐시 갱신 |
| UI | 플랜 열을 4종(FREE/BASIC/STD/SPC)으로 표시 — PLAN_PRM 제거 |
프론트엔드 반영
로그인 응답의 menu_keys에 플랜 제한을 반영하여 사이드바가 자동으로 비허용 메뉴를 숨기도록 한다.
resolve_menu_keys()함수 내에 플랜 기반 필터링 로직 추가- 기존 사이드바
menu_keys기반 렌더링 로직 그대로 활용 (추가 UI 변경 최소화)
마이그레이션 연동
마이그레이션(billing_plan_migration.py) 실행 시 Menu.visible_tiers 일괄 업데이트가 포함된다.
- 이러닝/전자근로계약서 메뉴 →
["PLAN_STD", "PLAN_SPC"] - 그 외 전체 메뉴 →
["PLAN_FREE", "PLAN_BASIC", "PLAN_STD", "PLAN_SPC"]
마이그레이션 전략 전체는 요금제 시스템 구현 설계 참고. 설계 원문
관련 스펙
- 근로계약서 스펙, 근로계약서 스펙 — 전자근로계약서 메뉴:
PLAN_STD이상만 접근 가능 - 관리자 등급 및 권한 관리 스펙 — 역할 기반 체크(1단계)와 플랜 기반 체크(2단계) 병렬 동작
- 이용요금 및 과금 정책 — 플랜 코드 정의 원천
- 인증 만료 시 빈 페이지 노출 이슈 — 미들웨어 레이어 관련 유의사항
백엔드 메뉴 게이팅 버그 수정 (2026-08-03)
menu_allowed가 /admin(대시보드)을 모든 /admin/* 경로의 상위경로로 처리하여, 대시보드 권한만 있는 관리자도 API 직접 호출로 전체 admin API에 접근 가능하던 보안 버그를 수정했다. [fix-task 0803 — 수정완료 항목]
/admin단독 키는 정확 일치만 허용(k != "/admin"가드 추가), 다른 키의 하위 상속(/admin/payroll → /base등)은 유지._MENU_PREFIXES에/admin/billing추가 → 정산 API도 게이팅 대상에 포함(menu_enforcement.py).
회귀 검증 결과:
| 케이스 | 결과 |
|---|---|
| 대시보드-only 역할 → 전체 admin API | 403 |
| 하위 상속 경로 | 200 유지 |
is_system / ["*"] 역할 | 전부 허용 |
| 정산 API — 최고관리자 | 200 |
| 정산 API — 미부여(조직관리자 등) | 403 |
커스텀 "전메뉴" 역할은 concrete 리스트라 billing 미포함 → 역할관리에서 별도 부여 필요(설계상 정상). [fix-task 0803 — 수정완료 항목]정산 메뉴 최고관리자 전용 노출
/admin/billing을 관리자 메뉴 카탈로그(admin_menu.py)에 추가했다. 최고관리자는 기본 노출되며, 역할관리에서 타 관리자에게 부여·회수 가능. 레거시 ["*"] 관리자는 여전히 노출(커스텀 역할 지정으로 제한 가능). [fix-task 0803 — 수정완료 항목]
대시보드 역할별 위젯 게이팅
게이팅 적용 후 권한 없는 도메인 위젯이 403 오류와 함께 노출되던 문제를 대응했다. 대시보드 전 위젯 및 HERO 근태 viz를 menu_keys로 게이팅(권한 없으면 fetch·렌더 안 함, 숨김 방식·사이드바와 일관). HERO는 헤드카운트·제목은 유지. 프론트 전용(admin/page.tsx). [fix-task 0803 — 수정완료 항목]
후속(리디자인): role-adaptive HERO — 역할별 다른 핵심 지표 표시.