Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -13,27 +20,84 @@
@RestControllerAdvice
public class GlobalExceptionHandler {

private static final String ERROR_LOG_HEADER = "ERROR: ";

@ExceptionHandler(UserJwtException.class)
public ResponseEntity<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> handleMissingServletRequestParameter(MissingServletRequestParameterException e) {
log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e);
return ResponseEntity.badRequest().body(
ErrorResponse.of("필수 파라미터가 누락되었습니다.",
"파라미터 '" + e.getParameterName() + "'이(가) 필요합니다."));
}

@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseEntity<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> 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<ErrorResponse> handleBusinessException(BusinessException e) {
log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e);
return ResponseEntity.badRequest().body(ErrorResponse.of(e.getErrorCode()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public ResponseEntity<SuccessResponse<QueryIncubatingEggResponse>> getIncubating
QueryIncubatingEggResponse response = incubatingEggQueryUsecase.execute(userId);
return ResponseEntity.ok().body(
SuccessResponse.of(
UserHttpResponseCode.MY_PAGE_DATA_FETCHED,
UserHttpResponseCode.MY_INCUBATING_EGG_FETCHED,
response
));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ public class MainViewController {
public ResponseEntity<SuccessResponse<MainViewResponse>> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,35 @@
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;
}

@Override
public String getCode() {
return this.code;
return this.name();
}

@Override
Expand All @@ -43,7 +47,7 @@ public String getLogMessage() {

@Override
public HttpStatus getHttpStatusCode() {
return null;
return this.code;
}
}

Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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))
Expand All @@ -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));
}
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down