Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/docs/asciidoc/post-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]

---
Original file line number Diff line number Diff line change
@@ -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<ApiResponse> loadTopBookmarkPosts() {
List<LoadUserPickTopBookmarkPostsResponse> result =
useCase.execute().stream().map(LoadUserPickTopBookmarkPostsResponse::from).toList();
return ResponseEntity.ok(ApiResponse.success(SuccessResponseCode.OK, result));
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public List<PostWithIdAndAuthorVo> 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<PostWithIdAndAuthorVo> getUserPickBiblePostCachePut() {
return execute();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PostWithIdAndAuthorVo> getTopBookmarkPosts() {
return execute();
}

@Override
@CachePut(
cacheNames = USER_PICK_TOP_BOOKMARK_POSTS_CACHE_NAME,
key = USER_PICK_TOP_BOOKMARK_POSTS_CACHE_KEY_ALL)
public List<PostWithIdAndAuthorVo> getTopBookmarkPostsCachePut() {
return execute();
}

private List<PostWithIdAndAuthorVo> execute() {
List<PostWithIdAndAuthorVo> posts =
loadPostPort.loadTopPostsByBookmarkCount(FindTopPostsByBookmarkCountQuery.of(4));
if (posts.isEmpty()) return List.of();
return posts;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ public List<PostWithIdAndAuthorVo> loadUserPickBiblePosts(FindUserPickBiblePosts
return postRepository.findTopNPostsByLikeCount(query.getLimit());
}

@Override
public List<PostWithIdAndAuthorVo> loadTopPostsByBookmarkCount(
FindTopPostsByBookmarkCountQuery query) {
return postRepository.findTopNPostsByBookmarkCount(query.getLimit());
}

@Override
public List<PostWithUserAndBookmarkCountVo> loadPostWithUserAndBookmarkCount(
FindByIdsQuery query) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,4 +16,6 @@ List<UserWithPostCountVo> findAllPostsWithUserAndBookmarkCount(
FindPostsByCreatedDateQuery query);

List<PostWithUserAndBookmarkCountVo> findAllPostsWithUserAndBookmarkCount(FindByIdsQuery query);

List<PostWithIdAndAuthorVo> findTopNPostsByBookmarkCount(int limit);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -115,4 +116,18 @@ public List<PostWithUserAndBookmarkCountVo> findAllPostsWithUserAndBookmarkCount
postJpaEntity.likeCount)
.fetch();
}

@Override
public List<PostWithIdAndAuthorVo> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<LoadUserPickTopBookmarkPostsVo> execute();
}
Original file line number Diff line number Diff line change
@@ -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<PostWithIdAndAuthorVo> getTopBookmarkPosts();

List<PostWithIdAndAuthorVo> getTopBookmarkPostsCachePut();
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ public interface LoadPostPort {
List<Post> loadUserPickPopularPosts(FindUserPickPopularPostsQuery query);

List<PostWithIdAndAuthorVo> loadUserPickBiblePosts(FindUserPickBiblePostsQuery query);

List<PostWithIdAndAuthorVo> loadTopPostsByBookmarkCount(FindTopPostsByBookmarkCountQuery query);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<LoadUserPickTopBookmarkPostsVo> execute() {
// 1) 캐시에서 상위 id 목록 조회 (4개)
List<PostWithIdAndAuthorVo> postList = cachePort.getTopBookmarkPosts();
if (postList.isEmpty()) return List.of();

// 2) id 목록
List<Long> postIds = postList.stream().map(PostWithIdAndAuthorVo::getPostId).toList();

// 3) 상세 조회 (유저/북마크 수 포함)
Map<Long, PostWithUserAndBookmarkCountVo> detailPostMap =
loadPostWithBookmarkCountPort
.loadPostWithUserAndBookmarkCount(FindByIdsQuery.from(postIds))
.stream()
.collect(Collectors.toMap(PostWithUserAndBookmarkCountVo::getId, vo -> vo));

// 4) 대표 이미지
List<PostImage> postImages =
loadPostImagePort.loadRepresentativeImagesByPostIds(FindByIdsQuery.from(postIds));
Map<Long, String> 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<String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String> hashtags;

public static LoadUserPickTopBookmarkPostsVo of(
Integer ranking,
PostWithUserAndBookmarkCountVo post,
String imageUrl,
List<String> hashtags) {
return new LoadUserPickTopBookmarkPostsVo(
ranking,
post.getId(),
post.getTitle(),
post.getUserId(),
post.getUserName(),
post.getViewCount(),
post.getLikeCount(),
post.getScrapCount(),
imageUrl,
hashtags);
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/ftm/server/common/consts/StaticConsts.java
Original file line number Diff line number Diff line change
Expand Up @@ -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'";
}
Loading
Loading