From b3d0c266b869601cc84a9a4ffac2af7f3010c511 Mon Sep 17 00:00:00 2001 From: myqewr Date: Wed, 28 May 2025 20:23:15 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=ED=8A=B8=EB=A0=8C=EB=94=A9=20?= =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80(#128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/post-api.adoc | 13 ++ .../GetTrendingPostsController.java | 29 +++++ .../response/GetTrendingPostsResponse.java | 29 +++++ .../LoadTrendingPostsWithCacheAdapter.java | 94 ++++++++++++++ .../post/PostDomainPersistenceAdapter.java | 17 ++- .../repository/PostRepository.java | 5 +- .../PostWithBookmarkCustomRepository.java | 10 ++ .../PostWithBookmarkCustomRepositoryImpl.java | 44 +++++++ .../in/post/LoadTrendingPostsUseCase.java | 10 ++ .../cache/LoadTrendingPostsWithCachePort.java | 9 ++ .../persistence/post/LoadPostImagePort.java | 2 + .../post/LoadPostWithBookmarkCountPort.java | 9 ++ .../query/FindPostsByCreatedDateQuery.java | 13 ++ .../post/LoadTrendingPostsService.java | 22 ++++ .../vo/post/PostWithBookmarkCountVo.java | 14 ++ .../application/vo/post/TrendingPostVo.java | 26 ++++ .../server/common/consts/StaticConsts.java | 3 + .../cache/CaffeineCacheConfig.java | 17 ++- .../ftm/server/post/LoadTrendingPostsTes.java | 120 ++++++++++++++++++ 19 files changed, 483 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/ftm/server/adapter/in/web/post/controller/GetTrendingPostsController.java create mode 100644 src/main/java/com/ftm/server/adapter/in/web/post/dto/response/GetTrendingPostsResponse.java create mode 100644 src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingPostsWithCacheAdapter.java create mode 100644 src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepository.java create mode 100644 src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepositoryImpl.java create mode 100644 src/main/java/com/ftm/server/application/port/in/post/LoadTrendingPostsUseCase.java create mode 100644 src/main/java/com/ftm/server/application/port/out/cache/LoadTrendingPostsWithCachePort.java create mode 100644 src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostWithBookmarkCountPort.java create mode 100644 src/main/java/com/ftm/server/application/query/FindPostsByCreatedDateQuery.java create mode 100644 src/main/java/com/ftm/server/application/service/post/LoadTrendingPostsService.java create mode 100644 src/main/java/com/ftm/server/application/vo/post/PostWithBookmarkCountVo.java create mode 100644 src/main/java/com/ftm/server/application/vo/post/TrendingPostVo.java create mode 100644 src/test/java/com/ftm/server/post/LoadTrendingPostsTes.java diff --git a/src/docs/asciidoc/post-api.adoc b/src/docs/asciidoc/post-api.adoc index 43420cc..6584c98 100644 --- a/src/docs/asciidoc/post-api.adoc +++ b/src/docs/asciidoc/post-api.adoc @@ -195,6 +195,19 @@ include::{snippetsDir}/deletePost/3/http-response.adoc[] include::{snippetsDir}/deletePost/4/http-response.adoc[] +=== **5. 트렌딩 게시글 조회** +트랜딩 게시글 상세 조회 api 입니다. + +==== Request +include::{snippetsDir}/loadTrendingPosts/1/http-request.adoc[] + +==== 성공 Response +include::{snippetsDir}/loadTrendingPosts/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/loadTrendingPosts/1/response-fields.adoc[] + +--- diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetTrendingPostsController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetTrendingPostsController.java new file mode 100644 index 0000000..7519974 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetTrendingPostsController.java @@ -0,0 +1,29 @@ +package com.ftm.server.adapter.in.web.post.controller; + +import com.ftm.server.adapter.in.web.post.dto.response.GetTrendingPostsResponse; +import com.ftm.server.application.port.in.post.LoadTrendingPostsUseCase; +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.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class GetTrendingPostsController { + + private final LoadTrendingPostsUseCase loadTrendingPostsUseCase; + + @GetMapping("/api/posts/trend") + public ResponseEntity getTrendingPosts() { + List result = + loadTrendingPostsUseCase.execute().stream() + .map(GetTrendingPostsResponse::from) + .toList(); + return ResponseEntity.status(HttpStatus.OK) + .body(ApiResponse.success(SuccessResponseCode.OK, result)); + } +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/GetTrendingPostsResponse.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/GetTrendingPostsResponse.java new file mode 100644 index 0000000..69b4abe --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/GetTrendingPostsResponse.java @@ -0,0 +1,29 @@ +package com.ftm.server.adapter.in.web.post.dto.response; + +import com.ftm.server.application.vo.post.TrendingPostVo; +import com.ftm.server.common.consts.PropertiesHolder; +import lombok.Data; + +@Data +public class GetTrendingPostsResponse { + private final Long postId; + private final Integer ranking; + private final String title; + private final Integer viewCount; + private final Integer likeCount; + private final Long scrapCount; + private final String imageUrl; + + public static GetTrendingPostsResponse from(TrendingPostVo vo) { + String imageUrl = + vo.getImageUrl() == null ? PropertiesHolder.POST_DEFAULT_IMAGE : vo.getImageUrl(); + return new GetTrendingPostsResponse( + vo.getPostId(), + vo.getRank(), + vo.getTitle(), + vo.getViewCount(), + vo.getLikeCount(), + vo.getScrapCount(), + PropertiesHolder.CDN_PATH + "/" + imageUrl); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingPostsWithCacheAdapter.java b/src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingPostsWithCacheAdapter.java new file mode 100644 index 0000000..85f24b1 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingPostsWithCacheAdapter.java @@ -0,0 +1,94 @@ +package com.ftm.server.adapter.out.cache; + +import static com.ftm.server.common.consts.StaticConsts.TRENDING_POSTS_CACHE_KEY_ALL; +import static com.ftm.server.common.consts.StaticConsts.TRENDING_POSTS_CACHE_NAME; + +import com.ftm.server.application.port.out.cache.LoadTrendingPostsWithCachePort; +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.query.FindPostsByCreatedDateQuery; +import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import com.ftm.server.application.vo.post.TrendingPostVo; +import com.ftm.server.common.annotation.Adapter; +import java.time.LocalDate; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; + +@Adapter +@Slf4j +@RequiredArgsConstructor +@CacheConfig(cacheManager = "trendingPostsCacheManager") +public class LoadTrendingPostsWithCacheAdapter implements LoadTrendingPostsWithCachePort { + + private final LoadPostWithBookmarkCountPort loadPostPort; + private final LoadPostImagePort loadPostImagePort; + + private static final int N = 15; + + @Override + @Cacheable(value = TRENDING_POSTS_CACHE_NAME, key = TRENDING_POSTS_CACHE_KEY_ALL) + public List loadTrendingPosts() { + + log.info("오잉 캐시 실행 안됨!!"); + // 현재보다 1주일 이전에 작성된 게시물만 조회(북마크 조회수 포함) (예전 게시물은 포함x) + List rawPosts = + loadPostPort.loadAllPostsWithBookmarkCount( + FindPostsByCreatedDateQuery.of(LocalDate.now())); + + if (rawPosts.isEmpty()) return List.of(); + + // 1. 최대값 계산 (정규화를 위해) + long maxView = 1, maxLike = 1, maxScrap = 1; + for (PostWithBookmarkCountVo post : rawPosts) { + maxView = Math.max(maxView, post.getViewCount()); + maxLike = Math.max(maxLike, post.getLikeCount()); + maxScrap = Math.max(maxScrap, post.getScrapCount()); + } + + // 2. 점수 계산 후 정렬 + long finalMaxView = maxView; + long finalMaxLike = maxLike; + long finalMaxScrap = maxScrap; + List topN = + rawPosts.stream() + .sorted( + Comparator.comparingDouble( + p -> + -((double) p.getViewCount() / finalMaxView + + (double) p.getLikeCount() + / finalMaxLike + + (double) p.getScrapCount() + / finalMaxScrap) + / 3.0)) + .limit(N) + .toList(); + + // 3. 각 게시물 이미지 조회 (없으면 null) + List postIds = topN.stream().map(PostWithBookmarkCountVo::getPostId).toList(); + + Map imageMap = new HashMap<>(); + postIds.forEach(p -> imageMap.put(p, null)); + loadPostImagePort + .loadRepresentativeImagesByPostIds(FindByIdsQuery.from(postIds)) + .forEach( + postImage -> imageMap.put(postImage.getPostId(), postImage.getObjectKey())); + + // 4. 결과 조합 (순위 부여) + return IntStream.range(0, topN.size()) + .mapToObj( + i -> { + PostWithBookmarkCountVo post = topN.get(i); + String imageKey = imageMap.get(post.getPostId()); + return TrendingPostVo.from(post, i + 1, imageKey); // rank = i+1 + }) + .toList(); + } +} 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 6543ba2..d501269 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,6 +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.PostWithBookmarkCountVo; import com.ftm.server.common.annotation.Adapter; import com.ftm.server.common.exception.CustomException; import com.ftm.server.common.response.enums.ErrorResponseCode; @@ -35,7 +36,8 @@ public class PostDomainPersistenceAdapter DeletePostPort, DeletePostImagePort, DeletePostProductPort, - DeletePostProductImagePort { + DeletePostProductImagePort, + LoadPostWithBookmarkCountPort { private final PostRepository postRepository; private final PostImageRepository postImageRepository; @@ -287,4 +289,17 @@ public void deletePostProductImages(List postProductImages) { List ids = postProductImages.stream().map(PostProductImage::getId).toList(); postProductImageRepository.deleteAllByIdInBatch(ids); } + + @Override + public List loadAllPostsWithBookmarkCount( + FindPostsByCreatedDateQuery query) { + return postRepository.findAllPostsWithBookmarkCount(query); + } + + @Override + public List loadRepresentativeImagesByPostIds(FindByIdsQuery query) { + return postImageRepository.findRepresentativeImagesByPostIdIn(query).stream() + .map(postImageMapper::toDomainEntity) + .toList(); + } } 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 87b2d43..ca6d23b 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 @@ -7,7 +7,10 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface PostRepository extends JpaRepository, PostCustomRepository { +public interface PostRepository + extends JpaRepository, + PostCustomRepository, + PostWithBookmarkCustomRepository { List findByUserId(Long userId); 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 new file mode 100644 index 0000000..a95ef68 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepository.java @@ -0,0 +1,10 @@ +package com.ftm.server.adapter.out.persistence.repository; + +import com.ftm.server.application.query.FindPostsByCreatedDateQuery; +import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import java.util.List; + +public interface PostWithBookmarkCustomRepository { + + List findAllPostsWithBookmarkCount(FindPostsByCreatedDateQuery 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 new file mode 100644 index 0000000..49347d2 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostWithBookmarkCustomRepositoryImpl.java @@ -0,0 +1,44 @@ +package com.ftm.server.adapter.out.persistence.repository; + +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.FindPostsByCreatedDateQuery; +import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class PostWithBookmarkCustomRepositoryImpl implements PostWithBookmarkCustomRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public List findAllPostsWithBookmarkCount( + FindPostsByCreatedDateQuery query) { + + LocalDateTime twoWeeksAgo = + query.getDate().minusWeeks(1).atStartOfDay(); // 현재 기준 1주일 전 게시물만 조회 + + return queryFactory + .select( + Projections.constructor( + PostWithBookmarkCountVo.class, + postJpaEntity.id, + postJpaEntity.title, + postJpaEntity.viewCount, + postJpaEntity.likeCount, + bookmarkJpaEntity.id.count())) + .from(postJpaEntity) + .leftJoin(bookmarkJpaEntity) + .on(bookmarkJpaEntity.post.eq(postJpaEntity)) + .where(postJpaEntity.createdAt.goe(twoWeeksAgo)) + .groupBy(postJpaEntity.id) + .fetch(); + } +} diff --git a/src/main/java/com/ftm/server/application/port/in/post/LoadTrendingPostsUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/LoadTrendingPostsUseCase.java new file mode 100644 index 0000000..99bd1ef --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/post/LoadTrendingPostsUseCase.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.port.in.post; + +import com.ftm.server.application.vo.post.TrendingPostVo; +import com.ftm.server.common.annotation.UseCase; +import java.util.List; + +@UseCase +public interface LoadTrendingPostsUseCase { + List execute(); +} diff --git a/src/main/java/com/ftm/server/application/port/out/cache/LoadTrendingPostsWithCachePort.java b/src/main/java/com/ftm/server/application/port/out/cache/LoadTrendingPostsWithCachePort.java new file mode 100644 index 0000000..006a9c9 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/cache/LoadTrendingPostsWithCachePort.java @@ -0,0 +1,9 @@ +package com.ftm.server.application.port.out.cache; + +import com.ftm.server.application.vo.post.TrendingPostVo; +import java.util.List; + +public interface LoadTrendingPostsWithCachePort { + + List loadTrendingPosts(); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostImagePort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostImagePort.java index 3c93ee7..db9ca42 100644 --- a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostImagePort.java +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostImagePort.java @@ -12,4 +12,6 @@ public interface LoadPostImagePort { List loadPostImagesByPostId(FindByPostIdQuery query); List loadPostImagesByPostIds(FindByIdsQuery query); + + List loadRepresentativeImagesByPostIds(FindByIdsQuery 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 new file mode 100644 index 0000000..42146dd --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostWithBookmarkCountPort.java @@ -0,0 +1,9 @@ +package com.ftm.server.application.port.out.persistence.post; + +import com.ftm.server.application.query.FindPostsByCreatedDateQuery; +import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import java.util.List; + +public interface LoadPostWithBookmarkCountPort { + List loadAllPostsWithBookmarkCount(FindPostsByCreatedDateQuery query); +} diff --git a/src/main/java/com/ftm/server/application/query/FindPostsByCreatedDateQuery.java b/src/main/java/com/ftm/server/application/query/FindPostsByCreatedDateQuery.java new file mode 100644 index 0000000..317ddfc --- /dev/null +++ b/src/main/java/com/ftm/server/application/query/FindPostsByCreatedDateQuery.java @@ -0,0 +1,13 @@ +package com.ftm.server.application.query; + +import java.time.LocalDate; +import lombok.Data; + +@Data +public class FindPostsByCreatedDateQuery { + private final LocalDate date; + + public static FindPostsByCreatedDateQuery of(LocalDate date) { + return new FindPostsByCreatedDateQuery(date); + } +} diff --git a/src/main/java/com/ftm/server/application/service/post/LoadTrendingPostsService.java b/src/main/java/com/ftm/server/application/service/post/LoadTrendingPostsService.java new file mode 100644 index 0000000..7acf286 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/post/LoadTrendingPostsService.java @@ -0,0 +1,22 @@ +package com.ftm.server.application.service.post; + +import com.ftm.server.application.port.in.post.LoadTrendingPostsUseCase; +import com.ftm.server.application.port.out.cache.LoadTrendingPostsWithCachePort; +import com.ftm.server.application.vo.post.TrendingPostVo; +import java.util.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class LoadTrendingPostsService implements LoadTrendingPostsUseCase { + + private final LoadTrendingPostsWithCachePort loadTrendingPostsWithCachePort; + + @Override + @Transactional(readOnly = true) + public List execute() { + return loadTrendingPostsWithCachePort.loadTrendingPosts(); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/post/PostWithBookmarkCountVo.java b/src/main/java/com/ftm/server/application/vo/post/PostWithBookmarkCountVo.java new file mode 100644 index 0000000..a52c4a4 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/PostWithBookmarkCountVo.java @@ -0,0 +1,14 @@ +package com.ftm.server.application.vo.post; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class PostWithBookmarkCountVo { + private final Long postId; + private final String title; + private final Integer viewCount; + private final Integer likeCount; + private final Long scrapCount; +} diff --git a/src/main/java/com/ftm/server/application/vo/post/TrendingPostVo.java b/src/main/java/com/ftm/server/application/vo/post/TrendingPostVo.java new file mode 100644 index 0000000..f031928 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/TrendingPostVo.java @@ -0,0 +1,26 @@ +package com.ftm.server.application.vo.post; + +import lombok.Data; + +@Data +public class TrendingPostVo { + + private final Long postId; + private final Integer rank; + private final String title; + private final Integer viewCount; + private final Integer likeCount; + private final Long scrapCount; + private final String imageUrl; + + public static TrendingPostVo from(PostWithBookmarkCountVo vo, Integer rank, String imageUrl) { + return new TrendingPostVo( + vo.getPostId(), + rank, + vo.getTitle(), + vo.getViewCount(), + vo.getLikeCount(), + vo.getScrapCount(), + imageUrl); + } +} 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 31ce48c..433471c 100644 --- a/src/main/java/com/ftm/server/common/consts/StaticConsts.java +++ b/src/main/java/com/ftm/server/common/consts/StaticConsts.java @@ -15,4 +15,7 @@ public class StaticConsts { public static final int PENDING_SOCIAL_USER_SESSION_TTL = 300; // 5분 public static final String GROOMING_TESTS_INFO_CACHE_NAME = "ftm:grooming:tests:info"; public static final String GROOMING_TESTS_INFO_CACHE_KEY_ALL = "'all'"; + + public static final String TRENDING_POSTS_CACHE_NAME = "ftm:posts:trend"; + public static final String TRENDING_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 f6df7a6..64c24ef 100644 --- a/src/main/java/com/ftm/server/infrastructure/cache/CaffeineCacheConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/cache/CaffeineCacheConfig.java @@ -1,21 +1,36 @@ package com.ftm.server.infrastructure.cache; import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.TimeUnit; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; @EnableCaching @Configuration public class CaffeineCacheConfig { @Bean - public CaffeineCacheManager cacheManager() { + @Primary + public CaffeineCacheManager cacheManager() { // 기본 캐시 매니저(아무것도 지정하지 않았을 시) CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setAllowNullValues(false); // null 값 저장하지 않음 manager.setCaffeine(Caffeine.newBuilder().maximumSize(100).recordStats()); return manager; } + + @Bean("trendingPostsCacheManager") // 트렌딩 게시물 전용 캐시 매지너 TTL 설정 달리함 : 5분에 한번씩 캐시 무효화. + public CaffeineCacheManager cacheManagerForTrendingPosts() { + CaffeineCacheManager manager = new CaffeineCacheManager(); + manager.setAllowNullValues(false); // null 값 저장하지 않음 + manager.setCaffeine( + Caffeine.newBuilder() + .expireAfterWrite(5, TimeUnit.MINUTES) + .maximumSize(10) + .recordStats()); + return manager; + } } diff --git a/src/test/java/com/ftm/server/post/LoadTrendingPostsTes.java b/src/test/java/com/ftm/server/post/LoadTrendingPostsTes.java new file mode 100644 index 0000000..010eed1 --- /dev/null +++ b/src/test/java/com/ftm/server/post/LoadTrendingPostsTes.java @@ -0,0 +1,120 @@ +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.STRING; +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.GroomingCategory; +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 LoadTrendingPostsTes 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[].viewCount").type(NUMBER).description("조회수"), + fieldWithPath("data[].likeCount").type(NUMBER).description("좋아요 수"), + fieldWithPath("data[].scrapCount").type(NUMBER).description("스크랩 수"), + fieldWithPath("data[].imageUrl").type(STRING).description("이미지 url")); + + private ResultActions getResultActions() throws Exception { + return mockMvc.perform(RestDocumentationRequestBuilders.get("/api/posts/trend")); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "loadTrendingPosts/" + 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", + GroomingCategory.FASHION, + new ArrayList<>(), + "content1", + new ArrayList<>()), + new ArrayList<>(), + new ArrayList<>()))); + + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user.getId(), + new SavePostRequest( + "test2", + GroomingCategory.FASHION, + new ArrayList<>(), + "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)); + } +}