diff --git a/src/main/java/org/runimo/runimo/auth/filters/JwtAuthenticationFilter.java b/src/main/java/org/runimo/runimo/auth/filters/JwtAuthenticationFilter.java index 5cc7a969..8f0094b6 100644 --- a/src/main/java/org/runimo/runimo/auth/filters/JwtAuthenticationFilter.java +++ b/src/main/java/org/runimo/runimo/auth/filters/JwtAuthenticationFilter.java @@ -45,7 +45,7 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull Ht } filterChain.doFilter(request, response); } catch (Exception e) { - log.warn("[ERROR]JWT broken : {}", e.getMessage()); + log.warn("[ERROR] JWT broken : {}", e.getMessage()); setErrorResponse(UserErrorCode.JWT_BROKEN, response); } finally { SecurityContextHolder.clearContext(); diff --git a/src/main/java/org/runimo/runimo/auth/verifier/KakaoTokenVerifier.java b/src/main/java/org/runimo/runimo/auth/verifier/KakaoTokenVerifier.java index 93e76b57..f5d0c97b 100644 --- a/src/main/java/org/runimo/runimo/auth/verifier/KakaoTokenVerifier.java +++ b/src/main/java/org/runimo/runimo/auth/verifier/KakaoTokenVerifier.java @@ -9,6 +9,8 @@ import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.runimo.runimo.auth.repository.OAuthTokenRepository; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.exceptions.UserJwtException; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -88,7 +90,7 @@ public DecodedJWT verifyToken(DecodedJWT token) { .build() .verify(token); } catch (JWTVerificationException exception) { - throw new IllegalArgumentException("ID token verification failed", exception); + throw new UserJwtException(UserHttpResponseCode.JWT_TOKEN_BROKEN,exception.getMessage()); } } diff --git a/src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java b/src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java index f4b2becb..931f60c8 100644 --- a/src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java @@ -1,9 +1,16 @@ package org.runimo.runimo.exceptions; +import jakarta.persistence.LockTimeoutException; import lombok.extern.slf4j.Slf4j; import org.runimo.runimo.common.response.ErrorResponse; import org.runimo.runimo.user.exceptions.SignUpException; +import org.runimo.runimo.user.exceptions.UserJwtException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +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; @@ -13,27 +20,84 @@ @RestControllerAdvice public class GlobalExceptionHandler { + private static final String ERROR_LOG_HEADER = "ERROR: "; + + @ExceptionHandler(UserJwtException.class) + public ResponseEntity handleUserJwtException(UserJwtException e) { + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); + return ResponseEntity.status(e.getHttpStatusCode()).body(ErrorResponse.of(e.getErrorCode())); + } + @ExceptionHandler(SignUpException.class) public ResponseEntity handleSignUpException(SignUpException e) { - log.debug("ERROR: {}}", e.getMessage(), e); - return ResponseEntity.badRequest().body(ErrorResponse.of(e.getErrorCode())); + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); + return ResponseEntity.status(e.getHttpStatusCode()).body(ErrorResponse.of(e.getErrorCode())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException e) { + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); + StringBuilder detailMessage = new StringBuilder(); + e.getBindingResult().getAllErrors().forEach(error -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + detailMessage.append(fieldName).append(": ").append(errorMessage).append(", "); + }); + String details = !detailMessage.isEmpty() ? + detailMessage.substring(0, detailMessage.length() - 2) : ""; + return ResponseEntity.badRequest().body(ErrorResponse.of("유효성 검증에 실패했습니다.", details)); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity handleMissingServletRequestParameter(MissingServletRequestParameterException e) { + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); + return ResponseEntity.badRequest().body( + ErrorResponse.of("필수 파라미터가 누락되었습니다.", + "파라미터 '" + e.getParameterName() + "'이(가) 필요합니다.")); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e) { + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body( + ErrorResponse.of("지원하지 않는 HTTP 메소드입니다.", + e.getMethod() + " 메소드는 지원되지 않습니다.")); + } + + /** + * XLOCK 타임아웃 에러 처리기 + * 타임아웃이 발생하면 LockTimeoutException이 발생한다. + * TODO : 타임아웃 발생시 디스코드로 알림. + * */ + @ExceptionHandler(LockTimeoutException.class) + public ResponseEntity handleLockTimeoutException(LockTimeoutException e) { + log.error("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body( + ErrorResponse.of("잠시 후 다시 시도해주세요", "대기 시간 초과")); } @ExceptionHandler(NoSuchElementException.class) public ResponseEntity handleNoSuchElementException(NoSuchElementException e) { - log.debug("ERROR: {}}", e.getMessage(), e); + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); return ResponseEntity.notFound().build(); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { - log.debug("ERROR: {}}", e.getMessage(), e); + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); return ResponseEntity.badRequest().body(ErrorResponse.of("잘못된 요청입니다.", e.getMessage())); } @ExceptionHandler(IllegalStateException.class) public ResponseEntity handleIllegalStateException(IllegalStateException e) { - log.debug("ERROR: {}}", e.getMessage(), e); + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); return ResponseEntity.badRequest().body(ErrorResponse.of("잘못된 요청입니다.", e.getMessage())); } + + // Root 서비스 에러 처리 + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleBusinessException(BusinessException e) { + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); + return ResponseEntity.badRequest().body(ErrorResponse.of(e.getErrorCode())); + } } diff --git a/src/main/java/org/runimo/runimo/user/controller/EggController.java b/src/main/java/org/runimo/runimo/user/controller/EggController.java index 1d4660e8..3a1718a4 100644 --- a/src/main/java/org/runimo/runimo/user/controller/EggController.java +++ b/src/main/java/org/runimo/runimo/user/controller/EggController.java @@ -83,7 +83,7 @@ public ResponseEntity> getIncubating QueryIncubatingEggResponse response = incubatingEggQueryUsecase.execute(userId); return ResponseEntity.ok().body( SuccessResponse.of( - UserHttpResponseCode.MY_PAGE_DATA_FETCHED, + UserHttpResponseCode.MY_INCUBATING_EGG_FETCHED, response )); } diff --git a/src/main/java/org/runimo/runimo/user/controller/MainViewController.java b/src/main/java/org/runimo/runimo/user/controller/MainViewController.java index ceb542a2..fd254e51 100644 --- a/src/main/java/org/runimo/runimo/user/controller/MainViewController.java +++ b/src/main/java/org/runimo/runimo/user/controller/MainViewController.java @@ -32,6 +32,6 @@ public class MainViewController { public ResponseEntity> queryMainView( @UserId Long userId) { MainViewResponse response = mainViewQueryUsecase.execute(userId); - return ResponseEntity.ok(SuccessResponse.of(UserHttpResponseCode.MY_PAGE_DATA_FETCHED, response)); + return ResponseEntity.ok(SuccessResponse.of(UserHttpResponseCode.MAIN_PAGE_DATA_FETCHED, response)); } } diff --git a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java index 56792272..92044dc3 100644 --- a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java +++ b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java @@ -4,23 +4,27 @@ import org.springframework.http.HttpStatus; public enum UserHttpResponseCode implements CustomResponseCode { - MY_PAGE_DATA_FETCHED("USH2001", "마이페이지 데이터 조회 성공", "마이페이지 데이터 조회 성공"), - SIGNUP_SUCCESS("USH2002", "회원가입 성공", "회원가입 성공"), - LOGIN_SUCCESS("USH2003", "로그인 성공", "로그인 성공"), - REFRESH_SUCCESS("USH2004", "토큰 재발급 성공", "토큰 재발급 성공"), - - USE_ITEM_SUCCESS("USH2005", "아이템 사용 성공", "아이템 사용 성공"), - REGISTER_EGG_SUCCESS("USH2006", "부화기 등록 성공", "부화기 등록 성공"), - USE_LOVE_POINT_SUCCESS("USH2007","애정 사용 성공" , "애정 사용 성공"), - - LOGIN_FAIL_NOT_SIGN_IN("UEH4041", "로그인 실패 - 회원가입하지 않은 사용자", "로그인 실패 - 회원가입하지 않은 사용자"), - SIGNIN_FAIL_ALREADY_EXIST("UEH4042", "로그인 실패 - 이미 존재하는 사용자", "로그인 실패 - 이미 존재하는 사용자"),; - - private final String code; + MY_PAGE_DATA_FETCHED(HttpStatus.OK, "마이페이지 데이터 조회 성공", "마이페이지 데이터 조회 성공"), + MAIN_PAGE_DATA_FETCHED(HttpStatus.OK, "메인페이지 데이터 조회 성공", "메인페이지 데이터 조회 성공"), + SIGNUP_SUCCESS(HttpStatus.CREATED, "회원가입 성공", "회원가입 성공"), + LOGIN_SUCCESS(HttpStatus.OK, "로그인 성공", "로그인 성공"), + REFRESH_SUCCESS(HttpStatus.OK, "토큰 재발급 성공", "토큰 재발급 성공"), + + USE_ITEM_SUCCESS(HttpStatus.OK, "아이템 사용 성공", "아이템 사용 성공"), + REGISTER_EGG_SUCCESS(HttpStatus.CREATED, "부화기 등록 성공", "부화기 등록 성공"), + USE_LOVE_POINT_SUCCESS(HttpStatus.OK,"애정 사용 성공" , "애정 사용 성공"), + MY_INCUBATING_EGG_FETCHED(HttpStatus.OK, "부화기중인 알 조회 성공", "부화중인 알 조회 성공"), + + LOGIN_FAIL_NOT_SIGN_IN(HttpStatus.UNAUTHORIZED, "로그인 실패 - 회원가입하지 않은 사용자", "로그인 실패 - 회원가입하지 않은 사용자"), + SIGNIN_FAIL_ALREADY_EXIST(HttpStatus.CONFLICT, "로그인 실패 - 이미 존재하는 사용자", "로그인 실패 - 이미 존재하는 사용자"), + JWT_TOKEN_BROKEN(HttpStatus.BAD_REQUEST, "JWT 토큰이 손상되었습니다", "JWT 토큰이 손상되었습니다"),; + + private final HttpStatus code; private final String clientMessage; private final String logMessage; - UserHttpResponseCode(String code, String clientMessage, String logMessage) { + + UserHttpResponseCode(HttpStatus code, String clientMessage, String logMessage) { this.code = code; this.clientMessage = clientMessage; this.logMessage = logMessage; @@ -28,7 +32,7 @@ public enum UserHttpResponseCode implements CustomResponseCode { @Override public String getCode() { - return this.code; + return this.name(); } @Override @@ -43,7 +47,7 @@ public String getLogMessage() { @Override public HttpStatus getHttpStatusCode() { - return null; + return this.code; } } diff --git a/src/main/java/org/runimo/runimo/user/exceptions/UserJwtException.java b/src/main/java/org/runimo/runimo/user/exceptions/UserJwtException.java new file mode 100644 index 00000000..1d7784ee --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/exceptions/UserJwtException.java @@ -0,0 +1,14 @@ +package org.runimo.runimo.user.exceptions; + +import org.runimo.runimo.exceptions.BusinessException; +import org.runimo.runimo.exceptions.code.CustomResponseCode; + +public class UserJwtException extends BusinessException { + protected UserJwtException(CustomResponseCode errorCode) { + super(errorCode); + } + + public UserJwtException(CustomResponseCode errorCode, String logMessage) { + super(errorCode, logMessage); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecaseImpl.java index 17b3a90f..484a78a8 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/user/service/usecases/auth/UserOAuthUsecaseImpl.java @@ -19,8 +19,6 @@ import org.runimo.runimo.user.service.dtos.UserSignupCommand; import org.springframework.stereotype.Service; -import java.util.NoSuchElementException; - @Service @RequiredArgsConstructor public class UserOAuthUsecaseImpl implements UserOAuthUsecase { @@ -37,7 +35,6 @@ public AuthResponse validateAndLogin(final String rawToken, final SocialProvider String pid = oidcService.validateOidcTokenAndGetProviderId(token, provider); OAuthInfo oAuthInfo = oAuthInfoRepository.findByProviderAndProviderId(provider, pid) .orElseThrow(() -> new SignUpException(UserHttpResponseCode.LOGIN_FAIL_NOT_SIGN_IN)); - //oidcNonceService.useNonce(token, provider); TokenPair tokenPair = jwtfactory.generateTokenPair(oAuthInfo.getUser()); return new AuthResponse(oAuthInfo.getUser(), tokenPair); } diff --git a/src/test/java/org/runimo/runimo/records/api/RecordAcceptanceTest.java b/src/test/java/org/runimo/runimo/records/api/RecordAcceptanceTest.java index a6cf6e64..f47ddb57 100644 --- a/src/test/java/org/runimo/runimo/records/api/RecordAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/records/api/RecordAcceptanceTest.java @@ -154,7 +154,7 @@ void tearDown() { .then() .log().all() .statusCode(200) - .body("code", equalTo("USH2001")) + .body("code", equalTo("MY_PAGE_DATA_FETCHED")) .body("payload.daily_stats.size()", equalTo(7)) .body("payload.daily_stats[0].date", equalTo("2025-03-31")) .body("payload.daily_stats[0].distance", equalTo(1000)) @@ -189,7 +189,7 @@ void tearDown() { .then() .log().ifValidationFails() .statusCode(200) - .body("code", equalTo("USH2001")) + .body("code", equalTo("MY_PAGE_DATA_FETCHED")) .body("payload.daily_stats.size()", equalTo(7)) .body("payload.daily_stats[0].date", equalTo("2025-03-31")) .body("payload.daily_stats[0].distance", equalTo(1000)) diff --git a/src/test/java/org/runimo/runimo/user/api/IncubatingEggAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/IncubatingEggAcceptanceTest.java index 3ed5912d..269603b9 100644 --- a/src/test/java/org/runimo/runimo/user/api/IncubatingEggAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/IncubatingEggAcceptanceTest.java @@ -18,8 +18,11 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.jdbc.Sql; +import java.util.Optional; + import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") @@ -59,7 +62,7 @@ void tearDown() { .then() .log().all() .statusCode(200) - .body("code", equalTo("USH2001")) + .body("code", equalTo("MY_INCUBATING_EGG_FETCHED")) .body("payload.incubating_eggs.size()", greaterThan(0)) .body("payload.incubating_eggs[0].name", equalTo("마당알")) .body("payload.incubating_eggs[0].id", equalTo(1)) @@ -84,7 +87,7 @@ void tearDown() { .then() .log().all() .statusCode(HttpStatus.CREATED.value()) - .body("code", equalTo("USH2006")) + .body("code", equalTo("REGISTER_EGG_SUCCESS")) .body("payload.current_love_point_amount", equalTo(0)) .body("payload.required_love_point_amount", equalTo(100)); } @@ -103,7 +106,7 @@ void tearDown() { .then() .log().all() .statusCode(200) - .body("code", equalTo("USH2007")) + .body("code", equalTo("USE_LOVE_POINT_SUCCESS")) .body("payload.current_love_point_amount", equalTo(70)) .body("payload.required_love_point_amount", equalTo(100)) .body("payload.egg_hatchable", equalTo(false)); diff --git a/src/test/java/org/runimo/runimo/user/api/MainViewAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/MainViewAcceptanceTest.java index d1e67bd4..bc3c277a 100644 --- a/src/test/java/org/runimo/runimo/user/api/MainViewAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/MainViewAcceptanceTest.java @@ -57,7 +57,7 @@ void tearDown() { .then() .log().ifValidationFails() .statusCode(200) - .body("code", equalTo("USH2001")) + .body("code", equalTo("MAIN_PAGE_DATA_FETCHED")) .body("payload.nickname", equalTo("Daniel")) .body("payload.profile_image_url", equalTo("https://example.com/images/user1.png")) .body("payload.total_running_count", equalTo(2)) diff --git a/src/test/java/org/runimo/runimo/user/api/MyPageAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/MyPageAcceptanceTest.java index 38e962a6..a15d8ad4 100644 --- a/src/test/java/org/runimo/runimo/user/api/MyPageAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/MyPageAcceptanceTest.java @@ -56,7 +56,7 @@ void tearDown() { .then() .log().all() .statusCode(HttpStatus.OK.value()) - .body("code", equalTo("USH2001")) + .body("code", equalTo("MY_PAGE_DATA_FETCHED")) .body("payload.nickname", equalTo("Daniel")) .body("payload.profile_image_url", equalTo("https://example.com/images/user1.png")) .body("payload.total_distance_in_meters", equalTo(10000)) diff --git a/src/test/java/org/runimo/runimo/user/controller/MainViewControllerTest.java b/src/test/java/org/runimo/runimo/user/controller/MainViewControllerTest.java index 2a89ebcf..3b8f830e 100644 --- a/src/test/java/org/runimo/runimo/user/controller/MainViewControllerTest.java +++ b/src/test/java/org/runimo/runimo/user/controller/MainViewControllerTest.java @@ -54,7 +54,7 @@ class MainViewControllerTest { .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.code").value("USH2001")) + .andExpect(jsonPath("$.code").value("MAIN_PAGE_DATA_FETCHED")) .andExpect(jsonPath("$.payload.nickname").value("Daniel")) .andExpect(jsonPath("$.payload.profile_image_url").value("https://example.com/images/user1.png")) .andExpect(jsonPath("$.payload.total_running_count").value(100))