From 1c180c7f1e5e0f0477adde1532a70a77d9dc0969 Mon Sep 17 00:00:00 2001 From: DH_Choi <58378676+vectorch9@users.noreply.github.com> Date: Thu, 26 Oct 2023 14:32:10 +0900 Subject: [PATCH] =?UTF-8?q?[Feature]=20=ED=8C=94=EB=A1=9C=EC=9E=89=20?= =?UTF-8?q?=ED=94=BC=EB=93=9C,=20=EC=82=AC=EC=9A=A9=EC=9E=90=EA=B0=80=20?= =?UTF-8?q?=EC=98=AC=EB=A6=B0=20=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EB=A5=BC=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=9C=EB=8B=A4=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: transform이 동작하지 않는 에러 수정 * refactor: PageArgumentResolver가 lastId 값이 없다면 null로 채우도록 수정 * feature: 팔로잉 피드 조회 구현 * fix: 팔로잉, 팔로워 목록 조회 시 lastId가 null인 경우 동적쿼리를 사용 * feature: 사용자가 업로드한 게시물 목록 조회 구현 --- .../daybyquest/global/config/JpaConfig.java | 3 +- .../query/NoOffsetIdPageArgumentResolver.java | 2 +- .../application/GetPostByUsernameService.java | 36 ++++++++++ .../GetPostFromFollowingService.java | 30 ++++++++ .../application/PostResponseConverter.java | 28 ++++++++ .../post/dto/response/PagePostsResponse.java | 19 +++++ .../post/presentation/PostQueryApi.java | 30 +++++++- .../java/daybyquest/post/query/PostDao.java | 11 +++ .../post/query/PostDaoQuerydslImpl.java | 72 ++++++++++++++++--- .../relation/query/FollowDaoQuerydslImpl.java | 13 +++- .../daybyquest/user/query/ProfileDao.java | 3 + .../user/query/ProfileDaoQuerydslImpl.java | 11 +++ 12 files changed, 245 insertions(+), 13 deletions(-) create mode 100644 src/main/java/daybyquest/post/application/GetPostByUsernameService.java create mode 100644 src/main/java/daybyquest/post/application/GetPostFromFollowingService.java create mode 100644 src/main/java/daybyquest/post/application/PostResponseConverter.java create mode 100644 src/main/java/daybyquest/post/dto/response/PagePostsResponse.java diff --git a/src/main/java/daybyquest/global/config/JpaConfig.java b/src/main/java/daybyquest/global/config/JpaConfig.java index c435a86..2801f9d 100644 --- a/src/main/java/daybyquest/global/config/JpaConfig.java +++ b/src/main/java/daybyquest/global/config/JpaConfig.java @@ -1,5 +1,6 @@ package daybyquest.global.config; +import com.querydsl.jpa.JPQLTemplates; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -16,6 +17,6 @@ public class JpaConfig { @Bean public JPAQueryFactory queryFactory() { - return new JPAQueryFactory(entityManager); + return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager); } } diff --git a/src/main/java/daybyquest/global/query/NoOffsetIdPageArgumentResolver.java b/src/main/java/daybyquest/global/query/NoOffsetIdPageArgumentResolver.java index 72c7d8d..3085840 100644 --- a/src/main/java/daybyquest/global/query/NoOffsetIdPageArgumentResolver.java +++ b/src/main/java/daybyquest/global/query/NoOffsetIdPageArgumentResolver.java @@ -31,7 +31,7 @@ public Object resolveArgument(@Nonnull MethodParameter parameter, ModelAndViewCo private Long convertAndValidateLastId(String lastId) { if (lastId == null) { - return 0L; + return null; } if (!NUMBER.matcher(lastId).matches()) { throw new BadRequestException(); diff --git a/src/main/java/daybyquest/post/application/GetPostByUsernameService.java b/src/main/java/daybyquest/post/application/GetPostByUsernameService.java new file mode 100644 index 0000000..985735d --- /dev/null +++ b/src/main/java/daybyquest/post/application/GetPostByUsernameService.java @@ -0,0 +1,36 @@ +package daybyquest.post.application; + +import daybyquest.global.query.LongIdList; +import daybyquest.global.query.NoOffsetIdPage; +import daybyquest.post.dto.response.PagePostsResponse; +import daybyquest.post.query.PostDao; +import daybyquest.post.query.PostData; +import daybyquest.user.domain.Users; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class GetPostByUsernameService { + + private final PostDao postDao; + + private final Users users; + + private final PostResponseConverter converter; + + public GetPostByUsernameService(final PostDao postDao, final Users users, + final PostResponseConverter converter) { + this.postDao = postDao; + this.users = users; + this.converter = converter; + } + + @Transactional(readOnly = true) + public PagePostsResponse invoke(final Long loginId, final String username, final NoOffsetIdPage page) { + final Long targetId = users.getUserIdByUsername(username); + final LongIdList postIds = postDao.findPostIdsByUserId(loginId, targetId, page); + final List postData = postDao.findAllByIdIn(loginId, postIds.getIds()); + return new PagePostsResponse(converter.convertFromPostData(loginId, postData), postIds.getLastId()); + } +} diff --git a/src/main/java/daybyquest/post/application/GetPostFromFollowingService.java b/src/main/java/daybyquest/post/application/GetPostFromFollowingService.java new file mode 100644 index 0000000..deaf2eb --- /dev/null +++ b/src/main/java/daybyquest/post/application/GetPostFromFollowingService.java @@ -0,0 +1,30 @@ +package daybyquest.post.application; + +import daybyquest.global.query.LongIdList; +import daybyquest.global.query.NoOffsetIdPage; +import daybyquest.post.dto.response.PagePostsResponse; +import daybyquest.post.query.PostDao; +import daybyquest.post.query.PostData; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class GetPostFromFollowingService { + + private final PostDao postDao; + + private final PostResponseConverter converter; + + public GetPostFromFollowingService(final PostDao postDao, final PostResponseConverter converter) { + this.postDao = postDao; + this.converter = converter; + } + + @Transactional(readOnly = true) + public PagePostsResponse invoke(final Long loginId, final NoOffsetIdPage page) { + final LongIdList postIds = postDao.findPostIdsFromFollowings(loginId, page); + final List postData = postDao.findAllByIdIn(loginId, postIds.getIds()); + return new PagePostsResponse(converter.convertFromPostData(loginId, postData), postIds.getLastId()); + } +} diff --git a/src/main/java/daybyquest/post/application/PostResponseConverter.java b/src/main/java/daybyquest/post/application/PostResponseConverter.java new file mode 100644 index 0000000..539a8c0 --- /dev/null +++ b/src/main/java/daybyquest/post/application/PostResponseConverter.java @@ -0,0 +1,28 @@ +package daybyquest.post.application; + +import daybyquest.post.dto.response.PostResponse; +import daybyquest.post.query.PostData; +import daybyquest.user.query.Profile; +import daybyquest.user.query.ProfileDao; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; + +@Service +public class PostResponseConverter { + + private final ProfileDao profileDao; + + public PostResponseConverter(final ProfileDao profileDao) { + this.profileDao = profileDao; + } + + public List convertFromPostData(final Long loginId, final List postData) { + final Set userIds = postData.stream().map(PostData::getUserId).collect(Collectors.toSet()); + final Map profiles = profileDao.findMapByUserIdIn(loginId, userIds); + return postData.stream() + .map((pd) -> PostResponse.of(pd, profiles.get(pd.getUserId()))).toList(); + } +} diff --git a/src/main/java/daybyquest/post/dto/response/PagePostsResponse.java b/src/main/java/daybyquest/post/dto/response/PagePostsResponse.java new file mode 100644 index 0000000..bf5a0c2 --- /dev/null +++ b/src/main/java/daybyquest/post/dto/response/PagePostsResponse.java @@ -0,0 +1,19 @@ +package daybyquest.post.dto.response; + +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PagePostsResponse { + + private List posts; + + private Long lastId; + + public PagePostsResponse(final List posts, final Long lastId) { + this.posts = posts; + this.lastId = lastId; + } +} diff --git a/src/main/java/daybyquest/post/presentation/PostQueryApi.java b/src/main/java/daybyquest/post/presentation/PostQueryApi.java index 7b8e203..a5ade50 100644 --- a/src/main/java/daybyquest/post/presentation/PostQueryApi.java +++ b/src/main/java/daybyquest/post/presentation/PostQueryApi.java @@ -2,7 +2,11 @@ import daybyquest.auth.Authorization; import daybyquest.auth.UserId; +import daybyquest.global.query.NoOffsetIdPage; +import daybyquest.post.application.GetPostByUsernameService; +import daybyquest.post.application.GetPostFromFollowingService; import daybyquest.post.application.GetPostService; +import daybyquest.post.dto.response.PagePostsResponse; import daybyquest.post.dto.response.PostResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -14,8 +18,16 @@ public class PostQueryApi { private final GetPostService getPostService; - public PostQueryApi(final GetPostService getPostService) { + private final GetPostFromFollowingService getPostFromFollowingService; + + private final GetPostByUsernameService getPostByUsernameService; + + public PostQueryApi(final GetPostService getPostService, + final GetPostFromFollowingService getPostFromFollowingService, + final GetPostByUsernameService getPostByUsernameService) { this.getPostService = getPostService; + this.getPostFromFollowingService = getPostFromFollowingService; + this.getPostByUsernameService = getPostByUsernameService; } @GetMapping("/post/{postId}") @@ -24,4 +36,20 @@ public ResponseEntity getPost(@UserId final Long loginId, @PathVar final PostResponse response = getPostService.invoke(loginId, postId); return ResponseEntity.ok(response); } + + @GetMapping("/feed") + @Authorization + public ResponseEntity getPostFromFollowings(@UserId final Long loginId, + final NoOffsetIdPage page) { + final PagePostsResponse response = getPostFromFollowingService.invoke(loginId, page); + return ResponseEntity.ok(response); + } + + @GetMapping("/profile/{username}/post") + @Authorization + public ResponseEntity getPostByUsername(@UserId final Long loginId, + @PathVariable final String username, final NoOffsetIdPage page) { + final PagePostsResponse response = getPostByUsernameService.invoke(loginId, username, page); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/daybyquest/post/query/PostDao.java b/src/main/java/daybyquest/post/query/PostDao.java index 8113ea8..376f374 100644 --- a/src/main/java/daybyquest/post/query/PostDao.java +++ b/src/main/java/daybyquest/post/query/PostDao.java @@ -1,6 +1,17 @@ package daybyquest.post.query; +import daybyquest.global.query.LongIdList; +import daybyquest.global.query.NoOffsetIdPage; +import java.util.Collection; +import java.util.List; + public interface PostDao { PostData getByPostId(final Long userId, final Long postId); + + LongIdList findPostIdsFromFollowings(final Long userId, final NoOffsetIdPage page); + + LongIdList findPostIdsByUserId(final Long userId, final Long targetId, final NoOffsetIdPage page); + + List findAllByIdIn(final Long userId, final Collection postIds); } diff --git a/src/main/java/daybyquest/post/query/PostDaoQuerydslImpl.java b/src/main/java/daybyquest/post/query/PostDaoQuerydslImpl.java index 101a561..d76ed2e 100644 --- a/src/main/java/daybyquest/post/query/PostDaoQuerydslImpl.java +++ b/src/main/java/daybyquest/post/query/PostDaoQuerydslImpl.java @@ -1,15 +1,24 @@ package daybyquest.post.query; +import static com.querydsl.core.group.GroupBy.groupBy; +import static com.querydsl.core.group.GroupBy.list; import static daybyquest.image.vo.QImage.image; import static daybyquest.like.domain.QPostLike.postLike; import static daybyquest.post.domain.QPost.post; +import static daybyquest.relation.domain.QFollow.follow; +import com.querydsl.core.types.ConstructorExpression; import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import daybyquest.global.error.exception.NotExistPostException; +import daybyquest.global.query.LongIdList; +import daybyquest.global.query.NoOffsetIdPage; import daybyquest.image.vo.Image; +import java.util.Collection; import java.util.List; +import java.util.Map; import org.springframework.stereotype.Repository; @Repository @@ -23,14 +32,7 @@ public PostDaoQuerydslImpl(final JPAQueryFactory factory) { @Override public PostData getByPostId(final Long userId, final Long postId) { - final PostData postData = factory.select(Projections.constructor(PostData.class, - post.userId, - post.id, - post.content, - post.updatedAt, - JPAExpressions.selectFrom(postLike) - .where(postLike.userId.eq(userId).and(postLike.postId.eq(postId))) - .exists())) + final PostData postData = factory.select(postDataProjection(userId)) .from(post) .where(post.id.eq(postId)) .fetchOne(); @@ -45,4 +47,58 @@ public PostData getByPostId(final Long userId, final Long postId) { postData.setImages(images); return postData; } + + private ConstructorExpression postDataProjection(final Long userId) { + return Projections.constructor(PostData.class, + post.userId, + post.id, + post.content, + post.updatedAt, + JPAExpressions.selectFrom(postLike) + .where(postLike.userId.eq(userId).and(postLike.postId.eq(post.id))) + .exists()); + } + + @Override + public LongIdList findPostIdsFromFollowings(final Long userId, final NoOffsetIdPage page) { + return new LongIdList(factory.select(post.id) + .from(post) + .where(post.userId.in(JPAExpressions.select(follow.targetId) + .from(follow) + .where(follow.userId.eq(userId))) + , ltPostId(page.getLastId()) + ) + .orderBy(post.id.desc()) + .limit(page.getLimit()) + .fetch()); + } + + private BooleanExpression ltPostId(final Long postId) { + return postId == null ? null : post.id.lt(postId); + } + + @Override + public LongIdList findPostIdsByUserId(final Long userId, final Long targetId, final NoOffsetIdPage page) { + return new LongIdList(factory.select(post.id) + .from(post) + .where(post.userId.eq(targetId) + , ltPostId(page.getLastId()) + ) + .orderBy(post.id.desc()) + .limit(page.getLimit()) + .fetch()); + } + + @Override + public List findAllByIdIn(final Long userId, final Collection postIds) { + final Map postDataMap = factory.from(post) + .where(post.id.in(postIds)) + .transform(groupBy(post.id).as(postDataProjection(userId))); + final Map> imageMap = factory.from(post) + .leftJoin(post.images, image) + .where(post.id.in(postIds)) + .transform(groupBy(post.id).as(list(image))); + postDataMap.forEach((id, postData) -> postData.setImages(imageMap.get(id))); + return postDataMap.values().stream().toList(); + } } diff --git a/src/main/java/daybyquest/relation/query/FollowDaoQuerydslImpl.java b/src/main/java/daybyquest/relation/query/FollowDaoQuerydslImpl.java index a997699..bc07ab5 100644 --- a/src/main/java/daybyquest/relation/query/FollowDaoQuerydslImpl.java +++ b/src/main/java/daybyquest/relation/query/FollowDaoQuerydslImpl.java @@ -2,6 +2,7 @@ import static daybyquest.relation.domain.QFollow.follow; +import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import daybyquest.global.query.LongIdList; import daybyquest.global.query.NoOffsetIdPage; @@ -20,17 +21,25 @@ public FollowDaoQuerydslImpl(final JPAQueryFactory factory) { public LongIdList getFollowingIds(final Long userId, final NoOffsetIdPage page) { return new LongIdList(factory.select(follow.targetId) .from(follow) - .where(follow.userId.eq(userId).and(follow.targetId.gt(page.getLastId()))) + .where(follow.userId.eq(userId), isTargetIdGtLastId(page.getLastId())) .limit(page.getLimit()) .fetch()); } + private BooleanExpression isTargetIdGtLastId(final Long lastId) { + return lastId == null ? null : follow.targetId.gt(lastId); + } + @Override public LongIdList getFollowerIds(final Long targetId, final NoOffsetIdPage page) { return new LongIdList(factory.select(follow.userId) .from(follow) - .where(follow.targetId.eq(targetId).and(follow.userId.gt(page.getLastId()))) + .where(follow.targetId.eq(targetId), isUserIdGtLastId(page.getLastId())) .limit(page.getLimit()) .fetch()); } + + private BooleanExpression isUserIdGtLastId(final Long lastId) { + return lastId == null ? null : follow.userId.gt(lastId); + } } diff --git a/src/main/java/daybyquest/user/query/ProfileDao.java b/src/main/java/daybyquest/user/query/ProfileDao.java index 22e4a40..d077c9c 100644 --- a/src/main/java/daybyquest/user/query/ProfileDao.java +++ b/src/main/java/daybyquest/user/query/ProfileDao.java @@ -2,6 +2,7 @@ import java.util.Collection; import java.util.List; +import java.util.Map; public interface ProfileDao { @@ -12,4 +13,6 @@ public interface ProfileDao { Profile getMine(final Long userId); List findAllByUserIdIn(final Long userId, final Collection targetIds); + + Map findMapByUserIdIn(final Long userId, final Collection targetIds); } diff --git a/src/main/java/daybyquest/user/query/ProfileDaoQuerydslImpl.java b/src/main/java/daybyquest/user/query/ProfileDaoQuerydslImpl.java index 4bb2f4d..880c8cf 100644 --- a/src/main/java/daybyquest/user/query/ProfileDaoQuerydslImpl.java +++ b/src/main/java/daybyquest/user/query/ProfileDaoQuerydslImpl.java @@ -5,6 +5,7 @@ import static daybyquest.relation.domain.QFollow.follow; import static daybyquest.user.domain.QUser.user; +import com.querydsl.core.group.GroupBy; import com.querydsl.core.types.ConstructorExpression; import com.querydsl.core.types.Projections; import com.querydsl.jpa.JPAExpressions; @@ -12,6 +13,7 @@ import daybyquest.global.error.exception.NotExistUserException; import java.util.Collection; import java.util.List; +import java.util.Map; import org.springframework.stereotype.Repository; @Repository @@ -103,4 +105,13 @@ public List findAllByUserIdIn(final Long userId, final Collection .orderBy(user.id.asc()) .fetch(); } + + @Override + public Map findMapByUserIdIn(final Long userId, final Collection targetIds) { + return factory + .from(user) + .where(user.id.in(targetIds)) + .orderBy(user.id.asc()) + .transform(GroupBy.groupBy(user.id).as(profileProjection(userId))); + } }