-
Notifications
You must be signed in to change notification settings - Fork 0
Feat#13 댓글/대댓글기능 #14
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
Merged
The head ref may contain hidden characters: "feat#13-\uB313\uAE00/\uB300\uB313\uAE00\uAE30\uB2A5"
Merged
Feat#13 댓글/대댓글기능 #14
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
725d2b8
feat: 댓글 작성 기능 구현
Doncham 028c963
feat: 댓글 생성 컨트롤러,서비스 테스트
Doncham 453b7e4
feat: 댓글 삭제 컨트롤러,서비스,테스트
Doncham cb357f5
feat: Comment 업데이트 로직 + 테스트
Doncham 060e824
feat: Comment 조회 로직 + 단위 테스트 + 테스트 리팩토링
Doncham b05909b
feat: 댓글/대댓글 조회 통합 테스트
Doncham fe23705
feat: 댓글/대댓글 페이지(1)
Doncham c081994
feat: 댓글 조회 Depth(2) 제한
Doncham 21dad4b
refactor: 풀리퀘에서 받은 리뷰 적용(1)
Doncham f353107
feat: 댓글 순환 참조 방지 로직 구현, 테스트
Doncham 2a36d56
refactor: 새로고침 x, 상태 업데이터 o
Doncham File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
backend/src/main/java/org/juniortown/backend/comment/controller/CommentController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package org.juniortown.backend.comment.controller; | ||
|
|
||
| import org.juniortown.backend.comment.dto.request.CommentCreateRequest; | ||
| import org.juniortown.backend.comment.dto.request.CommentUpdateRequest; | ||
| import org.juniortown.backend.comment.dto.response.CommentCreateResponse; | ||
| import org.juniortown.backend.comment.service.CommentService; | ||
| import org.juniortown.backend.user.dto.CustomUserDetails; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||
| import org.springframework.web.bind.annotation.PatchMapping; | ||
| 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; | ||
|
|
||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api") | ||
| public class CommentController { | ||
| private final CommentService commentService; | ||
| @PostMapping("/comments") | ||
| public ResponseEntity<CommentCreateResponse> createComment(@AuthenticationPrincipal CustomUserDetails customUserDetails, | ||
| @Valid @RequestBody CommentCreateRequest commentCreateRequest) { | ||
| Long userId = customUserDetails.getUserId(); | ||
|
|
||
| CommentCreateResponse response = commentService.createComment(userId, commentCreateRequest); | ||
|
|
||
| return ResponseEntity.status(HttpStatus.CREATED).body(response); | ||
| } | ||
|
|
||
| @DeleteMapping("/comments/{commentId}") | ||
| public ResponseEntity<?> commentDeleteRequest(@AuthenticationPrincipal CustomUserDetails customUserDetails, | ||
| @PathVariable Long commentId) { | ||
| Long userId = customUserDetails.getUserId(); | ||
| commentService.deleteComment(userId, commentId); | ||
| return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); | ||
| } | ||
| @PatchMapping("/comments/{commentId}") | ||
| public ResponseEntity<?> commentUpdateRequest(@AuthenticationPrincipal CustomUserDetails customUserDetails, | ||
| @PathVariable Long commentId, @Valid @RequestBody CommentUpdateRequest commentUpdateRequest) { | ||
| Long userId = customUserDetails.getUserId(); | ||
| commentService.updateComment(userId, commentId, commentUpdateRequest); | ||
| return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); | ||
| } | ||
| } |
23 changes: 23 additions & 0 deletions
23
backend/src/main/java/org/juniortown/backend/comment/dto/request/CommentCreateRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package org.juniortown.backend.comment.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Getter | ||
| public class CommentCreateRequest { | ||
| @NotNull(message = "게시글 ID를 입력해주세요.") | ||
| private final Long postId; | ||
| private final Long parentId; | ||
| @NotBlank(message = "댓글 내용을 입력해주세요.") | ||
| private final String content; | ||
|
|
||
| @Builder | ||
| public CommentCreateRequest(Long postId, Long parentId, String content) { | ||
| this.postId = postId; | ||
| this.parentId = parentId; | ||
| this.content = content; | ||
| } | ||
| } |
15 changes: 15 additions & 0 deletions
15
backend/src/main/java/org/juniortown/backend/comment/dto/request/CommentUpdateRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package org.juniortown.backend.comment.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.NotBlank; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class CommentUpdateRequest { | ||
| @NotBlank(message = "댓글 내용을 입력해주세요.") | ||
| private final String content; | ||
| @Builder | ||
| public CommentUpdateRequest(String content) { | ||
| this.content = content; | ||
| } | ||
| } |
40 changes: 40 additions & 0 deletions
40
backend/src/main/java/org/juniortown/backend/comment/dto/response/CommentCreateResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package org.juniortown.backend.comment.dto.response; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| import org.juniortown.backend.comment.entity.Comment; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class CommentCreateResponse { | ||
| private final Long commentId; | ||
| private final String content; | ||
| private final Long postId; | ||
| private final Long parentId; | ||
| private final Long userId; | ||
| private final String username; | ||
| private final LocalDateTime createdAt; | ||
| @Builder | ||
| public CommentCreateResponse(Long commentId, String content, Long postId, Long parentId, Long userId, String username, LocalDateTime createdAt) { | ||
| this.commentId = commentId; | ||
| this.content = content; | ||
| this.postId = postId; | ||
| this.parentId = parentId; | ||
| this.userId = userId; | ||
| this.username = username; | ||
| this.createdAt = createdAt; | ||
| } | ||
| public static CommentCreateResponse from(Comment comment) { | ||
| return CommentCreateResponse.builder() | ||
| .commentId(comment.getId()) | ||
| .content(comment.getContent()) | ||
| .postId(comment.getPost().getId()) | ||
| .parentId(comment.getParent() != null ? comment.getParent().getId() : null) | ||
| .userId(comment.getUser().getId()) | ||
| .username(comment.getUser().getName()) | ||
| .createdAt(comment.getCreatedAt()) | ||
| .build(); | ||
| } | ||
| } |
54 changes: 54 additions & 0 deletions
54
backend/src/main/java/org/juniortown/backend/comment/dto/response/CommentsInPost.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package org.juniortown.backend.comment.dto.response; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| import org.juniortown.backend.comment.entity.Comment; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class CommentsInPost { | ||
| private final Long commentId; | ||
| private final Long postId; | ||
| private final Long parentId; | ||
| private final Long userId; | ||
| private final String content; | ||
| private final String username; | ||
| private final LocalDateTime createdAt; | ||
| private final LocalDateTime updatedAt; | ||
| private final LocalDateTime deletedAt; | ||
| private final List<CommentsInPost> children = new ArrayList<>(); | ||
| @Builder | ||
| public CommentsInPost(Long commentId, Long postId, Long parentId, Long userId, String content, String username, LocalDateTime createdAt, | ||
| LocalDateTime updatedAt, LocalDateTime deletedAt) { | ||
| this.commentId = commentId; | ||
| this.postId = postId; | ||
| this.parentId = parentId; | ||
| this.userId = userId; | ||
| this.content = content; | ||
| this.username = username; | ||
| this.createdAt = createdAt; | ||
| this.updatedAt = updatedAt; | ||
| this.deletedAt = deletedAt; | ||
| } | ||
|
|
||
| public static CommentsInPost from(Comment comment) { | ||
| return CommentsInPost.builder() | ||
| .commentId(comment.getId()) | ||
| .postId(comment.getPost().getId()) | ||
| // 근데 이게 꼭 필요한가? userId 쓰는 경우가 생각이 안나 | ||
| .userId(comment.getUser().getId()) | ||
| .parentId(comment.getParent() != null ? comment.getParent().getId() : null) | ||
| .content(comment.getContent()) | ||
| // 이게 원래는 user 테이블을 조회해야하는데 comment에 username이 있으니까 | ||
| // 조회를 안해도 되는 사기 스킬 | ||
| .username(comment.getUsername()) | ||
| .createdAt(comment.getCreatedAt()) | ||
| .updatedAt(comment.getUpdatedAt()) | ||
| .deletedAt(comment.getDeletedAt()) | ||
| .build(); | ||
| } | ||
| } |
83 changes: 83 additions & 0 deletions
83
backend/src/main/java/org/juniortown/backend/comment/entity/Comment.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package org.juniortown.backend.comment.entity; | ||
|
|
||
| import java.time.Clock; | ||
| import java.time.LocalDateTime; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| import org.juniortown.backend.comment.dto.request.CommentUpdateRequest; | ||
| import org.juniortown.backend.comment.exception.AlreadyDeletedCommentException; | ||
| import org.juniortown.backend.entity.BaseTimeEntity; | ||
| import org.juniortown.backend.post.entity.Post; | ||
| import org.juniortown.backend.user.entity.User; | ||
|
|
||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.Lob; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.OneToMany; | ||
| import jakarta.persistence.OneToOne; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Entity | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Getter | ||
| public class Comment extends BaseTimeEntity { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne | ||
| @JoinColumn(name = "parent_id") | ||
| private Comment parent; | ||
|
|
||
| @Lob | ||
| private String content; | ||
|
|
||
| @ManyToOne | ||
| @JoinColumn(name = "user_id", nullable = false) | ||
| private User user; | ||
|
|
||
| @Column(name = "username", nullable = false) | ||
| private String username; | ||
|
|
||
| @ManyToOne | ||
| @JoinColumn(name = "post_id", nullable = false) | ||
| private Post post; | ||
|
|
||
| // 임시로 작성 | ||
| @OneToMany(mappedBy = "parent") | ||
| private List<Comment> replies = new ArrayList<>(); | ||
|
|
||
| @Column(name = "deleted_at") | ||
| private LocalDateTime deletedAt; | ||
| @Builder | ||
| public Comment(Comment parent, String content, User user,String username, Post post) { | ||
| this.parent = parent; | ||
| this.content = content; | ||
| this.user = user; | ||
| this.username = username; | ||
| this.post = post; | ||
| } | ||
|
|
||
| public void softDelete(Clock clock) { | ||
| if(this.deletedAt != null) { | ||
| throw new AlreadyDeletedCommentException(); | ||
| } | ||
| this.deletedAt = LocalDateTime.now(clock); | ||
| } | ||
|
|
||
| public void update(CommentUpdateRequest commentUpdateRequest, Clock clock) { | ||
| if (this.deletedAt != null) { | ||
| throw new AlreadyDeletedCommentException(); | ||
| } | ||
| this.content = commentUpdateRequest.getContent(); | ||
| } | ||
| } | ||
17 changes: 17 additions & 0 deletions
17
...rc/main/java/org/juniortown/backend/comment/exception/AlreadyDeletedCommentException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package org.juniortown.backend.comment.exception; | ||
|
|
||
| import org.juniortown.backend.exception.CustomException; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public class AlreadyDeletedCommentException extends CustomException { | ||
| public static final String MESSAGE = "이미 삭제된 댓글입니다."; | ||
|
|
||
| public AlreadyDeletedCommentException() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return HttpStatus.NOT_FOUND.value(); | ||
| } | ||
| } |
16 changes: 16 additions & 0 deletions
16
...nd/src/main/java/org/juniortown/backend/comment/exception/CircularReferenceException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package org.juniortown.backend.comment.exception; | ||
|
|
||
| import org.juniortown.backend.exception.CustomException; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public class CircularReferenceException extends CustomException { | ||
| public static final String MESSAGE = "댓글 순환 참조 발생!"; | ||
| public CircularReferenceException() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return HttpStatus.BAD_REQUEST.value(); | ||
| } | ||
| } |
16 changes: 16 additions & 0 deletions
16
backend/src/main/java/org/juniortown/backend/comment/exception/CommentNotFoundException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package org.juniortown.backend.comment.exception; | ||
|
|
||
| import org.juniortown.backend.exception.CustomException; | ||
|
|
||
| public class CommentNotFoundException extends CustomException { | ||
| public static final String MESSAGE = "해당 댓글을 찾을 수 없습니다."; | ||
|
|
||
| public CommentNotFoundException() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return 404; | ||
| } | ||
| } |
17 changes: 17 additions & 0 deletions
17
backend/src/main/java/org/juniortown/backend/comment/exception/DepthLimitTwoException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package org.juniortown.backend.comment.exception; | ||
|
|
||
| import org.juniortown.backend.exception.CustomException; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public class DepthLimitTwoException extends CustomException { | ||
| public static final String MESSAGE = "댓글은 최대 2단계까지만 가능합니다."; | ||
|
|
||
| public DepthLimitTwoException() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return HttpStatus.BAD_REQUEST.value(); | ||
| } | ||
| } |
17 changes: 17 additions & 0 deletions
17
.../main/java/org/juniortown/backend/comment/exception/NoRightForCommentDeleteException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package org.juniortown.backend.comment.exception; | ||
|
|
||
| import org.juniortown.backend.exception.CustomException; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public class NoRightForCommentDeleteException extends CustomException { | ||
| public static final String MESSAGE = "해당 댓글을 삭제할 권한이 없습니다."; | ||
|
|
||
| public NoRightForCommentDeleteException() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return HttpStatus.FORBIDDEN.value(); | ||
| } | ||
| } |
17 changes: 17 additions & 0 deletions
17
.../main/java/org/juniortown/backend/comment/exception/NoRightForCommentUpdateException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package org.juniortown.backend.comment.exception; | ||
|
|
||
| import org.juniortown.backend.exception.CustomException; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public class NoRightForCommentUpdateException extends CustomException { | ||
| public static final String MESSAGE = "해당 댓글을 수정할 권한이 없습니다."; | ||
|
|
||
| public NoRightForCommentUpdateException() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return HttpStatus.FORBIDDEN.value(); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
🛠️ Refactor suggestion
순환 참조 가능성 검증 부재
댓글이 자기 자신을 부모로 가지거나 순환 참조가 발생할 수 있는 검증 로직이 없습니다.
서비스 레이어에서 부모 댓글 설정 시 다음과 같은 검증이 필요합니다:
🤖 Prompt for AI Agents
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.
오 이부분을 검증하는 걸 생각 못했네 좀 찝찝했는데 잘 찝어줬네 라임 죽인다
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.
@Doncham 감사합니다! 🎯
댓글 시스템에서 순환 참조나 무한 깊이 문제는 나중에 발견하기 어려운 버그가 될 수 있어서 미리 잡아내는 것이 중요합니다. 특히 이번 PR에서 1단계 깊이만 허용한다고 하셨으니, 서비스 레이어에서 이런 검증 로직을 추가하시면 더 안전한 댓글 시스템이 될 것 같습니다.
코드 리뷰가 도움이 되었다니 기쁩니다! 😊