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

feat: 태그 검색 구현 #71

Merged
merged 6 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
33 changes: 26 additions & 7 deletions src/main/java/com/funeat/product/application/ProductService.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@
import com.funeat.recipe.dto.SortingRecipesResponse;
import com.funeat.recipe.persistence.RecipeImageRepository;
import com.funeat.recipe.persistence.RecipeRepository;
import com.funeat.review.persistence.ReviewRepository;
import com.funeat.review.persistence.ReviewTagRepository;
import com.funeat.tag.domain.Tag;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -61,23 +62,20 @@ public class ProductService {
private final CategoryRepository categoryRepository;
private final ProductRepository productRepository;
private final ReviewTagRepository reviewTagRepository;
private final ReviewRepository reviewRepository;
private final ProductRecipeRepository productRecipeRepository;
private final RecipeImageRepository recipeImageRepository;
private final RecipeRepository recipeRepository;
private final MemberRepository memberRepository;
private final RecipeFavoriteRepository recipeFavoriteRepository;

public ProductService(final CategoryRepository categoryRepository, final ProductRepository productRepository,
final ReviewTagRepository reviewTagRepository, final ReviewRepository reviewRepository,
final ReviewTagRepository reviewTagRepository,
final ProductRecipeRepository productRecipeRepository,
final RecipeImageRepository recipeImageRepository,
final RecipeRepository recipeRepository, final MemberRepository memberRepository,
final RecipeFavoriteRepository recipeFavoriteRepository) {
final RecipeImageRepository recipeImageRepository, final RecipeRepository recipeRepository,
final MemberRepository memberRepository, final RecipeFavoriteRepository recipeFavoriteRepository) {
this.categoryRepository = categoryRepository;
this.productRepository = productRepository;
this.reviewTagRepository = reviewTagRepository;
this.reviewRepository = reviewRepository;
this.productRecipeRepository = productRecipeRepository;
this.recipeImageRepository = recipeImageRepository;
this.recipeRepository = recipeRepository;
Expand Down Expand Up @@ -211,4 +209,25 @@ private RecipeDto createRecipeDto(final Long memberId, final Recipe recipe) {
final Boolean favorite = recipeFavoriteRepository.existsByMemberAndRecipeAndFavoriteTrue(member, recipe);
return RecipeDto.toDto(recipe, images, products, favorite);
}

public SearchProductsResponse getSearchResultsByTag(final Long tagId, final Long lastProductId) {
final List<Product> findProducts = findAllByTag(tagId, lastProductId);
final int resultSize = getResultSize(findProducts);
final List<Product> products = findProducts.subList(0, resultSize);

final boolean hasNext = hasNextPage(findProducts);
final List<SearchProductDto> productDtos = products.stream()
.map(SearchProductDto::toDto)
.toList();

return SearchProductsResponse.toResponse(hasNext, productDtos);
}

private List<Product> findAllByTag(Long tagId, Long lastProductId) {
final PageRequest size = PageRequest.ofSize(DEFAULT_CURSOR_PAGINATION_SIZE);
if (lastProductId == 0) {
return productRepository.searchProductsByTopTagsFirst(tagId, size);
}
return productRepository.searchProductsByTopTags(tagId, lastProductId, size);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import com.funeat.product.domain.Product;
import com.funeat.product.dto.ProductReviewCountDto;
import java.util.List;

import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ProductRepository extends BaseRepository<Product, Long> {
public interface ProductRepository extends BaseRepository<Product, Long>, ProductRepositoryCustom {

@Query("SELECT new com.funeat.product.dto.ProductReviewCountDto(p, COUNT(r.id)) "
+ "FROM Product p "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.funeat.product.persistence;

import com.funeat.product.domain.Product;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface ProductRepositoryCustom {

List<Product> searchProductsByTopTagsFirst(final Long tagId, final Pageable pageable);

List<Product> searchProductsByTopTags(final Long tagId, final Long lastProductId, final Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.funeat.product.persistence;

import com.funeat.product.domain.Product;
import com.funeat.tag.domain.Tag;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class ProductRepositoryImpl implements ProductRepositoryCustom {
wugawuga marked this conversation as resolved.
Show resolved Hide resolved

@PersistenceContext
private EntityManager entityManager;

@Override
public List<Product> searchProductsByTopTagsFirst(final Long tagId, final Pageable pageable) {
final String jpql = """
SELECT DISTINCT p\s
FROM Product p
WHERE p.id IN (
SELECT p2.id\s
FROM Product p2\s
JOIN p2.reviews r2\s
JOIN r2.reviewTags rt2\s
WHERE rt2.tag.id = :tagId\s
AND rt2.tag.id IN (
SELECT rt3.tag.id\s
FROM Review r3\s
JOIN r3.reviewTags rt3\s
WHERE r3.product.id = p2.id\s
GROUP BY rt3.tag.id\s
ORDER BY COUNT(rt3.tag.id) DESC\s
LIMIT 3
)
)
ORDER BY p.id DESC
""";
wugawuga marked this conversation as resolved.
Show resolved Hide resolved

final TypedQuery<Product> query = entityManager.createQuery(jpql, Product.class);
query.setParameter("tagId", tagId);
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());

return query.getResultList();
}

@Override
public List<Product> searchProductsByTopTags(final Long tagId, final Long lastProductId, final Pageable pageable) {
final String jpql = """
SELECT DISTINCT p\s
FROM Product p\s
WHERE p.id < :lastProductId\s
AND p.id IN (
SELECT p2.id\s
FROM Product p2\s
JOIN p2.reviews r2\s
JOIN r2.reviewTags rt2\s
WHERE rt2.tag.id = :tagId\s
AND rt2.tag.id IN (
SELECT rt3.tag.id\s
FROM Review r3\s
JOIN r3.reviewTags rt3\s
WHERE r3.product.id = p2.id\s
GROUP BY rt3.tag.id\s
ORDER BY COUNT(rt3.tag.id) DESC\s
LIMIT 3
)
GROUP BY p2.id\s
HAVING COUNT(DISTINCT rt2.tag.id) <= 3\s
)\s
ORDER BY p.id DESC
""";

final TypedQuery<Product> query = entityManager.createQuery(jpql, Product.class);
query.setParameter("tagId", tagId);
query.setParameter("lastProductId", lastProductId);
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());

return query.getResultList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,11 @@ public ResponseEntity<SortingRecipesResponse> getProductRecipes(@AuthenticationP
final SortingRecipesResponse response = productService.getProductRecipes(loginInfo.getId(), productId, pageable);
return ResponseEntity.ok(response);
}

@GetMapping("/search/tags/results")
public ResponseEntity<SearchProductsResponse> getSearchResultByTag(@RequestParam final Long tagId,
@RequestParam final Long lastProductId) {
final SearchProductsResponse response = productService.getSearchResultsByTag(tagId, lastProductId);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,13 @@ ResponseEntity<SearchProductResultsResponse> getSearchResults(@RequestParam fina
ResponseEntity<SortingRecipesResponse> getProductRecipes(@AuthenticationPrincipal final LoginInfo loginInfo,
@PathVariable final Long productId,
@PageableDefault final Pageable pageable);

@Operation(summary = "해당 태그 상품 목록 조회", description = "해당 태그가 포함된 상품 목록을 조회한다.")
@ApiResponse(
responseCode = "200",
description = "해당 태그 상품 목록 조회 성공."
)
@GetMapping("/search/tags/results")
public ResponseEntity<SearchProductsResponse> getSearchResultByTag(@RequestParam final Long tagId,
@RequestParam final Long lastProductId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@
import static com.funeat.acceptance.common.CommonSteps.정상_처리;
import static com.funeat.acceptance.common.CommonSteps.찾을수_없음;
import static com.funeat.acceptance.common.CommonSteps.페이지를_검증한다;
import static com.funeat.acceptance.product.ProductSteps.상품_검색_결과_조회_요청;
import static com.funeat.acceptance.product.ProductSteps.상품_랭킹_조회_요청;
import static com.funeat.acceptance.product.ProductSteps.상품_레시피_목록_요청;
import static com.funeat.acceptance.product.ProductSteps.상품_상세_조회_요청;
import static com.funeat.acceptance.product.ProductSteps.상품_자동_완성_검색_요청;
import static com.funeat.acceptance.product.ProductSteps.카테고리별_상품_목록_조회_요청;
import static com.funeat.acceptance.product.ProductSteps.*;
wugawuga marked this conversation as resolved.
Show resolved Hide resolved
import static com.funeat.acceptance.recipe.RecipeSteps.레시피_작성_요청;
import static com.funeat.acceptance.recipe.RecipeSteps.여러명이_레시피_좋아요_요청;
import static com.funeat.acceptance.review.ReviewSteps.리뷰_작성_요청;
Expand Down Expand Up @@ -657,6 +652,32 @@ class getProductRecipes_성공_테스트 {
}
}

@Nested
class getSearchResultsByTag_성공_테스트 {

@Test
void 상품_상세_정보를_조회한다() {
// given
final var 카테고리 = 카테고리_간편식사_생성();
단일_카테고리_저장(카테고리);
final var 상품 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리));
final var 상품2 = 단일_상품_저장(상품_삼각김밥_가격1000원_평점3점_생성(카테고리));
final var 태그1 = 단일_태그_저장(태그_맛있어요_TASTE_생성());
final var 태그2 = 단일_태그_저장(태그_단짠단짠_TASTE_생성());

리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지1), 리뷰추가요청_재구매X_생성(점수_4점, List.of(태그1, 태그2)));
리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품, 사진_명세_요청(이미지2), 리뷰추가요청_재구매X_생성(점수_4점, List.of(태그2)));
리뷰_작성_요청(로그인_쿠키_획득(멤버1), 상품2, 사진_명세_요청(이미지3), 리뷰추가요청_재구매X_생성(점수_1점, List.of(태그2)));

// when
final var 응답 = 태그_상품_검색_결과_조회_요청(태그2, 0L);

// then
STATUS_CODE를_검증한다(응답, 정상_처리);
상품_검색_결과를_검증한다(응답, false, List.of(상품2, 상품1));
}
}

private void 카테고리별_상품_목록_조회_결과를_검증한다(final ExtractableResponse<Response> response, final List<Long> productIds) {
final var actual = response.jsonPath()
.getList("products", ProductInCategoryDto.class);
Expand Down
11 changes: 11 additions & 0 deletions src/test/java/com/funeat/acceptance/product/ProductSteps.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,15 @@ public class ProductSteps {
.then()
.extract();
}

public static ExtractableResponse<Response> 태그_상품_검색_결과_조회_요청(final Long tagId, final Long lastProductId) {
return given()
.log().all()
.queryParam("tagId", tagId)
.queryParam("lastProductId", lastProductId)
.when()
.get("/api/search/tags/results")
.then()
.extract();
}
}
10 changes: 10 additions & 0 deletions src/test/java/com/funeat/fixture/ReviewFixture.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
import com.funeat.member.domain.Member;
import com.funeat.product.domain.Product;
import com.funeat.review.domain.Review;
import com.funeat.review.domain.ReviewTag;
import com.funeat.review.dto.ReviewCreateRequest;
import com.funeat.review.dto.ReviewFavoriteRequest;
import com.funeat.review.dto.SortingReviewRequest;
import com.funeat.tag.domain.Tag;

import java.time.LocalDateTime;
import java.util.List;

Expand Down Expand Up @@ -121,4 +124,11 @@ public class ReviewFixture {
public static SortingReviewRequest 리뷰정렬요청_존재하지않는정렬_생성() {
return new SortingReviewRequest("test,test", 1L);
}

public static class ReviewTagFixture {

public static ReviewTag 리뷰태그_생성(final Review review, final Tag tag) {
return ReviewTag.createReviewTag(review, tag);
}
}
}
Loading