# 메뉴 관리 개발 가이드 포탈 GNB/마이페이지 메뉴는 `menu.yml` → DB(PTL_MENU_*) → 캐시 → 템플릿 렌더 구조로 동작하며, 노출/배치 관리는 eapim-admin **포탈메뉴관리**(파트너포탈 > 포탈관리 > 메뉴 관리)에서 수행한다. ## 구성 요소 | 구성 | 위치 | 역할 | |---|---|---| | `menu.yml` | `src/main/resources/menu.yml` | 기본 메뉴 정의 (id/노출명/path/권한/기본 배치) | | `roles.yml` | `src/main/resources/roles.yml` | 역할 정의 (`portal.portal_security` 이동분) | | 엔티티/공유 서비스 | `elink-portal-common` `com.eactive.apim.portal.menu.*` | PTL_MENU_ITEM·PTL_MENU_PLACEMENT·PTL_ROLE(+AUTHORITY), `PortalMenuDataService` | | 시더 | `djb/menu/MenuSeeder.java` | 부팅 시 yml→DB 적재 (ApplicationReadyEvent) | | 캐시 | `djb/menu/MenuService.java` | role 비의존 트리 스냅샷, TTL 1시간(PTL_PROPERTY) | | 렌더 | `djb/menu/MenuModelAdvice.java` → 모델 `menuView` | 요청별 노출(EXPOSE_ROLES) 필터 | | 접근 제어 | `djb/menu/MenuAccessInterceptor.java` | ACCESS_ROLES 서버측 집행 (경로 정확 일치) | | 내부 API | `djb/menu/MenuInternalController.java` | `POST /internal/menu/reload` (admin 캐시 리로드 수신) | 메뉴를 소비하는 템플릿: `fragment/djbank/header_container.html`(데스크톱 nav·마이페이지 드롭다운·모바일 drawer), `fragment/djbank/service_sidebar.html`. 모두 `${menuView}` 를 반복 렌더하므로 **메뉴 추가 시 템플릿 수정 불필요**. ## menu.yml 스키마 ```yaml portal-menu: items: - id: support # kebab-case 필수 (^[a-z0-9-]+$). 변경 금지(변경=신규 항목) name: "고객지원" group: true # 상위 그룹. path 생략 시 클릭 없음(자식 있어야 노출) section: GNB # GNB(기본) | MYPAGE. 자식은 부모 섹션 상속 expose-roles: [] # 생략=전체(익명 포함), AUTHENTICATED=로그인자, 그 외 역할코드 any-of children: - { id: support-faq, name: "FAQ", path: /faq_list } - { id: my-page-webhook, name: "Webhook 관리", path: /webhook, icon: fa-bell, expose-roles: [ROLE_WEBHOOK], access-roles: [ROLE_WEBHOOK] } ``` - `expose-roles` = 메뉴 **노출** 조건, `access-roles` = URL **접근** 조건(인터셉터 차단, redirect). - `icon` 은 마이페이지 드롭다운 전용(FontAwesome 클래스). - 정렬은 yml 나열 순서(기본 배치 sort = index×10). ## 시딩 규칙 (MenuSeeder) 1. **항목**: id 기준 upsert. yml 값이 바뀌면 DFLT_*(기본값 스냅샷)를 갱신하고, **관리자가 수정하지 않은 필드(현재값==구 기본값)만** 새 기본값을 따라간다. 구조 필드(`group`/`section`/`icon`/`new-window`)는 항상 yml 이 이긴다. 2. **배치**: `PTL_MENU_PLACEMENT` 가 **비어있을 때만** 기본 배치로 최초 시딩. 이후 배치는 admin 이 소유한다 — 재배포/재기동에도 보존됨. 3. yml 에서 항목을 제거해도 DB 는 삭제하지 않고 경고 로그만 남긴다(수동 정리). 4. 부팅 시딩 주체는 인증 사용자가 없으므로 `CREATED_BY=SYSTEM`. ## 캐시와 리로드 - 스냅샷 TTL: PTL_PROPERTY `Portal / menu.cache.ttl-seconds` (기본 3600초). - 즉시 반영: `curl -X POST http://127.0.0.1:39130/internal/menu/reload` (admin 포탈메뉴관리의 [캐시 Reload] 버튼이 동일 호출 수행). - 내부 API 가드: `Portal / menu.internal.allow-ips` 허용 IP 목록(기본 loopback) + X-Forwarded-For 동반 요청 거부. CSRF 면제(`/internal/menu/**`). - admin 측 호출 URL: `Portal / portal.internal.menu-reload-url`. ## 새 메뉴 추가 절차 **기본 메뉴(코드 배포와 함께)** 1. 페이지/라우트 준비 (`portal.pages` 또는 `@GetMapping` — 기존 방식 그대로) 2. `menu.yml` 에 항목 추가 (필요 시 breadcrumb 용 `page.home` 트리도 갱신 — 별도 체계 유지) 3. 재기동 → 시딩 로그 확인 → 헤더/드로어 노출 확인 4. 이미 운영 중인 DB 라면 배치는 자동 추가되지 않음(배치 시딩은 최초 1회) — admin 화면에서 미배치 → 원하는 위치로 드래그 후 저장 **운영자 임시 메뉴(외부 링크 등)**: admin 포탈메뉴관리 [메뉴 추가] → 미배치 생성 → 드래그 배치 → 저장 → 캐시 Reload. 커스텀 항목은 미배치 시 삭제된다. ## 로컬 개발 주의 - `gradle bootRun` 으로 시딩까지 확인하려면 damo-manager 가 classpath 에 필요: `JAVA_TOOL_OPTIONS="-Xbootclasspath/a:<...>/apache-tomcat-9.0.115-djb/lib/damo-manager.jar"` (미지정 시 감사 컬럼 암호화 컨버터에서 NoClassDefFoundError). - 템플릿/메뉴 반영 확인은 서버 재시작 후 curl 로. - elink-portal-common 수정 후 Q클래스 duplicate 컴파일 오류 시 각 모듈 `build/generated` 삭제 후 재컴파일. ## 역할(roles.yml) 변경 - 로그인 권한 확장은 `PortalRolesProperties`(yml 바인딩)를 직접 사용 — DB 미러(PTL_ROLE*)는 admin 권한 선택 체크박스 소스 전용. - 역할 추가 시 `roles.yml` 의 `authority-names` 에 한글 라벨을 함께 등록해야 admin 화면에 표기된다.