diff --git a/src/main/java/com/example/spot/common/api/code/status/ErrorStatus.java b/src/main/java/com/example/spot/common/api/code/status/ErrorStatus.java index 3c48557e..71cfdcc4 100644 --- a/src/main/java/com/example/spot/common/api/code/status/ErrorStatus.java +++ b/src/main/java/com/example/spot/common/api/code/status/ErrorStatus.java @@ -30,6 +30,8 @@ public enum ErrorStatus implements BaseErrorCode { _RSA_ERROR(HttpStatus.BAD_REQUEST, "COMMON4015", "RSA 에러가 발생했습니다."), _NOT_ADMIN(HttpStatus.FORBIDDEN, "COMMON4016", "관리자 권한이 없습니다."), _INVALID_ENUM_VALUE(HttpStatus.BAD_REQUEST, "COMMON4017", "유효하지 않은 Enum 값입니다."), + _EXTERNAL_API_ERROR(HttpStatus.BAD_GATEWAY, "COMMON4018", "외부 API 호출 중 오류가 발생했습니다."), + // 네이버 소셜 로그인 관련 에러 _NAVER_SIGN_IN_INTEGRATION_FAILED(HttpStatus.UNAUTHORIZED, "NAVER4001", "네이버 로그인 연동에 실패하였습니다."), _NAVER_ACCESS_TOKEN_ISSUANCE_FAILED(HttpStatus.UNAUTHORIZED, "NAVER4002", "네이버 액세스 토큰 발급에 실패하였습니다."), @@ -70,7 +72,7 @@ public enum ErrorStatus implements BaseErrorCode { _STUDY_OWNER_ONLY_CAN_WITHDRAW(HttpStatus.FORBIDDEN, "STUDY4007", "스터디장만 해당 API를 통해 스터디를 탈퇴할 수 있습니다."), _STUDY_NOT_RECRUITING(HttpStatus.BAD_REQUEST, "STUDY4007", "스터디 모집기한이 아닙니다."), _STUDY_APPLICANT_NOT_FOUND(HttpStatus.NOT_FOUND, "STUDY4009", "처리를 기다리는 스터디 신청을 찾을 수 없습니다."), - _STUDY_APPLY_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "STUDY4010","스터디 신청이 이미 처리된 회원입니다."), + _STUDY_APPLY_ALREADY_PROCESSED(HttpStatus.BAD_REQUEST, "STUDY4010", "스터디 신청이 이미 처리된 회원입니다."), _STUDY_OWNER_CANNOT_APPLY(HttpStatus.BAD_REQUEST, "STUDY4011", "스터디장은 스터디에 신청할 수 없습니다."), _STUDY_IS_FULL(HttpStatus.BAD_REQUEST, "STUDY4012", "스터디 인원이 가득 찼습니다."), _ONLY_STUDY_OWNER_CAN_ACCESS_APPLICANTS(HttpStatus.FORBIDDEN, "STUDY4013", "스터디장만 신청자 목록에 접근할 수 있습니다."), @@ -199,9 +201,8 @@ public enum ErrorStatus implements BaseErrorCode { _STUDY_TODO_IS_NOT_BELONG_TO_STUDY(HttpStatus.BAD_REQUEST, "TODO4003", "해당 투두 리스트가 해당 스터디에 속해있지 않습니다."), _ONLY_STUDY_MEMBER_CAN_ACCESS_TODO_LIST(HttpStatus.FORBIDDEN, "TODO4004", "스터디 멤버만 투두 리스트에 접근할 수 있습니다."), _TODO_LIST_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "SCHEDULE4004", "일정을 조회하려는 멤버가 스터디에 가입되지 않았습니다."), - _STUDY_TODO_NULL(HttpStatus.BAD_REQUEST, "TODO4005", "투두 리스트 아이디가 입력되지 않았습니다."),; - - ; + _STUDY_TODO_NULL(HttpStatus.BAD_REQUEST, "TODO4005", "투두 리스트 아이디가 입력되지 않았습니다."), + ;; private final HttpStatus httpStatus; diff --git a/src/main/java/com/example/spot/common/api/exception/ExceptionAdvice.java b/src/main/java/com/example/spot/common/api/exception/ExceptionAdvice.java index 94efbb44..fc6504d1 100644 --- a/src/main/java/com/example/spot/common/api/exception/ExceptionAdvice.java +++ b/src/main/java/com/example/spot/common/api/exception/ExceptionAdvice.java @@ -2,6 +2,7 @@ import com.example.spot.common.api.ApiResponse; import com.example.spot.common.api.code.status.ErrorStatus; +import com.example.spot.common.api.exception.base.ExternalApiException; import io.sentry.Sentry; import jakarta.validation.ConstraintViolationException; import java.util.List; @@ -22,6 +23,7 @@ public class ExceptionAdvice { /** * GeneralException 처리 + * * @param exception GeneralException * @return ApiResponse - GeneralException */ @@ -33,6 +35,7 @@ public ApiResponse baseExceptionHandle(GeneralException exception) /** * Exception 처리 + * * @param exception Exception * @return ApiResponse - INTERNAL_SERVER_ERROR */ @@ -44,6 +47,7 @@ public ApiResponse exceptionHandle(Exception exception) { /** * MethodArgumentTypeMismatchException 처리 - 잘못된 값 입력 + * * @param ex MethodArgumentTypeMismatchException * @return ApiResponse - BAD_VALUE_REQUEST */ @@ -56,17 +60,20 @@ public ApiResponse handleMethodArgumentTypeMismatchException(MethodArgumentTypeM /** * ConstraintViolationException 처리 + * * @param exception ConstraintViolationException 객체 * @return 클라이언트에게 반환할 ApiResponse 객체 */ @ExceptionHandler(ConstraintViolationException.class) - public ResponseEntity>> handleConstraintViolationException(ConstraintViolationException exception) { + public ResponseEntity>> handleConstraintViolationException( + ConstraintViolationException exception) { // 모든 필드 오류 메시지를 수집 List errors = exception.getConstraintViolations() - .stream() - // 각 ConstraintViolation에서 필드 경로와 메시지를 포맷하여 수집 - .map(constraintViolation -> String.format("'%s': %s ", constraintViolation.getPropertyPath(), constraintViolation.getMessage())) - .collect(Collectors.toList()); + .stream() + // 각 ConstraintViolation에서 필드 경로와 메시지를 포맷하여 수집 + .map(constraintViolation -> String.format("'%s': %s ", constraintViolation.getPropertyPath(), + constraintViolation.getMessage())) + .collect(Collectors.toList()); // 모든 에러 메시지를 하나의 문자열로 결합 String errorMessage = String.join(", ", errors); @@ -75,9 +82,9 @@ public ResponseEntity>> handleConstraintViolationExcept // ApiResponse 객체를 생성하여 오류 정보를 포함 ApiResponse> response = ApiResponse.onFailure( - ErrorStatus._BAD_REQUEST.getCode(), // HTTP 상태 코드 - ErrorStatus._BAD_REQUEST.getMessage(), // 오류 메시지 - errors // 오류 목록 + ErrorStatus._BAD_REQUEST.getCode(), // HTTP 상태 코드 + ErrorStatus._BAD_REQUEST.getMessage(), // 오류 메시지 + errors // 오류 목록 ); // BAD_REQUEST 상태와 함께 ApiResponse 반환 @@ -86,7 +93,8 @@ public ResponseEntity>> handleConstraintViolationExcept @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity>> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) { + public ResponseEntity>> handleMethodArgumentNotValidException( + MethodArgumentNotValidException exception) { List errors = exception.getBindingResult() .getFieldErrors() .stream() @@ -104,6 +112,22 @@ public ResponseEntity>> handleMethodArgumentNotValidExc return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); } + @ExceptionHandler(ExternalApiException.class) + public ResponseEntity> handleExternalApiException(ExternalApiException exception) { + String errorMessage = String.format("외부 API 호출 실패: %s", exception.getMessage()); + log.error("ExternalApiException. error message: {}", errorMessage, exception); + + captureException(exception); + + ApiResponse response = ApiResponse.onFailure( + ErrorStatus._EXTERNAL_API_ERROR.getCode(), + ErrorStatus._EXTERNAL_API_ERROR.getMessage(), + exception.getResponseBody() + ); + + return new ResponseEntity<>(response, HttpStatus.BAD_GATEWAY); + } + private static String validateGender(FieldError fieldError) { String field = fieldError.getField(); String message = fieldError.getDefaultMessage(); diff --git a/src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java b/src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java index e2bf31e7..82fd3551 100644 --- a/src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java +++ b/src/main/java/com/example/spot/common/api/exception/base/ExternalApiException.java @@ -1,7 +1,15 @@ package com.example.spot.common.api.exception.base; public class ExternalApiException extends RuntimeException { - public ExternalApiException(String s, Exception e) { + + private final String responseBody; + + public ExternalApiException(String s, String body, Exception e) { super(s, e); + this.responseBody = body; + } + + public String getResponseBody() { + return responseBody; } } diff --git a/src/main/java/com/example/spot/common/infrastructure/feign/SafeFeignExecutor.java b/src/main/java/com/example/spot/common/infrastructure/feign/SafeFeignExecutor.java index 986462be..b5e7fe8c 100644 --- a/src/main/java/com/example/spot/common/infrastructure/feign/SafeFeignExecutor.java +++ b/src/main/java/com/example/spot/common/infrastructure/feign/SafeFeignExecutor.java @@ -14,9 +14,10 @@ public static T run(Supplier call) { return call.get(); } catch (FeignException e) { String message = e.getMessage() != null ? e.getMessage() : ""; + String body = e.contentUTF8(); String masked = mask(message); throw new ExternalApiException( - "Feign API 호출 실패(" + e.status() + "): " + masked, e + "Feign API 호출 실패(" + e.status() + "): " + masked, body, e ); } }