-
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
The head ref may contain hidden characters: "feat#13-\uB313\uAE00/\uB300\uB313\uAE00\uAE30\uB2A5"
Feat#13 댓글/대댓글기능 #14
Changes from 8 commits
725d2b8
028c963
453b7e4
cb357f5
060e824
b05909b
fe23705
c081994
21dad4b
f353107
2a36d56
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 |
|---|---|---|
| @@ -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(); | ||
| } | ||
Doncham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| 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; | ||
| } | ||
| } |
| 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; | ||
| } | ||
| } |
| 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(); | ||
| } | ||
| } |
| 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 List<CommentsInPost> children = new ArrayList<>(); | ||
Doncham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| @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(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| 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; | ||
|
Comment on lines
+37
to
+39
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. 🛠️ Refactor suggestion 순환 참조 가능성 검증 부재 댓글이 자기 자신을 부모로 가지거나 순환 참조가 발생할 수 있는 검증 로직이 없습니다. 서비스 레이어에서 부모 댓글 설정 시 다음과 같은 검증이 필요합니다:
public void validateParent(Comment parent) {
if (parent != null) {
if (parent.getId().equals(this.getId())) {
throw new InvalidCommentHierarchyException("댓글은 자기 자신을 부모로 가질 수 없습니다.");
}
if (parent.getParent() != null) {
throw new InvalidCommentHierarchyException("대댓글에는 답글을 달 수 없습니다.");
}
}
}🤖 Prompt for AI Agents
Owner
Author
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. @Doncham 감사합니다! 🎯 댓글 시스템에서 순환 참조나 무한 깊이 문제는 나중에 발견하기 어려운 버그가 될 수 있어서 미리 잡아내는 것이 중요합니다. 특히 이번 PR에서 1단계 깊이만 허용한다고 하셨으니, 서비스 레이어에서 이런 검증 로직을 추가하시면 더 안전한 댓글 시스템이 될 것 같습니다. 코드 리뷰가 도움이 되었다니 기쁩니다! 😊 |
||
|
|
||
| @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(); | ||
| //this.updatedAt = LocalDateTime.now(clock); | ||
Doncham marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| 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(); | ||
| } | ||
| } |
| 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; | ||
| } | ||
| } |
| 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(); | ||
| } | ||
| } |
| 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(); | ||
| } | ||
| } |
| 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 ParentPostMismatchException extends CustomException { | ||
| public static final String MESSAGE = "부모 댓글의 게시글과 대댓글이 속한 게시글이 일치하지 않습니다."; | ||
|
|
||
| public ParentPostMismatchException() { | ||
| super(MESSAGE); | ||
| } | ||
|
|
||
| @Override | ||
| public int getStatusCode() { | ||
| return 400; // Bad Request | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package org.juniortown.backend.comment.repository; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.juniortown.backend.comment.entity.Comment; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| public interface CommentRepository extends JpaRepository<Comment, Long> { | ||
| List<Comment> findByPostIdOrderByCreatedAtAsc(Long postId); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.