-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT] 예약 서버 인증 및 공통 응답 정의 (#279) #280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f4f7576
1a7c47f
5766ccf
749a4c8
0036ebb
b7ba3ca
3693d02
feafcd8
f8b3806
90fbac1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package net.catsnap.CatsnapReservation.shared; | ||
|
|
||
| import org.springframework.http.HttpStatusCode; | ||
|
|
||
| /** | ||
| * 결과 코드 인터페이스 | ||
| * <p> | ||
| * 모든 레이어에서 공통으로 사용하는 결과 코드 인터페이스입니다. | ||
| * 도메인 에러와 프레젠테이션 에러 모두 이 인터페이스를 구현합니다. | ||
| * </p> | ||
| * <p> | ||
| * 결과 코드 형식: | ||
| * <ul> | ||
| * <li>성공 코드는 "S"로 시작 (예: SC000, SM001)</li> | ||
| * <li>에러 코드는 "E"로 시작 (예: EC000, EM001)</li> | ||
| * <li>2번째 문자는 도메인 카테고리를 나타냄 - R : reservation 모듈</li> | ||
| * </ul> | ||
| * </p> | ||
| */ | ||
| public interface ResultCode { | ||
|
|
||
| /** | ||
| * HTTP 상태 코드를 반환합니다. | ||
| * <p> | ||
| * ResponseEntity의 상태 코드로 사용됩니다. | ||
| * </p> | ||
| */ | ||
| HttpStatusCode getHttpStatus(); | ||
|
|
||
| /** | ||
| * 비즈니스 상태 코드를 반환합니다. | ||
| * <p> | ||
| * 응답 body에 포함되어 더 세밀한 상태를 나타냅니다. | ||
| * </p> | ||
| */ | ||
| String getCode(); | ||
|
|
||
| /** | ||
| * 상태에 대한 설명 메시지를 반환합니다. | ||
| */ | ||
| String getMessage(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package net.catsnap.CatsnapReservation.shared.domain.error; | ||
|
|
||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
| import net.catsnap.CatsnapReservation.shared.ResultCode; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.HttpStatusCode; | ||
|
|
||
| /** | ||
| * 도메인 에러 코드 | ||
| * <p> | ||
| * 비즈니스 규칙 위반 시 발생하는 에러를 정의합니다. | ||
| * 예약 중복, 불가능한 시간대, 이미 취소된 예약 등 도메인 로직 관련 에러를 담당합니다. | ||
| * </p> | ||
| */ | ||
| @Getter | ||
| @RequiredArgsConstructor | ||
| public enum DomainErrorCode implements ResultCode { | ||
|
|
||
| /** | ||
| * 도메인 제약 조건 위반 | ||
| * <p> | ||
| * 도메인 로직 상 허용되지 않는 값이나 상태인 경우 | ||
| * </p> | ||
| */ | ||
| DOMAIN_CONSTRAINT_VIOLATION(HttpStatus.BAD_REQUEST, "ED000", "해당 값이 유효하지 않습니다."); | ||
redblackblossom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private final HttpStatusCode httpStatus; | ||
|
||
| private final String code; | ||
| private final String message; | ||
redblackblossom marked this conversation as resolved.
Show resolved
Hide resolved
redblackblossom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package net.catsnap.CatsnapReservation.shared.domain.error; | ||
|
|
||
| import lombok.Getter; | ||
| import net.catsnap.CatsnapReservation.shared.ResultCode; | ||
|
|
||
| /** | ||
| * 도메인 예외 최상위 클래스 | ||
| * <p> | ||
| * 도메인 레이어에서 비즈니스 규칙 위반 시 발생하는 모든 예외의 기본 클래스입니다. | ||
| * ResultCode를 포함하여 일관된 응답 형식을 제공합니다. | ||
| * </p> | ||
| */ | ||
| @Getter | ||
| public class DomainException extends RuntimeException { | ||
|
|
||
| private final ResultCode resultCode; | ||
|
|
||
| /** | ||
| * ResultCode를 포함한 비즈니스 예외를 생성합니다. | ||
| * | ||
| * @param resultCode 응답에 사용할 결과 코드 | ||
| */ | ||
| public DomainException(ResultCode resultCode) { | ||
| super(resultCode.getMessage()); | ||
| this.resultCode = resultCode; | ||
| } | ||
|
|
||
| /** | ||
| * ResultCode와 원인 예외를 포함한 비즈니스 예외를 생성합니다. | ||
| * | ||
| * @param resultCode 응답에 사용할 결과 코드 | ||
| * @param cause 원인 예외 | ||
| */ | ||
| public DomainException(ResultCode resultCode, Throwable cause) { | ||
| super(resultCode.getMessage(), cause); | ||
| this.resultCode = resultCode; | ||
| } | ||
|
|
||
| /** | ||
| * ResultCode와 추가 메시지를 포함한 비즈니스 예외를 생성합니다. | ||
| * | ||
| * @param resultCode 응답에 사용할 결과 코드 | ||
| * @param additionalMessage 추가 메시지 | ||
| */ | ||
| public DomainException(ResultCode resultCode, String additionalMessage) { | ||
| super(resultCode.getMessage() + " - " + additionalMessage); | ||
| this.resultCode = resultCode; | ||
| } | ||
redblackblossom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
redblackblossom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| package net.catsnap.CatsnapReservation.shared.presentation; | ||
|
|
||
| import lombok.extern.slf4j.Slf4j; | ||
| import net.catsnap.CatsnapReservation.shared.domain.error.DomainException; | ||
| import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationErrorCode; | ||
| import net.catsnap.CatsnapReservation.shared.presentation.error.PresentationException; | ||
| import net.catsnap.CatsnapReservation.shared.presentation.response.ResultResponse; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.http.converter.HttpMessageNotReadableException; | ||
| import org.springframework.validation.FieldError; | ||
| import org.springframework.web.HttpMediaTypeNotSupportedException; | ||
| 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; | ||
| import org.springframework.web.servlet.NoHandlerFoundException; | ||
|
|
||
| /** | ||
| * 전역 예외 처리 핸들러 | ||
| * <p> | ||
| * 애플리케이션에서 발생하는 예외를 일관된 응답 형식으로 변환합니다. | ||
| * </p> | ||
| */ | ||
| @Slf4j | ||
| @RestControllerAdvice | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| /** | ||
| * 존재하지 않는 API 엔드포인트 요청 처리 | ||
| */ | ||
| @ExceptionHandler(NoHandlerFoundException.class) | ||
| public ResponseEntity<ResultResponse<Void>> handleNoHandlerFoundException( | ||
| NoHandlerFoundException e) { | ||
| log.warn("NoHandlerFoundException: {}", e.getMessage()); | ||
| return ResultResponse.of(PresentationErrorCode.NOT_FOUND_API); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * 필수 요청 파라미터 누락 처리 | ||
| */ | ||
| @ExceptionHandler(MissingServletRequestParameterException.class) | ||
| public ResponseEntity<ResultResponse<Void>> handleMissingServletRequestParameterException( | ||
| MissingServletRequestParameterException e) { | ||
| log.warn("MissingServletRequestParameterException: {}", e.getMessage()); | ||
| return ResultResponse.of(PresentationErrorCode.MISSING_REQUEST_PARAMETER); | ||
| } | ||
|
|
||
| /** | ||
| * 잘못된 요청 바디 처리 | ||
| */ | ||
| @ExceptionHandler(HttpMessageNotReadableException.class) | ||
| public ResponseEntity<ResultResponse<Void>> handleHttpMessageNotReadableException( | ||
| HttpMessageNotReadableException e) { | ||
| log.warn("HttpMessageNotReadableException: {}", e.getMessage()); | ||
| return ResultResponse.of(PresentationErrorCode.INVALID_REQUEST_BODY); | ||
| } | ||
|
|
||
| /** | ||
| * 지원하지 않는 HTTP 메서드 처리 | ||
| */ | ||
| @ExceptionHandler(HttpRequestMethodNotSupportedException.class) | ||
| public ResponseEntity<ResultResponse<Void>> handleHttpRequestMethodNotSupportedException( | ||
| HttpRequestMethodNotSupportedException e) { | ||
| log.warn("HttpRequestMethodNotSupportedException: {}", e.getMessage()); | ||
| return ResultResponse.of(PresentationErrorCode.METHOD_NOT_ALLOWED); | ||
| } | ||
|
|
||
| /** | ||
| * 지원하지 않는 미디어 타입 처리 | ||
| */ | ||
| @ExceptionHandler(HttpMediaTypeNotSupportedException.class) | ||
| public ResponseEntity<ResultResponse<Void>> handleHttpMediaTypeNotSupportedException( | ||
| HttpMediaTypeNotSupportedException e) { | ||
| log.warn("HttpMediaTypeNotSupportedException: {}", e.getMessage()); | ||
| return ResultResponse.of(PresentationErrorCode.UNSUPPORTED_MEDIA_TYPE); | ||
| } | ||
|
|
||
| /** | ||
| * @Valid 검증 실패 처리 | ||
| */ | ||
| @ExceptionHandler(MethodArgumentNotValidException.class) | ||
| public ResponseEntity<ResultResponse<Void>> handleMethodArgumentNotValidException( | ||
| MethodArgumentNotValidException e) { | ||
| FieldError fieldError = e.getBindingResult().getFieldError(); | ||
| String errorMessage = | ||
| fieldError != null ? fieldError.getDefaultMessage() : "입력값이 올바르지 않습니다."; | ||
|
|
||
| log.warn("MethodArgumentNotValidException: {}", errorMessage); | ||
| return ResultResponse.of(PresentationErrorCode.INVALID_REQUEST_BODY); | ||
redblackblossom marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
Comment on lines
+79
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 검증 실패 상세를 응답에 포함하는 방안 검토
🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * 프레젠테이션 예외 처리 (인증/인가 실패 포함) | ||
| * <p> | ||
| * 인증 헤더가 없거나 유효하지 않은 경우, 권한이 없는 경우 등 | ||
| * 프레젠테이션 레이어에서 발생하는 예외를 처리합니다. | ||
| * </p> | ||
| */ | ||
| @ExceptionHandler(PresentationException.class) | ||
| public ResponseEntity<ResultResponse<Void>> handlePresentationException( | ||
| PresentationException e) { | ||
| log.warn("PresentationException: [{}] {}", e.getResultCode().getCode(), e.getMessage()); | ||
| return ResultResponse.of(e.getResultCode()); | ||
| } | ||
|
|
||
| /** | ||
| * 도메인 예외 처리 | ||
| * <p> | ||
| * DomainException에 포함된 ResultCode를 사용하여 응답을 생성합니다. | ||
| * </p> | ||
| */ | ||
| @ExceptionHandler(DomainException.class) | ||
| public ResponseEntity<ResultResponse<Void>> handleDomainException(DomainException e) { | ||
| log.warn("DomainException: [{}] {}", e.getResultCode().getCode(), e.getMessage()); | ||
| return ResultResponse.of(e.getResultCode()); | ||
| } | ||
|
|
||
| /** | ||
| * 기타 모든 예외 처리 | ||
| */ | ||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ResultResponse<Void>> handleException(Exception e) { | ||
| log.error("Unexpected exception occurred", e); | ||
| return ResultResponse.of(PresentationErrorCode.INTERNAL_SERVER_ERROR); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,102 @@ | ||||||||||||||
| package net.catsnap.CatsnapReservation.shared.presentation.error; | ||||||||||||||
|
|
||||||||||||||
| import lombok.Getter; | ||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||
| import net.catsnap.CatsnapReservation.shared.ResultCode; | ||||||||||||||
| import org.springframework.http.HttpStatus; | ||||||||||||||
| import org.springframework.http.HttpStatusCode; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 프레젠테이션 레이어 에러 코드 | ||||||||||||||
| * <p> | ||||||||||||||
| * API 요청/응답 처리 과정에서 발생하는 에러를 정의합니다. | ||||||||||||||
| * 잘못된 요청 형식, 존재하지 않는 엔드포인트, 인증/인가 실패 등 HTTP 레벨의 에러를 담당합니다. | ||||||||||||||
| * </p> | ||||||||||||||
| */ | ||||||||||||||
| @Getter | ||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||
| public enum PresentationErrorCode implements ResultCode { | ||||||||||||||
|
|
||||||||||||||
| // ========== API 요청 관련 에러 (EA0XX) ========== | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 존재하지 않는 API 엔드포인트 | ||||||||||||||
| */ | ||||||||||||||
| NOT_FOUND_API(HttpStatus.NOT_FOUND, "EA000", "존재하지 않는 API입니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 필수 요청 파라미터 누락 | ||||||||||||||
| */ | ||||||||||||||
| MISSING_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, "EA001", "필수 요청 파라미터가 누락되었습니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 잘못된 요청 바디 | ||||||||||||||
| */ | ||||||||||||||
| INVALID_REQUEST_BODY(HttpStatus.BAD_REQUEST, "EA002", "요청 바디가 올바르지 않습니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 서버 내부 오류 | ||||||||||||||
| */ | ||||||||||||||
| INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "EA003", "서버 내부 오류가 발생했습니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 잘못된 요청 형식 | ||||||||||||||
| */ | ||||||||||||||
| INVALID_REQUEST_FORMAT(HttpStatus.BAD_REQUEST, "EA004", "요청 형식이 올바르지 않습니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 메서드 지원 안 함 | ||||||||||||||
| */ | ||||||||||||||
| METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "EA005", "지원하지 않는 HTTP 메서드입니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 미지원 미디어 타입 | ||||||||||||||
| */ | ||||||||||||||
| UNSUPPORTED_MEDIA_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "EA006", "지원하지 않는 미디어 타입입니다."), | ||||||||||||||
|
|
||||||||||||||
| // ========== 인증/인가 관련 에러 (EA1XX) ========== | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 인증 정보 없음 (401 Unauthorized) | ||||||||||||||
| * <p> | ||||||||||||||
| * 요청에 인증 헤더가 없거나 비어있는 경우 | ||||||||||||||
| * </p> | ||||||||||||||
| */ | ||||||||||||||
| UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "EA100", "인증 정보가 없습니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 유효하지 않은 권한 정보 (401 Unauthorized) | ||||||||||||||
| * <p> | ||||||||||||||
| * 인증 헤더의 권한 값이 유효하지 않은 경우 | ||||||||||||||
| * </p> | ||||||||||||||
| */ | ||||||||||||||
| INVALID_AUTHORITY(HttpStatus.UNAUTHORIZED, "EA101", "유효하지 않은 권한 정보입니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 접근 권한 없음 (403 Forbidden) | ||||||||||||||
| * <p> | ||||||||||||||
| * 인증은 되었으나 해당 리소스에 접근할 권한이 없는 경우 | ||||||||||||||
| * </p> | ||||||||||||||
| */ | ||||||||||||||
| FORBIDDEN(HttpStatus.FORBIDDEN, "EA102", "접근 권한이 없습니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 유효하지 않은 Passport (401 Unauthorized) | ||||||||||||||
| * <p> | ||||||||||||||
| * Passport 서명 검증에 실패했거나 파싱할 수 없는 경우 | ||||||||||||||
| * </p> | ||||||||||||||
| */ | ||||||||||||||
| INVALID_PASSPORT(HttpStatus.UNAUTHORIZED, "EA103", "유효하지 않은 Passport입니다."), | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 만료된 Passport (401 Unauthorized) | ||||||||||||||
| * <p> | ||||||||||||||
| * Passport의 유효기간이 만료된 경우 | ||||||||||||||
| * </p> | ||||||||||||||
| */ | ||||||||||||||
| EXPIRED_PASSPORT(HttpStatus.UNAUTHORIZED, "EA104", "만료된 Passport입니다."); | ||||||||||||||
|
|
||||||||||||||
|
||||||||||||||
| @Override | |
| public String getMessage() { | |
| return message; | |
| } |
Uh oh!
There was an error while loading. Please reload this page.