From 3e4ced9b3dde97b1bae2072abb3c262d3e5610b8 Mon Sep 17 00:00:00 2001 From: Rinjae Date: Wed, 5 Aug 2026 14:19:33 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EB=89=B4=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20menu.yml=20GNB=20=EC=A0=95=EC=9D=98=20?= =?UTF-8?q?-=20=EC=A0=91=EA=B7=BC=20=EC=9D=B8=ED=84=B0=EC=85=89=ED=84=B0/?= =?UTF-8?q?=EB=B7=B0=20=EC=A3=BC=EC=9E=85/=EB=82=B4=EB=B6=80=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../portal/menu/entity/PortalMenuItem.java | 113 ++++++++++++++++ .../menu/entity/PortalMenuPlacement.java | 48 +++++++ .../apim/portal/menu/entity/PortalRole.java | 47 +++++++ .../menu/entity/PortalRoleAuthority.java | 24 ++++ .../menu/entity/PortalRoleAuthorityId.java | 26 ++++ .../repository/PortalMenuItemRepository.java | 13 ++ .../PortalMenuPlacementRepository.java | 13 ++ .../PortalRoleAuthorityRepository.java | 10 ++ .../menu/repository/PortalRoleRepository.java | 13 ++ .../menu/service/PortalMenuDataService.java | 124 ++++++++++++++++++ 10 files changed, 431 insertions(+) create mode 100644 src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuItem.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuPlacement.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/entity/PortalRole.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthority.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthorityId.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuItemRepository.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuPlacementRepository.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleAuthorityRepository.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleRepository.java create mode 100644 src/main/java/com/eactive/apim/portal/menu/service/PortalMenuDataService.java diff --git a/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuItem.java b/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuItem.java new file mode 100644 index 0000000..2975022 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuItem.java @@ -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; + +/** + * 포탈 메뉴 항목 마스터. + * + *

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); + } +} diff --git a/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuPlacement.java b/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuPlacement.java new file mode 100644 index 0000000..eb54015 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/entity/PortalMenuPlacement.java @@ -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; + +/** + * 포탈 메뉴 현재 배치. + * + *

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); + } +} diff --git a/src/main/java/com/eactive/apim/portal/menu/entity/PortalRole.java b/src/main/java/com/eactive/apim/portal/menu/entity/PortalRole.java new file mode 100644 index 0000000..87791be --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/entity/PortalRole.java @@ -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 미러). + * + *

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; +} diff --git a/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthority.java b/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthority.java new file mode 100644 index 0000000..4d43016 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthority.java @@ -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; +} diff --git a/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthorityId.java b/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthorityId.java new file mode 100644 index 0000000..e604d58 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/entity/PortalRoleAuthorityId.java @@ -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; +} diff --git a/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuItemRepository.java b/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuItemRepository.java new file mode 100644 index 0000000..c450b80 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuItemRepository.java @@ -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 { + + List findAllBySourceType(String sourceType); +} diff --git a/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuPlacementRepository.java b/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuPlacementRepository.java new file mode 100644 index 0000000..4f308ed --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/repository/PortalMenuPlacementRepository.java @@ -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 { + + List findAllByOrderByParentIdAscSortOrderAsc(); +} diff --git a/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleAuthorityRepository.java b/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleAuthorityRepository.java new file mode 100644 index 0000000..3d55141 --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleAuthorityRepository.java @@ -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 { +} diff --git a/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleRepository.java b/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleRepository.java new file mode 100644 index 0000000..9fdbf5d --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/repository/PortalRoleRepository.java @@ -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 { + + List findAllByOrderBySortOrderAsc(); +} diff --git a/src/main/java/com/eactive/apim/portal/menu/service/PortalMenuDataService.java b/src/main/java/com/eactive/apim/portal/menu/service/PortalMenuDataService.java new file mode 100644 index 0000000..1a7ee1e --- /dev/null +++ b/src/main/java/com/eactive/apim/portal/menu/service/PortalMenuDataService.java @@ -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 공용). + * + *

트랜잭션 경계: {@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 loadAllItems() { + return menuItemRepository.findAll(); + } + + @Transactional(readOnly = true) + public List loadAllPlacements() { + return menuPlacementRepository.findAllByOrderByParentIdAscSortOrderAsc(); + } + + /** + * 현재 배치를 통째로 교체한다(일괄 저장). 스윔레인 "저장" 버튼과 + * 메뉴 초기화가 공용으로 사용한다. + */ + public void replacePlacements(List placements) { + menuPlacementRepository.deleteAllInBatch(); + menuPlacementRepository.flush(); + menuPlacementRepository.saveAll(placements); + } + + /** + * 메뉴 삭제 가드. + *

+ * + * @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 이 시딩한 기본값으로 되돌린다. + * + */ + public void resetToDefaults() { + List allItems = menuItemRepository.findAll(); + + List adminItems = allItems.stream() + .filter(item -> !item.isPortalSource()) + .collect(Collectors.toList()); + menuItemRepository.deleteAll(adminItems); + + List 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 buildDefaultPlacements(List 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()); + } +}