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
13 changes: 13 additions & 0 deletions src/docs/asciidoc/post-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]

---


Original file line number Diff line number Diff line change
@@ -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<ApiResponse> getTrendingPosts() {
List<GetTrendingPostsResponse> result =
loadTrendingPostsUseCase.execute().stream()
.map(GetTrendingPostsResponse::from)
.toList();
return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.success(SuccessResponseCode.OK, result));
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<TrendingPostVo> loadTrendingPosts() {

log.info("오잉 캐시 실행 안됨!!");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😁

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗.. 못 지웠네요…ㅎㅎ

// 현재보다 1주일 이전에 작성된 게시물만 조회(북마크 조회수 포함) (예전 게시물은 포함x)
List<PostWithBookmarkCountVo> 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<PostWithBookmarkCountVo> 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<Long> postIds = topN.stream().map(PostWithBookmarkCountVo::getPostId).toList();

Map<Long, String> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -35,7 +36,8 @@ public class PostDomainPersistenceAdapter
DeletePostPort,
DeletePostImagePort,
DeletePostProductPort,
DeletePostProductImagePort {
DeletePostProductImagePort,
LoadPostWithBookmarkCountPort {

private final PostRepository postRepository;
private final PostImageRepository postImageRepository;
Expand Down Expand Up @@ -287,4 +289,17 @@ public void deletePostProductImages(List<PostProductImage> postProductImages) {
List<Long> ids = postProductImages.stream().map(PostProductImage::getId).toList();
postProductImageRepository.deleteAllByIdInBatch(ids);
}

@Override
public List<PostWithBookmarkCountVo> loadAllPostsWithBookmarkCount(
FindPostsByCreatedDateQuery query) {
return postRepository.findAllPostsWithBookmarkCount(query);
}

@Override
public List<PostImage> loadRepresentativeImagesByPostIds(FindByIdsQuery query) {
return postImageRepository.findRepresentativeImagesByPostIdIn(query).stream()
.map(postImageMapper::toDomainEntity)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface PostRepository extends JpaRepository<PostJpaEntity, Long>, PostCustomRepository {
public interface PostRepository
extends JpaRepository<PostJpaEntity, Long>,
PostCustomRepository,
PostWithBookmarkCustomRepository {

List<PostJpaEntity> findByUserId(Long userId);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<PostWithBookmarkCountVo> findAllPostsWithBookmarkCount(FindPostsByCreatedDateQuery query);
}
Original file line number Diff line number Diff line change
@@ -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<PostWithBookmarkCountVo> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<TrendingPostVo> execute();
}
Original file line number Diff line number Diff line change
@@ -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<TrendingPostVo> loadTrendingPosts();
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface LoadPostImagePort {
List<PostImage> loadPostImagesByPostId(FindByPostIdQuery query);

List<PostImage> loadPostImagesByPostIds(FindByIdsQuery query);

List<PostImage> loadRepresentativeImagesByPostIds(FindByIdsQuery query);
}
Original file line number Diff line number Diff line change
@@ -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<PostWithBookmarkCountVo> loadAllPostsWithBookmarkCount(FindPostsByCreatedDateQuery query);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<TrendingPostVo> execute() {
return loadTrendingPostsWithCachePort.loadTrendingPosts();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading