Skip to content

Commit

Permalink
[BE] refactor: 상품 자동 완성 API 페이징 방식 변경 (#757)
Browse files Browse the repository at this point in the history
* refactor: 상품 자동 완성 API 커서 기반 페이징으로 변경

* test: 상품 자동 완성 repository 테스트 추가

* test: 상품 자동 완성 인수 테스트 수정

* style: import 정렬 순서 변경

* refactor: SearchProductsResponse 생성자 private으로 수정

* refactor: PageRequest 생성 방식 변경

* refactor: 상품 자동 완성 조회 쿼리 수정
  • Loading branch information
Go-Jaecheol authored Dec 29, 2023
1 parent c6e61aa commit bae8feb
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class ProductService {
private static final int THREE = 3;
private static final int TOP = 0;
private static final int RANKING_SIZE = 3;
private static final int PAGE_SIZE = 10;
private static final int DEFAULT_PAGE_SIZE = 10;
private static final int DEFAULT_CURSOR_PAGINATION_SIZE = 11;

Expand Down Expand Up @@ -135,15 +136,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.ofSize(PAGE_SIZE);
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;
private SearchProductsResponse(final boolean hasNext, final List<SearchProductDto> products) {
this.hasNext = hasNext;
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 @@ -26,7 +26,16 @@ List<ProductReviewCountDto> findAllByAverageRatingGreaterThan3(final LocalDateTi
+ "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 last ON last.id = :lastId "
+ "WHERE p.name LIKE CONCAT('%', :name, '%') "
+ "AND (last.name LIKE CONCAT(:name, '%') "
+ "AND ((p.name LIKE CONCAT(:name, '%') AND p.id < :lastId) OR (p.name NOT LIKE CONCAT(:name, '%'))) "
+ "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 @@ -51,9 +51,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 @@ -54,7 +54,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
Expand Up @@ -98,6 +98,7 @@
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;
Expand Down Expand Up @@ -414,14 +415,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 @@ -432,14 +430,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 @@ -449,21 +444,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 @@ -477,14 +469,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
Expand Up @@ -2,7 +2,6 @@

import static io.restassured.RestAssured.given;


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

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
Expand Up @@ -24,6 +24,7 @@
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.PageRequest;

@SuppressWarnings("NonAsciiCharacters")
class ProductRepositoryTest extends RepositoryTest {
Expand Down Expand Up @@ -102,7 +103,7 @@ class findAllByAverageRatingGreaterThan3_성공_테스트 {
}

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

@Test
void 상품명에_검색어가_포함된_상품들을_조회한다() {
Expand All @@ -114,12 +115,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

0 comments on commit bae8feb

Please sign in to comment.