Skip to content

Commit

Permalink
[Feature] 팔로잉 피드, 사용자가 올린 게시물 목록 조회를 구현한다 (#35)
Browse files Browse the repository at this point in the history
* fix: transform이 동작하지 않는 에러 수정

* refactor: PageArgumentResolver가 lastId 값이 없다면 null로 채우도록 수정

* feature: 팔로잉 피드 조회 구현

* fix: 팔로잉, 팔로워 목록 조회 시 lastId가 null인 경우 동적쿼리를 사용

* feature: 사용자가 업로드한 게시물 목록 조회 구현
  • Loading branch information
vectorch9 authored Oct 26, 2023
1 parent 9609cad commit 1c180c7
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 13 deletions.
3 changes: 2 additions & 1 deletion src/main/java/daybyquest/global/config/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,6 +17,6 @@ public class JpaConfig {

@Bean
public JPAQueryFactory queryFactory() {
return new JPAQueryFactory(entityManager);
return new JPAQueryFactory(JPQLTemplates.DEFAULT, entityManager);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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> postData = postDao.findAllByIdIn(loginId, postIds.getIds());
return new PagePostsResponse(converter.convertFromPostData(loginId, postData), postIds.getLastId());
}
}
Original file line number Diff line number Diff line change
@@ -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> postData = postDao.findAllByIdIn(loginId, postIds.getIds());
return new PagePostsResponse(converter.convertFromPostData(loginId, postData), postIds.getLastId());
}
}
Original file line number Diff line number Diff line change
@@ -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<PostResponse> convertFromPostData(final Long loginId, final List<PostData> postData) {
final Set<Long> userIds = postData.stream().map(PostData::getUserId).collect(Collectors.toSet());
final Map<Long, Profile> profiles = profileDao.findMapByUserIdIn(loginId, userIds);
return postData.stream()
.map((pd) -> PostResponse.of(pd, profiles.get(pd.getUserId()))).toList();
}
}
19 changes: 19 additions & 0 deletions src/main/java/daybyquest/post/dto/response/PagePostsResponse.java
Original file line number Diff line number Diff line change
@@ -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<PostResponse> posts;

private Long lastId;

public PagePostsResponse(final List<PostResponse> posts, final Long lastId) {
this.posts = posts;
this.lastId = lastId;
}
}
30 changes: 29 additions & 1 deletion src/main/java/daybyquest/post/presentation/PostQueryApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}")
Expand All @@ -24,4 +36,20 @@ public ResponseEntity<PostResponse> getPost(@UserId final Long loginId, @PathVar
final PostResponse response = getPostService.invoke(loginId, postId);
return ResponseEntity.ok(response);
}

@GetMapping("/feed")
@Authorization
public ResponseEntity<PagePostsResponse> 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<PagePostsResponse> getPostByUsername(@UserId final Long loginId,
@PathVariable final String username, final NoOffsetIdPage page) {
final PagePostsResponse response = getPostByUsernameService.invoke(loginId, username, page);
return ResponseEntity.ok(response);
}
}
11 changes: 11 additions & 0 deletions src/main/java/daybyquest/post/query/PostDao.java
Original file line number Diff line number Diff line change
@@ -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<PostData> findAllByIdIn(final Long userId, final Collection<Long> postIds);
}
72 changes: 64 additions & 8 deletions src/main/java/daybyquest/post/query/PostDaoQuerydslImpl.java
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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();
Expand All @@ -45,4 +47,58 @@ public PostData getByPostId(final Long userId, final Long postId) {
postData.setImages(images);
return postData;
}

private ConstructorExpression<PostData> 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<PostData> findAllByIdIn(final Long userId, final Collection<Long> postIds) {
final Map<Long, PostData> postDataMap = factory.from(post)
.where(post.id.in(postIds))
.transform(groupBy(post.id).as(postDataProjection(userId)));
final Map<Long, List<Image>> 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();
}
}
13 changes: 11 additions & 2 deletions src/main/java/daybyquest/relation/query/FollowDaoQuerydslImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
3 changes: 3 additions & 0 deletions src/main/java/daybyquest/user/query/ProfileDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Collection;
import java.util.List;
import java.util.Map;

public interface ProfileDao {

Expand All @@ -12,4 +13,6 @@ public interface ProfileDao {
Profile getMine(final Long userId);

List<Profile> findAllByUserIdIn(final Long userId, final Collection<Long> targetIds);

Map<Long, Profile> findMapByUserIdIn(final Long userId, final Collection<Long> targetIds);
}
11 changes: 11 additions & 0 deletions src/main/java/daybyquest/user/query/ProfileDaoQuerydslImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
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;
import com.querydsl.jpa.impl.JPAQueryFactory;
import daybyquest.global.error.exception.NotExistUserException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Repository;

@Repository
Expand Down Expand Up @@ -103,4 +105,13 @@ public List<Profile> findAllByUserIdIn(final Long userId, final Collection<Long>
.orderBy(user.id.asc())
.fetch();
}

@Override
public Map<Long, Profile> findMapByUserIdIn(final Long userId, final Collection<Long> targetIds) {
return factory
.from(user)
.where(user.id.in(targetIds))
.orderBy(user.id.asc())
.transform(GroupBy.groupBy(user.id).as(profileProjection(userId)));
}
}

0 comments on commit 1c180c7

Please sign in to comment.