From 41f3e4f9d34c81613d370969161b33d5942353a2 Mon Sep 17 00:00:00 2001 From: JO HYUNGJOON Date: Thu, 19 Feb 2026 20:47:01 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=20refactor:=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=9D=B8?= =?UTF-8?q?=EA=B8=B0=20=EC=83=81=ED=92=88=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/repository/ProductRepository.java | 30 ----- .../product/service/ProductService.java | 114 +----------------- 2 files changed, 2 insertions(+), 142 deletions(-) diff --git a/src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java b/src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java index c513077..f5e9eab 100644 --- a/src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java @@ -218,36 +218,6 @@ List findSimilarCustomersPurchases( "ORDER BY (COALESCE(p.viewCount, 0) + COALESCE(p.cartCount, 0)) DESC") List findPopularProducts(Pageable pageable); - /** - * 개인화 추천 - 같은 카테고리 + 비슷한 가격대 - * 정렬: viewCount + cartCount 순 - */ - @EntityGraph(attributePaths = {"brand", "category"}) - @Query("SELECT p FROM Product p " + - "WHERE p.onSale = true " + - "AND p.productType <> com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE " + - "AND p.category.id IN :categoryIds " + - "AND p.id NOT IN :excludeProductIds " + - "AND (" + - " (p.discountPrice IS NOT NULL AND p.discountPrice > 0 AND p.discountPrice BETWEEN :minPrice AND :maxPrice) " + - " OR (p.discountPrice IS NULL OR p.discountPrice = 0) AND p.price BETWEEN :minPrice AND :maxPrice" + - ") " + - "ORDER BY (COALESCE(p.viewCount, 0) + COALESCE(p.cartCount, 0)) DESC") - List findRecommendedProducts( - @Param("categoryIds") List categoryIds, - @Param("minPrice") Integer minPrice, - @Param("maxPrice") Integer maxPrice, - @Param("excludeProductIds") List excludeProductIds, - Pageable pageable - ); - - /** - * 상품 ID 목록으로 상품 조회 - */ - @EntityGraph(attributePaths = {"brand", "category"}) - @Query("SELECT p FROM Product p WHERE p.id IN :productIds AND p.onSale = true") - List findByIdInAndOnSaleTrue(@Param("productIds") List productIds); - // 특정 브랜드를 사용하는 상품이 있는지 확인 boolean existsByBrandId(Long brandId); diff --git a/src/main/java/com/ongil/backend/domain/product/service/ProductService.java b/src/main/java/com/ongil/backend/domain/product/service/ProductService.java index 37ba018..91f6b2e 100644 --- a/src/main/java/com/ongil/backend/domain/product/service/ProductService.java +++ b/src/main/java/com/ongil/backend/domain/product/service/ProductService.java @@ -1,10 +1,7 @@ package com.ongil.backend.domain.product.service; -import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import org.springframework.data.domain.Page; @@ -14,8 +11,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.ongil.backend.domain.cart.repository.CartRepository; -import com.ongil.backend.domain.order.repository.OrderItemRepository; import com.ongil.backend.domain.product.converter.ProductConverter; import com.ongil.backend.domain.product.converter.SizeGuideConverter; @@ -57,8 +52,6 @@ public class ProductService { private final ProductRepository productRepository; private final ProductOptionRepository productOptionRepository; private final ProductViewHistoryRepository productViewHistoryRepository; - private final CartRepository cartRepository; - private final OrderItemRepository orderItemRepository; private final ProductConverter productConverter; private final AiMaterialService aiMaterialService; private final UserRepository userRepository; @@ -68,8 +61,6 @@ public class ProductService { private final CategoryRepository categoryRepository; private static final int SIMILAR_CUSTOMERS_LIMIT = 4; - private static final int PRICE_RANGE = 10000; - private static final int DAYS_TO_LOOK_BACK = 30; // 상품 상세 조회 @Transactional @@ -388,113 +379,12 @@ private SizeGuideResponse buildResponseWithBodyInfoOnly(User user, Product produ /** * 홈화면 추천 상품 조회 - * - 로그인: 최근 30일 조회/장바구니 기준 같은 카테고리 + 비슷한 가격(±10,000원) 필터 적용 - * - 비로그인: 전체 인기 상품 - * - 정렬: 전체 고객 기준 viewCount + cartCount 순 + * - 인기 상품 순(viewCount + cartCount) 반환 + * - 특가 상품(SPECIAL_SALE) 제외 */ public List getRecommendedProducts(Long userId, int size) { - if (userId == null) { - return getPopularProducts(size); - } - return getPersonalizedRecommendations(userId, size); - } - - /** - * 인기 상품 조회 - */ - private List getPopularProducts(int size) { Pageable pageable = PageRequest.of(0, size); List products = productRepository.findPopularProducts(pageable); return productConverter.toSimpleResponseList(products); } - - /** - * 개인화 추천 (로그인 사용자) - */ - private List getPersonalizedRecommendations(Long userId, int size) { - LocalDateTime since = LocalDateTime.now().minusDays(DAYS_TO_LOOK_BACK); - - // 1. 최근 30일간 조회한 상품 ID - List viewedProductIds = productViewHistoryRepository - .findDistinctProductIdsByUserIdAndCreatedAtAfter(userId, since); - - // 2. 장바구니에 담은 상품 ID - List cartProductIds = cartRepository.findProductIdsByUserId(userId); - - // 3. 기준 상품 ID 합치기 - Set baseProductIds = new HashSet<>(); - baseProductIds.addAll(viewedProductIds); - baseProductIds.addAll(cartProductIds); - - // 기록이 없으면 인기 상품 반환 (신규 사용자) - if (baseProductIds.isEmpty()) { - return getPopularProducts(size); - } - - // 4. 구매한 상품 ID (제외 대상) - List purchasedProductIds = orderItemRepository.findProductIdsByUserId(userId); - - // 5. 기준 상품들 조회 - List baseProducts = productRepository.findByIdInAndOnSaleTrue( - new ArrayList<>(baseProductIds) - ); - - if (baseProducts.isEmpty()) { - return getPopularProducts(size); - } - - // 6. 필터 조건 계산 - List categoryIds = baseProducts.stream() - .map(p -> p.getCategory().getId()) - .distinct() - .collect(Collectors.toList()); - - int avgPrice = (int) baseProducts.stream() - .mapToInt(Product::getEffectivePrice) - .average() - .orElse(0); - - int minPrice = Math.max(0, avgPrice - PRICE_RANGE); - int maxPrice = avgPrice + PRICE_RANGE; - - // 7. 제외할 상품 ID (기준 상품 + 구매한 상품) - Set excludeIds = new HashSet<>(baseProductIds); - excludeIds.addAll(purchasedProductIds); - if (excludeIds.isEmpty()) { - excludeIds.add(-1L); - } - - // 8. 추천 상품 조회 (카테고리 + 가격 필터, 인기순 정렬) - Pageable pageable = PageRequest.of(0, size); - List recommendations = productRepository.findRecommendedProducts( - categoryIds, - minPrice, - maxPrice, - new ArrayList<>(excludeIds), - pageable - ); - - // 9. 부족하면 인기 상품으로 채우기 (제외 대상을 고려해 충분히 조회) - if (recommendations.size() < size) { - Set foundIds = recommendations.stream() - .map(Product::getId) - .collect(Collectors.toSet()); - excludeIds.addAll(foundIds); - - int needed = size - recommendations.size(); - int fetchSize = needed + excludeIds.size(); - List popularProducts = productRepository.findPopularProducts( - PageRequest.of(0, fetchSize) - ); - - for (Product p : popularProducts) { - if (!excludeIds.contains(p.getId())) { - recommendations.add(p); - if (recommendations.size() >= size) break; - } - } - } - - return productConverter.toSimpleResponseList(recommendations); - } } \ No newline at end of file From f3c167b01944e3ff55c94b80bce7046fae9219ff Mon Sep 17 00:00:00 2001 From: JO HYUNGJOON Date: Thu, 19 Feb 2026 20:55:40 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=20refactor:=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EA=B0=9C=EC=9D=B8=ED=99=94=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20=EC=9D=B8=EA=B8=B0?= =?UTF-8?q?=20=EC=83=81=ED=92=88=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/product/controller/ProductController.java | 3 +-- .../ongil/backend/domain/product/service/ProductService.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ongil/backend/domain/product/controller/ProductController.java b/src/main/java/com/ongil/backend/domain/product/controller/ProductController.java index 8aa1417..aa16668 100644 --- a/src/main/java/com/ongil/backend/domain/product/controller/ProductController.java +++ b/src/main/java/com/ongil/backend/domain/product/controller/ProductController.java @@ -150,10 +150,9 @@ public DataResponse getSizeGuide( ) @GetMapping("/recommend") public DataResponse> getRecommendedProducts( - @AuthenticationPrincipal Long userId, @RequestParam(defaultValue = "10") @Min(1) @Max(100) int size ) { - List products = productService.getRecommendedProducts(userId, size); + List products = productService.getRecommendedProducts(size); return DataResponse.from(products); } } \ No newline at end of file diff --git a/src/main/java/com/ongil/backend/domain/product/service/ProductService.java b/src/main/java/com/ongil/backend/domain/product/service/ProductService.java index 91f6b2e..0d9e5f2 100644 --- a/src/main/java/com/ongil/backend/domain/product/service/ProductService.java +++ b/src/main/java/com/ongil/backend/domain/product/service/ProductService.java @@ -382,7 +382,7 @@ private SizeGuideResponse buildResponseWithBodyInfoOnly(User user, Product produ * - 인기 상품 순(viewCount + cartCount) 반환 * - 특가 상품(SPECIAL_SALE) 제외 */ - public List getRecommendedProducts(Long userId, int size) { + public List getRecommendedProducts(int size) { Pageable pageable = PageRequest.of(0, size); List products = productRepository.findPopularProducts(pageable); return productConverter.toSimpleResponseList(products);