diff --git a/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java index 89970d334..e8dd61473 100644 --- a/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java +++ b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java @@ -37,7 +37,7 @@ public ResponseEntity findPostsByCodeAndCategory( @PathVariable(value = "code") String code, @RequestParam(value = "category", defaultValue = "전체") String category) { List postsByCodeAndPostCategory = postQueryService - .findPostsByCodeAndPostCategory(code, category, siteUserId); + .findPostsByCodeAndPostCategoryOrderByCreatedAtDesc(code, category, siteUserId); return ResponseEntity.ok().body(postsByCodeAndPostCategory); } } diff --git a/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java index 39b42f49a..4291eb57c 100644 --- a/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java @@ -12,30 +12,30 @@ public interface CommentRepository extends JpaRepository { @Query(value = """ - WITH RECURSIVE CommentTree AS ( - SELECT - id, parent_id, post_id, site_user_id, content, - created_at, updated_at, is_deleted, - 0 AS level, CAST(id AS CHAR(255)) AS path - FROM comment - WHERE post_id = :postId AND parent_id IS NULL - AND site_user_id NOT IN ( - SELECT blocked_id FROM user_block WHERE blocker_id = :siteUserId - ) - UNION ALL - SELECT - c.id, c.parent_id, c.post_id, c.site_user_id, c.content, - c.created_at, c.updated_at, c.is_deleted, - ct.level + 1, CONCAT(ct.path, '->', c.id) - FROM comment c - INNER JOIN CommentTree ct ON c.parent_id = ct.id - WHERE c.site_user_id NOT IN ( - SELECT blocked_id FROM user_block WHERE blocker_id = :siteUserId - ) - ) - SELECT * FROM CommentTree - ORDER BY path - """, nativeQuery = true) + WITH RECURSIVE CommentTree AS ( + SELECT + id, parent_id, post_id, site_user_id, content, + created_at, updated_at, is_deleted, + 0 AS level, CAST(id AS CHAR(255)) AS path + FROM comment + WHERE post_id = :postId AND parent_id IS NULL + AND site_user_id NOT IN ( + SELECT blocked_id FROM user_block WHERE blocker_id = :siteUserId + ) + UNION ALL + SELECT + c.id, c.parent_id, c.post_id, c.site_user_id, c.content, + c.created_at, c.updated_at, c.is_deleted, + ct.level + 1, CONCAT(ct.path, '->', c.id) + FROM comment c + INNER JOIN CommentTree ct ON c.parent_id = ct.id + WHERE c.site_user_id NOT IN ( + SELECT blocked_id FROM user_block WHERE blocker_id = :siteUserId + ) + ) + SELECT * FROM CommentTree + ORDER BY path, created_at + """, nativeQuery = true) List findCommentTreeByPostIdExcludingBlockedUsers(@Param("postId") Long postId, @Param("siteUserId") Long siteUserId); default Comment getById(Long id) { diff --git a/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java index aad12fab1..cca590270 100644 --- a/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java +++ b/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java @@ -14,16 +14,17 @@ public interface PostRepository extends JpaRepository { - List findByBoardCode(String boardCode); + List findByBoardCodeOrderByCreatedAtDesc(String boardCode); @Query(""" - SELECT p FROM Post p - WHERE p.boardCode = :boardCode - AND p.siteUserId NOT IN ( - SELECT ub.blockedId FROM UserBlock ub WHERE ub.blockerId = :siteUserId - ) - """) - List findByBoardCodeExcludingBlockedUsers(@Param("boardCode") String boardCode, @Param("siteUserId") Long siteUserId); + SELECT p FROM Post p + WHERE p.boardCode = :boardCode + AND p.siteUserId NOT IN ( + SELECT ub.blockedId FROM UserBlock ub WHERE ub.blockerId = :siteUserId + ) + ORDER BY p.createdAt DESC + """) + List findByBoardCodeExcludingBlockedUsersOrderByCreatedAtDesc(@Param("boardCode") String boardCode, @Param("siteUserId") Long siteUserId); @EntityGraph(attributePaths = {"postImageList"}) Optional findPostById(Long id); diff --git a/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java index 9602cd454..413ec400d 100644 --- a/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java @@ -46,17 +46,17 @@ public class PostQueryService { private final RedisUtils redisUtils; @Transactional(readOnly = true) - public List findPostsByCodeAndPostCategory(String code, String category, Long siteUserId) { + public List findPostsByCodeAndPostCategoryOrderByCreatedAtDesc(String code, String category, Long siteUserId) { String boardCode = validateCode(code); PostCategory postCategory = validatePostCategory(category); boardRepository.getByCode(boardCode); - List postList; // todo : 추후 개선 필요(현재 최신순으로 응답나가지 않고 있음) + List postList; if (siteUserId != null) { - postList = postRepository.findByBoardCodeExcludingBlockedUsers(boardCode, siteUserId); + postList = postRepository.findByBoardCodeExcludingBlockedUsersOrderByCreatedAtDesc(boardCode, siteUserId); } else { - postList = postRepository.findByBoardCode(boardCode); + postList = postRepository.findByBoardCodeOrderByCreatedAtDesc(boardCode); } return PostListResponse.from(getPostListByPostCategory(postList, postCategory)); } diff --git a/src/test/java/com/example/solidconnection/common/helper/TestTimeHelper.java b/src/test/java/com/example/solidconnection/common/helper/TestTimeHelper.java new file mode 100644 index 000000000..c8ca10b3a --- /dev/null +++ b/src/test/java/com/example/solidconnection/common/helper/TestTimeHelper.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.common.helper; + +import com.example.solidconnection.common.BaseEntity; +import java.lang.reflect.Field; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; + +public class TestTimeHelper { + + public static void setCreatedAt(BaseEntity entity, ZonedDateTime time) { + try { + Field field = BaseEntity.class.getDeclaredField("createdAt"); + field.setAccessible(true); + field.set(entity, time.truncatedTo(ChronoUnit.MICROS)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/java/com/example/solidconnection/community/comment/fixture/CommentFixture.java b/src/test/java/com/example/solidconnection/community/comment/fixture/CommentFixture.java index 4d0f3b438..4bc1579d7 100644 --- a/src/test/java/com/example/solidconnection/community/comment/fixture/CommentFixture.java +++ b/src/test/java/com/example/solidconnection/community/comment/fixture/CommentFixture.java @@ -32,4 +32,19 @@ public class CommentFixture { .parentComment(parentComment) .createChild(); } + + public Comment 자식_댓글_지연저장( + String content, + Post post, + SiteUser siteUser, + Comment parentComment, + long secondsDelay + ) { + return commentFixtureBuilder + .content(content) + .post(post) + .siteUser(siteUser) + .parentComment(parentComment) + .createChildWithDelaySeconds(secondsDelay); + } } diff --git a/src/test/java/com/example/solidconnection/community/comment/fixture/CommentFixtureBuilder.java b/src/test/java/com/example/solidconnection/community/comment/fixture/CommentFixtureBuilder.java index 02a8ba889..7c380a103 100644 --- a/src/test/java/com/example/solidconnection/community/comment/fixture/CommentFixtureBuilder.java +++ b/src/test/java/com/example/solidconnection/community/comment/fixture/CommentFixtureBuilder.java @@ -1,5 +1,6 @@ package com.example.solidconnection.community.comment.fixture; +import com.example.solidconnection.common.helper.TestTimeHelper; import com.example.solidconnection.community.comment.domain.Comment; import com.example.solidconnection.community.comment.repository.CommentRepository; import com.example.solidconnection.community.post.domain.Post; @@ -46,8 +47,18 @@ public Comment createParent() { public Comment createChild() { Comment comment = new Comment(content); - comment.setPostAndSiteUserId(post, siteUser.getId()); comment.setParentCommentAndPostAndSiteUserId(parentComment, post, siteUser.getId()); return commentRepository.save(comment); } + + public Comment createChildWithDelaySeconds(long seconds) { + Comment comment = new Comment(content); + comment.setParentCommentAndPostAndSiteUserId(parentComment, post, siteUser.getId()); + + Comment saved = commentRepository.save(comment); + + TestTimeHelper.setCreatedAt(saved, saved.getCreatedAt().plusSeconds(seconds)); + + return commentRepository.save(saved); + } } diff --git a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java index 63a824e53..86247f775 100644 --- a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java @@ -82,34 +82,55 @@ void setUp() { class 댓글_조회_테스트 { @Test - void 게시글의_모든_댓글을_조회한다() { + void 게시글의_모든_댓글과_대댓글을_생성시간_기준으로_정렬해_조회한다() { // given Comment parentComment = commentFixture.부모_댓글("부모 댓글", post, user1); - Comment childComment = commentFixture.자식_댓글("자식 댓글 1", post, user2, parentComment); - List comments = List.of(parentComment, childComment); + Comment childComment1 = commentFixture.자식_댓글("자식 댓글 1", post, user2, parentComment); + Comment childComment2 = commentFixture.자식_댓글_지연저장("자식 댓글 2", post, user1, parentComment, 3); + Comment childComment3 = commentFixture.자식_댓글_지연저장("자식 댓글 3", post, user2, parentComment, 5); + List comments = List.of(parentComment, childComment1, childComment2, childComment3); // when List responses = commentService.findCommentsByPostId(user1.getId(), post.getId()); // then assertAll( - () -> assertThat(responses).hasSize(comments.size()), () -> assertThat(responses) .filteredOn(response -> response.id().equals(parentComment.getId())) .singleElement() .satisfies(response -> assertAll( - () -> assertThat(response.id()).isEqualTo(parentComment.getId()), () -> assertThat(response.parentId()).isNull(), () -> assertThat(response.isOwner()).isTrue() )), () -> assertThat(responses) - .filteredOn(response -> response.id().equals(childComment.getId())) + .filteredOn(response -> response.id().equals(childComment1.getId())) .singleElement() .satisfies(response -> assertAll( - () -> assertThat(response.id()).isEqualTo(childComment.getId()), () -> assertThat(response.parentId()).isEqualTo(parentComment.getId()), () -> assertThat(response.isOwner()).isFalse() - )) + )), + () -> assertThat(responses) + .filteredOn(response -> response.id().equals(childComment2.getId())) + .singleElement() + .satisfies(response -> assertAll( + () -> assertThat(response.parentId()).isEqualTo(parentComment.getId()), + () -> assertThat(response.isOwner()).isTrue() + )), + () -> assertThat(responses) + .filteredOn(response -> response.id().equals(childComment3.getId())) + .singleElement() + .satisfies(response -> assertAll( + () -> assertThat(response.parentId()).isEqualTo(parentComment.getId()), + () -> assertThat(response.isOwner()).isFalse() + )), + () -> assertThat(responses) + .extracting(PostFindCommentResponse::id) + .containsExactly( + parentComment.getId(), + childComment1.getId(), + childComment2.getId(), + childComment3.getId() + ) ); } @@ -198,24 +219,27 @@ class 댓글_조회_테스트 { userBlockFixture.유저_차단(user1.getId(), user2.getId()); Comment parentComment1 = commentFixture.부모_댓글("부모 댓글1", post, user1); Comment childComment1 = commentFixture.자식_댓글("자식 댓글1", post, user1, parentComment1); - Comment childComment2 = commentFixture.자식_댓글("자식 댓글2", post, user2, parentComment1); - Comment parentCommen2 = commentFixture.부모_댓글("부모 댓글2", post, user2); - Comment childComment3 = commentFixture.자식_댓글("자식 댓글1", post, user1, parentCommen2); - Comment childComment4 = commentFixture.자식_댓글("자식 댓글1", post, user1, parentCommen2); - + Comment childComment2 = commentFixture.자식_댓글_지연저장("자식 댓글2", post, user2, parentComment1, 2); + Comment childComment3 = commentFixture.자식_댓글_지연저장("자식 댓글3", post, user1, parentComment1, 3); + Comment parentComment2 = commentFixture.부모_댓글("부모 댓글2", post, user2); + Comment childComment4 = commentFixture.자식_댓글("자식 댓글1", post, user1, parentComment2); + Comment childComment5 = commentFixture.자식_댓글_지연저장("자식 댓글1", post, user1, parentComment2, 2); // when List responses = commentService.findCommentsByPostId(user1.getId(), post.getId()); // then assertAll( - () -> assertThat(responses).hasSize(2), - () -> assertThat(responses) - .extracting(PostFindCommentResponse::id) - .containsExactly(parentComment1.getId(), childComment1.getId()), - () -> assertThat(responses) - .extracting(PostFindCommentResponse::id) - .doesNotContain(childComment2.getId(), parentCommen2.getId(), childComment3.getId(), childComment4.getId()) + () -> assertThat(responses).hasSize(3), + () -> assertThat(responses) + .extracting(PostFindCommentResponse::id) + .containsExactly(parentComment1.getId(), childComment1.getId(), childComment3.getId()), + () -> assertThat(responses) + .extracting(PostFindCommentResponse::id) + .doesNotContain(childComment2.getId(), parentComment2.getId(), childComment4.getId(), childComment5.getId()), + () -> assertThat(responses) + .extracting(PostFindCommentResponse::id) + .containsSubsequence(parentComment1.getId(), childComment1.getId(), childComment3.getId()) ); } } diff --git a/src/test/java/com/example/solidconnection/community/post/fixture/PostFixture.java b/src/test/java/com/example/solidconnection/community/post/fixture/PostFixture.java index 5ddf13888..51d2dcb2d 100644 --- a/src/test/java/com/example/solidconnection/community/post/fixture/PostFixture.java +++ b/src/test/java/com/example/solidconnection/community/post/fixture/PostFixture.java @@ -47,4 +47,25 @@ public class PostFixture { .siteUser(siteUser) .create(); } + + public Post 게시글_지연저장( + String title, + String content, + Boolean isQuestion, + PostCategory postCategory, + Board board, + SiteUser siteUser, + long secondsDelay + ) { + return postFixtureBuilder + .title(title) + .content(content) + .isQuestion(isQuestion) + .likeCount(0L) + .viewCount(0L) + .postCategory(postCategory) + .board(board) + .siteUser(siteUser) + .createWithDelaySeconds(secondsDelay); + } } diff --git a/src/test/java/com/example/solidconnection/community/post/fixture/PostFixtureBuilder.java b/src/test/java/com/example/solidconnection/community/post/fixture/PostFixtureBuilder.java index 3473d61e2..235ae75b6 100644 --- a/src/test/java/com/example/solidconnection/community/post/fixture/PostFixtureBuilder.java +++ b/src/test/java/com/example/solidconnection/community/post/fixture/PostFixtureBuilder.java @@ -1,5 +1,6 @@ package com.example.solidconnection.community.post.fixture; +import com.example.solidconnection.common.helper.TestTimeHelper; import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.community.post.domain.PostCategory; @@ -74,4 +75,20 @@ public Post create() { post.setBoardAndSiteUserId(board.getCode(), siteUser.getId()); return postRepository.save(post); } + + public Post createWithDelaySeconds(long seconds) { + Post post = new Post( + title, + content, + isQuestion, + likeCount, + viewCount, + postCategory); + post.setBoardAndSiteUserId(board.getCode(), siteUser.getId()); + + Post saved = postRepository.save(post); + + TestTimeHelper.setCreatedAt(saved, saved.getCreatedAt().plusSeconds(seconds)); + return postRepository.save(post); + } } diff --git a/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java index beeb3fc88..f3eaf41a8 100644 --- a/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java @@ -64,6 +64,7 @@ class PostQueryServiceTest { private Post post1; private Post post2; private Post post3; + private Post post4; @BeforeEach void setUp() { @@ -76,28 +77,39 @@ void setUp() { boardFixture.자유게시판(), user ); - post2 = postFixture.게시글( + post2 = postFixture.게시글_지연저장( "제목2", "내용2", false, PostCategory.자유, boardFixture.미주권(), - user + user, + 3 ); - post3 = postFixture.게시글( + post3 = postFixture.게시글_지연저장( "제목3", "내용3", true, PostCategory.질문, boardFixture.자유게시판(), - user + user, + 6 + ); + post4 = postFixture.게시글_지연저장( + "제목4", + "내용4", + false, + PostCategory.자유, + boardFixture.자유게시판(), + user, + 9 ); } @Test - void 게시판_코드와_카테고리로_게시글_목록을_조회한다() { + void 게시판_코드와_카테고리로_게시글_목록을_최신순으로_조회한다() { // given - List posts = List.of(post1, post2, post3); + List posts = List.of(post4, post3, post2, post1); List expectedPosts = posts.stream() .filter(post -> post.getCategory().equals(PostCategory.자유) && post.getBoardCode().equals(BoardCode.FREE.name())) @@ -105,30 +117,35 @@ void setUp() { List expectedResponses = PostListResponse.from(expectedPosts); // when - List actualResponses = postQueryService.findPostsByCodeAndPostCategory( + List actualResponses = postQueryService.findPostsByCodeAndPostCategoryOrderByCreatedAtDesc( BoardCode.FREE.name(), PostCategory.자유.name(), null ); // then - assertThat(actualResponses) - .usingRecursiveComparison() - .ignoringFieldsOfTypes(ZonedDateTime.class) - .isEqualTo(expectedResponses); + assertAll( + () -> assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses), + () -> assertThat(actualResponses) + .extracting(PostListResponse::id) + .containsExactly(post4.getId(), post1.getId()) + ); } @Test void 전체_카테고리로_조회시_해당_게시판의_모든_게시글을_조회한다() { // given - List posts = List.of(post1, post2, post3); + List posts = List.of(post4, post3, post2, post1); List expectedPosts = posts.stream() .filter(post -> post.getBoardCode().equals(BoardCode.FREE.name())) .toList(); List expectedResponses = PostListResponse.from(expectedPosts); // when - List actualResponses = postQueryService.findPostsByCodeAndPostCategory( + List actualResponses = postQueryService.findPostsByCodeAndPostCategoryOrderByCreatedAtDesc( BoardCode.FREE.name(), PostCategory.전체.name(), null @@ -200,7 +217,7 @@ void setUp() { postImageFixture.게시글_이미지(secondImageUrl, post1); // when - List actualResponses = postQueryService.findPostsByCodeAndPostCategory( + List actualResponses = postQueryService.findPostsByCodeAndPostCategoryOrderByCreatedAtDesc( BoardCode.FREE.name(), PostCategory.전체.name(), null @@ -218,7 +235,7 @@ void setUp() { @Test void 게시글에_이미지가_없다면_썸네일로_null을_반환한다() { // when - List actualResponses = postQueryService.findPostsByCodeAndPostCategory( + List actualResponses = postQueryService.findPostsByCodeAndPostCategoryOrderByCreatedAtDesc( BoardCode.FREE.name(), PostCategory.전체.name(), null @@ -244,7 +261,7 @@ void setUp() { Post notBlockedPost = postFixture.게시글(board, notBlockedUser); // when - List response = postQueryService.findPostsByCodeAndPostCategory( + List response = postQueryService.findPostsByCodeAndPostCategoryOrderByCreatedAtDesc( BoardCode.FREE.name(), PostCategory.전체.name(), user.getId() @@ -256,4 +273,36 @@ void setUp() { () -> assertThat(response).extracting(PostListResponse::id).doesNotContain(blockedPost.getId()) ); } + + @Test + void 차단한_사용자의_게시글은_제외하고_게시글_목록을_최신순으로_조회한다() { + // given + SiteUser blockedUser = siteUserFixture.사용자(1, "blockedUser"); + userBlockFixture.유저_차단(user.getId(), blockedUser.getId()); + Board board = boardFixture.자유게시판(); + Post blockedPost = postFixture.게시글(board, blockedUser); + List expectedResponse = List.of(post4, post3, post1); + + // when + List actualResponses = postQueryService.findPostsByCodeAndPostCategoryOrderByCreatedAtDesc( + BoardCode.FREE.name(), + PostCategory.전체.name(), + user.getId() + ); + + // then + assertAll( + () -> assertThat(actualResponses) + .extracting(PostListResponse::id) + .containsExactlyElementsOf( + expectedResponse.stream() + .map(Post::getId) + .toList() + ), + () -> assertThat(actualResponses) + .extracting(PostListResponse::id) + .doesNotContain(blockedPost.getId()) + ); + + } }