diff --git a/src/docs/asciidoc/post-api.adoc b/src/docs/asciidoc/post-api.adoc index 0c43751..59d9438 100644 --- a/src/docs/asciidoc/post-api.adoc +++ b/src/docs/asciidoc/post-api.adoc @@ -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[] + + --- \ No newline at end of file diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetUserPickPostsController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetUserPickPostsController.java new file mode 100644 index 0000000..7797c59 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/GetUserPickPostsController.java @@ -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 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)); + } +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/GetUserPickPostsLatestResponse.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/GetUserPickPostsLatestResponse.java new file mode 100644 index 0000000..466c299 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/GetUserPickPostsLatestResponse.java @@ -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 data; + private LocalDateTime nextCursorDateTime; // 다음 요청을 위한 datetime + private Boolean hasNext; + + public static GetUserPickPostsLatestResponse from(GetUserPickAllPostsLatestWithCursorVo vo) { + return new GetUserPickPostsLatestResponse( + vo.getPostList(), vo.getNextCursorDateTime(), vo.getHasNext()); + } +} 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 0fa6a9e..37616f7 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 @@ -168,6 +168,18 @@ public List loadTopPostsByBookmarkCount( return postRepository.findTopNPostsByBookmarkCount(query.getLimit()); } + @Override + public List loadUserPickAllPostsByLatest( + FindUserPickLatestPostsByCursorQuery query) { + return postRepository.findPostsByLatestCursor(query).stream() + .map( + e -> + new BookmarkYnWrapperVo( + e.getBookmarkYn(), + postMapper.toDomainEntity((PostJpaEntity) e.getData()))) + .toList(); + } + @Override public List loadPostWithUserAndBookmarkCount( FindByIdsQuery query) { 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 12b0e09..afb1f62 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 @@ -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; @@ -15,4 +17,6 @@ public interface PostCustomRepository { Slice findAllByUserIdWithPaging(FindPostsByPagingQuery query); List findAllByCreatedDateInOneWeekAndUserGrouping(FindPostsByCreatedDateQuery query); + + List findPostsByLatestCursor(FindUserPickLatestPostsByCursorQuery 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 29a0ac8..9100f38 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 @@ -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; @@ -69,4 +75,58 @@ public List findAllByCreatedDateInOneWeekAndUserGrouping( .where(postJpaEntity.createdAt.goe(oneWeekAgo)) .fetch(); } + + @Override + public List 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(); + } } diff --git a/src/main/java/com/ftm/server/application/port/in/post/GetUserPickPostsUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/GetUserPickPostsUseCase.java new file mode 100644 index 0000000..474cfa0 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/post/GetUserPickPostsUseCase.java @@ -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); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java index 55650cb..b500d78 100644 --- a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostPort.java @@ -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; @@ -21,4 +22,7 @@ public interface LoadPostPort { List loadUserPickBiblePosts(FindUserPickBiblePostsQuery query); List loadTopPostsByBookmarkCount(FindTopPostsByBookmarkCountQuery query); + + List loadUserPickAllPostsByLatest( + FindUserPickLatestPostsByCursorQuery query); } diff --git a/src/main/java/com/ftm/server/application/query/FindUserPickLatestPostsByCursorQuery.java b/src/main/java/com/ftm/server/application/query/FindUserPickLatestPostsByCursorQuery.java new file mode 100644 index 0000000..ce9649f --- /dev/null +++ b/src/main/java/com/ftm/server/application/query/FindUserPickLatestPostsByCursorQuery.java @@ -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); + } +} diff --git a/src/main/java/com/ftm/server/application/service/post/GetUserPickAllPostsByLatestService.java b/src/main/java/com/ftm/server/application/service/post/GetUserPickAllPostsByLatestService.java new file mode 100644 index 0000000..6d2a6b8 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/post/GetUserPickAllPostsByLatestService.java @@ -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 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 result = convertToVo(postsWithBookmarkYn.stream().toList()); + + return GetUserPickAllPostsLatestWithCursorVo.of(result, hasNext, lastCreatedAt); + } + + private List convertToVo(List posts) { + + List postIds = posts.stream().map(b -> ((Post) b.getData()).getId()).toList(); + + Map detailPostMap = + loadPostWithBookmarkCountPort + .loadPostWithUserAndBookmarkCount(FindByIdsQuery.from(postIds)) + .stream() + .collect(toMap(PostWithUserAndBookmarkCountVo::getId, vo -> vo)); + + Map 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 hashtags = toHashtagList(p.getHashtags()); + + return GetUserPickPostsVo.of( + p, + PropertiesHolder.CDN_PATH + "/" + imageUrl, + hashtags, + b.getBookmarkYn()); + }) + .toList(); + } + + private List toHashtagList(PostHashtag[] hashtags) { + return (hashtags == null || hashtags.length == 0) + ? List.of() + : Arrays.stream(hashtags).map(PostHashtag::getTag).toList(); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/post/BookmarkYnWrapperVo.java b/src/main/java/com/ftm/server/application/vo/post/BookmarkYnWrapperVo.java new file mode 100644 index 0000000..79564cd --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/BookmarkYnWrapperVo.java @@ -0,0 +1,11 @@ +package com.ftm.server.application.vo.post; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class BookmarkYnWrapperVo { + Boolean bookmarkYn; + T data; +} diff --git a/src/main/java/com/ftm/server/application/vo/post/GetUserPickAllPostsLatestWithCursorVo.java b/src/main/java/com/ftm/server/application/vo/post/GetUserPickAllPostsLatestWithCursorVo.java new file mode 100644 index 0000000..b878589 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/GetUserPickAllPostsLatestWithCursorVo.java @@ -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 postList; + Boolean hasNext; + LocalDateTime nextCursorDateTime; + + public static GetUserPickAllPostsLatestWithCursorVo of( + List postList, Boolean hasNext, LocalDateTime nextCursorDateTime) { + return new GetUserPickAllPostsLatestWithCursorVo(postList, hasNext, nextCursorDateTime); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/post/GetUserPickPostsVo.java b/src/main/java/com/ftm/server/application/vo/post/GetUserPickPostsVo.java new file mode 100644 index 0000000..bd537ae --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/GetUserPickPostsVo.java @@ -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 GetUserPickPostsVo { + 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 hashtags; + private final Boolean userBookmarkYn; + + public static GetUserPickPostsVo of( + PostWithUserAndBookmarkCountVo post, + String imageUrl, + List hashtags, + Boolean userBookmarkYn) { + return new GetUserPickPostsVo( + post.getId(), + post.getTitle(), + post.getUserId(), + post.getUserName(), + post.getViewCount(), + post.getLikeCount(), + post.getScrapCount(), + imageUrl, + hashtags, + userBookmarkYn); + } +} diff --git a/src/test/java/com/ftm/server/post/LoadUserPickLatestPostsTest.java b/src/test/java/com/ftm/server/post/LoadUserPickLatestPostsTest.java new file mode 100644 index 0000000..edfe22b --- /dev/null +++ b/src/test/java/com/ftm/server/post/LoadUserPickLatestPostsTest.java @@ -0,0 +1,161 @@ +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.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +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.PostHashtag; +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.restdocs.request.ParameterDescriptor; +import org.springframework.restdocs.snippet.Attributes; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +public class LoadUserPickLatestPostsTest extends BaseTest { + + @Autowired private SavePostPort savePostPort; + + private final List queryParameters = + List.of( + parameterWithName("limit") + .description("한번 로딩 시 불러올 게시글의 개수") + .attributes(new Attributes.Attribute("constraint", "Integer")), + parameterWithName("lastCursor") + .optional() + .description("이전 로딩의 마지막 커서 시점 입력") + .attributes( + new Attributes.Attribute( + "constraint", + "YYYY-MM-DDTHH:mm:SS.msss , 이전 스크롤 로딩 응답값(nextCursorDateTime) 필드 그대로 사용. 첫번째 스크롤인 경우 전달 X"))); + + private final List responseFields = + List.of( + fieldWithPath("status").type(NUMBER).description("응답 상태"), + fieldWithPath("code").type(STRING).description("상태 코드"), + fieldWithPath("message").type(STRING).description("메시지"), + fieldWithPath("data").type(OBJECT).optional().description("응답 데이터"), + fieldWithPath("data.data") + .type(ARRAY) + .optional() + .description("응답 데이터가 없으면 빈 배열"), + fieldWithPath("data.data[].postId").type(NUMBER).description("게시글 ID"), + fieldWithPath("data.data[].title").type(STRING).description("게시글 제목"), + fieldWithPath("data.data[].authorId").type(NUMBER).description("작성자 user ID"), + fieldWithPath("data.data[].authorName").type(STRING).description("작성자 이름"), + fieldWithPath("data.data[].viewCount").type(NUMBER).description("조회수"), + fieldWithPath("data.data[].likeCount").type(NUMBER).description("좋아요 수"), + fieldWithPath("data.data[].scrapCount").type(NUMBER).description("스크랩 수"), + fieldWithPath("data.data[].imageUrl").type(STRING).description("이미지 url"), + fieldWithPath("data.data[].hashtags") + .type(ARRAY) + .description("게시글 해시태그 : 한글 태그 표시. 없는 경우 빈 배열([])로 표시"), + fieldWithPath("data.data[].userBookmarkYn") + .type(BOOLEAN) + .description("사용자 북마크 등록 여부"), + fieldWithPath("data.nextCursorDateTime") + .type(STRING) + .optional() + .description("다음 스크롤 로딩 요청 시, 요청 파라미터 lastCursor의 값이 됨") + .attributes( + new Attributes.Attribute( + "nullable", "hasNext가 false인 경우 null")), + fieldWithPath("data.hasNext") + .type(BOOLEAN) + .description("불러올 게시글이 추가로 남았는지 여부")); + + private ResultActions getResultActions() throws Exception { + return mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/posts/userpick/all/latest") + .queryParam("limit", "5")); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "loadUserPickAllLatest/" + identifier, + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint(), getModifiedHeader()), + responseFields(responseFields), + queryParameters(queryParameters), + resource( + ResourceSnippetParameters.builder() + .tag("유저픽 게시글") + .summary("\"그루밍 이야기\" 최신순 조회 api") + .description("그루밍 라운지 내 \"그루밍 이야기\" 최신순 목록 조회 api 입니다.") + .responseFields(responseFields) + .build())); + } + + @Test + @Transactional + @DisplayName("테스트 성공") + public void test1() throws Exception { + // given + + SessionAndUser sessionAndUser = createUserAndLoginAndReturnUser(); // 로그인 처리 + + User user = sessionAndUser.user(); + + // test 용 post 생성 + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user.getId(), + new SavePostRequest( + "test1", + List.of(PostHashtag.SUN_CARE, PostHashtag.CLEANSING), + "content1", + new ArrayList<>()), + new ArrayList<>(), + new ArrayList<>()))); + + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user.getId(), + new SavePostRequest( + "test2", + List.of( + PostHashtag.BOTTOM_CLOTHING, + PostHashtag.FASHION_ACCESSORIES), + "content2", + new ArrayList<>()), + new ArrayList<>(), + new ArrayList<>()))); + + // when + ResultActions resultActions = getResultActions(); + + // then + resultActions + .andExpect(status().is(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data.data", hasSize(2))); + + // documentation + resultActions.andDo(getDocument(1)); + } +}