-
Notifications
You must be signed in to change notification settings - Fork 4
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
댓글 작성 기능 구현 #151
Changes from 8 commits
5d2755c
ab3cfb1
4b390da
17fcafb
c8f173c
eb7817b
84a9e31
9c7ba51
d14f23a
d7bf9de
c8c4a51
ebc28b9
0e74bdb
db959ef
e2e044e
32239f2
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 |
---|---|---|
@@ -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; | ||
|
@@ -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"); | ||
|
||
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.
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. 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. 오호 직접 실험까지! 감사합니다 |
||
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( | ||
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.
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. 비즈니스 로직과 연관이 있는 Controller, Service에서는 게시글과 연관관계를 표현하기 위해 테이블 상의 명칭을 따랐던 것 같아요 😄 |
||
@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(); | ||
} | ||
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.
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. 좋습니다 👏🏻 |
||
|
||
} |
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 | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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
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.
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. 좋은 의견 감사합니다 👍 해당 속성은 댓글의
|
||
|
||
@Builder | ||
private Post( | ||
final Member member, | ||
|
@@ -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) | ||
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.
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. 프론트에서 백엔드의 예외 메시지를 그대로 사용한다고 해서 클라이언트에게 자세한 메시지를 전달하려고 했습니다 :) 올바르지 않은 글자로도 충분할까요? 🤔 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. 사용자에게 어느정도 정보까지 알려줄지가 중요할 것 같은데요. |
||
); | ||
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.
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. 사용자에게 조금 더 자세한 예외 메시지를 전달하려고 하다보니 코드 속에 에러 메시지가 들어가게 된 것 같아요! 루쿠는 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. 저는 구체적일수록 좋다는 입장이지만 |
||
} | ||
} | ||
|
||
} |
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)); | ||
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.
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. 놓쳤던 부분입니다 .. 체고 👍🔥 |
||
|
||
post.addComment( | ||
Comment.builder() | ||
.member(member) | ||
.content(commentRegisterRequest.content()) | ||
.build() | ||
); | ||
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.
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. 연관관계 편의 메서드 내부에서 관계를 맺어주는게 더 안전하다고 생각합니다! 연관관계를 맺어주는 코드가 분리되어 있으면, 다시 말해서 외부에서 따라서 연관관계 편의 메서드에서 댓글의 게시글을 지정하여 의도한 동작을 보장하도록 하였습니다. 연관관계 편의 메서드에서 댓글의 게시글을 지정하게 되면 게시글이 없는 댓글, 다른 게시글이 들어있는 댓글이어도 해당 게시글의 댓글로 지정되어 연관관계 맺을 때 양방향 안정성을 가져갈 수 있게 됩니다 😄 그리고 양방향으로 연관관계가 되어있는 경우 항상 양쪽의 연관관계를 연결시켜줘야 하기 때문에 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.
그렇다면 다른 게시글에 들어있는 댓글을 addCommnet를 통해 실수로 해당 게시글의 댓글로 지정되어버리면 다른 게시글에 있는 댓글이 해당 게시글로 잘못 들어가는 경우가 생길수도 있지 않을까요?! 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.
위와 같은 결과를 확인할 수 있습니다. 가장 안전한 방법을 선택한다면 따라서 지금과 같은 방법을 사용하려고 하는데 괜찮으신가요?! 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.
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. 빼고 확인해보았을 때 변수로 빼는 것이 더 가독성이 좋은 것 같아서 수정했습니다 👍 |
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,11 +5,16 @@ | |
@Getter | ||
public class BaseException extends RuntimeException { | ||
|
||
private final ExceptionType exceptionType; | ||
private final int code; | ||
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.
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. 상황에 따라 다른 것 같아요! 값을 입력하지 않았을 때 기본값(ex) |
||
|
||
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()); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q
신기한 기능이네요!
swagger에 토큰을 적용해볼 수 있는 코드라고 생각하면 될까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
맞습니다 :) 해당 기능을 사용하면 Swagger 전역적으로 토큰을 적용할 수 있습니다. 현재는
Bearer
토큰을 설정해두었는데, 설정을 수정하면Basic
토큰도 사용이 가능합니다!