From 43b022ea295ca6ec5022c234e6b407e90bb3cd16 Mon Sep 17 00:00:00 2001 From: "donghyuck, son" Date: Wed, 29 Apr 2026 21:48:17 +0900 Subject: [PATCH] =?UTF-8?q?[ai-assisted]=20feat(user):=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: - #376 Why: - Admin 회원 목록에는 사용자 삭제 액션이 있지만 기본 사용자 관리 컨트롤러에 DELETE /api/mgmt/users/{id} 엔드포인트가 없어 클라이언트 연결이 불가능했다. What: - 기본 UserMgmtController에 features:user admin 권한의 DELETE /{id} 엔드포인트를 추가했다. - UserMgmtApi에는 default delete 계약을 추가해 커스텀 구현체 호환성을 유지했다. - ApplicationUserServiceImpl.delete 성공 시 사용자 캐시를 정리하도록 보강했다. - user README와 starter README에 삭제 API와 응답 형식을 문서화했다. Validation: - ./gradlew :studio-platform-user:compileJava :studio-platform-user-default:test :starter:studio-platform-starter-user:test: 성공 - ./gradlew test: 성공 - git diff --check: 성공 --- starter/studio-platform-starter-user/README.md | 3 +++ .../user/service/impl/ApplicationUserServiceImpl.java | 4 ++++ .../base/user/web/controller/UserMgmtController.java | 8 ++++++++ .../UserMgmtControllerAuthorizationTest.java | 10 ++++++++++ studio-platform-user/README.md | 8 ++++++++ .../one/base/user/web/controller/UserMgmtApi.java | 4 ++++ 6 files changed, 37 insertions(+) diff --git a/starter/studio-platform-starter-user/README.md b/starter/studio-platform-starter-user/README.md index 7bf2ca5f..ae4c2e35 100644 --- a/starter/studio-platform-starter-user/README.md +++ b/starter/studio-platform-starter-user/README.md @@ -140,6 +140,9 @@ studio: - `/api/mgmt/roles` - `/api/self` +기본 사용자 관리 API에는 `DELETE /api/mgmt/users/{id}`가 포함된다. 이 엔드포인트는 +`features:user` admin 권한을 요구하며, 성공 시 `204 No Content`를 반환한다. + 기본 컨트롤러는 `ApplicationUserMapper`/`ApplicationUserService` 빈이 있을 때만 등록된다. 커스텀 컨트롤러를 제공할 때는 `UserMgmtApi`/`UserPublicApi`/`UserAuthPublicApi`/`UserMeApi` 인터페이스를 구현하면 기본 컨트롤러가 자동으로 비활성화된다. diff --git a/studio-platform-user-default/src/main/java/studio/one/base/user/service/impl/ApplicationUserServiceImpl.java b/studio-platform-user-default/src/main/java/studio/one/base/user/service/impl/ApplicationUserServiceImpl.java index 5ad4803a..bac4e43c 100644 --- a/studio-platform-user-default/src/main/java/studio/one/base/user/service/impl/ApplicationUserServiceImpl.java +++ b/studio-platform-user-default/src/main/java/studio/one/base/user/service/impl/ApplicationUserServiceImpl.java @@ -175,6 +175,10 @@ public ApplicationUser update(Long userId, Consumer mutator) { } @Transactional + @Caching(evict = { + @CacheEvict(cacheNames = CacheNames.User.BY_USER_ID, key = "#userId"), + @CacheEvict(cacheNames = CacheNames.User.BY_USERNAME, allEntries = true) + }) public void delete(Long userId) { ApplicationUser u = get(userId); userRepo.delete(u); diff --git a/studio-platform-user-default/src/main/java/studio/one/base/user/web/controller/UserMgmtController.java b/studio-platform-user-default/src/main/java/studio/one/base/user/web/controller/UserMgmtController.java index 0a259fbb..553cd812 100644 --- a/studio-platform-user-default/src/main/java/studio/one/base/user/web/controller/UserMgmtController.java +++ b/studio-platform-user-default/src/main/java/studio/one/base/user/web/controller/UserMgmtController.java @@ -176,6 +176,14 @@ public ResponseEntity> update(@PathVariable Long id, @Reque return ResponseEntity.ok(ApiResponse.ok(userMapper.toDto(updated))); } + @DeleteMapping("/{id}") + @PreAuthorize("@endpointAuthz.can('features:user','admin')") + @Override + public ResponseEntity delete(@PathVariable Long id) { + userService.delete(id); + return ResponseEntity.noContent().build(); + } + @GetMapping("/password-policy") @PreAuthorize("@endpointAuthz.can('features:user','admin')") @Override diff --git a/studio-platform-user-default/src/test/java/studio/one/base/user/web/controller/UserMgmtControllerAuthorizationTest.java b/studio-platform-user-default/src/test/java/studio/one/base/user/web/controller/UserMgmtControllerAuthorizationTest.java index 0ea0d192..72db2b81 100644 --- a/studio-platform-user-default/src/test/java/studio/one/base/user/web/controller/UserMgmtControllerAuthorizationTest.java +++ b/studio-platform-user-default/src/test/java/studio/one/base/user/web/controller/UserMgmtControllerAuthorizationTest.java @@ -86,6 +86,16 @@ void passwordResetDelegatesActorUsername() { verify(userService).resetPassword(eq(10L), eq("NextPassword123!"), eq("admin"), eq("test-reason")); } + @Test + void deleteDelegatesToUserService() { + UserMgmtController controller = controller(); + + var response = controller.delete(10L); + + assertEquals(204, response.getStatusCode().value()); + verify(userService).delete(10L); + } + @Test void updateUserRolesDelegatesDistinctRoleIds() { UserMgmtController controller = controller(); diff --git a/studio-platform-user/README.md b/studio-platform-user/README.md index a8fc0823..4ecc65c6 100644 --- a/studio-platform-user/README.md +++ b/studio-platform-user/README.md @@ -24,6 +24,7 @@ Response: `ApiResponse` Password policy is defined by configuration and enforced on: - **Self password change**: `PUT /api/self/password` - **Admin reset**: `POST /api/mgmt/users/{id}/password` +- **Admin delete**: `DELETE /api/mgmt/users/{id}` removes the user through the configured `ApplicationUserService` implementation. ### Policy Config (YAML) ```yaml @@ -117,6 +118,13 @@ Returns current password policy configuration for admin UI. Response: `ApiResponse` +### DELETE /api/mgmt/users/{id} +Deletes a user from the admin API. + +Auth: `features:user` admin permission + +Response: `204 No Content` + ## Example Requests ### PATCH diff --git a/studio-platform-user/src/main/java/studio/one/base/user/web/controller/UserMgmtApi.java b/studio-platform-user/src/main/java/studio/one/base/user/web/controller/UserMgmtApi.java index 95706b42..1f806f14 100644 --- a/studio-platform-user/src/main/java/studio/one/base/user/web/controller/UserMgmtApi.java +++ b/studio-platform-user/src/main/java/studio/one/base/user/web/controller/UserMgmtApi.java @@ -50,6 +50,10 @@ ResponseEntity>> find( ResponseEntity> update(Long id, @RequestBody UpdateUserRequest req); + default ResponseEntity delete(Long id) { + throw new UnsupportedOperationException("User delete endpoint is not implemented"); + } + ResponseEntity> passwordPolicy(); ResponseEntity passwordReset(Long id, @Valid @RequestBody ChangePasswordRequest req,