Skip to content

feat: 상품 상세·후기 API 구현 (productDetail·productReviews·리뷰 댓글/좋아요 해제) - #167

Merged
chanwoo7 merged 10 commits into
developfrom
feat/product-detail-and-reviews
Aug 3, 2026
Merged

feat: 상품 상세·후기 API 구현 (productDetail·productReviews·리뷰 댓글/좋아요 해제)#167
chanwoo7 merged 10 commits into
developfrom
feat/product-detail-and-reviews

Conversation

@chanwoo7

@chanwoo7 chanwoo7 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

상품 상세(상품 정보 탭·후기 탭·후기 상세·사진후기) 화면에 필요한 구매자용 API 일체를 구현합니다.

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

Scope

  • Prisma: ReviewComment 모델 신설(+마이그레이션), ProductOptionGroup.description 컬럼 추가, soft-delete 미들웨어에 ReviewComment 등록
  • product feature: product-detail.graphql / product-reviews.graphql SDL 신설, ProductReviewRepository 신설, detail/review 서비스·매퍼·리졸버·DTO·상수 추가
  • user feature: user-engagement.graphqlunlikeReview·writeReviewComment·deleteMyReviewComment 추가, UserRepository에 unlike/댓글 메서드 추가
  • seller feature: 옵션 그룹 생성/수정 input·출력 타입에 description 반영
  • 버그 수정: 좋아요 해제 후 재좋아요 시 uk_review_like 유니크 제약 충돌 → soft-delete된 좋아요 복원 방식으로 수정 (재좋아요 시 알림은 최초 1회만 발송)

진행 상황

  • Prisma 스키마 변경 + 마이그레이션 생성/적용
  • 판매자 옵션 그룹 description 반영 + 테스트
  • productDetail 쿼리 + 테스트
  • productReviews / reviewDetail / reviewComments 쿼리 + 테스트
  • unlikeReview / 댓글 작성·삭제 뮤테이션 + 테스트
  • yarn validate 전체 통과 (173 suites / 1,450 tests)

Impact

  • 기존 API 파괴적 변경 없음 (신규 쿼리/뮤테이션 + nullable 컬럼 추가만)
  • likeReview 동작 변경: 해제했던 좋아요 복원 시 알림 미발송(스팸 방지), 복원 실패 버그 해소
  • 공개 조회 쿼리는 모두 비로그인 접근 가능(OptionalJwtAuthGuard), nested relation 전체에 soft-delete/활성 가드 명시
  • 좋아요순 정렬은 soft-delete 좋아요 제외 집계가 필요해 raw SQL 키셋 페이지네이션으로 구현 ((likeCount, id) 복합 커서)

Test plan

  • ProductDetailService 단위 테스트: NOT_FOUND 분기(비활성 상품/매장), 이미지 정렬·soft-delete 제외, 옵션 그룹/아이템 활성 필터·그룹 설명, 할인율, 리뷰 카운트, 찜 여부
  • ProductReviewService 단위 테스트: 최신순/좋아요순(동률 tie-break, soft-delete 좋아요 제외, 키셋 커서), 포토 필터, 커스텀 정보 스냅샷, 댓글 수 집계, 탈퇴 작성자 익명화, isMine
  • UserEngagementService 단위 테스트: unlike 멱등/복원, 댓글 작성 trim/삭제 권한(Forbidden)/soft-delete 리뷰 방어
  • Resolver 통합 테스트: 3개 리졸버 각각 비로그인/로그인 경로 + 에러 전파
  • DTO 검증 테스트: 댓글 content trim/길이(1~500)

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 046ba067-377f-4f25-ab51-cefdf749bd1f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

🧹 knip — dead-code 리포트

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

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

@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

@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: 999c3f219f

ℹ️ 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 +627 to +629
const review = await this.prisma.review.findFirst({
where: { id: args.reviewId },
select: { id: true },

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 Reject comments on non-public reviews

When a review's product or store has been deactivated or soft-deleted, this lookup still succeeds because it checks only the review ID. The mutation consequently returns a successfully created comment, but reviewComments immediately rejects the same review through existsPublicReview, so the user cannot retrieve the comment they just posted. Apply the same review/product/store visibility predicate used by the public read path before inserting.

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.

반영: createReviewComment 리뷰 조회에 product/store 활성 가드 추가 (5e9e201). 공개 조회(existsPublicReview)와 동일 조건.

Comment thread prisma/schema.prisma
media ReviewMedia[]

likes ReviewLike[]
comments ReviewComment[]

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 Soft-delete comments when their review is deleted

Adding comments to reviews without extending ReviewRepository.softDeleteReview leaves all comment rows active when a review is deleted. Because createOrRestoreReviewWithMedia later restores that same review ID and replaces its content/media, every comment from the deleted version becomes visible again on the rewritten review. Soft-delete the associated comments in the existing review-deletion transaction, as is already done for review media.

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.

반영: softDeleteReview 트랜잭션에서 댓글도 soft-delete (5e9e201). 재작성 복원 시 옛 댓글 미노출 회귀 테스트 추가.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.62% 4298/4403
🟢 Branches 93.67% 1362/1454
🟢 Functions 95.27% 826/867
🟢 Lines 98.02% 3916/3995

Test suite run success

1462 tests passing in 173 suites.

Report generated by 🧪jest coverage report action from 654f438

@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!

@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: 5e9e201694

ℹ️ 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 +129 to +132
? Prisma.sql`HAVING COUNT(l.id) < (
SELECT COUNT(*) FROM review_like cl
WHERE cl.review_id = ${args.cursor} AND cl.deleted_at IS 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.

P2 Badge Encode the like count in pagination cursors

When the cursor review's likes change between requests, this subquery uses its new count rather than the count that determined the previous page boundary. For example, if page 1 ends at a review with four likes and it gains likes before page 2, reviews already returned with counts between four and the new count satisfy this HAVING clause and appear again; losing likes can instead skip unseen reviews. Include the boundary like count in the cursor (along with the ID) so the composite key remains stable across requests.

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.

반영: 커서를 ":" 불투명 토큰으로 변경, 경계 시점 좋아요 수를 그대로 이어받도록 수정. 경계 리뷰 좋아요 변동 시 중복 미발생 회귀 테스트 추가.

@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: 24cdde7f1a

ℹ️ 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 +641 to +644
return this.prisma.reviewComment.create({
data: {
review_id: args.reviewId,
account_id: args.accountId,

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 Serialize comment creation with review deletion

If the review owner deletes the review after the findFirst check but before this insert, the deletion transaction has already soft-deleted all existing comments, yet this insert still succeeds because the review row remains physically present. The new comment is hidden until createOrRestoreReviewWithMedia restores the same review ID, at which point it appears on the rewritten review; the separate check and insert are fresh evidence beyond the previously fixed cleanup issue. Lock or recheck the review while coordinating the insert with the deletion transaction.

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.

반영: 체크+insert를 트랜잭션으로 묶고 리뷰 row FOR SHARE 잠금 (d264073). 삭제 tx의 review UPDATE와 직렬화 — 잠금 선점 시 삭제 tx의 댓글 정리가 신규 댓글을 포함하고, 반대 순서면 not-found.

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 Stabilize every ranked row across pagination requests

The encoded cursor stabilizes only the boundary review, while this HAVING clause still evaluates every other review's current like count. For example, if page 1 returns A(5) and B(4), then A is unliked down to 2 before page 2, A now satisfies COUNT(l.id) < 4 and is returned again; conversely, an unseen review that rises above 4 is skipped. The newly added unlikeReview path makes downward movement a normal input, so preserving the boundary count alone does not prevent duplicates or omissions; the ranking needs a request-stable snapshot/version or another immutable paging key.

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.

미반영(의도적): 경계 외 row까지 요청 간 고정하려면 랭킹 스냅샷/버전 인프라가 필요 — 리뷰 목록 대비 과설계로 판단. 경계 커서 고정으로 계통적 중복(경계 drift)은 제거됐고, 잔여 anomaly는 비경계 row의 좋아요 수가 요청 사이에 경계를 넘는 경우로 드물며 FE id dedup으로 흡수 가능. 커서는 불투명 토큰으로 문서화되어 추후 스냅샷 도입 여지는 열려 있음.

@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: d264073f44

ℹ️ 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".

if (!match) {
throw new BadRequestException(PRODUCT_REVIEW_ERRORS.INVALID_LIKES_CURSOR);
}
return { likeCount: Number(match[1]), id: BigInt(match[2]) };

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 Reject non-finite like-count cursors

When a client supplies a digit-only like count large enough to overflow JavaScript numbers, such as a 309-digit value, it passes the regex but Number(match[1]) becomes Infinity. That non-finite boundary is then passed to the raw SQL comparison, producing an invalid/incorrect query result instead of the intended BadRequestException; validate that the parsed count is a safe non-negative integer before using it.

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.

반영: parseLikesCursor에 Number.isSafeInteger 검증 추가, 범위 초과 시 BAD_USER_INPUT. 309자리 커서 회귀 테스트 포함.

`updated_at` DATETIME(3) NOT NULL,
`deleted_at` DATETIME(3) NULL,

INDEX `idx_review_comment_review`(`review_id`, `created_at`),

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 Index comments by the pagination key

For reviews with many comments, this index cannot efficiently serve listReviewComments, whose predicate is review_id = ... AND id > ... and whose ordering is id ASC. MySQL must either sort the review's matching rows or scan the primary-key range while filtering other reviews on every page; using (review_id, id) aligns the index with the cursor query and keeps page retrieval proportional to the requested limit.

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.

반영: 인덱스를 (review_id, id)로 교체하는 마이그레이션 추가. FK가 인덱스를 요구하므로 DROP/ADD 단일 ALTER로 원자 처리. created_at 정렬 사용처 없음 확인.

@chanwoo7
chanwoo7 merged commit 26e56e4 into develop Aug 3, 2026
11 checks passed
@chanwoo7
chanwoo7 deleted the feat/product-detail-and-reviews branch August 3, 2026 18:00
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