Skip to content

chore: 상품 상세·후기 조회/댓글 API 릴리즈 - #168

Merged
chanwoo7 merged 13 commits into
mainfrom
develop
Aug 3, 2026
Merged

chore: 상품 상세·후기 조회/댓글 API 릴리즈#168
chanwoo7 merged 13 commits into
mainfrom
develop

Conversation

@chanwoo7

@chanwoo7 chanwoo7 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

상품 상세 화면(상품 정보 탭·후기 탭·후기 상세·사진후기)용 구매자 API 릴리즈입니다. (PR #167, develop 머지 완료분)

  • productDetail — 상품 상세(이미지·가격/할인율·구매 전 필독사항·옵션 섹션·찜 여부·후기 카운트)
  • productReviews — 상품 리뷰 목록(포토 필터, 최신순/좋아요순, 커서 페이지네이션, 커스텀 정보 스냅샷, 댓글 수)
  • reviewDetail / reviewComments — 후기 상세 + 댓글 목록
  • unlikeReview / writeReviewComment / deleteMyReviewComment — 좋아요 해제·댓글 작성/본인 삭제
  • 판매자 옵션 그룹 description 지원

Scope

  • Prisma 마이그레이션 2건: review_comment 테이블 신설 + product_option_group.description 컬럼 / review_comment 인덱스 (review_id, id) 교체
  • product feature: 상세·리뷰 SDL/리포지토리/서비스/리졸버 신설
  • user feature: engagement 뮤테이션 3종 추가, 재좋아요 복원 처리
  • seller feature: 옵션 그룹 description 등록/수정/노출

진행 상황

Impact

  • 기존 API 파괴적 변경 없음 (신규 쿼리/뮤테이션 + nullable 컬럼 추가)
  • 배포 시 prisma migrate deploy로 마이그레이션 2건 적용 필요 (테이블 신설·nullable 컬럼·인덱스 교체 — 잠금 부담 낮음)
  • likeReview 동작 개선: 해제 후 재좋아요 시 unique 충돌 버그 수정(soft-delete 복원), 복원 시 알림 미발송

Test plan

  • 서비스 단위 테스트: 상세/리뷰 목록 필터·정렬·커서, soft-delete 가드, 탈퇴 작성자 익명화, 댓글 권한
  • 리졸버 통합 테스트: 비로그인/로그인 경로, 에러 전파
  • 좋아요순 커서 안정성: 경계 좋아요 변동 중복 방지, 형식/범위 검증
  • 리뷰 삭제↔댓글 정리, 재작성 복원 시 좀비 댓글 미발생
  • 신규 마이그레이션 fresh DB replay (테스트 컨테이너)

Summary by CodeRabbit

  • 새로운 기능
    • 상품 상세 정보를 조회할 수 있습니다.
    • 상품 리뷰 목록·상세·댓글을 조회하고, 사진 필터와 최신순·좋아요순 정렬 및 페이지네이션을 사용할 수 있습니다.
    • 리뷰 좋아요를 취소하고, 댓글을 작성하거나 본인 댓글을 삭제할 수 있습니다.
    • 옵션 그룹에 안내 문구를 등록·수정·조회할 수 있습니다.
  • 개선
    • 삭제된 리뷰와 댓글이 공개 조회에서 제외됩니다.
    • 탈퇴한 작성자의 리뷰·댓글 정보가 익명화됩니다.
    • 잘못된 상품·리뷰·댓글 요청에 대한 오류 처리가 강화되었습니다.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@chanwoo7, you've reached your PR review limit, so we couldn't start this review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c33f9193-ce9b-4148-897d-b3cd7ae638c1

📥 Commits

Reviewing files that changed from the base of the PR and between 26e56e4 and 9fdf264.

📒 Files selected for processing (3)
  • src/features/product/repositories/product.repository.ts
  • src/features/seller/services/seller-product-query.service.spec.ts
  • src/features/user/repositories/user.repository.ts
📝 Walkthrough

Walkthrough

상품 상세 조회와 상품 리뷰·댓글 조회 GraphQL API를 추가했습니다. 리뷰 좋아요 해제와 댓글 작성·삭제 mutation을 추가했습니다. 리뷰 댓글 저장소와 soft-delete 정책을 추가했습니다. 옵션 그룹 설명 필드를 판매자 입력과 상품 상세 응답에 연결했습니다.

Changes

상품 상세 조회

Layer / File(s) Summary
상품 상세 계약과 저장소
src/features/product/product-detail.graphql, src/features/product/types/*, src/features/product/repositories/product.repository.ts
상품 상세 타입과 옵션 구조를 추가했습니다. 활성 상품·매장, 이미지·옵션, 리뷰 수, 찜 상태를 조회합니다.
상품 상세 서비스와 리졸버
src/features/product/services/product-detail-*, src/features/product/resolvers/product-detail-query.resolver.ts, src/features/product/product.module.ts
상품 ID를 검증하고 상품 상세를 반환합니다. 비로그인 요청과 로그인 사용자의 찜 상태를 처리합니다.
상품 상세 통합 검증
src/features/product/services/product-detail.service.spec.ts, src/features/product/resolvers/product-detail-query.resolver.spec.ts
상품 상태, 정렬, soft-delete, 리뷰 수, 찜 상태와 예외를 실제 데이터베이스로 검증합니다.

상품 리뷰 조회

Layer / File(s) Summary
리뷰 조회 계약
src/features/product/product-reviews.graphql, src/features/product/dto/inputs/*, src/features/product/types/product-review-output.type.ts, src/features/product/constants/*
리뷰 목록·상세·댓글 조회 타입과 입력 검증을 추가했습니다. 최신순·좋아요순, 사진 필터, 커서와 페이지 크기를 정의했습니다.
리뷰 조회 저장소
src/features/product/repositories/product-review.repository.ts
활성 리뷰의 페이지 ID, 본문, 상세 상품, 미디어, 옵션, 작성자, 좋아요·댓글 집계를 조회합니다.
리뷰 페이지 서비스와 응답 매핑
src/features/product/services/product-review.service.ts, src/features/product/services/product-review-mappers.helper.ts, src/features/product/resolvers/product-review-query.resolver.ts
리뷰 목록·상세·댓글 조회를 구현했습니다. 좋아요순 키셋 커서와 삭제 경계 처리를 적용합니다.
리뷰 조회 통합 검증
src/features/product/services/product-review.service.spec.ts, src/features/product/resolvers/product-review-query.resolver.spec.ts
정렬, 필터, 페이지네이션, soft-delete, 집계, 익명화와 인증별 응답을 검증합니다.

리뷰 댓글 참여 기능

Layer / File(s) Summary
댓글 저장소와 삭제 정책
prisma/migrations/*, prisma/schema.prisma, src/prisma/soft-delete.middleware.ts, src/features/user/repositories/review.repository.ts
ReviewComment 모델과 복합 인덱스를 추가했습니다. 리뷰 삭제 시 연결 댓글을 soft-delete합니다.
댓글 입력과 mutation 계약
src/features/user/dto/inputs/write-review-comment.input.ts, src/features/user/user-engagement.graphql, src/features/user/resolvers/user-engagement-mutation.resolver.ts, src/features/user/types/*
댓글 작성 입력과 응답 타입을 추가했습니다. 좋아요 해제, 댓글 작성, 본인 댓글 삭제 mutation을 추가했습니다.
댓글 참여 저장소와 서비스
src/features/user/repositories/user.repository.ts, src/features/user/services/user-engagement.service.ts
soft-delete 좋아요 복원과 좋아요 해제를 구현했습니다. 활성 리뷰에 댓글을 생성하고 작성자 본인만 댓글을 삭제합니다.
댓글 참여 통합 검증
src/features/user/dto/inputs/write-review-comment.input.spec.ts, src/features/user/services/*spec.ts, src/features/user/resolvers/user-engagement.resolver.spec.ts
입력 길이·trim, 좋아요 멱등성, 댓글 생성·삭제 권한과 리뷰 삭제 후 댓글 상태를 검증합니다.

옵션 그룹 설명

Layer / File(s) Summary
옵션 그룹 설명 계약과 처리
prisma/schema.prisma, src/features/seller/constants/*, src/features/seller/dto/inputs/*, src/features/seller/seller-product.graphql, src/features/seller/services/*, src/features/seller/types/*
nullable 설명 필드와 1000자 제한을 추가했습니다. 생성·수정 시 trim과 공백 입력의 null 변환을 적용합니다.
옵션 그룹 설명 검증
src/features/seller/services/seller-option.service.spec.ts
옵션 그룹 설명의 기본값, trim, 공백 입력 처리와 수정 동작을 검증합니다.

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 연결 응답 반환
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 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 (3 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 상품 상세·후기 조회와 댓글 API 릴리즈라는 주요 변경 내용을 간결하게 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🩺 NestJS Doctor — 89/100 (Good)

진단 270건 (error 0).

Category error warning info
architecture 0 0 13
correctness 0 118 0
performance 0 24 16
schema 0 0 86
security 0 13 0
architecture / security 상위 항목
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'IAuditLogRepository'.
  • warning security/security/no-exposed-env-vars: Direct 'process.env.NODE_ENV' access in 'AuthController'. Use ConfigService instead.
  • warning security/security/require-guards-on-endpoints: Endpoint 'start' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'callback' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'refresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'logout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogin' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerRefresh' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'sellerLogout' has no @UseGuards() at class or method level.
  • warning security/security/require-guards-on-endpoints: Endpoint 'devIssueToken' has no @UseGuards() at class or method level.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/conversation/repositories/conversation.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'ConversationRepository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/order/repositories/order.repository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal type 'OrderRepository'.
  • info architecture/architecture/no-barrel-export-internals: Barrel file re-exports internal module '@/features/product/repositories/product.repository'.

오탐 포함 가능 · 기준 docs/guide/architecture-conventions.md

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🧹 knip — dead-code 리포트

요약 항목 없음
전체 리포트
(knip 출력 없음 — 이슈 0이거나 실행 실패)

청소 후보(오탐 가능) · 기준 docs/guide/architecture-conventions.md

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.20319% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
.../product/services/product-review-mappers.helper.ts 95.23% 0 Missing and 1 partial ⚠️
...eatures/product/services/product-review.service.ts 98.52% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.68% 4301/4403
🟢 Branches 93.67% 1362/1454
🟢 Functions 95.62% 829/867
🟢 Lines 98.1% 3919/3995

Test suite run success

1463 tests passing in 173 suites.

Report generated by 🧪jest coverage report action from 9fdf264

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +130 to +131
? Prisma.sql`HAVING COUNT(l.id) < ${args.cursor.likeCount}
OR (COUNT(l.id) = ${args.cursor.likeCount} AND r.id < ${args.cursor.id})`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

미반영(기존 결정 유지): PR #167 동일 지적에 근거 dismiss 완료 — 전체 랭킹 스냅샷은 별도 인프라 필요로 과설계 판단. 경계 고정 커서로 계통적 중복 제거, 잔여 anomaly는 FE id dedup으로 흡수(문서 안내 포함).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 부분에도 범위 제한을 두는 방안을 검토하세요.

likeCountNumber.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이 설정된 매장에 대해 reviewDetailproductReviews(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.tsPRODUCT_REVIEW_SORTSPRODUCT_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.tsMAX_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으로 소프트 삭제된 항목의 제외를 각각 검증하지만, 옵션 그룹 자체에 대해서는 같은 검증이 없습니다. findProductDetailByIdoption_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

📥 Commits

Reviewing files that changed from the base of the PR and between c73071e and 26e56e4.

📒 Files selected for processing (47)
  • prisma/migrations/20260803164651_add_review_comment_and_option_group_description/migration.sql
  • prisma/migrations/20260803175209_index_review_comment_by_pagination_key/migration.sql
  • prisma/schema.prisma
  • src/features/product/constants/product-detail-error-messages.ts
  • src/features/product/constants/product-review-error-messages.ts
  • src/features/product/constants/product-review.constants.ts
  • src/features/product/dto/inputs/product-reviews.input.ts
  • src/features/product/dto/inputs/review-comments.input.ts
  • src/features/product/product-detail.graphql
  • src/features/product/product-reviews.graphql
  • src/features/product/product.module.ts
  • src/features/product/repositories/product-review.repository.ts
  • src/features/product/repositories/product.repository.ts
  • src/features/product/resolvers/product-detail-query.resolver.spec.ts
  • src/features/product/resolvers/product-detail-query.resolver.ts
  • src/features/product/resolvers/product-review-query.resolver.spec.ts
  • src/features/product/resolvers/product-review-query.resolver.ts
  • src/features/product/services/product-detail-mappers.helper.ts
  • src/features/product/services/product-detail.service.spec.ts
  • src/features/product/services/product-detail.service.ts
  • src/features/product/services/product-review-mappers.helper.ts
  • src/features/product/services/product-review.service.spec.ts
  • src/features/product/services/product-review.service.ts
  • src/features/product/types/product-detail-output.type.ts
  • src/features/product/types/product-review-output.type.ts
  • src/features/seller/constants/seller.constants.ts
  • src/features/seller/dto/inputs/seller-create-option-group.input.ts
  • src/features/seller/dto/inputs/seller-update-option-group.input.ts
  • src/features/seller/seller-product.graphql
  • src/features/seller/services/seller-option.service.spec.ts
  • src/features/seller/services/seller-option.service.ts
  • src/features/seller/services/seller-product-mappers.helper.ts
  • src/features/seller/types/seller-output.type.ts
  • src/features/user/constants/user-review-error-messages.ts
  • src/features/user/constants/user.constants.ts
  • src/features/user/dto/inputs/write-review-comment.input.spec.ts
  • src/features/user/dto/inputs/write-review-comment.input.ts
  • src/features/user/repositories/review.repository.ts
  • src/features/user/repositories/user.repository.ts
  • src/features/user/resolvers/user-engagement-mutation.resolver.ts
  • src/features/user/resolvers/user-engagement.resolver.spec.ts
  • src/features/user/services/user-engagement.service.spec.ts
  • src/features/user/services/user-engagement.service.ts
  • src/features/user/services/user-review.service.spec.ts
  • src/features/user/types/user-review-output.type.ts
  • src/features/user/user-engagement.graphql
  • src/prisma/soft-delete.middleware.ts

Comment on lines +330 to +340
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]));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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)
PY

Repository: CaQuick/caquick-be

Length of output: 317


Prisma client 생성을 CI 런타임 순서에 추가하세요.

ReviewComment 모델은 prisma/schema.prisma에 있지만, validate나 워크플로우의 lint/tscprisma: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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

false positive: @prisma/client postinstall이 yarn install 시 prisma generate를 자동 실행. 이 PR 포함 모든 CI check(lint+tsc)가 green인 것이 실증. validate에 generate 단계 추가 불필요.

Comment thread src/features/product/repositories/product.repository.ts
Comment on lines +118 to +125
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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

limit이 0일 때 빈 페이지의 마지막 요소를 참조하여 TypeError가 발생합니다. 두 위치 모두 hasMorerows.length > limit으로 계산한 뒤 페이지의 마지막 요소를 인덱싱합니다. limit이 0이면 저장소가 take: 1로 1건을 조회하므로 hasMore는 true가 되고 페이지는 빈 배열이 됩니다. 근본 원인은 하나입니다. 페이지가 비어 있을 때 커서를 만들지 않도록 막아야 합니다. 함께 ProductReviewsInputReviewCommentsInputlimit에 최소값 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

false positive: ProductReviewsInput.limit/ReviewCommentsInput.limit 모두 @min(1) 강제(class-validator) — limit=0은 서비스 도달 전 BAD_USER_INPUT. hasMore=true면 page는 항상 non-empty.

Comment on lines +102 to +118

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();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

미반영: 레포 전체가 Testcontainers 실DB 통합 테스트 아키텍처(173 suites, truncate 격리로 결정적). 해당 스펙만 mock 전환 시 컨벤션 일관성 훼손. 시간/uuid/네트워크는 통제 중이며 DB는 의도적 실DB.

Comment on lines +205 to +213
// 리뷰 재작성(createOrRestoreReviewWithMedia)이 같은 review id를 복원하므로
// 댓글을 남겨두면 삭제 전 댓글이 새 리뷰에 되살아난다. 함께 정리한다.
await tx.reviewComment.updateMany({
where: {
review_id: args.reviewId,
deleted_at: null,
},
data: { deleted_at: args.now },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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.json

Repository: 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
fi

Repository: CaQuick/caquick-be

Length of output: 9510


prisma:generatelint/validate 전 실행 경로에 추가하세요.

model ReviewComment는 스키마에 있지만, package.jsonlintvalidateprisma 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

false positive: 위 3706684309와 동일 — @prisma/client postinstall이 generate 수행, CI check green 실증.

Comment on lines +636 to +646
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
`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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


🏁 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' .
fi

Repository: 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

미반영: 전 환경 MySQL 8.0 고정(README·docker-compose·Testcontainers·운영 RDS). <8.0 지원 계획 없음 — fail-fast 원칙상 하위 호환 분기를 두지 않는다.

Comment thread src/features/user/repositories/user.repository.ts
@chanwoo7
chanwoo7 merged commit 992ddb6 into main Aug 3, 2026
15 checks passed
@chanwoo7
chanwoo7 deleted the develop branch August 3, 2026 19:02
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