From 949b62d5cee96094e9e3343d7772ef54dc3fcf38 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:34:16 +0900 Subject: [PATCH 01/14] =?UTF-8?q?[chore]=20=ED=95=84=EC=9A=94=ED=95=9C=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/AuthException.java | 18 ++++++++++++++++++ .../common/exception/BusinessException.java | 19 +++++++++++++++++++ .../common/exception/DomainRuleException.java | 15 +++++++++++++++ .../exception/EntityNotFoundException.java | 11 +++++++++++ 4 files changed, 63 insertions(+) create mode 100644 src/main/java/konkuk/chacall/global/common/exception/AuthException.java create mode 100644 src/main/java/konkuk/chacall/global/common/exception/BusinessException.java create mode 100644 src/main/java/konkuk/chacall/global/common/exception/DomainRuleException.java create mode 100644 src/main/java/konkuk/chacall/global/common/exception/EntityNotFoundException.java 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); + } +} From 5e3384353379d8a3e819da0562c856ab76d7506e Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:34:55 +0900 Subject: [PATCH 02/14] =?UTF-8?q?[fix]=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=86=8D=EC=84=B1=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/chacall/domain/foodtruck/domain/FoodTruck.java | 6 ++++-- src/main/java/konkuk/chacall/domain/user/User.java | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) 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/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; } From 3e7abe428026d66c2d36470bd220ab67055d0cef Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:35:10 +0900 Subject: [PATCH 03/14] =?UTF-8?q?[chore]=20security=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=9D=BC=EC=8B=9C=20=ED=95=B4=EC=A0=9C=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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' } From 51883a672fb1f942dbdcd5f573afa884c22e1851 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:37:54 +0900 Subject: [PATCH 04/14] =?UTF-8?q?[chore]=20ResponseCode=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/common/dto/ResponseCode.java | 11 +++++ .../global/common/dto/SuccessCode.java | 18 ++++++++ .../common/exception/code/ErrorCode.java | 46 +++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/main/java/konkuk/chacall/global/common/dto/ResponseCode.java create mode 100644 src/main/java/konkuk/chacall/global/common/dto/SuccessCode.java create mode 100644 src/main/java/konkuk/chacall/global/common/exception/code/ErrorCode.java 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/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; + } +} From 841ac7a5e6c10d3b9a5d6ca8094d00b20bef259b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:38:04 +0900 Subject: [PATCH 05/14] =?UTF-8?q?[chore]=20Response=20Dto=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/common/dto/BaseResponse.java | 35 ++++++++++++++++++ .../global/common/dto/ErrorResponse.java | 37 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/main/java/konkuk/chacall/global/common/dto/BaseResponse.java create mode 100644 src/main/java/konkuk/chacall/global/common/dto/ErrorResponse.java 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..6d33a85c --- /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({"success", "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..d1a46e7d --- /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({"success", "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()); + } +} From 5d94b2cbe16f227e9aa73344b1723315eda0d93b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:38:18 +0900 Subject: [PATCH 06/14] =?UTF-8?q?[chore]=20ChatRoom=EC=9D=98=20fk=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..4a2484fb 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; } From 7856cf3656f05dfcc6f4a88796fdb04c7896eaeb Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:38:49 +0900 Subject: [PATCH 07/14] =?UTF-8?q?[chore]=20=EC=A0=84=EC=97=AD=20ExceptionH?= =?UTF-8?q?andler=20=EC=A0=95=EC=9D=98=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GlobalExceptionHandler.java | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 src/main/java/konkuk/chacall/global/common/exception/handler/GlobalExceptionHandler.java 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)); + } + +} From 56f195644beeee5d6eefaedf8ddf612862455268 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:38:57 +0900 Subject: [PATCH 08/14] =?UTF-8?q?[chore]=20gitignore=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 339a7c5cf231ca1b38c1205477a0484cefdb8db5 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:39:09 +0900 Subject: [PATCH 09/14] =?UTF-8?q?[chore]=20ExceptionHandler=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20api=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chacall/domain/test/TestController.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/main/java/konkuk/chacall/domain/test/TestController.java 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..89befc39 --- /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 + public 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 From 958c1c9bbecb97b72b4bec7cf83a86ebcf1c3d64 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:39:17 +0900 Subject: [PATCH 10/14] =?UTF-8?q?[chore]=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=99=98=EA=B2=BD=20=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application.properties | 1 - 1 file changed, 1 deletion(-) delete mode 100644 src/main/resources/application.properties 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 From 9b1b54b611073a8e357c319a51492f1a1529ca5a Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 9 Sep 2025 16:44:56 +0900 Subject: [PATCH 11/14] =?UTF-8?q?[chore]=20pr=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=84=A4=EC=A0=95=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/PULL_REQUEST_TEMPLATE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md 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의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요? From 60c14e9643358b8d0f4530ad184f7d6e39e00d39 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Wed, 10 Sep 2025 14:17:24 +0900 Subject: [PATCH 12/14] =?UTF-8?q?[chore]=20JsonProperty=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EC=A0=81=EC=9A=A9=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/chacall/global/common/dto/BaseResponse.java | 2 +- .../java/konkuk/chacall/global/common/dto/ErrorResponse.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/chacall/global/common/dto/BaseResponse.java b/src/main/java/konkuk/chacall/global/common/dto/BaseResponse.java index 6d33a85c..d9af9048 100644 --- a/src/main/java/konkuk/chacall/global/common/dto/BaseResponse.java +++ b/src/main/java/konkuk/chacall/global/common/dto/BaseResponse.java @@ -5,7 +5,7 @@ import lombok.Getter; @Getter -@JsonPropertyOrder({"success", "code", "message", "data"}) +@JsonPropertyOrder({"isSuccess", "code", "message", "data"}) public class BaseResponse { @JsonProperty("isSuccess") diff --git a/src/main/java/konkuk/chacall/global/common/dto/ErrorResponse.java b/src/main/java/konkuk/chacall/global/common/dto/ErrorResponse.java index d1a46e7d..4dff5b99 100644 --- a/src/main/java/konkuk/chacall/global/common/dto/ErrorResponse.java +++ b/src/main/java/konkuk/chacall/global/common/dto/ErrorResponse.java @@ -5,7 +5,7 @@ import lombok.Getter; @Getter -@JsonPropertyOrder({"success", "code", "message"}) +@JsonPropertyOrder({"isSuccess", "code", "message"}) public class ErrorResponse { @JsonProperty("isSuccess") From a4b6c108ef507c7627c79043fd24cd3697e8b268 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Wed, 10 Sep 2025 14:17:33 +0900 Subject: [PATCH 13/14] =?UTF-8?q?[fix]=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=EC=9D=B4=EB=A6=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/chacall/domain/chat/domain/ChatRoom.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 4a2484fb..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 = "member_Id", nullable = false, referencedColumnName = "user_Id") + @JoinColumn(name = "member_id", nullable = false, referencedColumnName = "user_id") private User member; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "owner_Id", nullable = false, referencedColumnName = "user_Id") + @JoinColumn(name = "owner_id", nullable = false, referencedColumnName = "user_id") private User owner; } From 8868a7cb38b4f985322b48f3268ac50a3fa3f55b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Wed, 10 Sep 2025 14:17:47 +0900 Subject: [PATCH 14/14] =?UTF-8?q?[fix]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=82=B4=EB=B6=80=20dto?= =?UTF-8?q?=20private=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/chacall/domain/test/TestController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/chacall/domain/test/TestController.java b/src/main/java/konkuk/chacall/domain/test/TestController.java index 89befc39..f93151da 100644 --- a/src/main/java/konkuk/chacall/domain/test/TestController.java +++ b/src/main/java/konkuk/chacall/domain/test/TestController.java @@ -84,7 +84,7 @@ public String constraint(@RequestParam("id") @Valid @Min(1) int id) { // DTO 내부 유효성 검증용 클래스 @Getter - public static class UserRequest { + private static class UserRequest { @NotBlank(message = "이름은 필수 값입니다.") private String name;