Skip to content
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

댓글 작성 기능 구현 #151

Merged
merged 16 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
20 changes: 19 additions & 1 deletion backend/src/main/java/com/votogether/config/OpenAPIConfig.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.votogether.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.security.SecurityScheme.In;
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
import io.swagger.v3.oas.models.servers.Server;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -37,9 +42,22 @@ public OpenAPI openAPI() {
.version("v1.0.0")
.description("보투게더 API");

final SecurityScheme securityScheme = new SecurityScheme()
.type(Type.HTTP)
.in(In.HEADER)
.name("Authorization")
.scheme("bearer")
.bearerFormat("JWT")
.description("Bearer JWT");

final SecurityRequirement securityRequirement = new SecurityRequirement()
.addList("bearerAuth");
Comment on lines +45 to +54
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q
신기한 기능이네요!
swagger에 토큰을 적용해볼 수 있는 코드라고 생각하면 될까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다 :) 해당 기능을 사용하면 Swagger 전역적으로 토큰을 적용할 수 있습니다. 현재는 Bearer 토큰을 설정해두었는데, 설정을 수정하면 Basic 토큰도 사용이 가능합니다!


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q
이러한 설정을 따로 안해주면 어떤 문제가 생기는건가요??

Copy link
Collaborator Author

@woo-chang woo-chang Jul 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 궁금해서 직접 실험해 보았습니다!

스크린샷 2023-07-28 오전 11 17 32

위의 사진은 SecurityRequirement를 적용하기 전이고, 아래의 사진은 적용한 후의 모습입니다.

스크린샷 2023-07-28 오전 11 18 06

각 API 마다 Security를 설정할 수 있게 변한 것 같아요 :) 전역적으로 설정할 수 있는 버튼이 최상단에 있어서 있어도, 없어도 문제는 없을 것 같아요!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 직접 실험까지! 감사합니다

return new OpenAPI()
.info(info)
.servers(List.of(devServer, prodServer));
.servers(List.of(devServer, prodServer))
.components(new Components().addSecuritySchemes("bearerAuth", securityScheme))
.security(List.of(securityRequirement));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.votogether.domain.post.controller;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.dto.request.CommentRegisterRequest;
import com.votogether.domain.post.service.PostCommentService;
import com.votogether.exception.ExceptionResponse;
import com.votogether.global.jwt.Auth;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "게시글 댓글", description = "게시글 댓글 API")
@RequiredArgsConstructor
@RequestMapping("/posts")
@RestController
public class PostCommentController {

private final PostCommentService postCommentService;

@Operation(summary = "게시글 댓글 작성", description = "게시글 댓글을 작성한다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "게시글 댓글 작성 성공"),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 게시글에 대한 댓글 작성",
content = @Content(schema = @Schema(implementation = ExceptionResponse.class))
)
})
@PostMapping("/{postId}/comments")
public ResponseEntity<Void> registerComment(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q
Controller, Service는 PostComment, Repository, Entity는 Comment라는 네이밍을 가져간 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비즈니스 로직과 연관이 있는 Controller, Service에서는 게시글과 연관관계를 표현하기 위해 PostComment를 사용했고 영속 책임이 있는 Repository, Entity는 대상이 댓글이라 Comment를 사용했습니다!

테이블 상의 명칭을 따랐던 것 같아요 😄

@Auth final Member member,
@PathVariable @Parameter(description = "댓글 작성 게시글 ID") final Long postId,
@Valid @RequestBody CommentRegisterRequest commentRegisterRequest
) {
postCommentService.registerComment(member, postId, commentRegisterRequest);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3
register보다는 create등의 다른 네이밍이 조금 더 자연스럽지 않을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다 👏🏻


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.votogether.domain.post.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

@Schema(description = "게시글 댓글 작성 요청")
public record CommentRegisterRequest(
@Schema(description = "댓글 내용", example = "hello")
@NotBlank(message = "댓글 내용은 존재해야 합니다.")
String content
) {
}
12 changes: 12 additions & 0 deletions backend/src/main/java/com/votogether/domain/post/entity/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import com.votogether.domain.category.entity.Category;
import com.votogether.domain.common.BaseEntity;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.entity.comment.Comment;
import com.votogether.domain.post.exception.PostExceptionType;
import com.votogether.domain.vote.entity.Vote;
import com.votogether.exception.BadRequestException;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
Expand All @@ -15,7 +17,9 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
Expand Down Expand Up @@ -50,6 +54,9 @@ public class Post extends BaseEntity {
@Column(columnDefinition = "datetime(2)", nullable = false)
private LocalDateTime deadline;

@OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST)
private List<Comment> comments = new ArrayList<>();
Comment on lines +57 to +58
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2
@onetomany 속성에 orphanRemoval = true도 추가해주는 것은 어떨까요? 그러면 comments에 있는 Comment와 해당 Comment의 DB정보의 생명주기가 일치해져서 더 관리하기가 쉬워질 것 같습니다.

Copy link
Collaborator Author

@woo-chang woo-chang Jul 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 의견 감사합니다 👍

해당 속성은 댓글의 post 값이 null이 되었을 때 삭제 쿼리가 날라가는 것으로 알고 있습니다 ㅎㅎ 보통 댓글을 삭제하는 경우를 생각해 보았을 때 ID를 이미 알고 있는 상황이라고 생각이 들어요!

ID를 알고 있다면 해당 ID로 삭제 쿼리를 날리는 것이 일반적인 삭제 로직이 될 것 같은데 어떤 상황에서 해당 기능을 활용할 수 있는지 궁금합니다 :)


@Builder
private Post(
final Member member,
Expand Down Expand Up @@ -129,4 +136,9 @@ private void validatePostOption(PostOption postOption) {
}
}

public void addComment(final Comment comment) {
comments.add(comment);
comment.setPost(this);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.votogether.domain.post.entity.comment;

import com.votogether.domain.common.BaseEntity;
import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.entity.Post;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class Comment extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Setter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@Embedded
private Content content;

@Builder
private Comment(final Post post, final Member member, final String content) {
this.post = post;
this.member = member;
this.content = new Content(content);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.votogether.domain.post.entity.comment;

import com.votogether.domain.post.exception.CommentExceptionType;
import com.votogether.exception.BadRequestException;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Embeddable
class Content {

private static final int MAXIMUM_LENGTH = 500;

@Column(name = "content", nullable = false, length = MAXIMUM_LENGTH)
private String value;

public Content(final String value) {
validate(value);
this.value = value;
}

private void validate(final String content) {
if (content.length() > MAXIMUM_LENGTH) {
throw new BadRequestException(
CommentExceptionType.INVALID_CONTENT_LENGTH.getCode(),
String.format("댓글 길이는 최대 %d자까지 가능합니다.", MAXIMUM_LENGTH)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q
메시지를 따로 작성해주신 이유가 있을까요?
Enum에서 가지고 있는 메시지로도 충분히 예외를 표시해줄 수 있을 것 같은데
최대 길이가 꼭 포함되어야 하는 이유가 있을까요?

Copy link
Collaborator Author

@woo-chang woo-chang Jul 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프론트에서 백엔드의 예외 메시지를 그대로 사용한다고 해서 클라이언트에게 자세한 메시지를 전달하려고 했습니다 :)

올바르지 않은 글자로도 충분할까요? 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자에게 어느정도 정보까지 알려줄지가 중요할 것 같은데요.
닉네임 정책이 클라이언트의 페이지 상에 표시가 된다면 올바르지 않은 글자 수라는 예외 메시지로도 충분할 것 같다는 생각이고,
지금의 형태도 유지보수성을 고려하여 작성해주신 것 같아서 좋아보여요.
다즐 편하신대로 적용해서 반영해주세요~

);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opinion
저는 예외 메세지나 에러코드가 모두 enum에서 관리되었으면 좋겠어요. 코드속에 에러메시지가 지정되어있으면 유지보수측면에서 에러메시지를 관리할때 헷갈리지 않을까? 라는 의견입니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용자에게 조금 더 자세한 예외 메시지를 전달하려고 하다보니 코드 속에 에러 메시지가 들어가게 된 것 같아요!

루쿠는 올바르지 않은 댓글 길이입니다! 정도면 충분히 예외 상황을 인지할 수 있다고 생각하시나요?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 구체적일수록 좋다는 입장이지만
만약 최대 가능한 댓글 길이를 적어준다고 해도 사용자는 현재 자신이 쓰고있는 댓글의 길이가 어느정도 되는지 확인을 못할거 같다는 생각입니다. 그래서 나중에 댓글을 작성할때 옆에 화면에 15/500 처럼 댓글길이를 보여준다면
댓글길이를 확인해주세요! 와 같은 에러메시지로 충분할것 같다는 생각입니다.

}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.votogether.domain.post.exception;

import com.votogether.exception.ExceptionType;
import lombok.Getter;

@Getter
public enum CommentExceptionType implements ExceptionType {

INVALID_CONTENT_LENGTH(2000, "유효하지 않은 댓글 길이입니다.");

private final int code;
private final String message;

CommentExceptionType(final int code, final String message) {
this.code = code;
this.message = message;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.votogether.domain.post.repository;

import com.votogether.domain.post.entity.comment.Comment;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CommentRepository extends JpaRepository<Comment, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.votogether.domain.post.service;

import com.votogether.domain.member.entity.Member;
import com.votogether.domain.post.dto.request.CommentRegisterRequest;
import com.votogether.domain.post.entity.Post;
import com.votogether.domain.post.entity.comment.Comment;
import com.votogether.domain.post.exception.PostExceptionType;
import com.votogether.domain.post.repository.PostRepository;
import com.votogether.exception.BadRequestException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class PostCommentService {

private final PostRepository postRepository;

@Transactional
public void registerComment(
final Member member,
final Long postId,
final CommentRegisterRequest commentRegisterRequest
) {
final Post post = postRepository.findById(postId)
.orElseThrow(() -> new BadRequestException(PostExceptionType.POST_NOT_FOUND));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2
NotFoundException을 사용해도 되지 않을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

놓쳤던 부분입니다 .. 체고 👍🔥


post.addComment(
Comment.builder()
.member(member)
.content(commentRegisterRequest.content())
.build()
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q
Comment를 만들때 빌더에서 post를 초기화 하지않고 post.addCommnet 메서드내에서 Comment의 세터로 post를 초기화하신 이유가 궁급합니다

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

연관관계 편의 메서드 내부에서 관계를 맺어주는게 더 안전하다고 생각합니다!

연관관계를 맺어주는 코드가 분리되어 있으면, 다시 말해서 외부에서 Post를 지정하고 내부에서는 Comment를 목록에 추가하는 상태인 경우 addComment에는 게시글이 지정되어 있지 않은 댓글, 해당 게시글이 아닌 다른 게시글로 지정된 댓글 등 올바르지 않은 댓글이 다 들어올 수 있는 상태가 되어버립니다! 내부적으로도 해당 댓글의 게시글을 수정하지 않으니 의도하지 않은 동작을 할 가능성이 매우 높아지게 됩니다.

따라서 연관관계 편의 메서드에서 댓글의 게시글을 지정하여 의도한 동작을 보장하도록 하였습니다. 연관관계 편의 메서드에서 댓글의 게시글을 지정하게 되면 게시글이 없는 댓글, 다른 게시글이 들어있는 댓글이어도 해당 게시글의 댓글로 지정되어 연관관계 맺을 때 양방향 안정성을 가져갈 수 있게 됩니다 😄

그리고 양방향으로 연관관계가 되어있는 경우 항상 양쪽의 연관관계를 연결시켜줘야 하기 때문에 addComment 안에서 수행하는 것이 안전한 동작을 보장할 수 있을 것 같아요 :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

연관관계 편의 메서드에서 댓글의 게시글을 지정하게 되면 게시글이 없는 댓글, 다른 게시글이 들어있는 댓글이어도 해당 게시글의 댓글로 지정되어 연관관계 맺을 때 양방향 안정성을 가져갈 수 있게 됩니다 😄

그렇다면 다른 게시글에 들어있는 댓글을 addCommnet를 통해 실수로 해당 게시글의 댓글로 지정되어버리면 다른 게시글에 있는 댓글이 해당 게시글로 잘못 들어가는 경우가 생길수도 있지 않을까요?!
post.addComment를 할때 comment필드의 post와 this가 같은지를 검증하는 로직을 추가하는거에 대해서는 어떻게 생각하시나요??

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addComment에서 댓글의 게시글을 지정하지 않을 경우

  • 댓글의 ID가 없는 경우
    • 게시글이 지정되지 않은 경우 : 게시글 ID가 없어서 예외 발생
    • 게시글이 다른 게시글로 지정된 경우 : 다른 게시글에 댓글 등록
    • 게시글이 해당 게시글로 지정된 경우 : 해당 게시글에 댓글 등록
  • 댓글의 ID가 있는 경우
    • 게시글이 다른 게시글로 지정된 경우 : 아무런 쿼리 X
    • 게시글이 해당 게시글로 지정된 경우 : 아무런 쿼리 X

addComment에서 댓글의 게시글을 지정하는 경우

  • 댓글의 ID가 없는 경우
    • 게시글이 지정되지 않은 경우 : 해당 게시글에 댓글 등록
    • 게시글이 다른 게시글로 지정된 경우 : 해당 게시글에 댓글 등록
    • 게시글이 해당 게시글로 지정된 경우 : 해당 게시글에 댓글 등록
  • 댓글의 ID가 있는 경우
    • 게시글이 다른 게시글로 지정된 경우 : 해당 게시글로 댓글 업데이트
    • 게시글이 해당 게시글로 지정된 경우 : 아무런 쿼리 X

위와 같은 결과를 확인할 수 있습니다. 가장 안전한 방법을 선택한다면 addComment에서 댓글 게시글을 지정하고, ID가 없는지까지 확인하는 방법일텐데 ID가 있는지 없는지는 서비스에서 검증해야할 것 같습니다.

따라서 지금과 같은 방법을 사용하려고 하는데 괜찮으신가요?! ☺️

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q
이런 경우엔 다즐은 Comment를 따로 변수로 빼는 것이 더 가독성이 좋은지, 지금 상태가 더 가독성이 좋은지 궁금합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빼고 확인해보았을 때 변수로 빼는 것이 더 가독성이 좋은 것 같아서 수정했습니다 👍

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ public BadRequestException(final ExceptionType exceptionType) {
super(exceptionType);
}

public BadRequestException(final int code, final String message) {
super(code, message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
@Getter
public class BaseException extends RuntimeException {

private final ExceptionType exceptionType;
private final int code;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q
혹시 다즐은 어디서든 필드를 정의할 때, 원시타입과 래퍼 클래스 타입 중 어떤 것을 더 선호하는지, 그 이유는 무엇인지 궁금합니다.

Copy link
Collaborator Author

@woo-chang woo-chang Jul 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상황에 따라 다른 것 같아요!

값을 입력하지 않았을 때 기본값(ex) int인 경우 0, boolean인 경우 false)이 들어가도 괜찮은 경우라면 원시타입을 사용하고, 그렇지 않다면 래퍼클래스를 사용하는 것 같아요 :)


public BaseException(final ExceptionType exceptionType) {
super(exceptionType.getMessage());
this.exceptionType = exceptionType;
this.code = exceptionType.getCode();
}

public BaseException(final int code, final String message) {
super(message);
this.code = code;
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package com.votogether.exception;

public record ExceptionResponse(int code, String message) {
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "예외 발생 응답")
public record ExceptionResponse(
@Schema(description = "예외 코드", example = "-9999")
int code,
@Schema(description = "예외 메시지", example = "알 수 없는 서버 예외가 발생하였습니다.")
String message
) {

public static ExceptionResponse from(final BaseException e) {
final ExceptionType exceptionType = e.getExceptionType();
return new ExceptionResponse(exceptionType.getCode(), exceptionType.getMessage());
return new ExceptionResponse(e.getCode(), e.getMessage());
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.votogether.exception;

import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

@Slf4j
@RestControllerAdvice
Expand All @@ -17,6 +20,34 @@ public ResponseEntity<ExceptionResponse> handleException(final Exception e) {
.body(new ExceptionResponse(-9999, "알 수 없는 서버 에러가 발생했습니다."));
}

@ExceptionHandler
public ResponseEntity<ExceptionResponse> handleMethodArgumentTypeMismatchException(
final MethodArgumentTypeMismatchException e
) {
final String errorMessage = String.format(
"%s는 %s 타입이 필요합니다.",
e.getPropertyName(),
e.getRequiredType().getSimpleName()
);
log.warn("[" + e.getClass() + "] : " + errorMessage);
return ResponseEntity.badRequest()
.body(new ExceptionResponse(-9998, errorMessage));
}

@ExceptionHandler
public ResponseEntity<ExceptionResponse> handleMethodArgumentNotValidException(
final MethodArgumentNotValidException e
) {
final List<String> errorMessages = e.getBindingResult()
.getAllErrors()
.stream()
.map(error -> error.getDefaultMessage())
.toList();
log.warn("[" + e.getClass() + "] : " + errorMessages);
return ResponseEntity.badRequest()
.body(new ExceptionResponse(-9997, errorMessages.toString()));
}

@ExceptionHandler
public ResponseEntity<ExceptionResponse> handleBadRequestException(final BadRequestException e) {
log.warn("[" + e.getClass() + "] : " + e.getMessage());
Expand Down
Loading