diff --git a/src/docs/asciidoc/post-api.adoc b/src/docs/asciidoc/post-api.adoc index cd232a9..0c43751 100644 --- a/src/docs/asciidoc/post-api.adoc +++ b/src/docs/asciidoc/post-api.adoc @@ -290,4 +290,18 @@ include::{snippetsDir}/loadUserPickBible/1/http-response.adoc[] ==== Response Body Fields include::{snippetsDir}/loadUserPickBible/1/response-fields.adoc[] +--- + + +=== **11. 그루밍 라운지 - "다시 찾아보고 싶은 그루밍" 조회** + +==== Request +include::{snippetsDir}/loadUserPickTopBookmark/1/http-request.adoc[] + +==== 성공 Response +include::{snippetsDir}/loadUserPickTopBookmark/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/loadUserPickTopBookmark/1/response-fields.adoc[] + --- \ No newline at end of file diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadUserPickTopBookmarkPostsController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadUserPickTopBookmarkPostsController.java new file mode 100644 index 0000000..d0421ac --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadUserPickTopBookmarkPostsController.java @@ -0,0 +1,25 @@ +package com.ftm.server.adapter.in.web.post.controller; + +import com.ftm.server.adapter.in.web.post.dto.response.LoadUserPickTopBookmarkPostsResponse; +import com.ftm.server.application.port.in.post.LoadUserPickTopBookmarkPostsUseCase; +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.SuccessResponseCode; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class LoadUserPickTopBookmarkPostsController { + + private final LoadUserPickTopBookmarkPostsUseCase useCase; + + @GetMapping("/api/posts/userpick/top-bookmarks") + public ResponseEntity loadTopBookmarkPosts() { + List result = + useCase.execute().stream().map(LoadUserPickTopBookmarkPostsResponse::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/LoadUserPickTopBookmarkPostsResponse.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadUserPickTopBookmarkPostsResponse.java new file mode 100644 index 0000000..9735cbd --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadUserPickTopBookmarkPostsResponse.java @@ -0,0 +1,36 @@ +package com.ftm.server.adapter.in.web.post.dto.response; + +import com.ftm.server.application.vo.post.LoadUserPickTopBookmarkPostsVo; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoadUserPickTopBookmarkPostsResponse { + + 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 LoadUserPickTopBookmarkPostsResponse from(LoadUserPickTopBookmarkPostsVo vo) { + return new LoadUserPickTopBookmarkPostsResponse( + 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 index 4cbb456..853b10c 100644 --- a/src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickBiblePostsWithCacheAdapter.java +++ b/src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickBiblePostsWithCacheAdapter.java @@ -27,7 +27,9 @@ public List getUserPickBiblePost() { } @Override - @CachePut(cacheNames = USER_PICK_BIBLE_POSTS_CACHE_NAME, key = USER_PICK_BIBLE_POSTS_CACHE_NAME) + @CachePut( + cacheNames = USER_PICK_BIBLE_POSTS_CACHE_NAME, + key = USER_PICK_BIBLE_POSTS_CACHE_KEY_ALL) public List getUserPickBiblePostCachePut() { return execute(); } diff --git a/src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickTopBookmarkPostsWithCacheAdapter.java b/src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickTopBookmarkPostsWithCacheAdapter.java new file mode 100644 index 0000000..b9268cd --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/cache/LoadUserPickTopBookmarkPostsWithCacheAdapter.java @@ -0,0 +1,47 @@ +package com.ftm.server.adapter.out.cache; + +import static com.ftm.server.common.consts.StaticConsts.USER_PICK_TOP_BOOKMARK_POSTS_CACHE_KEY_ALL; +import static com.ftm.server.common.consts.StaticConsts.USER_PICK_TOP_BOOKMARK_POSTS_CACHE_NAME; + +import com.ftm.server.application.port.out.cache.LoadUserPickTopBookmarkPostsWithCachePort; +import com.ftm.server.application.port.out.persistence.post.LoadPostPort; +import com.ftm.server.application.query.FindTopPostsByBookmarkCountQuery; +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.CacheConfig; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; + +@Adapter +@RequiredArgsConstructor +@CacheConfig(cacheManager = "userPickTopBookmarkPostsCacheManager") +public class LoadUserPickTopBookmarkPostsWithCacheAdapter + implements LoadUserPickTopBookmarkPostsWithCachePort { + + private final LoadPostPort loadPostPort; + + @Override + @Cacheable( + cacheNames = USER_PICK_TOP_BOOKMARK_POSTS_CACHE_NAME, + key = USER_PICK_TOP_BOOKMARK_POSTS_CACHE_KEY_ALL) + public List getTopBookmarkPosts() { + return execute(); + } + + @Override + @CachePut( + cacheNames = USER_PICK_TOP_BOOKMARK_POSTS_CACHE_NAME, + key = USER_PICK_TOP_BOOKMARK_POSTS_CACHE_KEY_ALL) + public List getTopBookmarkPostsCachePut() { + return execute(); + } + + private List execute() { + List posts = + loadPostPort.loadTopPostsByBookmarkCount(FindTopPostsByBookmarkCountQuery.of(4)); + if (posts.isEmpty()) return List.of(); + return posts; + } +} 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 774df68..0fa6a9e 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 @@ -162,6 +162,12 @@ public List loadUserPickBiblePosts(FindUserPickBiblePosts return postRepository.findTopNPostsByLikeCount(query.getLimit()); } + @Override + public List loadTopPostsByBookmarkCount( + FindTopPostsByBookmarkCountQuery query) { + return postRepository.findTopNPostsByBookmarkCount(query.getLimit()); + } + @Override public List loadPostWithUserAndBookmarkCount( FindByIdsQuery query) { 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 31face8..c3d47bd 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 @@ -3,6 +3,7 @@ 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.PostWithIdAndAuthorVo; import com.ftm.server.application.vo.post.PostWithUserAndBookmarkCountVo; import com.ftm.server.application.vo.post.UserWithPostCountVo; import java.util.List; @@ -15,4 +16,6 @@ List findAllPostsWithUserAndBookmarkCount( FindPostsByCreatedDateQuery query); List findAllPostsWithUserAndBookmarkCount(FindByIdsQuery query); + + List findTopNPostsByBookmarkCount(int limit); } 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 15973a7..1452704 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 @@ -6,6 +6,7 @@ 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.PostWithIdAndAuthorVo; import com.ftm.server.application.vo.post.PostWithUserAndBookmarkCountVo; import com.ftm.server.application.vo.post.UserWithPostCountVo; import com.ftm.server.domain.enums.UserRole; @@ -115,4 +116,18 @@ public List findAllPostsWithUserAndBookmarkCount postJpaEntity.likeCount) .fetch(); } + + @Override + public List findTopNPostsByBookmarkCount(int limit) { + return queryFactory + .select(Projections.constructor(PostWithIdAndAuthorVo.class, postJpaEntity.id)) + .from(postJpaEntity) + .leftJoin(bookmarkJpaEntity) + .on(bookmarkJpaEntity.post.eq(postJpaEntity)) + .where(postJpaEntity.isDeleted.eq(false)) + .groupBy(postJpaEntity.id) + .orderBy(bookmarkJpaEntity.id.countDistinct().desc(), postJpaEntity.id.desc()) + .limit(limit) + .fetch(); + } } diff --git a/src/main/java/com/ftm/server/adapter/out/scheduler/GetUserPickTopBookmarkPostsScheduler.java b/src/main/java/com/ftm/server/adapter/out/scheduler/GetUserPickTopBookmarkPostsScheduler.java new file mode 100644 index 0000000..1d0d140 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/scheduler/GetUserPickTopBookmarkPostsScheduler.java @@ -0,0 +1,26 @@ +package com.ftm.server.adapter.out.scheduler; + +import com.ftm.server.application.port.out.cache.LoadUserPickTopBookmarkPostsWithCachePort; +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 GetUserPickTopBookmarkPostsScheduler { + + private final LoadUserPickTopBookmarkPostsWithCachePort cachePort; + + // cache TTL 1시간 만료 전에 주기적으로 58분 간격으로 갱신 + @Scheduled(fixedRateString = "PT58M", initialDelayString = "PT1M") + public void run() { + log.info( + "Loading UserPick Top Bookmark Posts at {}", + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + cachePort.getTopBookmarkPostsCachePut(); + } +} diff --git a/src/main/java/com/ftm/server/application/port/in/post/LoadUserPickTopBookmarkPostsUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/LoadUserPickTopBookmarkPostsUseCase.java new file mode 100644 index 0000000..a5052a5 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/post/LoadUserPickTopBookmarkPostsUseCase.java @@ -0,0 +1,8 @@ +package com.ftm.server.application.port.in.post; + +import com.ftm.server.application.vo.post.LoadUserPickTopBookmarkPostsVo; +import java.util.List; + +public interface LoadUserPickTopBookmarkPostsUseCase { + List execute(); +} diff --git a/src/main/java/com/ftm/server/application/port/out/cache/LoadUserPickTopBookmarkPostsWithCachePort.java b/src/main/java/com/ftm/server/application/port/out/cache/LoadUserPickTopBookmarkPostsWithCachePort.java new file mode 100644 index 0000000..eb7e1ff --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/cache/LoadUserPickTopBookmarkPostsWithCachePort.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 LoadUserPickTopBookmarkPostsWithCachePort { + + List getTopBookmarkPosts(); + + List getTopBookmarkPostsCachePut(); +} 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 95ab848..55650cb 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 @@ -19,4 +19,6 @@ public interface LoadPostPort { List loadUserPickPopularPosts(FindUserPickPopularPostsQuery query); List loadUserPickBiblePosts(FindUserPickBiblePostsQuery query); + + List loadTopPostsByBookmarkCount(FindTopPostsByBookmarkCountQuery query); } diff --git a/src/main/java/com/ftm/server/application/query/FindTopPostsByBookmarkCountQuery.java b/src/main/java/com/ftm/server/application/query/FindTopPostsByBookmarkCountQuery.java new file mode 100644 index 0000000..f5b34a9 --- /dev/null +++ b/src/main/java/com/ftm/server/application/query/FindTopPostsByBookmarkCountQuery.java @@ -0,0 +1,14 @@ +package com.ftm.server.application.query; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FindTopPostsByBookmarkCountQuery { + private final int limit; + + public static FindTopPostsByBookmarkCountQuery of(int limit) { + return new FindTopPostsByBookmarkCountQuery(limit); + } +} diff --git a/src/main/java/com/ftm/server/application/service/post/LoadUserPickTopBookmarkPostsService.java b/src/main/java/com/ftm/server/application/service/post/LoadUserPickTopBookmarkPostsService.java new file mode 100644 index 0000000..d5558f7 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/post/LoadUserPickTopBookmarkPostsService.java @@ -0,0 +1,79 @@ +package com.ftm.server.application.service.post; + +import com.ftm.server.application.port.in.post.LoadUserPickTopBookmarkPostsUseCase; +import com.ftm.server.application.port.out.cache.LoadUserPickTopBookmarkPostsWithCachePort; +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.LoadUserPickTopBookmarkPostsVo; +import com.ftm.server.application.vo.post.PostWithIdAndAuthorVo; +import com.ftm.server.application.vo.post.PostWithUserAndBookmarkCountVo; +import com.ftm.server.common.consts.PropertiesHolder; +import com.ftm.server.domain.entity.PostImage; +import com.ftm.server.domain.enums.PostHashtag; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoadUserPickTopBookmarkPostsService implements LoadUserPickTopBookmarkPostsUseCase { + + private final LoadUserPickTopBookmarkPostsWithCachePort cachePort; + private final LoadPostWithBookmarkCountPort loadPostWithBookmarkCountPort; + private final LoadPostImagePort loadPostImagePort; + + @Override + public List execute() { + // 1) 캐시에서 상위 id 목록 조회 (4개) + List postList = cachePort.getTopBookmarkPosts(); + if (postList.isEmpty()) return List.of(); + + // 2) id 목록 + List postIds = postList.stream().map(PostWithIdAndAuthorVo::getPostId).toList(); + + // 3) 상세 조회 (유저/북마크 수 포함) + Map detailPostMap = + loadPostWithBookmarkCountPort + .loadPostWithUserAndBookmarkCount(FindByIdsQuery.from(postIds)) + .stream() + .collect(Collectors.toMap(PostWithUserAndBookmarkCountVo::getId, vo -> vo)); + + // 4) 대표 이미지 + List postImages = + loadPostImagePort.loadRepresentativeImagesByPostIds(FindByIdsQuery.from(postIds)); + Map 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); + List hashtags = + p.getHashtags() == null || p.getHashtags().length == 0 + ? List.of() + : Arrays.stream(p.getHashtags()) + .map(PostHashtag::getTag) + .toList(); + return LoadUserPickTopBookmarkPostsVo.of( + ranking, + p, + PropertiesHolder.CDN_PATH + "/" + imageUrl, + hashtags); + }) + .toList(); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/post/LoadUserPickTopBookmarkPostsVo.java b/src/main/java/com/ftm/server/application/vo/post/LoadUserPickTopBookmarkPostsVo.java new file mode 100644 index 0000000..1354e0e --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/LoadUserPickTopBookmarkPostsVo.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 LoadUserPickTopBookmarkPostsVo { + 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 LoadUserPickTopBookmarkPostsVo of( + Integer ranking, + PostWithUserAndBookmarkCountVo post, + String imageUrl, + List hashtags) { + return new LoadUserPickTopBookmarkPostsVo( + 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 402cccc..ea83349 100644 --- a/src/main/java/com/ftm/server/common/consts/StaticConsts.java +++ b/src/main/java/com/ftm/server/common/consts/StaticConsts.java @@ -24,4 +24,8 @@ public class StaticConsts { 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'"; + + public static final String USER_PICK_TOP_BOOKMARK_POSTS_CACHE_NAME = + "ftm:posts:userpick:top-bookmarks"; + public static final String USER_PICK_TOP_BOOKMARK_POSTS_CACHE_KEY_ALL = "'all'"; } diff --git a/src/main/java/com/ftm/server/infrastructure/cache/CaffeineCacheConfig.java b/src/main/java/com/ftm/server/infrastructure/cache/CaffeineCacheConfig.java index 94d7705..fcaf6e0 100644 --- a/src/main/java/com/ftm/server/infrastructure/cache/CaffeineCacheConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/cache/CaffeineCacheConfig.java @@ -56,4 +56,11 @@ public CaffeineCacheManager cacheManagerForUserPickPopularPosts() { .expireAfterWrite(1, TimeUnit.HOURS)); return mgr; } + + @Bean("userPickTopBookmarkPostsCacheManager") // 유저픽 게시글 - 북마크 상위 게시물 용 캐시 매니저 + public CaffeineCacheManager cacheManagerForUserPickTopBookmarkPosts() { + CaffeineCacheManager mgr = new CaffeineCacheManager(); + mgr.setCaffeine(Caffeine.newBuilder().maximumSize(10).expireAfterWrite(1, TimeUnit.HOURS)); + return mgr; + } } diff --git a/src/test/java/com/ftm/server/post/LoadUserPickTopBookmarkPostsTest.java b/src/test/java/com/ftm/server/post/LoadUserPickTopBookmarkPostsTest.java new file mode 100644 index 0000000..4b9e1a7 --- /dev/null +++ b/src/test/java/com/ftm/server/post/LoadUserPickTopBookmarkPostsTest.java @@ -0,0 +1,129 @@ +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 LoadUserPickTopBookmarkPostsTest 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/top-bookmarks")); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "loadUserPickTopBookmark/" + 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)); + } +}