From 86bfab929e5524a2697030004a31b6323c475364 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Thu, 7 Aug 2025 13:55:32 +0900 Subject: [PATCH 1/4] fix: Add static factory method to create DiscordExceptionNotifyEventRequest without meta --- .../dto/discord/DiscordExceptionNotifyEventRequest.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/life/mosu/mosuserver/infra/notify/dto/discord/DiscordExceptionNotifyEventRequest.java b/src/main/java/life/mosu/mosuserver/infra/notify/dto/discord/DiscordExceptionNotifyEventRequest.java index fae6e139..f13b8b63 100644 --- a/src/main/java/life/mosu/mosuserver/infra/notify/dto/discord/DiscordExceptionNotifyEventRequest.java +++ b/src/main/java/life/mosu/mosuserver/infra/notify/dto/discord/DiscordExceptionNotifyEventRequest.java @@ -14,10 +14,17 @@ public static DiscordExceptionNotifyEventRequest of( return new DiscordExceptionNotifyEventRequest(exceptionCause, exceptionMessage, meta); } + public static DiscordExceptionNotifyEventRequest of( + String exceptionCause, + String exceptionMessage + ) { + return new DiscordExceptionNotifyEventRequest(exceptionCause, exceptionMessage, null); + } + public String getMessage() { return "❌ **알림 전송 실패**\n" + String.format("- ⚠️ exception Cause : `%s`\n", exceptionCause) + String.format("- 📨 exception Message: `%s`\n", exceptionMessage) - + String.format("- 📋 meta: `%s`", meta); + + String.format("- 📋 meta: `%s`", meta == null ? "없음" : meta); } } From 9529042aef3c93e28258f2a5c0f28b461f62bf96 Mon Sep 17 00:00:00 2001 From: KNU-K Date: Thu, 7 Aug 2025 13:57:20 +0900 Subject: [PATCH 2/4] feat: Add ErrorResponse class for standardized error handling --- .../global/exception/ErrorResponse.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/life/mosu/mosuserver/global/exception/ErrorResponse.java diff --git a/src/main/java/life/mosu/mosuserver/global/exception/ErrorResponse.java b/src/main/java/life/mosu/mosuserver/global/exception/ErrorResponse.java new file mode 100644 index 00000000..30fab963 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/exception/ErrorResponse.java @@ -0,0 +1,18 @@ +package life.mosu.mosuserver.global.exception; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + + private int status; + private String message; + private Object errors; + private String code; +} \ No newline at end of file From 3c94505831cccb7cc4fc7e30bbb29e391f07239b Mon Sep 17 00:00:00 2001 From: KNU-K Date: Thu, 7 Aug 2025 13:57:28 +0900 Subject: [PATCH 3/4] feat: Implement ErrorResponseFactory for creating standardized error responses --- .../exception/ErrorResponseFactory.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/life/mosu/mosuserver/global/exception/ErrorResponseFactory.java diff --git a/src/main/java/life/mosu/mosuserver/global/exception/ErrorResponseFactory.java b/src/main/java/life/mosu/mosuserver/global/exception/ErrorResponseFactory.java new file mode 100644 index 00000000..1fecfe6e --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/global/exception/ErrorResponseFactory.java @@ -0,0 +1,23 @@ +package life.mosu.mosuserver.global.exception; + +import org.springframework.http.HttpStatus; + +public class ErrorResponseFactory { + + public static ErrorResponse of(HttpStatus status, String message, Object errors) { + return ErrorResponse.builder() + .status(status.value()) + .message(message) + .errors(errors) + .build(); + } + + public static ErrorResponse of(HttpStatus status, String message, Object errors, String code) { + return ErrorResponse.builder() + .status(status.value()) + .message(message) + .errors(errors) + .code(code) + .build(); + } +} From 4d0a2e94408065b8765f03d354b1f71d6d10744e Mon Sep 17 00:00:00 2001 From: KNU-K Date: Thu, 7 Aug 2025 13:57:38 +0900 Subject: [PATCH 4/4] feat: Enhance GlobalExceptionHandler to include Discord notifications for exceptions --- .../exception/GlobalExceptionHandler.java | 180 +++++++++--------- 1 file changed, 92 insertions(+), 88 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 59bed19a..88a98c2c 100644 --- a/src/main/java/life/mosu/mosuserver/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/life/mosu/mosuserver/global/exception/GlobalExceptionHandler.java @@ -3,6 +3,9 @@ import jakarta.persistence.EntityNotFoundException; import java.util.LinkedHashMap; import java.util.Map; +import life.mosu.mosuserver.infra.notify.NotifyClientAdapter; +import life.mosu.mosuserver.infra.notify.dto.discord.DiscordExceptionNotifyEventRequest; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -15,145 +18,146 @@ @Slf4j @RestControllerAdvice +@RequiredArgsConstructor public class GlobalExceptionHandler { - /** - * DTO 유효성 검사 실패 - * - * @return 400 Bad Request - */ + private final NotifyClientAdapter notifier; + + private void notifyIfNeeded(Exception ex) { + try { + DiscordExceptionNotifyEventRequest request = DiscordExceptionNotifyEventRequest.of( + ex.getCause().toString(), + ex.getMessage() + ); + notifier.send(request); + } catch (Exception notifyEx) { + log.error("[Discord Notify Error]", notifyEx); + } + } + @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleMethodArgumentNotValidException( + public ResponseEntity handleMethodArgumentNotValidException( MethodArgumentNotValidException ex) { - Map response = new LinkedHashMap<>(); - Map errors = new LinkedHashMap<>(); - - errors.put("message", "입력값이 올바르지 않습니다."); + notifyIfNeeded(ex); + Map errors = new LinkedHashMap<>(); ex.getBindingResult().getFieldErrors().forEach(error -> { errors.put(error.getField(), error.getDefaultMessage()); }); - response.put("status", HttpStatus.BAD_REQUEST.value()); - response.put("message", "유효성 검사에 실패했습니다."); - response.put("errors", errors); + + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .message("유효성 검사에 실패했습니다.") + .errors(errors) + .build(); return ResponseEntity.badRequest().body(response); } - /** - * 잘못된 파라미터 요청 - * - * @return 400 Bad Request - */ @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleIllegalArgumentException( + public ResponseEntity handleIllegalArgumentException( IllegalArgumentException ex) { - Map response = new LinkedHashMap<>(); - response.put("status", HttpStatus.BAD_REQUEST.value()); - response.put("errors", "잘못된 요청입니다."); - response.put("message", ex.getMessage()); + notifyIfNeeded(ex); + + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .message(ex.getMessage()) + .errors("잘못된 요청입니다.") + .build(); return ResponseEntity.badRequest().body(response); } - /** - * 엔티티가 존재하지 않을 때 - * - * @return 404 Not Found - */ @ExceptionHandler(EntityNotFoundException.class) - public ResponseEntity> handleEntityNotFoundException( - EntityNotFoundException ex) { - Map response = new LinkedHashMap<>(); - response.put("status", HttpStatus.NOT_FOUND.value()); - response.put("message", "요청한 리소스가 존재하지 않습니다."); - response.put("errors", ex.getMessage()); + public ResponseEntity handleEntityNotFoundException(EntityNotFoundException ex) { + notifyIfNeeded(ex); + + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.NOT_FOUND.value()) + .message("요청한 리소스가 존재하지 않습니다.") + .errors(ex.getMessage()) + .build(); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } - /** - * 인증 예외 처리 - * - * @return 401 Unauthorized - */ @ExceptionHandler(AuthenticationException.class) - public ResponseEntity> handleAuthenticationException( - AuthenticationException ex) { - Map response = new LinkedHashMap<>(); - response.put("status", HttpStatus.UNAUTHORIZED.value()); - response.put("message", "인증에 실패했습니다"); - response.put("errors", ex.getMessage()); + public ResponseEntity handleAuthenticationException(AuthenticationException ex) { + notifyIfNeeded(ex); + + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.UNAUTHORIZED.value()) + .message("인증에 실패했습니다") + .errors(ex.getMessage()) + .build(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); } - /** - * 커스텀 AuthenticationException 처리 - * - * @return 401 Unauthorized - */ @ExceptionHandler(life.mosu.mosuserver.global.exception.AuthenticationException.class) - public ResponseEntity> handleAuthenticationException( + public ResponseEntity handleCustomAuthenticationException( life.mosu.mosuserver.global.exception.AuthenticationException ex) { - Map response = new LinkedHashMap<>(); - response.put("status", HttpStatus.UNAUTHORIZED.value()); - response.put("message", "인증에 실패했습니다"); - response.put("errors", ex.getMessage()); + notifyIfNeeded(ex); + + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.UNAUTHORIZED.value()) + .message("인증에 실패했습니다") + .errors(ex.getMessage()) + .build(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); } - /** - * 권한이 없는 경우 예외 처리 - * - * @return 403 Forbidden - */ @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity> handleAccessDeniedException( - AccessDeniedException ex) { - Map response = new LinkedHashMap<>(); - response.put("status", HttpStatus.FORBIDDEN.value()); - response.put("message", "인가를 실패 했습니다"); - response.put("errors", ex.getMessage()); + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) { + notifyIfNeeded(ex); + + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.FORBIDDEN.value()) + .message("인가를 실패 했습니다") + .errors(ex.getMessage()) + .build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); } - /** - * @return 409 Bad Request - * @RequestBody JSON 파싱 실패 (필드명 불일치, 데이터 타입 불일치, JSON 형식 오류 등) - */ @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity> handleHttpMessageNotReadableException( + public ResponseEntity handleHttpMessageNotReadableException( HttpMessageNotReadableException ex) { - Map response = new LinkedHashMap<>(); - response.put("status", HttpStatus.CONFLICT.value()); - response.put("message", "필드명 또는 데이터 타입이 일치하지 않습니다."); - response.put("errors", ex.getMessage()); + notifyIfNeeded(ex); + + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.CONFLICT.value()) + .message("필드명 또는 데이터 타입이 일치하지 않습니다.") + .errors(ex.getMessage()) + .build(); return ResponseEntity.status(HttpStatus.CONFLICT).body(response); } @ExceptionHandler(Exception.class) - public ResponseEntity> handleGeneralException(Exception ex) { - System.out.println("Exception: " + ex.getMessage()); - Map response = new LinkedHashMap<>(); - response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); - response.put("message", "서버 오류가 발생했습니다."); - response.put("errors", ErrorCode.SERVER_ERROR); + public ResponseEntity handleGeneralException(Exception ex) { + notifyIfNeeded(ex); + + ErrorResponse response = ErrorResponse.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .message("서버 오류가 발생했습니다.") + .errors(ErrorCode.SERVER_ERROR) + .build(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } @ExceptionHandler(CustomRuntimeException.class) - public ResponseEntity> handleCustomRuntimeException( - CustomRuntimeException ex) { - Map response = new LinkedHashMap<>(); - response.put("status", ex.getStatus().value()); - response.put("message", ex.getMessage()); - response.put("code", ex.getCode()); + public ResponseEntity handleCustomRuntimeException(CustomRuntimeException ex) { + notifyIfNeeded(ex); + + ErrorResponse response = ErrorResponse.builder() + .status(ex.getStatus().value()) + .message(ex.getMessage()) + .code(ex.getCode()) + .build(); return ResponseEntity.status(ex.getStatus()).body(response); } -} \ No newline at end of file +}