From afe4bb4ca5aa8d48ea3abd888e26ee3074747ea9 Mon Sep 17 00:00:00 2001 From: hs12 Date: Sun, 15 Jun 2025 18:59:52 +0900 Subject: [PATCH 01/24] =?UTF-8?q?[feat]=20:=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EC=9E=85=EC=B0=B0=EA=B0=80=EB=A1=9C=20=EA=B0=80=EA=B2=A9=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/auction/AuctionQueryRepositoryImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..a6070e26 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 @@ -164,11 +164,11 @@ 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) { From 96d91d311dc2bb5ae0e8c92d38af4f62a5c2d633 Mon Sep 17 00:00:00 2001 From: hs12 Date: Sun, 15 Jun 2025 19:00:19 +0900 Subject: [PATCH 02/24] =?UTF-8?q?[test]=20:=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EC=9E=85=EC=B0=B0=EA=B0=80=EB=A1=9C=20=EA=B0=80=EA=B2=A9=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/auction/AuctionQueryRepositoryImplTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImplTest.java b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImplTest.java index 42ca9a5c..46984359 100644 --- a/core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImplTest.java +++ b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionQueryRepositoryImplTest.java @@ -56,9 +56,9 @@ void setUp() { productCategoryRepository.saveAll(List.of(category1, category2)); } - @DisplayName("[경매 시작 금액에 min값을 설정해 필터링할 수 있다.(minGoe)]") + @DisplayName("[최근 입찰가에 대한 하한 조건을 걸 수 있다.]") @Test - void searchAuction_initPrice_min_filter() { + void searchAuction_currentBiddingPrice_min_filter() { //given Auction auction1 = AuctionFixture.auction(category1, 2000); Auction auction2 = AuctionFixture.auction(category2, 5000); @@ -83,7 +83,7 @@ void searchAuction_initPrice_min_filter() { @DisplayName("[경매 시작 금액에 max값을 설정해 필터링할 수 있다.(maxLoe)]") @Test - void searchAuction_initPrice_max_filter() { + void searchAuction_currentBiddingPrice_max_filter() { //given Auction auction1 = AuctionFixture.auction(category1, 2000); Auction auction2 = AuctionFixture.auction(category2, 5000); From 99c958d69c7651d9410a9adc10eeba74ad6cfef3 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 19:48:23 +0900 Subject: [PATCH 03/24] =?UTF-8?q?[chore]=20:=20sql=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/resources/db/bidding.sql | 0 core/src/main/resources/db/bookmark.sql | 16 ++++++++ core/src/main/resources/db/init.sql | 22 +++++++++++ core/src/main/resources/db/product.sql | 39 ++++++++++++++++++++ core/src/main/resources/db/product_image.sql | 4 ++ core/src/main/resources/db/user.sql | 12 ++++++ 6 files changed, 93 insertions(+) create mode 100644 core/src/main/resources/db/bidding.sql create mode 100644 core/src/main/resources/db/bookmark.sql create mode 100644 core/src/main/resources/db/init.sql create mode 100644 core/src/main/resources/db/product.sql create mode 100644 core/src/main/resources/db/product_image.sql create mode 100644 core/src/main/resources/db/user.sql 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..b79f0e13 --- /dev/null +++ b/core/src/main/resources/db/product.sql @@ -0,0 +1,39 @@ +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; 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 From 75c290022a7c925506a95ca6aeec33d78810efa3 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 19:49:04 +0900 Subject: [PATCH 04/24] =?UTF-8?q?[chore]=20:=20=EA=B2=BD=EB=A7=A4,=20?= =?UTF-8?q?=EA=B2=BD=EB=A7=A4=20=EA=B2=80=EC=83=89=20sql=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/resources/db/auction.sql | 54 +++++++++++++++++++ core/src/main/resources/db/auction_search.sql | 44 +++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 core/src/main/resources/db/auction.sql create mode 100644 core/src/main/resources/db/auction_search.sql 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..558fb06e --- /dev/null +++ b/core/src/main/resources/db/auction_search.sql @@ -0,0 +1,44 @@ +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; \ No newline at end of file From 30d3eea898afadee9833bfe6d07bc0c4921de681 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 19:52:38 +0900 Subject: [PATCH 05/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handsup/auction/domain/AuctionSearch.java | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 core/src/main/java/dev/handsup/auction/domain/AuctionSearch.java diff --git a/core/src/main/java/dev/handsup/auction/domain/AuctionSearch.java b/core/src/main/java/dev/handsup/auction/domain/AuctionSearch.java new file mode 100644 index 00000000..25dbb94e --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/domain/AuctionSearch.java @@ -0,0 +1,92 @@ +package dev.handsup.auction.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; + } +} + From b07f82d9d6bf0221a7e023ad9f9b8c3f043492e4 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 19:55:54 +0900 Subject: [PATCH 06/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=8B=9C=20=EA=B2=BD=EB=A7=A4=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EA=B0=99=EC=9D=B4=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/mapper/AuctionSearchMapper.java | 44 +++++++++++++++++++ .../auction/AuctionSearchRepository.java | 32 ++++++++++++++ .../auction/service/AuctionService.java | 9 +++- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java create mode 100644 core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchRepository.java diff --git a/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java b/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java new file mode 100644 index 00000000..f00f411a --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java @@ -0,0 +1,44 @@ +package dev.handsup.auction.dto.mapper; + +import static lombok.AccessLevel.*; + +import dev.handsup.auction.domain.Auction; +import dev.handsup.auction.domain.AuctionSearch; +import dev.handsup.auction.domain.auction_field.AuctionStatus; +import dev.handsup.auction.domain.product.Product; +import dev.handsup.auction.dto.response.AuctionSearchResponse; +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() + ); + } +} + + 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..a28c811d --- /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.auction.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/service/AuctionService.java b/core/src/main/java/dev/handsup/auction/service/AuctionService.java index 31db6aef..9db3e279 100644 --- a/core/src/main/java/dev/handsup/auction/service/AuctionService.java +++ b/core/src/main/java/dev/handsup/auction/service/AuctionService.java @@ -11,12 +11,14 @@ 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.mapper.AuctionSearchMapper; 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; @@ -30,14 +32,17 @@ 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) From a5a4bc715f478f1a1e269c06b28e76f73c14b252 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 19:56:39 +0900 Subject: [PATCH 07/24] =?UTF-8?q?[feat]=20:=20=EC=8A=A4=EC=BC=80=EC=A4=84?= =?UTF-8?q?=EB=9F=AC=EB=A1=9C=20=ED=86=B5=EA=B3=84=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/handsup/auction/scheduler/AuctionScheduler.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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..d1b1389c 100644 --- a/core/src/main/java/dev/handsup/auction/scheduler/AuctionScheduler.java +++ b/core/src/main/java/dev/handsup/auction/scheduler/AuctionScheduler.java @@ -4,7 +4,7 @@ import org.springframework.stereotype.Component; import dev.handsup.auction.repository.auction.AuctionQueryRepository; -import dev.handsup.bidding.repository.BiddingRepository; +import dev.handsup.auction.repository.auction.AuctionSearchRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -13,10 +13,15 @@ @Slf4j public class AuctionScheduler { private final AuctionQueryRepository auctionQueryRepository; - private final BiddingRepository biddingRepository; + private final AuctionSearchRepository auctionSearchRepository; @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") public void updateAuctionStatus() { auctionQueryRepository.updateAuctionStatusAfterEndDate(); } + + @Scheduled(cron = "0 */3 * * * *") + public void updateAuctionSearch(){ + auctionSearchRepository.updateAuctionSearch(); + } } From d01bb1db330a51c64d9516c22adb179cbe5239fe Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 19:57:39 +0900 Subject: [PATCH 08/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=8F=99=EC=A0=81=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/AuctionSearchQueryRepository.java | 11 ++ .../AuctionSearchQueryRepositoryImpl.java | 125 ++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java create mode 100644 core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java 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..6738b3de --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java @@ -0,0 +1,11 @@ +package dev.handsup.auction.repository.auction; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import dev.handsup.auction.domain.AuctionSearch; +import dev.handsup.auction.dto.request.AuctionSearchCondition; + +public interface AuctionSearchQueryRepository { + Slice searchAuctions(AuctionSearchCondition auctionSearchCondition, Pageable pageable); +} 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..4ea20cf4 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java @@ -0,0 +1,125 @@ +package dev.handsup.auction.repository.auction; + +import static dev.handsup.auction.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.AuctionSearch; +import dev.handsup.auction.domain.auction_field.TradeMethod; +import dev.handsup.auction.dto.request.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(searchAuctionSort(pageable)) + .limit(pageable.getPageSize() + 1L) + .offset(pageable.getOffset()) + .fetch(); + boolean hasNext = hasNext(pageable.getPageSize(), content); + return new SliceImpl<>(content, pageable, hasNext); + } + + private OrderSpecifier searchAuctionSort(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; + } +} From 7b339c384a4c2b500553d6266d683ba7e243d0d6 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 20:01:50 +0900 Subject: [PATCH 09/24] =?UTF-8?q?[test]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EB=8F=99=EC=A0=81=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/AuctionSearchRepositoryTest.java | 175 ++++++++++++++++++ .../handsup/fixture/AuctionSearchFixture.java | 119 ++++++++++++ 2 files changed, 294 insertions(+) create mode 100644 core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java create mode 100644 core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java 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..93bdc52a --- /dev/null +++ b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java @@ -0,0 +1,175 @@ +package dev.handsup.auction.repository.auction; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.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.test.util.ReflectionTestUtils; + +import dev.handsup.auction.domain.AuctionSearch; +import dev.handsup.auction.domain.auction_field.TradeMethod; +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.AuctionSearchFixture; +import dev.handsup.fixture.ProductFixture; +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 + assertAll( + () -> assertThat(auctionSearches).hasSize(2), + () -> 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 + assertAll( + () -> assertThat(auctionSearches).hasSize(2), + () -> 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 + assertAll( + () -> assertThat(auctionSearches).hasSize(2), + () -> 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 + assertAll( + () -> assertThat(auctionSearches).hasSize(1), + () -> assertThat(auctionSearches.get(0)).isEqualTo(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 + assertAll( + () -> assertThat(auctionSearches).hasSize(2), + () -> assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch1) + ); + } +} 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..9ab2ef84 --- /dev/null +++ b/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java @@ -0,0 +1,119 @@ +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.auction.domain.AuctionSearch; +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 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, ProductCategory productCategory, int currentBiddingPrice) { + AuctionSearch auctionSearch = AuctionSearch.builder() + .auctionId(auctionId) + .productId(productId) + .category(productCategory.getValue()) + .isNewProduct(false) + .title(TITLE) + .imgUrl(IMAGE_URL) + .endDate(END_DATE) + .tradingLocation(TradingLocation.of(SI, GU, DONG)) + .tradeMethod(TradeMethod.DIRECT) + .createdAt(LocalDateTime.now()) + .build(); + return auctionSearch; + } + + 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, 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, 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(); + } +} From 2c618eba33e7e5c9218352346caa4b446ee8c1a7 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 20:02:36 +0900 Subject: [PATCH 10/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SearchApiController.java | 12 +++++++ .../dto/response/AuctionSearchResponse.java | 35 +++++++++++++++++++ .../handsup/search/service/SearchService.java | 14 ++++++++ 3 files changed, 61 insertions(+) create mode 100644 core/src/main/java/dev/handsup/auction/dto/response/AuctionSearchResponse.java 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..48031e62 100644 --- a/api/src/main/java/dev/handsup/search/controller/SearchApiController.java +++ b/api/src/main/java/dev/handsup/search/controller/SearchApiController.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RestController; import dev.handsup.auction.dto.request.AuctionSearchCondition; +import dev.handsup.auction.dto.response.AuctionSearchResponse; import dev.handsup.auction.dto.response.AuctionSimpleResponse; import dev.handsup.auth.annotation.NoAuth; import dev.handsup.common.dto.PageResponse; @@ -38,6 +39,17 @@ public ResponseEntity> searchAuctions( return ResponseEntity.ok(response); } + @NoAuth + @Operation(summary = "경매 검색 API", description = "경매를 검색한다") + @ApiResponse(useReturnTypeSchema = true) + @PostMapping("/v2") + public ResponseEntity> optimizedSearchAuctions( + @Valid @RequestBody AuctionSearchCondition condition, + Pageable pageable) { + PageResponse response = searchService.optimizedSearchAuctions(condition, pageable); + return ResponseEntity.ok(response); + } + @NoAuth @Operation(summary = "인기 검색어 조회 API", description = "인기 검색어를 조회한다.") @ApiResponse(useReturnTypeSchema = true) diff --git a/core/src/main/java/dev/handsup/auction/dto/response/AuctionSearchResponse.java b/core/src/main/java/dev/handsup/auction/dto/response/AuctionSearchResponse.java new file mode 100644 index 00000000..756fcb19 --- /dev/null +++ b/core/src/main/java/dev/handsup/auction/dto/response/AuctionSearchResponse.java @@ -0,0 +1,35 @@ +package dev.handsup.auction.dto.response; + +public record AuctionSearchResponse( + + Long auctionId, + String title, + int currentBiddingPrice, + String imageUrl, + int bookmarkCount, + String dong, + String createdAt, + boolean isProgress +) { + public static AuctionSearchResponse of( + Long auctionId, + String title, + int currentBiddingPrice, + String imageUrl, + int bookmarkCount, + String dong, + String createdAt, + boolean isProgress + ) { + return new AuctionSearchResponse( + auctionId, + title, + currentBiddingPrice, + imageUrl, + bookmarkCount, + dong, + createdAt, + 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..ddbdb0e5 100644 --- a/core/src/main/java/dev/handsup/search/service/SearchService.java +++ b/core/src/main/java/dev/handsup/search/service/SearchService.java @@ -6,9 +6,12 @@ import org.springframework.transaction.annotation.Transactional; import dev.handsup.auction.dto.mapper.AuctionMapper; +import dev.handsup.auction.dto.mapper.AuctionSearchMapper; import dev.handsup.auction.dto.request.AuctionSearchCondition; +import dev.handsup.auction.dto.response.AuctionSearchResponse; 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.CommonMapper; import dev.handsup.common.dto.PageResponse; @@ -20,6 +23,7 @@ @RequiredArgsConstructor public class SearchService { private final AuctionQueryRepository auctionQueryRepository; + private final AuctionSearchRepository auctionSearchRepository; private final RedisSearchRepository redisSearchRepository; @Transactional(readOnly = true) @@ -32,6 +36,16 @@ public PageResponse searchAuctions(AuctionSearchCondition return CommonMapper.toPageResponse(auctionResponsePage); } + @Transactional(readOnly = true) + public PageResponse optimizedSearchAuctions(AuctionSearchCondition condition, Pageable pageable) { + Slice auctionResponsePage = auctionSearchRepository + .searchAuctions(condition, pageable) + .map(AuctionSearchMapper::toAuctionSearchResponse); + redisSearchRepository.increaseSearchCount(condition.keyword()); + + return CommonMapper.toPageResponse(auctionResponsePage); + } + @Transactional(readOnly = true) public PopularKeywordsResponse getPopularKeywords() { return SearchMapper.toPopularKeywordsResponse(redisSearchRepository.getPopularKeywords(10)); From 6e82e20c139e130343cc6f60941297f0ed60abce Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 20:03:44 +0900 Subject: [PATCH 11/24] =?UTF-8?q?[chore]=20:=20API=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/test/http/auction.http | 21 +++++++++++ core/src/test/http/home.http | 4 ++ core/src/test/http/search.http | 67 +++++++++++++++++++++++++++++++++ core/src/test/http/user.http | 8 ++++ 4 files changed, 100 insertions(+) create mode 100644 core/src/test/http/auction.http create mode 100644 core/src/test/http/home.http create mode 100644 core/src/test/http/search.http create mode 100644 core/src/test/http/user.http 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/search.http b/core/src/test/http/search.http new file mode 100644 index 00000000..f519ced1 --- /dev/null +++ b/core/src/test/http/search.http @@ -0,0 +1,67 @@ +### 기본 검색 (키워드만) +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/v2?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": "존재하지않는상품명" +} + 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 From f16ba71d713a90f12acd67dc14f479d9fb7ee2a7 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 21:35:08 +0900 Subject: [PATCH 12/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=EC=BF=BC=EB=A6=AC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/AuctionSearchQueryRepository.java | 2 ++ .../AuctionSearchQueryRepositoryImpl.java | 23 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) 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 index 6738b3de..6f4bac45 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java @@ -8,4 +8,6 @@ public interface AuctionSearchQueryRepository { Slice searchAuctions(AuctionSearchCondition auctionSearchCondition, Pageable pageable); + + Slice sortAuctionByCriteria(String si, String gu, String dong, Pageable pageable); } 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 index 4ea20cf4..b9835199 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java @@ -41,7 +41,7 @@ public Slice searchAuctions(AuctionSearchCondition condition, Pag isNewProductEq(condition.isNewProduct()), isProgressEq(condition.isProgress()) ) - .orderBy(searchAuctionSort(pageable)) + .orderBy(auctionSearchSort(pageable)) .limit(pageable.getPageSize() + 1L) .offset(pageable.getOffset()) .fetch(); @@ -49,7 +49,26 @@ public Slice searchAuctions(AuctionSearchCondition condition, Pag return new SliceImpl<>(content, pageable, hasNext); } - private OrderSpecifier searchAuctionSort(Pageable pageable) { + @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); + } + + + private OrderSpecifier auctionSearchSort(Pageable pageable) { return pageable.getSort().stream() .findFirst() .map(order -> switch (order.getProperty()) { From 1e79a8a4ba35e2f8a1aa6c45aeff28573e7a7027 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 21:35:22 +0900 Subject: [PATCH 13/24] =?UTF-8?q?[test]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EB=8F=99?= =?UTF-8?q?=EC=A0=81=20=EC=BF=BC=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/AuctionSearchRepositoryTest.java | 50 +++++++++++++++++++ .../handsup/fixture/AuctionSearchFixture.java | 32 ++++++------ 2 files changed, 65 insertions(+), 17 deletions(-) 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 index 93bdc52a..03647bc2 100644 --- a/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java +++ b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java @@ -10,10 +10,12 @@ 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.AuctionSearch; 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.dto.request.AuctionSearchCondition; import dev.handsup.auction.repository.product.ProductCategoryRepository; @@ -172,4 +174,52 @@ void searchAuction_keyword_filter() { () -> 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); + } } diff --git a/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java b/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java index 9ab2ef84..f109f619 100644 --- a/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java +++ b/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java @@ -10,7 +10,6 @@ import dev.handsup.auction.domain.AuctionSearch; 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 lombok.NoArgsConstructor; @NoArgsConstructor(access = PRIVATE) @@ -23,22 +22,6 @@ public class AuctionSearchFixture { static final String DONG = "동선동"; static final String IMAGE_URL = "image.jpg"; - public static AuctionSearch auctionSearch(Long auctionId, Long productId, ProductCategory productCategory, int currentBiddingPrice) { - AuctionSearch auctionSearch = AuctionSearch.builder() - .auctionId(auctionId) - .productId(productId) - .category(productCategory.getValue()) - .isNewProduct(false) - .title(TITLE) - .imgUrl(IMAGE_URL) - .endDate(END_DATE) - .tradingLocation(TradingLocation.of(SI, GU, DONG)) - .tradeMethod(TradeMethod.DIRECT) - .createdAt(LocalDateTime.now()) - .build(); - return auctionSearch; - } - public static AuctionSearch auctionSearch(Long auctionId, Long productId, int currentBiddingPrice) { AuctionSearch auctionSearch = AuctionSearch.builder() .auctionId(auctionId) @@ -101,6 +84,21 @@ public static AuctionSearch auctionSearch(Long auctionId, Long productId) { .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() From 3b080b8f1766600783adfc3b7064d46c19aa8ac2 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 21:36:04 +0900 Subject: [PATCH 14/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?API=20=EC=B6=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/controller/SearchApiController.java | 18 ++++++++++++++++++ .../dto/mapper/AuctionSearchMapper.java | 15 +++++++++++++++ .../handsup/search/service/SearchService.java | 10 ++++++++++ 3 files changed, 43 insertions(+) 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 48031e62..ec89d5b9 100644 --- a/api/src/main/java/dev/handsup/search/controller/SearchApiController.java +++ b/api/src/main/java/dev/handsup/search/controller/SearchApiController.java @@ -1,16 +1,19 @@ package dev.handsup.search.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.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.AuctionSearchCondition; import dev.handsup.auction.dto.response.AuctionSearchResponse; import dev.handsup.auction.dto.response.AuctionSimpleResponse; +import dev.handsup.auction.dto.response.RecommendAuctionResponse; import dev.handsup.auth.annotation.NoAuth; import dev.handsup.common.dto.PageResponse; import dev.handsup.search.dto.PopularKeywordsResponse; @@ -58,4 +61,19 @@ public ResponseEntity getPopularKeywords() { PopularKeywordsResponse response = searchService.getPopularKeywords(); 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 = searchService.getRecommendAuctions(si, gu, dong, pageable); + return ResponseEntity.ok(response); + } } diff --git a/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java b/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java index f00f411a..c67217bd 100644 --- a/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java +++ b/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java @@ -7,6 +7,7 @@ import dev.handsup.auction.domain.auction_field.AuctionStatus; import dev.handsup.auction.domain.product.Product; import dev.handsup.auction.dto.response.AuctionSearchResponse; +import dev.handsup.auction.dto.response.RecommendAuctionResponse; import lombok.NoArgsConstructor; @NoArgsConstructor(access = PRIVATE) @@ -39,6 +40,20 @@ public static AuctionSearchResponse toAuctionSearchResponse(AuctionSearch auctio 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/search/service/SearchService.java b/core/src/main/java/dev/handsup/search/service/SearchService.java index ddbdb0e5..e39c11e1 100644 --- a/core/src/main/java/dev/handsup/search/service/SearchService.java +++ b/core/src/main/java/dev/handsup/search/service/SearchService.java @@ -10,6 +10,7 @@ import dev.handsup.auction.dto.request.AuctionSearchCondition; import dev.handsup.auction.dto.response.AuctionSearchResponse; import dev.handsup.auction.dto.response.AuctionSimpleResponse; +import dev.handsup.auction.dto.response.RecommendAuctionResponse; import dev.handsup.auction.repository.auction.AuctionQueryRepository; import dev.handsup.auction.repository.auction.AuctionSearchRepository; import dev.handsup.auction.repository.search.RedisSearchRepository; @@ -46,6 +47,15 @@ public PageResponse optimizedSearchAuctions(AuctionSearch 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 PopularKeywordsResponse getPopularKeywords() { return SearchMapper.toPopularKeywordsResponse(redisSearchRepository.getPopularKeywords(10)); From f038a1add22d7d64ddc71cc94edb6d9883e1106a Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 21:36:18 +0900 Subject: [PATCH 15/24] =?UTF-8?q?[chore]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?API=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/test/http/search.http | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/test/http/search.http b/core/src/test/http/search.http index f519ced1..80028dbb 100644 --- a/core/src/test/http/search.http +++ b/core/src/test/http/search.http @@ -65,3 +65,10 @@ Content-Type: application/json "keyword": "존재하지않는상품명" } + +### 경매 추천 +GET http://localhost:8080/api/auctions/recommend?si=서울시&gu=강남구&dong=역삼동&page=0&size=10&sort=북마크수 + + +### 경매 추천 ver2 +GET http://localhost:8080/api/auctions/recommend?si=서울시&gu=강남구&dong=역삼동&page=0&size=10&sort=북마크수 From a1540f7429c7dd4118752b862012d7f641f47a85 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 22:26:29 +0900 Subject: [PATCH 16/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=20=EC=BF=BC=EB=A6=AC=20=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/AuctionSearchQueryRepository.java | 6 +++++- .../auction/AuctionSearchQueryRepositoryImpl.java | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) 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 index 6f4bac45..50998dbf 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java @@ -1,5 +1,7 @@ package dev.handsup.auction.repository.auction; +import java.util.List; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -10,4 +12,6 @@ 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 index b9835199..f1704f83 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java @@ -67,6 +67,21 @@ public Slice sortAuctionByCriteria(String si, String gu, String d 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() From 853fd775d87fcafdbd6759550fb876c72f2b5ba4 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 22:26:37 +0900 Subject: [PATCH 17/24] =?UTF-8?q?[test]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EB=8F=99=EC=A0=81=20=EC=BF=BC=EB=A6=AC=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/AuctionSearchRepositoryTest.java | 23 +++++++++++++++++++ .../handsup/fixture/AuctionSearchFixture.java | 15 ++++++++++++ 2 files changed, 38 insertions(+) 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 index 03647bc2..599ce59a 100644 --- a/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java +++ b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java @@ -222,4 +222,27 @@ void sortAuctionByCriteria_bookmarkCount() { //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/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java b/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java index f109f619..98d68213 100644 --- a/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java +++ b/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java @@ -39,6 +39,21 @@ public static AuctionSearch auctionSearch(Long auctionId, Long productId, int cu return auctionSearch; } + 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) From 28b0a02c555070aa7152b7d4d461357169a32079 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 22:26:51 +0900 Subject: [PATCH 18/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SearchApiController.java | 15 ++++++++++++ .../handsup/search/service/SearchService.java | 23 +++++++++++++++++++ 2 files changed, 38 insertions(+) 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 ec89d5b9..1d134cd7 100644 --- a/api/src/main/java/dev/handsup/search/controller/SearchApiController.java +++ b/api/src/main/java/dev/handsup/search/controller/SearchApiController.java @@ -15,10 +15,13 @@ import dev.handsup.auction.dto.response.AuctionSimpleResponse; import dev.handsup.auction.dto.response.RecommendAuctionResponse; import dev.handsup.auth.annotation.NoAuth; +import dev.handsup.auth.jwt.JwtAuthorization; import dev.handsup.common.dto.PageResponse; import dev.handsup.search.dto.PopularKeywordsResponse; import dev.handsup.search.service.SearchService; +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 jakarta.validation.Valid; @@ -76,4 +79,16 @@ public ResponseEntity> getRecommendAuctio PageResponse response = searchService.getRecommendAuctions(si, gu, dong, pageable); return ResponseEntity.ok(response); } + + @Operation(summary = "유저 선호 카테고리 경매 조회 API", description = "유저가 선호하는 카테고리의 경매를 조회한다.") + @ApiResponse(useReturnTypeSchema = true) + @GetMapping("/recommend/category") + public ResponseEntity> getUserPreferredCategoryAuctionsV2( + @Parameter(hidden = true) @JwtAuthorization User user, + Pageable pageable + ) { + PageResponse response = searchService.getUserPreferredCategoryAuctions(user, + pageable); + return ResponseEntity.ok(response); + } } 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 e39c11e1..ce8d6fe9 100644 --- a/core/src/main/java/dev/handsup/search/service/SearchService.java +++ b/core/src/main/java/dev/handsup/search/service/SearchService.java @@ -1,10 +1,14 @@ 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.mapper.AuctionSearchMapper; import dev.handsup.auction.dto.request.AuctionSearchCondition; @@ -13,11 +17,13 @@ import dev.handsup.auction.dto.response.RecommendAuctionResponse; import dev.handsup.auction.repository.auction.AuctionQueryRepository; 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.search.dto.PopularKeywordsResponse; import dev.handsup.search.dto.SearchMapper; +import dev.handsup.user.domain.User; import lombok.RequiredArgsConstructor; @Service @@ -26,6 +32,8 @@ public class SearchService { private final AuctionQueryRepository auctionQueryRepository; private final AuctionSearchRepository auctionSearchRepository; private final RedisSearchRepository redisSearchRepository; + private final PreferredProductCategoryRepository preferredProductCategoryRepository; + @Transactional(readOnly = true) public PageResponse searchAuctions(AuctionSearchCondition condition, Pageable pageable) { @@ -56,6 +64,21 @@ public PageResponse getRecommendAuctions(String si, St 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)); From 2f31779c2ddfc2eee63a3a2cd75031b0e80e7a83 Mon Sep 17 00:00:00 2001 From: hs12 Date: Mon, 16 Jun 2025 22:26:58 +0900 Subject: [PATCH 19/24] =?UTF-8?q?[test]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/test/http/search.http | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/src/test/http/search.http b/core/src/test/http/search.http index 80028dbb..73dc91ce 100644 --- a/core/src/test/http/search.http +++ b/core/src/test/http/search.http @@ -70,5 +70,15 @@ Content-Type: application/json GET http://localhost:8080/api/auctions/recommend?si=서울시&gu=강남구&dong=역삼동&page=0&size=10&sort=북마크수 -### 경매 추천 ver2 +### 경매 조건 추천 ver2 GET http://localhost:8080/api/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/auctions/search/recommend/category?page=0&size=10 +Content-Type: application/json +Authorization: Bearer eyJ0eXBlIjoiand0IiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VySWQiOjEsImlhdCI6MTc0OTk4ODQ3NiwiZXhwIjoyMDY1MzQ4NDc2fQ._I9ddh2ADiTV6aIitn19RRkW2sglpkRbRMZtlqC7xeM From 855fff7077b43575d49ae1d2ae478f746b05ce12 Mon Sep 17 00:00:00 2001 From: hs12 Date: Tue, 17 Jun 2025 13:53:05 +0900 Subject: [PATCH 20/24] =?UTF-8?q?[refactor]=20:=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=ED=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auction/AuctionQueryRepositoryImpl.java | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) 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 a6070e26..d2d4a030 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 @@ -25,8 +25,6 @@ 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 lombok.RequiredArgsConstructor; @Repository @@ -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; } From 045a5ce5873dfc925f59113123157594b846b290 Mon Sep 17 00:00:00 2001 From: hs12 Date: Tue, 17 Jun 2025 13:53:26 +0900 Subject: [PATCH 21/24] =?UTF-8?q?[test]=20:=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=8B=9C=20=EA=B2=BD=EB=A7=A4=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EB=93=B1=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/handsup/auction/service/AuctionServiceTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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..29af4002 100644 --- a/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java +++ b/core/src/test/java/dev/handsup/auction/service/AuctionServiceTest.java @@ -22,17 +22,20 @@ import org.springframework.test.util.ReflectionTestUtils; import dev.handsup.auction.domain.Auction; +import dev.handsup.auction.domain.AuctionSearch; 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.mapper.AuctionSearchMapper; 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.PageResponse; @@ -63,6 +66,9 @@ class AuctionServiceTest { @Mock private PreferredProductCategoryRepository preferredProductCategoryRepository; + @Mock + private AuctionSearchRepository auctionSearchRepository; + @InjectMocks private AuctionService auctionService; @@ -90,10 +96,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()); From 029b792b689fad941b8d4c3accb687180151b7f5 Mon Sep 17 00:00:00 2001 From: hs12 Date: Tue, 17 Jun 2025 18:08:59 +0900 Subject: [PATCH 22/24] =?UTF-8?q?[feat]=20:=20=EA=B2=BD=EB=A7=A4,=20?= =?UTF-8?q?=EA=B2=BD=EB=A7=A4=20=EA=B2=80=EC=83=89=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AuctionApiController.java | 33 ---- .../controller/RecommendApiController.java | 82 +++++++++ .../controller/SearchApiController.java | 56 ++---- .../controller/AuctionApiControllerTest.java | 99 ----------- .../RecommendApiControllerTest.java | 150 ++++++++++++++++ .../controller/SearchApiControllerTest.java | 110 +++++++----- .../auction/dto/mapper/AuctionMapper.java | 12 +- .../dto/response/AuctionSimpleResponse.java | 35 ---- .../response/ChatRoomExistenceResponse.java | 9 - .../auction/AuctionQueryRepository.java | 2 +- .../auction/AuctionQueryRepositoryImpl.java | 2 +- .../repository/auction/AuctionRepository.java | 2 +- .../auction/AuctionSearchQueryRepository.java | 4 +- .../AuctionSearchQueryRepositoryImpl.java | 7 +- .../auction/AuctionSearchRepository.java | 2 +- .../auction/scheduler/AuctionScheduler.java | 6 +- .../auction/service/AuctionService.java | 35 +--- .../dto}/RecommendAuctionResponse.java | 2 +- .../recommend/service/RecommendService.java | 76 ++++++++ .../domain/AuctionSearch.java | 2 +- .../dto}/AuctionSearchCondition.java | 2 +- .../dto}/AuctionSearchMapper.java | 7 +- .../dto}/AuctionSearchResponse.java | 2 +- .../handsup/search/service/SearchService.java | 21 ++- core/src/test/http/search.http | 4 +- ...t.java => AuctionQueryRepositoryTest.java} | 165 +----------------- .../auction/AuctionSearchRepositoryTest.java | 32 +--- .../auction/service/AuctionServiceTest.java | 51 +----- .../service/RecommendServiceTest.java | 91 ++++++++++ .../search/service/SearchServiceTest.java | 29 ++- .../handsup/fixture/AuctionSearchFixture.java | 17 +- 31 files changed, 558 insertions(+), 589 deletions(-) create mode 100644 api/src/main/java/dev/handsup/recommend/controller/RecommendApiController.java create mode 100644 api/src/test/java/dev/handsup/recommend/controller/RecommendApiControllerTest.java delete mode 100644 core/src/main/java/dev/handsup/auction/dto/response/AuctionSimpleResponse.java delete mode 100644 core/src/main/java/dev/handsup/auction/dto/response/ChatRoomExistenceResponse.java rename core/src/main/java/dev/handsup/{auction/dto/response => recommend/dto}/RecommendAuctionResponse.java (93%) create mode 100644 core/src/main/java/dev/handsup/recommend/service/RecommendService.java rename core/src/main/java/dev/handsup/{auction => search}/domain/AuctionSearch.java (98%) rename core/src/main/java/dev/handsup/{auction/dto/request => search/dto}/AuctionSearchCondition.java (89%) rename core/src/main/java/dev/handsup/{auction/dto/mapper => search/dto}/AuctionSearchMapper.java (89%) rename core/src/main/java/dev/handsup/{auction/dto/response => search/dto}/AuctionSearchResponse.java (92%) rename core/src/test/java/dev/handsup/auction/repository/auction/{AuctionQueryRepositoryImplTest.java => AuctionQueryRepositoryTest.java} (53%) create mode 100644 core/src/test/java/dev/handsup/recommend/service/RecommendServiceTest.java 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 1d134cd7..359d68e3 100644 --- a/api/src/main/java/dev/handsup/search/controller/SearchApiController.java +++ b/api/src/main/java/dev/handsup/search/controller/SearchApiController.java @@ -1,27 +1,19 @@ package dev.handsup.search.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.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.AuctionSearchCondition; -import dev.handsup.auction.dto.response.AuctionSearchResponse; -import dev.handsup.auction.dto.response.AuctionSimpleResponse; -import dev.handsup.auction.dto.response.RecommendAuctionResponse; import dev.handsup.auth.annotation.NoAuth; -import dev.handsup.auth.jwt.JwtAuthorization; 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 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 jakarta.validation.Valid; @@ -30,65 +22,37 @@ @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 = "경매를 검색한다") + @Operation(summary = "최적화된 경매 검색 API", description = "경매를 검색한다") @ApiResponse(useReturnTypeSchema = true) - @PostMapping("/v2") - public ResponseEntity> optimizedSearchAuctions( + @PostMapping("/api/v2/auctions/search") + public ResponseEntity> searchAuctionsV2( @Valid @RequestBody AuctionSearchCondition condition, Pageable pageable) { - PageResponse response = searchService.optimizedSearchAuctions(condition, 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); } - - @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 = searchService.getRecommendAuctions(si, gu, dong, pageable); - return ResponseEntity.ok(response); - } - - @Operation(summary = "유저 선호 카테고리 경매 조회 API", description = "유저가 선호하는 카테고리의 경매를 조회한다.") - @ApiResponse(useReturnTypeSchema = true) - @GetMapping("/recommend/category") - public ResponseEntity> getUserPreferredCategoryAuctionsV2( - @Parameter(hidden = true) @JwtAuthorization User user, - Pageable pageable - ) { - PageResponse response = searchService.getUserPreferredCategoryAuctions(user, - pageable); - 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/AuctionSimpleResponse.java b/core/src/main/java/dev/handsup/auction/dto/response/AuctionSimpleResponse.java deleted file mode 100644 index c0e07e3e..00000000 --- a/core/src/main/java/dev/handsup/auction/dto/response/AuctionSimpleResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -package dev.handsup.auction.dto.response; - -public record AuctionSimpleResponse( - - Long auctionId, - String title, - int currentBiddingPrice, - String imageUrl, - int bookmarkCount, - String dong, - String createdAt, - String status -) { - public static AuctionSimpleResponse of( - Long auctionId, - String title, - int currentBiddingPrice, - String imageUrl, - int bookmarkCount, - String dong, - String createdAt, - String status - ) { - return new AuctionSimpleResponse( - auctionId, - title, - currentBiddingPrice, - imageUrl, - bookmarkCount, - dong, - createdAt, - status - ); - } -} 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 d2d4a030..cec33385 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,7 +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.search.dto.AuctionSearchCondition; import lombok.RequiredArgsConstructor; @Repository 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 index 50998dbf..61058eb8 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepository.java @@ -5,8 +5,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import dev.handsup.auction.domain.AuctionSearch; -import dev.handsup.auction.dto.request.AuctionSearchCondition; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.search.dto.AuctionSearchCondition; public interface AuctionSearchQueryRepository { Slice searchAuctions(AuctionSearchCondition auctionSearchCondition, Pageable pageable); 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 index f1704f83..524144df 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchQueryRepositoryImpl.java @@ -1,6 +1,6 @@ package dev.handsup.auction.repository.auction; -import static dev.handsup.auction.domain.QAuctionSearch.*; +import static dev.handsup.search.domain.QAuctionSearch.*; import static org.springframework.util.StringUtils.*; import java.util.List; @@ -14,9 +14,9 @@ import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; -import dev.handsup.auction.domain.AuctionSearch; import dev.handsup.auction.domain.auction_field.TradeMethod; -import dev.handsup.auction.dto.request.AuctionSearchCondition; +import dev.handsup.search.domain.AuctionSearch; +import dev.handsup.search.dto.AuctionSearchCondition; import lombok.RequiredArgsConstructor; @Repository @@ -82,7 +82,6 @@ public Slice findByProductCategories(List productCategori return new SliceImpl<>(content, pageable, hasNext); } - private OrderSpecifier auctionSearchSort(Pageable pageable) { return pageable.getSort().stream() .findFirst() 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 index a28c811d..51c7b2da 100644 --- a/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchRepository.java +++ b/core/src/main/java/dev/handsup/auction/repository/auction/AuctionSearchRepository.java @@ -4,7 +4,7 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -import dev.handsup.auction.domain.AuctionSearch; +import dev.handsup.search.domain.AuctionSearch; import jakarta.transaction.Transactional; public interface AuctionSearchRepository extends JpaRepository, AuctionSearchQueryRepository { 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 d1b1389c..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,7 +3,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import dev.handsup.auction.repository.auction.AuctionQueryRepository; +import dev.handsup.auction.repository.auction.AuctionRepository; import dev.handsup.auction.repository.auction.AuctionSearchRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,12 +12,12 @@ @RequiredArgsConstructor @Slf4j public class AuctionScheduler { - private final AuctionQueryRepository auctionQueryRepository; + 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 * * * *") 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 9db3e279..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,29 +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.mapper.AuctionSearchMapper; 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; @@ -35,7 +26,6 @@ public class AuctionService { 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()); @@ -51,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/auction/domain/AuctionSearch.java b/core/src/main/java/dev/handsup/search/domain/AuctionSearch.java similarity index 98% rename from core/src/main/java/dev/handsup/auction/domain/AuctionSearch.java rename to core/src/main/java/dev/handsup/search/domain/AuctionSearch.java index 25dbb94e..d1716f2a 100644 --- a/core/src/main/java/dev/handsup/auction/domain/AuctionSearch.java +++ b/core/src/main/java/dev/handsup/search/domain/AuctionSearch.java @@ -1,4 +1,4 @@ -package dev.handsup.auction.domain; +package dev.handsup.search.domain; import static jakarta.persistence.EnumType.*; import static jakarta.persistence.GenerationType.*; 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/auction/dto/mapper/AuctionSearchMapper.java b/core/src/main/java/dev/handsup/search/dto/AuctionSearchMapper.java similarity index 89% rename from core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java rename to core/src/main/java/dev/handsup/search/dto/AuctionSearchMapper.java index c67217bd..ddf3fe2b 100644 --- a/core/src/main/java/dev/handsup/auction/dto/mapper/AuctionSearchMapper.java +++ b/core/src/main/java/dev/handsup/search/dto/AuctionSearchMapper.java @@ -1,13 +1,12 @@ -package dev.handsup.auction.dto.mapper; +package dev.handsup.search.dto; import static lombok.AccessLevel.*; import dev.handsup.auction.domain.Auction; -import dev.handsup.auction.domain.AuctionSearch; +import dev.handsup.search.domain.AuctionSearch; import dev.handsup.auction.domain.auction_field.AuctionStatus; import dev.handsup.auction.domain.product.Product; -import dev.handsup.auction.dto.response.AuctionSearchResponse; -import dev.handsup.auction.dto.response.RecommendAuctionResponse; +import dev.handsup.recommend.dto.RecommendAuctionResponse; import lombok.NoArgsConstructor; @NoArgsConstructor(access = PRIVATE) diff --git a/core/src/main/java/dev/handsup/auction/dto/response/AuctionSearchResponse.java b/core/src/main/java/dev/handsup/search/dto/AuctionSearchResponse.java similarity index 92% rename from core/src/main/java/dev/handsup/auction/dto/response/AuctionSearchResponse.java rename to core/src/main/java/dev/handsup/search/dto/AuctionSearchResponse.java index 756fcb19..58e199bc 100644 --- a/core/src/main/java/dev/handsup/auction/dto/response/AuctionSearchResponse.java +++ b/core/src/main/java/dev/handsup/search/dto/AuctionSearchResponse.java @@ -1,4 +1,4 @@ -package dev.handsup.auction.dto.response; +package dev.handsup.search.dto; public record AuctionSearchResponse( 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 ce8d6fe9..79070795 100644 --- a/core/src/main/java/dev/handsup/search/service/SearchService.java +++ b/core/src/main/java/dev/handsup/search/service/SearchService.java @@ -10,17 +10,16 @@ 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.mapper.AuctionSearchMapper; -import dev.handsup.auction.dto.request.AuctionSearchCondition; -import dev.handsup.auction.dto.response.AuctionSearchResponse; -import dev.handsup.auction.dto.response.AuctionSimpleResponse; -import dev.handsup.auction.dto.response.RecommendAuctionResponse; -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; @@ -29,24 +28,24 @@ @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 = auctionQueryRepository + public PageResponse searchAuctions(AuctionSearchCondition condition, Pageable pageable) { + Slice auctionResponsePage = auctionRepository .searchAuctions(condition, pageable) - .map(AuctionMapper::toAuctionSimpleResponse); + .map(AuctionMapper::toAuctionSearchResponse); redisSearchRepository.increaseSearchCount(condition.keyword()); return CommonMapper.toPageResponse(auctionResponsePage); } @Transactional(readOnly = true) - public PageResponse optimizedSearchAuctions(AuctionSearchCondition condition, Pageable pageable) { + public PageResponse searchAuctionsV2(AuctionSearchCondition condition, Pageable pageable) { Slice auctionResponsePage = auctionSearchRepository .searchAuctions(condition, pageable) .map(AuctionSearchMapper::toAuctionSearchResponse); diff --git a/core/src/test/http/search.http b/core/src/test/http/search.http index 73dc91ce..3d89e722 100644 --- a/core/src/test/http/search.http +++ b/core/src/test/http/search.http @@ -71,7 +71,7 @@ GET http://localhost:8080/api/auctions/recommend?si=서울시&gu=강남구&dong= ### 경매 조건 추천 ver2 -GET http://localhost:8080/api/auctions/recommend?si=서울시&gu=강남구&dong=역삼동&page=0&size=10&sort=북마크수 +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 @@ -79,6 +79,6 @@ Content-Type: application/json Authorization: Bearer eyJ0eXBlIjoiand0IiwiYWxnIjoiSFMyNTYifQ.eyJ1c2VySWQiOjEsImlhdCI6MTc0OTk4ODQ3NiwiZXhwIjoyMDY1MzQ4NDc2fQ._I9ddh2ADiTV6aIitn19RRkW2sglpkRbRMZtlqC7xeM ### 경매 카테고리 추천 ver2 -GET http://localhost:8080/api/auctions/search/recommend/category?page=0&size=10 +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/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 46984359..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("[최근 입찰가에 대한 하한 조건을 걸 수 있다.]") - @Test - void searchAuction_currentBiddingPrice_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_currentBiddingPrice_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 index 599ce59a..1719db3d 100644 --- a/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java +++ b/core/src/test/java/dev/handsup/auction/repository/auction/AuctionSearchRepositoryTest.java @@ -1,7 +1,6 @@ package dev.handsup.auction.repository.auction; import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; import java.util.List; @@ -13,15 +12,15 @@ import org.springframework.data.domain.Sort; import org.springframework.test.util.ReflectionTestUtils; -import dev.handsup.auction.domain.AuctionSearch; 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.dto.request.AuctionSearchCondition; 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 { @@ -52,7 +51,6 @@ void setUp() { @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); @@ -70,10 +68,7 @@ void searchAuction_currentBiddingPrice_min_filter() { List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); //then - assertAll( - () -> assertThat(auctionSearches).hasSize(2), - () -> assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch2) - ); + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch2); } @@ -96,10 +91,7 @@ void searchAuction_isNewProduct_filter() { List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); //then - assertAll( - () -> assertThat(auctionSearches).hasSize(2), - () -> assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch1) - ); + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch1); } @DisplayName("[진행 중인 경매만 필터링할 수 있다. (isProgressEq)]") @@ -122,10 +114,7 @@ void searchAuction_isProgress_filter() { List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); //then - assertAll( - () -> assertThat(auctionSearches).hasSize(2), - () -> assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch2) - ); + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch2); } @DisplayName("[거래 방식으로 경매를 필터링할 수 있다. (tradeMethodEq)]") @@ -147,10 +136,7 @@ void searchAuction_tradeMethod_filter() { List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); //then - assertAll( - () -> assertThat(auctionSearches).hasSize(1), - () -> assertThat(auctionSearches.get(0)).isEqualTo(auctionSearch1) - ); + assertThat(auctionSearches).containsExactly(auctionSearch1); } @DisplayName("[검색 키워드로 필터링할 수 있다. (keywordContains)]") @@ -169,10 +155,7 @@ void searchAuction_keyword_filter() { List auctionSearches = auctionSearchRepository.searchAuctions(condition, pageRequest).getContent(); //then - assertAll( - () -> assertThat(auctionSearches).hasSize(2), - () -> assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch1) - ); + assertThat(auctionSearches).containsExactly(auctionSearch3, auctionSearch1); } @DisplayName("[입찰수 순으로 경매를 조회할 수 있다.]") @@ -243,6 +226,5 @@ void 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 29af4002..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,56 +16,39 @@ 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.AuctionSearch; 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.mapper.AuctionSearchMapper; 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.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; - @Mock private AuctionSearchRepository auctionSearchRepository; @@ -138,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 index 98d68213..86637012 100644 --- a/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java +++ b/core/src/testFixtures/java/dev/handsup/fixture/AuctionSearchFixture.java @@ -7,7 +7,7 @@ import org.springframework.test.util.ReflectionTestUtils; -import dev.handsup.auction.domain.AuctionSearch; +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; @@ -39,6 +39,21 @@ public static AuctionSearch auctionSearch(Long auctionId, Long productId, int cu 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) From 480ad4b583d13ea5ff9fc7c799f7f775ce3edaf5 Mon Sep 17 00:00:00 2001 From: hs12 Date: Sun, 22 Jun 2025 16:34:12 +0900 Subject: [PATCH 23/24] =?UTF-8?q?[refactor]=20:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=99=B8=EB=B6=80=20=EC=A1=B0=EC=9D=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/auction/AuctionQueryRepositoryImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 cec33385..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 @@ -35,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()), @@ -52,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); @@ -160,7 +160,7 @@ 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)); From efbae1aa6b9a6cf885902a36aa6be99ea5bcfd82 Mon Sep 17 00:00:00 2001 From: hs12 Date: Sun, 22 Jun 2025 16:35:07 +0900 Subject: [PATCH 24/24] =?UTF-8?q?[chore]=20:=20sql,=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/resources/db/auction_search.sql | 39 ++++++++++++++++++- core/src/main/resources/db/product.sql | 21 ++++++++++ core/src/test/http/recommend.http | 18 +++++++++ core/src/test/http/search.http | 34 +++++++--------- 4 files changed, 91 insertions(+), 21 deletions(-) create mode 100644 core/src/test/http/recommend.http diff --git a/core/src/main/resources/db/auction_search.sql b/core/src/main/resources/db/auction_search.sql index 558fb06e..d2964853 100644 --- a/core/src/main/resources/db/auction_search.sql +++ b/core/src/main/resources/db/auction_search.sql @@ -41,4 +41,41 @@ FROM auction a SELECT product_id, MIN(image_url) AS image_url FROM product_image GROUP BY product_id -) pi ON p.product_id = pi.product_id; \ No newline at end of file +) 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/product.sql b/core/src/main/resources/db/product.sql index b79f0e13..e0c53315 100644 --- a/core/src/main/resources/db/product.sql +++ b/core/src/main/resources/db/product.sql @@ -37,3 +37,24 @@ SET product_category_id = 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/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 index 3d89e722..b14d6903 100644 --- a/core/src/test/http/search.http +++ b/core/src/test/http/search.http @@ -23,7 +23,19 @@ Content-Type: application/json } ### 카테고리, 거래방식 등 복합 검색 예시 -POST http://localhost:8080/api/auctions/search/v2?page=0&size=10&sort=최신순 +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 { @@ -63,22 +75,4 @@ Content-Type: application/json { "keyword": "존재하지않는상품명" -} - - -### 경매 추천 -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 +} \ No newline at end of file