feat: 상품 상세·후기 API 구현 (productDetail·productReviews·리뷰 댓글/좋아요 해제) - #167
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
🧹 knip — dead-code 리포트전체 리포트
|
🩺 NestJS Doctor — 89/100 (Good)진단 270건 (error 0).
architecture / security 상위 항목
|
There was a problem hiding this comment.
💡 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".
| const review = await this.prisma.review.findFirst({ | ||
| where: { id: args.reviewId }, | ||
| select: { id: true }, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
반영: createReviewComment 리뷰 조회에 product/store 활성 가드 추가 (5e9e201). 공개 조회(existsPublicReview)와 동일 조건.
| media ReviewMedia[] | ||
|
|
||
| likes ReviewLike[] | ||
| comments ReviewComment[] |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
반영: softDeleteReview 트랜잭션에서 댓글도 soft-delete (5e9e201). 재작성 복원 시 옛 댓글 미노출 회귀 테스트 추가.
Coverage report
Test suite run success1462 tests passing in 173 suites. Report generated by 🧪jest coverage report action from 654f438 |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
💡 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".
| ? Prisma.sql`HAVING COUNT(l.id) < ( | ||
| SELECT COUNT(*) FROM review_like cl | ||
| WHERE cl.review_id = ${args.cursor} AND cl.deleted_at IS NULL | ||
| ) |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
반영: 커서를 ":" 불투명 토큰으로 변경, 경계 시점 좋아요 수를 그대로 이어받도록 수정. 경계 리뷰 좋아요 변동 시 중복 미발생 회귀 테스트 추가.
There was a problem hiding this comment.
💡 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".
| return this.prisma.reviewComment.create({ | ||
| data: { | ||
| review_id: args.reviewId, | ||
| account_id: args.accountId, |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
반영: 체크+insert를 트랜잭션으로 묶고 리뷰 row FOR SHARE 잠금 (d264073). 삭제 tx의 review UPDATE와 직렬화 — 잠금 선점 시 삭제 tx의 댓글 정리가 신규 댓글을 포함하고, 반대 순서면 not-found.
| ? 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.
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 👍 / 👎.
There was a problem hiding this comment.
미반영(의도적): 경계 외 row까지 요청 간 고정하려면 랭킹 스냅샷/버전 인프라가 필요 — 리뷰 목록 대비 과설계로 판단. 경계 커서 고정으로 계통적 중복(경계 drift)은 제거됐고, 잔여 anomaly는 비경계 row의 좋아요 수가 요청 사이에 경계를 넘는 경우로 드물며 FE id dedup으로 흡수 가능. 커서는 불투명 토큰으로 문서화되어 추후 스냅샷 도입 여지는 열려 있음.
There was a problem hiding this comment.
💡 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]) }; |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
반영: 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`), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
반영: 인덱스를 (review_id, id)로 교체하는 마이그레이션 추가. FK가 인덱스를 요구하므로 DROP/ADD 단일 ALTER로 원자 처리. created_at 정렬 사용처 없음 확인.
Summary
상품 상세(상품 정보 탭·후기 탭·후기 상세·사진후기) 화면에 필요한 구매자용 API 일체를 구현합니다.
productDetail— 상품 상세(이미지 캐러셀·가격/할인율·구매 전 필독사항·옵션 섹션·찜 여부·후기 카운트)productReviews— 상품 단위 리뷰 목록(포토 리뷰 필터, 최신순/좋아요순 정렬, 커서 페이지네이션, 커스텀 정보 스냅샷, 댓글 수)reviewDetail— 후기 상세(리뷰 본문 전문 + 판매 케이크 정보 카드)reviewComments— 리뷰 댓글 목록(등록순, 커서)unlikeReview/writeReviewComment/deleteMyReviewComment— 좋아요 해제·댓글 작성/본인 삭제description등록/수정/노출 지원 (옵션 섹션 인트로 문구)Scope
ReviewComment모델 신설(+마이그레이션),ProductOptionGroup.description컬럼 추가, soft-delete 미들웨어에ReviewComment등록product-detail.graphql/product-reviews.graphqlSDL 신설,ProductReviewRepository신설, detail/review 서비스·매퍼·리졸버·DTO·상수 추가user-engagement.graphql에unlikeReview·writeReviewComment·deleteMyReviewComment추가,UserRepository에 unlike/댓글 메서드 추가description반영uk_review_like유니크 제약 충돌 → soft-delete된 좋아요 복원 방식으로 수정 (재좋아요 시 알림은 최초 1회만 발송)진행 상황
yarn validate전체 통과 (173 suites / 1,450 tests)Impact
likeReview동작 변경: 해제했던 좋아요 복원 시 알림 미발송(스팸 방지), 복원 실패 버그 해소(likeCount, id)복합 커서)Test plan
ProductDetailService단위 테스트: NOT_FOUND 분기(비활성 상품/매장), 이미지 정렬·soft-delete 제외, 옵션 그룹/아이템 활성 필터·그룹 설명, 할인율, 리뷰 카운트, 찜 여부ProductReviewService단위 테스트: 최신순/좋아요순(동률 tie-break, soft-delete 좋아요 제외, 키셋 커서), 포토 필터, 커스텀 정보 스냅샷, 댓글 수 집계, 탈퇴 작성자 익명화, isMineUserEngagementService단위 테스트: unlike 멱등/복원, 댓글 작성 trim/삭제 권한(Forbidden)/soft-delete 리뷰 방어