|
| 1 | +package com.loopers.interfaces.api; |
| 2 | + |
| 3 | +import com.fasterxml.jackson.databind.JsonMappingException; |
| 4 | +import com.fasterxml.jackson.databind.exc.InvalidFormatException; |
| 5 | +import com.fasterxml.jackson.databind.exc.MismatchedInputException; |
| 6 | +import com.loopers.support.error.CoreException; |
| 7 | +import com.loopers.support.error.ErrorType; |
| 8 | +import lombok.extern.slf4j.Slf4j; |
| 9 | +import org.springframework.http.ResponseEntity; |
| 10 | +import org.springframework.http.converter.HttpMessageNotReadableException; |
| 11 | +import org.springframework.web.bind.MissingServletRequestParameterException; |
| 12 | +import org.springframework.web.bind.annotation.ExceptionHandler; |
| 13 | +import org.springframework.web.bind.annotation.RestControllerAdvice; |
| 14 | +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; |
| 15 | +import org.springframework.web.server.ServerWebInputException; |
| 16 | +import org.springframework.web.servlet.resource.NoResourceFoundException; |
| 17 | + |
| 18 | +import java.util.Arrays; |
| 19 | +import java.util.regex.Matcher; |
| 20 | +import java.util.regex.Pattern; |
| 21 | +import java.util.stream.Collectors; |
| 22 | + |
| 23 | +@RestControllerAdvice |
| 24 | +@Slf4j |
| 25 | +public class ApiControllerAdvice { |
| 26 | + @ExceptionHandler |
| 27 | + public ResponseEntity<ApiResponse<?>> handle(CoreException e) { |
| 28 | + log.warn("CoreException : {}", e.getCustomMessage() != null ? e.getCustomMessage() : e.getMessage(), e); |
| 29 | + return failureResponse(e.getErrorType(), e.getCustomMessage()); |
| 30 | + } |
| 31 | + |
| 32 | + @ExceptionHandler |
| 33 | + public ResponseEntity<ApiResponse<?>> handleBadRequest(MethodArgumentTypeMismatchException e) { |
| 34 | + String name = e.getName(); |
| 35 | + String type = e.getRequiredType() != null ? e.getRequiredType().getSimpleName() : "unknown"; |
| 36 | + String value = e.getValue() != null ? e.getValue().toString() : "null"; |
| 37 | + String message = String.format("요청 파라미터 '%s' (타입: %s)의 값 '%s'이(가) 잘못되었습니다.", name, type, value); |
| 38 | + return failureResponse(ErrorType.BAD_REQUEST, message); |
| 39 | + } |
| 40 | + |
| 41 | + @ExceptionHandler |
| 42 | + public ResponseEntity<ApiResponse<?>> handleBadRequest(MissingServletRequestParameterException e) { |
| 43 | + String name = e.getParameterName(); |
| 44 | + String type = e.getParameterType(); |
| 45 | + String message = String.format("필수 요청 파라미터 '%s' (타입: %s)가 누락되었습니다.", name, type); |
| 46 | + return failureResponse(ErrorType.BAD_REQUEST, message); |
| 47 | + } |
| 48 | + |
| 49 | + @ExceptionHandler |
| 50 | + public ResponseEntity<ApiResponse<?>> handleBadRequest(HttpMessageNotReadableException e) { |
| 51 | + String errorMessage; |
| 52 | + Throwable rootCause = e.getRootCause(); |
| 53 | + |
| 54 | + if (rootCause instanceof InvalidFormatException invalidFormat) { |
| 55 | + String fieldName = invalidFormat.getPath().stream() |
| 56 | + .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") |
| 57 | + .collect(Collectors.joining(".")); |
| 58 | + |
| 59 | + String valueIndicationMessage = ""; |
| 60 | + if (invalidFormat.getTargetType().isEnum()) { |
| 61 | + Class<?> enumClass = invalidFormat.getTargetType(); |
| 62 | + String enumValues = Arrays.stream(enumClass.getEnumConstants()) |
| 63 | + .map(Object::toString) |
| 64 | + .collect(Collectors.joining(", ")); |
| 65 | + valueIndicationMessage = "사용 가능한 값 : [" + enumValues + "]"; |
| 66 | + } |
| 67 | + |
| 68 | + String expectedType = invalidFormat.getTargetType().getSimpleName(); |
| 69 | + Object value = invalidFormat.getValue(); |
| 70 | + |
| 71 | + errorMessage = String.format("필드 '%s'의 값 '%s'이(가) 예상 타입(%s)과 일치하지 않습니다. %s", |
| 72 | + fieldName, value, expectedType, valueIndicationMessage); |
| 73 | + |
| 74 | + } else if (rootCause instanceof MismatchedInputException mismatchedInput) { |
| 75 | + String fieldPath = mismatchedInput.getPath().stream() |
| 76 | + .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") |
| 77 | + .collect(Collectors.joining(".")); |
| 78 | + errorMessage = String.format("필수 필드 '%s'이(가) 누락되었습니다.", fieldPath); |
| 79 | + |
| 80 | + } else if (rootCause instanceof JsonMappingException jsonMapping) { |
| 81 | + String fieldPath = jsonMapping.getPath().stream() |
| 82 | + .map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "?") |
| 83 | + .collect(Collectors.joining(".")); |
| 84 | + errorMessage = String.format("필드 '%s'에서 JSON 매핑 오류가 발생했습니다: %s", |
| 85 | + fieldPath, jsonMapping.getOriginalMessage()); |
| 86 | + |
| 87 | + } else { |
| 88 | + errorMessage = "요청 본문을 처리하는 중 오류가 발생했습니다. JSON 메세지 규격을 확인해주세요."; |
| 89 | + } |
| 90 | + |
| 91 | + return failureResponse(ErrorType.BAD_REQUEST, errorMessage); |
| 92 | + } |
| 93 | + |
| 94 | + @ExceptionHandler |
| 95 | + public ResponseEntity<ApiResponse<?>> handleBadRequest(ServerWebInputException e) { |
| 96 | + String missingParams = extractMissingParameter(e.getReason() != null ? e.getReason() : ""); |
| 97 | + if (!missingParams.isEmpty()) { |
| 98 | + String message = String.format("필수 요청 값 '%s'가 누락되었습니다.", missingParams); |
| 99 | + return failureResponse(ErrorType.BAD_REQUEST, message); |
| 100 | + } else { |
| 101 | + return failureResponse(ErrorType.BAD_REQUEST, null); |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + @ExceptionHandler |
| 106 | + public ResponseEntity<ApiResponse<?>> handleNotFound(NoResourceFoundException e) { |
| 107 | + return failureResponse(ErrorType.NOT_FOUND, null); |
| 108 | + } |
| 109 | + |
| 110 | + @ExceptionHandler |
| 111 | + public ResponseEntity<ApiResponse<?>> handle(Throwable e) { |
| 112 | + log.error("Exception : {}", e.getMessage(), e); |
| 113 | + return failureResponse(ErrorType.INTERNAL_ERROR, null); |
| 114 | + } |
| 115 | + |
| 116 | + private String extractMissingParameter(String message) { |
| 117 | + Pattern pattern = Pattern.compile("'(.+?)'"); |
| 118 | + Matcher matcher = pattern.matcher(message); |
| 119 | + return matcher.find() ? matcher.group(1) : ""; |
| 120 | + } |
| 121 | + |
| 122 | + private ResponseEntity<ApiResponse<?>> failureResponse(ErrorType errorType, String errorMessage) { |
| 123 | + return ResponseEntity.status(errorType.getStatus()) |
| 124 | + .body(ApiResponse.fail(errorType.getCode(), errorMessage != null ? errorMessage : errorType.getMessage())); |
| 125 | + } |
| 126 | +} |
0 commit comments