Conversation
feat: 상품 상세·후기 API 구현 (productDetail·productReviews·리뷰 댓글/좋아요 해제)
|
Warning Review limit reached
Next review available in: 35 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthrough상품 상세 조회와 상품 리뷰·댓글 조회 GraphQL API를 추가했습니다. 리뷰 좋아요 해제와 댓글 작성·삭제 mutation을 추가했습니다. 리뷰 댓글 저장소와 soft-delete 정책을 추가했습니다. 옵션 그룹 설명 필드를 판매자 입력과 상품 상세 응답에 연결했습니다. Changes상품 상세 조회
상품 리뷰 조회
리뷰 댓글 참여 기능
옵션 그룹 설명
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ProductReviewQueryResolver
participant ProductReviewService
participant ProductReviewRepository
Client->>ProductReviewQueryResolver: productReviews 또는 reviewDetail 또는 reviewComments
ProductReviewQueryResolver->>ProductReviewService: 조회 입력과 선택적 accountId 전달
ProductReviewService->>ProductReviewRepository: 페이지·본문·집계 조회
ProductReviewRepository-->>ProductReviewService: 리뷰 및 댓글 데이터 반환
ProductReviewService-->>Client: GraphQL 연결 응답 반환
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
🩺 NestJS Doctor — 89/100 (Good)진단 270건 (error 0).
architecture / security 상위 항목
|
🧹 knip — dead-code 리포트전체 리포트
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Coverage report
Test suite run success1463 tests passing in 173 suites. Report generated by 🧪jest coverage report action from 9fdf264 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 26e56e4e10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ? Prisma.sql`HAVING COUNT(l.id) < ${args.cursor.likeCount} | ||
| OR (COUNT(l.id) = ${args.cursor.likeCount} AND r.id < ${args.cursor.id})` |
There was a problem hiding this comment.
Snapshot the ranking before paginating by likes
When likes change between page requests, comparing every row's current count with only the boundary's previous count still loses or duplicates reviews. For example, after a cursor of 5:<id>, an unseen four-like review that gains two likes no longer satisfies COUNT(l.id) < 5 and is omitted from all subsequent pages; conversely, a previously returned review that drops below five can reappear. A stable ranking snapshot/version is needed if this pagination is expected to avoid gaps and duplicates.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
미반영(기존 결정 유지): PR #167 동일 지적에 근거 dismiss 완료 — 전체 랭킹 스냅샷은 별도 인프라 필요로 과설계 판단. 경계 고정 커서로 계통적 중복 제거, 잔여 anomaly는 FE id dedup으로 흡수(문서 안내 포함).
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (9)
src/features/product/types/product-review-output.type.ts (1)
6-11: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
mediaType를 Prisma enum과 연결하는 방안을 검토하세요.
mediaType은'IMAGE' | 'VIDEO'리터럴 유니온으로 중복 선언되어 있습니다. 저장소 row는 Prisma의ReviewMediaType을 사용합니다. Prisma enum에 값이 추가되면 이 타입과 SDL이 조용히 어긋납니다. 도메인 출력 타입에서 Prisma 타입을 직접 참조하지 않는 계층 규칙이 있으면 현재 형태를 유지하세요. 그 규칙이 없으면 enum을 재사용하세요.♻️ 제안 변경
+import type { ReviewMediaType } from '`@prisma/client`'; + export interface ProductReviewMedia { - mediaType: 'IMAGE' | 'VIDEO'; + mediaType: ReviewMediaType; mediaUrl: string; thumbnailUrl: string | null; sortOrder: number; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/product/types/product-review-output.type.ts` around lines 6 - 11, Review the ProductReviewMedia.mediaType definition and determine whether this layer permits direct Prisma type references; if no such boundary rule exists, replace the duplicated 'IMAGE' | 'VIDEO' union with the Prisma ReviewMediaType enum so future enum additions remain aligned. Otherwise, retain the current union.src/features/product/services/product-review.service.ts (1)
182-194: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value
id부분에도 범위 제한을 두는 방안을 검토하세요.
likeCount는Number.isSafeInteger로 방어합니다.id는 자릿수 제한이 없습니다. 예를 들어 500자리 숫자도 정규식을 통과하여 BigInt로 변환되고 raw SQL 파라미터로 전달됩니다. 이 값은 BIGINT 컬럼 범위를 벗어나므로 DB 오류가 발생할 수 있습니다. 그러면 400 대신 500이 반환됩니다. 정규식에서 자릿수를 제한하면 두 필드의 방어 수준이 같아집니다.♻️ 제안 변경
- const match = /^(\d+):(\d+)$/.exec(raw); + const match = /^(\d{1,19}):(\d{1,19})$/.exec(raw);참고로 라인 183에 대한 정적 분석의 command injection 경고는 오탐입니다. 이 호출은
RegExp.prototype.exec입니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/product/services/product-review.service.ts` around lines 182 - 194, Update parseLikesCursor to validate the id component’s range before converting it with BigInt, rejecting values outside the database BIGINT range with INVALID_LIKES_CURSOR so oversized cursors return a bad-request error instead of reaching raw SQL. Apply the restriction at the cursor validation step alongside the existing likeCount safety check.Source: Linters/SAST tools
src/features/product/services/product-review.service.spec.ts (1)
421-501: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win비활성 매장 분기에 대한 테스트를 추가하세요.
publicReviewWhere는 리뷰, 상품, 매장 세 가지 활성 조건을 검사합니다. 현재 테스트는 soft-delete 리뷰와 비활성 상품만 검증합니다. 매장이 비활성이거나 soft-delete된 경우는 검증되지 않습니다. 이 조건은 좋아요순 raw SQL에도 중복 구현되어 있으므로, 두 경로가 어긋나면 비공개 매장의 리뷰가 노출될 수 있습니다.is_active=false매장과deleted_at이 설정된 매장에 대해reviewDetail과productReviews(sort=LIKES 포함)가 리뷰를 숨기는지 검증하세요.As per path instructions: "정상 흐름뿐 아니라 주요 예외/분기 케이스가 포함되는지 확인하세요."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/product/services/product-review.service.spec.ts` around lines 421 - 501, reviewDetail 테스트에 비활성 매장 분기를 추가하고, is_active=false 및 deleted_at이 설정된 매장의 리뷰가 NotFoundException으로 숨겨지는지 검증하세요. 같은 매장 상태별로 productReviews의 기본 조회와 sort=LIKES raw SQL 경로에서도 해당 리뷰가 결과에 포함되지 않는지 확인하며, 기존 활성 매장 및 리뷰 검증은 유지하세요.Source: Path instructions
src/features/product/repositories/product-review.repository.ts (1)
122-151: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value좋아요순 조회용 공개 리뷰 조건을
publicReviewWhere와 동기화하세요.
listProductReviewIdsByLikes가 raw SQL에서publicReviewWhere와 같은 리뷰·상품·매장 활성 조건을 다시 구현합니다. 나중에 공개 리뷰 가드가 바뀌면 좋아요순 정렬만 조건이 어긋날 수 있어, 조건 조각을 상수/헬퍼로 분리하거나 최소한 주석으로 이 두 위치가 한 쌍임을 명시하세요.is_active = 1비교는 MySQL TinyInt 기준으로 현재 스키마와 맞습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/product/repositories/product-review.repository.ts` around lines 122 - 151, Update listProductReviewIdsByLikes to reuse the same public-review condition source as publicReviewWhere, including review, product, and store active/deleted filters, instead of duplicating them in the raw SQL. Extract the shared condition into a constant or helper and apply it in both locations, preserving the existing MySQL TinyInt comparisons.src/features/user/dto/inputs/write-review-comment.input.spec.ts (1)
18-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win오류 배열 길이를 먼저 단정하고
reviewId분기도 추가하세요.검증이 예상과 달리 통과하면
errors[0]은 undefined 가 됩니다. 테스트는 TypeError 로 실패하고 실패 원인이 드러나지 않습니다. 길이 단정과 제약 이름 단정을 추가하세요.reviewId누락·공백 케이스도 없습니다. 입력 계약의 주요 분기를 함께 덮으세요.♻️ 제안 diff
it('content는 trim 후 길이를 검증한다(공백만 입력 거절)', async () => { const dto = build({ reviewId: '123', content: ' ' }); const errors = await validate(dto); + expect(errors).toHaveLength(1); expect(errors[0].property).toBe('content'); }); it('content 500자 초과 거절', async () => { const dto = build({ reviewId: '123', content: 'a'.repeat(501) }); const errors = await validate(dto); + expect(errors).toHaveLength(1); expect(errors[0].property).toBe('content'); }); @@ it('content가 문자열이 아니면 거절(transform은 원값 유지)', async () => { const dto = build({ reviewId: '123', content: 123 }); const errors = await validate(dto); + expect(errors).toHaveLength(1); expect(errors[0].property).toBe('content'); }); + + it('reviewId 공백은 거절한다', async () => { + const dto = build({ reviewId: ' ', content: '너무 귀여워요' }); + const errors = await validate(dto); + expect(errors.map((e) => e.property)).toContain('reviewId'); + });
reviewId케이스의 기대값은 DTO 의 실제 검증 규칙(trim 여부)에 맞추세요.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/user/dto/inputs/write-review-comment.input.spec.ts` around lines 18 - 39, Update the validation tests around build and validate to assert the errors array length before inspecting individual entries, and assert the relevant constraint name for each failure. Add reviewId validation cases covering missing and whitespace-only values, matching the DTO’s actual trim behavior, while preserving the existing content cases.Source: Path instructions
src/features/product/dto/inputs/product-reviews.input.ts (1)
12-37: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value정렬 상수도 공통 파일로 옮기는 편이 좋습니다.
src/features/product/dto/inputs/product-reviews.input.ts의PRODUCT_REVIEW_SORTS와PRODUCT_REVIEW_SORTS타입이src/features/product/constants/product-review.constants.ts의 기본 페이지 크기 상수와 분리되어 있습니다. 정렬 목록도 한곳에서 참조하도록 정리해SDL·검증 로직이 어긋나지 않게 합니다.limit에 대한 현재 분리된 상수는 없는 상태라100한계 정리보다는 정렬 상수 이동이 우선입니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/product/dto/inputs/product-reviews.input.ts` around lines 12 - 37, Move the PRODUCT_REVIEW_SORTS constant and ProductReviewSort type from ProductReviewsInput into the shared product-review constants module, then update ProductReviewsInput and any other references to import and reuse them; leave the existing limit validation unchanged.src/features/user/dto/inputs/write-review-comment.input.ts (1)
18-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win매직 넘버 대신
MAX_REVIEW_COMMENT_LENGTH상수를 재사용하십시오.
@Length(1, 500)은user.constants.ts의MAX_REVIEW_COMMENT_LENGTH와 값이 중복됩니다. 두 값의 출처가 다르면 나중에 한쪽만 변경될 때 DTO 검증 기준과 서비스 검증 기준이 어긋납니다.MAX_REVIEW_COMMENT_LENGTH를 import해서 사용하십시오.♻️ 제안하는 수정
import { Transform } from 'class-transformer'; import { IsString, Length } from 'class-validator'; + +import { MAX_REVIEW_COMMENT_LENGTH } from '`@/features/user/constants/user.constants`'; /** * 리뷰 댓글 작성 입력. @@ `@IsString`() `@Transform`(({ value }: { value: unknown }) => typeof value === 'string' ? value.trim() : value, ) - `@Length`(1, 500) + `@Length`(1, MAX_REVIEW_COMMENT_LENGTH) content!: string; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/user/dto/inputs/write-review-comment.input.ts` around lines 18 - 19, Update the validation decorator on content in the review-comment input DTO to use the imported MAX_REVIEW_COMMENT_LENGTH from user.constants.ts instead of the hardcoded 500, while preserving the minimum length of 1.src/features/product/services/product-detail.service.spec.ts (1)
111-166: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win옵션 그룹의 소프트 삭제 필터링에 대한 테스트를 추가하십시오.
이 테스트는
is_active: false인 옵션 그룹의 제외만 검증합니다. 이미지(Line 83-90)와 옵션 아이템(Line 144-151)은deleted_at으로 소프트 삭제된 항목의 제외를 각각 검증하지만, 옵션 그룹 자체에 대해서는 같은 검증이 없습니다.findProductDetailById의option_groups.where에는deleted_at: null조건도 있으므로, 이 조건에 대한 테스트를 추가하면 회귀를 방지할 수 있습니다.♻️ 제안: 소프트 삭제된 옵션 그룹 테스트 추가
await prisma.productOptionGroup.create({ data: { product_id: product.id, name: '비활성 그룹', sort_order: 2, is_active: false, }, }); + await prisma.productOptionGroup.create({ + data: { + product_id: product.id, + name: '삭제된 그룹', + sort_order: 3, + deleted_at: new Date(), + }, + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/product/services/product-detail.service.spec.ts` around lines 111 - 166, Extend the test around productDetail in the test case containing the option-group fixtures to create an option group with deleted_at set and assert it is absent from result.optionGroups, alongside the existing inactive-group assertion. Keep the current ordering and active item checks unchanged while explicitly covering the option_groups deleted_at: null filter used by findProductDetailById.Source: Path instructions
src/features/seller/services/seller-product-mappers.helper.ts (1)
89-112: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win반환 타입을 명시하세요.
toOptionGroupOutput는 export 함수지만 반환 타입이 추론됩니다.SellerOptionGroupOutput같은 출력 계약 타입을 반환 타입으로 선언하세요. 이 변경으로 호출자와 GraphQL 출력 계약을 컴파일 단계에서 고정할 수 있습니다.As per path instructions:
src/**/*.ts: export 되는 함수/클래스는 입력/출력 타입이 명확해야 하며 any 사용은 허용하지 않습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/seller/services/seller-product-mappers.helper.ts` around lines 89 - 112, Update the exported toOptionGroupOutput function to explicitly return the established SellerOptionGroupOutput type, preserving the current mapped fields and nested optionItems structure so callers and the GraphQL output contract are compile-time validated.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/features/product/repositories/product-review.repository.ts`:
- Around line 330-340: Prisma Client 생성이 검증 및 CI 실행 전에 완료되도록 설정하세요. `yarn
validate` 스크립트에서 검증 명령보다 먼저 `prisma:generate`를 실행하고, `.github/workflows`의 의존성 설치
후 단계에도 동일한 생성 단계를 추가하세요. `aggregateCommentCounts`의 `ReviewComment` 사용이 생성된
클라이언트로 정상 컴파일되도록 기존 검증 및 lint/tsc 흐름은 유지하세요.
In `@src/features/product/repositories/product.repository.ts`:
- Around line 829-885: Update the sibling repository methods findProductById and
listProductsByStore so every nested ProductImage, ProductOptionGroup, and
ProductOptionItem relation explicitly filters deleted_at: null, matching
findProductDetailById. Preserve their existing active-state and relation
behavior, or reuse a shared nested soft-delete filter if one already exists.
In `@src/features/product/services/product-review.service.ts`:
- Around line 118-125: Prevent empty-page cursor access in ProductReviewService:
at src/features/product/services/product-review.service.ts lines 118-125,
require page.length > 0 when calculating nextCursor; apply the same guard to
last and pageIds[pageIds.length - 1] in lines 156-178. Also verify
ProductReviewsInput and ReviewCommentsInput enforce a minimum limit of 1.
In `@src/features/seller/services/seller-option.service.spec.ts`:
- Around line 102-118: Replace real database access with mocks or stubs in the
description-normalization creation test at
src/features/seller/services/seller-option.service.spec.ts:102-118, covering
ProductRepository, SellerRepository, and the audit-log repository while
preserving the trim and blank-to-null assertions. Apply the same test-double
setup to the update description-normalization test at
src/features/seller/services/seller-option.service.spec.ts:175-190; both sites
require direct changes.
- Around line 102-118: In
src/features/seller/services/seller-option.service.spec.ts lines 102-118, extend
the sellerCreateOptionGroup tests to verify a 1000-character description
succeeds and a 1001-character description rejects with BadRequestException,
while preserving the existing trim and blank normalization checks. In the same
file lines 175-190, add equivalent boundary assertions for the update request,
covering successful 1000-character input and BadRequestException for 1001
characters; keep test dependencies controlled through the existing setup and
service stubs.
In `@src/features/user/repositories/review.repository.ts`:
- Around line 205-213: Update the package scripts that run lint and validate so
they invoke prisma:generate first, ensuring the ReviewComment client used by the
transaction code is generated before type checking. Preserve the existing lint
and validate commands after generation.
In `@src/features/user/repositories/user.repository.ts`:
- Around line 660-677: Update softDeleteMyReviewComment so its
reviewComment.findFirst query explicitly includes deleted_at: null alongside the
comment ID, ensuring already soft-deleted comments are treated as not found
regardless of Prisma extension behavior.
- Around line 636-646: Update the transaction query in the review repository
method containing the locked review lookup so it remains compatible with the
supported MySQL versions: replace the MySQL 8-only `FOR SHARE OF r` syntax with
compatible `FOR SHARE` syntax or the project’s established alternative locking
strategy, while preserving the intended shared lock behavior.
---
Nitpick comments:
In `@src/features/product/dto/inputs/product-reviews.input.ts`:
- Around line 12-37: Move the PRODUCT_REVIEW_SORTS constant and
ProductReviewSort type from ProductReviewsInput into the shared product-review
constants module, then update ProductReviewsInput and any other references to
import and reuse them; leave the existing limit validation unchanged.
In `@src/features/product/repositories/product-review.repository.ts`:
- Around line 122-151: Update listProductReviewIdsByLikes to reuse the same
public-review condition source as publicReviewWhere, including review, product,
and store active/deleted filters, instead of duplicating them in the raw SQL.
Extract the shared condition into a constant or helper and apply it in both
locations, preserving the existing MySQL TinyInt comparisons.
In `@src/features/product/services/product-detail.service.spec.ts`:
- Around line 111-166: Extend the test around productDetail in the test case
containing the option-group fixtures to create an option group with deleted_at
set and assert it is absent from result.optionGroups, alongside the existing
inactive-group assertion. Keep the current ordering and active item checks
unchanged while explicitly covering the option_groups deleted_at: null filter
used by findProductDetailById.
In `@src/features/product/services/product-review.service.spec.ts`:
- Around line 421-501: reviewDetail 테스트에 비활성 매장 분기를 추가하고, is_active=false 및
deleted_at이 설정된 매장의 리뷰가 NotFoundException으로 숨겨지는지 검증하세요. 같은 매장 상태별로
productReviews의 기본 조회와 sort=LIKES raw SQL 경로에서도 해당 리뷰가 결과에 포함되지 않는지 확인하며, 기존 활성
매장 및 리뷰 검증은 유지하세요.
In `@src/features/product/services/product-review.service.ts`:
- Around line 182-194: Update parseLikesCursor to validate the id component’s
range before converting it with BigInt, rejecting values outside the database
BIGINT range with INVALID_LIKES_CURSOR so oversized cursors return a bad-request
error instead of reaching raw SQL. Apply the restriction at the cursor
validation step alongside the existing likeCount safety check.
In `@src/features/product/types/product-review-output.type.ts`:
- Around line 6-11: Review the ProductReviewMedia.mediaType definition and
determine whether this layer permits direct Prisma type references; if no such
boundary rule exists, replace the duplicated 'IMAGE' | 'VIDEO' union with the
Prisma ReviewMediaType enum so future enum additions remain aligned. Otherwise,
retain the current union.
In `@src/features/seller/services/seller-product-mappers.helper.ts`:
- Around line 89-112: Update the exported toOptionGroupOutput function to
explicitly return the established SellerOptionGroupOutput type, preserving the
current mapped fields and nested optionItems structure so callers and the
GraphQL output contract are compile-time validated.
In `@src/features/user/dto/inputs/write-review-comment.input.spec.ts`:
- Around line 18-39: Update the validation tests around build and validate to
assert the errors array length before inspecting individual entries, and assert
the relevant constraint name for each failure. Add reviewId validation cases
covering missing and whitespace-only values, matching the DTO’s actual trim
behavior, while preserving the existing content cases.
In `@src/features/user/dto/inputs/write-review-comment.input.ts`:
- Around line 18-19: Update the validation decorator on content in the
review-comment input DTO to use the imported MAX_REVIEW_COMMENT_LENGTH from
user.constants.ts instead of the hardcoded 500, while preserving the minimum
length of 1.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 04745a59-d8cf-4459-8b87-0e0cc0f0b915
📒 Files selected for processing (47)
prisma/migrations/20260803164651_add_review_comment_and_option_group_description/migration.sqlprisma/migrations/20260803175209_index_review_comment_by_pagination_key/migration.sqlprisma/schema.prismasrc/features/product/constants/product-detail-error-messages.tssrc/features/product/constants/product-review-error-messages.tssrc/features/product/constants/product-review.constants.tssrc/features/product/dto/inputs/product-reviews.input.tssrc/features/product/dto/inputs/review-comments.input.tssrc/features/product/product-detail.graphqlsrc/features/product/product-reviews.graphqlsrc/features/product/product.module.tssrc/features/product/repositories/product-review.repository.tssrc/features/product/repositories/product.repository.tssrc/features/product/resolvers/product-detail-query.resolver.spec.tssrc/features/product/resolvers/product-detail-query.resolver.tssrc/features/product/resolvers/product-review-query.resolver.spec.tssrc/features/product/resolvers/product-review-query.resolver.tssrc/features/product/services/product-detail-mappers.helper.tssrc/features/product/services/product-detail.service.spec.tssrc/features/product/services/product-detail.service.tssrc/features/product/services/product-review-mappers.helper.tssrc/features/product/services/product-review.service.spec.tssrc/features/product/services/product-review.service.tssrc/features/product/types/product-detail-output.type.tssrc/features/product/types/product-review-output.type.tssrc/features/seller/constants/seller.constants.tssrc/features/seller/dto/inputs/seller-create-option-group.input.tssrc/features/seller/dto/inputs/seller-update-option-group.input.tssrc/features/seller/seller-product.graphqlsrc/features/seller/services/seller-option.service.spec.tssrc/features/seller/services/seller-option.service.tssrc/features/seller/services/seller-product-mappers.helper.tssrc/features/seller/types/seller-output.type.tssrc/features/user/constants/user-review-error-messages.tssrc/features/user/constants/user.constants.tssrc/features/user/dto/inputs/write-review-comment.input.spec.tssrc/features/user/dto/inputs/write-review-comment.input.tssrc/features/user/repositories/review.repository.tssrc/features/user/repositories/user.repository.tssrc/features/user/resolvers/user-engagement-mutation.resolver.tssrc/features/user/resolvers/user-engagement.resolver.spec.tssrc/features/user/services/user-engagement.service.spec.tssrc/features/user/services/user-engagement.service.tssrc/features/user/services/user-review.service.spec.tssrc/features/user/types/user-review-output.type.tssrc/features/user/user-engagement.graphqlsrc/prisma/soft-delete.middleware.ts
| async aggregateCommentCounts( | ||
| reviewIds: bigint[], | ||
| ): Promise<Map<bigint, number>> { | ||
| if (reviewIds.length === 0) return new Map(); | ||
| const rows = await this.prisma.reviewComment.groupBy({ | ||
| by: ['review_id'], | ||
| where: { review_id: { in: reviewIds }, deleted_at: null }, | ||
| _count: { _all: true }, | ||
| }); | ||
| return new Map(rows.map((r) => [r.review_id, r._count._all])); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 스키마에 ReviewComment 모델이 있는지 확인
fd -t f 'schema.prisma' --exec rg -n -A 20 'model ReviewComment'
# lint/typecheck 전에 prisma generate가 실행되는지 확인
fd -t f 'package.json' -d 2 --exec jq '.scripts'Repository: CaQuick/caquick-be
Length of output: 2255
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== workflow files =="
fd -t f . .github/workflows 2>/dev/null || true
echo "== relevant workflow snippets =="
if [ -d .github/workflows ]; then
for f in .github/workflows/*.yml .github/workflows/*.yaml; do
[ -e "$f" ] || continue
echo "--- $f"
sed -n '1,220p' "$f" | rg -n -C 3 '(prisma|pnp|install|lint|validate|eslint|deps|restore|setup|tsx|npm|yarn|pnpm|generate|tsc)' || true
done
fi
echo "== package scripts and relevant deps =="
jq '{scripts: .scripts, devDependencies: (.devDependencies|with_entries(select(.key|test("eslint|typescript|prettier|prisma|tsconfig|tsx|nx")))), dependencies: (.dependencies|with_entries(select(.key|test("prisma|typescript|nestjs"))))}' package.json 2>/dev/null || jq '.scripts' package.json
echo "== prisma config files =="
fd -t f 'prisma\.schema|schema\.prisma|\.prettierrc|tsconfig.*json|eslint.*cjs|eslint.*js|package-lock\.json|yarn\.lock|pnpm-lock\.yaml' . | sed -n '1,120p'
echo "== target file relevant lines =="
cat -n src/features/product/repositories/product-review.repository.ts | sed -n '1,80p;300,390p' | sed -n '1,220p'Repository: CaQuick/caquick-be
Length of output: 13832
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
import json
from pathlib import Path
p = Path('package.json')
data = json.loads(p.read_text())
scripts = data.get('scripts', {})
pr = set(scripts.get('prisma:generate','').split())
val = scripts.get('validate','')
lint_key = [k for k in scripts if k == 'lint']
checks = {
"validate_exists": bool(val),
"prisma_generate_exists": bool(scripts.get('prisma:generate')),
"validate_has_prisma_generate": any(token in pr for token in val.split()) if val else False,
"lint_has_prisma_generate": any(token in pr for token in scripts.get('lint','').split()) if lint_key else False,
"install_in_validate": any(token in val.split() for token in {'npm','yarn','pnpm'}),
}
print(checks)
PYRepository: CaQuick/caquick-be
Length of output: 317
Prisma client 생성을 CI 런타임 순서에 추가하세요.
ReviewComment 모델은 prisma/schema.prisma에 있지만, validate나 워크플로우의 lint/tsc는 prisma:generate 없이 실행됩니다. PR 체크가 실패하지 않도록 yarn validate와 .github/workflows의 install 후 단계에서 yarn prisma:generate를 먼저 실행하세요.
🧰 Tools
🪛 ESLint
[error] 334-338: Unsafe assignment of an error typed value.
(@typescript-eslint/no-unsafe-assignment)
[error] 334-334: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
[error] 334-334: Unsafe member access .groupBy on a type that cannot be resolved.
(@typescript-eslint/no-unsafe-member-access)
[error] 339-339: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
[error] 339-339: Unsafe member access .map on a type that cannot be resolved.
(@typescript-eslint/no-unsafe-member-access)
[error] 339-339: Unsafe return of a value of type any[].
(@typescript-eslint/no-unsafe-return)
[error] 339-339: Unsafe member access .review_id on an any value.
(@typescript-eslint/no-unsafe-member-access)
[error] 339-339: Unsafe member access ._count on an any value.
(@typescript-eslint/no-unsafe-member-access)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/product/repositories/product-review.repository.ts` around lines
330 - 340, Prisma Client 생성이 검증 및 CI 실행 전에 완료되도록 설정하세요. `yarn validate` 스크립트에서
검증 명령보다 먼저 `prisma:generate`를 실행하고, `.github/workflows`의 의존성 설치 후 단계에도 동일한 생성
단계를 추가하세요. `aggregateCommentCounts`의 `ReviewComment` 사용이 생성된 클라이언트로 정상 컴파일되도록 기존
검증 및 lint/tsc 흐름은 유지하세요.
Source: Linters/SAST tools
There was a problem hiding this comment.
false positive: @prisma/client postinstall이 yarn install 시 prisma generate를 자동 실행. 이 PR 포함 모든 CI check(lint+tsc)가 green인 것이 실증. validate에 generate 단계 추가 불필요.
| const hasMore = rows.length > limit; | ||
| const page = hasMore ? rows.slice(0, limit) : rows; | ||
|
|
||
| return { | ||
| items: page.map((row) => toReviewCommentItem(row, accountId)), | ||
| totalCount, | ||
| hasMore, | ||
| nextCursor: hasMore ? page[page.length - 1].id.toString() : null, |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
limit이 0일 때 빈 페이지의 마지막 요소를 참조하여 TypeError가 발생합니다. 두 위치 모두 hasMore를 rows.length > limit으로 계산한 뒤 페이지의 마지막 요소를 인덱싱합니다. limit이 0이면 저장소가 take: 1로 1건을 조회하므로 hasMore는 true가 되고 페이지는 빈 배열이 됩니다. 근본 원인은 하나입니다. 페이지가 비어 있을 때 커서를 만들지 않도록 막아야 합니다. 함께 ProductReviewsInput과 ReviewCommentsInput이 limit에 최소값 1을 강제하는지 확인하세요.
src/features/product/services/product-review.service.ts#L118-L125:nextCursor계산에서page.length > 0조건을 추가하세요.src/features/product/services/product-review.service.ts#L156-L178: 좋아요순 경로의last사용과 최신순 경로의pageIds[pageIds.length - 1]사용에 같은 조건을 추가하세요.
🐛 제안 수정
- const hasMore = rows.length > limit;
+ const hasMore = rows.length > limit && limit > 0;
const page = hasMore ? rows.slice(0, limit) : rows;
return {
items: page.map((row) => toReviewCommentItem(row, accountId)),
totalCount,
hasMore,
- nextCursor: hasMore ? page[page.length - 1].id.toString() : null,
+ nextCursor:
+ hasMore && page.length > 0
+ ? page[page.length - 1].id.toString()
+ : null,
};- const hasMore = rows.length > args.limit;
+ const hasMore = rows.length > args.limit && args.limit > 0;
const page = hasMore ? rows.slice(0, args.limit) : rows;
- const last = page[page.length - 1];
+ const last = page.at(-1);
return {
pageIds: page.map((row) => row.id),
hasMore,
- nextCursor: hasMore ? `${last.likeCount}:${last.id.toString()}` : null,
+ nextCursor:
+ hasMore && last !== undefined
+ ? `${last.likeCount}:${last.id.toString()}`
+ : null,
};📍 Affects 1 file
src/features/product/services/product-review.service.ts#L118-L125(this comment)src/features/product/services/product-review.service.ts#L156-L178
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/product/services/product-review.service.ts` around lines 118 -
125, Prevent empty-page cursor access in ProductReviewService: at
src/features/product/services/product-review.service.ts lines 118-125, require
page.length > 0 when calculating nextCursor; apply the same guard to last and
pageIds[pageIds.length - 1] in lines 156-178. Also verify ProductReviewsInput
and ReviewCommentsInput enforce a minimum limit of 1.
There was a problem hiding this comment.
false positive: ProductReviewsInput.limit/ReviewCommentsInput.limit 모두 @min(1) 강제(class-validator) — limit=0은 서비스 도달 전 BAD_USER_INPUT. hasMore=true면 page는 항상 non-empty.
|
|
||
| it('description은 trim 저장, 공백뿐이면 null 정규화', async () => { | ||
| const { accountId, product } = await setupProductForSeller(); | ||
| const withText = await service.sellerCreateOptionGroup(accountId, { | ||
| productId: product.id.toString(), | ||
| name: '맛', | ||
| description: ' 크림 설명 ', | ||
| }); | ||
| expect(withText.description).toBe('크림 설명'); | ||
|
|
||
| const blank = await service.sellerCreateOptionGroup(accountId, { | ||
| productId: product.id.toString(), | ||
| name: '사이즈', | ||
| description: ' ', | ||
| }); | ||
| expect(blank.description).toBeNull(); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
DB 의존성을 mock 또는 stub으로 교체하세요.
이 테스트는 실제 Prisma 데이터베이스 경로를 사용합니다. DB 연결 상태와 잔존 데이터가 테스트 결과를 변경할 수 있습니다. ProductRepository, SellerRepository, 감사 로그 저장소를 test double로 교체하세요.
src/features/seller/services/seller-option.service.spec.ts#L102-L118: 생성 시 설명 정규화 테스트에서 DB 의존성을 stub으로 교체하세요.src/features/seller/services/seller-option.service.spec.ts#L175-L190: 수정 시 설명 정규화 테스트에서 DB 의존성을 stub으로 교체하세요.
As per path instructions: src/**/*.spec.ts: 테스트는 시간/uuid/네트워크/DB 의존성을 mock 또는 stub으로 통제하는지, 정상 흐름뿐 아니라 주요 예외/분기 케이스가 포함되는지 확인하세요.
📍 Affects 1 file
src/features/seller/services/seller-option.service.spec.ts#L102-L118(this comment)src/features/seller/services/seller-option.service.spec.ts#L175-L190
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/seller/services/seller-option.service.spec.ts` around lines 102
- 118, Replace real database access with mocks or stubs in the
description-normalization creation test at
src/features/seller/services/seller-option.service.spec.ts:102-118, covering
ProductRepository, SellerRepository, and the audit-log repository while
preserving the trim and blank-to-null assertions. Apply the same test-double
setup to the update description-normalization test at
src/features/seller/services/seller-option.service.spec.ts:175-190; both sites
require direct changes.
Source: Path instructions
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
설명 최대 길이 경계를 테스트하세요.
새 제한값은 1000입니다. 현재 테스트는 trim과 빈 문자열만 검증합니다. 생성과 수정 모두에서 1000자는 성공하고 1001자는 BadRequestException을 반환하는 테스트를 추가하세요.
src/features/seller/services/seller-option.service.spec.ts#L102-L118: 생성 요청의 1000자 및 1001자 경계를 검증하세요.src/features/seller/services/seller-option.service.spec.ts#L175-L190: 수정 요청의 1000자 및 1001자 경계를 검증하세요.
As per path instructions: src/**/*.spec.ts: 테스트는 시간/uuid/네트워크/DB 의존성을 mock 또는 stub으로 통제하는지, 정상 흐름뿐 아니라 주요 예외/분기 케이스가 포함되는지 확인하세요.
📍 Affects 1 file
src/features/seller/services/seller-option.service.spec.ts#L102-L118(this comment)src/features/seller/services/seller-option.service.spec.ts#L175-L190
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/seller/services/seller-option.service.spec.ts` around lines 102
- 118, In src/features/seller/services/seller-option.service.spec.ts lines
102-118, extend the sellerCreateOptionGroup tests to verify a 1000-character
description succeeds and a 1001-character description rejects with
BadRequestException, while preserving the existing trim and blank normalization
checks. In the same file lines 175-190, add equivalent boundary assertions for
the update request, covering successful 1000-character input and
BadRequestException for 1001 characters; keep test dependencies controlled
through the existing setup and service stubs.
Source: Path instructions
There was a problem hiding this comment.
미반영: 레포 전체가 Testcontainers 실DB 통합 테스트 아키텍처(173 suites, truncate 격리로 결정적). 해당 스펙만 mock 전환 시 컨벤션 일관성 훼손. 시간/uuid/네트워크는 통제 중이며 DB는 의도적 실DB.
| // 리뷰 재작성(createOrRestoreReviewWithMedia)이 같은 review id를 복원하므로 | ||
| // 댓글을 남겨두면 삭제 전 댓글이 새 리뷰에 되살아난다. 함께 정리한다. | ||
| await tx.reviewComment.updateMany({ | ||
| where: { | ||
| review_id: args.reviewId, | ||
| deleted_at: null, | ||
| }, | ||
| data: { deleted_at: args.now }, | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 스키마 모델 정의와 generate 파이프라인을 확인한다.
rg -n 'model ReviewComment' -A 20 prisma/schema.prisma
rg -n 'prisma generate|postinstall|"lint"|"validate"' package.jsonRepository: CaQuick/caquick-be
Length of output: 1229
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files matching review.repository.ts =="
git ls-files | rg 'review\.repository\.ts|prisma/schema\.prisma|package\.json|\.eslintrc|eslint\.config|commitlint|pre-commit|husky' || true
echo
echo "== target repository relevant lines =="
fd -a 'review\.repository\.ts' . | while read -r f; do
echo "-- $f"
sed -n '180,225p' "$f" | cat -n
done
echo
echo "== prisma schema client and generation directives =="
if [ -f prisma/schema.prisma ]; then
sed -n '1,60p' prisma/schema.prisma
echo "-- generation commands"
rg -n '^\s*"postinstall"|prisma generate|--postinstall|\.prisma/client|Client\\|Generator' package.json scripts/* 2>/dev/null || true
fi
echo
echo "== lint/typecheck scripts =="
if [ -f package.json ]; then
cat package.json
fiRepository: CaQuick/caquick-be
Length of output: 9510
prisma:generate를 lint/validate 전 실행 경로에 추가하세요.
model ReviewComment는 스키마에 있지만, package.json의 lint나 validate가 prisma generate를 호출하지 않습니다. lint/validate를 직접 실행하면 tx.reviewComment의 Prisma Client 타입이 생성되지 않아 타입체크가 실패합니다. CI 같은 실행 경로에서는 prisma:generate를 먼저 수행하도록 추가하세요.
🧰 Tools
🪛 ESLint
[error] 207-207: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
[error] 207-207: Unsafe member access .updateMany on a type that cannot be resolved.
(@typescript-eslint/no-unsafe-member-access)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/user/repositories/review.repository.ts` around lines 205 - 213,
Update the package scripts that run lint and validate so they invoke
prisma:generate first, ensuring the ReviewComment client used by the transaction
code is generated before type checking. Preserve the existing lint and validate
commands after generation.
Source: Linters/SAST tools
There was a problem hiding this comment.
false positive: 위 3706684309와 동일 — @prisma/client postinstall이 generate 수행, CI check green 실증.
| return this.prisma.$transaction(async (tx) => { | ||
| const locked = await tx.$queryRaw<{ id: bigint }[]>(Prisma.sql` | ||
| SELECT r.id | ||
| FROM review r | ||
| JOIN product p | ||
| ON p.id = r.product_id AND p.is_active = 1 AND p.deleted_at IS NULL | ||
| JOIN store s | ||
| ON s.id = p.store_id AND s.is_active = 1 AND s.deleted_at IS NULL | ||
| WHERE r.id = ${args.reviewId} AND r.deleted_at IS NULL | ||
| FOR SHARE OF r | ||
| `); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
MySQL 8.0.22 SELECT FOR SHARE OF syntax minimum version requirement
💡 Result:
The SELECT ... FOR SHARE OF table_name syntax was introduced in MySQL 8.0 [1][2]. This syntax, along with the FOR SHARE clause itself and the NOWAIT and SKIP LOCKED options, was part of the feature set implemented as part of MySQL 8.0 development (worklog WL#3597) [3][2]. While FOR SHARE serves as a functional replacement for the legacy LOCK IN SHARE MODE syntax, the OF table_name clause specifically allows for fine-grained locking of only the specified tables within a query [4][3][1]. This functionality has been available since the initial release of the MySQL 8.0 series [1][2].
Citations:
- 1: https://oneuptime.com/blog/post/2026-03-31-mysql-select-for-share/view
- 2: https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html
- 3: https://dev.mysql.com/worklog/task/?id=3597
- 4: https://dev.mysql.com/doc/refman/9.7/en/select.html
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked files mentioning mysql/database version/config:"
rg -n -i "mysql|database|version|FOR SHARE|FOR UPDATE|LOCK IN SHARE MODE" \
-g '!node_modules' -g '!dist' -g '!build' \
. | head -200
echo
echo "Relevant repository file section:"
if [ -f src/features/user/repositories/user.repository.ts ]; then
wc -l src/features/user/repositories/user.repository.ts
sed -n '620,680p' src/features/user/repositories/user.repository.ts
else
fd -a 'user.repository.ts' .
fiRepository: CaQuick/caquick-be
Length of output: 14006
프로덕션 환경을 MySQL 8.x 이상으로 제한하십시오.
FOR SHARE OF r은 MySQL 8 이상에서만 지원됩니다. README가 MySQL 8, Testcontainers가 mysql:8.0, Pro provider가 mysql:8.0 기반이지만, 다른 DB 버전/엔진 환경에서 같은 쿼리가 실행되면 댓글 생성 경로가 실패합니다. MySQL <8.0 환경에서는 FOR SHARE로 단순화하거나 다른 잠금 전략을 사용하여 문법 호환성을 맞춰야 합니다.
🧰 Tools
🪛 ESLint
[error] 636-657: Unsafe return of a value of type Promise<any>.
(@typescript-eslint/no-unsafe-return)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/features/user/repositories/user.repository.ts` around lines 636 - 646,
Update the transaction query in the review repository method containing the
locked review lookup so it remains compatible with the supported MySQL versions:
replace the MySQL 8-only `FOR SHARE OF r` syntax with compatible `FOR SHARE`
syntax or the project’s established alternative locking strategy, while
preserving the intended shared lock behavior.
There was a problem hiding this comment.
미반영: 전 환경 MySQL 8.0 고정(README·docker-compose·Testcontainers·운영 RDS). <8.0 지원 계획 없음 — fail-fast 원칙상 하위 호환 분기를 두지 않는다.
fix: 릴리즈 리뷰 반영 (seller 조회 nested soft-delete 가드·댓글 삭제 명시 가드)
Summary
상품 상세 화면(상품 정보 탭·후기 탭·후기 상세·사진후기)용 구매자 API 릴리즈입니다. (PR #167, develop 머지 완료분)
productDetail— 상품 상세(이미지·가격/할인율·구매 전 필독사항·옵션 섹션·찜 여부·후기 카운트)productReviews— 상품 리뷰 목록(포토 필터, 최신순/좋아요순, 커서 페이지네이션, 커스텀 정보 스냅샷, 댓글 수)reviewDetail/reviewComments— 후기 상세 + 댓글 목록unlikeReview/writeReviewComment/deleteMyReviewComment— 좋아요 해제·댓글 작성/본인 삭제description지원Scope
review_comment테이블 신설 +product_option_group.description컬럼 /review_comment인덱스(review_id, id)교체진행 상황
yarn validate통과 (173 suites / 1,462 tests)Impact
prisma migrate deploy로 마이그레이션 2건 적용 필요 (테이블 신설·nullable 컬럼·인덱스 교체 — 잠금 부담 낮음)likeReview동작 개선: 해제 후 재좋아요 시 unique 충돌 버그 수정(soft-delete 복원), 복원 시 알림 미발송Test plan
Summary by CodeRabbit