메뉴 기능 추가 - menu.yml GNB 정의 - 접근 인터셉터/뷰 주입/내부 API 구현
eapim-portal CI (from elink-portal-common) / build (push) Has been cancelled

This commit is contained in:
Rinjae
2026-08-05 14:19:33 +09:00
parent 6a32608a8d
commit 3e4ced9b3d
10 changed files with 431 additions and 0 deletions
@@ -0,0 +1,113 @@
package com.eactive.apim.portal.menu.entity;
import com.eactive.apim.portal.common.entity.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* 포탈 메뉴 항목 마스터.
*
* <p>PK 는 menu.yml 의 kebab-case 자연키(예: {@code service-intro}).
* PORTAL(기본) 항목은 eapim-portal 부팅 시 menu.yml 로 upsert 되고,
* ADMIN(커스텀) 항목은 eapim-admin 에서 등록/삭제한다.
* DFLT_* 컬럼은 최종 yml 시딩값 스냅샷으로, 관리자 수정과의 충돌 판정과
* "메뉴 초기화"의 원본으로 사용한다.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "PTL_MENU_ITEM")
public class PortalMenuItem extends Auditable implements Serializable {
private static final long serialVersionUID = 1L;
public static final String SOURCE_PORTAL = "PORTAL";
public static final String SOURCE_ADMIN = "ADMIN";
public static final String SECTION_GNB = "GNB";
public static final String SECTION_MYPAGE = "MYPAGE";
/** EXPOSE/ACCESS_ROLES 특수값: 로그인 사용자 전체 */
public static final String ROLE_AUTHENTICATED = "AUTHENTICATED";
@Id
@Column(name = "MENU_ID", length = 100, nullable = false)
@Comment("kebab-case 자연키 (menu.yml id)")
private String menuId;
@Column(name = "MENU_NAME", length = 200, nullable = false)
@Comment("노출명 (관리자 수정 가능)")
private String menuName;
@Column(name = "MENU_PATH", length = 500)
@Comment("이동 경로 (NULL=클릭 없는 그룹)")
private String menuPath;
@Column(name = "GROUP_YN", length = 1, nullable = false)
@Comment("그룹(상위 메뉴) 여부 - 구조 필드, menu.yml 소유")
private String groupYn = "N";
@Column(name = "MENU_SECTION", length = 20, nullable = false)
@Comment("섹션 (GNB=상단 글로벌 네비 / MYPAGE=마이페이지 드롭다운)")
private String menuSection = SECTION_GNB;
@Column(name = "NEW_WINDOW_YN", length = 1, nullable = false)
@Comment("새 창 열기 여부 (관리자 추가 외부링크용)")
private String newWindowYn = "N";
@Column(name = "ICON_CLASS", length = 100)
@Comment("아이콘 클래스 (FontAwesome, 마이페이지 드롭다운 표기)")
private String iconClass;
@Column(name = "EXPOSE_ROLES", length = 500)
@Comment("노출 권한 CSV (NULL=전체, AUTHENTICATED=로그인자, 그 외 역할코드 any-of)")
private String exposeRoles;
@Column(name = "ACCESS_ROLES", length = 500)
@Comment("접근 권한 CSV (NULL=제한 없음, 서버 인터셉터 집행)")
private String accessRoles;
@Column(name = "SOURCE_TYPE", length = 10, nullable = false)
@Comment("항목 출처 (PORTAL=menu.yml 기본 / ADMIN=관리자 커스텀)")
private String sourceType = SOURCE_PORTAL;
@Column(name = "DFLT_MENU_NAME", length = 200)
@Comment("기본 노출명 (최종 yml 시딩값)")
private String dfltMenuName;
@Column(name = "DFLT_MENU_PATH", length = 500)
@Comment("기본 경로 (최종 yml 시딩값)")
private String dfltMenuPath;
@Column(name = "DFLT_EXPOSE_ROLES", length = 500)
@Comment("기본 노출 권한 CSV (최종 yml 시딩값)")
private String dfltExposeRoles;
@Column(name = "DFLT_ACCESS_ROLES", length = 500)
@Comment("기본 접근 권한 CSV (최종 yml 시딩값)")
private String dfltAccessRoles;
@Column(name = "DFLT_PARENT_ID", length = 100)
@Comment("기본 배치 부모 (MENU_ID 논리 참조, NULL=최상위)")
private String dfltParentId;
@Column(name = "DFLT_SORT_ORDER")
@Comment("기본 배치 정렬 순서")
private Integer dfltSortOrder;
@Column(name = "DFLT_VISIBLE_YN", length = 1, nullable = false)
@Comment("기본 노출 여부")
private String dfltVisibleYn = "Y";
public boolean isGroup() {
return "Y".equals(groupYn);
}
public boolean isPortalSource() {
return SOURCE_PORTAL.equals(sourceType);
}
}
@@ -0,0 +1,48 @@
package com.eactive.apim.portal.menu.entity;
import com.eactive.apim.portal.common.entity.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* 포탈 메뉴 현재 배치.
*
* <p>row 없음 = 미배치. eapim-portal 부팅 시 테이블이 비어있을 때만
* {@link PortalMenuItem} 의 DFLT_* 값으로 최초 시딩하며, 이후 배치 관리는
* eapim-admin 이 수행한다(일괄 replace-all 저장).
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "PTL_MENU_PLACEMENT")
public class PortalMenuPlacement extends Auditable implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "MENU_ID", length = 100, nullable = false)
@Comment("PTL_MENU_ITEM.MENU_ID 논리 참조 (항목당 배치 최대 1건)")
private String menuId;
@Column(name = "PARENT_ID", length = 100)
@Comment("부모 메뉴 ID (NULL=최상위/레인)")
private String parentId;
@Column(name = "SORT_ORDER", nullable = false)
@Comment("동일 부모 내 정렬 순서")
private Integer sortOrder;
@Column(name = "VISIBLE_YN", length = 1, nullable = false)
@Comment("노출 여부 (N=배치 유지한 채 숨김)")
private String visibleYn = "Y";
public boolean isVisible() {
return "Y".equals(visibleYn);
}
}
@@ -0,0 +1,47 @@
package com.eactive.apim.portal.menu.entity;
import com.eactive.apim.portal.common.entity.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* 포탈 역할 사전 (roles.yml 미러).
*
* <p>eapim-portal 부팅 시 roles.yml 로 upsert 된다. 로그인 시 권한 확장은
* yml 바인딩을 직접 사용하며, 본 테이블은 eapim-admin 의 메뉴 권한 선택
* UI(체크박스) 소스로만 소비된다.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "PTL_ROLE")
public class PortalRole extends Auditable implements Serializable {
private static final long serialVersionUID = 1L;
public static final String TYPE_BASE = "BASE";
public static final String TYPE_AUTHORITY = "AUTHORITY";
@Id
@Column(name = "ROLE_CODE", length = 50, nullable = false)
@Comment("역할 코드 (예: ROLE_CORP_MANAGER)")
private String roleCode;
@Column(name = "ROLE_NAME", length = 200, nullable = false)
@Comment("역할 한글명")
private String roleName;
@Column(name = "ROLE_TYPE", length = 10, nullable = false)
@Comment("역할 유형 (BASE=기본 역할 / AUTHORITY=파생 권한)")
private String roleType;
@Column(name = "SORT_ORDER", nullable = false)
@Comment("표시 정렬 순서")
private Integer sortOrder;
}
@@ -0,0 +1,24 @@
package com.eactive.apim.portal.menu.entity;
import com.eactive.apim.portal.common.entity.Auditable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
/**
* 기본 역할 → 파생 권한 매핑 (roles.yml 미러, eapim-admin 조회 전용).
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "PTL_ROLE_AUTHORITY")
public class PortalRoleAuthority extends Auditable implements Serializable {
private static final long serialVersionUID = 1L;
@EmbeddedId
private PortalRoleAuthorityId id;
}
@@ -0,0 +1,26 @@
package com.eactive.apim.portal.menu.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class PortalRoleAuthorityId implements Serializable {
private static final long serialVersionUID = 1L;
@Column(name = "ROLE_CODE", length = 50, nullable = false)
@Comment("기본 역할 코드 (PTL_ROLE.ROLE_CODE 논리 참조)")
private String roleCode;
@Column(name = "AUTHORITY_CODE", length = 50, nullable = false)
@Comment("부여되는 파생 권한 코드")
private String authorityCode;
}
@@ -0,0 +1,13 @@
package com.eactive.apim.portal.menu.repository;
import com.eactive.apim.portal.menu.entity.PortalMenuItem;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
@EMSDataSource
public interface PortalMenuItemRepository extends BaseRepository<PortalMenuItem, String> {
List<PortalMenuItem> findAllBySourceType(String sourceType);
}
@@ -0,0 +1,13 @@
package com.eactive.apim.portal.menu.repository;
import com.eactive.apim.portal.menu.entity.PortalMenuPlacement;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
@EMSDataSource
public interface PortalMenuPlacementRepository extends BaseRepository<PortalMenuPlacement, String> {
List<PortalMenuPlacement> findAllByOrderByParentIdAscSortOrderAsc();
}
@@ -0,0 +1,10 @@
package com.eactive.apim.portal.menu.repository;
import com.eactive.apim.portal.menu.entity.PortalRoleAuthority;
import com.eactive.apim.portal.menu.entity.PortalRoleAuthorityId;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
@EMSDataSource
public interface PortalRoleAuthorityRepository extends BaseRepository<PortalRoleAuthority, PortalRoleAuthorityId> {
}
@@ -0,0 +1,13 @@
package com.eactive.apim.portal.menu.repository;
import com.eactive.apim.portal.menu.entity.PortalRole;
import com.eactive.eai.data.jpa.BaseRepository;
import com.eactive.eai.rms.data.EMSDataSource;
import java.util.List;
@EMSDataSource
public interface PortalRoleRepository extends BaseRepository<PortalRole, String> {
List<PortalRole> findAllByOrderBySortOrderAsc();
}
@@ -0,0 +1,124 @@
package com.eactive.apim.portal.menu.service;
import com.eactive.apim.portal.menu.entity.PortalMenuItem;
import com.eactive.apim.portal.menu.entity.PortalMenuPlacement;
import com.eactive.apim.portal.menu.repository.PortalMenuItemRepository;
import com.eactive.apim.portal.menu.repository.PortalMenuPlacementRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 메뉴 항목/배치 공유 데이터 서비스 (eapim-portal · eapim-admin 공용).
*
* <p>트랜잭션 경계: {@code PortalPropertyService} 와 동일하게 기본 @Transactional 을 사용한다.
* eapim-admin 에서는 호출측 ManService 가 {@code @Transactional("transactionManagerForEMS")}
* 로 감싸 EMS 트랜잭션에 참여시킨다.
*/
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class PortalMenuDataService {
private final PortalMenuItemRepository menuItemRepository;
private final PortalMenuPlacementRepository menuPlacementRepository;
@Transactional(readOnly = true)
public List<PortalMenuItem> loadAllItems() {
return menuItemRepository.findAll();
}
@Transactional(readOnly = true)
public List<PortalMenuPlacement> loadAllPlacements() {
return menuPlacementRepository.findAllByOrderByParentIdAscSortOrderAsc();
}
/**
* 현재 배치를 통째로 교체한다(일괄 저장). 스윔레인 "저장" 버튼과
* 메뉴 초기화가 공용으로 사용한다.
*/
public void replacePlacements(List<PortalMenuPlacement> placements) {
menuPlacementRepository.deleteAllInBatch();
menuPlacementRepository.flush();
menuPlacementRepository.saveAll(placements);
}
/**
* 메뉴 삭제 가드.
* <ul>
* <li>PORTAL(기본) 항목: 실제 삭제 금지 — 배치(placement)만 제거해 미배치로 전환</li>
* <li>ADMIN(커스텀) 항목: 배치 제거 + 항목 하드 삭제</li>
* </ul>
*
* @return true 이면 항목까지 삭제됨(커스텀), false 이면 미배치 전환만 수행(기본)
*/
public boolean deleteMenu(String menuId) {
PortalMenuItem item = menuItemRepository.findById(menuId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 메뉴: " + menuId));
menuPlacementRepository.findById(menuId).ifPresent(menuPlacementRepository::delete);
if (item.isPortalSource()) {
log.info("기본 메뉴 항목 미배치 전환: {}", menuId);
return false;
}
menuItemRepository.delete(item);
log.info("커스텀 메뉴 항목 삭제: {}", menuId);
return true;
}
/**
* 메뉴 초기화 — eapim-portal 이 시딩한 기본값으로 되돌린다.
* <ul>
* <li>PORTAL 항목: 현재 필드를 DFLT_* 값으로 복원</li>
* <li>ADMIN 항목: 전부 삭제</li>
* <li>배치: PORTAL 항목의 DFLT_PARENT_ID/DFLT_SORT_ORDER/DFLT_VISIBLE_YN 로 재구축</li>
* </ul>
*/
public void resetToDefaults() {
List<PortalMenuItem> allItems = menuItemRepository.findAll();
List<PortalMenuItem> adminItems = allItems.stream()
.filter(item -> !item.isPortalSource())
.collect(Collectors.toList());
menuItemRepository.deleteAll(adminItems);
List<PortalMenuItem> portalItems = allItems.stream()
.filter(PortalMenuItem::isPortalSource)
.collect(Collectors.toList());
for (PortalMenuItem item : portalItems) {
item.setMenuName(item.getDfltMenuName());
item.setMenuPath(item.getDfltMenuPath());
item.setExposeRoles(item.getDfltExposeRoles());
item.setAccessRoles(item.getDfltAccessRoles());
}
menuItemRepository.saveAll(portalItems);
replacePlacements(buildDefaultPlacements(portalItems));
log.info("메뉴 초기화 완료 - 기본 항목 {}건 복원, 커스텀 항목 {}건 삭제",
portalItems.size(), adminItems.size());
}
/**
* PORTAL 항목의 기본 위치 정보(DFLT_*)로 배치 목록을 생성한다.
* eapim-portal 최초 시딩과 메뉴 초기화가 공용으로 사용한다.
*/
public List<PortalMenuPlacement> buildDefaultPlacements(List<PortalMenuItem> portalItems) {
return portalItems.stream()
.filter(item -> item.getDfltSortOrder() != null)
.map(item -> {
PortalMenuPlacement placement = new PortalMenuPlacement();
placement.setMenuId(item.getMenuId());
placement.setParentId(item.getDfltParentId());
placement.setSortOrder(item.getDfltSortOrder());
placement.setVisibleYn(item.getDfltVisibleYn());
return placement;
})
.collect(Collectors.toList());
}
}