From 90ab53fe8e70fa304007fe3a0547f4f5aa28f030 Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Thu, 10 Sep 2026 06:01:34 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81=20(=EA=B2=8C=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=B9=88=20=EC=9E=85=EB=A0=A5=20=EB=B0=A9=EC=96=B4?= =?UTF-8?q?=C2=B7=EC=84=A4=EB=AA=85=205=EA=B1=B4=20=EC=A0=95=EC=A0=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit 지적 8건 중 6건 반영, 1건 미반영, 1건 false positive. 게이트 결함: - SDL 파일이 0개일 때 조용히 통과했다. src/를 못 읽으면 모든 카테고리가 0/0이 되고 percentOf가 100을 돌려줘 위반 없이 초록불이 뜬다. 이 게이트가 막으려던 것과 같은 종류의 거짓 안전이라 실패시킨다. 종료코드로 확인: 경로가 없으면 1, 정상이면 0 설명이 구현과 어긋난 것: - conversationMessages — last_read_at을 "현재 시각"으로 갱신한다고 적었으나 실제로는 이번에 반환한 최신 메시지의 created_at까지 끌어올리는 워터마크이고 반환할 메시지가 없으면 갱신하지 않는다 (conversation.repository.ts:367) - CreateOrderOutput.status — "항상 SUBMITTED"라 단정했으나 같은 idempotencyKey로 재시도하면 기존 주문을 그대로 돌려주므로 진행된 상태일 수 있다 (order-checkout.service.ts:70-71) - SellerProductListInput.isActive — "미지정 시 노출·미노출 모두"라 적었으나 input?.isActive ?? true라 활성만 반환한다 (seller-product-query.service.ts:51) - 픽업 슬롯 오전/오후 — "12:00 이전/이후"가 12:00을 양쪽에서 배제하는 것처럼 읽힌다. 실제로는 afternoon에 포함된다 - "regularPrice가 표시가다" → "표시가로 쓰인다" (5개 파일). 제안된 "표시된다"는 의미가 달라 표현만 다듬었다 미반영: 감사 로그 spec을 DB mock으로 바꾸라는 지적. 레포 전체가 testcontainers 실 DB 통합 테스트 아키텍처이고 CLAUDE.md가 "DB를 mock하지 않는다"를 명시 규칙으로 둔다. 이 스펙만 stub으로 바꾸면 컨벤션 일관성이 훼손된다. false positive: OngoingOrderSummary.orderId에 "주문번호" 설명이 붙었다는 지적. 설명은 orderNumber에 붙어 있고 orderId는 자명 필드라 비어 있다. 전체 1,874건 + 게이트 스크립트 42건 통과. --- scripts/check-sdl-description-coverage.ts | 10 ++++++++++ src/features/conversation/conversation-center.graphql | 3 ++- src/features/order/order-checkout.graphql | 5 ++++- src/features/pickup/pickup.types.graphql | 4 ++-- src/features/product/product-detail.graphql | 2 +- src/features/product/product-home.graphql | 2 +- src/features/product/product-search.graphql | 2 +- src/features/product/product-storefront.graphql | 2 +- src/features/seller/seller-product.graphql | 2 +- src/features/store/store-pickup-schedule.graphql | 4 ++-- src/features/user/user-wishlist.graphql | 2 +- 11 files changed, 26 insertions(+), 12 deletions(-) diff --git a/scripts/check-sdl-description-coverage.ts b/scripts/check-sdl-description-coverage.ts index c1c70c39..2c775cd6 100644 --- a/scripts/check-sdl-description-coverage.ts +++ b/scripts/check-sdl-description-coverage.ts @@ -84,6 +84,16 @@ function safeIsDir(path: string): boolean { function main(): void { const files = loadSdlFiles(); + if (files.length === 0) { + // 검사 대상이 없으면 모든 카테고리가 0/0이 되고 percentOf가 100을 돌려줘 + // 위반 없이 "통과"가 찍힌다. 이 게이트가 막으려는 것과 같은 종류의 거짓 안전이다. + console.error( + `[docs:check] SDL 파일을 찾지 못했습니다: ${SDL_ROOT}. ` + + '경로가 옳은지 확인하라 — 대상이 없으면 커버리지는 의미가 없다.', + ); + process.exitCode = 1; + return; + } const coverage = collectCoverage(files); console.log(`[docs:check] SDL 파일 ${files.length}개`); diff --git a/src/features/conversation/conversation-center.graphql b/src/features/conversation/conversation-center.graphql index 362d1416..5a94b444 100644 --- a/src/features/conversation/conversation-center.graphql +++ b/src/features/conversation/conversation-center.graphql @@ -3,7 +3,8 @@ extend type Query { myConversations(input: MyConversationsInput): MyConversationConnection! """ 채팅 상세 메시지 목록(최신순 키셋 커서). 구매자 본인 대화만 조회 가능하며, - 조회 시 last_read_at을 현재 시각으로 갱신한다(안읽음 배지 해소 부수효과). + 조회 시 last_read_at을 이번에 반환한 최신 메시지의 작성 시각까지 끌어올린다 + (안읽음 배지 해소 부수효과). 반환할 메시지가 없으면 갱신하지 않는다. 로그인 필수. """ conversationMessages(conversationId: ID!, input: ConversationMessagesInput): ConversationMessageConnection! diff --git a/src/features/order/order-checkout.graphql b/src/features/order/order-checkout.graphql index ab4c9189..0ae41297 100644 --- a/src/features/order/order-checkout.graphql +++ b/src/features/order/order-checkout.graphql @@ -28,7 +28,10 @@ type CreateOrderOutput { orderId: ID! """주문번호. 화면·문의에 쓰는 식별자.""" orderNumber: String! - """생성 직후 상태. 항상 SUBMITTED다.""" + """ + 주문 상태. 새로 생성된 경우 SUBMITTED다. 같은 idempotencyKey로 재시도하면 기존 + 주문을 그대로 돌려주므로 그 사이 진행된 상태(CONFIRMED·MADE·PICKED_UP 등)일 수 있다. + """ status: OrderStatusType! """확정된 픽업 일시. 서버가 매장 정책으로 재검증한 값이다.""" pickupAt: DateTime! diff --git a/src/features/pickup/pickup.types.graphql b/src/features/pickup/pickup.types.graphql index 7674284a..ed7994f6 100644 --- a/src/features/pickup/pickup.types.graphql +++ b/src/features/pickup/pickup.types.graphql @@ -33,9 +33,9 @@ type PickupDay { type PickupTimeSlots { """요청한 날짜. "YYYY-MM-DD" (KST).""" date: String! - """12:00 이전 슬롯.""" + """오전 슬롯. 12:00 이전(12:00은 afternoon에 들어간다).""" morning: [PickupSlot!]! - """12:00 이후 슬롯.""" + """오후 슬롯. 12:00 포함, 이후.""" afternoon: [PickupSlot!]! } diff --git a/src/features/product/product-detail.graphql b/src/features/product/product-detail.graphql index 7daf949e..3ba5da28 100644 --- a/src/features/product/product-detail.graphql +++ b/src/features/product/product-detail.graphql @@ -18,7 +18,7 @@ type ProductDetail { images: [String!]! """정가(원).""" regularPrice: Int! - """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가다.""" + """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가로 쓰인다.""" salePrice: Int """할인율(0~100). salePrice 없으면 0.""" discountRate: Int! diff --git a/src/features/product/product-home.graphql b/src/features/product/product-home.graphql index d08d2d06..9ef82864 100644 --- a/src/features/product/product-home.graphql +++ b/src/features/product/product-home.graphql @@ -116,7 +116,7 @@ type PopularCake { regionLabel: String """정가(원).""" regularPrice: Int! - """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가다.""" + """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가로 쓰인다.""" salePrice: Int """할인율(0~100). salePrice 없으면 0.""" discountRate: Int! diff --git a/src/features/product/product-search.graphql b/src/features/product/product-search.graphql index 635fc31a..c9ac1e24 100644 --- a/src/features/product/product-search.graphql +++ b/src/features/product/product-search.graphql @@ -64,7 +64,7 @@ type SearchProduct { regionLabel: String """정가(원).""" regularPrice: Int! - """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가다.""" + """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가로 쓰인다.""" salePrice: Int """할인율(0~100). salePrice 없으면 0.""" discountRate: Int! diff --git a/src/features/product/product-storefront.graphql b/src/features/product/product-storefront.graphql index 6eb6f48d..700eec77 100644 --- a/src/features/product/product-storefront.graphql +++ b/src/features/product/product-storefront.graphql @@ -63,7 +63,7 @@ type StoreProduct { thumbnailUrl: String """정가(원).""" regularPrice: Int! - """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가다.""" + """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가로 쓰인다.""" salePrice: Int """할인율(0~100). salePrice 없으면 0.""" discountRate: Int! diff --git a/src/features/seller/seller-product.graphql b/src/features/seller/seller-product.graphql index d8613804..d063bdab 100644 --- a/src/features/seller/seller-product.graphql +++ b/src/features/seller/seller-product.graphql @@ -222,7 +222,7 @@ input SellerProductListInput { limit: Int = 20 """이전 응답의 nextCursor. 첫 페이지는 생략한다.""" cursor: ID - """노출 여부 필터. 미지정 시 노출·미노출을 모두 포함한다.""" + """노출 여부 필터. 미지정 시 활성 상품만 반환한다(기본 true).""" isActive: Boolean """카테고리 필터. 해당 카테고리가 연결된 상품만.""" categoryId: ID diff --git a/src/features/store/store-pickup-schedule.graphql b/src/features/store/store-pickup-schedule.graphql index d60b00f1..c890cef9 100644 --- a/src/features/store/store-pickup-schedule.graphql +++ b/src/features/store/store-pickup-schedule.graphql @@ -35,9 +35,9 @@ type StorePickupDay { type StorePickupTimeSlots { """요청한 날짜. "YYYY-MM-DD" (KST).""" date: String! - """12:00 이전 슬롯.""" + """오전 슬롯. 12:00 이전(12:00은 afternoon에 들어간다).""" morning: [StorePickupSlot!]! - """12:00 이후 슬롯.""" + """오후 슬롯. 12:00 포함, 이후.""" afternoon: [StorePickupSlot!]! } diff --git a/src/features/user/user-wishlist.graphql b/src/features/user/user-wishlist.graphql index d865cf7b..17108569 100644 --- a/src/features/user/user-wishlist.graphql +++ b/src/features/user/user-wishlist.graphql @@ -41,7 +41,7 @@ type WishlistItemSummary { productName: String! """대표 상품 이미지. 없으면 null.""" representativeImageUrl: String - """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가다.""" + """할인가(원). 할인이 없으면 null이고 regularPrice가 표시가로 쓰인다.""" salePrice: Int """정가(원).""" regularPrice: Int! From ee0e3e1d48a2599dd31e7baa25d3dfa0b2c7a4ec Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Thu, 10 Sep 2026 06:03:04 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20limit=20=EB=B2=94=EC=9C=84=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=EB=A5=BC=20=EC=8B=A4=EC=A0=9C=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EB=8F=99=EC=9E=91=EC=97=90=20=EB=A7=9E=EC=B6=A4=20?= =?UTF-8?q?(=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98?= =?UTF-8?q?=EC=98=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @Min(1)/@Max(100)과 전역 ValidationPipe가 범위 밖 limit을 BAD_REQUEST로 거절한다. normalizeCursorInput의 clamp는 그 뒤라 실제로는 도달하지 않는다. '보정된다'는 서비스 코드만 보고 적은 표현이라 요청 경로와 맞지 않았다. 거절 동작을 유지하고 문구를 맞췄다 — 조용한 값 변경보다 명시적 거절이 낫고 이미 그렇게 동작한다. 같은 문구를 복사해 둔 seller SDL 6개 파일 전부 교체. --- src/features/seller/seller-common.graphql | 2 +- src/features/seller/seller-content.graphql | 2 +- src/features/seller/seller-conversation.graphql | 2 +- src/features/seller/seller-order.graphql | 2 +- src/features/seller/seller-product.graphql | 2 +- src/features/seller/seller-store.graphql | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/seller/seller-common.graphql b/src/features/seller/seller-common.graphql index 9fef5025..e7752426 100644 --- a/src/features/seller/seller-common.graphql +++ b/src/features/seller/seller-common.graphql @@ -5,7 +5,7 @@ ID 키셋 커서 기반 목록 조회 공통 입력. 판매자 목록 API가 공 정렬 기준이 바뀌면 이전에 받은 커서는 무효가 된다. """ input SellerCursorInput { - """한 번에 가져올 개수. 기본 20, 1~100으로 보정된다.""" + """한 번에 가져올 개수. 기본 20, 1~100만 허용하며 벗어나면 BAD_REQUEST.""" limit: Int = 20 """이전 응답의 nextCursor. 첫 페이지는 생략한다.""" cursor: ID diff --git a/src/features/seller/seller-content.graphql b/src/features/seller/seller-content.graphql index 9ce0b7c7..36875f89 100644 --- a/src/features/seller/seller-content.graphql +++ b/src/features/seller/seller-content.graphql @@ -260,7 +260,7 @@ type SellerAuditLogConnection { """감사 로그 조회 조건.""" input SellerAuditLogListInput { - """한 번에 가져올 개수. 기본 20, 1~100으로 보정된다.""" + """한 번에 가져올 개수. 기본 20, 1~100만 허용하며 벗어나면 BAD_REQUEST.""" limit: Int = 20 """이전 응답의 nextCursor. 첫 페이지는 생략한다.""" cursor: ID diff --git a/src/features/seller/seller-conversation.graphql b/src/features/seller/seller-conversation.graphql index 97a0710f..b43c51f5 100644 --- a/src/features/seller/seller-conversation.graphql +++ b/src/features/seller/seller-conversation.graphql @@ -30,7 +30,7 @@ type SellerConversation { 불투명 토큰이므로 값의 형식에 의존하지 말고 이전 응답의 nextCursor를 그대로 넘긴다. """ input SellerConversationListInput { - """한 번에 가져올 개수. 기본 20, 1~100으로 보정된다.""" + """한 번에 가져올 개수. 기본 20, 1~100만 허용하며 벗어나면 BAD_REQUEST.""" limit: Int = 20 """이전 응답의 nextCursor. 첫 페이지는 생략한다.""" cursor: String diff --git a/src/features/seller/seller-order.graphql b/src/features/seller/seller-order.graphql index b1fdcbdf..0e91d164 100644 --- a/src/features/seller/seller-order.graphql +++ b/src/features/seller/seller-order.graphql @@ -45,7 +45,7 @@ type SellerOrderConnection { 주문 목록 조회 조건. 모든 필터는 AND로 결합되고, 미지정 필터는 적용되지 않는다. """ input SellerOrderListInput { - """한 번에 가져올 개수. 기본 20, 1~100으로 보정된다.""" + """한 번에 가져올 개수. 기본 20, 1~100만 허용하며 벗어나면 BAD_REQUEST.""" limit: Int = 20 """이전 응답의 nextCursor. 첫 페이지는 생략한다.""" cursor: ID diff --git a/src/features/seller/seller-product.graphql b/src/features/seller/seller-product.graphql index d063bdab..7ede5a42 100644 --- a/src/features/seller/seller-product.graphql +++ b/src/features/seller/seller-product.graphql @@ -218,7 +218,7 @@ type SellerProductConnection { """상품 목록 조회 조건. 필터는 AND로 결합되고 미지정 필터는 적용되지 않는다.""" input SellerProductListInput { - """한 번에 가져올 개수. 기본 20, 1~100으로 보정된다.""" + """한 번에 가져올 개수. 기본 20, 1~100만 허용하며 벗어나면 BAD_REQUEST.""" limit: Int = 20 """이전 응답의 nextCursor. 첫 페이지는 생략한다.""" cursor: ID diff --git a/src/features/seller/seller-store.graphql b/src/features/seller/seller-store.graphql index 383e5f57..14889d42 100644 --- a/src/features/seller/seller-store.graphql +++ b/src/features/seller/seller-store.graphql @@ -28,7 +28,7 @@ extend type Mutation { """날짜 범위 필터가 붙은 커서 목록 입력. 일별 생산 수량 조회에 쓴다.""" input SellerDateCursorInput { - """한 번에 가져올 개수. 기본 20, 1~100으로 보정된다.""" + """한 번에 가져올 개수. 기본 20, 1~100만 허용하며 벗어나면 BAD_REQUEST.""" limit: Int = 20 """이전 응답의 nextCursor. 첫 페이지는 생략한다.""" cursor: ID