Skip to content

feat: 추천 상품 특가 제외 및 상품 없는 카테고리/브랜드 필터링#141

Merged
neibler merged 3 commits intodevelopfrom
feature/139
Feb 18, 2026
Merged

feat: 추천 상품 특가 제외 및 상품 없는 카테고리/브랜드 필터링#141
neibler merged 3 commits intodevelopfrom
feature/139

Conversation

@neibler
Copy link
Copy Markdown
Contributor

@neibler neibler commented Feb 18, 2026

🔍️ 작업 내용

  • Closes #
    feat: 추천 상품 특가 제외 및 상품 없는 카테고리/브랜드 필터링

✨ 상세 설명

🛠️ 추후 리팩토링 및 고도화 계획

📸 스크린샷 (선택)

💬 리뷰 요구사항

Summary by CodeRabbit

릴리스 노트

  • 버그 수정

    • 랜덤 브랜드 조회 시 판매 중인 상품이 있는 브랜드만 표시되도록 개선
    • 카테고리 및 서브카테고리 조회에서 판매 중인 상품이 없는 항목을 제외하도록 필터링
    • 추천 서브카테고리 목록에서도 판매 중인 상품이 없는 항목 제외
    • 랜덤 카테고리 조회 시 썸네일이 있는 항목만 선택하도록 강화
    • 특정 상품 유형(SPECIAL_SALE)을 추천/조회 결과에서 제외
  • 신규 기능

    • 카테고리 응답 생성 시 외부에서 서브카테고리 목록을 제공할 수 있는 옵션 추가

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 18, 2026

Warning

Rate limit exceeded

@neibler has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 56 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

판매 중(on_sale) 상품이 있는 카테고리·브랜드만 반환하도록 저장소 쿼리와 서비스 필터링을 강화했습니다. 또한 SPECIAL_SALE 타입 상품을 일부 쿼리에서 제외하고, CategoryConverter에 서브카테고리 리스트를 외부로부터 주입하는 오버로드를 추가했습니다.

Changes

Cohort / File(s) Summary
저장소 — 브랜드/상품 쿼리
src/main/java/com/ongil/backend/domain/brand/repository/BrandRepository.java, src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java
BrandRepository.findRandomBrands 쿼리를 EXISTS 서브쿼리로 변경해 on_sale 상품이 있는 브랜드만 선택. ProductRepository에 인기/추천 상품 쿼리에서 ProductType.SPECIAL_SALE 제외 조건 추가 및 on-sale 상품이 존재하는 카테고리 ID를 반환하는 findCategoryIdsWithOnSaleProducts() 메서드 추가.
서비스·변환 — 카테고리 필터링 및 변환
src/main/java/com/ongil/backend/domain/category/service/CategoryService.java, src/main/java/com/ongil/backend/domain/category/converter/CategoryConverter.java
CategoryService: getAllCategories/getRandomCategories/getRecommendedSubCategories가 on_sale 상품이 있는 서브카테고리만 포함하도록 로직 강화(부모 카테고리 제외 로직 포함). CategoryConverter: 외부에서 제공된 서브카테고리 리스트를 사용한 toResponse(Category, List<SubCategoryResponse>) 오버로드 추가.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #139: SPECIAL_SALE 제외 및 판매 중인 상품이 있는 카테고리/브랜드만 반환하도록 필터링하는 작업과 직접적으로 일치합니다.

Possibly related PRs

  • #27: CategoryConverter와 CategoryService 관련 변경사항(오버로드 및 필터링)과 코드 레벨에서 직접 연관됩니다.
  • #14: ProductRepository의 on-sale / special-sale 관련 쿼리 변경과 연관된 PR입니다.
  • #85: 제품·카테고리 관련 저장소/쿼리 로직을 수정한 PR로 코드 영향 범위가 겹칩니다.

Suggested labels

✨ Feature

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경사항의 핵심을 명확하게 요약합니다: 특가 제외 및 상품 없는 카테고리/브랜드 필터링이 정확히 코드 변경사항을 반영합니다.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/139

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/ongil/backend/domain/category/service/CategoryService.java (1)

100-109: ⚠️ Potential issue | 🟠 Major

캐시 없는 N+1 쿼리 — 매 요청마다 실행

getAllCategories와 달리 getRecommendedSubCategories는 Redis 캐시가 적용되어 있지 않습니다. 요청마다 findAllSubCategories()로 모든 하위 카테고리를 불러온 뒤, 각 서브카테고리에 대해 existsByCategoryIdAndOnSaleTrue를 개별 호출합니다. Java 스트림의 지연 평가 덕분에 limit(count) 이후에는 중단되지만, 앞쪽 하위 카테고리 대부분에 판매 중인 상품이 없을 경우 여전히 대다수 카테고리에 대해 DB 쿼리가 실행됩니다.

getAllCategories와 동일하게, 판매 중인 상품을 가진 카테고리 ID를 단일 쿼리로 일괄 조회하여 Set 기반 필터링으로 교체하거나, 이 메서드 결과도 적절한 TTL로 캐싱하는 것을 권장합니다.

 public List<CategorySimpleResponse> getRecommendedSubCategories(int count) {
     List<Category> subCategories = categoryRepository.findAllSubCategories();
+    Set<Long> activeCategoryIds = productRepository.findCategoryIdsWithOnSaleProducts();

     return subCategories.stream()
-        .filter(category -> productRepository.existsByCategoryIdAndOnSaleTrue(category.getId()))
+        .filter(category -> activeCategoryIds.contains(category.getId()))
         .limit(count)
         .map(categoryConverter::toSimpleResponse)
         .collect(Collectors.toList());
 }
🤖 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/category/service/CategoryService.java`
around lines 100 - 109, getRecommendedSubCategories suffers from N+1 queries
because it calls findAllSubCategories() then
existsByCategoryIdAndOnSaleTrue(...) per category; replace the per-category
existence checks with a single batched query (e.g., add/use a repository method
like productRepository.findDistinctCategoryIdsByOnSaleTrue() to return a
Set<Long>) and then filter the subCategories stream by set membership before
limit/count, or alternatively cache the resulting recommended subcategory
IDs/result with a TTL similar to getAllCategories; update
getRecommendedSubCategories to use the batched Set lookup (and/or caching)
instead of calling existsByCategoryIdAndOnSaleTrue for each category.
🧹 Nitpick comments (3)
src/main/java/com/ongil/backend/domain/brand/repository/BrandRepository.java (2)

18-18: LIMIT 3 하드코딩 대신 상수로 분리하는 것을 고려하세요.

같은 값이 여러 쿼리(카테고리, 브랜드 등)에 분산될 경우 유지보수가 어려워집니다.

🤖 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/brand/repository/BrandRepository.java`
at line 18, The native query currently hardcodes "LIMIT 3"; extract that magic
number into a compile-time constant (e.g., static final String RANDOM_LIMIT =
"LIMIT 3" or a shared int constant in a RepositoryConstants class) and replace
"LIMIT 3" with that constant in the `@Query` annotation in BrandRepository (and
other repositories like category/brand where the same limit appears) so updates
are centralized; if the constant is used across multiple repositories place it
in a shared constants class (e.g., RepositoryConstants.NUM_RANDOM_RESULTS) and
reference that symbol from each repository.

16-18: ORDER BY RAND()는 대용량 테이블에서 성능 문제를 유발하며, EXISTS 서브쿼리에 인덱스가 필요합니다.

  • ORDER BY RAND()는 전체 행을 임시 테이블에 올린 뒤 정렬하므로 O(n log n) 비용이 발생합니다. 브랜드 수가 적은 지금은 큰 문제가 없으나, 규모가 커질 경우 병목이 됩니다.
  • products 테이블에 (brand_id, on_sale) 복합 인덱스가 없다면 EXISTS 서브쿼리마다 풀 스캔이 발생합니다.

DB 마이그레이션 스크립트에 아래 인덱스 추가를 권장합니다:

CREATE INDEX idx_products_brand_on_sale ON products (brand_id, on_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/brand/repository/BrandRepository.java`
around lines 16 - 18, The native query in BrandRepository uses ORDER BY RAND()
which will not scale and the EXISTS subquery needs an index; add a composite
index on products(brand_id, on_sale) (e.g., idx_products_brand_on_sale) to avoid
full scans, and replace ORDER BY RAND() with a scalable random-selection
strategy (for example, sample by random id ranges, use DB-supported
TABLESAMPLE/BERNOULLI, or precomputed random values and then join/filter to pick
3 brands) in the query used by BrandRepository to avoid sorting the entire
result set.
src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java (1)

199-226: JPQL에 완전 한정 열거형(FQCN) 하드코딩 → @Param 파라미터화 권장

현재 쿼리 문자열에 com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE를 직접 삽입하고 있습니다. 패키지 경로가 변경될 경우 런타임까지 오류가 드러나지 않으며, 이미 파일 상단에 ProductType import도 있어 파라미터 방식이 더 일관성 있습니다.

♻️ 파라미터 방식으로 변경하는 예시
 `@Query`("SELECT p FROM Product p " +
   "WHERE p.onSale = true " +
-  "AND p.productType <> com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE " +
+  "AND p.productType <> :excludedType " +
   "ORDER BY (COALESCE(p.viewCount, 0) + COALESCE(p.cartCount, 0)) DESC")
-List<Product> findPopularProducts(Pageable pageable);
+List<Product> findPopularProducts(
+    `@Param`("excludedType") ProductType excludedType,
+    Pageable pageable);

호출 측에서 productRepository.findPopularProducts(ProductType.SPECIAL_SALE, pageable) 처럼 전달하면 됩니다. findRecommendedProducts도 동일하게 적용합니다.

🤖 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/repository/ProductRepository.java`
around lines 199 - 226, The JPQL currently hardcodes the FQCN
com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE in
ProductRepository queries; change both findPopularProducts and
findRecommendedProducts to accept a ProductType parameter and use a named JPQL
parameter (e.g. :excludedProductType) instead of the FQCN, add
`@Param`("excludedProductType") ProductType excludedProductType to the method
signatures, update the `@Query` strings to reference :excludedProductType in the
productType <> comparison, and update call sites to pass
ProductType.SPECIAL_SALE when invoking these repository methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/ongil/backend/domain/brand/repository/BrandRepository.java`:
- Line 17: The EXISTS subquery in BrandRepository that checks for products on
sale only uses "p.on_sale = true" and misses excluding SPECIAL_SALE items;
update the EXISTS clause in the repository query used for recommended brands to
also exclude products with type SPECIAL_SALE (e.g., add "AND p.type !=
'SPECIAL_SALE'" or the equivalent enum/constant reference) so it matches the
behavior of findPopularProducts/findProductsByCategory and prevents brands with
only SPECIAL_SALE items from appearing in recommendations.

In
`@src/main/java/com/ongil/backend/domain/category/service/CategoryService.java`:
- Around line 49-64: The current code calls
productRepository.existsByCategoryIdAndOnSaleTrue for each subcategory causing
an N+M query storm; instead batch-fetch the set of category IDs that have
on-sale products in one query (e.g. add a repository method like
findDistinctCategoryIdsByCategoryIdInAndOnSaleTrue(Collection<Long>) returning
List<Long>), build a Set<Long> availableCategoryIds, then stream
parentCategories and for each parent filter its getSubCategories() by
availableCategoryIds (map each to categoryConverter.toSubCategoryResponse) and
only collect parents with non-empty filteredSubs; also replace the map(...
return null ...).filter(...) pattern with flatMap (or filter + map) to avoid
returning nulls — reference parentCategories,
productRepository.existsByCategoryIdAndOnSaleTrue,
categoryConverter.toSubCategoryResponse, and categoryConverter.toResponse when
making the change.

---

Outside diff comments:
In
`@src/main/java/com/ongil/backend/domain/category/service/CategoryService.java`:
- Around line 100-109: getRecommendedSubCategories suffers from N+1 queries
because it calls findAllSubCategories() then
existsByCategoryIdAndOnSaleTrue(...) per category; replace the per-category
existence checks with a single batched query (e.g., add/use a repository method
like productRepository.findDistinctCategoryIdsByOnSaleTrue() to return a
Set<Long>) and then filter the subCategories stream by set membership before
limit/count, or alternatively cache the resulting recommended subcategory
IDs/result with a TTL similar to getAllCategories; update
getRecommendedSubCategories to use the batched Set lookup (and/or caching)
instead of calling existsByCategoryIdAndOnSaleTrue for each category.

---

Nitpick comments:
In
`@src/main/java/com/ongil/backend/domain/brand/repository/BrandRepository.java`:
- Line 18: The native query currently hardcodes "LIMIT 3"; extract that magic
number into a compile-time constant (e.g., static final String RANDOM_LIMIT =
"LIMIT 3" or a shared int constant in a RepositoryConstants class) and replace
"LIMIT 3" with that constant in the `@Query` annotation in BrandRepository (and
other repositories like category/brand where the same limit appears) so updates
are centralized; if the constant is used across multiple repositories place it
in a shared constants class (e.g., RepositoryConstants.NUM_RANDOM_RESULTS) and
reference that symbol from each repository.
- Around line 16-18: The native query in BrandRepository uses ORDER BY RAND()
which will not scale and the EXISTS subquery needs an index; add a composite
index on products(brand_id, on_sale) (e.g., idx_products_brand_on_sale) to avoid
full scans, and replace ORDER BY RAND() with a scalable random-selection
strategy (for example, sample by random id ranges, use DB-supported
TABLESAMPLE/BERNOULLI, or precomputed random values and then join/filter to pick
3 brands) in the query used by BrandRepository to avoid sorting the entire
result set.

In
`@src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java`:
- Around line 199-226: The JPQL currently hardcodes the FQCN
com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE in
ProductRepository queries; change both findPopularProducts and
findRecommendedProducts to accept a ProductType parameter and use a named JPQL
parameter (e.g. :excludedProductType) instead of the FQCN, add
`@Param`("excludedProductType") ProductType excludedProductType to the method
signatures, update the `@Query` strings to reference :excludedProductType in the
productType <> comparison, and update call sites to pass
ProductType.SPECIAL_SALE when invoking these repository methods.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/main/java/com/ongil/backend/domain/category/service/CategoryService.java (1)

107-113: getRecommendedSubCategories — 매번 동일한 결과 반환 가능성

findAllSubCategories()의 기본 정렬 순서 그대로 filterlimit이 적용되므로, activeCategoryIds 필터링 후에도 항상 같은 count개의 카테고리가 반환될 수 있습니다. "추천" 의미에 맞게 사전에 Collections.shuffle()을 적용하거나, 저장소 쿼리에 랜덤 정렬을 추가하는 것을 검토해보세요.

🤖 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/category/service/CategoryService.java`
around lines 107 - 113, getRecommendedSubCategories currently applies filter →
limit on the deterministic list returned by findAllSubCategories(), so the same
top N items may be returned every time; to make recommendations
non-deterministic, shuffle or randomize the subCategories collection before
filtering/limiting (or adjust the repository query to return a randomized
order). Locate getRecommendedSubCategories and before applying filter/limit on
subCategories (the result of findAllSubCategories()), call
Collections.shuffle(subCategories) or apply a repository-level random order,
then proceed to filter by activeCategoryIds (from
productRepository.findCategoryIdsWithOnSaleProducts()) and map with
categoryConverter::toSimpleResponse so results vary across calls.
src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java (1)

199-226: [선택적 개선] JPQL 인라인 FQCN → 바인딩 파라미터로 대체 가능

두 쿼리 모두 com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE을 쿼리 문자열에 직접 포함합니다. @Param으로 바인딩하면 FQCN 중복 없이 타입 안전하게 처리할 수 있습니다.

♻️ 리팩토링 예시 (findPopularProducts 기준)
 `@Query`("SELECT p FROM Product p " +
     "WHERE p.onSale = true " +
-    "AND p.productType <> com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE " +
+    "AND p.productType <> :excludeType " +
     "ORDER BY (COALESCE(p.viewCount, 0) + COALESCE(p.cartCount, 0)) DESC")
-List<Product> findPopularProducts(Pageable pageable);
+List<Product> findPopularProducts(
+    `@Param`("excludeType") ProductType excludeType,
+    Pageable pageable
+);

호출부: productRepository.findPopularProducts(ProductType.SPECIAL_SALE, pageable)

🤖 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/repository/ProductRepository.java`
around lines 199 - 226, Both JPQL queries in findPopularProducts and
findRecommendedProducts inline the fully-qualified enum constant
com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE; change these to
use a bind parameter to avoid FQCN duplication and improve type-safety: update
the JPQL to replace the literal with "AND p.productType <> :excludedType", add a
parameter of type ProductType (annotated with `@Param`("excludedType")) to both
repository methods (findPopularProducts and findRecommendedProducts), and update
any call sites to pass ProductType.SPECIAL_SALE when invoking these methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/ongil/backend/domain/category/service/CategoryService.java`:
- Around line 93-101: The loop in getRandomCategories triggers N+1 DB calls
because getTopProductThumbnail() calls findTopByCategoryIdOrderByPopularity()
per category; instead, pre-filter shuffledCategories using the same
activeCategoryIds obtained from findCategoryIdsWithOnSaleProducts() and
batch-fetch top thumbnails for the candidate category IDs (limit to count + a
small buffer), e.g., add a repository method like
findTopThumbnailsByCategoryIds(List<Long> categoryIds) and call it once for the
selected category IDs, then map thumbnails by categoryId and use that map inside
the existing loop (referencing getRandomCategories, getTopProductThumbnail,
shuffledCategories, count, activeCategoryIds,
categoryConverter.toRandomResponse) so you only perform ~1 query for thumbnails
rather than one per category.

---

Duplicate comments:
In
`@src/main/java/com/ongil/backend/domain/category/service/CategoryService.java`:
- Around line 53-68: The stream currently maps parentCategories to null for
parents without any active subcategories and then filters out nulls—replace this
anti-pattern by using flatMap: for each parent in the parentCategories stream,
build the filteredSubs list (using
parent.getSubCategories().stream().filter(...).map(categoryConverter::toSubCategoryResponse).collect(...))
and then return either Stream.empty() when filteredSubs.isEmpty() or
Stream.of(categoryConverter.toResponse(parent, filteredSubs)); this removes the
need to return null and the subsequent .filter(r -> r != null) while preserving
use of activeCategoryIds, categoryConverter.toSubCategoryResponse and
categoryConverter.toResponse.

---

Nitpick comments:
In
`@src/main/java/com/ongil/backend/domain/category/service/CategoryService.java`:
- Around line 107-113: getRecommendedSubCategories currently applies filter →
limit on the deterministic list returned by findAllSubCategories(), so the same
top N items may be returned every time; to make recommendations
non-deterministic, shuffle or randomize the subCategories collection before
filtering/limiting (or adjust the repository query to return a randomized
order). Locate getRecommendedSubCategories and before applying filter/limit on
subCategories (the result of findAllSubCategories()), call
Collections.shuffle(subCategories) or apply a repository-level random order,
then proceed to filter by activeCategoryIds (from
productRepository.findCategoryIdsWithOnSaleProducts()) and map with
categoryConverter::toSimpleResponse so results vary across calls.

In
`@src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java`:
- Around line 199-226: Both JPQL queries in findPopularProducts and
findRecommendedProducts inline the fully-qualified enum constant
com.ongil.backend.domain.product.enums.ProductType.SPECIAL_SALE; change these to
use a bind parameter to avoid FQCN duplication and improve type-safety: update
the JPQL to replace the literal with "AND p.productType <> :excludedType", add a
parameter of type ProductType (annotated with `@Param`("excludedType")) to both
repository methods (findPopularProducts and findRecommendedProducts), and update
any call sites to pass ProductType.SPECIAL_SALE when invoking these methods.

Comment on lines +93 to +101
List<CategoryRandomResponse> result = new ArrayList<>();
for (Category category : shuffledCategories) {
if (result.size() >= count) break;
String thumbnailUrl = getTopProductThumbnail(category);
if (thumbnailUrl != null) {
result.add(categoryConverter.toRandomResponse(category, thumbnailUrl));
}
}
return result;
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 | 🟠 Major

getRandomCategories 루프 내 N+1 — 카테고리당 DB 쿼리 1회 발생

getTopProductThumbnail()이 내부적으로 findTopByCategoryIdOrderByPopularity()를 호출하므로, 최악의 경우 전체 카테고리 수만큼 SELECT가 실행됩니다. getAllCategories()에서는 findCategoryIdsWithOnSaleProducts()로 N+1을 해결했지만, 이 메서드에는 동일한 사전 필터링이 적용되지 않았습니다.

activeCategoryIds로 사전 필터링하면 실제 DB 호출을 필요한 count개 근처로 줄일 수 있습니다.

🔧 수정 예시
 public List<CategoryRandomResponse> getRandomCategories(int count) {
     List<Category> allCategories = categoryRepository.findAllByOrderByDisplayOrder();
+    Set<Long> activeCategoryIds = new HashSet<>(productRepository.findCategoryIdsWithOnSaleProducts());

     List<Category> shuffledCategories = new ArrayList<>(allCategories);
     Collections.shuffle(shuffledCategories);

     List<CategoryRandomResponse> result = new ArrayList<>();
     for (Category category : shuffledCategories) {
         if (result.size() >= count) break;
+        // 판매 중인 상품이 없는 카테고리는 DB 조회 없이 건너뜀
+        Long targetId = category.getParentCategory() == null
+            ? (category.getSubCategories().isEmpty() ? null : category.getSubCategories().get(0).getId())
+            : category.getId();
+        if (targetId == null || !activeCategoryIds.contains(targetId)) continue;
         String thumbnailUrl = getTopProductThumbnail(category);
         if (thumbnailUrl != null) {
             result.add(categoryConverter.toRandomResponse(category, thumbnailUrl));
         }
     }
     return result;
 }
🤖 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/category/service/CategoryService.java`
around lines 93 - 101, The loop in getRandomCategories triggers N+1 DB calls
because getTopProductThumbnail() calls findTopByCategoryIdOrderByPopularity()
per category; instead, pre-filter shuffledCategories using the same
activeCategoryIds obtained from findCategoryIdsWithOnSaleProducts() and
batch-fetch top thumbnails for the candidate category IDs (limit to count + a
small buffer), e.g., add a repository method like
findTopThumbnailsByCategoryIds(List<Long> categoryIds) and call it once for the
selected category IDs, then map thumbnails by categoryId and use that map inside
the existing loop (referencing getRandomCategories, getTopProductThumbnail,
shuffledCategories, count, activeCategoryIds,
categoryConverter.toRandomResponse) so you only perform ~1 query for thumbnails
rather than one per category.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant