Skip to content

Commit e491e57

Browse files
authored
Merge pull request #141 from IT-Cotato/feature/139
feat: 추천 상품 특가 제외 및 상품 없는 카테고리/브랜드 필터링
2 parents 199679b + f6f6815 commit e491e57

File tree

4 files changed

+72
-14
lines changed

4 files changed

+72
-14
lines changed

src/main/java/com/ongil/backend/domain/brand/repository/BrandRepository.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public interface BrandRepository extends JpaRepository<Brand, Long> {
1313
@Query("SELECT b FROM Brand b ORDER BY b.name ASC")
1414
List<Brand> findAllOrderByName();
1515

16-
@Query(value = "SELECT * FROM brands ORDER BY RAND() LIMIT 3", nativeQuery = true)
16+
@Query(value = "SELECT b.* FROM brands b " +
17+
"WHERE EXISTS (SELECT 1 FROM products p WHERE p.brand_id = b.id AND p.on_sale = true AND p.product_type <> 'SPECIAL_SALE') " +
18+
"ORDER BY RAND() LIMIT 3", nativeQuery = true)
1719
List<Brand> findRandomBrands();
1820
}

src/main/java/com/ongil/backend/domain/category/converter/CategoryConverter.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ public CategoryResponse toResponse(Category category) {
3030
.build();
3131
}
3232

33+
/**
34+
* Category → CategoryResponse (필터링된 하위 카테고리 목록 지정)
35+
*/
36+
public CategoryResponse toResponse(Category category, List<SubCategoryResponse> subCategories) {
37+
return CategoryResponse.builder()
38+
.categoryId(category.getId())
39+
.name(category.getName())
40+
.iconUrl(category.getIconUrl())
41+
.displayOrder(category.getDisplayOrder())
42+
.subCategories(subCategories)
43+
.build();
44+
}
45+
3346
public List<CategoryResponse> toResponseList(List<Category> categories) {
3447
return categories.stream()
3548
.map(this::toResponse)

src/main/java/com/ongil/backend/domain/category/service/CategoryService.java

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import java.util.ArrayList;
44
import java.util.Collections;
5+
import java.util.HashSet;
56
import java.util.List;
7+
import java.util.Set;
68
import java.util.stream.Collectors;
79

810
import org.springframework.stereotype.Service;
@@ -32,7 +34,7 @@ public class CategoryService {
3234
private final CategoryConverter categoryConverter;
3335
private final RedisCacheService redisCacheService;
3436

35-
// 모든 카테고리 조회 (상위 + 하위)
37+
// 모든 카테고리 조회 (상위 + 하위, 상품이 있는 카테고리만)
3638
public List<CategoryResponse> getAllCategories() {
3739
// Redis 캐시 확인
3840
List<CategoryResponse> cached = redisCacheService.getList(
@@ -46,7 +48,24 @@ public List<CategoryResponse> getAllCategories() {
4648

4749
// Cache Miss → DB 조회
4850
List<Category> parentCategories = categoryRepository.findAllParentCategoriesWithSub();
49-
List<CategoryResponse> response = categoryConverter.toResponseList(parentCategories);
51+
Set<Long> activeCategoryIds = new HashSet<>(productRepository.findCategoryIdsWithOnSaleProducts());
52+
53+
List<CategoryResponse> response = parentCategories.stream()
54+
.map(parent -> {
55+
// 판매 중인 상품이 있는 하위 카테고리만 필터링
56+
List<SubCategoryResponse> filteredSubs = parent.getSubCategories().stream()
57+
.filter(sub -> activeCategoryIds.contains(sub.getId()))
58+
.map(categoryConverter::toSubCategoryResponse)
59+
.collect(Collectors.toList());
60+
61+
if (filteredSubs.isEmpty()) {
62+
return null; // 하위 카테고리에 상품이 전부 없으면 상위 카테고리도 제외
63+
}
64+
65+
return categoryConverter.toResponse(parent, filteredSubs);
66+
})
67+
.filter(r -> r != null)
68+
.collect(Collectors.toList());
5069

5170
// Redis 캐싱 (무한 TTL)
5271
redisCacheService.save(
@@ -64,27 +83,45 @@ public List<SubCategoryResponse> getSubCategories(Long parentCategoryId) {
6483
return categoryConverter.toSubCategoryResponseList(subCategories);
6584
}
6685

67-
// 랜덤 카테고리 조회
86+
// 랜덤 카테고리 조회 (상품이 있는 카테고리만)
6887
public List<CategoryRandomResponse> getRandomCategories(int count) {
6988
List<Category> allCategories = categoryRepository.findAllByOrderByDisplayOrder();
70-
71-
List<Category> shuffledCategories = new ArrayList<>(allCategories);
72-
Collections.shuffle(shuffledCategories);
73-
74-
return shuffledCategories.stream()
75-
.limit(count)
76-
.map(category -> {
77-
String thumbnailUrl = getTopProductThumbnail(category);
78-
return categoryConverter.toRandomResponse(category, thumbnailUrl);
89+
Set<Long> activeCategoryIds = new HashSet<>(productRepository.findCategoryIdsWithOnSaleProducts());
90+
91+
// 상품이 있는 카테고리만 사전 필터링 (상위 카테고리는 하위 중 하나라도 있으면 포함)
92+
List<Category> activeCategories = allCategories.stream()
93+
.filter(category -> {
94+
if (category.getParentCategory() != null) {
95+
// 하위 카테고리: 직접 확인
96+
return activeCategoryIds.contains(category.getId());
97+
}
98+
// 상위 카테고리: 하위 카테고리 중 하나라도 상품이 있으면 포함
99+
return category.getSubCategories().stream()
100+
.anyMatch(sub -> activeCategoryIds.contains(sub.getId()));
79101
})
80102
.collect(Collectors.toList());
103+
104+
Collections.shuffle(activeCategories);
105+
106+
// 사전 필터링된 카테고리만 썸네일 조회 (DB 호출 최소화)
107+
List<CategoryRandomResponse> result = new ArrayList<>();
108+
for (Category category : activeCategories) {
109+
if (result.size() >= count) break;
110+
String thumbnailUrl = getTopProductThumbnail(category);
111+
if (thumbnailUrl != null) {
112+
result.add(categoryConverter.toRandomResponse(category, thumbnailUrl));
113+
}
114+
}
115+
return result;
81116
}
82117

83-
// 추천 하위 카테고리 조회
118+
// 추천 하위 카테고리 조회 (상품이 있는 하위 카테고리만)
84119
public List<CategorySimpleResponse> getRecommendedSubCategories(int count) {
85120
List<Category> subCategories = categoryRepository.findAllSubCategories();
121+
Set<Long> activeCategoryIds = new HashSet<>(productRepository.findCategoryIdsWithOnSaleProducts());
86122

87123
return subCategories.stream()
124+
.filter(category -> activeCategoryIds.contains(category.getId()))
88125
.limit(count)
89126
.map(categoryConverter::toSimpleResponse)
90127
.collect(Collectors.toList());

src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ List<Object[]> findSimilarCustomersPurchases(
198198
@EntityGraph(attributePaths = {"brand", "category"})
199199
@Query("SELECT p FROM Product p " +
200200
"WHERE p.onSale = true " +
201+
"AND p.productType <> com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE " +
201202
"ORDER BY (COALESCE(p.viewCount, 0) + COALESCE(p.cartCount, 0)) DESC")
202203
List<Product> findPopularProducts(Pageable pageable);
203204

@@ -208,6 +209,7 @@ List<Object[]> findSimilarCustomersPurchases(
208209
@EntityGraph(attributePaths = {"brand", "category"})
209210
@Query("SELECT p FROM Product p " +
210211
"WHERE p.onSale = true " +
212+
"AND p.productType <> com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE " +
211213
"AND p.category.id IN :categoryIds " +
212214
"AND p.id NOT IN :excludeProductIds " +
213215
"AND (" +
@@ -235,4 +237,8 @@ List<Product> findRecommendedProducts(
235237

236238
// 특정 카테고리를 사용하는 상품이 있는지 확인
237239
boolean existsByCategoryId(Long categoryId);
240+
241+
// 판매 중인 상품이 존재하는 카테고리 ID 목록 일괄 조회
242+
@Query("SELECT DISTINCT p.category.id FROM Product p WHERE p.onSale = true")
243+
List<Long> findCategoryIdsWithOnSaleProducts();
238244
}

0 commit comments

Comments
 (0)