Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.payper.server.comment.controller;

import com.payper.server.comment.dto.CommentRequest;
import com.payper.server.comment.dto.CommentResponse;
import com.payper.server.comment.service.CommentService;
import com.payper.server.global.response.ApiResponse;
import com.payper.server.security.CustomUserDetails;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/comments")
@RequiredArgsConstructor
public class CommentController {
private final CommentService commentService;

/**
* 댓글 수정
* 작성자만 수정 가능
*/
@PutMapping("/{commentId}")
public ResponseEntity<ApiResponse<Void>> updateComment(
@AuthenticationPrincipal CustomUserDetails user,
@PathVariable Long commentId,
@RequestBody @Valid CommentRequest.UpdateComment request
) {
commentService.updateComment(user.getId(), commentId, request);
return ResponseEntity.ok(ApiResponse.ok());
}

/**
* 댓글 삭제
* 작성자만 삭제 가능
*
* 자식 댓글은 삭제하지 않음
*/
@DeleteMapping("/{commentId}")
public ResponseEntity<ApiResponse<Void>> deleteComment(
@AuthenticationPrincipal CustomUserDetails user,
@PathVariable Long commentId) {

commentService.deleteComment(user.getId(), commentId);
return ResponseEntity.ok(ApiResponse.ok());
}

/**
* 내가 쓴 댓글 조회
*
* 무한 스크롤 방식
*
* 정렬: 최신 순
*/
@GetMapping("/me")
public ResponseEntity<ApiResponse<CommentResponse.MyCommentList>> getMyComments(
@AuthenticationPrincipal CustomUserDetails user,
@RequestParam(required = false) Long cursorId,
@RequestParam(defaultValue = "20") int size
Comment thread
khyun9807 marked this conversation as resolved.
) {
CommentResponse.MyCommentList response = commentService.getMyComments(user.getId(), cursorId, size);
return ResponseEntity.ok(ApiResponse.ok(response));
}

/**
* 자식 댓글 조회
*/
@GetMapping("/{parentId}/replies")
public ResponseEntity<ApiResponse<CommentResponse.CommentList>> getReplies(
@PathVariable Long parentId,
@RequestParam(required = false) Long cursorId,
@RequestParam(defaultValue = "20") int size
) {
CommentResponse.CommentList response = commentService.getReplies(parentId, cursorId, size);
return ResponseEntity.ok(ApiResponse.ok(response));
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/payper/server/comment/dto/CommentRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.payper.server.comment.dto;

import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public class CommentRequest {

/**
* 댓글 작성 DTO
*/
public record CreateComment(
@NotBlank(message = "댓글을 적어주세요.")
@Size(max = 21800, message = "댓글은 21,800자 이하여야 합니다.")
String content,

@Nullable
Long parentCommentId
) {}

/**
* 댓글 수정 DTO
*/
public record UpdateComment(
@NotBlank(message = "댓글을 적어주세요.")
@Size(max = 21800, message = "댓글은 21,800자 이하여야 합니다.")
String content
) {}
}
91 changes: 91 additions & 0 deletions src/main/java/com/payper/server/comment/dto/CommentResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.payper.server.comment.dto;

import com.payper.server.comment.entity.Comment;

import java.time.LocalDateTime;
import java.util.List;

public class CommentResponse {

/**
* Post에 달린 Comment 리스트
*/
public record CommentList(
List<CommentResponse.CommentItem> comments,
Long nextCursor,
boolean hasNext
) {
public static CommentList from(List<Comment> comments, Long nextCursor, boolean hasNext) {
return new CommentList(
comments.stream()
.map(CommentResponse.CommentItem::from)
.toList(),
nextCursor,
hasNext
);
}
}

/**
* Comment Item
*/
public record CommentItem(
Long id,
String userName,
Long parentCommentId,
String content,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
public static CommentItem from(Comment comment) {
return new CommentItem(
comment.getId(),
comment.getUser().getName(),
comment.getParentComment() != null ? comment.getParentComment().getId() : null,
comment.isDeleted() ? "[삭제된 댓글입니다]" : comment.getContent(),
comment.getCreatedAt(),
comment.getUpdatedAt()
);
}
}

/**
* 내가 작성한 Comment 리스트
*/
public record MyCommentList(
List<CommentResponse.MyCommentItem> comments,
Long nextCursor,
boolean hasNext
) {
public static MyCommentList from(List<Comment> comments, Long nextCursor, boolean hasNext) {
return new MyCommentList(
comments.stream()
.map(CommentResponse.MyCommentItem::from)
.toList(),
nextCursor,
hasNext
);
}
}

/**
* 내가 작성한 Comment Item
*/
public record MyCommentItem(
Long id,
Long postId,
String content,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
public static MyCommentItem from(Comment comment) {
return new MyCommentItem(
comment.getId(),
comment.getPost().getId(),
comment.getContent(),
comment.getCreatedAt(),
comment.getUpdatedAt()
);
}
}
}
28 changes: 27 additions & 1 deletion src/main/java/com/payper/server/comment/entity/Comment.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,33 @@ public class Comment extends BaseTimeEntity {
* soft delete
*/
public void delete() {
if (this.isDeleted) { // 멱등성 고려
return;
}

this.isDeleted = true;
this.deletedAt = LocalDateTime.now(ZoneId.of("Asia/Seoul"));
this.deletedAt = LocalDateTime.now();
Comment thread
seoyeon2001 marked this conversation as resolved.
this.post.decreaseCommentCount();
}

/**
* 댓글 생성
*/
public static Comment create(Post post, User user, Comment parentComment, String content) {
return Comment.builder()
.post(post)
.user(user)
.parentComment(parentComment)
.content(content)
.isDeleted(false)
.deletedAt(null)
.build();
}

/**
* 댓글 수정
*/
public void update(String content) {
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.payper.server.comment.repository;

import com.payper.server.comment.entity.Comment;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.Optional;

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {

Optional<Comment> findByIdAndIsDeletedFalse(Long id);

@Query("""
select c from Comment c
join fetch c.user
where c.post.id = :postId
and c.parentComment is null
and (
c.isDeleted = false
or exists (
select 1 from Comment child
where child.parentComment = c
and child.isDeleted = false
)
)
order by c.createdAt asc, c.id asc
""")
Slice<Comment> findParent(@Param("postId") Long postId, Pageable pageable);

@Query("""
select c from Comment c
join fetch c.user
where c.post.id = :postId
and c.parentComment is null
and (
c.isDeleted = false
or exists (
select 1 from Comment child
where child.parentComment = c
and child.isDeleted = false
)
)
and (
c.createdAt > :createdAt
or (c.createdAt = :createdAt and c.id > :cursorId)
)
order by c.createdAt asc, c.id asc
""")
Slice<Comment> findParentNext(
@Param("postId") Long postId,
@Param("cursorId") Long cursorId,
@Param("createdAt") LocalDateTime createdAt,
Pageable pageable
);

@Query("""
select c from Comment c
join fetch c.user
where c.parentComment.id = :parentId
and c.isDeleted = false
order by c.createdAt asc, c.id asc
""")
Comment thread
seoyeon2001 marked this conversation as resolved.
Slice<Comment> findReply(@Param("parentId") Long parentId, Pageable pageable);

@Query("""
select c from Comment c
join fetch c.user
where c.parentComment.id = :parentId
and c.isDeleted = false
and (
c.createdAt > :createdAt
or (c.createdAt = :createdAt and c.id > :cursorId)
)
order by c.createdAt asc, c.id asc
""")
Slice<Comment> findReplyNext(
@Param("parentId") Long parentId,
@Param("cursorId") Long cursorId,
@Param("createdAt") LocalDateTime createdAt,
Pageable pageable
);

@Query("""
select c from Comment c
where c.user.id = :userId
and c.isDeleted = false
order by c.createdAt desc, c.id desc
""")
Slice<Comment> findFirstMyCommentPage(@Param("userId") Long userId, Pageable pageable);

@Query("""
select c from Comment c
where c.user.id = :userId
and c.isDeleted = false
and (
c.createdAt < :createdAt
or (c.createdAt = :createdAt and c.id < :cursorId)
)
order by c.createdAt desc, c.id desc
""")
Slice<Comment> findNextMyCommentPage(
@Param("userId") Long userId,
@Param("cursorId") Long cursorId,
@Param("createdAt") LocalDateTime createdAt,
Pageable pageable
);

@Modifying
@Query("""
update Comment c
set c.isDeleted = true,
c.deletedAt = :now
where c.post.id = :postId
and c.isDeleted = false
""")
long softDeleteByPostId(@Param("postId") Long postId, @Param("now") LocalDateTime now);
}
Loading