diff --git a/src/docs/asciidoc/post-api.adoc b/src/docs/asciidoc/post-api.adoc index 6584c98..e76b3e6 100644 --- a/src/docs/asciidoc/post-api.adoc +++ b/src/docs/asciidoc/post-api.adoc @@ -197,7 +197,7 @@ include::{snippetsDir}/deletePost/4/http-response.adoc[] === **5. 트렌딩 게시글 조회** -트랜딩 게시글 상세 조회 api 입니다. +트랜딩 게시글 조회 api 입니다. ==== Request include::{snippetsDir}/loadTrendingPosts/1/http-request.adoc[] @@ -211,3 +211,18 @@ include::{snippetsDir}/loadTrendingPosts/1/response-fields.adoc[] --- +=== **6. 트렌딩 핏더맨 조회** + +트랜딩 핏더맨 조회 api 입니다. + +==== Request +include::{snippetsDir}/loadTrendingMan/1/http-request.adoc[] + +==== 성공 Response +include::{snippetsDir}/loadTrendingMan/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/loadTrendingMan/1/response-fields.adoc[] + +--- + diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetTrendingManController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetTrendingManController.java new file mode 100644 index 0000000..65d3ea0 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetTrendingManController.java @@ -0,0 +1,29 @@ +package com.ftm.server.adapter.in.web.post.controller; + +import com.ftm.server.adapter.in.web.post.dto.response.LoadTrendingManResponse; +import com.ftm.server.application.port.in.post.LoadTrendingManUseCase; +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 GetTrendingManController { + + private final LoadTrendingManUseCase loadTrendingManUseCase; + + @GetMapping("/api/posts/trend/users") + public ResponseEntity getTrendingMan() { + List result = + loadTrendingManUseCase.execute().stream() + .map(LoadTrendingManResponse::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/LoadTrendingManResponse.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadTrendingManResponse.java new file mode 100644 index 0000000..a722c5e --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadTrendingManResponse.java @@ -0,0 +1,17 @@ +package com.ftm.server.adapter.in.web.post.dto.response; + +import com.ftm.server.application.vo.post.TrendingUserVo; +import lombok.Data; + +@Data +public class LoadTrendingManResponse { + private final Integer ranking; + private final Long userId; + private final String userName; + private final String userImageUrl; + + public static LoadTrendingManResponse from(TrendingUserVo vo) { + return new LoadTrendingManResponse( + vo.getRank(), vo.getUserId(), vo.getUserName(), vo.getUserImageUrl()); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingManWithCacheAdapter.java b/src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingManWithCacheAdapter.java new file mode 100644 index 0000000..3bf3a05 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingManWithCacheAdapter.java @@ -0,0 +1,90 @@ +package com.ftm.server.adapter.out.cache; + +import com.ftm.server.application.port.out.cache.LoadTrendingManWithCachePort; +import com.ftm.server.application.port.out.persistence.post.LoadPostWithBookmarkCountPort; +import com.ftm.server.application.port.out.persistence.user.LoadUserImagePort; +import com.ftm.server.application.query.FindPostsByCreatedDateQuery; +import com.ftm.server.application.query.FindUserImagesByIdsQuery; +import com.ftm.server.application.vo.post.TrendingUserVo; +import com.ftm.server.application.vo.post.UserWithPostCountVo; +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; + +@Adapter +@RequiredArgsConstructor +@Slf4j +@CacheConfig(cacheManager = "trendingUsersCacheManager") +public class LoadTrendingManWithCacheAdapter implements LoadTrendingManWithCachePort { + + private final LoadPostWithBookmarkCountPort loadPostPort; + + private final LoadUserImagePort loadUserImagePort; + + private final Integer N = 5; + + @Override + public List loadTrendingMan() { + + // 현재보다 1주일 이전에 작성된 게시물만 조회(북마크 조회수 포함) (예전 게시물은 포함x) + List userWithCount = + loadPostPort.loadAllPostsWithUserAndBookmarkCount( + FindPostsByCreatedDateQuery.of(LocalDate.now())); + + // 1. 최대값 계산 (정규화를 위해) + long maxView = 1, maxLike = 1, maxScrap = 1; + for (UserWithPostCountVo post : userWithCount) { + 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 = + userWithCount.stream() + .sorted( + Comparator.comparingDouble( + p -> + -((double) p.getViewCount() / finalMaxView + + (double) p.getLikeCount() + / finalMaxLike + + (double) p.getScrapCount() + / finalMaxScrap) + / 3.0)) + .limit(N) + .toList(); + + // 3. 각 User 이미지 조회 + List userIds = topN.stream().map(UserWithPostCountVo::getUserId).toList(); + + Map imageMap = new HashMap<>(); + userIds.forEach(p -> imageMap.put(p, null)); + loadUserImagePort + .loadUserImagesByUserIdIn(FindUserImagesByIdsQuery.of(userIds)) + .forEach(userImage -> imageMap.put(userImage.getId(), userImage.getObjectKey())); + + // 4. 결과 조합 (순위 부여) + return IntStream.range(0, topN.size()) + .mapToObj( + i -> { + UserWithPostCountVo userWithPostCountVo = topN.get(i); + String imageKey = imageMap.get(userWithPostCountVo.getUserId()); + return TrendingUserVo.of( + i + 1, + userWithPostCountVo.getUserId(), + userWithPostCountVo.getUserName(), + imageKey); // rank = i+1 + }) + .toList(); + } +} 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 index 85f24b1..9b95ff7 100644 --- a/src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingPostsWithCacheAdapter.java +++ b/src/main/java/com/ftm/server/adapter/out/cache/LoadTrendingPostsWithCacheAdapter.java @@ -37,7 +37,6 @@ public class LoadTrendingPostsWithCacheAdapter implements LoadTrendingPostsWithC @Cacheable(value = TRENDING_POSTS_CACHE_NAME, key = TRENDING_POSTS_CACHE_KEY_ALL) public List loadTrendingPosts() { - log.info("오잉 캐시 실행 안됨!!"); // 현재보다 1주일 이전에 작성된 게시물만 조회(북마크 조회수 포함) (예전 게시물은 포함x) List rawPosts = loadPostPort.loadAllPostsWithBookmarkCount( 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 d501269..b3320ee 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 @@ -6,13 +6,12 @@ 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.application.vo.post.UserWithPostCountVo; import com.ftm.server.common.annotation.Adapter; import com.ftm.server.common.exception.CustomException; import com.ftm.server.common.response.enums.ErrorResponseCode; import com.ftm.server.domain.entity.*; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -296,6 +295,12 @@ public List loadAllPostsWithBookmarkCount( return postRepository.findAllPostsWithBookmarkCount(query); } + @Override + public List loadAllPostsWithUserAndBookmarkCount( + FindPostsByCreatedDateQuery query) { + return postRepository.findAllPostsWithUserAndBookmarkCount(query); + } + @Override public List loadRepresentativeImagesByPostIds(FindByIdsQuery query) { return postImageRepository.findRepresentativeImagesByPostIdIn(query).stream() diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/user/UserDomainPersistenceAdapter.java b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/user/UserDomainPersistenceAdapter.java index c3609e8..283a8dc 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/user/UserDomainPersistenceAdapter.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/user/UserDomainPersistenceAdapter.java @@ -240,7 +240,7 @@ public void deleteGroomingTestResultByUserList( @Override public List deleteUserImageByUserList(DeleteUserImageByUserIdCommand command) { List imageKeyList = - userImageRepository.findAllByUserIdList(command.getUserIdList()); + userImageRepository.findAllImagesByUserIdList(command.getUserIdList()); userImageRepository.deleteAllByUserIdList(command.getUserIdList()); return imageKeyList; } @@ -300,4 +300,11 @@ public Optional loadBookmarkByUserIdAndPostId( bookmarkRepository.findByUserIdAndPostId(query.getUserId(), query.getPostId()); return bookmarkJpaEntity.map(bookmarkMapper::toDomainEntity); } + + @Override + public List loadUserImagesByUserIdIn(FindUserImagesByIdsQuery query) { + return userImageRepository.findAllByUserIdList(query.getUserIds()).stream() + .map(userImageMapper::toDomainEntity) + .toList(); + } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostCustomRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostCustomRepository.java index 443173d..12b0e09 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostCustomRepository.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostCustomRepository.java @@ -2,7 +2,9 @@ import com.ftm.server.adapter.out.persistence.model.PostJpaEntity; import com.ftm.server.application.query.FindPostByDeleteOptionQuery; +import com.ftm.server.application.query.FindPostsByCreatedDateQuery; import com.ftm.server.application.query.FindPostsByPagingQuery; +import com.querydsl.core.Tuple; import java.util.List; import org.springframework.data.domain.Slice; @@ -11,4 +13,6 @@ public interface PostCustomRepository { List findAllByDeletedBefore(FindPostByDeleteOptionQuery query); Slice findAllByUserIdWithPaging(FindPostsByPagingQuery query); + + List findAllByCreatedDateInOneWeekAndUserGrouping(FindPostsByCreatedDateQuery query); } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostCustomRepositoryImpl.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostCustomRepositoryImpl.java index 1ace775..29a0ac8 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostCustomRepositoryImpl.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostCustomRepositoryImpl.java @@ -4,8 +4,11 @@ import com.ftm.server.adapter.out.persistence.model.PostJpaEntity; import com.ftm.server.application.query.FindPostByDeleteOptionQuery; +import com.ftm.server.application.query.FindPostsByCreatedDateQuery; import com.ftm.server.application.query.FindPostsByPagingQuery; +import com.querydsl.core.Tuple; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; import lombok.RequiredArgsConstructor; @@ -51,4 +54,19 @@ public Slice findAllByUserIdWithPaging(FindPostsByPagingQuery que return new SliceImpl<>(result, pageable, hasNext); } + + @Override + public List findAllByCreatedDateInOneWeekAndUserGrouping( + FindPostsByCreatedDateQuery query) { + + LocalDateTime oneWeekAgo = + query.getDate().minusWeeks(1).atStartOfDay(); // 현재 기준 1주일 전 게시물만 조회 + + return queryFactory + .select(postJpaEntity.user, postJpaEntity.id.count()) + .from(postJpaEntity) + .groupBy(postJpaEntity.user) + .where(postJpaEntity.createdAt.goe(oneWeekAgo)) + .fetch(); + } } 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 a95ef68..e40638b 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 @@ -2,9 +2,13 @@ import com.ftm.server.application.query.FindPostsByCreatedDateQuery; import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import com.ftm.server.application.vo.post.UserWithPostCountVo; import java.util.List; public interface PostWithBookmarkCustomRepository { List findAllPostsWithBookmarkCount(FindPostsByCreatedDateQuery query); + + List findAllPostsWithUserAndBookmarkCount( + 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 index 49347d2..dd44b6e 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 @@ -5,6 +5,8 @@ import com.ftm.server.application.query.FindPostsByCreatedDateQuery; import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import com.ftm.server.application.vo.post.UserWithPostCountVo; +import com.ftm.server.domain.enums.UserRole; import com.querydsl.core.types.Projections; import com.querydsl.jpa.impl.JPAQueryFactory; import java.time.LocalDateTime; @@ -22,7 +24,7 @@ public class PostWithBookmarkCustomRepositoryImpl implements PostWithBookmarkCus public List findAllPostsWithBookmarkCount( FindPostsByCreatedDateQuery query) { - LocalDateTime twoWeeksAgo = + LocalDateTime oneWeekAgo = query.getDate().minusWeeks(1).atStartOfDay(); // 현재 기준 1주일 전 게시물만 조회 return queryFactory @@ -37,8 +39,46 @@ public List findAllPostsWithBookmarkCount( .from(postJpaEntity) .leftJoin(bookmarkJpaEntity) .on(bookmarkJpaEntity.post.eq(postJpaEntity)) - .where(postJpaEntity.createdAt.goe(twoWeeksAgo)) + .where(postJpaEntity.createdAt.goe(oneWeekAgo)) + .where(postJpaEntity.isDeleted.eq(false)) // 삭제 되지 않은 것만 트렌딩 게시물에 포함하기 .groupBy(postJpaEntity.id) .fetch(); } + + @Override + public List findAllPostsWithUserAndBookmarkCount( + FindPostsByCreatedDateQuery query) { + LocalDateTime oneWeekAgo = + query.getDate().minusWeeks(1).atStartOfDay(); // 현재 기준 1주일 전 게시물만 조회 + + return queryFactory + .select( + Projections.constructor( + UserWithPostCountVo.class, + postJpaEntity.user.id, + postJpaEntity.user.nickname, + postJpaEntity.viewCount.sum(), + postJpaEntity.likeCount.sum(), + bookmarkJpaEntity.id.count())) + .from(postJpaEntity) + .leftJoin(bookmarkJpaEntity) + .on(bookmarkJpaEntity.post.eq(postJpaEntity)) + .where( + postJpaEntity + .isDeleted + .eq(false) + .and( + postJpaEntity.createdAt.goe( + oneWeekAgo))) // 삭제되지 않은 1주일 이내 게시물만 포함. + .where( + postJpaEntity + .user + .isDeleted + .eq(false) + .and( + postJpaEntity.user.role.eq( + UserRole.USER))) // 삭제되지 않은 일반 유저만 포함 + .groupBy(postJpaEntity.user.id, postJpaEntity.user.nickname) + .fetch(); + } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/UserImageRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/UserImageRepository.java index 234a579..69b7c19 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/UserImageRepository.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/UserImageRepository.java @@ -17,5 +17,8 @@ public interface UserImageRepository extends JpaRepository userIds); @Query("SELECT ui.objectKey FROM UserImageJpaEntity ui WHERE ui.user.id in (:userIds)") - List findAllByUserIdList(@Param("userIds") List userIds); + List findAllImagesByUserIdList(@Param("userIds") List userIds); + + @Query("SELECT ui FROM UserImageJpaEntity ui WHERE ui.user.id in (:userIds)") + List findAllByUserIdList(@Param("userIds") List userIds); } diff --git a/src/main/java/com/ftm/server/application/port/in/post/LoadTrendingManUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/LoadTrendingManUseCase.java new file mode 100644 index 0000000..52fd98f --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/post/LoadTrendingManUseCase.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.port.in.post; + +import com.ftm.server.application.vo.post.TrendingUserVo; +import com.ftm.server.common.annotation.UseCase; +import java.util.List; + +@UseCase +public interface LoadTrendingManUseCase { + List execute(); +} diff --git a/src/main/java/com/ftm/server/application/port/out/cache/LoadTrendingManWithCachePort.java b/src/main/java/com/ftm/server/application/port/out/cache/LoadTrendingManWithCachePort.java new file mode 100644 index 0000000..6cf8d2f --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/cache/LoadTrendingManWithCachePort.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.port.out.cache; + +import com.ftm.server.application.vo.post.TrendingUserVo; +import com.ftm.server.common.annotation.Port; +import java.util.List; + +@Port +public interface LoadTrendingManWithCachePort { + List loadTrendingMan(); +} 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 42146dd..4d0548a 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 @@ -2,8 +2,14 @@ import com.ftm.server.application.query.FindPostsByCreatedDateQuery; import com.ftm.server.application.vo.post.PostWithBookmarkCountVo; +import com.ftm.server.application.vo.post.UserWithPostCountVo; +import com.ftm.server.common.annotation.Port; import java.util.List; +@Port public interface LoadPostWithBookmarkCountPort { List loadAllPostsWithBookmarkCount(FindPostsByCreatedDateQuery query); + + List loadAllPostsWithUserAndBookmarkCount( + FindPostsByCreatedDateQuery query); } diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/user/LoadUserImagePort.java b/src/main/java/com/ftm/server/application/port/out/persistence/user/LoadUserImagePort.java index 3485918..6c16820 100644 --- a/src/main/java/com/ftm/server/application/port/out/persistence/user/LoadUserImagePort.java +++ b/src/main/java/com/ftm/server/application/port/out/persistence/user/LoadUserImagePort.java @@ -1,8 +1,12 @@ package com.ftm.server.application.port.out.persistence.user; import com.ftm.server.application.query.FindByUserIdQuery; +import com.ftm.server.application.query.FindUserImagesByIdsQuery; import com.ftm.server.domain.entity.UserImage; +import java.util.List; public interface LoadUserImagePort { UserImage loadUserImageByUserId(FindByUserIdQuery query); + + List loadUserImagesByUserIdIn(FindUserImagesByIdsQuery query); } diff --git a/src/main/java/com/ftm/server/application/query/FindUserImagesByIdsQuery.java b/src/main/java/com/ftm/server/application/query/FindUserImagesByIdsQuery.java new file mode 100644 index 0000000..84f26cc --- /dev/null +++ b/src/main/java/com/ftm/server/application/query/FindUserImagesByIdsQuery.java @@ -0,0 +1,15 @@ +package com.ftm.server.application.query; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class FindUserImagesByIdsQuery { + List userIds; + + public static FindUserImagesByIdsQuery of(List userIds) { + return new FindUserImagesByIdsQuery(userIds); + } +} diff --git a/src/main/java/com/ftm/server/application/service/post/LoadTrendingManService.java b/src/main/java/com/ftm/server/application/service/post/LoadTrendingManService.java new file mode 100644 index 0000000..64b01ef --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/post/LoadTrendingManService.java @@ -0,0 +1,20 @@ +package com.ftm.server.application.service.post; + +import com.ftm.server.application.port.in.post.LoadTrendingManUseCase; +import com.ftm.server.application.port.out.cache.LoadTrendingManWithCachePort; +import com.ftm.server.application.vo.post.TrendingUserVo; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoadTrendingManService implements LoadTrendingManUseCase { + + private final LoadTrendingManWithCachePort loadTrendingManWithCachePort; + + @Override + public List execute() { + return loadTrendingManWithCachePort.loadTrendingMan(); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/post/TrendingUserVo.java b/src/main/java/com/ftm/server/application/vo/post/TrendingUserVo.java new file mode 100644 index 0000000..a70d4c2 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/TrendingUserVo.java @@ -0,0 +1,18 @@ +package com.ftm.server.application.vo.post; + +import com.ftm.server.common.consts.PropertiesHolder; +import lombok.Data; + +@Data +public class TrendingUserVo { + private final Integer rank; + private final Long userId; + private final String userName; + private final String userImageUrl; + + public static TrendingUserVo of( + Integer rank, Long userId, String userName, String userImageUrl) { + return new TrendingUserVo( + rank, userId, userName, PropertiesHolder.CDN_PATH + "/" + userImageUrl); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/post/UserWithPostCountVo.java b/src/main/java/com/ftm/server/application/vo/post/UserWithPostCountVo.java new file mode 100644 index 0000000..4afb417 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/UserWithPostCountVo.java @@ -0,0 +1,19 @@ +package com.ftm.server.application.vo.post; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class UserWithPostCountVo { + private final Long userId; + private final String userName; + private final Integer viewCount; + private final Integer likeCount; + private final Long scrapCount; + + public static UserWithPostCountVo of( + Long userId, String userName, Integer viewCount, Integer likeCount, Long scrapCount) { + return new UserWithPostCountVo(userId, userName, viewCount, likeCount, scrapCount); + } +} 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 64c24ef..d54e334 100644 --- a/src/main/java/com/ftm/server/infrastructure/cache/CaffeineCacheConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/cache/CaffeineCacheConfig.java @@ -33,4 +33,16 @@ public CaffeineCacheManager cacheManagerForTrendingPosts() { .recordStats()); return manager; } + + @Bean("trendingUsersCacheManager") // 트렌딩 게시물 전용 캐시 매지너 TTL 설정 달리함 : 5분에 한번씩 캐시 무효화. + public CaffeineCacheManager cacheManagerForTrendingUsers() { + 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/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java index 16ccdd2..ade2a16 100644 --- a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java @@ -57,7 +57,7 @@ public class SecurityConfig { "/api/users/options", "/api/grooming/tests", "/api/auth/session/validity", - "/api/posts/*" + "/api/posts/**" }; private static final String[] POST_ANONYMOUS_MATCHERS = { diff --git a/src/test/java/com/ftm/server/post/LoadTrendingManTest.java b/src/test/java/com/ftm/server/post/LoadTrendingManTest.java new file mode 100644 index 0000000..25e9634 --- /dev/null +++ b/src/test/java/com/ftm/server/post/LoadTrendingManTest.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.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 LoadTrendingManTest 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[].userId").type(NUMBER).description("게시글 ID"), + fieldWithPath("data[].userName").type(STRING).description("게시글 제목"), + fieldWithPath("data[].userImageUrl").type(STRING).description("이미지 url")); + + private ResultActions getResultActions() throws Exception { + return mockMvc.perform(RestDocumentationRequestBuilders.get("/api/posts/trend/users")); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "loadTrendingMan/" + 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 + + BaseTest.SessionAndUser sessionAndUser = createUserAndLoginAndReturnUser(); // 로그인 처리 + + User user1 = createUserAndLoginAndReturnUser("test1@gmail.com", "gqe123@").user(); + User user2 = createUserAndLoginAndReturnUser("test2@gmail.com", "gqe123@").user(); + + // test 용 post 생성 + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user1.getId(), + new SavePostRequest( + "test1", + GroomingCategory.FASHION, + new ArrayList<>(), + "content1", + new ArrayList<>()), + new ArrayList<>(), + new ArrayList<>()))); + + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user2.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)); + } +} diff --git a/src/test/java/com/ftm/server/post/LoadTrendingPostsTes.java b/src/test/java/com/ftm/server/post/LoadTrendingPostsTest.java similarity index 95% rename from src/test/java/com/ftm/server/post/LoadTrendingPostsTes.java rename to src/test/java/com/ftm/server/post/LoadTrendingPostsTest.java index 010eed1..279db16 100644 --- a/src/test/java/com/ftm/server/post/LoadTrendingPostsTes.java +++ b/src/test/java/com/ftm/server/post/LoadTrendingPostsTest.java @@ -32,7 +32,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; -public class LoadTrendingPostsTes extends BaseTest { +public class LoadTrendingPostsTest extends BaseTest { @Autowired private SavePostPort savePostPort; @@ -41,7 +41,10 @@ public class LoadTrendingPostsTes extends BaseTest { fieldWithPath("status").type(NUMBER).description("응답 상태"), fieldWithPath("code").type(STRING).description("상태 코드"), fieldWithPath("message").type(STRING).description("메시지"), - fieldWithPath("data").type(ARRAY).optional().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("게시글 제목"),