Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 상품에 대한 리뷰 목록 조회 동적 쿼리 구현 및 API 성능 개선 #607

Merged
merged 36 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4d23cff
feat: 좋아요 기준 내림차순 리뷰 목록 조회 쿼리 개선
70825 Sep 19, 2023
2dab498
test: 좋아요 기준 내림차순 테스트 재생성
70825 Sep 19, 2023
dfc6dc2
feat: 최신순으로 리뷰 목록 조회 쿼리 개선
70825 Sep 19, 2023
15febd7
test: 최신순 리뷰 목록 테스트 재생성
70825 Sep 19, 2023
80e83c2
feat: 평점순 정렬 리뷰 목록 조회 쿼리 개선
70825 Sep 19, 2023
c440c8c
test: 평점순 정렬 리뷰 목록 테스트 재생성
70825 Sep 19, 2023
5df31eb
feat: 정렬 조건에 따라 리뷰 목록을 반환하는 기능 추가
70825 Sep 19, 2023
2adbb63
feat: 정렬 기능 추가
70825 Sep 19, 2023
305a61d
refactor: 테스트 추가 및 conflict 해결
70825 Sep 19, 2023
6301f07
fix: 생성자가 여러개라 jackson이 json으로 변환하지 못하는 현상 수정
70825 Sep 20, 2023
be9c68f
fix: 2차 정렬 기준이 ID 기준 내림차순으로 수정
70825 Sep 20, 2023
e7c86de
fix: 좋아요를 누른 사람이 여러명이면 그 개수만큼 같은 리뷰를 반환하던 쿼리문 수정
70825 Sep 20, 2023
7595878
test: 프로덕션 코드 수정으로 인한 테스트 코드 수정
70825 Sep 20, 2023
f15a48a
refactor: 정렬 조건에 맞게 리뷰 목록 생성
70825 Sep 21, 2023
6230eae
refactor: 주석 삭제
70825 Sep 21, 2023
73a6891
fix: 데이터를 11개가 아니라 10개를 반환하도록 수정
70825 Sep 21, 2023
b0746b0
refactor: 리뷰 랭킹에서 좋아요가 같으면 최신 리뷰순으로 정렬하기 추가
70825 Sep 21, 2023
37eeb3a
temp: Criteria API + Specification으로 동적 쿼리 기능 구현
70825 Oct 12, 2023
3c709c5
refactor: SortSpec -> ReviewSortSpec 네이밍 변경
70825 Oct 16, 2023
e6074ae
refactor: 다른 곳에서 객체를 생성할 수 없도록 수정
70825 Oct 16, 2023
a5d8ccd
refactor: SortingReviewDto Wrapper 타입으로 변경, 유저 좋아요 데이터 기본값 변경
70825 Oct 16, 2023
0bf7ce1
refactor: static 삭제
70825 Oct 16, 2023
6706d35
refactor: SortingReviewDto의 멤버 변수에 final 추가
70825 Oct 16, 2023
36de6f1
refactor: 동적 쿼리 이전의 리뷰 목록 정렬 코드 삭제
70825 Oct 16, 2023
e09caee
refactor: 정렬 조건에 없는 필드 이름이 아니면 예외를 반환하도록 수정
70825 Oct 16, 2023
bbd289a
refactor: 클래스 네이밍 변경
70825 Oct 16, 2023
e72b3d0
refactor: 정렬만 하는 서비스 클래스 분리, Tag까지 가져올 수 있도록 수정
70825 Oct 16, 2023
1c8f0e9
Merge branch 'develop' into feat/issue-596
70825 Oct 16, 2023
28f2be9
fix: 충돌 해결
70825 Oct 16, 2023
6d7bb1e
test: Thread.sleep()을 1초가 아닌 0.1초로 수정
70825 Oct 16, 2023
4a6af05
refactor: Wrapper -> Primitive 타입으로 변경
70825 Oct 16, 2023
69fd2e2
refactor: is@@@ -> get@@@로 변경
70825 Oct 16, 2023
1c89357
refactor: 다음 페이지가 존재하는지는 hasNext로 통일
70825 Oct 16, 2023
db9b5d3
refactor: 리뷰 정렬 클래스 삭제, 상수 이름을 목적에 따라 분리
70825 Oct 16, 2023
0546c71
refactor: 에러 코드 네이밍 수정
70825 Oct 16, 2023
89e3bfe
refactor: exception 이름도 같이 수정
70825 Oct 16, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.funeat.review.dto.ReviewCreateRequest;
import com.funeat.review.dto.ReviewFavoriteRequest;
import com.funeat.review.dto.SortingReviewDto;
import com.funeat.review.dto.SortingReviewRequest;
import com.funeat.review.dto.SortingReviewsResponse;
import com.funeat.review.exception.ReviewException.NotAuthorOfReviewException;
import com.funeat.review.exception.ReviewException.ReviewNotFoundException;
Expand All @@ -50,9 +51,10 @@
@Transactional(readOnly = true)
public class ReviewService {

private static final int TOP = 0;
private static final int START = 0;
private static final int ONE = 1;
private static final String EMPTY_URL = "";
private static final int REVIEW_PAGE_SIZE = 10;

private final ReviewRepository reviewRepository;
private final TagRepository tagRepository;
Expand All @@ -61,20 +63,23 @@ public class ReviewService {
private final ProductRepository productRepository;
private final ReviewFavoriteRepository reviewFavoriteRepository;
private final ImageUploader imageUploader;
private final SortingReviewService sortingReviewService;
private final ApplicationEventPublisher eventPublisher;

public ReviewService(final ReviewRepository reviewRepository, final TagRepository tagRepository,
final ReviewTagRepository reviewTagRepository, final MemberRepository memberRepository,
final ProductRepository productRepository,
final ReviewFavoriteRepository reviewFavoriteRepository,
final ImageUploader imageUploader, final ApplicationEventPublisher eventPublisher) {
final ImageUploader imageUploader, final SortingReviewService sortingReviewService,
final ApplicationEventPublisher eventPublisher) {
this.reviewRepository = reviewRepository;
this.tagRepository = tagRepository;
this.reviewTagRepository = reviewTagRepository;
this.memberRepository = memberRepository;
this.productRepository = productRepository;
this.reviewFavoriteRepository = reviewFavoriteRepository;
this.imageUploader = imageUploader;
this.sortingReviewService = sortingReviewService;
this.eventPublisher = eventPublisher;
}

Expand Down Expand Up @@ -121,8 +126,7 @@ public void likeReview(final Long reviewId, final Long memberId, final ReviewFav

private ReviewFavorite saveReviewFavorite(final Member member, final Review review, final Boolean favorite) {
try {
final ReviewFavorite reviewFavorite = ReviewFavorite.create(member, review,
favorite);
final ReviewFavorite reviewFavorite = ReviewFavorite.create(member, review, favorite);
return reviewFavoriteRepository.save(reviewFavorite);
} catch (final DataIntegrityViolationException e) {
throw new MemberDuplicateFavoriteException(MEMBER_DUPLICATE_FAVORITE, member.getId());
Expand All @@ -134,33 +138,44 @@ public void updateProductImage(final Long productId) {
final Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId));

final PageRequest pageRequest = PageRequest.of(TOP, ONE);
final PageRequest pageRequest = PageRequest.of(START, ONE);

final List<Review> topFavoriteReview = reviewRepository.findPopularReviewWithImage(productId, pageRequest);
if (!topFavoriteReview.isEmpty()) {
final String topFavoriteReviewImage = topFavoriteReview.get(TOP).getImage();
final String topFavoriteReviewImage = topFavoriteReview.get(START).getImage();
product.updateImage(topFavoriteReviewImage);
}
}

public SortingReviewsResponse sortingReviews(final Long productId, final Pageable pageable, final Long memberId) {
final Member member = memberRepository.findById(memberId)
public SortingReviewsResponse sortingReviews(final Long productId, final Long memberId,
final SortingReviewRequest request) {
final Member findMember = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId));

final Product product = productRepository.findById(productId)
final Product findProduct = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId));

final Page<Review> reviewPage = reviewRepository.findReviewsByProduct(pageable, product);
final List<SortingReviewDto> sortingReviews = sortingReviewService.getSortingReviews(findMember, findProduct, request);
final int resultSize = getResultSize(sortingReviews);

final PageDto pageDto = PageDto.toDto(reviewPage);
final List<SortingReviewDto> reviewDtos = reviewPage.stream()
.map(review -> SortingReviewDto.toDto(review, member))
.collect(Collectors.toList());
final List<SortingReviewDto> resizeSortingReviews = sortingReviews.subList(START, resultSize);
final Boolean hasNext = hasNextPage(sortingReviews);

return SortingReviewsResponse.toResponse(resizeSortingReviews, hasNext);
}

private int getResultSize(final List<SortingReviewDto> sortingReviews) {
if (sortingReviews.size() <= REVIEW_PAGE_SIZE) {
return sortingReviews.size();
}
return REVIEW_PAGE_SIZE;
}

return SortingReviewsResponse.toResponse(pageDto, reviewDtos);
private Boolean hasNextPage(final List<SortingReviewDto> sortingReviews) {
return sortingReviews.size() > REVIEW_PAGE_SIZE;
}

public RankingReviewsResponse getTopReviews() {
final List<Review> rankingReviews = reviewRepository.findTop3ByOrderByFavoriteCountDesc();
final List<Review> rankingReviews = reviewRepository.findTop3ByOrderByFavoriteCountDescIdDesc();

final List<RankingReviewDto> dtos = rankingReviews.stream()
.map(RankingReviewDto::toDto)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.funeat.review.application;

import static com.funeat.review.exception.ReviewErrorCode.REVIEW_NOT_FOUND;

import com.funeat.member.domain.Member;
import com.funeat.product.domain.Product;
import com.funeat.review.domain.Review;
import com.funeat.review.dto.SortingReviewDto;
import com.funeat.review.dto.SortingReviewDtoWithoutTag;
import com.funeat.review.dto.SortingReviewRequest;
import com.funeat.review.exception.ReviewException.ReviewNotFoundException;
import com.funeat.review.persistence.ReviewRepository;
import com.funeat.review.specification.SortingReviewSpecification;
import com.funeat.tag.persistence.TagRepository;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
public class SortingReviewService {

private static final int FIRST_PAGE = 0;

private final ReviewRepository reviewRepository;
private final TagRepository tagRepository;

public SortingReviewService(final ReviewRepository reviewRepository, final TagRepository tagRepository) {
this.reviewRepository = reviewRepository;
this.tagRepository = tagRepository;
}

public List<SortingReviewDto> getSortingReviews(final Member member, final Product product,
final SortingReviewRequest request) {
final Long lastReviewId = request.getLastReviewId();
final String sortOption = request.getSort();

final Specification<Review> specification = getSortingSpecification(product, sortOption, lastReviewId);
final List<SortingReviewDtoWithoutTag> sortingReviewDtoWithoutTags = reviewRepository.getSortingReview(member,
specification, sortOption);

return addTagsToSortingReviews(sortingReviewDtoWithoutTags);
}

private List<SortingReviewDto> addTagsToSortingReviews(
final List<SortingReviewDtoWithoutTag> sortingReviewDtoWithoutTags) {
return sortingReviewDtoWithoutTags.stream()
.map(reviewDto -> SortingReviewDto.toDto(reviewDto,
tagRepository.findTagsByReviewId(reviewDto.getId())))
.collect(Collectors.toList());
}

private Specification<Review> getSortingSpecification(final Product product, final String sortOption,
final Long lastReviewId) {
if (lastReviewId == FIRST_PAGE) {
return SortingReviewSpecification.sortingFirstPageBy(product);
}

final Review lastReview = reviewRepository.findById(lastReviewId)
.orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, lastReviewId));
70825 marked this conversation as resolved.
Show resolved Hide resolved

return SortingReviewSpecification.sortingBy(product, sortOption, lastReview);
}
}
51 changes: 21 additions & 30 deletions backend/src/main/java/com/funeat/review/dto/SortingReviewDto.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.funeat.review.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.funeat.member.domain.Member;
import com.funeat.member.domain.favorite.ReviewFavorite;
import com.funeat.review.domain.Review;
import com.funeat.review.domain.ReviewTag;
import com.funeat.tag.domain.Tag;
import com.funeat.tag.dto.TagDto;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class SortingReviewDto {
Expand All @@ -23,6 +27,7 @@ public class SortingReviewDto {
private final boolean favorite;
private final LocalDateTime createdAt;

@JsonCreator
public SortingReviewDto(final Long id, final String userName, final String profileImage, final String image,
final Long rating, final List<TagDto> tags,
final String content, final boolean rebuy, final Long favoriteCount, final boolean favorite,
Expand All @@ -40,37 +45,23 @@ public SortingReviewDto(final Long id, final String userName, final String profi
this.createdAt = createdAt;
}

public static SortingReviewDto toDto(final Review review, final Member member) {
return new SortingReviewDto(
review.getId(),
review.getMember().getNickname(),
review.getMember().getProfileImage(),
review.getImage(),
review.getRating(),
findTagDtos(review),
review.getContent(),
review.getReBuy(),
review.getFavoriteCount(),
findReviewFavoriteChecked(review, member),
review.getCreatedAt()
);
}

private static List<TagDto> findTagDtos(final Review review) {
return review.getReviewTags().stream()
.map(ReviewTag::getTag)
public static SortingReviewDto toDto(final SortingReviewDtoWithoutTag sortingReviewDto, final List<Tag> tags) {
final List<TagDto> tagDtos = tags.stream()
.map(TagDto::toDto)
.collect(Collectors.toList());
}

private static boolean findReviewFavoriteChecked(final Review review, final Member member) {
return review.getReviewFavorites()
.stream()
.filter(reviewFavorite -> reviewFavorite.getReview().equals(review))
.filter(reviewFavorite -> reviewFavorite.getMember().equals(member))
.findFirst()
.map(ReviewFavorite::getFavorite)
.orElse(false);
return new SortingReviewDto(
sortingReviewDto.getId(),
sortingReviewDto.getUserName(),
sortingReviewDto.getProfileImage(),
sortingReviewDto.getImage(),
sortingReviewDto.getRating(),
tagDtos,
sortingReviewDto.getContent(),
sortingReviewDto.getRebuy(),
sortingReviewDto.getFavoriteCount(),
sortingReviewDto.getFavorite(),
sortingReviewDto.getCreatedAt());
}

public Long getId() {
Expand Down Expand Up @@ -101,15 +92,15 @@ public String getContent() {
return content;
}

public boolean isRebuy() {
public boolean getRebuy() {
return rebuy;
}

public Long getFavoriteCount() {
return favoriteCount;
}

public boolean isFavorite() {
public boolean getFavorite() {
return favorite;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.funeat.review.dto;

import java.time.LocalDateTime;
import java.util.Objects;

public class SortingReviewDtoWithoutTag {

private final Long id;
private final String userName;
private final String profileImage;
private final String image;
private final Long rating;
private final String content;
private final boolean rebuy;
private final Long favoriteCount;
private final boolean favorite;
private final LocalDateTime createdAt;

public SortingReviewDtoWithoutTag(final Long id, final String userName, final String profileImage,
final String image, final Long rating,
final String content, final boolean rebuy, final Long favoriteCount,
final Boolean favorite,
final LocalDateTime createdAt) {
final Boolean isFavorite = checkingFavorite(favorite);

this.id = id;
this.userName = userName;
this.profileImage = profileImage;
this.image = image;
this.rating = rating;
this.content = content;
this.rebuy = rebuy;
this.favoriteCount = favoriteCount;
this.favorite = isFavorite;
this.createdAt = createdAt;
}

private static Boolean checkingFavorite(final Boolean favorite) {
if (Objects.isNull(favorite)) {
return Boolean.FALSE;
}
return Boolean.TRUE;
}

public Long getId() {
return id;
}

public String getUserName() {
return userName;
}

public String getProfileImage() {
return profileImage;
}

public String getImage() {
return image;
}

public Long getRating() {
return rating;
}

public String getContent() {
return content;
}

public boolean getRebuy() {
return rebuy;
}

public Long getFavoriteCount() {
return favoriteCount;
}

public boolean getFavorite() {
return favorite;
}

public LocalDateTime getCreatedAt() {
return createdAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.funeat.review.dto;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.PositiveOrZero;

public class SortingReviewRequest {

@NotNull(message = "정렬 조건을 확인해주세요")
private String sort;

@NotNull(message = "마지막으로 조회한 리뷰 ID를 확인해주세요")
@PositiveOrZero(message = "마지막으로 조회한 ID는 0 이상이어야 합니다. (처음 조회하면 0)")
private Long lastReviewId;

public SortingReviewRequest(final String sort, final Long lastReviewId) {
this.sort = sort;
this.lastReviewId = lastReviewId;
}

public String getSort() {
return sort;
}

public Long getLastReviewId() {
return lastReviewId;
}
}
Loading