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
20 changes: 20 additions & 0 deletions src/docs/asciidoc/post-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -323,4 +323,24 @@ include::{snippetsDir}/loadUserPickAllLatest/1/http-response.adoc[]
include::{snippetsDir}/loadUserPickAllLatest/1/response-fields.adoc[]


---


=== **13. 그루밍 이야기 - "인기순" 조회**

무한 스크롤 - cursor 방식으로 구현했습니다.

==== Request
include::{snippetsDir}/loadUserPickAllPopular/1/http-request.adoc[]

==== Request Query Parameters
include::{snippetsDir}/loadUserPickAllPopular/1/query-parameters.adoc[]

==== 성공 Response
include::{snippetsDir}/loadUserPickAllPopular/1/http-response.adoc[]

==== Response Body Fields
include::{snippetsDir}/loadUserPickAllPopular/1/response-fields.adoc[]


---
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.ftm.server.adapter.in.web.post.controller;

import com.ftm.server.adapter.in.web.post.dto.response.GetUserPickPostsLatestResponse;
import com.ftm.server.adapter.in.web.post.dto.response.GetUserPickPostsPopularCursorResponse;
import com.ftm.server.application.port.in.post.GetUserPickPostsUseCase;
import com.ftm.server.application.query.FindUserPickLatestPostsByCursorQuery;
import com.ftm.server.application.query.FindUserPickPopularPostsByCursorQuery;
import com.ftm.server.application.vo.post.GetUserPickAllPostsPopularWithCursorVo;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.SuccessResponseCode;
import com.ftm.server.infrastructure.security.UserPrincipal;
Expand Down Expand Up @@ -39,4 +42,22 @@ public ResponseEntity<ApiResponse> getLatestPosts(

return ResponseEntity.ok(ApiResponse.success(SuccessResponseCode.OK, result));
}

@GetMapping("/api/posts/userpick/all/popular")
public ResponseEntity<ApiResponse> getUserPickPopularPosts(
@AuthenticationPrincipal UserPrincipal user,
@RequestParam(name = "limit") Integer size,
@RequestParam(name = "lastScore", required = false) Double lastScore,
@RequestParam(name = "lastPostId", required = false) Long lastId) {

GetUserPickAllPostsPopularWithCursorVo items =
getUserPickPostsUseCase.executePopular(
FindUserPickPopularPostsByCursorQuery.of(
size, lastScore, lastId, user == null ? null : user.getId()));

GetUserPickPostsPopularCursorResponse response =
GetUserPickPostsPopularCursorResponse.from(items);

return ResponseEntity.ok(ApiResponse.success(SuccessResponseCode.OK, response));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.ftm.server.adapter.in.web.post.dto.response;

import com.ftm.server.application.vo.post.GetUserPickAllPostsPopularWithCursorVo;
import com.ftm.server.application.vo.post.GetUserPickPostsVo;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class GetUserPickPostsPopularCursorResponse {

private final List<GetUserPickPostsVo> data;
private final Boolean hasNext;
private final Long lastPostId;
private final Double lastScore;

public static GetUserPickPostsPopularCursorResponse from(
GetUserPickAllPostsPopularWithCursorVo vo) {
return new GetUserPickPostsPopularCursorResponse(
vo.getPostList(), vo.getHasNext(), vo.getNextPostId(), vo.getNextScore());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ftm.server.adapter.out.cache;

import static com.ftm.server.common.consts.StaticConsts.*;

import com.ftm.server.application.port.out.cache.LoadUserPickAllPopularCachePort;
import com.ftm.server.application.port.out.persistence.post.LoadPostPort;
import com.ftm.server.application.vo.post.*;
import com.ftm.server.common.annotation.Adapter;
import java.util.*;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;

@Adapter
@RequiredArgsConstructor
@CacheConfig(cacheManager = "userPickAllPopularPostsCacheManager")
public class LoadUserPickAllLoadUserPickAllPopularCacheAdapter
implements LoadUserPickAllPopularCachePort {

private final LoadPostPort loadPostPort;

@Override
@Cacheable(
value = USER_PICK_STORY_POPULAR_POSTS_CACHE_NAME,
key = USER_PICK_STORY_POPULAR_POSTS_CACHE_KEY_ALL)
public List<UserPickPopularPostCursorVo> getUserPickAllPopularPosts() {
return loadPostPort.loadUserPickAllPostsByPopular();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ public List<PostIdAndBookmarkYnVo> loadPostIdAndBookmarkYn(FindByPostIdsAndUserQ
return postRepository.findPostIdWithBookmarkYn(query);
}

@Override
public List<UserPickPopularPostCursorVo> loadUserPickAllPostsByPopular() {
return postRepository.findAllPostsByPopular();
}

@Override
public List<PostWithUserAndBookmarkCountVo> loadPostWithUserAndBookmarkCount(
FindByIdsQuery query) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,27 @@ public List<BookmarkYnWrapperVo> findPostsByLatestCursor(
@Override
public List<PostIdAndBookmarkYnVo> findPostIdWithBookmarkYn(FindByPostIdsAndUserQuery query) {

return queryFactory
.select(
Projections.constructor(
PostIdAndBookmarkYnVo.class,
postJpaEntity.id,
bookmarkJpaEntity.id.isNotNull()))
.from(postJpaEntity)
.leftJoin(bookmarkJpaEntity)
.on(
bookmarkJpaEntity
.post
.eq(postJpaEntity)
.and(
bookmarkJpaEntity.user.id.eq(
query.getUserId())) // 특정 user 북마크 여부 확인
)
.where(postJpaEntity.id.in(query.getPostIds()))
.fetch();
List<PostIdAndBookmarkYnVo> temp =
queryFactory
.select(
Projections.constructor(
PostIdAndBookmarkYnVo.class,
postJpaEntity.id,
bookmarkJpaEntity.id.isNotNull()))
.from(postJpaEntity)
.leftJoin(bookmarkJpaEntity)
.on(
bookmarkJpaEntity
.post
.id
.eq(postJpaEntity.id)
.and(
bookmarkJpaEntity.user.id.eq(
query.getUserId())) // 특정 user 북마크 여부 확인
)
.where(postJpaEntity.id.in(query.getPostIds()))
.fetch();

return temp;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.ftm.server.application.query.FindPostsByCreatedDateQuery;
import com.ftm.server.application.vo.post.PostWithBookmarkCountVo;
import com.ftm.server.application.vo.post.PostWithUserAndBookmarkCountVo;
import com.ftm.server.application.vo.post.UserPickPopularPostCursorVo;
import com.ftm.server.application.vo.post.UserWithPostCountVo;
import java.util.List;

Expand All @@ -17,4 +18,6 @@ List<UserWithPostCountVo> findAllPostsWithUserAndBookmarkCount(
List<PostWithUserAndBookmarkCountVo> findAllPostsWithUserAndBookmarkCount(FindByIdsQuery query);

List<Long> findTopNPostsByBookmarkCount(int limit);

List<UserPickPopularPostCursorVo> findAllPostsByPopular();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@

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.PostWithUserAndBookmarkCountVo;
import com.ftm.server.application.vo.post.UserWithPostCountVo;
import com.ftm.server.application.vo.post.*;
import com.ftm.server.domain.enums.UserRole;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
Expand Down Expand Up @@ -97,13 +98,14 @@ public List<PostWithUserAndBookmarkCountVo> findAllPostsWithUserAndBookmarkCount
postJpaEntity.title,
postJpaEntity.content,
postJpaEntity.hashtags,
postJpaEntity.viewCount, // sum() 쓰지 마세요
postJpaEntity.likeCount, // sum() 쓰지 마세요
postJpaEntity.viewCount,
postJpaEntity.likeCount,
bookmarkJpaEntity.id.countDistinct() // 북마크 개수
))
.from(postJpaEntity)
.where(postJpaEntity.id.in(query.getIds()))
.leftJoin(bookmarkJpaEntity)
.on(bookmarkJpaEntity.post.eq(postJpaEntity))
.on(bookmarkJpaEntity.post.id.eq(postJpaEntity.id))
.groupBy(
postJpaEntity.id,
postJpaEntity.user.id,
Expand All @@ -129,4 +131,41 @@ public List<Long> findTopNPostsByBookmarkCount(int limit) {
.limit(limit)
.fetch();
}

@Override
public List<UserPickPopularPostCursorVo> findAllPostsByPopular() {

Tuple maxValues =
queryFactory
.select(postJpaEntity.likeCount.max(), postJpaEntity.viewCount.max())
.from(postJpaEntity)
.fetchOne();

int maxLike = maxValues.get(postJpaEntity.likeCount.max());
int maxView = maxValues.get(postJpaEntity.viewCount.max());

List<UserPickPopularPostCursorVo> result =
queryFactory.select(postJpaEntity).from(postJpaEntity).fetch().stream()
.map(
vo -> {
double normLike =
maxLike > 0 ? vo.getLikeCount() / (double) maxLike : 0;
double normView =
maxView > 0 ? vo.getViewCount() / (double) maxView : 0;
double weighted = normLike * 0.6 + normView * 0.4;

long hours =
Duration.between(vo.getCreatedAt(), LocalDateTime.now())
.toHours();
double timeDecay = Math.exp(-0.1 * hours);

return new UserPickPopularPostCursorVo(
vo.getId(), weighted * timeDecay);
})
.sorted(
Comparator.comparing(UserPickPopularPostCursorVo::getScore)
.reversed())
.toList();
return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.ftm.server.application.port.in.post;

import com.ftm.server.application.query.FindUserPickLatestPostsByCursorQuery;
import com.ftm.server.application.query.FindUserPickPopularPostsByCursorQuery;
import com.ftm.server.application.vo.post.GetUserPickAllPostsLatestWithCursorVo;
import com.ftm.server.application.vo.post.GetUserPickAllPostsPopularWithCursorVo;
import com.ftm.server.common.annotation.UseCase;

@UseCase
public interface GetUserPickPostsUseCase {

GetUserPickAllPostsLatestWithCursorVo executeLatest(FindUserPickLatestPostsByCursorQuery query);

GetUserPickAllPostsPopularWithCursorVo executePopular(
FindUserPickPopularPostsByCursorQuery query);
}
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.UserPickPopularPostCursorVo;
import com.ftm.server.common.annotation.Port;
import java.util.List;

@Port
public interface LoadUserPickAllPopularCachePort {

List<UserPickPopularPostCursorVo> getUserPickAllPopularPosts();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.ftm.server.application.query.*;
import com.ftm.server.application.vo.post.BookmarkYnWrapperVo;
import com.ftm.server.application.vo.post.PostIdAndBookmarkYnVo;
import com.ftm.server.application.vo.post.UserPickPopularPostCursorVo;
import com.ftm.server.common.annotation.Port;
import com.ftm.server.domain.entity.Post;
import java.util.List;
Expand All @@ -27,4 +28,6 @@ List<BookmarkYnWrapperVo> loadUserPickAllPostsByLatest(
FindUserPickLatestPostsByCursorQuery query);

List<PostIdAndBookmarkYnVo> loadPostIdAndBookmarkYn(FindByPostIdsAndUserQuery query);

List<UserPickPopularPostCursorVo> loadUserPickAllPostsByPopular();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ftm.server.application.query;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class FindUserPickPopularPostsByCursorQuery {

private Integer size;
private Double lastScore; // nullable
private Long lastId; // nullable
private final Long userId;

public static FindUserPickPopularPostsByCursorQuery of(
Integer size, Double lastScore, Long lastId, Long userId) {
return new FindUserPickPopularPostsByCursorQuery(size, lastScore, lastId, userId);
}
}
Loading
Loading