diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..435f85dc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## #️⃣연관된 이슈 + +> ex) #이슈번호, #이슈번호 + +## 📝작업 내용 + +> 이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) + +### 스크린샷 (선택) + +## 💬리뷰 요구사항(선택) + +> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요 +> +> ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요? diff --git a/.gitignore b/.gitignore index 28c5eeb6..be0a45d1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +*.yml ### STS ### .apt_generated diff --git a/build.gradle b/build.gradle index 45b92f56..7a95c8ee 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-security' +// implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' @@ -34,7 +34,7 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' +// testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java index 755f4de7..a4b0b1ad 100644 --- a/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java +++ b/src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java @@ -16,11 +16,11 @@ public class ChatRoom { private Long chatRoomId; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(name = "member_id", nullable = false, referencedColumnName = "user_id") private User member; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(name = "owner_id", nullable = false, referencedColumnName = "user_id") private User owner; } diff --git a/src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java b/src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java index 54a11685..8e550d5f 100644 --- a/src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java +++ b/src/main/java/konkuk/chacall/domain/foodtruck/domain/FoodTruck.java @@ -3,6 +3,8 @@ import jakarta.persistence.*; import konkuk.chacall.domain.foodtruck.domain.value.*; import konkuk.chacall.domain.user.User; +import konkuk.chacall.global.common.converter.MenuCategoryListConverter; +import konkuk.chacall.global.common.converter.PhotoUrlListConverter; import konkuk.chacall.global.common.domain.BaseEntity; import lombok.AccessLevel; import lombok.Getter; @@ -34,11 +36,11 @@ public class FoodTruck extends BaseEntity { @Column(nullable = false) private boolean timeDiscussRequired; - @Convert(converter = PhotoUrlList.class) + @Convert(converter = PhotoUrlListConverter.class) @Column(nullable = false) private PhotoUrlList foodTruckPhotoList; - @Convert(converter = MenuCategoryList.class) + @Convert(converter = MenuCategoryListConverter.class) @Column(nullable = false) private MenuCategoryList menuCategoryList; diff --git a/src/main/java/konkuk/chacall/domain/test/TestController.java b/src/main/java/konkuk/chacall/domain/test/TestController.java new file mode 100644 index 00000000..f93151da --- /dev/null +++ b/src/main/java/konkuk/chacall/domain/test/TestController.java @@ -0,0 +1,99 @@ +package konkuk.chacall.domain.test; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import konkuk.chacall.global.common.dto.BaseResponse; +import konkuk.chacall.global.common.exception.AuthException; +import konkuk.chacall.global.common.exception.BusinessException; +import konkuk.chacall.global.common.exception.DomainRuleException; +import konkuk.chacall.global.common.exception.EntityNotFoundException; +import konkuk.chacall.global.common.exception.code.ErrorCode; +import lombok.Getter; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/test") +public class TestController { + + @GetMapping("/hello") + public String hello() { + return "Hello, World!"; + } + + @GetMapping("/ping") + public BaseResponse ping() { + return BaseResponse.ok("pong"); + } + + // === 커스텀 예외들 === + @GetMapping("/auth-error") + public String authError() { + throw new AuthException(ErrorCode.AUTH_UNAUTHORIZED); + } + + @GetMapping("/entity-error") + public String entityError() { + throw new EntityNotFoundException(ErrorCode.USER_NOT_FOUND); + } + + @GetMapping("/domain-error") + public String domainError() { + throw new DomainRuleException(ErrorCode.USER_ALREADY_EXISTS); + } + + @GetMapping("/business-error") + public String businessError() { + throw new BusinessException(ErrorCode.USER_NICKNAME_DUPLICATION); + } + + @GetMapping("/runtime-error") + public String runtimeError() { + throw new RuntimeException("강제 RuntimeException 발생"); + } + + // === GlobalExceptionHandler 내장 케이스들 === + + // 1. MethodArgumentNotValidException 테스트 (@Valid DTO 사용) + @PostMapping("/validate-body") + public String validateBody(@Valid @RequestBody UserRequest request) { + return "유효성 통과: " + request.toString(); + } + + // 2. MethodArgumentTypeMismatchException 테스트 + // 호출: /test/type-mismatch?id=문자열 + @GetMapping("/type-mismatch") + public String typeMismatch(@RequestParam("id") Long id) { + return "입력한 id: " + id; + } + + // 3. MissingServletRequestParameterException 테스트 + // 호출 시 /test/missing-param 만 요청 -> user 파라미터 없음 + @GetMapping("/missing-param") + public String missingParam(@RequestParam("user") String user) { + return "user: " + user; + } + + // 4. ConstraintViolationException 테스트 + // 호출: /test/constraint?id=-5 + @GetMapping("/constraint") + public String constraint(@RequestParam("id") @Valid @Min(1) int id) { + return "id: " + id; + } + + // DTO 내부 유효성 검증용 클래스 + @Getter + private static class UserRequest { + @NotBlank(message = "이름은 필수 값입니다.") + private String name; + + @Size(min = 5, max = 20, message = "닉네임은 5~20자 사이여야 합니다.") + private String nickname; + + @Override + public String toString() { + return "UserRequest{name='" + name + "', nickname='" + nickname + "'}"; + } + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/chacall/domain/user/User.java b/src/main/java/konkuk/chacall/domain/user/User.java index 937c08d6..edf07fa3 100644 --- a/src/main/java/konkuk/chacall/domain/user/User.java +++ b/src/main/java/konkuk/chacall/domain/user/User.java @@ -13,7 +13,7 @@ public class User extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id", nullable = false) - private String userId; + private Long userId; @Column(length = 20, nullable = false) private String name; @@ -32,6 +32,6 @@ public class User extends BaseEntity { private Gender gender; @Enumerated(EnumType.STRING) - @Column(length = 1) + @Column(length = 10, nullable = false) private Role role; } diff --git a/src/main/java/konkuk/chacall/global/common/dto/BaseResponse.java b/src/main/java/konkuk/chacall/global/common/dto/BaseResponse.java new file mode 100644 index 00000000..d9af9048 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/dto/BaseResponse.java @@ -0,0 +1,35 @@ +package konkuk.chacall.global.common.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Getter; + +@Getter +@JsonPropertyOrder({"isSuccess", "code", "message", "data"}) +public class BaseResponse { + + @JsonProperty("isSuccess") + private final boolean success; + + private final int code; + + private final String message; + + private final T data; + + private BaseResponse(boolean success, int code, String message, T data) { + this.success = success; + this.code = code; + this.message = message; + this.data = data; + } + + private BaseResponse(ResponseCode response, T data) { + this(response.isSuccess(), response.getCode(), response.getMessage(), data); + } + + public static BaseResponse ok(T data) { + return new BaseResponse<>(SuccessCode.API_SUCCESS, data); + } + +} \ No newline at end of file diff --git a/src/main/java/konkuk/chacall/global/common/dto/ErrorResponse.java b/src/main/java/konkuk/chacall/global/common/dto/ErrorResponse.java new file mode 100644 index 00000000..4dff5b99 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/dto/ErrorResponse.java @@ -0,0 +1,37 @@ +package konkuk.chacall.global.common.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.Getter; + +@Getter +@JsonPropertyOrder({"isSuccess", "code", "message"}) +public class ErrorResponse { + + @JsonProperty("isSuccess") + private final boolean success; + + private final int code; + + private final String message; + + private ErrorResponse(boolean success, int code, String message) { + this.success = success; + this.code = code; + this.message = message; + } + + private ErrorResponse(ResponseCode response) { + this(response.isSuccess(), response.getCode(), response.getMessage()); + } + + public static ErrorResponse of(ResponseCode response) { + return new ErrorResponse(response); + } + + public static ErrorResponse of(ResponseCode response, String message) { + StringBuilder sb = new StringBuilder(); + sb.append(response.getMessage()).append(" ").append(message); + return new ErrorResponse(response.isSuccess(), response.getCode(), sb.toString()); + } +} diff --git a/src/main/java/konkuk/chacall/global/common/dto/ResponseCode.java b/src/main/java/konkuk/chacall/global/common/dto/ResponseCode.java new file mode 100644 index 00000000..42327dbf --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/dto/ResponseCode.java @@ -0,0 +1,11 @@ +package konkuk.chacall.global.common.dto; + +public interface ResponseCode { + int getCode(); + + String getMessage(); + + default boolean isSuccess() { + return this instanceof SuccessCode; + } +} diff --git a/src/main/java/konkuk/chacall/global/common/dto/SuccessCode.java b/src/main/java/konkuk/chacall/global/common/dto/SuccessCode.java new file mode 100644 index 00000000..4787f6b4 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/dto/SuccessCode.java @@ -0,0 +1,18 @@ +package konkuk.chacall.global.common.dto; + +import konkuk.chacall.global.common.dto.ResponseCode; +import lombok.Getter; + +@Getter +public enum SuccessCode implements ResponseCode { + API_SUCCESS(20000, "요청에 성공했습니다."), + ; + + private final int code; + private final String message; + + SuccessCode(int code, String message) { + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/konkuk/chacall/global/common/exception/AuthException.java b/src/main/java/konkuk/chacall/global/common/exception/AuthException.java new file mode 100644 index 00000000..232d9d7e --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/exception/AuthException.java @@ -0,0 +1,18 @@ +package konkuk.chacall.global.common.exception; + +import konkuk.chacall.global.common.exception.code.ErrorCode; +import lombok.Getter; + +@Getter +public class AuthException extends RuntimeException { + private final ErrorCode errorCode; + + public AuthException(ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public AuthException(ErrorCode errorCode, Exception e) { + super(e); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/konkuk/chacall/global/common/exception/BusinessException.java b/src/main/java/konkuk/chacall/global/common/exception/BusinessException.java new file mode 100644 index 00000000..4ec8ba56 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/exception/BusinessException.java @@ -0,0 +1,19 @@ +package konkuk.chacall.global.common.exception; + +import konkuk.chacall.global.common.exception.code.ErrorCode; +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public BusinessException(ErrorCode errorCode, Exception e) { + super(errorCode.getMessage(), e); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/konkuk/chacall/global/common/exception/DomainRuleException.java b/src/main/java/konkuk/chacall/global/common/exception/DomainRuleException.java new file mode 100644 index 00000000..3213d46a --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/exception/DomainRuleException.java @@ -0,0 +1,15 @@ +package konkuk.chacall.global.common.exception; + +import konkuk.chacall.global.common.exception.code.ErrorCode; +import lombok.Getter; + +@Getter +public class DomainRuleException extends BusinessException { + public DomainRuleException(ErrorCode errorCode) { + super(errorCode); + } + + public DomainRuleException(ErrorCode errorCode, Exception e) { + super(errorCode, e); + } +} diff --git a/src/main/java/konkuk/chacall/global/common/exception/EntityNotFoundException.java b/src/main/java/konkuk/chacall/global/common/exception/EntityNotFoundException.java new file mode 100644 index 00000000..11cdac44 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/exception/EntityNotFoundException.java @@ -0,0 +1,11 @@ +package konkuk.chacall.global.common.exception; + + +import konkuk.chacall.global.common.exception.code.ErrorCode; + +public class EntityNotFoundException extends BusinessException { + + public EntityNotFoundException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java b/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java new file mode 100644 index 00000000..65adea6b --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java @@ -0,0 +1,46 @@ +package konkuk.chacall.global.common.exception.code; + +import konkuk.chacall.global.common.dto.ResponseCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ErrorCode implements ResponseCode { + + API_NOT_FOUND(HttpStatus.NOT_FOUND, 40400, "요청한 API를 찾을 수 없습니다."), + API_METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, 40500, "허용되지 않는 HTTP 메소드입니다."), + API_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50000, "서버 내부 오류입니다."), + + API_BAD_REQUEST(HttpStatus.BAD_REQUEST, 40000, "잘못된 요청입니다."), + API_MISSING_PARAM(HttpStatus.BAD_REQUEST, 40001, "필수 파라미터가 없습니다."), + API_INVALID_PARAM(HttpStatus.BAD_REQUEST, 40002, "파라미터 값 중 유효하지 않은 값이 있습니다."), + API_INVALID_TYPE(HttpStatus.BAD_REQUEST, 40003, "파라미터 타입이 잘못되었습니다."), + + AUTH_INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 40100, "유효하지 않은 토큰입니다."), + AUTH_EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, 40101, "만료된 토큰입니다."), + AUTH_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, 40102, "인증되지 않은 사용자입니다."), + AUTH_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, 40103, "토큰을 찾을 수 없습니다."), + AUTH_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, 40104, "로그인에 실패했습니다."), + AUTH_UNSUPPORTED_SOCIAL_LOGIN(HttpStatus.UNAUTHORIZED, 40105, "지원하지 않는 소셜 로그인입니다."), + AUTH_INVALID_LOGIN_TOKEN_KEY(HttpStatus.UNAUTHORIZED, 40106, "유효하지 않은 로그인 토큰 키입니다."), + + /* 60000부터 비즈니스 예외 */ + /** + * User + */ + USER_NOT_FOUND(HttpStatus.NOT_FOUND, 60001, "사용자를 찾을 수 없습니다."), + USER_ALREADY_EXISTS(HttpStatus.CONFLICT, 60002, "이미 존재하는 사용자입니다."), + USER_NICKNAME_DUPLICATION(HttpStatus.CONFLICT, 60003, "이미 존재하는 닉네임입니다."), + + ; + + private final HttpStatus httpStatus; + private final int code; + private final String message; + + ErrorCode(HttpStatus httpStatus, int code, String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 00000000..b0d67581 --- /dev/null +++ b/src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,188 @@ +package konkuk.chacall.global.common.exception.handler; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import konkuk.chacall.global.common.dto.ErrorResponse; +import konkuk.chacall.global.common.exception.AuthException; +import konkuk.chacall.global.common.exception.BusinessException; +import konkuk.chacall.global.common.exception.DomainRuleException; +import konkuk.chacall.global.common.exception.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.http.ResponseEntity; +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.method.annotation.HandlerMethodValidationException; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import static konkuk.chacall.global.common.exception.code.ErrorCode.*; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class GlobalExceptionHandler { + + // 요청한 API가 없는 경우 + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity noHandlerExceptionHandler(NoHandlerFoundException e) { + return ResponseEntity + .status(API_NOT_FOUND.getHttpStatus()) + .body(ErrorResponse.of(API_NOT_FOUND)); + } + + // 허용되지 않은 HTTP 메소드로 요청한 경우 + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public ResponseEntity httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException e) { + log.error("[HttpRequestMethodNotSupportedExceptionHandler] {}", e.getMessage()); + return ResponseEntity + .status(API_METHOD_NOT_ALLOWED.getHttpStatus()) + .body(ErrorResponse.of(API_METHOD_NOT_ALLOWED)); + } + + // 요청 파라미터가 유효하지 않은 경우 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { + log.error("[MethodArgumentNotValidExceptionHandler] {}", e.getMessage()); + // 첫 번째 유효성 검사 실패 메시지만 가져오기 + String errorMessage = e.getBindingResult() + .getFieldErrors() + .stream() + .findFirst() + .map(error -> error.getDefaultMessage()) + .orElse("Validation failed"); + + return ResponseEntity + .status(API_INVALID_PARAM.getHttpStatus()) + .body(ErrorResponse.of(API_INVALID_PARAM, errorMessage)); + } + + // 요청 파라미터의 타입이 맞지 않는 경우 + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException e) { + log.error("[MethodArgumentTypeMismatchExceptionHandler] {}", e.getMessage()); + + return ResponseEntity + .status(API_INVALID_TYPE.getHttpStatus()) + .body(ErrorResponse.of(API_INVALID_TYPE, e.getName() + "는 " + e.getRequiredType() + " 타입이어야 합니다.")); + } + + // 요청 파라미터가 누락된 경우 + @ExceptionHandler(MissingServletRequestParameterException.class) + public ResponseEntity missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) { + log.error("[MissingServletRequestParameterExceptionHandler] {}", e.getMessage()); + return ResponseEntity + .status(API_MISSING_PARAM.getHttpStatus()) + .body(ErrorResponse.of(API_MISSING_PARAM, e.getParameterName() + "를 추가해서 요청해주세요.")); + } + + // @Validation 예외처리 + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity constraintViolationExceptionHandler(ConstraintViolationException e) { + log.error("[ConstraintViolationExceptionHandler] {}", e.getMessage()); + // 첫 번째 위반만 꺼내서 + ConstraintViolation violation = e.getConstraintViolations().stream().findFirst().orElse(null); + + // 기본 메시지 또는 제약조건 메시지 사용 + String errorMessage = Optional.ofNullable(violation) + .map(v -> v.getMessage()) + .orElse("유효성 검사에 실패했습니다."); + + // API_INVALID_PARAM 코드를 공통으로 사용 + return ResponseEntity + .status(API_INVALID_PARAM.getHttpStatus()) + .body(ErrorResponse.of(API_INVALID_PARAM, errorMessage)); + } + + @ExceptionHandler(HandlerMethodValidationException.class) + public ResponseEntity handlerMethodValidationException(HandlerMethodValidationException e) { + log.error("[HandlerMethodValidationException] {}", e.getMessage()); + + // 파라미터별 검증 실패 메시지를 모아서 detail 로 제공 + String detail = e.getParameterValidationResults().stream() + .map(result -> { + String paramName = Optional.ofNullable(result.getMethodParameter().getParameterName()) + .orElse("parameter"); + String messages = result.getResolvableErrors().stream() + .map(MessageSourceResolvable::getDefaultMessage) + .filter(Objects::nonNull) + .collect(Collectors.joining(", ")); + return paramName + ": " + (messages.isBlank() ? "유효하지 않은 값입니다." : messages); + }) + .collect(Collectors.joining(" | ")); + + if (detail.isBlank()) { + detail = "유효성 검사에 실패했습니다."; + } + + return ResponseEntity + .status(API_INVALID_PARAM.getHttpStatus()) + .body(ErrorResponse.of(API_INVALID_PARAM, detail)); + } + + // === 우선순위 1: 인증/인가 예외 === + @ExceptionHandler(AuthException.class) + public ResponseEntity authExceptionHandler(AuthException e) { + log.warn("[AuthException] {}", e.getMessage()); + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(ErrorResponse.of(e.getErrorCode())); + } + + // === 우선순위 2: 엔티티 NotFound === + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity entityNotFoundExceptionHandler(EntityNotFoundException e) { + log.info("[EntityNotFound] {}", e.getMessage()); + String detail = Optional.ofNullable(e.getCause()).map(Throwable::getMessage).orElse(""); + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(ErrorResponse.of(e.getErrorCode(), detail)); + } + + // === 우선순위 3: 도메인 규칙 위반 === + @ExceptionHandler(DomainRuleException.class) + public ResponseEntity domainRuleViolationExceptionHandler(DomainRuleException e) { + log.warn("[DomainRuleViolation] {}", e.getMessage()); + String detail = Optional.ofNullable(e.getCause()).map(Throwable::getMessage).orElse(""); + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(ErrorResponse.of(e.getErrorCode(), detail)); + } + + // === 우선순위 4: 일반 비즈니스 예외(캐치올 for BusinessException) === + @ExceptionHandler(BusinessException.class) + public ResponseEntity businessExceptionHandler(BusinessException e) { + log.warn("[BusinessException] {}", e.getMessage()); + String detail = Optional.ofNullable(e.getCause()).map(Throwable::getMessage).orElse(""); + return ResponseEntity + .status(e.getErrorCode().getHttpStatus()) + .body(ErrorResponse.of(e.getErrorCode(), detail)); + } + + // 서버 내부 오류 예외 처리 + @ExceptionHandler(RuntimeException.class) + public ResponseEntity runtimeExceptionHandler(RuntimeException e) { + log.error("[RuntimeExceptionHandler] {}", e.getMessage(), e); + return ResponseEntity + .status(API_SERVER_ERROR.getHttpStatus()) + .body(ErrorResponse.of(API_SERVER_ERROR)); + } + + // IllegalStateException 예외 처리 + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity illegalStateExceptionHandler(IllegalStateException e) { + log.error("[IllegalStateExceptionHandler] {}", e.getMessage()); + return ResponseEntity + .status(API_SERVER_ERROR.getHttpStatus()) + .body(ErrorResponse.of(API_SERVER_ERROR)); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 1a5ad48e..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=chacall