From b9b457c555fe225ea804a08e14432164efd26854 Mon Sep 17 00:00:00 2001 From: myqewr Date: Sun, 24 Aug 2025 17:38:14 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EA=B7=B8=EB=A3=A8=EB=B0=8D=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B4=EC=A7=80=20-=20"=EA=B7=B8=EB=A3=A8=EB=B0=8D=20?= =?UTF-8?q?=EB=B0=94=EC=9D=B4=EB=B8=94"=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20api=20=EA=B5=AC=ED=98=84(#153)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/post-api.adoc | 14 ++ .../GetUserPickPopularPostsController.java | 2 +- .../LoadUserPickBiblePostsController.java | 27 ++++ .../LoadUserPickBiblePostsResponse.java | 35 +++++ ...oadUserPickBiblePostsWithCacheAdapter.java | 45 ++++++ .../post/PostDomainPersistenceAdapter.java | 16 ++- .../repository/PostRepository.java | 5 + .../PostWithBookmarkCustomRepository.java | 4 + .../PostWithBookmarkCustomRepositoryImpl.java | 34 +++++ .../GetUserPickBiblePostsScheduler.java | 27 ++++ .../GetUserPickPopularPostsUseCase.java | 2 +- .../post/LoadUserPickBiblePostsUseCase.java | 10 ++ .../LoadUserPickBiblePostsWithCachePort.java | 11 ++ .../out/persistence/post/LoadPostPort.java | 8 +- .../post/LoadPostWithBookmarkCountPort.java | 4 + .../query/FindUserPickBiblePostsQuery.java | 15 ++ .../post/GetUserPickPopularPostsService.java | 2 +- .../post/LoadUserPickBiblePostsService.java | 82 +++++++++++ .../vo/post/PostWithIdAndAuthorVo.java | 10 ++ .../post/PostWithUserAndBookmarkCountVo.java | 19 +++ .../vo/post/UserPickBiblePostsVo.java | 38 ++++++ .../server/common/consts/StaticConsts.java | 3 + .../post/LoadUserPickBiblePostsTest.java | 128 ++++++++++++++++++ .../post/LoadUserPickPopularPostsTest.java | 1 - 24 files changed, 530 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadUserPickBiblePostsController.java create mode 100644 src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadUserPickBiblePostsResponse.java create mode 100644 src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickBiblePostsWithCacheAdapter.java create mode 100644 src/main/java/com/ftm/server/adapter/out/scheduler/GetUserPickBiblePostsScheduler.java rename src/main/java/com/ftm/server/application/port/in/{user => post}/GetUserPickPopularPostsUseCase.java (83%) create mode 100644 src/main/java/com/ftm/server/application/port/in/post/LoadUserPickBiblePostsUseCase.java create mode 100644 src/main/java/com/ftm/server/application/port/out/cache/LoadUserPickBiblePostsWithCachePort.java create mode 100644 src/main/java/com/ftm/server/application/query/FindUserPickBiblePostsQuery.java create mode 100644 src/main/java/com/ftm/server/application/service/post/LoadUserPickBiblePostsService.java create mode 100644 src/main/java/com/ftm/server/application/vo/post/PostWithIdAndAuthorVo.java create mode 100644 src/main/java/com/ftm/server/application/vo/post/PostWithUserAndBookmarkCountVo.java create mode 100644 src/main/java/com/ftm/server/application/vo/post/UserPickBiblePostsVo.java create mode 100644 src/test/java/com/ftm/server/post/LoadUserPickBiblePostsTest.java diff --git a/src/docs/asciidoc/post-api.adoc b/src/docs/asciidoc/post-api.adoc index 92efaa7..cd232a9 100644 --- a/src/docs/asciidoc/post-api.adoc +++ b/src/docs/asciidoc/post-api.adoc @@ -276,4 +276,18 @@ include::{snippetsDir}/loadUserPickPopular/1/http-response.adoc[] ==== Response Body Fields include::{snippetsDir}/loadUserPickPopular/1/response-fields.adoc[] +--- + + +=== **10. 그루밍 라운지 - "그루밍 바이블" 조회** + +==== Request +include::{snippetsDir}/loadUserPickBible/1/http-request.adoc[] + +==== 성공 Response +include::{snippetsDir}/loadUserPickBible/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/loadUserPickBible/1/response-fields.adoc[] + --- \ No newline at end of file diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetUserPickPopularPostsController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetUserPickPopularPostsController.java index f1a8ad4..7857196 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetUserPickPopularPostsController.java +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetUserPickPopularPostsController.java @@ -1,7 +1,7 @@ package com.ftm.server.adapter.in.web.post.controller; import com.ftm.server.adapter.in.web.post.dto.response.GetUserPickPopularPostsResponse; -import com.ftm.server.application.port.in.user.GetUserPickPopularPostsUseCase; +import com.ftm.server.application.port.in.post.GetUserPickPopularPostsUseCase; import com.ftm.server.common.response.ApiResponse; import com.ftm.server.common.response.enums.SuccessResponseCode; import java.util.List; diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadUserPickBiblePostsController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadUserPickBiblePostsController.java new file mode 100644 index 0000000..7bc1155 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadUserPickBiblePostsController.java @@ -0,0 +1,27 @@ +package com.ftm.server.adapter.in.web.post.controller; + +import com.ftm.server.adapter.in.web.post.dto.response.LoadUserPickBiblePostsResponse; +import com.ftm.server.application.port.in.post.LoadUserPickBiblePostsUseCase; +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.SuccessResponseCode; +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +public class LoadUserPickBiblePostsController { + + private final LoadUserPickBiblePostsUseCase userPickBiblePostsUseCase; + + @GetMapping("/api/posts/userpick/bible") + public ResponseEntity loadUserPickGroomingBiblePosts() { + List result = + userPickBiblePostsUseCase.execute().stream() + .map(LoadUserPickBiblePostsResponse::from) + .toList(); + return ResponseEntity.ok(ApiResponse.success(SuccessResponseCode.OK, result)); + } +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadUserPickBiblePostsResponse.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadUserPickBiblePostsResponse.java new file mode 100644 index 0000000..cad4016 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadUserPickBiblePostsResponse.java @@ -0,0 +1,35 @@ +package com.ftm.server.adapter.in.web.post.dto.response; + +import com.ftm.server.application.vo.post.UserPickBiblePostsVo; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoadUserPickBiblePostsResponse { + private final Integer ranking; + private final Long postId; + private final String title; + private final Long authorId; + private final String authorName; + private final Integer viewCount; + private final Integer likeCount; + private final Long scrapCount; + private final String imageUrl; + private final List hashtags; + + public static LoadUserPickBiblePostsResponse from(UserPickBiblePostsVo vo) { + return new LoadUserPickBiblePostsResponse( + vo.getRanking(), + vo.getPostId(), + vo.getTitle(), + vo.getAuthorId(), + vo.getAuthorName(), + vo.getViewCount(), + vo.getLikeCount(), + vo.getScrapCount(), + vo.getImageUrl(), + vo.getHashtags()); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickBiblePostsWithCacheAdapter.java b/src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickBiblePostsWithCacheAdapter.java new file mode 100644 index 0000000..4cbb456 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickBiblePostsWithCacheAdapter.java @@ -0,0 +1,45 @@ +package com.ftm.server.adapter.out.cache; + +import static com.ftm.server.common.consts.StaticConsts.*; + +import com.ftm.server.application.port.out.cache.LoadUserPickBiblePostsWithCachePort; +import com.ftm.server.application.port.out.persistence.post.LoadPostPort; +import com.ftm.server.application.query.FindUserPickBiblePostsQuery; +import com.ftm.server.application.vo.post.PostWithIdAndAuthorVo; +import com.ftm.server.common.annotation.Adapter; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; + +@Adapter +@RequiredArgsConstructor +public class LoadUserPickBiblePostsWithCacheAdapter implements LoadUserPickBiblePostsWithCachePort { + + private final LoadPostPort loadPostPort; + + @Override + @Cacheable( + cacheNames = USER_PICK_BIBLE_POSTS_CACHE_NAME, + key = USER_PICK_BIBLE_POSTS_CACHE_KEY_ALL) + public List getUserPickBiblePost() { + return execute(); + } + + @Override + @CachePut(cacheNames = USER_PICK_BIBLE_POSTS_CACHE_NAME, key = USER_PICK_BIBLE_POSTS_CACHE_NAME) + public List getUserPickBiblePostCachePut() { + return execute(); + } + + public List execute() { + // 최근 1개월 상위 4개 post id를 조회 + + List postList = + loadPostPort.loadUserPickBiblePosts(FindUserPickBiblePostsQuery.of(4)); + + if (postList.isEmpty()) return List.of(); + + return postList; + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java index bea5e0d..774df68 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java @@ -5,10 +5,7 @@ import com.ftm.server.adapter.out.persistence.repository.*; import com.ftm.server.application.port.out.persistence.post.*; import com.ftm.server.application.query.*; -import com.ftm.server.application.vo.post.PostAndBookmarkCountVo; -import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; -import com.ftm.server.application.vo.post.UserIdAndNameVo; -import com.ftm.server.application.vo.post.UserWithPostCountVo; +import com.ftm.server.application.vo.post.*; import com.ftm.server.common.annotation.Adapter; import com.ftm.server.common.exception.CustomException; import com.ftm.server.common.response.enums.ErrorResponseCode; @@ -160,6 +157,17 @@ public List loadUserPickPopularPosts(FindUserPickPopularPostsQuery query) .toList(); } + @Override + public List loadUserPickBiblePosts(FindUserPickBiblePostsQuery query) { + return postRepository.findTopNPostsByLikeCount(query.getLimit()); + } + + @Override + public List loadPostWithUserAndBookmarkCount( + FindByIdsQuery query) { + return postRepository.findAllPostsWithUserAndBookmarkCount(query); + } + @Override public List loadPostImagesByPostId(FindByPostIdQuery query) { PostJpaEntity postJpaEntity = diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostRepository.java index ce4a9b8..01e88a7 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostRepository.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostRepository.java @@ -2,6 +2,7 @@ import com.ftm.server.adapter.out.persistence.model.PostJpaEntity; import com.ftm.server.application.vo.post.PostAndBookmarkCountVo; +import com.ftm.server.application.vo.post.PostWithIdAndAuthorVo; import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -41,6 +42,10 @@ public interface PostRepository List findTopNPostsByViewCountAndLikeCount( @Param("since") LocalDateTime since, @Param("limit") int limit); + @Query( + "select new com.ftm.server.application.vo.post.PostWithIdAndAuthorVo(p.id) from PostJpaEntity p order by p.likeCount DESC") + List findTopNPostsByLikeCount(@Param("limit") int limit); + @Query( """ SELECT new com.ftm.server.application.vo.post.PostAndBookmarkCountVo(p.id, COUNT(b)) diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepository.java index e40638b..31face8 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepository.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepository.java @@ -1,7 +1,9 @@ package com.ftm.server.adapter.out.persistence.repository; +import com.ftm.server.application.query.FindByIdsQuery; import com.ftm.server.application.query.FindPostsByCreatedDateQuery; import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import com.ftm.server.application.vo.post.PostWithUserAndBookmarkCountVo; import com.ftm.server.application.vo.post.UserWithPostCountVo; import java.util.List; @@ -11,4 +13,6 @@ public interface PostWithBookmarkCustomRepository { List findAllPostsWithUserAndBookmarkCount( FindPostsByCreatedDateQuery query); + + List findAllPostsWithUserAndBookmarkCount(FindByIdsQuery query); } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepositoryImpl.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepositoryImpl.java index dd44b6e..15973a7 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepositoryImpl.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepositoryImpl.java @@ -3,8 +3,10 @@ import static com.ftm.server.adapter.out.persistence.model.QBookmarkJpaEntity.bookmarkJpaEntity; import static com.ftm.server.adapter.out.persistence.model.QPostJpaEntity.postJpaEntity; +import com.ftm.server.application.query.FindByIdsQuery; import com.ftm.server.application.query.FindPostsByCreatedDateQuery; import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import com.ftm.server.application.vo.post.PostWithUserAndBookmarkCountVo; import com.ftm.server.application.vo.post.UserWithPostCountVo; import com.ftm.server.domain.enums.UserRole; import com.querydsl.core.types.Projections; @@ -81,4 +83,36 @@ public List findAllPostsWithUserAndBookmarkCount( .groupBy(postJpaEntity.user.id, postJpaEntity.user.nickname) .fetch(); } + + @Override + public List findAllPostsWithUserAndBookmarkCount( + FindByIdsQuery query) { + return queryFactory + .select( + Projections.constructor( + PostWithUserAndBookmarkCountVo.class, + postJpaEntity.id, + postJpaEntity.user.id, + postJpaEntity.user.nickname, + postJpaEntity.title, + postJpaEntity.content, + postJpaEntity.hashtags, + postJpaEntity.viewCount, // sum() 쓰지 마세요 + postJpaEntity.likeCount, // sum() 쓰지 마세요 + bookmarkJpaEntity.id.countDistinct() // 북마크 개수 + )) + .from(postJpaEntity) + .leftJoin(bookmarkJpaEntity) + .on(bookmarkJpaEntity.post.eq(postJpaEntity)) + .groupBy( + postJpaEntity.id, + postJpaEntity.user.id, + postJpaEntity.user.nickname, + postJpaEntity.title, + postJpaEntity.content, + postJpaEntity.hashtags, + postJpaEntity.viewCount, + postJpaEntity.likeCount) + .fetch(); + } } diff --git a/src/main/java/com/ftm/server/adapter/out/scheduler/GetUserPickBiblePostsScheduler.java b/src/main/java/com/ftm/server/adapter/out/scheduler/GetUserPickBiblePostsScheduler.java new file mode 100644 index 0000000..c78a061 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/scheduler/GetUserPickBiblePostsScheduler.java @@ -0,0 +1,27 @@ +package com.ftm.server.adapter.out.scheduler; + +import com.ftm.server.application.port.out.cache.LoadUserPickBiblePostsWithCachePort; +import com.ftm.server.common.annotation.Adapter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; + +@Slf4j +@Adapter +@RequiredArgsConstructor +public class GetUserPickBiblePostsScheduler { + + private final LoadUserPickBiblePostsWithCachePort loadUserPickBiblePostsWithCachePort; + + // 마지막 실행으로부터 57분 뒤에 재실행됨. + // cache TTL 값인 1시간이 끝나기 이전에 캐시를 업데이트 해 놓는다. + @Scheduled(fixedRateString = "PT57M", initialDelayString = "PT1M") + public void run() { + log.info( + "Loading UserPick Bible Posts at {}", + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + loadUserPickBiblePostsWithCachePort.getUserPickBiblePostCachePut(); + } +} diff --git a/src/main/java/com/ftm/server/application/port/in/user/GetUserPickPopularPostsUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/GetUserPickPopularPostsUseCase.java similarity index 83% rename from src/main/java/com/ftm/server/application/port/in/user/GetUserPickPopularPostsUseCase.java rename to src/main/java/com/ftm/server/application/port/in/post/GetUserPickPopularPostsUseCase.java index e8bcc4f..8274abd 100644 --- a/src/main/java/com/ftm/server/application/port/in/user/GetUserPickPopularPostsUseCase.java +++ b/src/main/java/com/ftm/server/application/port/in/post/GetUserPickPopularPostsUseCase.java @@ -1,4 +1,4 @@ -package com.ftm.server.application.port.in.user; +package com.ftm.server.application.port.in.post; import com.ftm.server.application.vo.post.UserPickPopularPostsVo; import com.ftm.server.common.annotation.UseCase; diff --git a/src/main/java/com/ftm/server/application/port/in/post/LoadUserPickBiblePostsUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/LoadUserPickBiblePostsUseCase.java new file mode 100644 index 0000000..7747141 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/post/LoadUserPickBiblePostsUseCase.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.port.in.post; + +import com.ftm.server.application.vo.post.UserPickBiblePostsVo; +import com.ftm.server.common.annotation.UseCase; +import java.util.List; + +@UseCase +public interface LoadUserPickBiblePostsUseCase { + List execute(); +} diff --git a/src/main/java/com/ftm/server/application/port/out/cache/LoadUserPickBiblePostsWithCachePort.java b/src/main/java/com/ftm/server/application/port/out/cache/LoadUserPickBiblePostsWithCachePort.java new file mode 100644 index 0000000..23fa040 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/cache/LoadUserPickBiblePostsWithCachePort.java @@ -0,0 +1,11 @@ +package com.ftm.server.application.port.out.cache; + +import com.ftm.server.application.vo.post.PostWithIdAndAuthorVo; +import java.util.List; + +public interface LoadUserPickBiblePostsWithCachePort { + + List getUserPickBiblePost(); + + List getUserPickBiblePostCachePut(); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java index 740419b..95ab848 100644 --- a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java @@ -1,9 +1,7 @@ package com.ftm.server.application.port.out.persistence.post; -import com.ftm.server.application.query.FindByIdQuery; -import com.ftm.server.application.query.FindByUserIdsQuery; -import com.ftm.server.application.query.FindPostByDeleteOptionQuery; -import com.ftm.server.application.query.FindUserPickPopularPostsQuery; +import com.ftm.server.application.query.*; +import com.ftm.server.application.vo.post.PostWithIdAndAuthorVo; import com.ftm.server.common.annotation.Port; import com.ftm.server.domain.entity.Post; import java.util.List; @@ -19,4 +17,6 @@ public interface LoadPostPort { List loadPostsByDeleteOption(FindPostByDeleteOptionQuery query); List loadUserPickPopularPosts(FindUserPickPopularPostsQuery query); + + List loadUserPickBiblePosts(FindUserPickBiblePostsQuery query); } diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostWithBookmarkCountPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostWithBookmarkCountPort.java index 8bde99c..d23616d 100644 --- a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostWithBookmarkCountPort.java +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostWithBookmarkCountPort.java @@ -1,9 +1,11 @@ package com.ftm.server.application.port.out.persistence.post; import com.ftm.server.application.query.FindBookmarkCountByPostIdsQuery; +import com.ftm.server.application.query.FindByIdsQuery; import com.ftm.server.application.query.FindPostsByCreatedDateQuery; import com.ftm.server.application.vo.post.PostAndBookmarkCountVo; import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import com.ftm.server.application.vo.post.PostWithUserAndBookmarkCountVo; import com.ftm.server.application.vo.post.UserWithPostCountVo; import com.ftm.server.common.annotation.Port; import java.util.List; @@ -16,4 +18,6 @@ List loadAllPostsWithUserAndBookmarkCount( FindPostsByCreatedDateQuery query); List getPostAndBookmarkCount(FindBookmarkCountByPostIdsQuery query); + + List loadPostWithUserAndBookmarkCount(FindByIdsQuery query); } diff --git a/src/main/java/com/ftm/server/application/query/FindUserPickBiblePostsQuery.java b/src/main/java/com/ftm/server/application/query/FindUserPickBiblePostsQuery.java new file mode 100644 index 0000000..d7d8bb3 --- /dev/null +++ b/src/main/java/com/ftm/server/application/query/FindUserPickBiblePostsQuery.java @@ -0,0 +1,15 @@ +package com.ftm.server.application.query; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FindUserPickBiblePostsQuery { + + private final Integer limit; + + public static FindUserPickBiblePostsQuery of(Integer limit) { + return new FindUserPickBiblePostsQuery(limit); + } +} diff --git a/src/main/java/com/ftm/server/application/service/post/GetUserPickPopularPostsService.java b/src/main/java/com/ftm/server/application/service/post/GetUserPickPopularPostsService.java index 651f50f..05b20c6 100644 --- a/src/main/java/com/ftm/server/application/service/post/GetUserPickPopularPostsService.java +++ b/src/main/java/com/ftm/server/application/service/post/GetUserPickPopularPostsService.java @@ -1,6 +1,6 @@ package com.ftm.server.application.service.post; -import com.ftm.server.application.port.in.user.GetUserPickPopularPostsUseCase; +import com.ftm.server.application.port.in.post.GetUserPickPopularPostsUseCase; import com.ftm.server.application.port.out.cache.LoadUserPickPopularWithCachePort; import com.ftm.server.application.port.out.persistence.post.LoadPostImagePort; import com.ftm.server.application.port.out.persistence.post.LoadPostWithBookmarkCountPort; diff --git a/src/main/java/com/ftm/server/application/service/post/LoadUserPickBiblePostsService.java b/src/main/java/com/ftm/server/application/service/post/LoadUserPickBiblePostsService.java new file mode 100644 index 0000000..98029d3 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/post/LoadUserPickBiblePostsService.java @@ -0,0 +1,82 @@ +package com.ftm.server.application.service.post; + +import com.ftm.server.application.port.in.post.LoadUserPickBiblePostsUseCase; +import com.ftm.server.application.port.out.cache.LoadUserPickBiblePostsWithCachePort; +import com.ftm.server.application.port.out.persistence.post.LoadPostImagePort; +import com.ftm.server.application.port.out.persistence.post.LoadPostWithBookmarkCountPort; +import com.ftm.server.application.query.FindByIdsQuery; +import com.ftm.server.application.vo.post.*; +import com.ftm.server.common.consts.PropertiesHolder; +import com.ftm.server.domain.entity.PostImage; +import com.ftm.server.domain.enums.PostHashtag; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoadUserPickBiblePostsService implements LoadUserPickBiblePostsUseCase { + + private final LoadUserPickBiblePostsWithCachePort loadUserPickBibleWithCachePort; + + private final LoadPostWithBookmarkCountPort loadPostWithBookmarkCountPort; + private final LoadPostImagePort loadPostImagePort; + + @Override + public List execute() { + // 1. 좋아요 누적 순으로 상위 4개의 게시물을 cache 에서 조회 + List postList = + loadUserPickBibleWithCachePort.getUserPickBiblePost(); + + // 2) id 목록 + List postIds = postList.stream().map(PostWithIdAndAuthorVo::getPostId).toList(); + + // 3) post 상세 조회 + Map detailPostMap = + loadPostWithBookmarkCountPort + .loadPostWithUserAndBookmarkCount(FindByIdsQuery.from(postIds)) + .stream() + .collect(Collectors.toMap(PostWithUserAndBookmarkCountVo::getId, vo -> vo)); + + // 4) 대표 이미지 + List postImages = + loadPostImagePort.loadRepresentativeImagesByPostIds(FindByIdsQuery.from(postIds)); + var imageUrlMap = + postImages.stream() + .collect( + java.util.stream.Collectors.toMap( + PostImage::getPostId, + PostImage::getObjectKey, + (a, b) -> a // 중복 시 첫 이미지 + )); + + // 5) 합치기 (postList 순서 = 랭킹) + return IntStream.range(0, postIds.size()) + .mapToObj( + i -> { + Long postId = postIds.get(i); + PostWithUserAndBookmarkCountVo p = detailPostMap.get(postId); + int ranking = i + 1; + String imageUrl = + imageUrlMap.getOrDefault( + p.getId(), + PropertiesHolder.POST_DEFAULT_IMAGE); // 없으면 null + List hashtags = + p.getHashtags() == null || p.getHashtags().length == 0 + ? List.of() + : Arrays.stream(p.getHashtags()) + .map(PostHashtag::getTag) + .toList(); + return UserPickBiblePostsVo.of( + ranking, + p, + PropertiesHolder.CDN_PATH + "/" + imageUrl, + hashtags); + }) + .toList(); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/post/PostWithIdAndAuthorVo.java b/src/main/java/com/ftm/server/application/vo/post/PostWithIdAndAuthorVo.java new file mode 100644 index 0000000..1d695c5 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/PostWithIdAndAuthorVo.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.vo.post; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PostWithIdAndAuthorVo { + private final Long postId; +} diff --git a/src/main/java/com/ftm/server/application/vo/post/PostWithUserAndBookmarkCountVo.java b/src/main/java/com/ftm/server/application/vo/post/PostWithUserAndBookmarkCountVo.java new file mode 100644 index 0000000..3f7871f --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/PostWithUserAndBookmarkCountVo.java @@ -0,0 +1,19 @@ +package com.ftm.server.application.vo.post; + +import com.ftm.server.domain.enums.PostHashtag; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PostWithUserAndBookmarkCountVo { + private final Long id; + private final Long userId; + private final String userName; + private final String title; + private final String content; + private final PostHashtag[] hashtags; + private final Integer viewCount; + private final Integer likeCount; + private final Long scrapCount; +} diff --git a/src/main/java/com/ftm/server/application/vo/post/UserPickBiblePostsVo.java b/src/main/java/com/ftm/server/application/vo/post/UserPickBiblePostsVo.java new file mode 100644 index 0000000..73fb3b7 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/UserPickBiblePostsVo.java @@ -0,0 +1,38 @@ +package com.ftm.server.application.vo.post; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class UserPickBiblePostsVo { + private final Integer ranking; + private final Long postId; + private final String title; + private final Long authorId; + private final String authorName; + private final Integer viewCount; + private final Integer likeCount; + private final Long scrapCount; + private final String imageUrl; + private final List hashtags; + + public static UserPickBiblePostsVo of( + Integer ranking, + PostWithUserAndBookmarkCountVo post, + String imageUrl, + List hashtags) { + return new UserPickBiblePostsVo( + ranking, + post.getId(), + post.getTitle(), + post.getUserId(), + post.getUserName(), + post.getViewCount(), + post.getLikeCount(), + post.getScrapCount(), + imageUrl, + hashtags); + } +} diff --git a/src/main/java/com/ftm/server/common/consts/StaticConsts.java b/src/main/java/com/ftm/server/common/consts/StaticConsts.java index 502426b..402cccc 100644 --- a/src/main/java/com/ftm/server/common/consts/StaticConsts.java +++ b/src/main/java/com/ftm/server/common/consts/StaticConsts.java @@ -21,4 +21,7 @@ public class StaticConsts { public static final String USER_PICK_POPULAR_POSTS_CACHE_NAME = "ftm:posts:userpick:popular"; public static final String USER_PICK_POPULAR_POSTS_CACHE_KEY_ALL = "'all'"; + + public static final String USER_PICK_BIBLE_POSTS_CACHE_NAME = "ftm:posts:userpick:bible"; + public static final String USER_PICK_BIBLE_POSTS_CACHE_KEY_ALL = "'all'"; } diff --git a/src/test/java/com/ftm/server/post/LoadUserPickBiblePostsTest.java b/src/test/java/com/ftm/server/post/LoadUserPickBiblePostsTest.java new file mode 100644 index 0000000..000ac57 --- /dev/null +++ b/src/test/java/com/ftm/server/post/LoadUserPickBiblePostsTest.java @@ -0,0 +1,128 @@ +package com.ftm.server.post; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.ftm.server.BaseTest; +import com.ftm.server.adapter.in.web.post.dto.request.SavePostRequest; +import com.ftm.server.application.command.post.SavePostCommand; +import com.ftm.server.application.port.out.persistence.post.SavePostPort; +import com.ftm.server.domain.entity.Post; +import com.ftm.server.domain.entity.User; +import com.ftm.server.domain.enums.PostHashtag; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +public class LoadUserPickBiblePostsTest extends BaseTest { + + @Autowired private SavePostPort savePostPort; + + private final List responseFields = + List.of( + fieldWithPath("status").type(NUMBER).description("응답 상태"), + fieldWithPath("code").type(STRING).description("상태 코드"), + fieldWithPath("message").type(STRING).description("메시지"), + fieldWithPath("data") + .type(ARRAY) + .optional() + .description("응답 데이터 : 대상 게시물이 없는 경우 빈 배열"), + fieldWithPath("data[].ranking").type(NUMBER).description("순위"), + fieldWithPath("data[].postId").type(NUMBER).description("게시글 ID"), + fieldWithPath("data[].title").type(STRING).description("게시글 제목"), + fieldWithPath("data[].authorId").type(NUMBER).description("작성자 user ID"), + fieldWithPath("data[].authorName").type(STRING).description("작성자 이름"), + fieldWithPath("data[].viewCount").type(NUMBER).description("조회수"), + fieldWithPath("data[].likeCount").type(NUMBER).description("좋아요 수"), + fieldWithPath("data[].scrapCount").type(NUMBER).description("스크랩 수"), + fieldWithPath("data[].imageUrl").type(STRING).description("이미지 url"), + fieldWithPath("data[].hashtags") + .type(ARRAY) + .description("게시글 해시태그 : 한글 태그 표시. 없는 경우 빈 배열([])로 표시")); + + private ResultActions getResultActions() throws Exception { + return mockMvc.perform(RestDocumentationRequestBuilders.get("/api/posts/userpick/bible")); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "loadUserPickBible/" + identifier, + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint(), getModifiedHeader()), + responseFields(responseFields), + resource( + ResourceSnippetParameters.builder() + .tag("유저픽 게시글") + .summary("\"그루밍 바이블\" 목록 조회 api") + .description("그루밍 라운지 내 \"그루밍 바이블\" 목록 조회 api 입니다.") + .responseFields(responseFields) + .build())); + } + + @Test + @Transactional + @DisplayName("테스트 성공") + public void test1() throws Exception { + // given + + SessionAndUser sessionAndUser = createUserAndLoginAndReturnUser(); // 로그인 처리 + + User user = sessionAndUser.user(); + + // test 용 post 생성 + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user.getId(), + new SavePostRequest( + "test1", + List.of(PostHashtag.SUN_CARE, PostHashtag.CLEANSING), + "content1", + new ArrayList<>()), + new ArrayList<>(), + new ArrayList<>()))); + + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user.getId(), + new SavePostRequest( + "test2", + List.of( + PostHashtag.BOTTOM_CLOTHING, + PostHashtag.FASHION_ACCESSORIES), + "content2", + new ArrayList<>()), + new ArrayList<>(), + new ArrayList<>()))); + + // when + ResultActions resultActions = getResultActions(); + + // then + resultActions + .andExpect(status().is(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data", hasSize(2))); + + // documentation + resultActions.andDo(getDocument(1)); + } +} diff --git a/src/test/java/com/ftm/server/post/LoadUserPickPopularPostsTest.java b/src/test/java/com/ftm/server/post/LoadUserPickPopularPostsTest.java index a22d08c..c256451 100644 --- a/src/test/java/com/ftm/server/post/LoadUserPickPopularPostsTest.java +++ b/src/test/java/com/ftm/server/post/LoadUserPickPopularPostsTest.java @@ -81,7 +81,6 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { @DisplayName("테스트 성공") public void test1() throws Exception { // given - SessionAndUser sessionAndUser = createUserAndLoginAndReturnUser(); // 로그인 처리 User user = sessionAndUser.user();