From 9c5a6857238561a77cd681b63afb6708f9f779ea Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Sun, 4 Oct 2026 23:47:31 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A6=AC=EB=B7=B0=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=EC=9D=B4=20cursor=20null=EC=9D=84=20=EC=B2=AB=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - productReviews·storeReviews의 cursor는 SDL nullable인데 service가 !== undefined로만 걸러, cursor: null이 커서 파싱에 들어가 400(INVALID_CURSOR·INVALID_LIKES_CURSOR) - service ?? undefined 정규화 + DTO cursor를 | null로 선언(다음 누락은 tsc가 잡게) - 회귀: 두 범위 × LATEST·LIKES에서 cursor null이면 첫 페이지 4건(수정 전 4건 모두 커서 형식 오류로 실패 확인) --- .../review/dto/inputs/product-reviews.input.ts | 2 +- .../review/dto/inputs/store-reviews.input.ts | 2 +- .../review/services/review-listing.service.spec.ts | 14 +++++++++++++- .../review/services/review-listing.service.ts | 5 +++-- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/features/review/dto/inputs/product-reviews.input.ts b/src/features/review/dto/inputs/product-reviews.input.ts index f9bbe3c3..a8f44265 100644 --- a/src/features/review/dto/inputs/product-reviews.input.ts +++ b/src/features/review/dto/inputs/product-reviews.input.ts @@ -30,7 +30,7 @@ export class ProductReviewsInput { @IsOptional() @IsString() @IsNotEmpty() - cursor?: string; + cursor?: string | null; @IsOptional() @IsInt() diff --git a/src/features/review/dto/inputs/store-reviews.input.ts b/src/features/review/dto/inputs/store-reviews.input.ts index 5ed50380..d1159fc1 100644 --- a/src/features/review/dto/inputs/store-reviews.input.ts +++ b/src/features/review/dto/inputs/store-reviews.input.ts @@ -30,7 +30,7 @@ export class StoreReviewsInput { @IsOptional() @IsString() @IsNotEmpty() - cursor?: string; + cursor?: string | null; @IsOptional() @IsInt() diff --git a/src/features/review/services/review-listing.service.spec.ts b/src/features/review/services/review-listing.service.spec.ts index 0a54cad8..53ce6495 100644 --- a/src/features/review/services/review-listing.service.spec.ts +++ b/src/features/review/services/review-listing.service.spec.ts @@ -106,7 +106,7 @@ describe('ReviewListingService (real DB)', () => { args: { photoOnly?: boolean; sort?: 'LATEST' | 'LIKES'; - cursor?: string; + cursor?: string | null; limit?: number; } = {}, accountId?: bigint, @@ -270,6 +270,18 @@ describe('ReviewListingService (real DB)', () => { ); }); + it.each(['LATEST', 'LIKES'] as const)( + 'cursor null은 첫 페이지로 처리한다 (%s)', + async (sort) => { + const target = await makeTarget(); + const review = await makeReview(target); + + const result = await list(kind, target, { sort, cursor: null }); + + expect(result.items.map((r) => r.id)).toEqual([review.id.toString()]); + }, + ); + it('cursor "0"은 페이지를 리셋하지 않고 빈 결과를 반환한다', async () => { const target = await makeTarget(); await makeReview(target); diff --git a/src/features/review/services/review-listing.service.ts b/src/features/review/services/review-listing.service.ts index e5007f3f..1fb1693c 100644 --- a/src/features/review/services/review-listing.service.ts +++ b/src/features/review/services/review-listing.service.ts @@ -29,7 +29,7 @@ import type { interface ListingArgs { photoOnly?: boolean; sort?: ReviewSort; - cursor?: string; + cursor?: string | null; limit?: number; } @@ -115,7 +115,8 @@ export class ReviewListingService { photoOnly, sort, limit, - cursorRaw: input.cursor, + // 명시적 cursor: null도 첫 페이지 요청이다 + cursorRaw: input.cursor ?? undefined, }), this.repo.countReviews({ scope, photoOnly: false }), this.repo.countReviews({ scope, photoOnly: true }),