diff --git a/src/main/java/com/daramg/server/post/application/PostQueryService.java b/src/main/java/com/daramg/server/post/application/PostQueryService.java index d856f8d..93433a9 100644 --- a/src/main/java/com/daramg/server/post/application/PostQueryService.java +++ b/src/main/java/com/daramg/server/post/application/PostQueryService.java @@ -113,6 +113,21 @@ public PageResponseDto getUserDraftPosts(Long userId, PageReque ); } + public PageResponseDto getRecentPosts(PageRequestDto pageRequest, User user) { + List posts = postQueryRepository.getRecentPostsWithPaging(pageRequest); + + Set likedPostIds = getLikedPostIds(posts, user); + Set scrappedPostIds = getScrappedPostIds(posts, user); + + return pagingUtils.createPageResponse( + posts, + pageRequest.getValidatedSize(), + post -> toPostResponseDto(post, user, likedPostIds, scrappedPostIds), + Post::getCreatedAt, + Post::getId + ); + } + public PageResponseDto getUserScrappedPosts(Long userId, PageRequestDto pageRequest){ List posts = postQueryRepository.getUserScrappedPostsWithPaging(userId, pageRequest); diff --git a/src/main/java/com/daramg/server/post/presentation/PostQueryController.java b/src/main/java/com/daramg/server/post/presentation/PostQueryController.java index 70d2e7f..303faa0 100644 --- a/src/main/java/com/daramg/server/post/presentation/PostQueryController.java +++ b/src/main/java/com/daramg/server/post/presentation/PostQueryController.java @@ -51,6 +51,15 @@ public PageResponseDto getAllPublishedStoryPosts( return postQueryService.getAllPublishedStoryPosts(request, user); } + @GetMapping("/recent") + @ResponseStatus(HttpStatus.OK) + public PageResponseDto getRecentPosts( + @AuthenticationPrincipal User user, + @ModelAttribute PageRequestDto request + ) { + return postQueryService.getRecentPosts(request, user); + } + @GetMapping("/{userId}/published") @ResponseStatus(HttpStatus.OK) public PageResponseDto getUserPublishedPosts( diff --git a/src/main/java/com/daramg/server/post/repository/PostQueryRepository.java b/src/main/java/com/daramg/server/post/repository/PostQueryRepository.java index c6a31a3..e9a101f 100644 --- a/src/main/java/com/daramg/server/post/repository/PostQueryRepository.java +++ b/src/main/java/com/daramg/server/post/repository/PostQueryRepository.java @@ -21,4 +21,5 @@ public interface PostQueryRepository { List getUserScrappedPostsWithPaging(Long userId, PageRequestDto pageRequest); List getPostsByComposerIdWithPaging(Long composerId, PageRequestDto pageRequest); Map findStoryPostStatsByAllComposers(); + List getRecentPostsWithPaging(PageRequestDto pageRequest); } diff --git a/src/main/java/com/daramg/server/post/repository/PostQueryRepositoryImpl.java b/src/main/java/com/daramg/server/post/repository/PostQueryRepositoryImpl.java index 3e56375..abca338 100644 --- a/src/main/java/com/daramg/server/post/repository/PostQueryRepositoryImpl.java +++ b/src/main/java/com/daramg/server/post/repository/PostQueryRepositoryImpl.java @@ -21,6 +21,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -73,7 +75,21 @@ public List getAllCurationPostsWithPaging(PageRequestDto pageReque @Override public List getAllStoryPostsWithPaging(PageRequestDto pageRequest) { - return getAllPostsWithPaging(pageRequest, storyPost, storyPost._super); + JPAQuery query = queryFactory + .selectFrom(storyPost) + .leftJoin(storyPost._super.user, user).fetchJoin() + .leftJoin(storyPost.primaryComposer).fetchJoin() + .where( + storyPost._super.isBlocked.isFalse() + .and(storyPost._super.postStatus.eq(PostStatus.PUBLISHED)) + ); + + return pagingUtils.applyCursorPagination( + query, + pageRequest, + storyPost._super.createdAt, + storyPost._super.id + ); } @Override @@ -212,6 +228,64 @@ public Map findStoryPostStatsByAllComposers() { )); } + @Override + public List getRecentPostsWithPaging(PageRequestDto pageRequest) { + Instant sevenDaysAgo = Instant.now().minus(7, ChronoUnit.DAYS); + + List freePosts = queryFactory + .selectFrom(freePost) + .leftJoin(freePost._super.user, user).fetchJoin() + .where( + freePost._super.isBlocked.isFalse() + .and(freePost._super.postStatus.eq(PostStatus.PUBLISHED)) + .and(freePost._super.createdAt.goe(sevenDaysAgo)) + ) + .orderBy(freePost._super.createdAt.desc(), freePost._super.id.desc()) + .fetch(); + + List storyPosts = queryFactory + .selectFrom(storyPost) + .leftJoin(storyPost._super.user, user).fetchJoin() + .leftJoin(storyPost.primaryComposer).fetchJoin() + .where( + storyPost._super.isBlocked.isFalse() + .and(storyPost._super.postStatus.eq(PostStatus.PUBLISHED)) + .and(storyPost._super.createdAt.goe(sevenDaysAgo)) + ) + .orderBy(storyPost._super.createdAt.desc(), storyPost._super.id.desc()) + .fetch(); + + List curationPosts = queryFactory + .selectFrom(curationPost) + .leftJoin(curationPost._super.user, user).fetchJoin() + .leftJoin(curationPost.primaryComposer).fetchJoin() + .where( + curationPost._super.isBlocked.isFalse() + .and(curationPost._super.postStatus.eq(PostStatus.PUBLISHED)) + .and(curationPost._super.createdAt.goe(sevenDaysAgo)) + ) + .orderBy(curationPost._super.createdAt.desc(), curationPost._super.id.desc()) + .fetch(); + + List allPosts = new ArrayList<>(); + allPosts.addAll(freePosts); + allPosts.addAll(storyPosts); + allPosts.addAll(curationPosts); + + allPosts.sort((p1, p2) -> { + int dateCompare = p2.getCreatedAt().compareTo(p1.getCreatedAt()); + if (dateCompare != 0) return dateCompare; + return Long.compare(p2.getId(), p1.getId()); + }); + + return pagingUtils.applyCursorPaginationToList( + allPosts, + pageRequest, + Post::getCreatedAt, + Post::getId + ); + } + private List getAllPostsWithPaging( PageRequestDto pageRequest, EntityPathBase qEntity, diff --git a/src/test/java/com/daramg/server/post/application/PostQueryServiceTest.java b/src/test/java/com/daramg/server/post/application/PostQueryServiceTest.java index 284070d..dfd960b 100644 --- a/src/test/java/com/daramg/server/post/application/PostQueryServiceTest.java +++ b/src/test/java/com/daramg/server/post/application/PostQueryServiceTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.util.ReflectionTestUtils; import java.time.Instant; @@ -77,6 +78,9 @@ public class PostQueryServiceTest extends ServiceTestSupport { @Autowired private CommentLikeRepository commentLikeRepository; + @Autowired + private JdbcTemplate jdbcTemplate; + private User user; private User otherUser; private List freePosts; @@ -1281,5 +1285,174 @@ void getComposerWithPosts_ReturnsOnlyPublishedPosts() { .doesNotContain("DRAFT 스토리"); } } + + @Nested + @DisplayName("최근 게시물 조회 테스트") + class GetRecentPostsTest { + + private Composer composer; + + @BeforeEach + void setUpComposer() { + composer = Composer.builder() + .koreanName("베토벤") + .englishName("Ludwig van Beethoven") + .gender(Gender.MALE) + .era(Era.CLASSICAL) + .continent(Continent.EUROPE) + .build(); + composerRepository.save(composer); + } + + @Test + @DisplayName("7일 이내 PUBLISHED 게시물만 반환된다") + void getRecentPosts_ReturnsOnlyPostsWithinSevenDays() { + // given + FreePost recentPost = FreePost.from( + new PostCreateVo.Free(user, "최근 게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of()) + ); + FreePost oldPost = FreePost.from( + new PostCreateVo.Free(user, "오래된 게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of()) + ); + postRepository.save(recentPost); + postRepository.save(oldPost); + jdbcTemplate.update("UPDATE posts SET created_at = ? WHERE id = ?", + Instant.now().minus(8, ChronoUnit.DAYS), oldPost.getId()); + + PageRequestDto pageRequest = new PageRequestDto(null, 100); + + // when + PageResponseDto response = postQueryService.getRecentPosts(pageRequest, null); + + // then + assertThat(response.getContent()) + .extracting(PostResponseDto::title) + .contains("최근 게시물") + .doesNotContain("오래된 게시물"); + } + + @Test + @DisplayName("DRAFT 게시물은 반환되지 않는다") + void getRecentPosts_ExcludesDraftPosts() { + // given + FreePost draftPost = FreePost.from( + new PostCreateVo.Free(user, "DRAFT 게시물", "내용", PostStatus.DRAFT, + List.of(), null, List.of()) + ); + postRepository.save(draftPost); + + PageRequestDto pageRequest = new PageRequestDto(null, 100); + + // when + PageResponseDto response = postQueryService.getRecentPosts(pageRequest, null); + + // then + assertThat(response.getContent()) + .extracting(PostResponseDto::title) + .doesNotContain("DRAFT 게시물"); + } + + @Test + @DisplayName("FreePost, StoryPost, CurationPost 모두 반환된다") + void getRecentPosts_ReturnsAllPostTypes() { + // given + FreePost freePost = FreePost.from( + new PostCreateVo.Free(user, "자유 게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of()) + ); + StoryPost storyPost = StoryPost.from( + new PostCreateVo.Story(user, "스토리 게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of(), composer) + ); + CurationPost curationPost = CurationPost.from( + new PostCreateVo.Curation(user, "큐레이션 게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of(), composer, List.of()) + ); + postRepository.saveAll(List.of(freePost, storyPost, curationPost)); + + PageRequestDto pageRequest = new PageRequestDto(null, 100); + + // when + PageResponseDto response = postQueryService.getRecentPosts(pageRequest, null); + + // then + assertThat(response.getContent()) + .extracting(PostResponseDto::title) + .contains("자유 게시물", "스토리 게시물", "큐레이션 게시물"); + } + + @Test + @DisplayName("StoryPost와 CurationPost 응답에 primaryComposer가 포함된다") + void getRecentPosts_IncludesPrimaryComposerForStoryAndCuration() { + // given + StoryPost storyPost = StoryPost.from( + new PostCreateVo.Story(user, "스토리 게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of(), composer) + ); + CurationPost curationPost = CurationPost.from( + new PostCreateVo.Curation(user, "큐레이션 게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of(), composer, List.of()) + ); + postRepository.saveAll(List.of(storyPost, curationPost)); + + PageRequestDto pageRequest = new PageRequestDto(null, 100); + + // when + PageResponseDto response = postQueryService.getRecentPosts(pageRequest, null); + + // then + assertThat(response.getContent()) + .filteredOn(p -> p.type() != com.daramg.server.post.domain.PostType.FREE) + .allSatisfy(p -> { + assertThat(p.primaryComposer()).isNotNull(); + assertThat(p.primaryComposer().koreanName()).isEqualTo("베토벤"); + }); + } + + @Test + @DisplayName("로그인 유저의 경우 isLiked, isScrapped가 포함된다") + void getRecentPosts_WithLoginUser_IncludesLikedAndScrapped() { + // given + FreePost post = FreePost.from( + new PostCreateVo.Free(user, "게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of()) + ); + postRepository.save(post); + + PostLike like = PostLike.of(post, user); + postLikeRepository.save(like); + + PageRequestDto pageRequest = new PageRequestDto(null, 100); + + // when + PageResponseDto response = postQueryService.getRecentPosts(pageRequest, user); + + // then + assertThat(response.getContent().getFirst().isLiked()).isTrue(); + assertThat(response.getContent().getFirst().isScrapped()).isFalse(); + } + + @Test + @DisplayName("비로그인 유저의 경우 isLiked, isScrapped가 null이다") + void getRecentPosts_WithoutLoginUser_IsLikedAndIsScrappedAreNull() { + // given + FreePost post = FreePost.from( + new PostCreateVo.Free(user, "게시물", "내용", PostStatus.PUBLISHED, + List.of(), null, List.of()) + ); + postRepository.save(post); + + PageRequestDto pageRequest = new PageRequestDto(null, 100); + + // when + PageResponseDto response = postQueryService.getRecentPosts(pageRequest, null); + + // then + assertThat(response.getContent().getFirst().isLiked()).isNull(); + assertThat(response.getContent().getFirst().isScrapped()).isNull(); + } + } }