Skip to content

chore: 릴리즈 — storeReviews 좋아요순 정렬 - #174

Merged
chanwoo7 merged 1 commit into
mainfrom
develop
Aug 6, 2026
Merged

chore: 릴리즈 — storeReviews 좋아요순 정렬#174
chanwoo7 merged 1 commit into
mainfrom
develop

Conversation

@chanwoo7

@chanwoo7 chanwoo7 commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

storeReviews 좋아요순 정렬 추가 릴리즈입니다.

  • feat: storeReviews 좋아요순 정렬(sort=LIKES) 추가 #173 feat: StoreReviewsInput.sort(LATEST/LIKES) 추가 — FE가 페이지 내 재정렬로는 전체 기준 좋아요순을 만들 수 없던 문제 해결. productReviews와 동일 의미론(soft-delete 좋아요 제외 집계, 동률 최신순, "<likeCount>:<id>" 키셋 커서 + 안전 정수 검증).

Scope

  • src/features/store/ — SDL(enum·sort 입력)·DTO·repository(id 페이지 + hydrate 재편, raw 키셋)·service·에러 상수 (additive 변경)

진행 상황

Impact

  • 매장 후기 좋아요순이 전체 기준으로 정확해짐 (FE는 sort: LIKES만 넘기면 됨).
  • 기존 호출 영향 없음: sort 기본 LATEST, LATEST 커서는 기존 id 방식 그대로.
  • LIKES 커서는 동일 sort 안에서만 유효 — productReviews와 동일 규칙. DB 마이그레이션 없음.

Test plan

  • 좋아요순 정렬·photoOnly 조합·키셋 커서 이어받기·잘못된 커서(BAD_USER_INPUT)·resolver 전달 등 5케이스 추가
  • store 스위트 13개(81건) 포함 전체 테스트 green

Summary by CodeRabbit

  • 새 기능

    • 매장 리뷰를 최신순 또는 좋아요순으로 정렬할 수 있습니다.
    • 좋아요순 리뷰 조회에 커서 기반 페이지네이션을 지원합니다.
    • 정렬 기준에 맞는 다음 페이지 커서를 제공합니다.
    • 사진이 포함된 리뷰만 조회하는 옵션과 정렬을 함께 사용할 수 있습니다.
  • 버그 수정

    • 잘못된 커서 형식이나 유효하지 않은 좋아요 수 입력 시 명확한 오류를 표시합니다.
    • 페이지 이동 중 리뷰 순서가 안정적으로 유지되며, 삭제된 리뷰는 결과에서 제외됩니다.

FE 요청 반영. 페이지 내 클라이언트 정렬로는 전체 기준 좋아요순이
불가능해 productReviews와 동일 의미론의 sort(LATEST/LIKES)를 추가한다.

- repository를 id 페이지 + hydrate 구조로 재편(product 미러)
- 좋아요순은 soft-delete 좋아요 제외 집계 기준이라 raw 키셋
  페이지네이션((likeCount, id) 커서) 사용
- "<likeCount>:<id>" 커서 파싱에 안전 정수 검증 포함
- 기존 호출 영향 없음(sort 기본 LATEST, LATEST 커서는 기존 id 방식)
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

스토어 리뷰 조회에 LATESTLIKES 정렬을 추가했다. 좋아요순 조회는 (likeCount, id) 키셋 커서를 사용한다. 서비스는 리뷰 ID 페이지를 조회한 뒤 본문과 좋아요 정보를 hydrate한다. GraphQL 입력과 통합 테스트도 변경했다.

Changes

스토어 리뷰 정렬 및 페이지네이션

Layer / File(s) Summary
정렬 입력 및 GraphQL 계약
src/features/store/store-reviews.graphql, src/features/store/dto/inputs/store-reviews.input.ts
StoreReviewsInput에 기본값이 LATESTsort 필드를 추가했다. LATESTLIKES를 허용하고, 커서를 동일한 정렬 기준의 불투명한 토큰으로 설명한다.
정렬별 ID 조회와 hydrate
src/features/store/services/store-review.service.ts, src/features/store/repositories/store-review.repository.ts, src/features/store/constants/store-review-error-messages.ts
최신순 및 좋아요순으로 리뷰 ID를 페이지 조회한다. 좋아요순은 삭제되지 않은 좋아요 수와 (likeCount, id) 커서를 사용한다. 서비스는 리뷰 본문, 좋아요 수, 로그인 사용자의 좋아요 여부를 병렬로 hydrate하고 ID 순서를 유지한다.
정렬 및 커서 검증 테스트
src/features/store/services/store-review.service.spec.ts, src/features/store/resolvers/store-review-query.resolver.spec.ts
좋아요순 정렬, photoOnly 필터, 동률 커서 페이지네이션, 삭제된 좋아요 제외, 잘못된 커서의 BadRequestException 처리를 검증한다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StoreReviewsResolver
  participant StoreReviewService
  participant StoreReviewRepository
  StoreReviewsResolver->>StoreReviewService: storeReviews(sort, cursor, photoOnly)
  StoreReviewService->>StoreReviewRepository: 정렬별 ID 페이지 조회
  StoreReviewRepository-->>StoreReviewService: 리뷰 ID 페이지 반환
  StoreReviewService->>StoreReviewRepository: 리뷰 본문과 미디어 일괄 조회
  StoreReviewRepository-->>StoreReviewService: 리뷰 행 반환
  StoreReviewService-->>StoreReviewsResolver: hydrate된 리뷰와 nextCursor 반환
Loading

Possibly related PRs

  • CaQuick/caquick-be#161: 기존 스토어 리뷰 조회 흐름을 확장해 좋아요순 정렬과 키셋 커서 페이지네이션을 추가한 변경과 직접 연결된다.
  • CaQuick/caquick-be#168: 리뷰 저장소와 서비스에 좋아요순 정렬 및 ID 기반 hydrate를 적용한 변경과 직접 연결된다.
  • CaQuick/caquick-be#172: photoOnly 필터를 유지하면서 스토어 리뷰 DTO, 저장소, 서비스, GraphQL 스키마를 확장한 변경과 직접 연결된다.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 제목은 storeReviews에 좋아요순 정렬을 추가하는 주요 변경 내용을 간결하게 설명합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 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 6, 2026

Copy link
Copy Markdown

🧹 knip — dead-code 리포트

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

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

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

🩺 NestJS Doctor — 89/100 (Good)

진단 271건 (error 0).

Category error warning info
architecture 0 0 13
correctness 0 119 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

@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
...ures/store/repositories/store-review.repository.ts 87.50% 1 Missing ⚠️
...rc/features/store/services/store-review.service.ts 97.14% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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

🤖 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/store/services/store-review.service.spec.ts`:
- Around line 60-79: Isolate tests from the real database: in
src/features/store/services/store-review.service.spec.ts lines 60-79, stub
StoreReviewRepository instead of creating accounts or calling
prisma.reviewLike.create; in lines 250-256, remove truncateAll and use a fixed
date for soft-delete assertions. In
src/features/store/resolvers/store-review-query.resolver.spec.ts lines 70-82,
mock StoreReviewService.storeReviews so the resolver tests cover only routing
and response transformation.

In `@src/features/store/services/store-review.service.ts`:
- Around line 55-89: Replace the LIKE cursor contract in store-review.service.ts
lines 55-89 with a stable snapshot/version-based contract that preserves
pagination without duplicates or omissions. In store-review.service.ts lines
129-146, reuse the like counts captured during ID selection or hydrate from the
same snapshot so ordering and returned counts match. In
store-review.repository.ts lines 88-108, make subsequent pages filter and sort
by the same snapshot ordering key instead of the current COUNT(l.id); update the
cursor payload and parsing as needed across these symbols.
🪄 Autofix

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: f899cb73-3320-409d-a5e4-82a3b690ce2b

📥 Commits

Reviewing files that changed from the base of the PR and between 0396e41 and b75c39c.

📒 Files selected for processing (7)
  • src/features/store/constants/store-review-error-messages.ts
  • src/features/store/dto/inputs/store-reviews.input.ts
  • src/features/store/repositories/store-review.repository.ts
  • src/features/store/resolvers/store-review-query.resolver.spec.ts
  • src/features/store/services/store-review.service.spec.ts
  • src/features/store/services/store-review.service.ts
  • src/features/store/store-reviews.graphql

Comment on lines +60 to +79
async function addLikes(reviewId: bigint, count: number) {
for (let i = 0; i < count; i += 1) {
const liker = await createAccount(prisma, { account_type: 'USER' });
await prisma.reviewLike.create({
data: { review_id: reviewId, account_id: liker.id },
});
}
}

async function addMedia(reviewId: bigint) {
await prisma.reviewMedia.create({
data: {
review_id: reviewId,
media_type: 'IMAGE',
media_url: 'a.png',
sort_order: 0,
},
});
}

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
set -euo pipefail

ast-grep outline src/features/store/services/store-review.service.spec.ts --items all
ast-grep outline src/features/store/resolvers/store-review-query.resolver.spec.ts --items all

rg -n -C 3 \
  'jest\.mock|mockImplementation|mockResolvedValue|stub|PrismaClient|new Prisma|beforeAll|beforeEach|new Date' \
  src/features/store/services/store-review.service.spec.ts \
  src/features/store/resolvers/store-review-query.resolver.spec.ts

Repository: CaQuick/caquick-be

Length of output: 9539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== store-review.service.spec.ts relevant sections =="
sed -n '1,90p;230,265p' src/features/store/services/store-review.service.spec.ts

echo
echo "== store-review-query.resolver.spec.ts relevant sections =="
sed -n '1,55p;60,85p' src/features/store/resolvers/store-review-query.resolver.spec.ts

echo
echo "== service implementation outline =="
ast-grep outline src/features/store/services/store-review.service.ts --items all || true

echo
echo "== repository implementation outline =="
ast-grep outline src/features/store/repositories/store-review.repository.ts --items all || true

Repository: CaQuick/caquick-be

Length of output: 8530


테스트 의존성을 고립시키세요.

StoreReviewService.spec](src/features/store/services/store-review.service.spec.ts)는 real DB라서 prisma.reviewLike.create, truncateAll, 실제 Prisma 클라이언트가 테스트를 실행 환경과 DB 상태에 의존하게 만듭니다. 서비스 테스트에서는 StoreReviewRepository를 stub하고, soft-delete 시각은 고정된 날짜로 사용하세요. Resolver 테스트는 DB 경로가 아니라 StoreReviewService.storeReviews를 mock해 resolver 라우팅/변환만 검증하세요.

📍 Affects 2 files
  • src/features/store/services/store-review.service.spec.ts#L60-L79 (this comment)
  • src/features/store/services/store-review.service.spec.ts#L250-L256
  • src/features/store/resolvers/store-review-query.resolver.spec.ts#L70-L82
🤖 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/store/services/store-review.service.spec.ts` around lines 60 -
79, Isolate tests from the real database: in
src/features/store/services/store-review.service.spec.ts lines 60-79, stub
StoreReviewRepository instead of creating accounts or calling
prisma.reviewLike.create; in lines 250-256, remove truncateAll and use a fixed
date for soft-delete assertions. In
src/features/store/resolvers/store-review-query.resolver.spec.ts lines 70-82,
mock StoreReviewService.storeReviews so the resolver tests cover only routing
and response transformation.

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.

real-DB 통합 spec은 레포 전역 테스트 컨벤션(createTestingModuleWithRealDb + testcontainers + truncate). 특히 좋아요순은 raw 키셋 SQL·soft-delete 집계 제외가 핵심이라 stub으로는 검증 불가, real DB 경로가 목적에 부합. resolver spec도 기존 전 feature와 동일한 통합 경로 검증 패턴이라 유지.

Comment on lines +55 to +89
/**
* 정렬별 리뷰 id 페이지 + 다음 커서 계산.
*
* 좋아요순 커서는 "<likeCount>:<id>" 불투명 토큰 — 경계 시점의 좋아요 수를
* 담아, 이후 좋아요 수가 변해도 페이지가 중복/누락되지 않는다.
* 최신순 커서는 마지막 리뷰 id. 커서는 동일 sort 안에서만 유효하다.
*/
private async fetchReviewIdPage(args: {
storeId: bigint;
photoOnly: boolean;
sort: 'LATEST' | 'LIKES';
limit: number;
cursorRaw?: string;
}): Promise<{
pageIds: bigint[];
hasMore: boolean;
nextCursor: string | null;
}> {
if (args.sort === 'LIKES') {
const rows = await this.repo.listStoreReviewIdsByLikes({
storeId: args.storeId,
photoOnly: args.photoOnly,
limit: args.limit,
cursor: args.cursorRaw
? this.parseLikesCursor(args.cursorRaw)
: undefined,
});
const hasMore = rows.length > args.limit;
const page = hasMore ? rows.slice(0, args.limit) : rows;
const last = page[page.length - 1];
return {
pageIds: page.map((row) => row.id),
hasMore,
nextCursor: hasMore ? `${last.likeCount}:${last.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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

변경 가능한 좋아요 수로는 현재 커서 계약을 보장할 수 없습니다.

첫 페이지의 경계가 2:100일 때, 이미 반환한 id=200 리뷰의 좋아요 수가 3에서 1로 감소하면 다음 요청의 COUNT(l.id) < 2 조건에 다시 포함됩니다. 아직 반환하지 않은 리뷰의 좋아요 수가 증가하면 반대로 누락됩니다. 또한 hydrate 단계가 좋아요 수를 다시 집계하므로 반환된 likeCount와 항목 순서가 일치하지 않을 수 있습니다.

PR의 중복·누락 없음 계약을 유지하려면 페이지 간 안정적인 스냅샷 또는 버전 토큰을 구현하세요. 안정성을 제공하지 않을 경우 해당 계약을 제거하고 eventual consistency를 명시하세요.

  • src/features/store/services/store-review.service.ts#L55-L89: 좋아요 수 변경 후에도 안정적이라는 커서 계약을 스냅샷 기반 계약으로 변경하세요.
  • src/features/store/services/store-review.service.ts#L129-L146: ID 페이지 선택 시점의 좋아요 수를 재사용하거나 같은 스냅샷에서 hydrate하세요.
  • src/features/store/repositories/store-review.repository.ts#L88-L108: 후속 페이지가 현재 COUNT(l.id)가 아닌 동일 스냅샷의 정렬 키를 사용하도록 변경하세요.
📍 Affects 2 files
  • src/features/store/services/store-review.service.ts#L55-L89 (this comment)
  • src/features/store/services/store-review.service.ts#L129-L146
  • src/features/store/repositories/store-review.repository.ts#L88-L108
🤖 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/store/services/store-review.service.ts` around lines 55 - 89,
Replace the LIKE cursor contract in store-review.service.ts lines 55-89 with a
stable snapshot/version-based contract that preserves pagination without
duplicates or omissions. In store-review.service.ts lines 129-146, reuse the
like counts captured during ID selection or hydrate from the same snapshot so
ordering and returned counts match. In store-review.repository.ts lines 88-108,
make subsequent pages filter and sort by the same snapshot ordering key instead
of the current COUNT(l.id); update the cursor payload and parsing as needed
across these symbols.

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.

#173 Codex 동일 지적에 회신한 수용 트레이드오프. 경계 스냅샷 커서는 인접 페이지 안정화 목적이고, 전역 좋아요 변동까지 막으려면 랭킹 스냅샷/seen-set이 필요해 공개 목록 조회에 과한 비용. 릴리즈된 productReviews와 동일 구현·동일 기준.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 97.64% 4338/4443
🟢 Branches 93.56% 1381/1476
🟢 Functions 95.66% 838/876
🟢 Lines 98.11% 3954/4030

Test suite run success

1472 tests passing in 173 suites.

Report generated by 🧪jest coverage report action from b75c39c

@chanwoo7
chanwoo7 merged commit c4a23fc into main Aug 6, 2026
15 checks passed
@chanwoo7
chanwoo7 deleted the develop branch August 6, 2026 03:40
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