From 603934d49aca3d9b62718e5346e2bf22347755ee Mon Sep 17 00:00:00 2001 From: Sin-Kang Date: Sat, 30 May 2026 16:14:02 +0900 Subject: [PATCH] fix(admin): return 409 (not 500) on duplicate menu / group code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consistency gap found by auditing every create endpoint after the menu duplicate surfaced during integration testing. GlobalExceptionHandler maps IllegalStateException -> 409 CONFLICT, and User / Role / Permission / Tenant services all do a findBy...isPresent() pre-check and throw it on a duplicate. But MenuAdminService.create and GroupService.create had no such check — they went straight to repository.save(), hit the uq_platform_{menu,group}_tenant_code unique constraint, and bubbled a DataIntegrityViolationException that no handler caught, producing a 500 instead of a clean 409. Both tables already declare the unique constraint, so this only adds the friendly pre-check that the other four resources already had: - JpaPlatformGroupRepository / JpaPlatformMenuRepository: new findByTenantIdAndCode(tenantId, code). - GroupService.create / MenuAdminService.create: throw IllegalStateException("... already exists ...") when a row with the same (tenantId, code) exists. Verified end-to-end against the running sample-app: POST /menus {code: dup.menu.test} -> 201 then 409 POST /groups {code: dup-grp} -> 201 then 409 POST /roles {code: PLATFORM_ADMIN}-> 409 (unchanged, sanity) Note: there's still a small race between the pre-check and the insert under concurrent creates — the DB constraint is the real guard and would 500 in that rare window. Making that path also return 409 would mean catching DataIntegrityViolationException in GlobalExceptionHandler; left out here to keep the change minimal and consistent with the existing four services, which have the same theoretical race. --- .../kit/menu/core/repository/JpaPlatformMenuRepository.java | 3 +++ .../kr/devslab/kit/menu/core/service/MenuAdminService.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/devslab-kit-menu-core/src/main/java/kr/devslab/kit/menu/core/repository/JpaPlatformMenuRepository.java b/devslab-kit-menu-core/src/main/java/kr/devslab/kit/menu/core/repository/JpaPlatformMenuRepository.java index 9ab4628..7693de9 100644 --- a/devslab-kit-menu-core/src/main/java/kr/devslab/kit/menu/core/repository/JpaPlatformMenuRepository.java +++ b/devslab-kit-menu-core/src/main/java/kr/devslab/kit/menu/core/repository/JpaPlatformMenuRepository.java @@ -1,6 +1,7 @@ package kr.devslab.kit.menu.core.repository; import java.util.List; +import java.util.Optional; import java.util.UUID; import kr.devslab.kit.menu.core.entity.PlatformMenuEntity; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,4 +9,6 @@ public interface JpaPlatformMenuRepository extends JpaRepository { List findAllByTenantIdOrderBySortOrderAsc(String tenantId); + + Optional findByTenantIdAndCode(String tenantId, String code); } diff --git a/devslab-kit-menu-core/src/main/java/kr/devslab/kit/menu/core/service/MenuAdminService.java b/devslab-kit-menu-core/src/main/java/kr/devslab/kit/menu/core/service/MenuAdminService.java index 0a711d0..55b3220 100644 --- a/devslab-kit-menu-core/src/main/java/kr/devslab/kit/menu/core/service/MenuAdminService.java +++ b/devslab-kit-menu-core/src/main/java/kr/devslab/kit/menu/core/service/MenuAdminService.java @@ -31,6 +31,9 @@ public PlatformMenuEntity create( String requiredPermissionCode, String icon ) { + if (repository.findByTenantIdAndCode(tenantId.value(), code).isPresent()) { + throw new IllegalStateException("Menu already exists: tenant=" + tenantId + " code=" + code); + } PlatformMenuEntity entity = new PlatformMenuEntity( UUID.randomUUID(), tenantId.value(),