Skip to content

[Feat] 상품 목록 조회(정렬), 특가 상품 목록 조회(10개), 비슷한 상품 목록(6개)#14

Merged
kangcheolung merged 6 commits intodevelopfrom
feature/13
Jan 10, 2026
Merged

[Feat] 상품 목록 조회(정렬), 특가 상품 목록 조회(10개), 비슷한 상품 목록(6개)#14
kangcheolung merged 6 commits intodevelopfrom
feature/13

Conversation

@kangcheolung
Copy link
Copy Markdown
Member

@kangcheolung kangcheolung commented Jan 10, 2026

🔍️ 작업 내용

  • Closes #
    상품 목록 (페이징, 필터, 정렬), 특가 상품 목록(할인율 높은 순으로 10개 정렬), 비슷한 상품 6개(같은 카테고리 + 비슷한 가격대)

✨ 상세 설명

  • 상품 목록 정렬 부분은 createSort 메서드로 인기순, 리뷰순, 가격 높은 순, 가격 낮은 순으로 정렬이 가능하게 하고 만약 같을 경우를 생각해 인기순이 같을 경우는 id순으로 리뷰, 가격 높은순, 가격 낮은 순이 같을 경우 인기순을 2순위로 했습니다.
  • 비슷한 상품과 특가 상품 목록 조회의 경우 각각 6개와 10개로 한정되있어 Page가 아닌 list로 반환함.
  • 모두 N+1 문제 방지를 위해 @EntityGraph 사용

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

프론트와 api 연동 후 이상한 부분 있을시 수정
redis 사용 여부 생각 후 필요시 추가

📸 스크린샷 (선택)

💬 리뷰 요구사항

Summary by CodeRabbit

  • 새로운 기능

    • 상품 검색 및 필터링(카테고리·브랜드·가격·사이즈) 및 페이지네이션 지원
    • 정렬 옵션 추가(인기순·리뷰순·가격 오름/내림)
    • 특가 상품 목록(상위 할인상품) 조회
    • 유사 상품 추천(동일 카테고리·가격대) 조회
    • 상품 목차에 리뷰 수 및 인기도 지표 노출
  • Chores

    • 로컬 설정의 JPA 스키마 모드 업데이트(생성→갱신)

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 10, 2026

Walkthrough

Product 엔티티에 reviewCount@Formula 기반 popularity 필드를 추가하고, 제품 검색/정렬/유사상품/특가 조회용 DTO·엔드포인트·서비스·레포지토리 메서드를 도입하고 보안 구성에서 제품/브랜드/카테고리 경로를 공개 처리했습니다.

Changes

Cohort / File(s) 변경 요약
엔티티 확장
src/main/java/com/ongil/backend/domain/product/entity/Product.java
reviewCount 필드 추가 (@Column(name = "review_count")), popularity 필드 추가 (@Formula("coalesce(view_count, 0) + coalesce(purchase_count, 0)"))
정렬 타입 추가
src/main/java/com/ongil/backend/domain/product/enums/ProductSortType.java
POPULAR, REVIEW, PRICE_HIGH, PRICE_LOW 열거형 추가 (설명 문자열 포함)
레포지토리 확장
src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java
findAllByCondition(...), findByOnSaleTrueAndProductTypeOrderByDiscountRateDesc(...), findByOnSaleTrueAndCategoryIdAndIdNotAndPriceBetween(...) 메서드 추가; @EntityGraph 적용
컨트롤러·보안 변경
src/main/java/com/ongil/backend/product/controller/ProductController.java, src/main/java/com/ongil/backend/global/config/SecurityConfig.java
GET /api/products(필터/정렬/페이징), /api/products/special-sale, /api/products/{id}/similar 엔드포인트 추가; /api/products/**, /api/brands/**, /api/categories/** 공개 처리
DTO 및 변환기
src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java, src/main/java/com/ongil/backend/product/dto/response/ProductSimpleResponse.java, src/main/java/com/ongil/backend/product/dto/converter/ProductConverter.java
ProductSearchCondition 추가 (priceRange 파싱 메서드 포함); ProductSimpleResponsereviewCount 추가; 변환기에서 reviewCount 매핑 추가
서비스 로직
src/main/java/com/ongil/backend/product/service/ProductService.java
getProducts(...), getSpecialSaleProducts(), getSimilarProducts(Long) 추가; 정렬 생성(createSort) 및 가격 범위 처리 로직 도입; 클래스 레벨 @Transactional(readOnly = true) 적용
설정 변경
src/main/resources/application-local.yml
Hibernate DDL 모드 createupdate 변경

Sequence Diagram

sequenceDiagram
    actor Client
    participant Controller as ProductController
    participant Service as ProductService
    participant Repository as ProductRepository
    participant DB as Database

    Client->>Controller: GET /api/products?categoryId=...&sortType=POPULAR&page=...
    Controller->>Service: getProducts(condition, sortType, pageable)
    Service->>Service: parsePriceRange(), createSort()
    Service->>Repository: findAllByCondition(..., pageable)
    Repository->>DB: Execute SQL (with EntityGraph)
    DB-->>Repository: Product page
    Repository-->>Service: Page<Product>
    Service->>Service: Convert to ProductSimpleResponse (include reviewCount)
    Service-->>Controller: Page<ProductSimpleResponse>
    Controller-->>Client: DataResponse(Page<ProductSimpleResponse>)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

✨ Feature

Suggested reviewers

  • neibler
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 이번 변경 사항의 핵심 기능들을 명확하게 요약하고 있으며, 상품 목록 조회(정렬), 특가 상품 목록, 비슷한 상품 목록 조회라는 세 가지 주요 기능을 정확하게 반영하고 있습니다.

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

✨ Finishing touches
  • 📝 Generate docstrings

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: 6

🤖 Fix all issues with AI agents
In @src/main/java/com/ongil/backend/domain/product/entity/Product.java:
- Around line 73-77: You added reviewCount as a non-nullable column and defined
popularity with @Formula, which can break runtime/schema validation if existing
rows have NULL review_count or if view_count/purchase_count are NULL; create a
DB migration that backfills existing rows setting review_count = 0 and adds the
new column with DEFAULT 0 and NOT NULL, and update the @Formula on the
popularity field to be null-safe by using COALESCE for view_count and
purchase_count (e.g., COALESCE(view_count, 0) + COALESCE(purchase_count, 0)) so
popularity never becomes NULL.

In
@src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java:
- Around line 16-34: Update parsePriceRange() to validate order and
non-negativity and to distinguish no-filter from parse errors: when priceRange
is null/empty return Optional.empty() (or a sentinel indicating "no filter"),
otherwise parse trimmed parts into integers, reject if parts length != 2 or
parsing fails by throwing a clear IllegalArgumentException or returning a
distinct Optional.empty() vs Optional.ofError pattern; additionally validate
minPrice >= 0, maxPrice >= 0 and minPrice <= maxPrice before returning new
Integer[] {minPrice, maxPrice} (use the method name parsePriceRange() and its
return type to implement Optional or throw a specific exception so callers can
differentiate "no filter" vs "invalid input").

In @src/main/java/com/ongil/backend/product/service/ProductService.java:
- Around line 138-157: The createSort(ProductSortType sortType) method currently
adds an id tiebreaker only for POPULAR; to ensure stable pagination, update each
case (REVIEW, PRICE_HIGH, PRICE_LOW) to append Sort.Order.asc("id") as the final
tie-breaker (maintain existing primary/secondary orders such as reviewCount,
price, popularity), so Sort.by(...) for all branches includes an id order to
guarantee deterministic DB ordering.
- Around line 54-81: In getProducts, you create pageableWithSort using
createSort(sortType) but then pass the original pageable into
productRepository.findAllByCondition so sorting is ignored; fix by passing
pageableWithSort to productRepository.findAllByCondition (replace the pageable
argument with pageableWithSort) so the Sort from createSort(sortType) is
applied, and ensure variable names pageableWithSort and
productRepository.findAllByCondition in the getProducts method are updated
accordingly.
🧹 Nitpick comments (3)
src/main/resources/application-local.yml (1)

15-15: 스키마 변경 처리 방식을 인지하고 계십시오.

ddl-auto: update로 변경하면 기존 데이터를 보존하면서 스키마를 점진적으로 업데이트합니다. 개발 환경에서는 편리하지만, 컬럼 이름 변경이나 타입 변경 같은 복잡한 마이그레이션은 제대로 처리하지 못할 수 있습니다.

로컬 환경이므로 적절한 선택이지만, 프로덕션 배포 시에는 Flyway나 Liquibase 같은 마이그레이션 도구 사용을 고려해주세요.

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

48-56: (운영 관점) 유사상품 쿼리 성능을 위해 복합 인덱스 후보가 명확해 보여요

Line 50-56 패턴은 보통 (on_sale, category_id, price) 같은 복합 인덱스가 효율에 크게 기여합니다(데이터가 커질수록).

src/main/java/com/ongil/backend/product/service/ProductService.java (1)

92-110: 유사상품은 현재 정렬이 없어 응답이 비결정적입니다(테스트/UX 관점에서 정렬 추가 추천)

Line 100-101이 PageRequest.of(0, 6)만 사용해서 DB 기본 반환 순서에 의존합니다. 최소한 popularity desc, id asc 같은 정렬을 넣으면 결과가 안정적입니다.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3c56fa5 and 4855d8e.

📒 Files selected for processing (10)
  • src/main/java/com/ongil/backend/domain/product/entity/Product.java
  • src/main/java/com/ongil/backend/domain/product/enums/ProductSortType.java
  • src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java
  • src/main/java/com/ongil/backend/global/config/SecurityConfig.java
  • src/main/java/com/ongil/backend/product/controller/ProductController.java
  • src/main/java/com/ongil/backend/product/dto/converter/ProductConverter.java
  • src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java
  • src/main/java/com/ongil/backend/product/dto/response/ProductSimpleResponse.java
  • src/main/java/com/ongil/backend/product/service/ProductService.java
  • src/main/resources/application-local.yml
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

SOLID 원칙, 스프링 어노테이션, 의존성 주입 패턴, 예외 처리에 중점을 둔다

Files:

  • src/main/java/com/ongil/backend/product/dto/response/ProductSimpleResponse.java
  • src/main/java/com/ongil/backend/global/config/SecurityConfig.java
  • src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java
  • src/main/java/com/ongil/backend/domain/product/entity/Product.java
  • src/main/java/com/ongil/backend/product/dto/converter/ProductConverter.java
  • src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java
  • src/main/java/com/ongil/backend/product/controller/ProductController.java
  • src/main/java/com/ongil/backend/domain/product/enums/ProductSortType.java
  • src/main/java/com/ongil/backend/product/service/ProductService.java
**/*.{yml,yaml,properties}

⚙️ CodeRabbit configuration file

스프링 설정, 보안 설정, DB 연결, 환경 설정을 검증한다

Files:

  • src/main/resources/application-local.yml
🧬 Code graph analysis (1)
src/main/java/com/ongil/backend/product/service/ProductService.java (1)
src/main/java/com/ongil/backend/global/common/exception/EntityNotFoundException.java (1)
  • EntityNotFoundException (4-8)
🔇 Additional comments (6)
src/main/java/com/ongil/backend/domain/product/enums/ProductSortType.java (1)

6-15: LGTM!

정렬 타입 enum이 깔끔하게 정의되었습니다. Lombok 어노테이션을 적절히 활용하고 있으며, description 필드를 통해 사용자 친화적인 표현을 제공합니다.

src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java (1)

6-14: LGTM!

DTO 클래스 구조가 깔끔하고 Builder 패턴을 적절히 활용하고 있습니다.

src/main/java/com/ongil/backend/product/dto/converter/ProductConverter.java (1)

73-73: LGTM!

reviewCount 필드가 올바르게 추가되었습니다. 기존 코드 스타일과 일관성을 유지하고 있습니다.

src/main/java/com/ongil/backend/product/dto/response/ProductSimpleResponse.java (1)

31-35: reviewCount 추가는 OK, nullability(응답에서 null vs 0)만 팀 컨벤션으로 고정 추천

Line 34의 Integer reviewCount는 확장 자체는 무리 없습니다. 다만 엔티티 기본값이 0인 만큼, 응답에서도 “항상 0 이상”을 보장할지(primitive/int 또는 변환 시 0 디폴트) “null 허용”을 할지 컨벤션을 정해두면 프론트 연동 시 혼선이 줄어요.
[Suggest_recommended_refactor]

src/main/java/com/ongil/backend/product/controller/ProductController.java (2)

36-56: 목록 API 시그니처/위임 구조는 깔끔합니다. priceRange 포맷 오류 시 에러 응답만 의도대로 떨어지는지 확인 필요

Line 39-45에서 priceRange/clothingSize가 문자열이라, 파싱 실패 케이스가 있으면(예: “1~2만원” 같은 입력) 컨트롤러 레벨에서 어떤 에러로 내려가는지(400 vs 500)만 한번 확인해두면 안전합니다. (코딩 가이드라인 기준: 예외 처리 관점)


58-70: 특가/유사상품 엔드포인트 추가는 의도 명확하고 충돌도 없어 보여요

Line 58-70의 /special-sale, /{productId}/similar는 경로도 직관적이고 기존 /{productId}와도 충돌 가능성이 낮습니다.

Comment on lines +73 to +77
@Column(name = "review_count", nullable = false)
private Integer reviewCount = 0;

@Formula("view_count + purchase_count")
private Integer popularity;
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

DB 마이그레이션/백필 없으면 review_count NOT NULL에서 바로 터질 수 있어요 + @Formula는 COALESCE 권장

  • Line 73-74: review_countnullable = false로 추가했으니, 기존 레코드 백필(0) + 컬럼 디폴트(0) 포함한 마이그레이션이 반드시 필요합니다(없으면 schema validate/insert 시점에 실패 가능).
  • Line 76-77: view_count/purchase_count가 혹시라도 NULL인 데이터가 있으면 popularity가 NULL로 전파될 수 있어 정렬이 깨질 수 있습니다. COALESCE로 방어 추천.
제안 diff (COALESCE로 null-safe)
-	@Formula("view_count + purchase_count")
+	@Formula("coalesce(view_count, 0) + coalesce(purchase_count, 0)")
 	private Integer popularity;
🤖 Prompt for AI Agents
In @src/main/java/com/ongil/backend/domain/product/entity/Product.java around
lines 73 - 77, You added reviewCount as a non-nullable column and defined
popularity with @Formula, which can break runtime/schema validation if existing
rows have NULL review_count or if view_count/purchase_count are NULL; create a
DB migration that backfills existing rows setting review_count = 0 and adds the
new column with DEFAULT 0 and NOT NULL, and update the @Formula on the
popularity field to be null-safe by using COALESCE for view_count and
purchase_count (e.g., COALESCE(view_count, 0) + COALESCE(purchase_count, 0)) so
popularity never becomes NULL.

Comment on lines +21 to +39
// 조건에 따른 상품 조회
@EntityGraph(attributePaths = {"brand", "category"})
@Query("""
SELECT p FROM Product p
WHERE (:categoryId IS NULL OR p.category.id = :categoryId)
AND (:brandId IS NULL OR p.brand.id = :brandId)
AND (:minPrice IS NULL OR p.price >= :minPrice)
AND (:maxPrice IS NULL OR p.price <= :maxPrice)
AND (:size IS NULL OR p.sizes LIKE CONCAT('%', :size, '%'))
AND p.onSale = true
""")
Page<Product> findAllByCondition(
@Param("categoryId") Long categoryId,
@Param("brandId") Long brandId,
@Param("minPrice") Integer minPrice,
@Param("maxPrice") Integer maxPrice,
@Param("size") String size,
Pageable pageable
);
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

sizes LIKE는 사용자 입력에 %/_가 들어오면 매칭이 과해질 수 있어요(escape 고려)

Line 29의 p.sizes LIKE CONCAT('%', :size, '%')는 기능적으로는 OK인데, 요청 파라미터에 %/_가 들어오면 와일드카드로 동작해서 “필터 우회”처럼 보일 수 있습니다(보안 이슈라기보단 정확도 이슈). 가능하면 escape 처리(입력 전처리 or JPQL ESCAPE) 쪽을 고려해볼만 합니다.

Comment on lines +39 to +41
.requestMatchers("/api/products/**").permitAll()
.requestMatchers("/api/brands/**").permitAll()
.requestMatchers("/api/categories/**").permitAll()
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

HTTP 메서드별로 접근 권한을 세분화하세요.

현재 /api/products/**, /api/brands/**, /api/categories/** 경로에 대해 모든 HTTP 메서드를 인증 없이 허용하고 있습니다. 이는 잠재적인 보안 취약점입니다:

  • GET 요청: 조회는 public 접근 허용이 합리적
  • POST/PUT/DELETE 요청: 생성/수정/삭제는 인증이 필요해야 함

현재 구현으로는 인증 없이 상품, 브랜드, 카테고리를 생성/수정/삭제할 수 있는 엔드포인트가 노출될 경우 보안 문제가 발생합니다.

🔒 HTTP 메서드별 권한 제어 제안
 .authorizeHttpRequests(auth -> auth
 	.requestMatchers("/ping", "/h2-console/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
 	.requestMatchers("/auth/logout", "/auth/withdraw").authenticated()
 	.requestMatchers("/auth/oauth/kakao", "/auth/oauth/google", "/auth/token/refresh").permitAll()
 	.requestMatchers("/auth/**").permitAll()
-	.requestMatchers("/api/products/**").permitAll()
-	.requestMatchers("/api/brands/**").permitAll()
-	.requestMatchers("/api/categories/**").permitAll()
+	.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
+	.requestMatchers(HttpMethod.GET, "/api/brands/**").permitAll()
+	.requestMatchers(HttpMethod.GET, "/api/categories/**").permitAll()
+	.requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("ADMIN")
+	.requestMatchers(HttpMethod.PUT, "/api/products/**").hasRole("ADMIN")
+	.requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
+	// brands, categories도 동일하게 적용
 	.anyRequest().authenticated()
 )

HttpMethod import도 추가해야 합니다:

import org.springframework.http.HttpMethod;

Comment on lines +16 to +34
public Integer[] parsePriceRange() {
if (priceRange == null || priceRange.trim().isEmpty()) {
return null;
}

try {
String[] parts = priceRange.split("-");
if (parts.length != 2) {
return null;
}

Integer minPrice = Integer.parseInt(parts[0].trim());
Integer maxPrice = Integer.parseInt(parts[1].trim());

return new Integer[] {minPrice, maxPrice};
} catch (NumberFormatException e) {
return null;
}
}
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

가격 범위 검증 로직을 추가하세요.

parsePriceRange() 메서드에 다음과 같은 검증 누락이 있습니다:

  1. 최소-최대 가격 순서 검증 없음: "1000-500" 같은 잘못된 입력도 그대로 반환됩니다.
  2. 음수 가격 검증 없음: "-100-200" 같은 입력이 허용됩니다.
  3. 에러 처리 모호함: null 반환이 "필터 없음"인지 "잘못된 입력"인지 구분할 수 없습니다.

이로 인해 잘못된 필터링 결과가 발생하거나 디버깅이 어려워질 수 있습니다.

✅ 검증 로직 추가 제안
 public Integer[] parsePriceRange() {
 	if (priceRange == null || priceRange.trim().isEmpty()) {
 		return null;
 	}
 
 	try {
 		String[] parts = priceRange.split("-");
 		if (parts.length != 2) {
 			return null;
 		}
 
 		Integer minPrice = Integer.parseInt(parts[0].trim());
 		Integer maxPrice = Integer.parseInt(parts[1].trim());
+
+		// 음수 가격 검증
+		if (minPrice < 0 || maxPrice < 0) {
+			return null;
+		}
+
+		// 가격 범위 순서 검증
+		if (minPrice > maxPrice) {
+			return null;
+		}
 
 		return new Integer[] {minPrice, maxPrice};
 	} catch (NumberFormatException e) {
 		return null;
 	}
 }

더 나아가, Optional이나 커스텀 예외를 사용하여 "필터 없음"과 "파싱 에러"를 명확히 구분하는 것도 고려해보세요.

🤖 Prompt for AI Agents
In
@src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java
around lines 16 - 34, Update parsePriceRange() to validate order and
non-negativity and to distinguish no-filter from parse errors: when priceRange
is null/empty return Optional.empty() (or a sentinel indicating "no filter"),
otherwise parse trimmed parts into integers, reject if parts length != 2 or
parsing fails by throwing a clear IllegalArgumentException or returning a
distinct Optional.empty() vs Optional.ofError pattern; additionally validate
minPrice >= 0, maxPrice >= 0 and minPrice <= maxPrice before returning new
Integer[] {minPrice, maxPrice} (use the method name parsePriceRange() and its
return type to implement Optional or throw a specific exception so callers can
differentiate "no filter" vs "invalid input").

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

🤖 Fix all issues with AI agents
In @src/main/java/com/ongil/backend/product/service/ProductService.java:
- Around line 92-110: The Product loaded in getSimilarProducts uses
productRepository.findById(productId) which returns a LAZY category and causes
an N+1 when calling product.getCategory().getId(); change the call to use a
repository method that fetches category eagerly (e.g., add
findByIdWithCategory(Long id) or findByIdWithCategoryAndBrand if preferred on
ProductRepository annotated with @EntityGraph(attributePaths = {"category"}) or
a custom JPQL join fetch) and replace productRepository.findById(productId) with
that eager-loading method so product.getCategory().getId() does not trigger an
extra query.
🧹 Nitpick comments (1)
src/main/java/com/ongil/backend/domain/product/entity/Product.java (1)

73-74: Builder 패턴 일관성 검토 권장

reviewCount 필드가 추가되었지만 Builder 생성자(94-120라인)에는 포함되지 않았습니다. viewCount, purchaseCount도 기본값을 가지고 있어 Builder에 포함되지 않은 패턴을 따르고 있지만, 데이터 마이그레이션이나 테스트 시나리오에서 초기 리뷰 수를 설정해야 할 경우 유연성이 떨어질 수 있습니다.

필요 시 Builder에 reviewCount 파라미터를 추가하는 것을 고려해보세요.

♻️ Builder에 reviewCount 추가 예시
 @Builder
 public Product(String name, String description, Integer price,
 	String materialOriginal,
 	String aiMaterialAdvantages,
 	String aiMaterialDisadvantages,
 	String aiMaterialCare,
 	String washingMethod, String imageUrls,
 	String sizes, String colors, Integer discountRate,
-	Integer discountPrice, ProductType productType,
+	Integer discountPrice, Integer reviewCount, ProductType productType,
 	Brand brand, Category category) {
 	this.name = name;
 	this.description = description;
 	this.price = price;
 	this.materialOriginal = materialOriginal;
 	this.aiMaterialAdvantages = aiMaterialAdvantages;
 	this.aiMaterialDisadvantages = aiMaterialDisadvantages;
 	this.aiMaterialCare = aiMaterialCare;
 	this.washingMethod = washingMethod;
 	this.imageUrls = imageUrls;
 	this.sizes = sizes;
 	this.colors = colors;
 	this.discountRate = discountRate;
 	this.discountPrice = discountPrice;
+	this.reviewCount = reviewCount != null ? reviewCount : 0;
 	this.productType = productType;
 	this.brand = brand;
 	this.category = category;
 }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4855d8e and 87d873b.

📒 Files selected for processing (3)
  • src/main/java/com/ongil/backend/domain/product/entity/Product.java
  • src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java
  • src/main/java/com/ongil/backend/product/service/ProductService.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/ongil/backend/product/dto/request/ProductSearchCondition.java
🧰 Additional context used
📓 Path-based instructions (1)
src/main/java/**/*.java

⚙️ CodeRabbit configuration file

SOLID 원칙, 스프링 어노테이션, 의존성 주입 패턴, 예외 처리에 중점을 둔다

Files:

  • src/main/java/com/ongil/backend/product/service/ProductService.java
  • src/main/java/com/ongil/backend/domain/product/entity/Product.java
🔇 Additional comments (5)
src/main/java/com/ongil/backend/product/service/ProductService.java (4)

28-28: 클래스 레벨 읽기 전용 트랜잭션 적용 승인

클래스 레벨에 @Transactional(readOnly = true)를 적용한 것은 서비스 레이어의 모범 사례입니다. getProductDetail 메서드가 개별적으로 @Transactional로 오버라이드하여 조회수 증가 등의 쓰기 작업을 처리하는 구조도 올바릅니다.


83-90: 특가 상품 조회 로직 승인

상위 10개 특가 상품을 discountRate 내림차순으로 조회하는 로직이 명확하고 올바릅니다. PageRequest.of(0, 10, ...)을 사용하여 고정된 개수를 반환하는 방식도 PR 목표에 부합합니다.


54-81: N+1 쿼리 방지 검증: 이미 적절하게 구현됨

findAllByCondition 메서드에 @EntityGraph(attributePaths = {"brand", "category"})가 이미 적용되어 있으므로 Brand와 Category를 즉시 로딩하여 N+1 문제를 방지하고 있습니다. Product Entity의 LAZY 로딩 설정을 @EntityGraph가 올바르게 오버라이드하므로 추가 조치는 불필요합니다.


138-160: 다단계 tie-breaker 정렬 로직은 우수하며 @formula 필드 정렬도 정상 작동

다단계 tie-breaker를 사용한 정렬 설계가 명확하고 우수합니다:

  • POPULAR: popularity → id
  • REVIEW: reviewCount → popularity → id
  • PRICE_HIGH/LOW: price → popularity → id

popularity 필드의 @Formula 사용과 관련하여, Spring Data JPA와 Hibernate는 이 패턴을 완전히 지원합니다. Formula 표현식이 SQL에 인라인되는 방식도 정상적이므로 정렬 기능은 예상대로 작동합니다.

다만 Formula 표현식이 SELECT와 ORDER BY에 반복 포함될 수 있어 성능 고려사항이 있습니다. 현재 사용 중인 공식(coalesce(view_count, 0) + coalesce(purchase_count, 0))은 간단한 산술 연산이라 성능 영향은 제한적입니다. 향후 대규모 트래픽 환경에서 필요하면 계산된 컬럼(generated column) 또는 물리화된 뷰(materialized view) 도입을 검토할 수 있습니다.

src/main/java/com/ongil/backend/domain/product/entity/Product.java (1)

76-77: @formula 기반 popularity 필드는 올바르게 구현됨

@Formula로 정의된 popularity 필드가 ProductService의 4개 정렬 로직(POPULAR, REVIEW, PRICE_HIGH, PRICE_LOW)에서 secondary 정렬 기준으로 사용 중입니다. Hibernate는 @formula 필드를 ORDER BY 절에서 올바르게 처리하며, 데이터베이스 수준 연산을 통해 N+1 문제를 방지하는 이 패턴은 검증된 구현입니다.

Comment on lines +92 to +110
public List<ProductSimpleResponse> getSimilarProducts(Long productId) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.PRODUCT_NOT_FOUND));

int basePrice = product.getPrice();
int minPrice = (int)(basePrice * 0.8);
int maxPrice = (int)(basePrice * 1.2);

Pageable pageable = PageRequest.of(0, 6);
Page<Product> similarProducts = productRepository.findByOnSaleTrueAndCategoryIdAndIdNotAndPriceBetween(
product.getCategory().getId(),
productId,
minPrice,
maxPrice,
pageable
);

return similarProducts.map(productConverter::toSimpleResponse).getContent();
}
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

🧩 Analysis chain

🏁 Script executed:

find src -type f -name "*ProductRepository*" | head -20

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 147


🏁 Script executed:

cat -n src/main/java/com/ongil/backend/product/service/ProductService.java | sed -n '92,110p'

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 857


🏁 Script executed:

# Find ProductRepository file
fd "ProductRepository" src --type f

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 147


🏁 Script executed:

# Check the directory structure for repository files
ls -la src/main/java/com/ongil/backend/product/repository/ 2>/dev/null || ls -la src/main/java/com/ongil/backend/*/repository/ 2>/dev/null | head -30

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 49


🏁 Script executed:

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

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 2385


🏁 Script executed:

# Find Product entity file
fd "Product.java" src --type f | grep -v Repository

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 133


🏁 Script executed:

cat -n src/main/java/com/ongil/backend/domain/product/entity/Product.java | head -100

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 3478


🏁 Script executed:

# Check if findById is overridden in ProductRepository
rg -n "findById|@EntityGraph" src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 237


🏁 Script executed:

# Check for any eager fetch in Product entity
rg -n "fetch.*EAGER|fetch.*LAZY|@ManyToOne|@OneToMany" src/main/java/com/ongil/backend/domain/product/entity/Product.java

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 144


findById 호출 시 Category 즉시 로딩 필요

102라인의 product.getCategory().getId()에서 N+1 쿼리가 발생합니다. 93라인의 productRepository.findById(productId) 호출 시 Category가 지연 로딩(LAZY) 상태로 반환되므로, 카테고리 ID 접근 시 추가 쿼리가 트리거됩니다.

해결 방법:

  • ProductRepository에 EntityGraph를 적용한 findById 커스텀 메서드 추가
  • 또는 findById 대신 category를 포함하는 커스텀 조회 메서드 사용

참고: findByOnSaleTrueAndCategoryIdAndIdNotAndPriceBetween 메서드는 이미 @EntityGraph(attributePaths = {"brand", "category"})로 보호되어 있으므로 해당 쿼리는 N+1 문제가 없습니다.

🤖 Prompt for AI Agents
In @src/main/java/com/ongil/backend/product/service/ProductService.java around
lines 92 - 110, The Product loaded in getSimilarProducts uses
productRepository.findById(productId) which returns a LAZY category and causes
an N+1 when calling product.getCategory().getId(); change the call to use a
repository method that fetches category eagerly (e.g., add
findByIdWithCategory(Long id) or findByIdWithCategoryAndBrand if preferred on
ProductRepository annotated with @EntityGraph(attributePaths = {"category"}) or
a custom JPQL join fetch) and replace productRepository.findById(productId) with
that eager-loading method so product.getCategory().getId() does not trigger an
extra query.

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

Labels

✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant