diff --git a/api/src/main/java/dev/handsup/auction/controller/AuctionApiController.java b/api/src/main/java/dev/handsup/auction/controller/AuctionApiController.java index 15f1f85d..1bf14baf 100644 --- a/api/src/main/java/dev/handsup/auction/controller/AuctionApiController.java +++ b/api/src/main/java/dev/handsup/auction/controller/AuctionApiController.java @@ -1,23 +1,18 @@ package dev.handsup.auction.controller; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import dev.handsup.auction.dto.request.RegisterAuctionRequest; import dev.handsup.auction.dto.response.AuctionDetailResponse; -import dev.handsup.auction.dto.response.RecommendAuctionResponse; import dev.handsup.auction.service.AuctionService; import dev.handsup.auth.annotation.NoAuth; import dev.handsup.auth.jwt.JwtAuthorization; -import dev.handsup.common.dto.PageResponse; import dev.handsup.user.domain.User; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -54,32 +49,4 @@ public ResponseEntity getAuctionDetail(@PathVariable("auc AuctionDetailResponse response = auctionService.getAuctionDetail(auctionId); return ResponseEntity.ok(response); } - - @NoAuth - @Operation(summary = "경매 추천 API", description = "정렬 조건에 따라 경매를 추천한다.") - @ApiResponse(useReturnTypeSchema = true) - @GetMapping("/recommend") - @Cacheable(cacheNames = "auctions") - public ResponseEntity> getRecommendAuctions( - @RequestParam(value = "si", required = false) String si, - @RequestParam(value = "gu", required = false) String gu, - @RequestParam(value = "dong", required = false) String dong, - Pageable pageable - ) { - PageResponse response = auctionService.getRecommendAuctions(si, gu, dong, pageable); - return ResponseEntity.ok(response); - } - - @Operation(summary = "유저 선호 카테고리 경매 조회 API", description = "유저가 선호하는 카테고리의 경매를 조회한다.") - @ApiResponse(useReturnTypeSchema = true) - @GetMapping("/recommend/category") - public ResponseEntity> getUserPreferredCategoryAuctions( - @Parameter(hidden = true) @JwtAuthorization User user, - Pageable pageable - ) { - PageResponse response = auctionService.getUserPreferredCategoryAuctions(user, - pageable); - return ResponseEntity.ok(response); - } - } diff --git a/api/src/main/java/dev/handsup/recommend/controller/RecommendApiController.java b/api/src/main/java/dev/handsup/recommend/controller/RecommendApiController.java new file mode 100644 index 00000000..e23da0dc --- /dev/null +++ b/api/src/main/java/dev/handsup/recommend/controller/RecommendApiController.java @@ -0,0 +1,82 @@ +package dev.handsup.recommend.controller; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import dev.handsup.auth.annotation.NoAuth; +import dev.handsup.auth.jwt.JwtAuthorization; +import dev.handsup.common.dto.PageResponse; +import dev.handsup.recommend.dto.RecommendAuctionResponse; +import dev.handsup.recommend.service.RecommendService; +import dev.handsup.user.domain.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "홈 추천 API") +@RestController +@RequiredArgsConstructor +public class RecommendApiController { + private final RecommendService recommendService; + + @NoAuth + @Operation(summary = "경매 추천 API", description = "정렬 조건에 따라 경매를 추천한다.") + @ApiResponse(useReturnTypeSchema = true) + @GetMapping("/api/auctions/recommend") + @Cacheable(cacheNames = "auctions") + public ResponseEntity> getRecommendAuctions( + @RequestParam(value = "si", required = false) String si, + @RequestParam(value = "gu", required = false) String gu, + @RequestParam(value = "dong", required = false) String dong, + Pageable pageable + ) { + PageResponse response = recommendService.getRecommendAuctions(si, gu, dong, pageable); + return ResponseEntity.ok(response); + } + + @NoAuth + @Operation(summary = "성능 개선된 경매 추천 API", description = "정렬 조건에 따라 경매를 추천한다.") + @ApiResponse(useReturnTypeSchema = true) + @GetMapping("/api/v2/auctions/recommend") + @Cacheable(cacheNames = "auctions") + public ResponseEntity> getRecommendAuctionsV2( + @RequestParam(value = "si", required = false) String si, + @RequestParam(value = "gu", required = false) String gu, + @RequestParam(value = "dong", required = false) String dong, + Pageable pageable + ) { + PageResponse response = recommendService.getRecommendAuctionsV2(si, gu, dong, pageable); + return ResponseEntity.ok(response); + } + + @Operation(summary = "유저 선호 카테고리 경매 조회 API", description = "유저가 선호하는 카테고리의 경매를 조회한다.") + @ApiResponse(useReturnTypeSchema = true) + @GetMapping("/api/auctions/recommend/category") + public ResponseEntity> getUserPreferredCategoryAuctions( + @Parameter(hidden = true) @JwtAuthorization User user, + Pageable pageable + ) { + PageResponse response = recommendService.getUserPreferredCategoryAuctions(user, + pageable); + return ResponseEntity.ok(response); + } + + @Operation(summary = "성능 개선된 유저 선호 카테고리 경매 조회 API", description = "유저가 선호하는 카테고리의 경매를 조회한다.") + @ApiResponse(useReturnTypeSchema = true) + @GetMapping("/api/v2/auctions/recommend/category") + public ResponseEntity> getUserPreferredCategoryAuctionsV2( + @Parameter(hidden = true) @JwtAuthorization User user, + Pageable pageable + ) { + PageResponse response = recommendService.getUserPreferredCategoryAuctionsV2(user, + pageable); + return ResponseEntity.ok(response); + } +} + diff --git a/api/src/main/java/dev/handsup/search/controller/SearchApiController.java b/api/src/main/java/dev/handsup/search/controller/SearchApiController.java index b728ca0b..359d68e3 100644 --- a/api/src/main/java/dev/handsup/search/controller/SearchApiController.java +++ b/api/src/main/java/dev/handsup/search/controller/SearchApiController.java @@ -5,13 +5,12 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import dev.handsup.auction.dto.request.AuctionSearchCondition; -import dev.handsup.auction.dto.response.AuctionSimpleResponse; import dev.handsup.auth.annotation.NoAuth; import dev.handsup.common.dto.PageResponse; +import dev.handsup.search.dto.AuctionSearchCondition; +import dev.handsup.search.dto.AuctionSearchResponse; import dev.handsup.search.dto.PopularKeywordsResponse; import dev.handsup.search.service.SearchService; import io.swagger.v3.oas.annotations.Operation; @@ -23,25 +22,35 @@ @Tag(name = "검색 API") @RestController @RequiredArgsConstructor -@RequestMapping("/api/auctions/search") public class SearchApiController { private final SearchService searchService; @NoAuth @Operation(summary = "경매 검색 API", description = "경매를 검색한다") @ApiResponse(useReturnTypeSchema = true) - @PostMapping - public ResponseEntity> searchAuctions( + @PostMapping("/api/auctions/search") + public ResponseEntity> searchAuctions( @Valid @RequestBody AuctionSearchCondition condition, Pageable pageable) { - PageResponse response = searchService.searchAuctions(condition, pageable); + PageResponse response = searchService.searchAuctions(condition, pageable); + return ResponseEntity.ok(response); + } + + @NoAuth + @Operation(summary = "최적화된 경매 검색 API", description = "경매를 검색한다") + @ApiResponse(useReturnTypeSchema = true) + @PostMapping("/api/v2/auctions/search") + public ResponseEntity> searchAuctionsV2( + @Valid @RequestBody AuctionSearchCondition condition, + Pageable pageable) { + PageResponse response = searchService.searchAuctionsV2(condition, pageable); return ResponseEntity.ok(response); } @NoAuth @Operation(summary = "인기 검색어 조회 API", description = "인기 검색어를 조회한다.") @ApiResponse(useReturnTypeSchema = true) - @GetMapping("/popular") + @GetMapping("/api/auctions/search/popular") public ResponseEntity getPopularKeywords() { PopularKeywordsResponse response = searchService.getPopularKeywords(); return ResponseEntity.ok(response); diff --git a/api/src/test/java/dev/handsup/auction/controller/AuctionApiControllerTest.java b/api/src/test/java/dev/handsup/auction/controller/AuctionApiControllerTest.java index 2a1f1e5d..5aea2ea6 100644 --- a/api/src/test/java/dev/handsup/auction/controller/AuctionApiControllerTest.java +++ b/api/src/test/java/dev/handsup/auction/controller/AuctionApiControllerTest.java @@ -12,8 +12,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import dev.handsup.auction.domain.Auction; import dev.handsup.auction.domain.auction_field.PurchaseTime; @@ -21,12 +19,10 @@ import dev.handsup.auction.domain.auction_field.TradingLocation; import dev.handsup.auction.domain.product.Product; import dev.handsup.auction.domain.product.ProductStatus; -import dev.handsup.auction.domain.product.product_category.PreferredProductCategory; import dev.handsup.auction.domain.product.product_category.ProductCategory; import dev.handsup.auction.dto.request.RegisterAuctionRequest; import dev.handsup.auction.exception.AuctionErrorCode; import dev.handsup.auction.repository.auction.AuctionRepository; -import dev.handsup.auction.repository.product.PreferredProductCategoryRepository; import dev.handsup.auction.repository.product.ProductCategoryRepository; import dev.handsup.common.support.ApiTestSupport; import dev.handsup.fixture.AuctionFixture; @@ -42,9 +38,6 @@ class AuctionApiControllerTest extends ApiTestSupport { @Autowired private ProductCategoryRepository productCategoryRepository; - @Autowired - private PreferredProductCategoryRepository preferredProductCategoryRepository; - @BeforeEach void setUp() { productCategory = ProductFixture.productCategory(DIGITAL_DEVICE); @@ -128,96 +121,4 @@ void getAuctionDetail() throws Exception { .andExpect(jsonPath("$.tradeDong").value(tradingLocation.getDong())) .andExpect(jsonPath("$.bookmarkCount").value(auction.getBookmarkCount())); } - - @DisplayName("[정렬 조건과 지역 필터에 따라 경매글 목록을 반환한다.]") - @Test - void getRecommendAuctionsWithFilter() throws Exception { - //given - String si = "서울시", gu = "서초구", dong1 = "방배동", dong2 = "반포동"; - String earlyEndDate = "2024-03-02", lateEndDate = "2024-03-10"; - Auction auction1 = AuctionFixture.auction(productCategory, lateEndDate, si, gu, dong1); - Auction auction2 = AuctionFixture.auction(productCategory, earlyEndDate, si, gu, dong1); - Auction auction3 = AuctionFixture.auction(productCategory, lateEndDate, si, gu, dong2); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - //when - mockMvc.perform(get("/api/auctions/recommend").param("sort", "마감일") - .param("si", si) - .param("gu", gu) - .param("dong", dong1) - .contentType(APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.size").value(2)) - .andExpect(jsonPath("$.content[0].auctionId").value(auction2.getId())) - .andExpect(jsonPath("$.content[0].endDate").value(auction2.getEndDate().atStartOfDay().toString())) - .andExpect(jsonPath("$.content[1].auctionId").value(auction1.getId())) - .andExpect(jsonPath("$.content[1].endDate").value(auction1.getEndDate().atStartOfDay().toString())) - .andExpect(jsonPath("$.hasNext").value(false)); - } - - @DisplayName("[정렬 조건에 따라 경매글 목록을 반환한다.]") - @Test - void getRecommendAuctionsWithOutFilter() throws Exception { - //given - Auction auction1 = AuctionFixture.auction(productCategory); - Auction auction2 = AuctionFixture.auction(productCategory); - Auction auction3 = AuctionFixture.auction(productCategory); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - //when - mockMvc.perform(get("/api/auctions/recommend").param("sort", "최근생성").contentType(APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.size").value(3)) - .andExpect(jsonPath("$.content[0].auctionId").value(auction3.getId())) - .andExpect(jsonPath("$.content[1].auctionId").value(auction2.getId())) - .andExpect(jsonPath("$.content[2].auctionId").value(auction1.getId())) - .andExpect(jsonPath("$.hasNext").value(false)); - } - - @DisplayName("[정렬 조건이 없을 시 예외를 반환한다.]") - @Test - void getRecommendAuctions_fails() throws Exception { - mockMvc.perform(get("/api/auctions/recommend").contentType(APPLICATION_JSON)) - .andDo(MockMvcResultHandlers.print()) - .andExpect(jsonPath("$.message").value(AuctionErrorCode.EMPTY_SORT_INPUT.getMessage())) - .andExpect(jsonPath("$.code").value(AuctionErrorCode.EMPTY_SORT_INPUT.getCode())); - } - - @DisplayName("[정렬 조건이 잘못되면 예외를 반환한다.]") - @Test - void getRecommendAuctions_fails2() throws Exception { - mockMvc.perform(get("/api/auctions/recommend").contentType(APPLICATION_JSON).param("sort", "NAN")) - .andDo(MockMvcResultHandlers.print()) - .andExpect(jsonPath("$.message").value(AuctionErrorCode.INVALID_SORT_INPUT.getMessage())) - .andExpect(jsonPath("$.code").value(AuctionErrorCode.INVALID_SORT_INPUT.getCode())); - } - - @DisplayName("[유저 선호 카테고리 경매를 북마크 순으로 정렬한다.]") - @Test - void getUserPreferredCategoryAuctions() throws Exception { - ProductCategory productCategory2 = productCategoryRepository.save(ProductCategory.from("생활/주방")); - ProductCategory notPreferredProductCategory = productCategoryRepository.save(ProductCategory.from("티켓/교환권")); - - preferredProductCategoryRepository.saveAll(List.of( - PreferredProductCategory.of(user, productCategory), - PreferredProductCategory.of(user, productCategory2) - )); - Auction auction1 = AuctionFixture.auction(productCategory); - ReflectionTestUtils.setField(auction1, "bookmarkCount", 3); - Auction auction2 = AuctionFixture.auction(productCategory2); - ReflectionTestUtils.setField(auction2, "bookmarkCount", 5); - - Auction auction3 = AuctionFixture.auction(notPreferredProductCategory); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - //when - mockMvc.perform(get("/api/auctions/recommend/category") - .header(AUTHORIZATION, "Bearer " + accessToken) - .contentType(APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.size").value(2)) - .andExpect(jsonPath("$.content[0].auctionId").value(auction2.getId())) - .andExpect(jsonPath("$.content[1].auctionId").value(auction1.getId())) - .andExpect(jsonPath("$.hasNext").value(false)); - } } diff --git a/api/src/test/java/dev/handsup/recommend/controller/RecommendApiControllerTest.java b/api/src/test/java/dev/handsup/recommend/controller/RecommendApiControllerTest.java new file mode 100644 index 00000000..4fa6ed24 --- /dev/null +++ b/api/src/test/java/dev/handsup/recommend/controller/RecommendApiControllerTest.java @@ -0,0 +1,150 @@ +package dev.handsup.recommend.controller; + +import static org.springframework.http.HttpHeaders.*; +import static org.springframework.http.MediaType.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDate; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.util.ReflectionTestUtils; + +import dev.handsup.auction.domain.auction_field.TradingLocation; +import dev.handsup.auction.domain.product.product_category.PreferredProductCategory; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; +import dev.handsup.auction.repository.product.PreferredProductCategoryRepository; +import dev.handsup.auction.repository.product.ProductCategoryRepository; +import dev.handsup.common.support.ApiTestSupport; +import dev.handsup.fixture.AuctionSearchFixture; +import dev.handsup.fixture.ProductFixture; +import dev.handsup.search.domain.AuctionSearch; + +@DisplayName("[Auction 통합 테스트]") +class RecommendApiControllerTest extends ApiTestSupport { + + private final String DIGITAL_DEVICE = "디지털 기기"; + private ProductCategory productCategory; + @Autowired + private AuctionSearchRepository auctionSearchRepository; + @Autowired + private ProductCategoryRepository productCategoryRepository; + + @Autowired + private PreferredProductCategoryRepository preferredProductCategoryRepository; + + @BeforeEach + void setUp() { + productCategory = ProductFixture.productCategory(DIGITAL_DEVICE); + productCategoryRepository.save(productCategory); + userRepository.save(user); + } + + @DisplayName("[지역 필터에 따라 경매글 목록을 반환한다.]") + @Test + void getRecommendAuctionsWithFilter() throws Exception { + //given + String si = "서울시", gu = "서초구", dong = "방배동", trash = "반포동"; + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L,1L, TradingLocation.of(si,gu,dong)); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L,2L, TradingLocation.of(si,gu,trash)); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L,3L, TradingLocation.of(si,gu,dong)); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + + //when + mockMvc.perform(get("/api/v2/auctions/recommend").param("sort", "최신순") + .param("si", si) + .param("gu", gu) + .param("dong", dong) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size").value(2)) + .andExpect(jsonPath("$.content[0].auctionId").value(auctionSearch3.getAuctionId())) + .andExpect(jsonPath("$.content[1].auctionId").value(auctionSearch1.getAuctionId())) + .andExpect(jsonPath("$.hasNext").value(false)); + } + + @DisplayName("[정렬 조건과 지역 필터에 따라 경매글 목록을 반환한다.]") + @Test + void getRecommendAuctionsWithFilterAndSort() throws Exception { + //given + String si = "서울시", gu = "서초구", dong = "방배동", trash = "반포동"; + + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L,1L,TradingLocation.of(si,gu,dong), + LocalDate.now().minusDays(1)); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L,2L,TradingLocation.of(si,gu,trash), + LocalDate.now()); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L,3L,TradingLocation.of(si,gu,dong), + LocalDate.now().plusDays(1)); + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + + //when + mockMvc.perform(get("/api/v2/auctions/recommend").param("sort", "마감일") + .param("si", si) + .param("gu", gu) + .param("dong", dong) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size").value(2)) + .andExpect(jsonPath("$.content[0].auctionId").value(auctionSearch1.getAuctionId())) + .andExpect(jsonPath("$.content[0].endDate").value(auctionSearch1.getEndDate().atStartOfDay().toString())) + .andExpect(jsonPath("$.content[1].auctionId").value(auctionSearch3.getAuctionId())) + .andExpect(jsonPath("$.content[1].endDate").value(auctionSearch3.getEndDate().atStartOfDay().toString())) + .andExpect(jsonPath("$.hasNext").value(false)); + } + + @DisplayName("[정렬 조건이 없으면 최신순으로 반환한다.]") + @Test + void getRecommendAuctionsWithOutSort() throws Exception { + //given + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L,1L); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L,2L); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2)); + + //when + mockMvc.perform(get("/api/v2/auctions/recommend").contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size").value(2)) + .andExpect(jsonPath("$.content[0].auctionId").value(auctionSearch2.getAuctionId())) + .andExpect(jsonPath("$.content[1].auctionId").value(auctionSearch1.getAuctionId())) + .andExpect(jsonPath("$.hasNext").value(false)); + } + + + @DisplayName("[유저 선호 카테고리 경매를 북마크 순으로 정렬한다.]") + @Test + void getUserPreferredCategoryAuctions() throws Exception { + ProductCategory productCategory2 = productCategoryRepository.save(ProductCategory.from("생활/주방")); + ProductCategory notPreferredProductCategory = productCategoryRepository.save(ProductCategory.from("티켓/교환권")); + + preferredProductCategoryRepository.saveAll(List.of( + PreferredProductCategory.of(user, productCategory), + PreferredProductCategory.of(user, productCategory2) + )); + + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L,productCategory.getValue(),1L); + ReflectionTestUtils.setField(auctionSearch1, "bookmarkCount", 3); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L,productCategory.getValue(),2L); + ReflectionTestUtils.setField(auctionSearch2, "bookmarkCount", 5); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L,notPreferredProductCategory.getValue(),3L); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + + //when + mockMvc.perform(get("/api/v2/auctions/recommend/category") + .header(AUTHORIZATION, "Bearer " + accessToken) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.size").value(2)) + .andExpect(jsonPath("$.content[0].auctionId").value(auctionSearch2.getId())) + .andExpect(jsonPath("$.content[1].auctionId").value(auctionSearch1.getId())) + .andExpect(jsonPath("$.hasNext").value(false)); + } +} + diff --git a/api/src/test/java/dev/handsup/search/controller/SearchApiControllerTest.java b/api/src/test/java/dev/handsup/search/controller/SearchApiControllerTest.java index f6036102..61f552bb 100644 --- a/api/src/test/java/dev/handsup/search/controller/SearchApiControllerTest.java +++ b/api/src/test/java/dev/handsup/search/controller/SearchApiControllerTest.java @@ -16,25 +16,26 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; -import dev.handsup.auction.domain.Auction; +import dev.handsup.auction.domain.auction_field.TradingLocation; import dev.handsup.auction.domain.product.product_category.ProductCategory; -import dev.handsup.auction.dto.request.AuctionSearchCondition; -import dev.handsup.auction.repository.auction.AuctionRepository; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; import dev.handsup.auction.repository.product.ProductCategoryRepository; import dev.handsup.auction.repository.search.RedisSearchRepository; import dev.handsup.common.support.ApiTestSupport; -import dev.handsup.fixture.AuctionFixture; +import dev.handsup.fixture.AuctionSearchFixture; import dev.handsup.fixture.ProductFixture; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.search.dto.AuctionSearchCondition; @DisplayName("[검색 API 통합 테스트]") class SearchApiControllerTest extends ApiTestSupport { private final String DIGITAL_DEVICE = "디지털 기기"; - private ProductCategory productCategory; + private final String KEYWORD = "버즈"; + private ProductCategory productCategory; @Autowired - private AuctionRepository auctionRepository; - + private AuctionSearchRepository auctionSearchRepository; @Autowired private RedisSearchRepository redisSearchRepository; @Autowired @@ -59,72 +60,89 @@ public void clear() { @DisplayName("[경매를 검색해서 조회할 수 있다. 정렬 조건이 없을 경우 최신순으로 정렬한다.]") @Test void searchAuction() throws Exception { - Auction auction1 = AuctionFixture.auction(productCategory, "버즈 이어폰 팔아요"); - Auction auction2 = AuctionFixture.auction(productCategory, "에버어즈팟"); - Auction auction3 = AuctionFixture.auction(productCategory, "버즈 이어폰 팔아요"); + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L,1L, KEYWORD+"팔까요?"); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L,2L, "버증팔아요"); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L,3L, KEYWORD+"팔아요"); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword("버즈").build(); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); + .keyword(KEYWORD) + .build(); - mockMvc.perform(post("/api/auctions/search") + mockMvc.perform(post("/api/v2/auctions/search") .contentType(APPLICATION_JSON) .content(toJson(condition))) .andDo(MockMvcResultHandlers.print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.content[0].title").value(auction1.getTitle())) - .andExpect(jsonPath("$.content[0].currentBiddingPrice").value(auction1.getCurrentBiddingPrice())) - .andExpect(jsonPath("$.content[0].bookmarkCount").value(auction1.getBookmarkCount())) - .andExpect(jsonPath("$.content[0].dong").value(auction1.getTradingLocation().getDong())) + .andExpect(jsonPath("$.content[0].title").value(auctionSearch3.getTitle())) + .andExpect(jsonPath("$.content[0].currentBiddingPrice").value(auctionSearch3.getCurrentBiddingPrice())) + .andExpect(jsonPath("$.content[0].bookmarkCount").value(auctionSearch3.getBookmarkCount())) + .andExpect(jsonPath("$.content[0].dong").value(auctionSearch3.getTradingLocation().getDong())) .andExpect(jsonPath("$.content[0].createdAt").exists()) - .andExpect(jsonPath("$.content[1].title").value(auction3.getTitle())); + .andExpect(jsonPath("$.content[1].title").value(auctionSearch1.getTitle())); } - @DisplayName("[경매를 북마크 순으로 정렬할 수 있다.]") + @DisplayName("[경매를 지역으로 필터링하고, 북마크 순으로 정렬할 수 있다.]") @Test void searchAuctionSort() throws Exception { - Auction auction1 = AuctionFixture.auction(productCategory); - Auction auction2 = AuctionFixture.auction(productCategory); - Auction auction3 = AuctionFixture.auction(productCategory); - ReflectionTestUtils.setField(auction2, "bookmarkCount", 5); - ReflectionTestUtils.setField(auction3, "bookmarkCount", 3); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); + String si = "서울시", gu = "서초구", dong = "방배동", trash = "반포동"; + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, 1L, TradingLocation.of(si,gu,dong)); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, 2L,TradingLocation.of(si,gu,trash)); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, 3L,TradingLocation.of(si,gu,dong)); + + int bookmarkCnt = 0; + ReflectionTestUtils.setField(auctionSearch1, "bookmarkCount", bookmarkCnt); + ReflectionTestUtils.setField(auctionSearch2, "bookmarkCount", bookmarkCnt+1); + ReflectionTestUtils.setField(auctionSearch3, "bookmarkCount", bookmarkCnt+2); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword("버즈").build(); + .si(si) + .gu(gu) + .dong(dong) + .keyword(KEYWORD).build(); - mockMvc.perform(post("/api/auctions/search") + mockMvc.perform(post("/api/v2/auctions/search") .content(toJson(condition)) .contentType(APPLICATION_JSON) .param("sort", "북마크수")) .andDo(MockMvcResultHandlers.print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.size").value(3)) - .andExpect(jsonPath("$.content[0].auctionId").value(auction2.getId())) - .andExpect(jsonPath("$.content[1].auctionId").value(auction3.getId())) - .andExpect(jsonPath("$.content[2].auctionId").value(auction1.getId())); + .andExpect(jsonPath("$.size").value(2)) + .andExpect(jsonPath("$.content[0].auctionId").value(auctionSearch3.getAuctionId())) + .andExpect(jsonPath("$.content[1].auctionId").value(auctionSearch1.getAuctionId())); } - @DisplayName("[검색된 경매를 필터링할 수 있다.]") + @DisplayName("[최근 입찰 가격으로 필터링할 수 있다.]") @Test - void searchAuctionFilter() throws Exception { - Auction auction1 = AuctionFixture.auction(productCategory, "버즈", 15000); - Auction auction2 = AuctionFixture.auction(productCategory, "에어팟", 15000); - Auction auction3 = AuctionFixture.auction(productCategory, "버즈 팔아요", 25000); - ReflectionTestUtils.setField(auction2, "bookmarkCount", 5); - ReflectionTestUtils.setField(auction3, "bookmarkCount", 3); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); + void searchAuctionPriceFilter() throws Exception { + int minRange = 10000, maxRange = 20000; + + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, 1L, minRange); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, 2L, (maxRange)); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, 3L, (minRange+maxRange)/2); + AuctionSearch auctionSearch4 = AuctionSearchFixture.auctionSearch(4L, 4L, maxRange*2); + + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3,auctionSearch4)); AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword("버즈") - .minPrice(10000) - .maxPrice(20000) + .keyword(KEYWORD) + .minPrice(minRange) + .maxPrice(maxRange) .build(); - mockMvc.perform(post("/api/auctions/search") + mockMvc.perform(post("/api/v2/auctions/search") .content(toJson(condition)) - .contentType(APPLICATION_JSON)) + .contentType(APPLICATION_JSON) + .param("sort", "")) .andDo(MockMvcResultHandlers.print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.size").value(1)) - .andExpect(jsonPath("$.content[0].auctionId").value(auction1.getId())); + .andExpect(jsonPath("$.size").value(3)) + .andExpect(jsonPath("$.content[0].auctionId").value(auctionSearch3.getId())) + .andExpect(jsonPath("$.content[1].auctionId").value(auctionSearch2.getId())) + .andExpect(jsonPath("$.content[2].auctionId").value(auctionSearch1.getId())); } @DisplayName("[인기 검색어 순으로 조회할 수 있다.]") diff --git a/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionMapper.java b/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionMapper.java index 2dd739d8..2b50f514 100644 --- a/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionMapper.java +++ b/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionMapper.java @@ -3,6 +3,7 @@ import static lombok.AccessLevel.*; import dev.handsup.auction.domain.Auction; +import dev.handsup.auction.domain.auction_field.AuctionStatus; import dev.handsup.auction.domain.auction_field.PurchaseTime; import dev.handsup.auction.domain.auction_field.TradeMethod; import dev.handsup.auction.domain.auction_field.TradingLocation; @@ -12,8 +13,8 @@ import dev.handsup.auction.domain.product.product_category.ProductCategory; import dev.handsup.auction.dto.request.RegisterAuctionRequest; import dev.handsup.auction.dto.response.AuctionDetailResponse; -import dev.handsup.auction.dto.response.AuctionSimpleResponse; -import dev.handsup.auction.dto.response.RecommendAuctionResponse; +import dev.handsup.recommend.dto.RecommendAuctionResponse; +import dev.handsup.search.dto.AuctionSearchResponse; import dev.handsup.user.domain.User; import lombok.NoArgsConstructor; @@ -74,8 +75,9 @@ public static AuctionDetailResponse toAuctionDetailResponse(Auction auction) { ); } - public static AuctionSimpleResponse toAuctionSimpleResponse(Auction auction) { - return AuctionSimpleResponse.of( + public static AuctionSearchResponse toAuctionSearchResponse(Auction auction) { + boolean isProgress = auction.getStatus()== AuctionStatus.BIDDING; + return AuctionSearchResponse.of( auction.getId(), auction.getTitle(), auction.getCurrentBiddingPrice(), @@ -83,7 +85,7 @@ public static AuctionSimpleResponse toAuctionSimpleResponse(Auction auction) { auction.getBookmarkCount(), auction.getTradingLocation().getDong(), auction.getCreatedAt().toString(), - auction.getStatus().toString() + isProgress ); } diff --git a/core/src/main/java/dev/handsup/auction/dto/response/ChatRoomExistenceResponse.java b/core/src/main/java/dev/handsup/auction/dto/response/ChatRoomExistenceResponse.java deleted file mode 100644 index d103c68d..00000000 --- a/core/src/main/java/dev/handsup/auction/dto/response/ChatRoomExistenceResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package dev.handsup.auction.dto.response; - -public record ChatRoomExistenceResponse( - Boolean isExist -) { - public static ChatRoomExistenceResponse from(Boolean isExist) { - return new ChatRoomExistenceResponse(isExist); - } -} diff --git a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionQueryRepository.java b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionQueryRepository.java index f609a851..d7303145 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionQueryRepository.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionQueryRepository.java @@ -7,7 +7,7 @@ import dev.handsup.auction.domain.Auction; import dev.handsup.auction.domain.product.product_category.ProductCategory; -import dev.handsup.auction.dto.request.AuctionSearchCondition; +import dev.handsup.search.dto.AuctionSearchCondition; public interface AuctionQueryRepository { Slice searchAuctions(AuctionSearchCondition auctionSearchCondition, Pageable pageable); diff --git a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImpl.java b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImpl.java index 60007384..093a4a04 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImpl.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImpl.java @@ -24,9 +24,7 @@ import dev.handsup.auction.domain.auction_field.TradeMethod; import dev.handsup.auction.domain.product.ProductStatus; import dev.handsup.auction.domain.product.product_category.ProductCategory; -import dev.handsup.auction.dto.request.AuctionSearchCondition; -import dev.handsup.auction.exception.AuctionErrorCode; -import dev.handsup.common.exception.ValidationException; +import dev.handsup.search.dto.AuctionSearchCondition; import lombok.RequiredArgsConstructor; @Repository @@ -37,10 +35,10 @@ public class AuctionQueryRepositoryImpl implements AuctionQueryRepository { @Override public Slice searchAuctions(AuctionSearchCondition condition, Pageable pageable) { - List content = queryFactory.select(QAuction.auction) + List content = queryFactory.select(auction) .from(auction) .join(auction.product, product).fetchJoin() - .leftJoin(product.productCategory, productCategory).fetchJoin() + .join(product.productCategory, productCategory).fetchJoin() .where( keywordContains(condition.keyword()), categoryEq(condition.productCategory()), @@ -54,8 +52,8 @@ public Slice searchAuctions(AuctionSearchCondition condition, Pageable isProgressEq(condition.isProgress()) ) .orderBy(searchAuctionSort(pageable)) - .limit(pageable.getPageSize() + 1L) .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1) .fetch(); boolean hasNext = hasNext(pageable.getPageSize(), content); return new SliceImpl<>(content, pageable, hasNext); @@ -72,7 +70,7 @@ public Slice sortAuctionByCriteria(String si, String gu, String dong, P guEq(gu), dongEq(dong) ) - .orderBy(recommendAuctionSort(pageable)) + .orderBy(searchAuctionSort(pageable)) .limit(pageable.getPageSize() + 1L) .offset(pageable.getOffset()) .fetch(); @@ -126,19 +124,6 @@ private OrderSpecifier searchAuctionSort(Pageable pageable) { .orElse(auction.createdAt.desc()); // 기본값 최신순 } - private OrderSpecifier recommendAuctionSort(Pageable pageable) { - return pageable.getSort().stream() - .findFirst() - .map(order -> switch (order.getProperty()) { - case "북마크수" -> auction.bookmarkCount.desc(); - case "마감일" -> auction.endDate.asc(); - case "입찰수" -> auction.biddingCount.desc(); - case "최근생성" -> auction.createdAt.desc(); - default -> throw new ValidationException(AuctionErrorCode.INVALID_SORT_INPUT); //기본값 비허용 - }) - .orElseThrow(() -> new ValidationException(AuctionErrorCode.EMPTY_SORT_INPUT)); //null 비허용 - } - private BooleanExpression keywordContains(String keyword) { return keyword != null ? auction.title.contains(keyword) : null; } @@ -164,18 +149,18 @@ private BooleanExpression dongEq(String dong) { } private BooleanExpression initPriceMin(Integer minPrice) { - return (minPrice != null) ? auction.initPrice.goe(minPrice) : null; + return (minPrice != null) ? auction.currentBiddingPrice.goe(minPrice) : null; } private BooleanExpression initPriceMax(Integer maxPrice) { - return (maxPrice != null) ? auction.initPrice.loe(maxPrice) : null; + return (maxPrice != null) ? auction.currentBiddingPrice.loe(maxPrice) : null; } private BooleanExpression isNewProductEq(Boolean isNewProduct) { if (isNewProduct == null) { return null; } - if (Boolean.TRUE.equals(isNewProduct)) { + if (isNewProduct) { return auction.product.status.eq(ProductStatus.NEW); } else { return auction.product.status.eq(ProductStatus.CLEAN).or(auction.product.status.eq(ProductStatus.DIRTY)); diff --git a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionRepository.java b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionRepository.java index 284b5520..1cae3bd4 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionRepository.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionRepository.java @@ -10,7 +10,7 @@ import dev.handsup.auction.domain.auction_field.AuctionStatus; import dev.handsup.user.domain.User; -public interface AuctionRepository extends JpaRepository { +public interface AuctionRepository extends JpaRepository, AuctionQueryRepository { @Query("select distinct b.auction from Bookmark b " + "where b.user = :user") diff --git a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java new file mode 100644 index 00000000..61058eb8 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java @@ -0,0 +1,17 @@ +package dev.handsup.auction.repository.auction; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.search.dto.AuctionSearchCondition; + +public interface AuctionSearchQueryRepository { + Slice searchAuctions(AuctionSearchCondition auctionSearchCondition, Pageable pageable); + + Slice sortAuctionByCriteria(String si, String gu, String dong, Pageable pageable); + + Slice findByProductCategories(List productCategories, Pageable pageable); +} \ No newline at end of file diff --git a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java new file mode 100644 index 00000000..524144df --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java @@ -0,0 +1,158 @@ +package dev.handsup.auction.repository.auction; + +import static dev.handsup.search.domain.QAuctionSearch.*; +import static org.springframework.util.StringUtils.*; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.search.dto.AuctionSearchCondition; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class AuctionSearchQueryRepositoryImpl implements AuctionSearchQueryRepository { + + private final JPAQueryFactory queryFactory; + + @Override + public Slice searchAuctions(AuctionSearchCondition condition, Pageable pageable) { + List content = queryFactory.select(auctionSearch) + .from(auctionSearch) + .where( + keywordContains(condition.keyword()), + categoryEq(condition.productCategory()), + tradeMethodEq(condition.tradeMethod()), + siEq(condition.si()), + guEq(condition.gu()), + dongEq(condition.dong()), + initPriceMin(condition.minPrice()), + initPriceMax(condition.maxPrice()), + isNewProductEq(condition.isNewProduct()), + isProgressEq(condition.isProgress()) + ) + .orderBy(auctionSearchSort(pageable)) + .limit(pageable.getPageSize() + 1L) + .offset(pageable.getOffset()) + .fetch(); + boolean hasNext = hasNext(pageable.getPageSize(), content); + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public Slice sortAuctionByCriteria(String si, String gu, String dong, Pageable pageable) { + List content = queryFactory.select(auctionSearch) + .from(auctionSearch) + .where( + auctionSearch.isProgress.isTrue(), + siEq(si), + guEq(gu), + dongEq(dong) + ) + .orderBy(auctionSearchSort(pageable)) + .limit(pageable.getPageSize() + 1L) + .offset(pageable.getOffset()) + .fetch(); + boolean hasNext = hasNext(pageable.getPageSize(), content); + return new SliceImpl<>(content, pageable, hasNext); + } + + @Override + public Slice findByProductCategories(List productCategories, Pageable pageable) { + List content = queryFactory.select(auctionSearch) + .from(auctionSearch) + .where( + auctionSearch.category.in(productCategories) + ) + .orderBy(auctionSearch.bookmarkCount.desc()) + .limit(pageable.getPageSize() + 1L) + .offset(pageable.getOffset()) + .fetch(); + boolean hasNext = hasNext(pageable.getPageSize(), content); + return new SliceImpl<>(content, pageable, hasNext); + } + + private OrderSpecifier auctionSearchSort(Pageable pageable) { + return pageable.getSort().stream() + .findFirst() + .map(order -> switch (order.getProperty()) { + case "북마크수" -> auctionSearch.bookmarkCount.desc(); + case "마감일" -> auctionSearch.endDate.asc(); + case "입찰수" -> auctionSearch.biddingCount.desc(); + default -> auctionSearch.createdAt.desc(); + }) + .orElse(auctionSearch.createdAt.desc()); // 기본값 최신순 + } + + private BooleanExpression keywordContains(String keyword) { + return keyword != null ? auctionSearch.title.contains(keyword) : null; + } + + private BooleanExpression categoryEq(String productCategory) { + return hasText(productCategory) ? auctionSearch.category.eq(productCategory) : null; + } + + private BooleanExpression tradeMethodEq(String tradeMethod) { + return hasText(tradeMethod) ? auctionSearch.tradeMethod.eq(TradeMethod.of(tradeMethod)) : null; + } + + private BooleanExpression siEq(String si) { + return hasText(si) ? auctionSearch.tradingLocation.si.eq(si) : null; + } + + private BooleanExpression guEq(String gu) { + return hasText(gu) ? auctionSearch.tradingLocation.gu.eq(gu) : null; + } + + private BooleanExpression dongEq(String dong) { + return hasText(dong) ? auctionSearch.tradingLocation.dong.eq(dong) : null; + } + + private BooleanExpression initPriceMin(Integer minPrice) { + return (minPrice != null) ? auctionSearch.currentBiddingPrice.goe(minPrice) : null; + } + + private BooleanExpression initPriceMax(Integer maxPrice) { + return (maxPrice != null) ? auctionSearch.currentBiddingPrice.loe(maxPrice) : null; + } + + private BooleanExpression isNewProductEq(Boolean isNewProduct) { + if (isNewProduct == null) { + return null; + } + if (isNewProduct) { + return auctionSearch.isNewProduct.isTrue(); + } else { + return auctionSearch.isNewProduct.isFalse(); + } + } + + private BooleanExpression isProgressEq(Boolean isProgress) { + if (isProgress == null) { + return null; + } + if (isProgress) { + return auctionSearch.isProgress.isTrue(); + } else { + return auctionSearch.isProgress.isFalse(); + } + } + + private boolean hasNext(int pageSize, List auctionSearches) { + if (auctionSearches.size() <= pageSize) { + return false; + } + auctionSearches.remove(pageSize); + return true; + } +} diff --git a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchRepository.java b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchRepository.java new file mode 100644 index 00000000..51c7b2da --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchRepository.java @@ -0,0 +1,32 @@ +package dev.handsup.auction.repository.auction; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; + +import dev.handsup.search.domain.AuctionSearch; +import jakarta.transaction.Transactional; + +public interface AuctionSearchRepository extends JpaRepository, AuctionSearchQueryRepository { + + @Transactional + @Modifying + @Query( + value = """ + UPDATE auction_search s + JOIN ( + SELECT a.auction_id, + COUNT(DISTINCT b.bookmark_id) AS bookmark_count, + COUNT(DISTINCT bd.bidding_id) AS bidding_count + FROM auction a + LEFT JOIN bookmark b ON a.auction_id = b.auction_id + LEFT JOIN bidding bd ON a.auction_id = bd.auction_id + GROUP BY a.auction_id + ) cnt ON s.auction_id = cnt.auction_id + SET s.bookmark_count = cnt.bookmark_count, + s.bidding_count = cnt.bidding_count + """, + nativeQuery = true + ) + void updateAuctionSearch(); +} \ No newline at end of file diff --git a/core/src/main/java/dev/handsup/auction/scheduler/AuctionScheduler.java b/core/src/main/java/dev/handsup/auction/scheduler/AuctionScheduler.java index 195e3573..2fcf5bbf 100644 --- a/core/src/main/java/dev/handsup/auction/scheduler/AuctionScheduler.java +++ b/core/src/main/java/dev/handsup/auction/scheduler/AuctionScheduler.java @@ -3,8 +3,8 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import dev.handsup.auction.repository.auction.AuctionQueryRepository; -import dev.handsup.bidding.repository.BiddingRepository; +import dev.handsup.auction.repository.auction.AuctionRepository; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,11 +12,16 @@ @RequiredArgsConstructor @Slf4j public class AuctionScheduler { - private final AuctionQueryRepository auctionQueryRepository; - private final BiddingRepository biddingRepository; + private final AuctionRepository auctionRepository; + private final AuctionSearchRepository auctionSearchRepository; @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") public void updateAuctionStatus() { - auctionQueryRepository.updateAuctionStatusAfterEndDate(); + auctionRepository.updateAuctionStatusAfterEndDate(); + } + + @Scheduled(cron = "0 */3 * * * *") + public void updateAuctionSearch(){ + auctionSearchRepository.updateAuctionSearch(); } } diff --git a/core/src/main/java/dev/handsup/auction/service/AuctionService.java b/core/src/main/java/dev/handsup/auction/service/AuctionService.java index 31db6aef..1f7219b6 100644 --- a/core/src/main/java/dev/handsup/auction/service/AuctionService.java +++ b/core/src/main/java/dev/handsup/auction/service/AuctionService.java @@ -1,27 +1,20 @@ package dev.handsup.auction.service; -import java.util.List; - -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import dev.handsup.auction.domain.Auction; -import dev.handsup.auction.domain.product.product_category.PreferredProductCategory; import dev.handsup.auction.domain.product.product_category.ProductCategory; import dev.handsup.auction.dto.mapper.AuctionMapper; import dev.handsup.auction.dto.request.RegisterAuctionRequest; import dev.handsup.auction.dto.response.AuctionDetailResponse; -import dev.handsup.auction.dto.response.RecommendAuctionResponse; import dev.handsup.auction.exception.AuctionErrorCode; -import dev.handsup.auction.repository.auction.AuctionQueryRepository; import dev.handsup.auction.repository.auction.AuctionRepository; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; import dev.handsup.auction.repository.product.PreferredProductCategoryRepository; import dev.handsup.auction.repository.product.ProductCategoryRepository; -import dev.handsup.common.dto.CommonMapper; -import dev.handsup.common.dto.PageResponse; import dev.handsup.common.exception.NotFoundException; +import dev.handsup.search.dto.AuctionSearchMapper; import dev.handsup.user.domain.User; import lombok.RequiredArgsConstructor; @@ -30,14 +23,16 @@ public class AuctionService { private final AuctionRepository auctionRepository; + private final AuctionSearchRepository auctionSearchRepository; private final ProductCategoryRepository productCategoryRepository; private final PreferredProductCategoryRepository preferredProductCategoryRepository; - private final AuctionQueryRepository auctionQueryRepository; public AuctionDetailResponse registerAuction(RegisterAuctionRequest request, User user) { ProductCategory productCategory = getProductCategoryByValue(request.productCategory()); - Auction auction = AuctionMapper.toAuction(request, productCategory, user); - return AuctionMapper.toAuctionDetailResponse(auctionRepository.save(auction)); + Auction auction = auctionRepository.save(AuctionMapper.toAuction(request, productCategory, user)); + auctionSearchRepository.save(AuctionSearchMapper.toAuctionSearch(auction)); + + return AuctionMapper.toAuctionDetailResponse(auction); } @Transactional(readOnly = true) @@ -46,29 +41,6 @@ public AuctionDetailResponse getAuctionDetail(Long auctionId) { return AuctionMapper.toAuctionDetailResponse(auction); } - @Transactional(readOnly = true) - public PageResponse getRecommendAuctions(String si, String gu, String dong, - Pageable pageable) { - Slice auctionResponsePage = auctionQueryRepository - .sortAuctionByCriteria(si, gu, dong, pageable) - .map(AuctionMapper::toRecommendAuctionResponse); - return CommonMapper.toPageResponse(auctionResponsePage); - } - - @Transactional(readOnly = true) - public PageResponse getUserPreferredCategoryAuctions(User user, Pageable pageable) { - List productCategories = preferredProductCategoryRepository.findByUser(user) - .stream() - .map(PreferredProductCategory::getProductCategory) - .toList(); - - Slice auctionResponsePage = auctionQueryRepository - .findByProductCategories(productCategories, pageable) - .map(AuctionMapper::toRecommendAuctionResponse); - - return CommonMapper.toPageResponse(auctionResponsePage); - } - private ProductCategory getProductCategoryByValue(String productCategoryValue) { return productCategoryRepository.findByValue(productCategoryValue) .orElseThrow(() -> new NotFoundException(AuctionErrorCode.NOT_FOUND_PRODUCT_CATEGORY)); diff --git a/core/src/main/java/dev/handsup/auction/dto/response/RecommendAuctionResponse.java b/core/src/main/java/dev/handsup/recommend/dto/RecommendAuctionResponse.java similarity index 93% rename from core/src/main/java/dev/handsup/auction/dto/response/RecommendAuctionResponse.java rename to core/src/main/java/dev/handsup/recommend/dto/RecommendAuctionResponse.java index 29fe6622..fcdbc265 100644 --- a/core/src/main/java/dev/handsup/auction/dto/response/RecommendAuctionResponse.java +++ b/core/src/main/java/dev/handsup/recommend/dto/RecommendAuctionResponse.java @@ -1,4 +1,4 @@ -package dev.handsup.auction.dto.response; +package dev.handsup.recommend.dto; public record RecommendAuctionResponse( Long auctionId, diff --git a/core/src/main/java/dev/handsup/recommend/service/RecommendService.java b/core/src/main/java/dev/handsup/recommend/service/RecommendService.java new file mode 100644 index 00000000..7491a741 --- /dev/null +++ b/core/src/main/java/dev/handsup/recommend/service/RecommendService.java @@ -0,0 +1,76 @@ +package dev.handsup.recommend.service; + +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import dev.handsup.auction.domain.product.product_category.PreferredProductCategory; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import dev.handsup.auction.dto.mapper.AuctionMapper; +import dev.handsup.auction.repository.auction.AuctionRepository; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; +import dev.handsup.auction.repository.product.PreferredProductCategoryRepository; +import dev.handsup.common.dto.CommonMapper; +import dev.handsup.common.dto.PageResponse; +import dev.handsup.recommend.dto.RecommendAuctionResponse; +import dev.handsup.search.dto.AuctionSearchMapper; +import dev.handsup.user.domain.User; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class RecommendService { + private final AuctionRepository auctionRepository; + private final AuctionSearchRepository auctionSearchRepository; + private final PreferredProductCategoryRepository preferredProductCategoryRepository; + + @Transactional(readOnly = true) + public PageResponse getRecommendAuctions(String si, String gu, String dong, + Pageable pageable) { + Slice auctionResponsePage = auctionRepository + .sortAuctionByCriteria(si, gu, dong, pageable) + .map(AuctionMapper::toRecommendAuctionResponse); + return CommonMapper.toPageResponse(auctionResponsePage); + } + + @Transactional(readOnly = true) + public PageResponse getRecommendAuctionsV2(String si, String gu, String dong, + Pageable pageable) { + Slice auctionResponsePage = auctionSearchRepository + .sortAuctionByCriteria(si, gu, dong, pageable) + .map(AuctionSearchMapper::toRecommendAuctionResponse); + return CommonMapper.toPageResponse(auctionResponsePage); + } + + @Transactional(readOnly = true) + public PageResponse getUserPreferredCategoryAuctions(User user, Pageable pageable) { + List productCategories = preferredProductCategoryRepository.findByUser(user) + .stream() + .map(PreferredProductCategory::getProductCategory) + .toList(); + + Slice auctionResponsePage = auctionRepository + .findByProductCategories(productCategories, pageable) + .map(AuctionMapper::toRecommendAuctionResponse); + + return CommonMapper.toPageResponse(auctionResponsePage); + } + + @Transactional(readOnly = true) + public PageResponse getUserPreferredCategoryAuctionsV2(User user, Pageable pageable) { + List productCategories = preferredProductCategoryRepository.findByUser(user) + .stream() + .map(PreferredProductCategory::getProductCategory) // ProductCategory 추출 + .map(ProductCategory::getValue) // String value 추출 + .toList(); + + Slice auctionResponsePage = auctionSearchRepository + .findByProductCategories(productCategories, pageable) + .map(AuctionSearchMapper::toRecommendAuctionResponse); + + return CommonMapper.toPageResponse(auctionResponsePage); + } +} diff --git a/core/src/main/java/dev/handsup/search/domain/AuctionSearch.java b/core/src/main/java/dev/handsup/search/domain/AuctionSearch.java new file mode 100644 index 00000000..d1716f2a --- /dev/null +++ b/core/src/main/java/dev/handsup/search/domain/AuctionSearch.java @@ -0,0 +1,92 @@ +package dev.handsup.search.domain; + +import static jakarta.persistence.EnumType.*; +import static jakarta.persistence.GenerationType.*; +import static lombok.AccessLevel.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.auction.domain.auction_field.TradingLocation; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = PROTECTED) +public class AuctionSearch{ + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "auction_search_id") + private Long id; + + @Column(name = "auction_id", nullable = false, unique = true) + private Long auctionId; + + @Column(name = "product_id", nullable = false, unique = true) + private Long productId; + + @Column(name = "category", nullable = false) + private String category; + + @Column(name = "title", nullable = false) + private String title; + + @Column(name = "img_url", nullable = false) + private String imgUrl; + + @Column(name = "end_date", nullable = false) + private LocalDate endDate; + + @Embedded + private TradingLocation tradingLocation; + + @Column(name = "trade_method", nullable = false) + @Enumerated(STRING) + private TradeMethod tradeMethod; + + @Column(name = "is_new_product", nullable = false) + private boolean isNewProduct; + + @Column(name = "is_progress", nullable = false) + private boolean isProgress = true; + + @Column(name = "current_bidding_price", nullable = false) + private int currentBiddingPrice; + + @Column(name = "bidding_count", nullable = false) + private int biddingCount = 0; + + @Column(name = "bookmark_count", nullable = false) + private int bookmarkCount = 0; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Builder + public AuctionSearch(Long auctionId, Long productId, String category, String title, String imgUrl, + LocalDate endDate, TradingLocation tradingLocation, TradeMethod tradeMethod, + int currentBiddingPrice, boolean isNewProduct, LocalDateTime createdAt) { + this.auctionId = auctionId; + this.productId = productId; + this.category = category; + this.isNewProduct = isNewProduct; + this.title = title; + this.imgUrl = imgUrl; + this.endDate = endDate; + this.tradingLocation = tradingLocation; + this.tradeMethod = tradeMethod; + this.currentBiddingPrice = currentBiddingPrice; + this.createdAt = createdAt; + } +} + diff --git a/core/src/main/java/dev/handsup/auction/dto/request/AuctionSearchCondition.java b/core/src/main/java/dev/handsup/search/dto/AuctionSearchCondition.java similarity index 89% rename from core/src/main/java/dev/handsup/auction/dto/request/AuctionSearchCondition.java rename to core/src/main/java/dev/handsup/search/dto/AuctionSearchCondition.java index 784ae025..5e21fbd4 100644 --- a/core/src/main/java/dev/handsup/auction/dto/request/AuctionSearchCondition.java +++ b/core/src/main/java/dev/handsup/search/dto/AuctionSearchCondition.java @@ -1,4 +1,4 @@ -package dev.handsup.auction.dto.request; +package dev.handsup.search.dto; import jakarta.validation.constraints.NotBlank; import lombok.Builder; diff --git a/core/src/main/java/dev/handsup/search/dto/AuctionSearchMapper.java b/core/src/main/java/dev/handsup/search/dto/AuctionSearchMapper.java new file mode 100644 index 00000000..ddf3fe2b --- /dev/null +++ b/core/src/main/java/dev/handsup/search/dto/AuctionSearchMapper.java @@ -0,0 +1,58 @@ +package dev.handsup.search.dto; + +import static lombok.AccessLevel.*; + +import dev.handsup.auction.domain.Auction; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.auction.domain.auction_field.AuctionStatus; +import dev.handsup.auction.domain.product.Product; +import dev.handsup.recommend.dto.RecommendAuctionResponse; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class AuctionSearchMapper { + public static AuctionSearch toAuctionSearch(Auction auction) { + Product product = auction.getProduct(); + return AuctionSearch.builder() + .auctionId(auction.getId()) + .productId(product.getId()) + .category(product.getProductCategory().getValue()) + .title(auction.getTitle()) + .isNewProduct(auction.getStatus()== AuctionStatus.BIDDING) + .imgUrl(product.getImages().get(0).toString()) + .endDate(auction.getEndDate()) + .tradingLocation(auction.getTradingLocation()) + .tradeMethod(auction.getTradeMethod()) + .createdAt(auction.getCreatedAt()) + .build(); + } + + public static AuctionSearchResponse toAuctionSearchResponse(AuctionSearch auctionSearch) { + return AuctionSearchResponse.of( + auctionSearch.getId(), + auctionSearch.getTitle(), + auctionSearch.getCurrentBiddingPrice(), + auctionSearch.getImgUrl(), + auctionSearch.getBookmarkCount(), + auctionSearch.getTradingLocation().getDong(), + auctionSearch.getCreatedAt().toString(), + auctionSearch.isProgress() + ); + } + + public static RecommendAuctionResponse toRecommendAuctionResponse(AuctionSearch auctionSearch) { + return RecommendAuctionResponse.of( + auctionSearch.getId(), + auctionSearch.getTitle(), + auctionSearch.getTradingLocation().getDong(), + auctionSearch.getCurrentBiddingPrice(), + auctionSearch.getImgUrl(), + auctionSearch.getBookmarkCount(), + auctionSearch.getBiddingCount(), + auctionSearch.getCreatedAt().toString(), + auctionSearch.getEndDate().atStartOfDay().toString() + ); + } +} + + diff --git a/core/src/main/java/dev/handsup/auction/dto/response/AuctionSimpleResponse.java b/core/src/main/java/dev/handsup/search/dto/AuctionSearchResponse.java similarity index 65% rename from core/src/main/java/dev/handsup/auction/dto/response/AuctionSimpleResponse.java rename to core/src/main/java/dev/handsup/search/dto/AuctionSearchResponse.java index c0e07e3e..58e199bc 100644 --- a/core/src/main/java/dev/handsup/auction/dto/response/AuctionSimpleResponse.java +++ b/core/src/main/java/dev/handsup/search/dto/AuctionSearchResponse.java @@ -1,6 +1,6 @@ -package dev.handsup.auction.dto.response; +package dev.handsup.search.dto; -public record AuctionSimpleResponse( +public record AuctionSearchResponse( Long auctionId, String title, @@ -9,9 +9,9 @@ public record AuctionSimpleResponse( int bookmarkCount, String dong, String createdAt, - String status + boolean isProgress ) { - public static AuctionSimpleResponse of( + public static AuctionSearchResponse of( Long auctionId, String title, int currentBiddingPrice, @@ -19,9 +19,9 @@ public static AuctionSimpleResponse of( int bookmarkCount, String dong, String createdAt, - String status + boolean isProgress ) { - return new AuctionSimpleResponse( + return new AuctionSearchResponse( auctionId, title, currentBiddingPrice, @@ -29,7 +29,7 @@ public static AuctionSimpleResponse of( bookmarkCount, dong, createdAt, - status + isProgress ); } } diff --git a/core/src/main/java/dev/handsup/search/service/SearchService.java b/core/src/main/java/dev/handsup/search/service/SearchService.java index 28866c4a..79070795 100644 --- a/core/src/main/java/dev/handsup/search/service/SearchService.java +++ b/core/src/main/java/dev/handsup/search/service/SearchService.java @@ -1,37 +1,83 @@ package dev.handsup.search.service; +import java.util.List; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import dev.handsup.auction.domain.product.product_category.PreferredProductCategory; +import dev.handsup.auction.domain.product.product_category.ProductCategory; import dev.handsup.auction.dto.mapper.AuctionMapper; -import dev.handsup.auction.dto.request.AuctionSearchCondition; -import dev.handsup.auction.dto.response.AuctionSimpleResponse; -import dev.handsup.auction.repository.auction.AuctionQueryRepository; +import dev.handsup.auction.repository.auction.AuctionRepository; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; +import dev.handsup.auction.repository.product.PreferredProductCategoryRepository; import dev.handsup.auction.repository.search.RedisSearchRepository; import dev.handsup.common.dto.CommonMapper; import dev.handsup.common.dto.PageResponse; +import dev.handsup.recommend.dto.RecommendAuctionResponse; +import dev.handsup.search.dto.AuctionSearchCondition; +import dev.handsup.search.dto.AuctionSearchMapper; +import dev.handsup.search.dto.AuctionSearchResponse; import dev.handsup.search.dto.PopularKeywordsResponse; import dev.handsup.search.dto.SearchMapper; +import dev.handsup.user.domain.User; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class SearchService { - private final AuctionQueryRepository auctionQueryRepository; + private final AuctionRepository auctionRepository; + private final AuctionSearchRepository auctionSearchRepository; private final RedisSearchRepository redisSearchRepository; + private final PreferredProductCategoryRepository preferredProductCategoryRepository; + + + @Transactional(readOnly = true) + public PageResponse searchAuctions(AuctionSearchCondition condition, Pageable pageable) { + Slice auctionResponsePage = auctionRepository + .searchAuctions(condition, pageable) + .map(AuctionMapper::toAuctionSearchResponse); + redisSearchRepository.increaseSearchCount(condition.keyword()); + + return CommonMapper.toPageResponse(auctionResponsePage); + } @Transactional(readOnly = true) - public PageResponse searchAuctions(AuctionSearchCondition condition, Pageable pageable) { - Slice auctionResponsePage = auctionQueryRepository + public PageResponse searchAuctionsV2(AuctionSearchCondition condition, Pageable pageable) { + Slice auctionResponsePage = auctionSearchRepository .searchAuctions(condition, pageable) - .map(AuctionMapper::toAuctionSimpleResponse); + .map(AuctionSearchMapper::toAuctionSearchResponse); redisSearchRepository.increaseSearchCount(condition.keyword()); return CommonMapper.toPageResponse(auctionResponsePage); } + @Transactional(readOnly = true) + public PageResponse getRecommendAuctions(String si, String gu, String dong, + Pageable pageable) { + Slice auctionResponsePage = auctionSearchRepository + .sortAuctionByCriteria(si, gu, dong, pageable) + .map(AuctionSearchMapper::toRecommendAuctionResponse); + return CommonMapper.toPageResponse(auctionResponsePage); + } + + @Transactional(readOnly = true) + public PageResponse getUserPreferredCategoryAuctions(User user, Pageable pageable) { + List productCategories = preferredProductCategoryRepository.findByUser(user) + .stream() + .map(PreferredProductCategory::getProductCategory) // ProductCategory 추출 + .map(ProductCategory::getValue) // String value 추출 + .toList(); + + Slice auctionResponsePage = auctionSearchRepository + .findByProductCategories(productCategories, pageable) + .map(AuctionSearchMapper::toRecommendAuctionResponse); + + return CommonMapper.toPageResponse(auctionResponsePage); + } + @Transactional(readOnly = true) public PopularKeywordsResponse getPopularKeywords() { return SearchMapper.toPopularKeywordsResponse(redisSearchRepository.getPopularKeywords(10)); diff --git a/core/src/main/resources/db/auction.sql b/core/src/main/resources/db/auction.sql new file mode 100644 index 00000000..d37946c5 --- /dev/null +++ b/core/src/main/resources/db/auction.sql @@ -0,0 +1,54 @@ +SET SESSION cte_max_recursion_depth = 500000; + +INSERT INTO auction ( + seller_id, title, product_id, init_price, current_bidding_price, buy_price, + end_date, trade_method, auction_status, bidding_count, bookmark_count, created_at, updated_at +) +WITH RECURSIVE numbers AS ( + SELECT 1 AS n + UNION ALL + SELECT n + 1 FROM numbers WHERE n < 500000 +) +SELECT + FLOOR(1 + (RAND(n) * 10)), -- seller_id: 1~10 랜덤 + CASE + WHEN MOD(n-1, 4) = 0 THEN '에어팟' + WHEN MOD(n-1, 4) = 1 THEN '도서' + WHEN MOD(n-1, 4) = 2 THEN '목걸이' + ELSE '운동화' + END AS title, + n, -- product_id: 1~500000 일대일 + 10000 + (FLOOR(RAND(n+20) * 90000)), -- init_price: 10,000~99,999 + 10000 + (FLOOR(RAND(n+30) * 99000)), -- current_bidding_price + CASE WHEN MOD(n, 5) = 0 THEN 20000 + FLOOR(RAND(n+40) * 90000) ELSE NULL END, -- buy_price + DATE_ADD('2025-07-01', INTERVAL FLOOR(RAND(n+50)*180) DAY), -- end_date + CASE WHEN MOD(n, 2) = 0 THEN 'DIRECT' ELSE 'DELIVER' END, -- trade_method (enum 주의) + CASE MOD(n, 3) WHEN 0 THEN 'BIDDING' WHEN 1 THEN 'TRADING' ELSE 'COMPLETED' END, -- auction_status + FLOOR(RAND(n+60)*21), -- bidding_count: 0~20 + FLOOR(RAND(n+70)*21), -- bookmark_count: 0~20 + NOW(), + NOW() +FROM numbers; + + + +UPDATE auction +SET + si = '서울시', + gu = CASE MOD(auction_id, 6) + WHEN 0 THEN '관악구' + WHEN 1 THEN '서초구' + WHEN 2 THEN '관악구' + WHEN 3 THEN '강남구' + WHEN 4 THEN '송파구' + ELSE '중구' + END, + dong = CASE MOD(auction_id, 6) + WHEN 0 THEN '봉천동' + WHEN 1 THEN '방배동' + WHEN 2 THEN '신림동' + WHEN 3 THEN '역삼동' + WHEN 4 THEN '가락동' + ELSE '무교동' + END +WHERE auction_id BETWEEN 1 AND 500000; \ No newline at end of file diff --git a/core/src/main/resources/db/auction_search.sql b/core/src/main/resources/db/auction_search.sql new file mode 100644 index 00000000..d2964853 --- /dev/null +++ b/core/src/main/resources/db/auction_search.sql @@ -0,0 +1,81 @@ +INSERT INTO auction_search ( + auction_id, + product_id, + category, + title, + img_url, + end_date, + si, gu, dong, + trade_method, + current_bidding_price, + bidding_count, + bookmark_count, + is_new_product, + is_progress, + created_at +) +SELECT + a.auction_id, + a.product_id, + pc.value AS category, + a.title, + pi.image_url AS img_url, + a.end_date, + a.si, + a.gu, + a.dong, + a.trade_method, + a.current_bidding_price, + a.bidding_count, + a.bookmark_count, + CASE + WHEN p.status = 'NEW' THEN 1 + ELSE 0 + END AS is_new_product, + true, + a.created_at +FROM auction a + JOIN product p ON a.product_id = p.product_id + JOIN product_category pc ON p.product_category_id = pc.product_category_id + LEFT JOIN ( + SELECT product_id, MIN(image_url) AS image_url + FROM product_image + GROUP BY product_id +) pi ON p.product_id = pi.product_id; + + + +UPDATE auction_search +SET category = CONCAT( + '카테고리_', + LPAD( + MOD(auction_search_id - 1, 100000) + 1, + 6, '0' + ) + ) +WHERE auction_search_id >= 1; + + +UPDATE auction_search +SET category = + CASE + WHEN MOD(auction_search_id - 1, 13) = 0 THEN '디지털 기기' + WHEN MOD(auction_search_id - 1, 13) = 1 THEN '가구/인테리어' + WHEN MOD(auction_search_id - 1, 13) = 2 THEN '패션/잡화' + WHEN MOD(auction_search_id - 1, 13) = 3 THEN '생활가전' + WHEN MOD(auction_search_id - 1, 13) = 4 THEN '생활/주방' + WHEN MOD(auction_search_id - 1, 13) = 5 THEN '스포츠/레저' + WHEN MOD(auction_search_id - 1, 13) = 6 THEN '취미/게임/음반' + WHEN MOD(auction_search_id - 1, 13) = 7 THEN '뷰티/미용' + WHEN MOD(auction_search_id - 1, 13) = 8 THEN '반려동물용품' + WHEN MOD(auction_search_id - 1, 13) = 9 THEN '티켓/교환권' + WHEN MOD(auction_search_id - 1, 13) = 10 THEN '도서' + WHEN MOD(auction_search_id - 1, 13) = 11 THEN '유아도서' + ELSE '기타중고물품' + END +where auction_search_id >= 1; + + +UPDATE auction_search +SET bookmark_count = FLOOR(RAND() * 1000) +where auction_search_id >= 1; \ No newline at end of file diff --git a/core/src/main/resources/db/bidding.sql b/core/src/main/resources/db/bidding.sql new file mode 100644 index 00000000..e69de29b diff --git a/core/src/main/resources/db/bookmark.sql b/core/src/main/resources/db/bookmark.sql new file mode 100644 index 00000000..86d719db --- /dev/null +++ b/core/src/main/resources/db/bookmark.sql @@ -0,0 +1,16 @@ +SET SESSION cte_max_recursion_depth = 200000; + +INSERT INTO bookmark (user_id, auction_id, created_at) +WITH RECURSIVE numbers AS ( + SELECT 1 AS n + UNION ALL + SELECT n + 1 FROM numbers WHERE n < 100000 +) +SELECT + FLOOR(1 + RAND(n) * 10) AS user_id, + FLOOR(1 + RAND(n + 1234) * 500000) AS auction_id, + NOW() AS created_at +FROM numbers; + + + diff --git a/core/src/main/resources/db/init.sql b/core/src/main/resources/db/init.sql new file mode 100644 index 00000000..2766eb66 --- /dev/null +++ b/core/src/main/resources/db/init.sql @@ -0,0 +1,22 @@ +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (1, '디지털 기기'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (2, '가구/인테리어'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (3, '패션/잡화'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (4, '생활가전'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (5, '생활/주방'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (6, '스포츠/레저'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (7, '취미/게임/음반'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (8, '뷰티/미용'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (9, '반려동물용품'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (10, '티켓/교환권'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (11, '도서'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (12, '유아도서'); +INSERT INTO handsup_local.product_category (product_category_id, value) VALUES (13, '기타중고물품'); + + +INSERT INTO handsup_local.review_label (review_label_id, review_label_value) VALUES (1, '응답이 빨라요'); +INSERT INTO handsup_local.review_label (review_label_id, review_label_value) VALUES (2, '시간 약속을 잘 지켜요'); +INSERT INTO handsup_local.review_label (review_label_id, review_label_value) VALUES (3, '친절하고 매너있어요'); +INSERT INTO handsup_local.review_label (review_label_id, review_label_value) VALUES (4, '제가 있는 곳까지 와서 거래했어요'); +INSERT INTO handsup_local.review_label (review_label_id, review_label_value) VALUES (5, '물품설명이 자세해요'); +INSERT INTO handsup_local.review_label (review_label_id, review_label_value) VALUES (6, '물품상태가 설명한 것과 같아요'); +INSERT INTO handsup_local.review_label (review_label_id, review_label_value) VALUES (7, '저렴하게 구매했어요'); \ No newline at end of file diff --git a/core/src/main/resources/db/product.sql b/core/src/main/resources/db/product.sql new file mode 100644 index 00000000..e0c53315 --- /dev/null +++ b/core/src/main/resources/db/product.sql @@ -0,0 +1,60 @@ +use handsup_local; +-- product 테이블에 50만건 더미 데이터 생성 +SET SESSION cte_max_recursion_depth = 500000; + +INSERT INTO product (status, description, purchase_time, product_category_id, created_at, updated_at) +WITH RECURSIVE numbers AS ( + SELECT 1 AS n + UNION ALL + SELECT n + 1 FROM numbers WHERE n < 500000 +) +SELECT + CASE MOD(n, 3) + WHEN 0 THEN 'NEW' + WHEN 1 THEN 'CLEAN' + ELSE 'DIRTY' + END, + CONCAT('샘플 설명 ', n), + CASE MOD(n, 6) + WHEN 0 THEN 'UNDER_ONE_MONTH' + WHEN 1 THEN 'UNDER_THREE_MONTH' + WHEN 2 THEN 'UNDER_SIX_MONTH' + WHEN 3 THEN 'UNDER_ONE_YEAR' + WHEN 4 THEN 'ABOVE_ONE_YEAR' + ELSE 'UNKNOWN' + END, + FLOOR(1 + (RAND(n) * 13)), + NOW(), + NOW() +FROM numbers; + +UPDATE product +SET product_category_id = + CASE + WHEN MOD(product_id-1, 4) = 0 THEN 1 -- 에어팟 + WHEN MOD(product_id-1, 4) = 1 THEN 11 -- 도서 + WHEN MOD(product_id-1, 4) = 2 THEN 3 -- 목걸이 + ELSE 3 -- 운동화 + END +WHERE product_id BETWEEN 1 AND 500000; + + + +UPDATE product +SET product_category_id = + CASE + WHEN MOD(product_id-1, 13) = 0 THEN 1 -- 에어팟 + WHEN MOD(product_id-1, 13) = 1 THEN 2 -- 도서 + WHEN MOD(product_id-1, 13) = 2 THEN 3 -- 목걸이 + WHEN MOD(product_id-1, 13) = 3 THEN 4 -- 목걸이 + WHEN MOD(product_id-1, 13) = 4 THEN 5 -- 목걸이 + WHEN MOD(product_id-1, 13) = 5 THEN 6 -- 목걸이 + WHEN MOD(product_id-1, 13) = 6 THEN 7 -- 목걸이 + WHEN MOD(product_id-1, 13) = 7 THEN 8 -- 목걸이 + WHEN MOD(product_id-1, 13) = 8 THEN 9 -- 목걸이 + WHEN MOD(product_id-1, 13) = 9 THEN 10 -- 목걸이 + WHEN MOD(product_id-1, 13) = 10 THEN 11 -- 목걸이 + WHEN MOD(product_id-1, 13) = 11 THEN 12 -- 목걸이 + ELSE 13 -- 운동화 + END +WHERE product_id BETWEEN 1 AND 500000; diff --git a/core/src/main/resources/db/product_image.sql b/core/src/main/resources/db/product_image.sql new file mode 100644 index 00000000..1f49b5c9 --- /dev/null +++ b/core/src/main/resources/db/product_image.sql @@ -0,0 +1,4 @@ +INSERT INTO product_image (product_id, image_url) +SELECT product_id, 'https://hands-up-bucket.s3.ap-northeast-2.amazonaws.com/airpod.jpg' +FROM product +WHERE product_id BETWEEN 1 AND 500000; \ No newline at end of file diff --git a/core/src/main/resources/db/user.sql b/core/src/main/resources/db/user.sql new file mode 100644 index 00000000..3e5c1975 --- /dev/null +++ b/core/src/main/resources/db/user.sql @@ -0,0 +1,12 @@ +INSERT INTO user (email, password, nickname, score, si, gu, dong, profile_image_url, report_count, read_notification_count, created_at, updated_at) +VALUES + ('user1@email.com', 'testpw1!', 'user1', 100, '서울', '강남구', '역삼동', NULL, 0, 0, NOW(), NOW()), + ('user2@email.com', 'testpw1!', 'user2', 100, '서울', '서초구', '반포동', NULL, 0, 0, NOW(), NOW()), + ('user3@email.com', 'testpw1!', 'user3', 100, '부산', '해운대구', '우동', NULL, 0, 0, NOW(), NOW()), + ('user4@email.com', 'testpw1!', 'user4', 100, '부산', '수영구', '광안동', NULL, 0, 0, NOW(), NOW()), + ('user5@email.com', 'testpw1!', 'user5', 100, '대구', '수성구', '범어동', NULL, 0, 0, NOW(), NOW()), + ('user6@email.com', 'testpw1!', 'user6', 100, '대구', '중구', '동인동', NULL, 0, 0, NOW(), NOW()), + ('user7@email.com', 'testpw1!', 'user7', 100, '인천', '연수구', '송도동', NULL, 0, 0, NOW(), NOW()), + ('user8@email.com', 'testpw1!', 'user8', 100, '인천', '남동구', '구월동', NULL, 0, 0, NOW(), NOW()), + ('user9@email.com', 'testpw1!', 'user9', 100, '광주', '동구', '계림동', NULL, 0, 0, NOW(), NOW()), + ('user10@email.com', 'testpw1!', 'user10', 100, '대전', '서구', '둔산동', NULL, 0, 0, NOW(), NOW()); \ No newline at end of file diff --git a/core/src/test/http/auction.http b/core/src/test/http/auction.http new file mode 100644 index 00000000..4b9c2203 --- /dev/null +++ b/core/src/test/http/auction.http @@ -0,0 +1,21 @@ +### GET request to example server +POST http://localhost:8080/api/auctions +Content-Type: application/json +Authorization: Bearer eyJ0eXBlIjoiand0IiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VySWQiOjEsImlhdCI6MTc0OTk4ODQ3NiwiZXhwIjoyMDY1MzQ4NDc2fQ._I9ddh2ADiTV6aIitn19RRkW2sglpkRbRMZtlqC7xeM + +{ + "title": "에어팟 거의 새거", + "productCategory": "디지털 기기", + "initPrice": 3000, + "endDate": "2024-02-17", + "productStatus": "깨끗해요", + "purchaseTime": "6개월 이하", + "description": "거의 새거임", + "tradeMethod": "직거래", + "imageUrls": [ + "https://s3.ap-northeast-2.amazonaws.com/handsup-bucket/images/f2ed3069-3c65-4cb5-97dc-2f24a64ca103.jpg" + ], + "si": "서울시", + "gu": "성북구", + "dong": "동선동" +} \ No newline at end of file diff --git a/core/src/test/http/home.http b/core/src/test/http/home.http new file mode 100644 index 00000000..b40ef5d9 --- /dev/null +++ b/core/src/test/http/home.http @@ -0,0 +1,4 @@ +### 경매 추천 - 지역 필터(서울시 관악구 봉천동) +### 최근생성, 마감일, 입찰수, 북마크수 +GET http://localhost:8080/api/auctions/recommend?page=0&size=10&sort=최근생성&si=서울시&gu=관악구&dong=봉천동 +Content-Type: application/json \ No newline at end of file diff --git a/core/src/test/http/recommend.http b/core/src/test/http/recommend.http new file mode 100644 index 00000000..ba1b05fd --- /dev/null +++ b/core/src/test/http/recommend.http @@ -0,0 +1,18 @@ +### 경매 추천 +GET http://localhost:8080/api/auctions/recommend?si=서울시&gu=강남구&dong=역삼동&page=0&size=10&sort=북마크수 + + +### 경매 조건 추천 ver2 +GET http://localhost:8080/api/v2/auctions/recommend?si=서울시&gu=강남구&dong=역삼동&page=0&size=10&sort=북마크수 + +### 경매 카테고리 추천 ver1 +GET http://localhost:8080/api/auctions/recommend/category?page=0&size=10 +Content-Type: application/json +Authorization: Bearer eyJ0eXBlIjoiand0IiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VySWQiOjEsImlhdCI6MTc0OTk4ODQ3NiwiZXhwIjoyMDY1MzQ4NDc2fQ._I9ddh2ADiTV6aIitn19RRkW2sglpkRbRMZtlqC7xeM + +### 경매 카테고리 추천 ver2 +GET http://localhost:8080/api/v2/auctions/recommend/category?page=0&size=10 +Content-Type: application/json +Authorization: Bearer eyJ0eXBlIjoiand0IiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VySWQiOjEsImlhdCI6MTc0OTk4ODQ3NiwiZXhwIjoyMDY1MzQ4NDc2fQ._I9ddh2ADiTV6aIitn19RRkW2sglpkRbRMZtlqC7xeM + + diff --git a/core/src/test/http/search.http b/core/src/test/http/search.http new file mode 100644 index 00000000..b14d6903 --- /dev/null +++ b/core/src/test/http/search.http @@ -0,0 +1,78 @@ +### 기본 검색 (키워드만) +POST http://localhost:8080/api/auctions/search?page=0&size=10&sort=최신순 +Content-Type: application/json + +{ + "keyword": "에어팟" +} + +### + + + +### 카테고리, 거래방식 등 복합 검색 예시 +POST http://localhost:8080/api/auctions/search?page=0&size=10&sort=최신순 +Content-Type: application/json + +{ + "keyword": "에어팟", +"productCategory": "디지털 기기", + "si": "서울시", + "gu": "강남구", + "dong": "역삼동" +} + +### 카테고리, 거래방식 등 복합 검색 예시 +POST http://localhost:8080/api/auctions/search?page=0&size=10&sort=최신순 +Content-Type: application/json + +{ + "keyword": "에어팟", + "productCategory": "디지털 기기", + "si": "서울시", + "gu": "강남구", + "dong": "역삼동" +} + +### 카테고리, 거래방식 등 복합 검색 예시 +POST http://localhost:8080/api/v2/auctions/search?page=0&size=10&sort=최신순 +Content-Type: application/json + +{ + "keyword": "에어팟", + "productCategory": "디지털 기기", + "si": "서울시", + "gu": "강남구", + "dong": "역삼동" +} + +### + +### 북마크순 정렬 검색 +POST http://localhost:8080/api/auctions/search?page=0&size=10&sort=북마크수 +Content-Type: application/json + +{ + "keyword": "에어팟" +} + +### + +### 마감일 임박순 + 카테고리 +POST http://localhost:8080/api/auctions/search?page=0&size=10&sort=마감일 +Content-Type: application/json + +{ + "keyword": "청소기", + "productCategory": "APPLIANCES" +} + +### + +### 검색결과 없는 경우(키워드 없음) +POST http://localhost:8080/api/auctions/search?page=0&size=10 +Content-Type: application/json + +{ + "keyword": "존재하지않는상품명" +} \ No newline at end of file diff --git a/core/src/test/http/user.http b/core/src/test/http/user.http new file mode 100644 index 00000000..27d36e13 --- /dev/null +++ b/core/src/test/http/user.http @@ -0,0 +1,8 @@ +### 로그인 요청 (user1) +POST http://localhost:8080/api/auth/login +Content-Type: application/json + +{ + "email": "user1@email.com", + "password": "testpw1!" +} \ No newline at end of file diff --git a/core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImplTest.java b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryTest.java similarity index 53% rename from core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImplTest.java rename to core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryTest.java index 42ca9a5c..fd4dd6fd 100644 --- a/core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImplTest.java +++ b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryTest.java @@ -17,18 +17,16 @@ import dev.handsup.auction.domain.Auction; import dev.handsup.auction.domain.auction_field.AuctionStatus; -import dev.handsup.auction.domain.auction_field.TradeMethod; -import dev.handsup.auction.domain.product.ProductStatus; import dev.handsup.auction.domain.product.product_category.ProductCategory; -import dev.handsup.auction.dto.request.AuctionSearchCondition; import dev.handsup.auction.repository.product.ProductCategoryRepository; import dev.handsup.common.support.DataJpaTestSupport; import dev.handsup.fixture.AuctionFixture; import dev.handsup.fixture.ProductFixture; +import dev.handsup.search.dto.AuctionSearchCondition; import jakarta.persistence.EntityManager; @DisplayName("[AuctionQueryRepositoryImpl 테스트]") -class AuctionQueryRepositoryImplTest extends DataJpaTestSupport { +class AuctionQueryRepositoryTest extends DataJpaTestSupport { private final String DIGITAL_DEVICE = "디지털 기기"; private final String APPLIANCE = "가전제품"; @@ -38,14 +36,11 @@ class AuctionQueryRepositoryImplTest extends DataJpaTestSupport { private ProductCategory category1; private ProductCategory category2; @Autowired - private AuctionQueryRepository auctionQueryRepository; + private AuctionRepository auctionRepository; @Autowired private EntityManager em; - @Autowired - private AuctionRepository auctionRepository; - @Autowired private ProductCategoryRepository productCategoryRepository; @@ -56,150 +51,6 @@ void setUp() { productCategoryRepository.saveAll(List.of(category1, category2)); } - @DisplayName("[경매 시작 금액에 min값을 설정해 필터링할 수 있다.(minGoe)]") - @Test - void searchAuction_initPrice_min_filter() { - //given - Auction auction1 = AuctionFixture.auction(category1, 2000); - Auction auction2 = AuctionFixture.auction(category2, 5000); - Auction auction3 = AuctionFixture.auction(category2, 10000); - - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword(KEYWORD) - .minPrice(5000) - .build(); - - //when - List auctions = auctionQueryRepository.searchAuctions(condition, pageRequest).getContent(); - - //then - assertAll( - () -> assertThat(auctions).hasSize(2), - () -> assertThat(auctions).containsExactly(auction2, auction3) - ); - } - - @DisplayName("[경매 시작 금액에 max값을 설정해 필터링할 수 있다.(maxLoe)]") - @Test - void searchAuction_initPrice_max_filter() { - //given - Auction auction1 = AuctionFixture.auction(category1, 2000); - Auction auction2 = AuctionFixture.auction(category2, 5000); - Auction auction3 = AuctionFixture.auction(category2, 10000); - - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword(KEYWORD) - .maxPrice(5000) - .build(); - - //when - List auctions = auctionQueryRepository.searchAuctions(condition, pageRequest).getContent(); - - //then - assertAll( - () -> assertThat(auctions).hasSize(2), - () -> assertThat(auctions).containsExactly(auction1, auction2) - ); - } - - @DisplayName("[경매 상품 미개봉 여부로 경매를 필터링할 수 있다. (isNewProductEq)]") - @Test - void searchAuction_isNewProduct_filter() { - //given - Auction auction1 = AuctionFixture.auction(category1, ProductStatus.NEW); - Auction auction2 = AuctionFixture.auction(category2, ProductStatus.DIRTY); - Auction auction3 = AuctionFixture.auction(category2, ProductStatus.CLEAN); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword(KEYWORD) - .isNewProduct(false) - .build(); - - //when - List auctions = auctionQueryRepository.searchAuctions(condition, pageRequest).getContent(); - - //then - assertAll( - () -> assertThat(auctions).hasSize(2), - () -> assertThat(auctions).containsExactly(auction2, auction3) - ); - } - - @DisplayName("[진행 중인 경매만 필터링할 수 있다. (isProgressEq)]") - @Test - void searchAuction_isProgress_filter() { - //given - Auction auction1 = AuctionFixture.auction(category1); - Auction auction2 = AuctionFixture.auction(category2); - Auction auction3 = AuctionFixture.auction(category2); - auction1.updateAuctionStatusTrading(); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword(KEYWORD) - .isProgress(true) - .build(); - - //when - List auctions = auctionQueryRepository.searchAuctions(condition, pageRequest).getContent(); - - //then - assertAll( - () -> assertThat(auctions).hasSize(2), - () -> assertThat(auctions).containsExactly(auction2, auction3) - ); - } - - @DisplayName("[거래 방식으로 경매를 필터링할 수 있다. (tradeMethodEq)]") - @Test - void searchAuction_tradeMethod_filter() { - //given - Auction auction1 = AuctionFixture.auction(category1, TradeMethod.DELIVER); - Auction auction2 = AuctionFixture.auction(category2, TradeMethod.DIRECT); - auctionRepository.saveAll(List.of(auction1, auction2)); - - AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword(KEYWORD) - .tradeMethod("택배") - .build(); - - //when - List auctions = auctionQueryRepository.searchAuctions(condition, pageRequest).getContent(); - - //then - assertAll( - () -> assertThat(auctions).hasSize(1), - () -> assertThat(auctions.get(0)).isEqualTo(auction1) - ); - } - - @DisplayName("[검색 키워드로 필터링할 수 있다. (keywordContains)]") - @Test - void searchAuction_keyword_filter() { - //given - Auction auction1 = AuctionFixture.auction(category1, "버즈팔아요"); - Auction auction2 = AuctionFixture.auction(category1, "버증팔아요"); - Auction auction3 = AuctionFixture.auction(category2, "버즈팔아요"); - auctionRepository.saveAll(List.of(auction1, auction2, auction3)); - - AuctionSearchCondition condition = AuctionSearchCondition.builder() - .keyword(KEYWORD) - .productCategory(DIGITAL_DEVICE) - .build(); - //when - List auctions = auctionQueryRepository.searchAuctions(condition, pageRequest).getContent(); - - //then - assertAll( - () -> assertThat(auctions).hasSize(1), - () -> assertThat(auctions.get(0)).isEqualTo(auction1) - ); - } @DisplayName("[다음 슬라이스에 요소가 있으면 hasNext()=true]") @Test @@ -217,7 +68,7 @@ void searchAuction_hasNext() { PageRequest pageRequest = PageRequest.of(0, 1); //when - Slice auctions = auctionQueryRepository.searchAuctions(condition, pageRequest); + Slice auctions = auctionRepository.searchAuctions(condition, pageRequest); //then assertThat(auctions.hasNext()).isTrue(); @@ -235,7 +86,7 @@ void sortAuctionByCriteria() { auctionRepository.saveAll(List.of(auction1, auction2)); PageRequest pageRequest = PageRequest.of(0, 10, Sort.by("입찰수")); //when - List auctions = auctionQueryRepository.sortAuctionByCriteria(null, null, null, pageRequest) + List auctions = auctionRepository.sortAuctionByCriteria(null, null, null, pageRequest) .getContent(); //then assertThat(auctions).containsExactly(auction2, auction1); @@ -257,7 +108,7 @@ void sortAuctionByCriteria2() { PageRequest pageRequest = PageRequest.of(0, 10, Sort.by("북마크수")); //when - List auctions = auctionQueryRepository.sortAuctionByCriteria(si, gu, dong1, pageRequest) + List auctions = auctionRepository.sortAuctionByCriteria(si, gu, dong1, pageRequest) .getContent(); //then assertThat(auctions).containsExactly(auction2, auction1); @@ -277,7 +128,7 @@ void findByProductCategories() { auctionRepository.saveAll(List.of(auction1, auction2, auction3)); //when - List auctions = auctionQueryRepository.findByProductCategories( + List auctions = auctionRepository.findByProductCategories( List.of(category1, category2), pageRequest).getContent(); //then assertThat(auctions).containsExactly(auction2, auction1); @@ -301,7 +152,7 @@ void updateAuctionStatus() { //when //벌크 업데이트(영속성 컨텍스트 거치지 않음) 후 영속성 컨텍스트 비움 - auctionQueryRepository.updateAuctionStatusAfterEndDate(); + auctionRepository.updateAuctionStatusAfterEndDate(); em.flush(); em.clear(); diff --git a/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java new file mode 100644 index 00000000..1719db3d --- /dev/null +++ b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java @@ -0,0 +1,230 @@ +package dev.handsup.auction.repository.auction; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.test.util.ReflectionTestUtils; + +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.auction.domain.auction_field.TradingLocation; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import dev.handsup.auction.repository.product.ProductCategoryRepository; +import dev.handsup.common.support.DataJpaTestSupport; +import dev.handsup.fixture.AuctionSearchFixture; +import dev.handsup.fixture.ProductFixture; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.search.dto.AuctionSearchCondition; +import jakarta.persistence.EntityManager; + +class AuctionSearchRepositoryTest extends DataJpaTestSupport { + private final String DIGITAL_DEVICE = "디지털 기기"; + private final String APPLIANCE = "가전제품"; + + private final String KEYWORD = "버즈"; + private final PageRequest pageRequest = PageRequest.of(0, 10); + private ProductCategory category1; + private ProductCategory category2; + @Autowired + private AuctionSearchRepository auctionSearchRepository; + + @Autowired + private EntityManager em; + + @Autowired + private ProductCategoryRepository productCategoryRepository; + + @BeforeEach + void setUp() { + category1 = ProductFixture.productCategory(DIGITAL_DEVICE); + category2 = ProductFixture.productCategory(APPLIANCE); + productCategoryRepository.saveAll(List.of(category1, category2)); + } + + @DisplayName("[최근 입찰가로 필터링할 수 있다.]") + @Test + void searchAuction_currentBiddingPrice_min_filter() { + //given + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, 1L, 2000); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, 2L, 5000); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, 3L, 10000); + AuctionSearch auctionSearch4 = AuctionSearchFixture.auctionSearch(4L, 4L, 12000); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3,auctionSearch4)); + + AuctionSearchCondition condition = AuctionSearchCondition.builder() + .keyword(KEYWORD) + .minPrice(5000) + .maxPrice(10000) + .build(); + + //when + List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); + + //then + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch2); + } + + + @DisplayName("[경매 상품 미개봉 여부로 경매를 필터링할 수 있다. (isNewProductEq)]") + @Test + void searchAuction_isNewProduct_filter() { + //given + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, 1L, true); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, 2L, false); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, 3L, true); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + + AuctionSearchCondition condition = AuctionSearchCondition.builder() + .keyword(KEYWORD) + .isNewProduct(true) + .build(); + + //when + List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); + + //then + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch1); + } + + @DisplayName("[진행 중인 경매만 필터링할 수 있다. (isProgressEq)]") + @Test + void searchAuction_isProgress_filter() { + //given + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, 1L); + ReflectionTestUtils.setField(auctionSearch1, "isProgress", false); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, 2L); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, 3L); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + + AuctionSearchCondition condition = AuctionSearchCondition.builder() + .keyword(KEYWORD) + .isProgress(true) + .build(); + + //when + List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); + + //then + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch2); + } + + @DisplayName("[거래 방식으로 경매를 필터링할 수 있다. (tradeMethodEq)]") + @Test + void searchAuction_tradeMethod_filter() { + //given + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, 1L, TradeMethod.DIRECT); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, 2L, TradeMethod.DELIVER); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, 3L, TradeMethod.DELIVER); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + + AuctionSearchCondition condition = AuctionSearchCondition.builder() + .keyword(KEYWORD) + .tradeMethod("직거래") + .build(); + + //when + List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); + + //then + assertThat(auctionSearches).containsExactly(auctionSearch1); + } + + @DisplayName("[검색 키워드로 필터링할 수 있다. (keywordContains)]") + @Test + void searchAuction_keyword_filter() { + //given + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L,1L, KEYWORD+"팔까요?"); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L,2L, "버증팔아요"); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L,3L, KEYWORD+"팔아요"); + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + + AuctionSearchCondition condition = AuctionSearchCondition.builder() + .keyword(KEYWORD) + .build(); + //when + List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); + + //then + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch1); + } + + @DisplayName("[입찰수 순으로 경매를 조회할 수 있다.]") + @Test + void sortAuctionByCriteria_biddingCount() { + //given + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, 1L); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, 2L); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, 3L); + + int biddingCnt = 0; + ReflectionTestUtils.setField(auctionSearch1, "biddingCount", biddingCnt); + ReflectionTestUtils.setField(auctionSearch2, "biddingCount", biddingCnt+1); + ReflectionTestUtils.setField(auctionSearch3, "biddingCount", biddingCnt+2); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2,auctionSearch3)); + + + PageRequest request = PageRequest.of(0, 10, Sort.by("입찰수")); + //when + List auctionSearches = auctionSearchRepository.sortAuctionByCriteria(null, null, null, request) + .getContent(); + //then + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch2, auctionSearch1); + } + + @DisplayName("[특정 지역 필터 + 북마크순으로 경매를 조회할 수 있다.]") + @Test + void sortAuctionByCriteria_bookmarkCount() { + //given + String si = "서울시", gu = "서초구", dong1 = "방배동", dong2 = "반포동"; + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, 1L, TradingLocation.of(si,gu,dong1)); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, 2L,TradingLocation.of(si,gu,dong1)); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, 3L,TradingLocation.of(si,gu,dong2)); + + int bookmarkCnt = 0; + ReflectionTestUtils.setField(auctionSearch1, "bookmarkCount", bookmarkCnt); + ReflectionTestUtils.setField(auctionSearch2, "bookmarkCount", bookmarkCnt+1); + ReflectionTestUtils.setField(auctionSearch3, "bookmarkCount", bookmarkCnt+2); + + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + PageRequest request = PageRequest.of(0, 10, Sort.by("북마크수")); + + //when + List auctionSearches = auctionSearchRepository.sortAuctionByCriteria(si, gu, dong1, request) + .getContent(); + //then + assertThat(auctionSearches).containsExactly(auctionSearch2, auctionSearch1); + } + + @DisplayName("[사용자 선호 카테고리에 속하는 해당하는 경매를 북마크순으로 조회할 수 있다.]") + @Test + void findByProductCategories() { + //given + int bookmarkCnt = 0; + String notPreferredCategory = "스포츠/레저"; + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L, category1.getValue(),1L); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L, category2.getValue(),2L); + AuctionSearch auctionSearch3 = AuctionSearchFixture.auctionSearch(3L, notPreferredCategory,3L); + + ReflectionTestUtils.setField(auctionSearch1, "bookmarkCount", bookmarkCnt+2); + ReflectionTestUtils.setField(auctionSearch2, "bookmarkCount", bookmarkCnt+1); + ReflectionTestUtils.setField(auctionSearch3, "bookmarkCount", bookmarkCnt); + auctionSearchRepository.saveAll(List.of(auctionSearch1, auctionSearch2, auctionSearch3)); + + //when + List auctionSearches = auctionSearchRepository.findByProductCategories( + List.of(category1.getValue(), category2.getValue()), pageRequest).getContent(); + //then + assertThat(auctionSearches).containsExactly(auctionSearch1, auctionSearch2); + } +} diff --git a/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java b/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java index 09504032..cb82bb0b 100644 --- a/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java +++ b/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java @@ -16,52 +16,41 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.SliceImpl; -import org.springframework.data.domain.Sort; import org.springframework.test.util.ReflectionTestUtils; import dev.handsup.auction.domain.Auction; import dev.handsup.auction.domain.auction_field.PurchaseTime; import dev.handsup.auction.domain.auction_field.TradeMethod; import dev.handsup.auction.domain.product.ProductStatus; -import dev.handsup.auction.domain.product.product_category.PreferredProductCategory; import dev.handsup.auction.domain.product.product_category.ProductCategory; import dev.handsup.auction.dto.request.RegisterAuctionRequest; import dev.handsup.auction.dto.response.AuctionDetailResponse; -import dev.handsup.auction.dto.response.RecommendAuctionResponse; import dev.handsup.auction.exception.AuctionErrorCode; -import dev.handsup.auction.repository.auction.AuctionQueryRepository; import dev.handsup.auction.repository.auction.AuctionRepository; -import dev.handsup.auction.repository.product.PreferredProductCategoryRepository; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; import dev.handsup.auction.repository.product.ProductCategoryRepository; -import dev.handsup.common.dto.PageResponse; import dev.handsup.common.exception.NotFoundException; import dev.handsup.fixture.AuctionFixture; import dev.handsup.fixture.ProductFixture; import dev.handsup.fixture.UserFixture; -import dev.handsup.user.domain.User; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.search.dto.AuctionSearchMapper; @DisplayName("[경매 서비스 테스트]") @ExtendWith(MockitoExtension.class) class AuctionServiceTest { - private final User user = UserFixture.user1(); private final String DIGITAL_DEVICE = "디지털 기기"; private final ProductCategory productCategory = ProductFixture.productCategory(DIGITAL_DEVICE); private final Auction auction = AuctionFixture.auction(); - private final PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("북마크수")); @Mock private AuctionRepository auctionRepository; - @Mock - private AuctionQueryRepository auctionQueryRepository; - @Mock private ProductCategoryRepository productCategoryRepository; @Mock - private PreferredProductCategoryRepository preferredProductCategoryRepository; + private AuctionSearchRepository auctionSearchRepository; @InjectMocks private AuctionService auctionService; @@ -90,10 +79,12 @@ void registerAuction() { "성북구", "동선동" ); + AuctionSearch auctionSearch = AuctionSearchMapper.toAuctionSearch(auction); given(productCategoryRepository.findByValue(DIGITAL_DEVICE)) .willReturn(Optional.of(productCategory)); given(auctionRepository.save(any(Auction.class))).willReturn(auction); + given(auctionSearchRepository.save(any(AuctionSearch.class))).willReturn(auctionSearch); // when AuctionDetailResponse response = auctionService.registerAuction(request, UserFixture.user1()); @@ -130,34 +121,4 @@ void getAuctionDetail_fails() { .isInstanceOf(NotFoundException.class) .hasMessageContaining(AuctionErrorCode.NOT_FOUND_AUCTION.getMessage()); } - - @DisplayName("[정렬 조건에 따라 추천 경매를 조회할 수 있다.]") - @Test - void getRecommendAuctions() { - //given - given(auctionQueryRepository.sortAuctionByCriteria(null, null, null, pageRequest)) - .willReturn(new SliceImpl<>(List.of(auction), pageRequest, false)); - //when - PageResponse response - = auctionService.getRecommendAuctions(null, null, null, pageRequest); - //then - assertThat(response.content().get(0)).isNotNull(); - } - - @DisplayName("[유저 선호 카테고리에 맞는 경매를 북마크 순으로 조회할 수 있다.]") - @Test - void getUserPreferredCategoryAuctions() { - //given - PreferredProductCategory preferredProductCategory = PreferredProductCategory.of(user, productCategory); - given(preferredProductCategoryRepository.findByUser(user)) - .willReturn(List.of(preferredProductCategory)); - given(auctionQueryRepository.findByProductCategories(List.of(productCategory), pageRequest)) - .willReturn(new SliceImpl<>(List.of(auction), pageRequest, false)); - //when - PageResponse response = auctionService.getUserPreferredCategoryAuctions( - user, pageRequest); - - //then - assertThat(response.content().get(0)).isNotNull(); - } } diff --git a/core/src/test/java/dev/handsup/recommend/service/RecommendServiceTest.java b/core/src/test/java/dev/handsup/recommend/service/RecommendServiceTest.java new file mode 100644 index 00000000..94372a40 --- /dev/null +++ b/core/src/test/java/dev/handsup/recommend/service/RecommendServiceTest.java @@ -0,0 +1,91 @@ +package dev.handsup.recommend.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort; + +import dev.handsup.auction.domain.auction_field.TradingLocation; +import dev.handsup.auction.domain.product.product_category.PreferredProductCategory; +import dev.handsup.auction.domain.product.product_category.ProductCategory; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; +import dev.handsup.auction.repository.product.PreferredProductCategoryRepository; +import dev.handsup.common.dto.PageResponse; +import dev.handsup.fixture.AuctionSearchFixture; +import dev.handsup.fixture.ProductFixture; +import dev.handsup.fixture.UserFixture; +import dev.handsup.recommend.dto.RecommendAuctionResponse; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.user.domain.User; + +@DisplayName("[추천 서비스 테스트]") +@ExtendWith(MockitoExtension.class) +class RecommendServiceTest { + private final PageRequest pageRequest = PageRequest.of(0, 5, Sort.by("북마크수")); + private final User user = UserFixture.user1(); + private final String DIGITAL_DEVICE = "디지털 기기"; + private final String FASHION = "패션/잡화"; + private final ProductCategory productCategory1 = ProductFixture.productCategory(DIGITAL_DEVICE); + private final ProductCategory productCategory2 = ProductFixture.productCategory(FASHION); + + @Mock + private PreferredProductCategoryRepository preferredProductCategoryRepository; + + @Mock + private AuctionSearchRepository auctionSearchRepository; + + @InjectMocks + private RecommendService recommendService; + + @DisplayName("[정렬 조건에 따라 추천 경매를 조회할 수 있다.]") + @Test + void getRecommendAuctions() { + //given + String si = "서울시", gu = "성북구", dong = "동선동"; + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L,1L,TradingLocation.of(si,gu,dong)); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(3L,3L,TradingLocation.of(si,gu,dong)); + + given(auctionSearchRepository.sortAuctionByCriteria(si, gu, dong, pageRequest)) + .willReturn(new SliceImpl<>(List.of(auctionSearch1,auctionSearch2), pageRequest, false)); + //when + PageResponse response + = recommendService.getRecommendAuctionsV2(si, gu, dong, pageRequest); + //then + assertThat(response.content()).hasSize(2); + } + + @DisplayName("[유저 선호 카테고리에 맞는 경매를 북마크 순으로 조회할 수 있다.]") + @Test + void getUserPreferredCategoryAuctions() { + //given + AuctionSearch auctionSearch1 = AuctionSearchFixture.auctionSearch(1L,DIGITAL_DEVICE,1L); + AuctionSearch auctionSearch2 = AuctionSearchFixture.auctionSearch(2L,FASHION,2L); + + + PreferredProductCategory preferredProductCategory1 = PreferredProductCategory.of(user, productCategory1); + PreferredProductCategory preferredProductCategory2 = PreferredProductCategory.of(user, productCategory2); + + given(preferredProductCategoryRepository.findByUser(user)) + .willReturn(List.of(preferredProductCategory1, preferredProductCategory2)); + given(auctionSearchRepository.findByProductCategories(List.of(productCategory1.getValue(),productCategory2.getValue()), pageRequest)) + .willReturn(new SliceImpl<>(List.of(auctionSearch1,auctionSearch2), pageRequest, false)); + + //when + PageResponse response = recommendService.getUserPreferredCategoryAuctionsV2( + user, pageRequest); + + //then + assertThat(response.content()).hasSize(2); + } +} + diff --git a/core/src/test/java/dev/handsup/search/service/SearchServiceTest.java b/core/src/test/java/dev/handsup/search/service/SearchServiceTest.java index 788d03e0..05eea438 100644 --- a/core/src/test/java/dev/handsup/search/service/SearchServiceTest.java +++ b/core/src/test/java/dev/handsup/search/service/SearchServiceTest.java @@ -4,7 +4,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; -import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -15,27 +14,25 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.SliceImpl; -import org.springframework.test.util.ReflectionTestUtils; -import dev.handsup.auction.domain.Auction; -import dev.handsup.auction.dto.request.AuctionSearchCondition; -import dev.handsup.auction.dto.response.AuctionSimpleResponse; -import dev.handsup.auction.repository.auction.AuctionQueryRepository; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; import dev.handsup.auction.repository.search.RedisSearchRepository; import dev.handsup.common.dto.PageResponse; -import dev.handsup.fixture.AuctionFixture; +import dev.handsup.fixture.AuctionSearchFixture; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.search.dto.AuctionSearchCondition; +import dev.handsup.search.dto.AuctionSearchResponse; import dev.handsup.search.dto.PopularKeywordResponse; import dev.handsup.search.dto.PopularKeywordsResponse; @DisplayName("[검색 service 테스트]") @ExtendWith(MockitoExtension.class) class SearchServiceTest { - private final Auction auction = AuctionFixture.auction(); private final int PAGE_NUMBER = 0; private final int PAGE_SIZE = 5; @Mock - private AuctionQueryRepository auctionQueryRepository; + private AuctionSearchRepository auctionSearchRepository; @Mock private RedisSearchRepository redisSearchRepository; @@ -47,23 +44,23 @@ class SearchServiceTest { @Test void searchAuctions() { //given - ReflectionTestUtils.setField(auction, "createdAt", LocalDateTime.now()); + AuctionSearch auctionSearch = AuctionSearchFixture.auctionSearch(1L,1L); PageRequest pageRequest = PageRequest.of(PAGE_NUMBER, PAGE_SIZE); AuctionSearchCondition condition = AuctionSearchCondition.builder() .keyword("버즈") .build(); - given(auctionQueryRepository.searchAuctions(condition, pageRequest)) - .willReturn(new SliceImpl<>(List.of(auction), pageRequest, false)); + given(auctionSearchRepository.searchAuctions(condition, pageRequest)) + .willReturn(new SliceImpl<>(List.of(auctionSearch), pageRequest, false)); //when - PageResponse response - = searchService.searchAuctions(condition, pageRequest); + PageResponse response + = searchService.searchAuctionsV2(condition, pageRequest); //then - AuctionSimpleResponse auctionSimpleResponse = response.content().get(0); - assertThat(auctionSimpleResponse).isNotNull(); + AuctionSearchResponse auctionSearchResponse = response.content().get(0); + assertThat(auctionSearchResponse).isNotNull(); verify(redisSearchRepository).increaseSearchCount(condition.keyword()); } diff --git a/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java b/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java new file mode 100644 index 00000000..86637012 --- /dev/null +++ b/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java @@ -0,0 +1,147 @@ +package dev.handsup.fixture; + +import static lombok.AccessLevel.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import org.springframework.test.util.ReflectionTestUtils; + +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.auction.domain.auction_field.TradingLocation; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = PRIVATE) +public class AuctionSearchFixture { + static final String TITLE = "거의 새상품 버즈 팔아요"; + static final LocalDate END_DATE = LocalDate.parse("2022-10-18"); + static final String DIGITAL_DEVICE = "디지털 기기"; + static final String SI = "서울시"; + static final String GU = "성북구"; + static final String DONG = "동선동"; + static final String IMAGE_URL = "image.jpg"; + + public static AuctionSearch auctionSearch(Long auctionId, Long productId, int currentBiddingPrice) { + AuctionSearch auctionSearch = AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(DIGITAL_DEVICE) + .isNewProduct(false) + .title(TITLE) + .imgUrl(IMAGE_URL) + .endDate(END_DATE) + .tradingLocation(TradingLocation.of(SI, GU, DONG)) + .tradeMethod(TradeMethod.DIRECT) + .createdAt(LocalDateTime.now()) + .build(); + ReflectionTestUtils.setField(auctionSearch, "currentBiddingPrice", currentBiddingPrice); + return auctionSearch; + } + + public static AuctionSearch auctionSearch(Long auctionId, Long productId,TradingLocation tradingLocation, LocalDate endDate) { + return AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(DIGITAL_DEVICE) + .isNewProduct(true) + .title(TITLE) + .imgUrl(IMAGE_URL) + .endDate(endDate) + .tradingLocation(tradingLocation) + .tradeMethod(TradeMethod.DIRECT) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static AuctionSearch auctionSearch(Long auctionId, String category, Long productId) { + return AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(category) + .isNewProduct(false) + .title(TITLE) + .imgUrl(IMAGE_URL) + .endDate(END_DATE) + .tradingLocation(TradingLocation.of(SI, GU, DONG)) + .tradeMethod(TradeMethod.DIRECT) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static AuctionSearch auctionSearch(Long auctionId, Long productId, TradeMethod tradeMethod) { + return AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(DIGITAL_DEVICE) + .isNewProduct(false) + .title(TITLE) + .imgUrl(IMAGE_URL) + .endDate(END_DATE) + .tradingLocation(TradingLocation.of(SI, GU, DONG)) + .tradeMethod(tradeMethod) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static AuctionSearch auctionSearch(Long auctionId, Long productId, String keyword) { + return AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(DIGITAL_DEVICE) + .isNewProduct(false) + .title(keyword) + .imgUrl(IMAGE_URL) + .endDate(END_DATE) + .tradingLocation(TradingLocation.of(SI, GU, DONG)) + .tradeMethod(TradeMethod.DIRECT) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static AuctionSearch auctionSearch(Long auctionId, Long productId) { + return AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(DIGITAL_DEVICE) + .isNewProduct(true) + .title(TITLE) + .imgUrl(IMAGE_URL) + .endDate(END_DATE) + .tradingLocation(TradingLocation.of(SI, GU, DONG)) + .tradeMethod(TradeMethod.DIRECT) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static AuctionSearch auctionSearch(Long auctionId, Long productId,TradingLocation tradingLocation) { + return AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(DIGITAL_DEVICE) + .isNewProduct(true) + .title(TITLE) + .imgUrl(IMAGE_URL) + .endDate(END_DATE) + .tradingLocation(tradingLocation) + .tradeMethod(TradeMethod.DIRECT) + .createdAt(LocalDateTime.now()) + .build(); + } + + + public static AuctionSearch auctionSearch(Long auctionId, Long productId, boolean isNewProduct) { + return AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(DIGITAL_DEVICE) + .isNewProduct(isNewProduct) + .title(TITLE) + .imgUrl(IMAGE_URL) + .endDate(END_DATE) + .tradingLocation(TradingLocation.of(SI, GU, DONG)) + .tradeMethod(TradeMethod.DIRECT) + .createdAt(LocalDateTime.now()) + .build(); + } +}