Skip to content

Commit 11c56a6

Browse files
authored
Feat/#174/auction search optimization (#175)
* [feat] : 최근 입찰가로 가격 필터링 * [test] : 최근 입찰가로 가격 필터링 테스트 반영 * [chore] : sql 파일 저장 * [chore] : 경매, 경매 검색 sql 저장 * [feat] : 경매 검색 테이블 추가 * [feat] : 경매 저장 시 경매 검색 같이 저장 * [feat] : 스케줄러로 통계 필드 업데이트 * [feat] : 경매 검색 동적 쿼리 추가 * [test] : 경매 검색 동적 쿼리 테스트 * [feat] : 경매 검색 성능 개선 API 추가 * [chore] : API 테스트 파일 추가 * [feat] : 경매 검색 테이블 동적 쿼리 작성 * [test] : 경매 검색 테이블 동적 쿼리 테스트 * [feat] : 경매 추천 성능 개선 API 추 * [chore] : 경매 추천 성능 개선 API 테스트 * [feat] : 경매 카테고리 추천 동적 쿼리 작 * [test] : 경매 카테고리 추천 동적 쿼리 테스트 * [feat] : 경매 카테고리 추천 성능 개선 API 추가 * [test] : 경매 카테고리 추천 성능 개선 API 테스트 * [refactor] : 정렬 기능 통 * [test] : 경매 등록 시 경매 검색 등록 로직 반영 * [feat] : 경매, 경매 검색 패키지 분 * [refactor] : 불필요한 외부 조인 삭제 * [chore] : sql, 요청 파일 수정
1 parent 76ff47a commit 11c56a6

42 files changed

Lines changed: 1715 additions & 499 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
package dev.handsup.auction.controller;
22

3-
import org.springframework.cache.annotation.Cacheable;
4-
import org.springframework.data.domain.Pageable;
53
import org.springframework.http.ResponseEntity;
64
import org.springframework.web.bind.annotation.GetMapping;
75
import org.springframework.web.bind.annotation.PathVariable;
86
import org.springframework.web.bind.annotation.PostMapping;
97
import org.springframework.web.bind.annotation.RequestBody;
108
import org.springframework.web.bind.annotation.RequestMapping;
11-
import org.springframework.web.bind.annotation.RequestParam;
129
import org.springframework.web.bind.annotation.RestController;
1310

1411
import dev.handsup.auction.dto.request.RegisterAuctionRequest;
1512
import dev.handsup.auction.dto.response.AuctionDetailResponse;
16-
import dev.handsup.auction.dto.response.RecommendAuctionResponse;
1713
import dev.handsup.auction.service.AuctionService;
1814
import dev.handsup.auth.annotation.NoAuth;
1915
import dev.handsup.auth.jwt.JwtAuthorization;
20-
import dev.handsup.common.dto.PageResponse;
2116
import dev.handsup.user.domain.User;
2217
import io.swagger.v3.oas.annotations.Operation;
2318
import io.swagger.v3.oas.annotations.Parameter;
@@ -54,32 +49,4 @@ public ResponseEntity<AuctionDetailResponse> getAuctionDetail(@PathVariable("auc
5449
AuctionDetailResponse response = auctionService.getAuctionDetail(auctionId);
5550
return ResponseEntity.ok(response);
5651
}
57-
58-
@NoAuth
59-
@Operation(summary = "경매 추천 API", description = "정렬 조건에 따라 경매를 추천한다.")
60-
@ApiResponse(useReturnTypeSchema = true)
61-
@GetMapping("/recommend")
62-
@Cacheable(cacheNames = "auctions")
63-
public ResponseEntity<PageResponse<RecommendAuctionResponse>> getRecommendAuctions(
64-
@RequestParam(value = "si", required = false) String si,
65-
@RequestParam(value = "gu", required = false) String gu,
66-
@RequestParam(value = "dong", required = false) String dong,
67-
Pageable pageable
68-
) {
69-
PageResponse<RecommendAuctionResponse> response = auctionService.getRecommendAuctions(si, gu, dong, pageable);
70-
return ResponseEntity.ok(response);
71-
}
72-
73-
@Operation(summary = "유저 선호 카테고리 경매 조회 API", description = "유저가 선호하는 카테고리의 경매를 조회한다.")
74-
@ApiResponse(useReturnTypeSchema = true)
75-
@GetMapping("/recommend/category")
76-
public ResponseEntity<PageResponse<RecommendAuctionResponse>> getUserPreferredCategoryAuctions(
77-
@Parameter(hidden = true) @JwtAuthorization User user,
78-
Pageable pageable
79-
) {
80-
PageResponse<RecommendAuctionResponse> response = auctionService.getUserPreferredCategoryAuctions(user,
81-
pageable);
82-
return ResponseEntity.ok(response);
83-
}
84-
8552
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package dev.handsup.recommend.controller;
2+
3+
import org.springframework.cache.annotation.Cacheable;
4+
import org.springframework.data.domain.Pageable;
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
import dev.handsup.auth.annotation.NoAuth;
11+
import dev.handsup.auth.jwt.JwtAuthorization;
12+
import dev.handsup.common.dto.PageResponse;
13+
import dev.handsup.recommend.dto.RecommendAuctionResponse;
14+
import dev.handsup.recommend.service.RecommendService;
15+
import dev.handsup.user.domain.User;
16+
import io.swagger.v3.oas.annotations.Operation;
17+
import io.swagger.v3.oas.annotations.Parameter;
18+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
19+
import io.swagger.v3.oas.annotations.tags.Tag;
20+
import lombok.RequiredArgsConstructor;
21+
22+
@Tag(name = "홈 추천 API")
23+
@RestController
24+
@RequiredArgsConstructor
25+
public class RecommendApiController {
26+
private final RecommendService recommendService;
27+
28+
@NoAuth
29+
@Operation(summary = "경매 추천 API", description = "정렬 조건에 따라 경매를 추천한다.")
30+
@ApiResponse(useReturnTypeSchema = true)
31+
@GetMapping("/api/auctions/recommend")
32+
@Cacheable(cacheNames = "auctions")
33+
public ResponseEntity<PageResponse<RecommendAuctionResponse>> getRecommendAuctions(
34+
@RequestParam(value = "si", required = false) String si,
35+
@RequestParam(value = "gu", required = false) String gu,
36+
@RequestParam(value = "dong", required = false) String dong,
37+
Pageable pageable
38+
) {
39+
PageResponse<RecommendAuctionResponse> response = recommendService.getRecommendAuctions(si, gu, dong, pageable);
40+
return ResponseEntity.ok(response);
41+
}
42+
43+
@NoAuth
44+
@Operation(summary = "성능 개선된 경매 추천 API", description = "정렬 조건에 따라 경매를 추천한다.")
45+
@ApiResponse(useReturnTypeSchema = true)
46+
@GetMapping("/api/v2/auctions/recommend")
47+
@Cacheable(cacheNames = "auctions")
48+
public ResponseEntity<PageResponse<RecommendAuctionResponse>> getRecommendAuctionsV2(
49+
@RequestParam(value = "si", required = false) String si,
50+
@RequestParam(value = "gu", required = false) String gu,
51+
@RequestParam(value = "dong", required = false) String dong,
52+
Pageable pageable
53+
) {
54+
PageResponse<RecommendAuctionResponse> response = recommendService.getRecommendAuctionsV2(si, gu, dong, pageable);
55+
return ResponseEntity.ok(response);
56+
}
57+
58+
@Operation(summary = "유저 선호 카테고리 경매 조회 API", description = "유저가 선호하는 카테고리의 경매를 조회한다.")
59+
@ApiResponse(useReturnTypeSchema = true)
60+
@GetMapping("/api/auctions/recommend/category")
61+
public ResponseEntity<PageResponse<RecommendAuctionResponse>> getUserPreferredCategoryAuctions(
62+
@Parameter(hidden = true) @JwtAuthorization User user,
63+
Pageable pageable
64+
) {
65+
PageResponse<RecommendAuctionResponse> response = recommendService.getUserPreferredCategoryAuctions(user,
66+
pageable);
67+
return ResponseEntity.ok(response);
68+
}
69+
70+
@Operation(summary = "성능 개선된 유저 선호 카테고리 경매 조회 API", description = "유저가 선호하는 카테고리의 경매를 조회한다.")
71+
@ApiResponse(useReturnTypeSchema = true)
72+
@GetMapping("/api/v2/auctions/recommend/category")
73+
public ResponseEntity<PageResponse<RecommendAuctionResponse>> getUserPreferredCategoryAuctionsV2(
74+
@Parameter(hidden = true) @JwtAuthorization User user,
75+
Pageable pageable
76+
) {
77+
PageResponse<RecommendAuctionResponse> response = recommendService.getUserPreferredCategoryAuctionsV2(user,
78+
pageable);
79+
return ResponseEntity.ok(response);
80+
}
81+
}
82+

api/src/main/java/dev/handsup/search/controller/SearchApiController.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@
55
import org.springframework.web.bind.annotation.GetMapping;
66
import org.springframework.web.bind.annotation.PostMapping;
77
import org.springframework.web.bind.annotation.RequestBody;
8-
import org.springframework.web.bind.annotation.RequestMapping;
98
import org.springframework.web.bind.annotation.RestController;
109

11-
import dev.handsup.auction.dto.request.AuctionSearchCondition;
12-
import dev.handsup.auction.dto.response.AuctionSimpleResponse;
1310
import dev.handsup.auth.annotation.NoAuth;
1411
import dev.handsup.common.dto.PageResponse;
12+
import dev.handsup.search.dto.AuctionSearchCondition;
13+
import dev.handsup.search.dto.AuctionSearchResponse;
1514
import dev.handsup.search.dto.PopularKeywordsResponse;
1615
import dev.handsup.search.service.SearchService;
1716
import io.swagger.v3.oas.annotations.Operation;
@@ -23,25 +22,35 @@
2322
@Tag(name = "검색 API")
2423
@RestController
2524
@RequiredArgsConstructor
26-
@RequestMapping("/api/auctions/search")
2725
public class SearchApiController {
2826
private final SearchService searchService;
2927

3028
@NoAuth
3129
@Operation(summary = "경매 검색 API", description = "경매를 검색한다")
3230
@ApiResponse(useReturnTypeSchema = true)
33-
@PostMapping
34-
public ResponseEntity<PageResponse<AuctionSimpleResponse>> searchAuctions(
31+
@PostMapping("/api/auctions/search")
32+
public ResponseEntity<PageResponse<AuctionSearchResponse>> searchAuctions(
3533
@Valid @RequestBody AuctionSearchCondition condition,
3634
Pageable pageable) {
37-
PageResponse<AuctionSimpleResponse> response = searchService.searchAuctions(condition, pageable);
35+
PageResponse<AuctionSearchResponse> response = searchService.searchAuctions(condition, pageable);
36+
return ResponseEntity.ok(response);
37+
}
38+
39+
@NoAuth
40+
@Operation(summary = "최적화된 경매 검색 API", description = "경매를 검색한다")
41+
@ApiResponse(useReturnTypeSchema = true)
42+
@PostMapping("/api/v2/auctions/search")
43+
public ResponseEntity<PageResponse<AuctionSearchResponse>> searchAuctionsV2(
44+
@Valid @RequestBody AuctionSearchCondition condition,
45+
Pageable pageable) {
46+
PageResponse<AuctionSearchResponse> response = searchService.searchAuctionsV2(condition, pageable);
3847
return ResponseEntity.ok(response);
3948
}
4049

4150
@NoAuth
4251
@Operation(summary = "인기 검색어 조회 API", description = "인기 검색어를 조회한다.")
4352
@ApiResponse(useReturnTypeSchema = true)
44-
@GetMapping("/popular")
53+
@GetMapping("/api/auctions/search/popular")
4554
public ResponseEntity<PopularKeywordsResponse> getPopularKeywords() {
4655
PopularKeywordsResponse response = searchService.getPopularKeywords();
4756
return ResponseEntity.ok(response);

api/src/test/java/dev/handsup/auction/controller/AuctionApiControllerTest.java

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,17 @@
1212
import org.junit.jupiter.api.DisplayName;
1313
import org.junit.jupiter.api.Test;
1414
import org.springframework.beans.factory.annotation.Autowired;
15-
import org.springframework.test.util.ReflectionTestUtils;
16-
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
1715

1816
import dev.handsup.auction.domain.Auction;
1917
import dev.handsup.auction.domain.auction_field.PurchaseTime;
2018
import dev.handsup.auction.domain.auction_field.TradeMethod;
2119
import dev.handsup.auction.domain.auction_field.TradingLocation;
2220
import dev.handsup.auction.domain.product.Product;
2321
import dev.handsup.auction.domain.product.ProductStatus;
24-
import dev.handsup.auction.domain.product.product_category.PreferredProductCategory;
2522
import dev.handsup.auction.domain.product.product_category.ProductCategory;
2623
import dev.handsup.auction.dto.request.RegisterAuctionRequest;
2724
import dev.handsup.auction.exception.AuctionErrorCode;
2825
import dev.handsup.auction.repository.auction.AuctionRepository;
29-
import dev.handsup.auction.repository.product.PreferredProductCategoryRepository;
3026
import dev.handsup.auction.repository.product.ProductCategoryRepository;
3127
import dev.handsup.common.support.ApiTestSupport;
3228
import dev.handsup.fixture.AuctionFixture;
@@ -42,9 +38,6 @@ class AuctionApiControllerTest extends ApiTestSupport {
4238
@Autowired
4339
private ProductCategoryRepository productCategoryRepository;
4440

45-
@Autowired
46-
private PreferredProductCategoryRepository preferredProductCategoryRepository;
47-
4841
@BeforeEach
4942
void setUp() {
5043
productCategory = ProductFixture.productCategory(DIGITAL_DEVICE);
@@ -128,96 +121,4 @@ void getAuctionDetail() throws Exception {
128121
.andExpect(jsonPath("$.tradeDong").value(tradingLocation.getDong()))
129122
.andExpect(jsonPath("$.bookmarkCount").value(auction.getBookmarkCount()));
130123
}
131-
132-
@DisplayName("[정렬 조건과 지역 필터에 따라 경매글 목록을 반환한다.]")
133-
@Test
134-
void getRecommendAuctionsWithFilter() throws Exception {
135-
//given
136-
String si = "서울시", gu = "서초구", dong1 = "방배동", dong2 = "반포동";
137-
String earlyEndDate = "2024-03-02", lateEndDate = "2024-03-10";
138-
Auction auction1 = AuctionFixture.auction(productCategory, lateEndDate, si, gu, dong1);
139-
Auction auction2 = AuctionFixture.auction(productCategory, earlyEndDate, si, gu, dong1);
140-
Auction auction3 = AuctionFixture.auction(productCategory, lateEndDate, si, gu, dong2);
141-
auctionRepository.saveAll(List.of(auction1, auction2, auction3));
142-
143-
//when
144-
mockMvc.perform(get("/api/auctions/recommend").param("sort", "마감일")
145-
.param("si", si)
146-
.param("gu", gu)
147-
.param("dong", dong1)
148-
.contentType(APPLICATION_JSON))
149-
.andExpect(status().isOk())
150-
.andExpect(jsonPath("$.size").value(2))
151-
.andExpect(jsonPath("$.content[0].auctionId").value(auction2.getId()))
152-
.andExpect(jsonPath("$.content[0].endDate").value(auction2.getEndDate().atStartOfDay().toString()))
153-
.andExpect(jsonPath("$.content[1].auctionId").value(auction1.getId()))
154-
.andExpect(jsonPath("$.content[1].endDate").value(auction1.getEndDate().atStartOfDay().toString()))
155-
.andExpect(jsonPath("$.hasNext").value(false));
156-
}
157-
158-
@DisplayName("[정렬 조건에 따라 경매글 목록을 반환한다.]")
159-
@Test
160-
void getRecommendAuctionsWithOutFilter() throws Exception {
161-
//given
162-
Auction auction1 = AuctionFixture.auction(productCategory);
163-
Auction auction2 = AuctionFixture.auction(productCategory);
164-
Auction auction3 = AuctionFixture.auction(productCategory);
165-
auctionRepository.saveAll(List.of(auction1, auction2, auction3));
166-
167-
//when
168-
mockMvc.perform(get("/api/auctions/recommend").param("sort", "최근생성").contentType(APPLICATION_JSON))
169-
.andExpect(status().isOk())
170-
.andExpect(jsonPath("$.size").value(3))
171-
.andExpect(jsonPath("$.content[0].auctionId").value(auction3.getId()))
172-
.andExpect(jsonPath("$.content[1].auctionId").value(auction2.getId()))
173-
.andExpect(jsonPath("$.content[2].auctionId").value(auction1.getId()))
174-
.andExpect(jsonPath("$.hasNext").value(false));
175-
}
176-
177-
@DisplayName("[정렬 조건이 없을 시 예외를 반환한다.]")
178-
@Test
179-
void getRecommendAuctions_fails() throws Exception {
180-
mockMvc.perform(get("/api/auctions/recommend").contentType(APPLICATION_JSON))
181-
.andDo(MockMvcResultHandlers.print())
182-
.andExpect(jsonPath("$.message").value(AuctionErrorCode.EMPTY_SORT_INPUT.getMessage()))
183-
.andExpect(jsonPath("$.code").value(AuctionErrorCode.EMPTY_SORT_INPUT.getCode()));
184-
}
185-
186-
@DisplayName("[정렬 조건이 잘못되면 예외를 반환한다.]")
187-
@Test
188-
void getRecommendAuctions_fails2() throws Exception {
189-
mockMvc.perform(get("/api/auctions/recommend").contentType(APPLICATION_JSON).param("sort", "NAN"))
190-
.andDo(MockMvcResultHandlers.print())
191-
.andExpect(jsonPath("$.message").value(AuctionErrorCode.INVALID_SORT_INPUT.getMessage()))
192-
.andExpect(jsonPath("$.code").value(AuctionErrorCode.INVALID_SORT_INPUT.getCode()));
193-
}
194-
195-
@DisplayName("[유저 선호 카테고리 경매를 북마크 순으로 정렬한다.]")
196-
@Test
197-
void getUserPreferredCategoryAuctions() throws Exception {
198-
ProductCategory productCategory2 = productCategoryRepository.save(ProductCategory.from("생활/주방"));
199-
ProductCategory notPreferredProductCategory = productCategoryRepository.save(ProductCategory.from("티켓/교환권"));
200-
201-
preferredProductCategoryRepository.saveAll(List.of(
202-
PreferredProductCategory.of(user, productCategory),
203-
PreferredProductCategory.of(user, productCategory2)
204-
));
205-
Auction auction1 = AuctionFixture.auction(productCategory);
206-
ReflectionTestUtils.setField(auction1, "bookmarkCount", 3);
207-
Auction auction2 = AuctionFixture.auction(productCategory2);
208-
ReflectionTestUtils.setField(auction2, "bookmarkCount", 5);
209-
210-
Auction auction3 = AuctionFixture.auction(notPreferredProductCategory);
211-
auctionRepository.saveAll(List.of(auction1, auction2, auction3));
212-
213-
//when
214-
mockMvc.perform(get("/api/auctions/recommend/category")
215-
.header(AUTHORIZATION, "Bearer " + accessToken)
216-
.contentType(APPLICATION_JSON))
217-
.andExpect(status().isOk())
218-
.andExpect(jsonPath("$.size").value(2))
219-
.andExpect(jsonPath("$.content[0].auctionId").value(auction2.getId()))
220-
.andExpect(jsonPath("$.content[1].auctionId").value(auction1.getId()))
221-
.andExpect(jsonPath("$.hasNext").value(false));
222-
}
223124
}

0 commit comments

Comments
 (0)