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] refactor: 상품 자동 완성 API 페이징 방식 변경 #757

Merged
merged 8 commits into from
Dec 29, 2023
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package com.funeat.product.application;

import static com.funeat.product.exception.CategoryErrorCode.CATEGORY_NOT_FOUND;
import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND;

import com.funeat.common.dto.PageDto;
import com.funeat.product.domain.Category;
import com.funeat.product.domain.Product;
Expand Down Expand Up @@ -30,16 +27,20 @@
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.Objects;
import java.util.stream.Collectors;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

import static com.funeat.product.exception.CategoryErrorCode.CATEGORY_NOT_FOUND;
import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND;

@Service
@Transactional(readOnly = true)
public class ProductService {
Expand All @@ -48,6 +49,7 @@ public class ProductService {
private static final int TOP = 0;
public static final String REVIEW_COUNT = "reviewCount";
private static final int RANKING_SIZE = 3;
private static final int PAGE_SIZE = 10;

private final CategoryRepository categoryRepository;
private final ProductRepository productRepository;
Expand Down Expand Up @@ -115,15 +117,23 @@ public RankingProductsResponse getTop3Products() {
return RankingProductsResponse.toResponse(rankingProductDtos);
}

public SearchProductsResponse searchProducts(final String query, final Pageable pageable) {
final Page<Product> products = productRepository.findAllByNameContaining(query, pageable);
public SearchProductsResponse searchProducts(final String query, final Long lastId) {
final List<Product> products = findAllByNameContaining(query, lastId);

final PageDto pageDto = PageDto.toDto(products);
final boolean hasNext = products.size() > PAGE_SIZE;
final List<SearchProductDto> productDtos = products.stream()
.map(SearchProductDto::toDto)
.collect(Collectors.toList());

return SearchProductsResponse.toResponse(pageDto, productDtos);
return SearchProductsResponse.toResponse(hasNext, productDtos);
}

private List<Product> findAllByNameContaining(final String query, final Long lastId) {
final PageRequest size = PageRequest.of(0, PAGE_SIZE);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PageRequest.ofSize(사이즈크기);
이 메소드를 사용해도 좋을 것 같아요! 내부들어가면 똑같습니다!

if (lastId == 0) {
return productRepository.findAllByNameContainingFirst(query, size);
}
return productRepository.findAllByNameContaining(query, lastId, size);
}

public SearchProductResultsResponse getSearchResults(final String query, final Pageable pageable) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
package com.funeat.product.dto;

import com.funeat.common.dto.PageDto;
import java.util.List;

public class SearchProductsResponse {

private final PageDto page;
private final boolean hasNext;
private final List<SearchProductDto> products;

public SearchProductsResponse(final PageDto page, final List<SearchProductDto> products) {
this.page = page;
public SearchProductsResponse(final boolean hasNext, final List<SearchProductDto> products) {
this.hasNext = hasNext;
Go-Jaecheol marked this conversation as resolved.
Show resolved Hide resolved
this.products = products;
}

public static SearchProductsResponse toResponse(final PageDto page, final List<SearchProductDto> products) {
return new SearchProductsResponse(page, products);
public static SearchProductsResponse toResponse(final boolean hasNext, final List<SearchProductDto> products) {
return new SearchProductsResponse(hasNext, products);
}

public PageDto getPage() {
return page;
public boolean isHasNext() {
return hasNext;
}

public List<SearchProductDto> getProducts() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
import com.funeat.product.domain.Product;
import com.funeat.product.dto.ProductInCategoryDto;
import com.funeat.product.dto.ProductReviewCountDto;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface ProductRepository extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {

@Query(value = "SELECT new com.funeat.product.dto.ProductInCategoryDto(p.id, p.name, p.price, p.image, p.averageRating, COUNT(r)) "
Expand Down Expand Up @@ -44,7 +45,16 @@ Page<ProductInCategoryDto> findAllByCategoryOrderByReviewCountDesc(@Param("categ
+ "ORDER BY "
+ "(CASE WHEN p.name LIKE CONCAT(:name, '%') THEN 1 ELSE 2 END), "
+ "p.id DESC")
Page<Product> findAllByNameContaining(@Param("name") final String name, final Pageable pageable);
List<Product> findAllByNameContainingFirst(@Param("name") final String name, final Pageable pageable);

@Query("SELECT p FROM Product p "
+ "JOIN Product p2 ON p2.id = :lastId "
+ "WHERE p.name LIKE CONCAT('%', :name, '%') "
+ "AND ((p2.name LIKE CONCAT(:name, '%') AND p.id < :lastId) OR (p.name NOT LIKE CONCAT(:name, '%') AND p.id < :lastId)) "
+ "ORDER BY "
+ "(CASE WHEN p.name LIKE CONCAT(:name, '%') THEN 1 ELSE 2 END), "
+ "p.id DESC")
List<Product> findAllByNameContaining(@Param("name") final String name, final Long lastId, final Pageable pageable);

@Query("SELECT new com.funeat.product.dto.ProductReviewCountDto(p, COUNT(r.id)) FROM Product p "
+ "LEFT JOIN Review r ON r.product.id = p.id "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ public ResponseEntity<RankingProductsResponse> getRankingProducts() {

@GetMapping("/search/products")
public ResponseEntity<SearchProductsResponse> searchProducts(@RequestParam final String query,
@PageableDefault final Pageable pageable) {
final PageRequest pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());
final SearchProductsResponse response = productService.searchProducts(query, pageRequest);
@RequestParam final Long lastId) {
final SearchProductsResponse response = productService.searchProducts(query, lastId);
return ResponseEntity.ok(response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ ResponseEntity<ProductsInCategoryResponse> getAllProductsInCategory(
)
@GetMapping
ResponseEntity<SearchProductsResponse> searchProducts(@RequestParam final String query,
@PageableDefault final Pageable pageable);
@RequestParam final Long lastId);

@Operation(summary = "상품 검색 결과 조회", description = "문자열을 받아 상품을 검색하고 검색 결과들을 조회한다.")
@ApiResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
package com.funeat.acceptance.product;

import com.funeat.acceptance.common.AcceptanceTest;
import com.funeat.product.domain.Category;
import com.funeat.product.dto.ProductInCategoryDto;
import com.funeat.product.dto.ProductResponse;
import com.funeat.product.dto.RankingProductDto;
import com.funeat.product.dto.SearchProductDto;
import com.funeat.product.dto.SearchProductResultDto;
import com.funeat.product.dto.SearchProductsResponse;
import com.funeat.recipe.dto.RecipeDto;
import com.funeat.tag.dto.TagDto;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

import static com.funeat.acceptance.auth.LoginSteps.로그인_쿠키_획득;
import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE를_검증한다;
import static com.funeat.acceptance.common.CommonSteps.사진_명세_요청;
Expand Down Expand Up @@ -90,22 +108,6 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;

import com.funeat.acceptance.common.AcceptanceTest;
import com.funeat.product.domain.Category;
import com.funeat.product.dto.ProductInCategoryDto;
import com.funeat.product.dto.ProductResponse;
import com.funeat.product.dto.RankingProductDto;
import com.funeat.product.dto.SearchProductDto;
import com.funeat.product.dto.SearchProductResultDto;
import com.funeat.recipe.dto.RecipeDto;
import com.funeat.tag.dto.TagDto;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@SuppressWarnings("NonAsciiCharacters")
class ProductAcceptanceTest extends AcceptanceTest {

Expand Down Expand Up @@ -433,14 +435,11 @@ class searchProducts_성공_테스트 {
final var 상품1 = 단일_상품_저장(상품_애플망고_가격3000원_평점5점_생성(카테고리));
final var 상품2 = 단일_상품_저장(상품_망고빙수_가격5000원_평점4점_생성(카테고리));

final var 예상_응답_페이지 = 응답_페이지_생성(총_데이터_개수(2L), 총_페이지(1L), 첫페이지O, 마지막페이지O, FIRST_PAGE, PAGE_SIZE);

// when
final var 응답 = 상품_자동_완성_검색_요청("망고", FIRST_PAGE);
final var 응답 = 상품_자동_완성_검색_요청("망고", 0L);

// then
STATUS_CODE를_검증한다(응답, 정상_처리);
페이지를_검증한다(응답, 예상_응답_페이지);
상품_자동_완성_검색_결과를_검증한다(응답, List.of(상품2, 상품1));
}

Expand All @@ -451,14 +450,11 @@ class searchProducts_성공_테스트 {
단일_카테고리_저장(카테고리);
반복_애플망고_상품_저장(2, 카테고리);

final var 예상_응답_페이지 = 응답_페이지_생성(총_데이터_개수(0L), 총_페이지(0L), 첫페이지O, 마지막페이지O, FIRST_PAGE, PAGE_SIZE);

// when
final var 응답 = 상품_자동_완성_검색_요청("김밥", FIRST_PAGE);
final var 응답 = 상품_자동_완성_검색_요청("김밥", 0L);

// then
STATUS_CODE를_검증한다(응답, 정상_처리);
페이지를_검증한다(응답, 예상_응답_페이지);
상품_자동_완성_검색_결과를_검증한다(응답, Collections.emptyList());
}

Expand All @@ -468,21 +464,18 @@ class searchProducts_성공_테스트 {
final var 카테고리 = 카테고리_간편식사_생성();
단일_카테고리_저장(카테고리);
단일_상품_저장(상품_망고빙수_가격5000원_평점4점_생성(카테고리));
반복_애플망고_상품_저장(10, 카테고리);

final var 예상_응답_페이지1 = 응답_페이지_생성(총_데이터_개수(11L), 총_페이지(2L), 첫페이지O, 마지막페이지X, FIRST_PAGE, PAGE_SIZE);
final var 예상_응답_페이지2 = 응답_페이지_생성(총_데이터_개수(11L), 총_페이지(2L), 첫페이지X, 마지막페이지O, SECOND_PAGE, PAGE_SIZE);
반복_애플망고_상품_저장(15, 카테고리);

// when
final var 응답1 = 상품_자동_완성_검색_요청("망고", FIRST_PAGE);
final var 응답2 = 상품_자동_완성_검색_요청("망고", SECOND_PAGE);
final var 응답1 = 상품_자동_완성_검색_요청("망고", 0L);

final var result = 응답1.as(SearchProductsResponse.class).getProducts();
final var lastId = result.get(result.size() - 1).getId();
final var 응답2 = 상품_자동_완성_검색_요청("망고", lastId);

// then
STATUS_CODE를_검증한다(응답1, 정상_처리);
페이지를_검증한다(응답1, 예상_응답_페이지1);

STATUS_CODE를_검증한다(응답2, 정상_처리);
페이지를_검증한다(응답2, 예상_응답_페이지2);

결과값이_이전_요청_결과값에_중복되는지_검증(응답1, 응답2);
}
Expand All @@ -496,14 +489,11 @@ class searchProducts_성공_테스트 {
반복_애플망고_상품_저장(9, 카테고리);
단일_상품_저장(상품_망고빙수_가격5000원_평점4점_생성(카테고리));

final var 예상_응답_페이지 = 응답_페이지_생성(총_데이터_개수(11L), 총_페이지(2L), 첫페이지O, 마지막페이지X, FIRST_PAGE, PAGE_SIZE);

// when
final var 응답 = 상품_자동_완성_검색_요청("망고", FIRST_PAGE);
final var 응답 = 상품_자동_완성_검색_요청("망고", 0L);

// then
STATUS_CODE를_검증한다(응답, 정상_처리);
페이지를_검증한다(응답, 예상_응답_페이지);
상품_자동_완성_검색_결과를_검증한다(응답, List.of(상품11, 상품1, 상품10, 상품9, 상품8, 상품7, 상품6, 상품5, 상품4, 상품3));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.funeat.acceptance.product;

import static io.restassured.RestAssured.given;


import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;

import static io.restassured.RestAssured.given;

@SuppressWarnings("NonAsciiCharacters")
public class ProductSteps {

Expand Down Expand Up @@ -36,10 +35,10 @@ public class ProductSteps {
.extract();
}

public static ExtractableResponse<Response> 상품_자동_완성_검색_요청(final String query, final Long page) {
public static ExtractableResponse<Response> 상품_자동_완성_검색_요청(final String query, final Long lastId) {
return given()
.queryParam("query", query)
.queryParam("page", page)
.queryParam("lastId", lastId)
.when()
.get("/api/search/products")
.then()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package com.funeat.product.persistence;

import com.funeat.common.RepositoryTest;
import com.funeat.product.dto.ProductInCategoryDto;
import com.funeat.product.dto.ProductReviewCountDto;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.PageRequest;

import java.util.List;

import static com.funeat.fixture.CategoryFixture.카테고리_간편식사_생성;
import static com.funeat.fixture.MemberFixture.멤버_멤버1_생성;
import static com.funeat.fixture.MemberFixture.멤버_멤버2_생성;
Expand Down Expand Up @@ -32,13 +41,6 @@
import static com.funeat.fixture.ReviewFixture.리뷰_이미지test5_평점5점_재구매O_생성;
import static org.assertj.core.api.Assertions.assertThat;

import com.funeat.common.RepositoryTest;
import com.funeat.product.dto.ProductInCategoryDto;
import com.funeat.product.dto.ProductReviewCountDto;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@SuppressWarnings("NonAsciiCharacters")
class ProductRepositoryTest extends RepositoryTest {

Expand Down Expand Up @@ -245,7 +247,7 @@ class findAllByAverageRatingGreaterThan3_성공_테스트 {
}

@Nested
class findAllByNameContaining_성공_테스트 {
class findAllByNameContainingFirst_성공_테스트 {

@Test
void 상품명에_검색어가_포함된_상품들을_조회한다() {
Expand All @@ -257,12 +259,36 @@ class findAllByNameContaining_성공_테스트 {
final var product2 = 상품_망고빙수_가격5000원_평점4점_생성(category);
복수_상품_저장(product1, product2);

final var page = 페이지요청_기본_생성(0, 10);
final var expected = List.of(product2, product1);

// when
final var actual = productRepository.findAllByNameContainingFirst("망고", PageRequest.of(0, 2));

// then
assertThat(actual).usingRecursiveComparison()
.isEqualTo(expected);
}
}

@Nested
class findAllByNameContaining_성공_테스트 {

@Test
void 상품명에_검색어가_포함된_상품들을_조회한다() {
// given
final var category = 카테고리_간편식사_생성();
단일_카테고리_저장(category);

final var product1 = 상품_애플망고_가격3000원_평점5점_생성(category);
final var product2 = 상품_망고빙수_가격5000원_평점4점_생성(category);
final var product3 = 상품_망고빙수_가격5000원_평점4점_생성(category);
final var product4 = 상품_망고빙수_가격5000원_평점4점_생성(category);
복수_상품_저장(product1, product2, product3, product4);

final var expected = List.of(product2, product1);

// when
final var actual = productRepository.findAllByNameContaining("망고", page).getContent();
final var actual = productRepository.findAllByNameContaining("망고", 3L, PageRequest.of(0, 4));

// then
assertThat(actual).usingRecursiveComparison()
Expand Down