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
35 changes: 34 additions & 1 deletion src/docs/asciidoc/user-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ include::{snippetsDir}/userExit/1/http-response.adoc[]
==== Response Body Fields
include::{snippetsDir}/userExit/1/response-fields.adoc[]


=== **10. 북마크 생성 api**

게시글 북마크를 생성
Expand All @@ -199,4 +200,36 @@ include::{snippetsDir}/createBookmark/1/response-fields.adoc[]

==== 실패 Response
실패1.
include::{snippetsDir}/createBookmark/3/http-response.adoc[]
include::{snippetsDir}/createBookmark/3/http-response.adoc[]


=== **11. 내가 작성한 유저픽 게시글 목록 조회**

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

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

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

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

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

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

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

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

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

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

include::{snippetsDir}/loadMyPosts/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.LoadMyPostsUseCase;
import com.ftm.server.application.query.FindPostsByPagingQuery;
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 LoadMyPostsController {

private final LoadMyPostsUseCase loadMyPostsUseCase;

@GetMapping("/api/users/me/posts")
public ResponseEntity<ApiResponse<LoadMyPostsResponse>> loadMyPosts(
@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 =
loadMyPostsUseCase.execute(
FindPostsByPagingQuery.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
@@ -0,0 +1,22 @@
package com.ftm.server.adapter.in.web.user.dto.response;

import com.ftm.server.application.vo.post.PostPagingVo;
import com.ftm.server.application.vo.post.PostSummaryVo;
import java.util.List;
import lombok.Getter;

@Getter
public class LoadMyPostsResponse {

private final List<PostSummaryVo> items;
private final Boolean hasNext;

private LoadMyPostsResponse(PostPagingVo postPagingVo) {
this.items = postPagingVo.getItems();
this.hasNext = postPagingVo.getHasNext();
}

public static LoadMyPostsResponse from(PostPagingVo postPagingVo) {
return new LoadMyPostsResponse(postPagingVo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Slice;

@Adapter
@RequiredArgsConstructor
Expand All @@ -32,6 +33,7 @@ public class UserDomainPersistenceAdapter
UpdateUserPort,
UpdateUserImagePort,
LoadPostUserDomainPort,
LoadPostImageUserDomainPort,
UpdatePostUserDomainPort,
DeleteUserImagePort,
DeleteGroomingTestResultPort,
Expand All @@ -47,6 +49,7 @@ public class UserDomainPersistenceAdapter
private final GroomingLevelRepository groomingLevelRepository;
private final UserImageRepository userImageRepository;
private final PostRepository postRepository;
private final PostImageRepository postImageRepository;
private final BookmarkRepository bookmarkRepository;
private final GroomingTestResultRepository groomingTestResultRepository;

Expand All @@ -56,6 +59,7 @@ public class UserDomainPersistenceAdapter
private final UserImageMapper userImageMapper;
private final PostMapper postMapper;
private final BookmarkMapper bookmarkMapper;
private final PostImageMapper postImageMapper;

@Override
public Optional<EmailVerificationLogs> loadEmailVerificationLogByEmail(FindByEmailQuery query) {
Expand Down Expand Up @@ -193,6 +197,18 @@ public List<Post> loadPostListByUser(FindByUserIdQuery query) {
.toList();
}

@Override
public Slice<Post> loadPostsByUserIdWithPaging(FindPostsByPagingQuery query) {
return postRepository.findAllByUserIdWithPaging(query).map(postMapper::toDomainEntity);
}

@Override
public List<PostImage> loadRepresentativeImagesByPostIds(FindByIdsQuery query) {
return postImageRepository.findRepresentativeImagesByPostIdIn(query).stream()
.map(postImageMapper::toDomainEntity)
.toList();
}

@Override
public void updatePostListBySystemUser(List<Post> postList) {
UserJpaEntity systemUser = userRepository.findById(postList.get(0).getUserId()).get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import com.ftm.server.adapter.out.persistence.model.PostJpaEntity;
import com.ftm.server.application.query.FindPostByDeleteOptionQuery;
import com.ftm.server.application.query.FindPostsByPagingQuery;
import java.util.List;
import org.springframework.data.domain.Slice;

public interface PostCustomRepository {

List<PostJpaEntity> findAllByDeletedBefore(FindPostByDeleteOptionQuery query);

Slice<PostJpaEntity> findAllByUserIdWithPaging(FindPostsByPagingQuery query);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@

import com.ftm.server.adapter.out.persistence.model.PostJpaEntity;
import com.ftm.server.application.query.FindPostByDeleteOptionQuery;
import com.ftm.server.application.query.FindPostsByPagingQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.time.LocalTime;
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
Expand All @@ -25,4 +29,26 @@ public List<PostJpaEntity> findAllByDeletedBefore(FindPostByDeleteOptionQuery qu
postJpaEntity.deletedAt.loe(query.getDeletedAt().atTime(LocalTime.MAX)))
.fetch();
}

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

List<PostJpaEntity> 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
@@ -0,0 +1,11 @@
package com.ftm.server.adapter.out.persistence.repository;

import com.ftm.server.adapter.out.persistence.model.PostImageJpaEntity;
import com.ftm.server.application.query.FindByIdsQuery;
import java.util.List;

public interface PostImageCustomRepository {

// 여러개의 게시글 이미지 중 대표 이미지 한 개 조회 (썸네일용 이미지, 업로드가 가장 먼저된 이미지 조회)
List<PostImageJpaEntity> findRepresentativeImagesByPostIdIn(FindByIdsQuery query);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.ftm.server.adapter.out.persistence.repository;

import static com.ftm.server.adapter.out.persistence.model.QPostImageJpaEntity.postImageJpaEntity;

import com.ftm.server.adapter.out.persistence.model.PostImageJpaEntity;
import com.ftm.server.application.query.FindByIdsQuery;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

@Repository
@RequiredArgsConstructor
public class PostImageCustomRepositoryImpl implements PostImageCustomRepository {

private final JPAQueryFactory queryFactory;

@Override
public List<PostImageJpaEntity> findRepresentativeImagesByPostIdIn(FindByIdsQuery query) {
return queryFactory
.selectFrom(postImageJpaEntity)
.where(
postImageJpaEntity.id.in(
JPAExpressions.select(postImageJpaEntity.id.min())
.from(postImageJpaEntity)
.where(postImageJpaEntity.post.id.in(query.getIds()))
.groupBy(postImageJpaEntity.post.id)))
.fetch();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface PostImageRepository extends JpaRepository<PostImageJpaEntity, Long> {
public interface PostImageRepository
extends JpaRepository<PostImageJpaEntity, Long>, PostImageCustomRepository {

List<PostImageJpaEntity> findAllByPost(PostJpaEntity post);

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.FindPostsByPagingQuery;
import com.ftm.server.application.vo.post.PostPagingVo;
import com.ftm.server.common.annotation.UseCase;

@UseCase
public interface LoadMyPostsUseCase {

PostPagingVo execute(FindPostsByPagingQuery 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.FindByIdsQuery;
import com.ftm.server.common.annotation.Port;
import com.ftm.server.domain.entity.PostImage;
import java.util.List;

@Port
public interface LoadPostImageUserDomainPort {

List<PostImage> loadRepresentativeImagesByPostIds(FindByIdsQuery query);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.ftm.server.application.port.out.persistence.user;

import com.ftm.server.application.query.FindByUserIdQuery;
import com.ftm.server.application.query.FindPostsByPagingQuery;
import com.ftm.server.domain.entity.Post;
import java.util.List;
import org.springframework.data.domain.Slice;

public interface LoadPostUserDomainPort {

List<Post> loadPostListByUser(FindByUserIdQuery query);

Slice<Post> loadPostsByUserIdWithPaging(FindPostsByPagingQuery 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 FindPostsByPagingQuery {

private final Long userId;
private final Pageable pageable;

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

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

import com.ftm.server.application.port.in.user.LoadMyPostsUseCase;
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.FindByIdsQuery;
import com.ftm.server.application.query.FindPostsByPagingQuery;
import com.ftm.server.application.vo.post.PostPagingVo;
import com.ftm.server.application.vo.post.PostSummaryVo;
import com.ftm.server.domain.entity.Post;
import com.ftm.server.domain.entity.PostImage;
import java.util.*;
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 LoadMyPostsService implements LoadMyPostsUseCase {

private final LoadPostUserDomainPort loadPostUserDomainPort;
private final LoadPostImageUserDomainPort loadPostImageUserDomainPort;

@Override
public PostPagingVo execute(FindPostsByPagingQuery query) {
// 페이징된 게시글 목록 조회 (최신순 정렬)
Slice<Post> posts = loadPostUserDomainPort.loadPostsByUserIdWithPaging(query);
List<Post> sortedPosts = posts.getContent();

List<Long> postIds = sortedPosts.stream().map(Post::getId).toList();

// 각 게시글의 이미지 중 대표(썸네일) 이미지 한개씩만 조회
List<PostImage> postImages =
loadPostImageUserDomainPort.loadRepresentativeImagesByPostIds(
FindByIdsQuery.from(postIds));

// postId -> 대표 이미지(PostImage) 매핑 (createdAt 기준)
Map<Long, PostImage> postIdToImageMap =
postImages.stream()
.collect(
Collectors.toMap(
PostImage::getPostId,
Function.identity(),
BinaryOperator.minBy(
Comparator.comparing(PostImage::getCreatedAt))));

// 정렬된 순서를 보장하면서 게시글, 게시글 이미지 매핑
List<PostSummaryVo> items =
sortedPosts.stream()
.map(post -> PostSummaryVo.from(post, postIdToImageMap.get(post.getId())))
.toList();

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