diff --git a/src/main/java/side/onetime/controller/EventController.java b/src/main/java/side/onetime/controller/EventController.java index a452f5c..85ee4ff 100644 --- a/src/main/java/side/onetime/controller/EventController.java +++ b/src/main/java/side/onetime/controller/EventController.java @@ -1,5 +1,6 @@ package side.onetime.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -20,7 +21,7 @@ public class EventController { // 이벤트 생성 API @PostMapping public ResponseEntity> createEvent( - @RequestBody CreateEventRequest createEventRequest, + @Valid @RequestBody CreateEventRequest createEventRequest, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { CreateEventResponse createEventResponse; diff --git a/src/main/java/side/onetime/controller/MemberController.java b/src/main/java/side/onetime/controller/MemberController.java index 36c5f70..6ba7f6b 100644 --- a/src/main/java/side/onetime/controller/MemberController.java +++ b/src/main/java/side/onetime/controller/MemberController.java @@ -1,5 +1,6 @@ package side.onetime.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -25,7 +26,7 @@ public class MemberController { // 멤버 등록 API @PostMapping("/action-register") public ResponseEntity> registerMember( - @RequestBody RegisterMemberRequest registerMemberRequest) { + @Valid @RequestBody RegisterMemberRequest registerMemberRequest) { RegisterMemberResponse registerMemberResponse = memberService.registerMember(registerMemberRequest); return ApiResponse.onSuccess(SuccessStatus._REGISTER_MEMBER, registerMemberResponse); @@ -34,7 +35,7 @@ public ResponseEntity> registerMember( // 멤버 로그인 API @PostMapping("/action-login") public ResponseEntity> loginMember( - @RequestBody LoginMemberRequest loginMemberRequest) { + @Valid @RequestBody LoginMemberRequest loginMemberRequest) { LoginMemberResponse loginMemberResponse = memberService.loginMember(loginMemberRequest); return ApiResponse.onSuccess(SuccessStatus._LOGIN_MEMBER, loginMemberResponse); @@ -43,7 +44,7 @@ public ResponseEntity> loginMember( // 이름 중복 확인 API @PostMapping("/name/action-check") public ResponseEntity> isDuplicate( - @RequestBody IsDuplicateRequest isDuplicateRequest) { + @Valid @RequestBody IsDuplicateRequest isDuplicateRequest) { IsDuplicateResponse isDuplicateResponse = memberService.isDuplicate(isDuplicateRequest); return ApiResponse.onSuccess(SuccessStatus._IS_POSSIBLE_NAME, isDuplicateResponse); diff --git a/src/main/java/side/onetime/controller/ScheduleController.java b/src/main/java/side/onetime/controller/ScheduleController.java index eb92ca6..944aea0 100644 --- a/src/main/java/side/onetime/controller/ScheduleController.java +++ b/src/main/java/side/onetime/controller/ScheduleController.java @@ -1,5 +1,6 @@ package side.onetime.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -23,7 +24,7 @@ public class ScheduleController { // 요일 스케줄 등록 API @PostMapping("/day") public ResponseEntity> createDaySchedules( - @RequestBody CreateDayScheduleRequest createDayScheduleRequest, + @Valid @RequestBody CreateDayScheduleRequest createDayScheduleRequest, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { if (authorizationHeader != null) { @@ -37,7 +38,7 @@ public ResponseEntity> createDaySchedules( // 날짜 스케줄 등록 API @PostMapping("/date") public ResponseEntity> createDateSchedules( - @RequestBody CreateDateScheduleRequest createDateScheduleRequest, + @Valid @RequestBody CreateDateScheduleRequest createDateScheduleRequest, @RequestHeader(value = "Authorization", required = false) String authorizationHeader) { if (authorizationHeader != null) { @@ -80,7 +81,7 @@ public ResponseEntity> getUserDaySchedules( // 멤버 필터링 요일 스케줄 조회 API @GetMapping("/day/action-filtering") public ResponseEntity>> getFilteredDaySchedules( - @RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) { + @Valid @RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) { List perDaySchedulesResponses = scheduleService.getFilteredDaySchedules(getFilteredSchedulesRequest); return ApiResponse.onSuccess(SuccessStatus._GET_FILTERED_DAY_SCHEDULES, perDaySchedulesResponses); @@ -118,7 +119,7 @@ public ResponseEntity> getUserDateSchedule // 멤버 필터링 날짜 스케줄 조회 API @GetMapping("/date/action-filtering") public ResponseEntity>> getFilteredDateSchedules( - @RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) { + @Valid @RequestBody GetFilteredSchedulesRequest getFilteredSchedulesRequest) { List perDateSchedulesResponses = scheduleService.getFilteredDateSchedules(getFilteredSchedulesRequest); return ApiResponse.onSuccess(SuccessStatus._GET_FILTERED_DATE_SCHEDULES, perDateSchedulesResponses); diff --git a/src/main/java/side/onetime/controller/TokenController.java b/src/main/java/side/onetime/controller/TokenController.java index 198765a..471d278 100644 --- a/src/main/java/side/onetime/controller/TokenController.java +++ b/src/main/java/side/onetime/controller/TokenController.java @@ -1,5 +1,6 @@ package side.onetime.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -21,7 +22,7 @@ public class TokenController { // 액세스 토큰 재발행 API @PostMapping("/action-reissue") public ResponseEntity> reissueToken( - @RequestBody ReissueTokenRequest reissueAccessTokenRequest) { + @Valid @RequestBody ReissueTokenRequest reissueAccessTokenRequest) { ReissueTokenResponse reissueTokenResponse = tokenService.reissueToken(reissueAccessTokenRequest); return ApiResponse.onSuccess(SuccessStatus._REISSUE_TOKENS, reissueTokenResponse); diff --git a/src/main/java/side/onetime/controller/UrlController.java b/src/main/java/side/onetime/controller/UrlController.java index 6d2d500..2425283 100644 --- a/src/main/java/side/onetime/controller/UrlController.java +++ b/src/main/java/side/onetime/controller/UrlController.java @@ -1,5 +1,6 @@ package side.onetime.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -23,7 +24,7 @@ public class UrlController { // 원본 -> 단축 URL API @PostMapping("/action-shorten") public ResponseEntity> convertToShortenUrl( - @RequestBody ConvertToShortenUrlRequest covertToShortenUrlRequest) { + @Valid @RequestBody ConvertToShortenUrlRequest covertToShortenUrlRequest) { ConvertToShortenUrlResponse convertToShortenUrlResponse = urlService.convertToShortenUrl(covertToShortenUrlRequest); return ApiResponse.onSuccess(SuccessStatus._CONVERT_TO_SHORTEN_URL, convertToShortenUrlResponse); @@ -32,7 +33,7 @@ public ResponseEntity> convertToShorten // 단축 -> 원본 URL API @PostMapping("/action-original") public ResponseEntity> convertToOriginalUrl( - @RequestBody ConvertToOriginalUrlRequest convertToOriginalUrlRequest) { + @Valid @RequestBody ConvertToOriginalUrlRequest convertToOriginalUrlRequest) { ConvertToOriginalUrlResponse convertToOriginalUrlResponse = urlService.convertToOriginalUrl(convertToOriginalUrlRequest); return ApiResponse.onSuccess(SuccessStatus._CONVERT_TO_ORIGINAL_URL, convertToOriginalUrlResponse); diff --git a/src/main/java/side/onetime/controller/UserController.java b/src/main/java/side/onetime/controller/UserController.java index b225892..2a135aa 100644 --- a/src/main/java/side/onetime/controller/UserController.java +++ b/src/main/java/side/onetime/controller/UserController.java @@ -1,5 +1,6 @@ package side.onetime.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -20,7 +21,7 @@ public class UserController { // 유저 온보딩 API @PostMapping("/onboarding") public ResponseEntity> onboardUser( - @RequestBody OnboardUserRequest onboardUserRequest) { + @Valid @RequestBody OnboardUserRequest onboardUserRequest) { OnboardUserResponse onboardUserResponse = userService.onboardUser(onboardUserRequest); return ApiResponse.onSuccess(SuccessStatus._ONBOARD_USER, onboardUserResponse); @@ -39,7 +40,7 @@ public ResponseEntity> getUserProfile( @PatchMapping("/profile/action-update") public ResponseEntity> updateUserProfile( @RequestHeader("Authorization") String authorizationHeader, - @RequestBody UpdateUserProfileRequest updateUserProfileRequest) { + @Valid @RequestBody UpdateUserProfileRequest updateUserProfileRequest) { userService.updateUserProfile(authorizationHeader, updateUserProfileRequest); return ApiResponse.onSuccess(SuccessStatus._UPDATE_USER_PROFILE); diff --git a/src/main/java/side/onetime/exception/GlobalExceptionHandler.java b/src/main/java/side/onetime/exception/GlobalExceptionHandler.java index ebad34a..c7f92fb 100644 --- a/src/main/java/side/onetime/exception/GlobalExceptionHandler.java +++ b/src/main/java/side/onetime/exception/GlobalExceptionHandler.java @@ -1,88 +1,134 @@ package side.onetime.exception; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import side.onetime.global.common.ApiResponse; -import side.onetime.global.common.code.BaseErrorCode; -import side.onetime.global.common.status.ErrorStatus; import side.onetime.global.common.dto.ErrorReasonDto; +import side.onetime.global.common.status.ErrorStatus; + +import java.util.List; +import java.util.stream.Collectors; -@RestControllerAdvice @Slf4j +@RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { - // Event - @ExceptionHandler(EventException.class) - public ResponseEntity> handleTokenException(EventException e) { - EventErrorResult errorResult = e.getEventErrorResult(); - return ApiResponse.onFailure(errorResult); + // 커스텀 예외 처리 + @ExceptionHandler(CustomException.class) + public ResponseEntity> handleCustomException(CustomException e) { + logError(e.getMessage(), e); + return ApiResponse.onFailure(e.getErrorCode()); } - // Member - @ExceptionHandler(MemberException.class) - public ResponseEntity> handleMemberException(MemberException e) { - MemberErrorResult errorResult = e.getMemberErrorResult(); - return ApiResponse.onFailure(errorResult); + // Security 인증 관련 처리 + @ExceptionHandler(SecurityException.class) + public ResponseEntity> handleSecurityException(SecurityException e) { + logError(e.getMessage(), e); + return ApiResponse.onFailure(ErrorStatus._UNAUTHORIZED); } - // Schedule - @ExceptionHandler(ScheduleException.class) - public ResponseEntity> handleScheduleException(ScheduleException e) { - ScheduleErrorResult errorResult = e.getScheduleErrorResult(); - return ApiResponse.onFailure(errorResult); + // IllegalArgumentException 처리 (잘못된 인자가 전달된 경우) + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + String errorMessage = "잘못된 요청입니다: " + e.getMessage(); + logError("IllegalArgumentException", errorMessage); + return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST, errorMessage); } - // Selection - @ExceptionHandler(SelectionException.class) - public ResponseEntity> handleSelectionException(SelectionException e) { - SelectionErrorResult errorResult = e.getSelectionErrorResult(); - return ApiResponse.onFailure(errorResult); + // ConstraintViolationException 처리 (쿼리 파라미터에 올바른 값이 들어오지 않은 경우) + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleValidationParameterError(ConstraintViolationException ex) { + String errorMessage = ex.getMessage(); + logError("ConstraintViolationException", errorMessage); + return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST, errorMessage); } - // User - @ExceptionHandler(UserException.class) - public ResponseEntity> handleUserException(UserException e) { - UserErrorResult errorResult = e.getUserErrorResult(); - return ApiResponse.onFailure(errorResult); + // MissingServletRequestParameterException 처리 (필수 쿼리 파라미터가 입력되지 않은 경우) + @Override + protected ResponseEntity handleMissingServletRequestParameter(MissingServletRequestParameterException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String errorMessage = "필수 파라미터 '" + ex.getParameterName() + "'가 없습니다."; + logError("MissingServletRequestParameterException", errorMessage); + return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST, errorMessage); } - // Token - @ExceptionHandler(TokenException.class) - public ResponseEntity> handleTokenException(TokenException e) { - TokenErrorResult errorResult = e.getTokenErrorResult(); - return ApiResponse.onFailure(errorResult); + // MethodArgumentNotValidException 처리 (RequestBody로 들어온 필드들의 유효성 검증에 실패한 경우) + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String combinedErrors = extractFieldErrors(ex.getBindingResult().getFieldErrors()); + logError("Validation error", combinedErrors); + return ApiResponse.onFailure(ErrorStatus._BAD_REQUEST, combinedErrors); } - // EventParticipation - @ExceptionHandler(EventParticipationException.class) - public ResponseEntity> handleEventParticipationException(EventParticipationException e) { - EventParticipationErrorResult errorResult = e.getEventParticipationErrorResult(); - return ApiResponse.onFailure(errorResult); + // NoHandlerFoundException 처리 (요청 경로에 매핑된 핸들러가 없는 경우) + @Override + protected ResponseEntity handleNoHandlerFoundException(NoHandlerFoundException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String errorMessage = "해당 경로에 대한 핸들러를 찾을 수 없습니다: " + ex.getRequestURL(); + logError("NoHandlerFoundException", errorMessage); + return ApiResponse.onFailure(ErrorStatus._NOT_FOUND_HANDLER, errorMessage); } - // AccessDeniedException 등 보안 관련 에러 처리 - @ExceptionHandler(SecurityException.class) - public ResponseEntity handleSecurityException(SecurityException e) { - log.error("SecurityException: {}", e.getMessage()); - return ResponseEntity.status(ErrorStatus._UNAUTHORIZED.getHttpStatus()) - .body(ErrorStatus._UNAUTHORIZED.getReasonHttpStatus()); + // HttpRequestMethodNotSupportedException 처리 (지원하지 않는 HTTP 메소드 요청이 들어온 경우) + @Override + protected ResponseEntity handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String errorMessage = "지원하지 않는 HTTP 메소드 요청입니다: " + ex.getMethod(); + logError("HttpRequestMethodNotSupportedException", errorMessage); + return ApiResponse.onFailure(ErrorStatus._METHOD_NOT_ALLOWED, errorMessage); + } + + // HttpMediaTypeNotSupportedException 처리 (지원하지 않는 미디어 타입 요청이 들어온 경우) + @Override + protected ResponseEntity handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, + HttpHeaders headers, + HttpStatusCode status, + WebRequest request) { + String errorMessage = "지원하지 않는 미디어 타입입니다: " + ex.getContentType(); + logError("HttpMediaTypeNotSupportedException", errorMessage); + return ApiResponse.onFailure(ErrorStatus._UNSUPPORTED_MEDIA_TYPE, errorMessage); } - // 기타 Exception 처리 + // 내부 서버 에러 처리 (500) @ExceptionHandler(Exception.class) - public ResponseEntity handleException(Exception e) { - log.error("Exception: {}", e.getMessage()); + public ResponseEntity> handleException(Exception e) { + // 서버 내부 에러 발생 시 로그에 예외 내용 기록 + logError(e.getMessage(), e); + return ApiResponse.onFailure(ErrorStatus._INTERNAL_SERVER_ERROR); + } - if (e instanceof IllegalArgumentException) { - return ResponseEntity.status(ErrorStatus._BAD_REQUEST.getHttpStatus()) - .body(ErrorStatus._BAD_REQUEST.getReasonHttpStatus()); - } + // 유효성 검증 오류 메시지 추출 메서드 (FieldErrors) + private String extractFieldErrors(List fieldErrors) { + return fieldErrors.stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", ")); + } - // 그 외 내부 서버 오류로 처리 - return ResponseEntity.status(ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus()) - .body(ErrorStatus._INTERNAL_SERVER_ERROR.getReasonHttpStatus()); + // 로그 기록 메서드 + private void logError(String message, Object errorDetails) { + log.error("{}: {}", message, errorDetails); } } \ No newline at end of file