Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,9 @@ public DataResponse<SizeGuideResponse> getSizeGuide(
)
@GetMapping("/recommend")
public DataResponse<List<ProductSimpleResponse>> getRecommendedProducts(
@AuthenticationPrincipal Long userId,
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int size
) {
List<ProductSimpleResponse> products = productService.getRecommendedProducts(userId, size);
List<ProductSimpleResponse> products = productService.getRecommendedProducts(size);
return DataResponse.from(products);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,36 +218,6 @@ List<Object[]> findSimilarCustomersPurchases(
"ORDER BY (COALESCE(p.viewCount, 0) + COALESCE(p.cartCount, 0)) DESC")
List<Product> 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<Product> findRecommendedProducts(
@Param("categoryIds") List<Long> categoryIds,
@Param("minPrice") Integer minPrice,
@Param("maxPrice") Integer maxPrice,
@Param("excludeProductIds") List<Long> excludeProductIds,
Pageable pageable
);

/**
* 상품 ID 목록으로 상품 조회
*/
@EntityGraph(attributePaths = {"brand", "category"})
@Query("SELECT p FROM Product p WHERE p.id IN :productIds AND p.onSale = true")
List<Product> findByIdInAndOnSaleTrue(@Param("productIds") List<Long> productIds);

// 특정 브랜드를 사용하는 상품이 있는지 확인
boolean existsByBrandId(Long brandId);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -388,113 +379,12 @@ private SizeGuideResponse buildResponseWithBodyInfoOnly(User user, Product produ

/**
* 홈화면 추천 상품 조회
* - 로그인: 최근 30일 조회/장바구니 기준 같은 카테고리 + 비슷한 가격(±10,000원) 필터 적용
* - 비로그인: 전체 인기 상품
* - 정렬: 전체 고객 기준 viewCount + cartCount 순
* - 인기 상품 순(viewCount + cartCount) 반환
* - 특가 상품(SPECIAL_SALE) 제외
Comment on lines +382 to +383
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Javadoc 주석에서 cartCount 참조 제거 필요

cartCount는 이미 DTO/응답에서 제거된 필드인데 주석에 "viewCount + cartCount"가 여전히 명시되어 있어 혼란을 줄 수 있습니다.

📝 주석 수정 제안
-	 * - 인기 상품 순(viewCount + cartCount) 반환
+	 * - 인기 상품 순(viewCount 기반) 반환

Based on learnings: cartCount 필드는 프론트엔드에서 사용하지 않아 이미 제거된 상태입니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* - 인기 상품 (viewCount + cartCount) 반환
* - 특가 상품(SPECIAL_SALE) 제외
* - 인기 상품 (viewCount 기반) 반환
* - 특가 상품(SPECIAL_SALE) 제외
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/ongil/backend/domain/product/service/ProductService.java`
around lines 382 - 383, Update the Javadoc in ProductService that currently
reads "인기 상품 순(viewCount + cartCount) 반환 - 특가 상품(SPECIAL_SALE) 제외" to remove any
reference to cartCount (since the DTO/response no longer includes it); change
the description to indicate popularity is determined by viewCount only and keep
the note about excluding SPECIAL_SALE, and ensure the updated comment sits above
the corresponding method in ProductService that returns popular products.

*/
public List<ProductSimpleResponse> getRecommendedProducts(Long userId, int size) {
if (userId == null) {
return getPopularProducts(size);
}
return getPersonalizedRecommendations(userId, size);
}

/**
* 인기 상품 조회
*/
private List<ProductSimpleResponse> getPopularProducts(int size) {
public List<ProductSimpleResponse> getRecommendedProducts(int size) {
Pageable pageable = PageRequest.of(0, size);
List<Product> products = productRepository.findPopularProducts(pageable);
return productConverter.toSimpleResponseList(products);
}

/**
* 개인화 추천 (로그인 사용자)
*/
private List<ProductSimpleResponse> getPersonalizedRecommendations(Long userId, int size) {
LocalDateTime since = LocalDateTime.now().minusDays(DAYS_TO_LOOK_BACK);

// 1. 최근 30일간 조회한 상품 ID
List<Long> viewedProductIds = productViewHistoryRepository
.findDistinctProductIdsByUserIdAndCreatedAtAfter(userId, since);

// 2. 장바구니에 담은 상품 ID
List<Long> cartProductIds = cartRepository.findProductIdsByUserId(userId);

// 3. 기준 상품 ID 합치기
Set<Long> baseProductIds = new HashSet<>();
baseProductIds.addAll(viewedProductIds);
baseProductIds.addAll(cartProductIds);

// 기록이 없으면 인기 상품 반환 (신규 사용자)
if (baseProductIds.isEmpty()) {
return getPopularProducts(size);
}

// 4. 구매한 상품 ID (제외 대상)
List<Long> purchasedProductIds = orderItemRepository.findProductIdsByUserId(userId);

// 5. 기준 상품들 조회
List<Product> baseProducts = productRepository.findByIdInAndOnSaleTrue(
new ArrayList<>(baseProductIds)
);

if (baseProducts.isEmpty()) {
return getPopularProducts(size);
}

// 6. 필터 조건 계산
List<Long> 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<Long> excludeIds = new HashSet<>(baseProductIds);
excludeIds.addAll(purchasedProductIds);
if (excludeIds.isEmpty()) {
excludeIds.add(-1L);
}

// 8. 추천 상품 조회 (카테고리 + 가격 필터, 인기순 정렬)
Pageable pageable = PageRequest.of(0, size);
List<Product> recommendations = productRepository.findRecommendedProducts(
categoryIds,
minPrice,
maxPrice,
new ArrayList<>(excludeIds),
pageable
);

// 9. 부족하면 인기 상품으로 채우기 (제외 대상을 고려해 충분히 조회)
if (recommendations.size() < size) {
Set<Long> foundIds = recommendations.stream()
.map(Product::getId)
.collect(Collectors.toSet());
excludeIds.addAll(foundIds);

int needed = size - recommendations.size();
int fetchSize = needed + excludeIds.size();
List<Product> 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);
}
}