diff --git a/backend/src/main/java/io/f1/backend/global/exception/CustomException.java b/backend/src/main/java/io/f1/backend/global/exception/CustomException.java index 2cd6202f..a2a14cdd 100644 --- a/backend/src/main/java/io/f1/backend/global/exception/CustomException.java +++ b/backend/src/main/java/io/f1/backend/global/exception/CustomException.java @@ -1,8 +1,17 @@ package io.f1.backend.global.exception; +import io.f1.backend.global.exception.errorcode.ErrorCode; + public class CustomException extends RuntimeException { - public CustomException(String message) { - super(message); + private final ErrorCode errorCode; + + public CustomException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; } } diff --git a/backend/src/main/java/io/f1/backend/global/exception/errorcode/AuthErrorCode.java b/backend/src/main/java/io/f1/backend/global/exception/errorcode/AuthErrorCode.java new file mode 100644 index 00000000..54ffb652 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/errorcode/AuthErrorCode.java @@ -0,0 +1,24 @@ +package io.f1.backend.global.exception.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum AuthErrorCode implements ErrorCode { + UNAUTHORIZED("E401001", HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."), + AUTH_SESSION_NOT_FOUND("E401002", HttpStatus.UNAUTHORIZED, "세션이 존재하지 않습니다. 로그인 후 이용해주세요."), + AUTH_SESSION_EXPIRED("E401003", HttpStatus.UNAUTHORIZED, "세션이 만료되었습니다. 다시 로그인해주세요."), + AUTH_SESSION_LOST("E401004", HttpStatus.UNAUTHORIZED, "세션 정보가 유실되었습니다. 다시 로그인해주세요."), + FORBIDDEN("E403001", HttpStatus.FORBIDDEN, "권한이 없습니다."), + + LOGIN_FAILED("E401005", HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 일치하지 않습니다."); + + private final String code; + + private final HttpStatus httpStatus; + + private final String message; +} diff --git a/backend/src/main/java/io/f1/backend/global/exception/errorcode/CommonErrorCode.java b/backend/src/main/java/io/f1/backend/global/exception/errorcode/CommonErrorCode.java new file mode 100644 index 00000000..a998d9c5 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/errorcode/CommonErrorCode.java @@ -0,0 +1,22 @@ +package io.f1.backend.global.exception.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum CommonErrorCode implements ErrorCode { + BAD_REQUEST_DATA("E400001", HttpStatus.BAD_REQUEST, "잘못된 요청 데이터입니다."), + INVALID_PAGINATION("E400006", HttpStatus.BAD_REQUEST, "page와 size는 1 이상의 정수여야 합니다."), + INTERNAL_SERVER_ERROR( + "E500001", HttpStatus.INTERNAL_SERVER_ERROR, "서버에러가 발생했습니다. 관리자에게 문의해주세요."), + INVALID_JSON_FORMAT("E400008", HttpStatus.BAD_REQUEST, "요청 형식이 올바르지 않습니다. JSON 문법을 확인해주세요."); + + private final String code; + + private final HttpStatus httpStatus; + + private final String message; +} diff --git a/backend/src/main/java/io/f1/backend/global/exception/errorcode/ErrorCode.java b/backend/src/main/java/io/f1/backend/global/exception/errorcode/ErrorCode.java new file mode 100644 index 00000000..a77eaa2d --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/errorcode/ErrorCode.java @@ -0,0 +1,12 @@ +package io.f1.backend.global.exception.errorcode; + +import org.springframework.http.HttpStatus; + +public interface ErrorCode { + + String getCode(); + + HttpStatus getHttpStatus(); + + String getMessage(); +} diff --git a/backend/src/main/java/io/f1/backend/global/exception/errorcode/QuestionErrorCode.java b/backend/src/main/java/io/f1/backend/global/exception/errorcode/QuestionErrorCode.java new file mode 100644 index 00000000..9b7c0c1e --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/errorcode/QuestionErrorCode.java @@ -0,0 +1,18 @@ +package io.f1.backend.global.exception.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum QuestionErrorCode implements ErrorCode { + QUESTION_NOT_FOUND("E404003", HttpStatus.NOT_FOUND, "존재하지 않는 문제입니다."); + + private final String code; + + private final HttpStatus httpStatus; + + private final String message; +} diff --git a/backend/src/main/java/io/f1/backend/global/exception/errorcode/QuizErrorCode.java b/backend/src/main/java/io/f1/backend/global/exception/errorcode/QuizErrorCode.java new file mode 100644 index 00000000..e03983b1 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/errorcode/QuizErrorCode.java @@ -0,0 +1,21 @@ +package io.f1.backend.global.exception.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum QuizErrorCode implements ErrorCode { + FILE_SIZE_TOO_LARGE("E400005", HttpStatus.BAD_REQUEST, "파일 크기가 너무 큽니다."), + UNSUPPORTED_MEDIA_TYPE("E415001", HttpStatus.UNSUPPORTED_MEDIA_TYPE, "지원하지 않는 파일 형식입니다."), + INVALID_FILTER("E400007", HttpStatus.BAD_REQUEST, "title 또는 creator 중 하나만 입력 가능합니다."), + QUIZ_NOT_FOUND("E404002", HttpStatus.NOT_FOUND, "존재하지 않는 퀴즈입니다."); + + private final String code; + + private final HttpStatus httpStatus; + + private final String message; +} diff --git a/backend/src/main/java/io/f1/backend/global/exception/errorcode/RoomErrorCode.java b/backend/src/main/java/io/f1/backend/global/exception/errorcode/RoomErrorCode.java new file mode 100644 index 00000000..191147ac --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/errorcode/RoomErrorCode.java @@ -0,0 +1,21 @@ +package io.f1.backend.global.exception.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum RoomErrorCode implements ErrorCode { + ROOM_USER_LIMIT_REACHED("E403002", HttpStatus.FORBIDDEN, "정원이 모두 찼습니다."), + ROOM_GAME_IN_PROGRESS("E403003", HttpStatus.FORBIDDEN, "게임이 진행 중 입니다."), + ROOM_NOT_FOUND("E404005", HttpStatus.NOT_FOUND, "존재하지 않는 방입니다."), + WRONG_PASSWORD("E401006", HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지않습니다."); + + private final String code; + + private final HttpStatus httpStatus; + + private final String message; +} diff --git a/backend/src/main/java/io/f1/backend/global/exception/errorcode/UserErrorCode.java b/backend/src/main/java/io/f1/backend/global/exception/errorcode/UserErrorCode.java new file mode 100644 index 00000000..a53f35a4 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/errorcode/UserErrorCode.java @@ -0,0 +1,22 @@ +package io.f1.backend.global.exception.errorcode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum UserErrorCode implements ErrorCode { + NICKNAME_EMPTY("E400002", HttpStatus.BAD_REQUEST, "닉네임은 필수 입력입니다."), + NICKNAME_TOO_LONG("E400003", HttpStatus.BAD_REQUEST, "닉네임은 6글자 이하로 입력해야 합니다."), + NICKNAME_NOT_ALLOWED("E400004", HttpStatus.BAD_REQUEST, "한글, 영문, 숫자만 입력해주세요."), + NICKNAME_CONFLICT("E409001", HttpStatus.CONFLICT, "중복된 닉네임입니다."), + USER_NOT_FOUND("E404001", HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."); + + private final String code; + + private final HttpStatus httpStatus; + + private final String message; +} diff --git a/backend/src/main/java/io/f1/backend/global/exception/handler/GlobalExceptionHandler.java b/backend/src/main/java/io/f1/backend/global/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 00000000..c5da75e0 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,66 @@ +package io.f1.backend.global.exception.handler; + +import io.f1.backend.global.exception.CustomException; +import io.f1.backend.global.exception.errorcode.CommonErrorCode; +import io.f1.backend.global.exception.errorcode.ErrorCode; +import io.f1.backend.global.exception.response.ErrorResponse; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(CustomException.class) + public ResponseEntity handleCustomException(CustomException e) { + log.warn(e.getMessage()); + ErrorCode errorCode = e.getErrorCode(); + + ErrorResponse response = new ErrorResponse(errorCode.getCode(), errorCode.getMessage()); + return new ResponseEntity<>(response, errorCode.getHttpStatus()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + log.warn("handleException: {}", e.getMessage()); + CommonErrorCode errorCode = CommonErrorCode.INTERNAL_SERVER_ERROR; + + ErrorResponse response = new ErrorResponse(errorCode.getCode(), errorCode.getMessage()); + return new ResponseEntity<>(response, errorCode.getHttpStatus()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException e) { + log.warn("MethodArgumentNotValidException: {}", e.getMessage()); + CommonErrorCode code = CommonErrorCode.BAD_REQUEST_DATA; + + String message = + e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .findFirst() + .orElse(code.getMessage()); + + ErrorResponse response = new ErrorResponse(code.getCode(), message); + + return new ResponseEntity<>(response, code.getHttpStatus()); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException( + HttpMessageNotReadableException e) { + log.warn("HttpMessageNotReadableException: {}", e.getMessage()); + CommonErrorCode code = CommonErrorCode.INVALID_JSON_FORMAT; + + ErrorResponse response = new ErrorResponse(code.getCode(), code.getMessage()); + + return new ResponseEntity<>(response, code.getHttpStatus()); + } +} diff --git a/backend/src/main/java/io/f1/backend/global/exception/response/ErrorResponse.java b/backend/src/main/java/io/f1/backend/global/exception/response/ErrorResponse.java new file mode 100644 index 00000000..42f423c0 --- /dev/null +++ b/backend/src/main/java/io/f1/backend/global/exception/response/ErrorResponse.java @@ -0,0 +1,12 @@ +package io.f1.backend.global.exception.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ErrorResponse { + + private final String code; + private final String message; +}