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
38 changes: 36 additions & 2 deletions src/docs/asciidoc/user-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ include::{snippetsDir}/createBookmark/3/http-response.adoc[]

마이페이지 > 작성한 게시글 목록 > 더보기 버튼 클릭 시 내가 작성한 유저픽 게시글 목록을 조회하는 api 입니다. +
무한 스크롤, 더보기 형식으로 조회하여 모든 데이터 개수와 번호를 부여하는 Page 방식이 아닌, +
다음 페이지의 데이터가 존재하는지의 여부를 함께 응답하는 Slice 방식을 사용했습니다.
다음 페이지의 데이터가 존재하는지의 여부를 함께 응답하는 Slice 방식을 사용했습니다. +
최신순으로 조회합니다.

==== Request
include::{snippetsDir}/loadMyPosts/1/http-request.adoc[]
Expand All @@ -232,4 +233,37 @@ include::{snippetsDir}/loadMyPosts/3/http-response.adoc[]

실패 3. 요청 페이지당 개수 유효성 검증에 실패할 경우

include::{snippetsDir}/loadMyPosts/4/http-response.adoc[]
include::{snippetsDir}/loadMyPosts/4/http-response.adoc[]


=== **12. 내가 북마크한 유저픽 게시글 목록 조회**

마이페이지 > 북마크한 게시글 목록 > 더보기 버튼 클릭 시 내가 작성한 유저픽 게시글 목록을 조회하는 api 입니다. +
무한 스크롤, 더보기 형식으로 조회하여 모든 데이터 개수와 번호를 부여하는 Page 방식이 아닌, +
다음 페이지의 데이터가 존재하는지의 여부를 함께 응답하는 Slice 방식을 사용했습니다. +
최신순으로 조회합니다.

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

==== Request Query Parameter Fields
include::{snippetsDir}/loadMyBookmarkPosts/1/query-parameters.adoc[]

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

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

==== 실패 Response
실패 1. 인증되지 않은 유저인 경우

include::{snippetsDir}/loadMyBookmarkPosts/2/http-response.adoc[]

실패 2. 요청 페이지 번호 유효성 검증에 실패할 경우

include::{snippetsDir}/loadMyBookmarkPosts/3/http-response.adoc[]

실패 3. 요청 페이지당 개수 유효성 검증에 실패할 경우

include::{snippetsDir}/loadMyBookmarkPosts/4/http-response.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.ftm.server.adapter.in.web.user.controller;

import com.ftm.server.adapter.in.web.user.dto.response.LoadMyPostsResponse;
import com.ftm.server.application.port.in.user.LoadMyBookmarkPostUseCase;
import com.ftm.server.application.query.FindBookmarksByPagingQuery;
import com.ftm.server.application.vo.post.PostPagingVo;
import com.ftm.server.common.exception.CustomException;
import com.ftm.server.common.response.ApiResponse;
import com.ftm.server.common.response.enums.ErrorResponseCode;
import com.ftm.server.common.response.enums.SuccessResponseCode;
import com.ftm.server.infrastructure.security.UserPrincipal;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
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 LoadMyBookmarkPostController {

private final LoadMyBookmarkPostUseCase loadMyBookmarkPostUseCase;

@GetMapping("/api/users/me/bookmarks")
public ResponseEntity<ApiResponse<LoadMyPostsResponse>> loadMyBookmarkPosts(
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "5") int size) {
// 요청 페이징 데이터 유효성 검증
if (page < 0) throw new CustomException(ErrorResponseCode.BAD_REQUEST_PAGING_INDEX_RANGE);
if (size < 1 || size > 10)
throw new CustomException(ErrorResponseCode.BAD_REQUEST_PAGING_SIZE_RANGE);

PostPagingVo vo =
loadMyBookmarkPostUseCase.execute(
FindBookmarksByPagingQuery.of(userPrincipal.getId(), page, size));

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.success(SuccessResponseCode.OK, LoadMyPostsResponse.from(vo)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public class UserDomainPersistenceAdapter
DeleteBookmarkPort,
SaveBookmarkPort,
CheckBookmarkPort,
CheckPostPort {
CheckPostPort,
LoadBookmarkPort {

// repository
private final EmailVerificationLogsRepository emailVerificationLogsRepository;
Expand Down Expand Up @@ -202,6 +203,13 @@ public Slice<Post> loadPostsByUserIdWithPaging(FindPostsByPagingQuery query) {
return postRepository.findAllByUserIdWithPaging(query).map(postMapper::toDomainEntity);
}

@Override
public List<Post> loadPostsByIds(FindByIdsQuery query) {
return postRepository.findAllById(query.getIds()).stream()
.map(postMapper::toDomainEntity)
.toList();
}

@Override
public List<PostImage> loadRepresentativeImagesByPostIds(FindByIdsQuery query) {
return postImageRepository.findRepresentativeImagesByPostIdIn(query).stream()
Expand Down Expand Up @@ -272,4 +280,11 @@ public Boolean checkIfBookmarkExists(FindBookmarkByUserIdAndPostIdQuery query) {
public Boolean checksPostById(FindByPostIdQuery query) {
return postRepository.existsById(query.getPostId());
}

@Override
public Slice<Bookmark> loadBookmarksByUserIdWithPaging(FindBookmarksByPagingQuery query) {
return bookmarkRepository
.findAllByUserIdWithPaging(query)
.map(bookmarkMapper::toDomainEntity);
}
}
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.adapter.out.persistence.model.BookmarkJpaEntity;
import com.ftm.server.application.query.FindBookmarksByPagingQuery;
import org.springframework.data.domain.Slice;

public interface BookmarkCustomRepository {

Slice<BookmarkJpaEntity> findAllByUserIdWithPaging(FindBookmarksByPagingQuery query);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.ftm.server.adapter.out.persistence.repository;

import static com.ftm.server.adapter.out.persistence.model.QBookmarkJpaEntity.bookmarkJpaEntity;

import com.ftm.server.adapter.out.persistence.model.BookmarkJpaEntity;
import com.ftm.server.application.query.FindBookmarksByPagingQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class BookmarkCustomRepositoryImpl implements BookmarkCustomRepository {

private final JPAQueryFactory queryFactory;

@Override
public Slice<BookmarkJpaEntity> findAllByUserIdWithPaging(FindBookmarksByPagingQuery query) {
Pageable pageable = query.getPageable();
List<BookmarkJpaEntity> content =
queryFactory
.selectFrom(bookmarkJpaEntity)
.where(bookmarkJpaEntity.user.id.eq(query.getUserId()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1) // 한 개 더 가져와서 hasNext 판별
.orderBy(bookmarkJpaEntity.createdAt.desc(), bookmarkJpaEntity.id.desc())
.fetch();

List<BookmarkJpaEntity> result = content;

boolean hasNext = content.size() > pageable.getPageSize();
if (hasNext) {
result = content.subList(0, pageable.getPageSize()); // 초과분 제거
}

return new SliceImpl<>(result, pageable, hasNext);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface BookmarkRepository extends JpaRepository<BookmarkJpaEntity, Long> {
public interface BookmarkRepository
extends JpaRepository<BookmarkJpaEntity, Long>, BookmarkCustomRepository {
@Modifying
@Query("DELETE FROM BookmarkJpaEntity b WHERE b.user.id in (:userIds)")
void deleteAllByUserIdList(@Param("userIds") List<Long> userIds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public Slice<PostJpaEntity> findAllByUserIdWithPaging(FindPostsByPagingQuery que
.where(postJpaEntity.user.id.eq(query.getUserId()))
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1) // 한 개 더 가져와서 hasNext 판별
.orderBy(postJpaEntity.createdAt.desc())
.orderBy(postJpaEntity.createdAt.desc(), postJpaEntity.id.desc())
.fetch();

List<PostJpaEntity> result = content;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ftm.server.application.port.in.user;

import com.ftm.server.application.query.FindBookmarksByPagingQuery;
import com.ftm.server.application.vo.post.PostPagingVo;
import com.ftm.server.common.annotation.UseCase;

@UseCase
public interface LoadMyBookmarkPostUseCase {

PostPagingVo execute(FindBookmarksByPagingQuery query);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ftm.server.application.port.out.persistence.user;

import com.ftm.server.application.query.FindBookmarksByPagingQuery;
import com.ftm.server.common.annotation.Port;
import com.ftm.server.domain.entity.Bookmark;
import org.springframework.data.domain.Slice;

@Port
public interface LoadBookmarkPort {

Slice<Bookmark> loadBookmarksByUserIdWithPaging(FindBookmarksByPagingQuery query);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ftm.server.application.port.out.persistence.user;

import com.ftm.server.application.query.FindByIdsQuery;
import com.ftm.server.application.query.FindByUserIdQuery;
import com.ftm.server.application.query.FindPostsByPagingQuery;
import com.ftm.server.domain.entity.Post;
Expand All @@ -11,4 +12,6 @@ public interface LoadPostUserDomainPort {
List<Post> loadPostListByUser(FindByUserIdQuery query);

Slice<Post> loadPostsByUserIdWithPaging(FindPostsByPagingQuery query);

List<Post> loadPostsByIds(FindByIdsQuery query);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.ftm.server.application.query;

import lombok.Getter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

@Getter
public class FindBookmarksByPagingQuery {

private final Long userId;
private final Pageable pageable;

private FindBookmarksByPagingQuery(Long userId, int page, int size) {
this.userId = userId;
this.pageable = PageRequest.of(page, size);
}

public static FindBookmarksByPagingQuery of(Long userId, int page, int size) {
return new FindBookmarksByPagingQuery(userId, page, size);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.ftm.server.application.service.user;

import com.ftm.server.application.port.in.user.LoadMyBookmarkPostUseCase;
import com.ftm.server.application.port.out.persistence.user.LoadBookmarkPort;
import com.ftm.server.application.port.out.persistence.user.LoadPostImageUserDomainPort;
import com.ftm.server.application.port.out.persistence.user.LoadPostUserDomainPort;
import com.ftm.server.application.query.FindBookmarksByPagingQuery;
import com.ftm.server.application.query.FindByIdsQuery;
import com.ftm.server.application.vo.post.PostPagingVo;
import com.ftm.server.application.vo.post.PostSummaryVo;
import com.ftm.server.domain.entity.Bookmark;
import com.ftm.server.domain.entity.Post;
import com.ftm.server.domain.entity.PostImage;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LoadMyBookmarkPostService implements LoadMyBookmarkPostUseCase {

private final LoadBookmarkPort loadBookmarkPort;
private final LoadPostUserDomainPort loadPostUserDomainPort;
private final LoadPostImageUserDomainPort loadPostImageUserDomainPort;

@Override
public PostPagingVo execute(FindBookmarksByPagingQuery query) {
Slice<Bookmark> bookmarks = loadBookmarkPort.loadBookmarksByUserIdWithPaging(query);
List<Long> postIds = bookmarks.getContent().stream().map(Bookmark::getPostId).toList();
FindByIdsQuery postIdsQuery = FindByIdsQuery.from(postIds);

// 북마크한 유저픽 게시글 목록 조회
// 최신순으로 정렬된 북마크 목록의 순서를 보장 -> postId : Post Map 매핑
List<Post> posts = loadPostUserDomainPort.loadPostsByIds(postIdsQuery);
Map<Long, Post> postMap =
posts.stream().collect(Collectors.toMap(Post::getId, Function.identity()));

// 게시글 이미지 목록 조회 (대표 이미지)
List<PostImage> postImages =
loadPostImageUserDomainPort.loadRepresentativeImagesByPostIds(postIdsQuery);
Map<Long, PostImage> postIdToImageMap =
postImages.stream()
.collect(
Collectors.toMap(
PostImage::getPostId,
Function.identity(),
BinaryOperator.minBy(
Comparator.comparing(PostImage::getCreatedAt))));

// 최신순으로 정렬된 postIds 를 기준으로 items 생성
List<PostSummaryVo> items =
postIds.stream()
.map(
postId -> {
Post post = postMap.get(postId);
PostImage postImage = postIdToImageMap.get(postId);
return PostSummaryVo.from(post, postImage);
})
.toList();

return PostPagingVo.from(items, bookmarks.hasNext());
}
}
Loading
Loading