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
19 changes: 19 additions & 0 deletions src/docs/asciidoc/post-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,23 @@ include::{snippetsDir}/loadUserPickTopBookmark/1/http-response.adoc[]
==== Response Body Fields
include::{snippetsDir}/loadUserPickTopBookmark/1/response-fields.adoc[]

---

=== **12. 그루밍 이야기 - "최신순" 조회**

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

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

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

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

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


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

import com.ftm.server.adapter.in.web.post.dto.response.GetUserPickPostsLatestResponse;
import com.ftm.server.application.port.in.post.GetUserPickPostsUseCase;
import com.ftm.server.application.query.FindUserPickLatestPostsByCursorQuery;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.SuccessResponseCode;
import com.ftm.server.infrastructure.security.UserPrincipal;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class GetUserPickPostsController {

private final GetUserPickPostsUseCase getUserPickPostsUseCase;

@GetMapping("/api/posts/userpick/all/latest")
public ResponseEntity<ApiResponse> getLatestPosts(
@RequestParam(required = false, name = "lastCursor")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
LocalDateTime nextCursorCreatedAt,
@RequestParam(required = false, defaultValue = "20") int limit,
@AuthenticationPrincipal UserPrincipal user) {

GetUserPickPostsLatestResponse result =
GetUserPickPostsLatestResponse.from(
getUserPickPostsUseCase.executeLatest(
FindUserPickLatestPostsByCursorQuery.of(
limit,
nextCursorCreatedAt,
user == null ? null : user.getId())));

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

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

@AllArgsConstructor
@Getter
public class GetUserPickPostsLatestResponse {

private List<GetUserPickPostsVo> data;
private LocalDateTime nextCursorDateTime; // 다음 요청을 위한 datetime
private Boolean hasNext;

public static GetUserPickPostsLatestResponse from(GetUserPickAllPostsLatestWithCursorVo vo) {
return new GetUserPickPostsLatestResponse(
vo.getPostList(), vo.getNextCursorDateTime(), vo.getHasNext());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,18 @@ public List<PostWithIdAndAuthorVo> loadTopPostsByBookmarkCount(
return postRepository.findTopNPostsByBookmarkCount(query.getLimit());
}

@Override
public List<BookmarkYnWrapperVo> loadUserPickAllPostsByLatest(
FindUserPickLatestPostsByCursorQuery query) {
return postRepository.findPostsByLatestCursor(query).stream()
.map(
e ->
new BookmarkYnWrapperVo(
e.getBookmarkYn(),
postMapper.toDomainEntity((PostJpaEntity) e.getData())))
.toList();
}

@Override
public List<PostWithUserAndBookmarkCountVo> loadPostWithUserAndBookmarkCount(
FindByIdsQuery query) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.ftm.server.application.query.FindPostByDeleteOptionQuery;
import com.ftm.server.application.query.FindPostsByCreatedDateQuery;
import com.ftm.server.application.query.FindPostsByPagingQuery;
import com.ftm.server.application.query.FindUserPickLatestPostsByCursorQuery;
import com.ftm.server.application.vo.post.BookmarkYnWrapperVo;
import com.querydsl.core.Tuple;
import java.util.List;
import org.springframework.data.domain.Slice;
Expand All @@ -15,4 +17,6 @@ public interface PostCustomRepository {
Slice<PostJpaEntity> findAllByUserIdWithPaging(FindPostsByPagingQuery query);

List<Tuple> findAllByCreatedDateInOneWeekAndUserGrouping(FindPostsByCreatedDateQuery query);

List<BookmarkYnWrapperVo> findPostsByLatestCursor(FindUserPickLatestPostsByCursorQuery query);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
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.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.ftm.server.application.query.FindUserPickLatestPostsByCursorQuery;
import com.ftm.server.application.vo.post.BookmarkYnWrapperVo;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand Down Expand Up @@ -69,4 +75,58 @@ public List<Tuple> findAllByCreatedDateInOneWeekAndUserGrouping(
.where(postJpaEntity.createdAt.goe(oneWeekAgo))
.fetch();
}

@Override
public List<BookmarkYnWrapperVo> findPostsByLatestCursor(
FindUserPickLatestPostsByCursorQuery query) {

BooleanBuilder condition = new BooleanBuilder();

// 커서가 있으면 조건 추가
if (query.getNextCursorCreatedAt() != null) {
condition.and(
postJpaEntity.createdAt.lt(
query.getNextCursorCreatedAt()) // createdAt이 더 작은 게시글
);
}

if (query.getUserId() == null) {
return queryFactory
.select(
Projections.constructor(
BookmarkYnWrapperVo.class,
Expressions.constant(false),
postJpaEntity))
.from(postJpaEntity)
.where(condition)
.orderBy(
postJpaEntity.createdAt.desc(),
postJpaEntity.id.desc()) // 최신순 + tie-breaker
.limit(query.getLimit() + 1) // hasNext 체크를 위해 +1
.fetch();
}

return queryFactory
.select(
Projections.constructor(
BookmarkYnWrapperVo.class,
bookmarkJpaEntity.id.isNotNull(),
postJpaEntity))
.from(postJpaEntity)
.leftJoin(bookmarkJpaEntity)
.on(
bookmarkJpaEntity
.post
.eq(postJpaEntity)
.and(
bookmarkJpaEntity.user.id.eq(
query.getUserId())) // 특정 user 북마크 여부 확인
)
.where(condition)
.orderBy(
postJpaEntity.createdAt.desc(),
postJpaEntity.id.desc()) // 최신순 + tie-breaker
.limit(query.getLimit() + 1) // hasNext 체크를 위해 +1
.fetch();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ftm.server.application.port.in.post;

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

@UseCase
public interface GetUserPickPostsUseCase {

GetUserPickAllPostsLatestWithCursorVo executeLatest(FindUserPickLatestPostsByCursorQuery query);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.ftm.server.application.port.out.persistence.post;

import com.ftm.server.application.query.*;
import com.ftm.server.application.vo.post.BookmarkYnWrapperVo;
import com.ftm.server.application.vo.post.PostWithIdAndAuthorVo;
import com.ftm.server.common.annotation.Port;
import com.ftm.server.domain.entity.Post;
Expand All @@ -21,4 +22,7 @@ public interface LoadPostPort {
List<PostWithIdAndAuthorVo> loadUserPickBiblePosts(FindUserPickBiblePostsQuery query);

List<PostWithIdAndAuthorVo> loadTopPostsByBookmarkCount(FindTopPostsByBookmarkCountQuery query);

List<BookmarkYnWrapperVo> loadUserPickAllPostsByLatest(
FindUserPickLatestPostsByCursorQuery query);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.ftm.server.application.query;

import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class FindUserPickLatestPostsByCursorQuery {
private final Integer limit;
private final LocalDateTime nextCursorCreatedAt;
private final Long userId;

public static FindUserPickLatestPostsByCursorQuery of(
Integer limit, LocalDateTime nextCursorCreatedAt, Long userId) {
return new FindUserPickLatestPostsByCursorQuery(limit, nextCursorCreatedAt, userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.ftm.server.application.service.post;

import static java.util.stream.Collectors.toMap;

import com.ftm.server.application.port.in.post.GetUserPickPostsUseCase;
import com.ftm.server.application.port.out.persistence.post.LoadPostImagePort;
import com.ftm.server.application.port.out.persistence.post.LoadPostPort;
import com.ftm.server.application.port.out.persistence.post.LoadPostWithBookmarkCountPort;
import com.ftm.server.application.query.FindByIdsQuery;
import com.ftm.server.application.query.FindUserPickLatestPostsByCursorQuery;
import com.ftm.server.application.vo.post.*;
import com.ftm.server.common.consts.PropertiesHolder;
import com.ftm.server.domain.entity.Post;
import com.ftm.server.domain.entity.PostImage;
import com.ftm.server.domain.enums.PostHashtag;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class GetUserPickAllPostsByLatestService implements GetUserPickPostsUseCase {

private final LoadPostPort loadPostPort;
private final LoadPostWithBookmarkCountPort loadPostWithBookmarkCountPort;
private final LoadPostImagePort loadPostImagePort;

@Override
public GetUserPickAllPostsLatestWithCursorVo executeLatest(
FindUserPickLatestPostsByCursorQuery query) {

List<BookmarkYnWrapperVo> postsWithBookmarkYn =
loadPostPort.loadUserPickAllPostsByLatest(query); // 최신순으로 조회. 마지막 cursor 위치를 기준으로

if (postsWithBookmarkYn.isEmpty()) {
return GetUserPickAllPostsLatestWithCursorVo.of(List.of(), false, null);
}

boolean hasNext = false;
LocalDateTime lastCreatedAt = null;
if (postsWithBookmarkYn.size() > query.getLimit()) {
hasNext = true;
postsWithBookmarkYn = postsWithBookmarkYn.subList(0, query.getLimit());
lastCreatedAt =
((Post) postsWithBookmarkYn.get(query.getLimit() - 1).getData()).getCreatedAt();
}

List<GetUserPickPostsVo> result = convertToVo(postsWithBookmarkYn.stream().toList());

return GetUserPickAllPostsLatestWithCursorVo.of(result, hasNext, lastCreatedAt);
}

private List<GetUserPickPostsVo> convertToVo(List<BookmarkYnWrapperVo> posts) {

List<Long> postIds = posts.stream().map(b -> ((Post) b.getData()).getId()).toList();

Map<Long, PostWithUserAndBookmarkCountVo> detailPostMap =
loadPostWithBookmarkCountPort
.loadPostWithUserAndBookmarkCount(FindByIdsQuery.from(postIds))
.stream()
.collect(toMap(PostWithUserAndBookmarkCountVo::getId, vo -> vo));

Map<Long, String> imageUrlMap =
loadPostImagePort
.loadRepresentativeImagesByPostIds(FindByIdsQuery.from(postIds))
.stream()
.collect(toMap(PostImage::getPostId, PostImage::getObjectKey, (a, b) -> a));

return posts.stream()
.map(
b -> {
PostWithUserAndBookmarkCountVo p =
detailPostMap.get(((Post) b.getData()).getId());
String imageUrl =
imageUrlMap.getOrDefault(
p.getId(), PropertiesHolder.POST_DEFAULT_IMAGE);
List<String> hashtags = toHashtagList(p.getHashtags());

return GetUserPickPostsVo.of(
p,
PropertiesHolder.CDN_PATH + "/" + imageUrl,
hashtags,
b.getBookmarkYn());
})
.toList();
}

private List<String> toHashtagList(PostHashtag[] hashtags) {
return (hashtags == null || hashtags.length == 0)
? List.of()
: Arrays.stream(hashtags).map(PostHashtag::getTag).toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ftm.server.application.vo.post;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class BookmarkYnWrapperVo<T> {
Boolean bookmarkYn;
T data;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ftm.server.application.vo.post;

import java.time.LocalDateTime;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class GetUserPickAllPostsLatestWithCursorVo {

List<GetUserPickPostsVo> postList;
Boolean hasNext;
LocalDateTime nextCursorDateTime;

public static GetUserPickAllPostsLatestWithCursorVo of(
List<GetUserPickPostsVo> postList, Boolean hasNext, LocalDateTime nextCursorDateTime) {
return new GetUserPickAllPostsLatestWithCursorVo(postList, hasNext, nextCursorDateTime);
}
}
Loading
Loading