From 2c7a1a292d285fe6f16959f132e6cdaa52b10142 Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Mon, 11 Aug 2025 02:42:50 +0900 Subject: [PATCH 1/2] feat: add exception handling for validation errors in GlobalExceptionHandler --- .../exception/GlobalExceptionHandler.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/java/life/mosu/mosuserver/global/exception/GlobalExceptionHandler.java b/src/main/java/life/mosu/mosuserver/global/exception/GlobalExceptionHandler.java index 645da67f..ddf53e9a 100644 --- a/src/main/java/life/mosu/mosuserver/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/life/mosu/mosuserver/global/exception/GlobalExceptionHandler.java @@ -1,8 +1,10 @@ package life.mosu.mosuserver.global.exception; import jakarta.persistence.EntityNotFoundException; -import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import life.mosu.mosuserver.infra.notify.NotifyClientAdapter; import life.mosu.mosuserver.infra.notify.dto.discord.DiscordExceptionNotifyEventRequest; @@ -16,6 +18,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.HandlerMethodValidationException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.reactive.resource.NoResourceFoundException; @@ -126,7 +129,7 @@ public ResponseEntity handleHttpMessageNotReadableException( return ResponseEntity.status(HttpStatus.CONFLICT).body(response); } - @ExceptionHandler(NoResourceFoundException.class ) + @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity handleNotFound(Exception ex) { notifyIfNeeded(ex); @@ -140,12 +143,40 @@ public ResponseEntity handleNotFound(Exception ex) { } @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleTypeMismatch(MethodArgumentTypeMismatchException ex) { + public ResponseEntity handleTypeMismatch( + MethodArgumentTypeMismatchException ex) { notifyIfNeeded(ex); ErrorResponse response = ErrorResponse.builder() .status(HttpStatus.BAD_REQUEST.value()) .code("TYPE_MISMATCH") - .message("요청 파라미터 타입이 올바르지 않습니다.") + .message(ex.getMessage().toLowerCase(Locale.ROOT)) + .build(); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(HandlerMethodValidationException.class) + public ResponseEntity handleHandlerMethodValidation(HandlerMethodValidationException ex) { + notifyIfNeeded(ex); + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .code("TYPE_MISMATCH") + .message(ex.getMessage().toLowerCase(Locale.ROOT)) + .build(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolation(ConstraintViolationException ex) { + notifyIfNeeded(ex); + String message = ex.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .findFirst() + .orElse("유효성 검사에 실패했습니다."); + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .code("VAILDATION_ERROR") + .message(message) .build(); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); From 20381b4e33fcdbe4218b54723c56602ee2cc84ab Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Mon, 11 Aug 2025 02:43:54 +0900 Subject: [PATCH 2/2] feat: add custom validation for login ID format in UserController --- .../mosu/mosuserver/global/annotation/LoginIdPattern.java | 4 ++-- .../mosu/mosuserver/presentation/user/UserController.java | 5 ++++- .../mosuserver/presentation/user/UserControllerDocs.java | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/java/life/mosu/mosuserver/global/annotation/LoginIdPattern.java b/src/main/java/life/mosu/mosuserver/global/annotation/LoginIdPattern.java index d9107131..a5fc650b 100644 --- a/src/main/java/life/mosu/mosuserver/global/annotation/LoginIdPattern.java +++ b/src/main/java/life/mosu/mosuserver/global/annotation/LoginIdPattern.java @@ -2,7 +2,7 @@ import jakarta.validation.Constraint; import jakarta.validation.Payload; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.ReportAsSingleViolation; import jakarta.validation.constraints.Pattern; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -10,10 +10,10 @@ import java.lang.annotation.Target; @Pattern(regexp = "^[a-zA-Z0-9_-]{6,12}$", message = "아이디 형식이 올바르지 않습니다.") -@NotBlank @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = {}) +@ReportAsSingleViolation public @interface LoginIdPattern { String message() default "아이디는 6~12자의 영문, 숫자, 특수문자(-, _)만 사용 가능합니다."; diff --git a/src/main/java/life/mosu/mosuserver/presentation/user/UserController.java b/src/main/java/life/mosu/mosuserver/presentation/user/UserController.java index ca87dbdd..0a5f0bf3 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/user/UserController.java +++ b/src/main/java/life/mosu/mosuserver/presentation/user/UserController.java @@ -1,6 +1,7 @@ package life.mosu.mosuserver.presentation.user; import life.mosu.mosuserver.application.user.UserService; +import life.mosu.mosuserver.global.annotation.LoginIdPattern; import life.mosu.mosuserver.global.annotation.UserId; import life.mosu.mosuserver.global.util.ApiResponseWrapper; import life.mosu.mosuserver.presentation.user.dto.request.IsLoginIdAvailableResponse; @@ -10,6 +11,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -18,6 +20,7 @@ @RestController @RequestMapping("/user") @RequiredArgsConstructor +@Validated public class UserController implements UserControllerDocs { private final UserService userService; @@ -46,7 +49,7 @@ public ResponseEntity> getCustomerKey( @GetMapping("/check-id") public ResponseEntity> isLoginIdAvailable( - @RequestParam String loginId + @LoginIdPattern @RequestParam String loginId ) { Boolean isLoginIdAvailable = userService.isLoginIdAvailable(loginId); diff --git a/src/main/java/life/mosu/mosuserver/presentation/user/UserControllerDocs.java b/src/main/java/life/mosu/mosuserver/presentation/user/UserControllerDocs.java index a7263050..c766bb29 100644 --- a/src/main/java/life/mosu/mosuserver/presentation/user/UserControllerDocs.java +++ b/src/main/java/life/mosu/mosuserver/presentation/user/UserControllerDocs.java @@ -7,14 +7,17 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import life.mosu.mosuserver.global.annotation.LoginIdPattern; import life.mosu.mosuserver.global.annotation.UserId; import life.mosu.mosuserver.global.util.ApiResponseWrapper; import life.mosu.mosuserver.presentation.user.dto.request.IsLoginIdAvailableResponse; import life.mosu.mosuserver.presentation.user.dto.response.CustomerKeyResponse; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "User", description = "사용자 관련 API") +@Validated public interface UserControllerDocs { @Operation(summary = "고객 키 조회", description = "사용자 ID를 이용해 결제에 사용될 고객 키(Customer Key)를 조회합니다.") @@ -37,6 +40,6 @@ public ResponseEntity> getCustomerKey( @ApiResponse(responseCode = "500", description = "서버 내부 오류") }) public ResponseEntity> isLoginIdAvailable( - @RequestParam String loginId + @LoginIdPattern @RequestParam String loginId ); } \ No newline at end of file