From 3d93e36a191c785df25d07085a54dac7a0318daf Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Thu, 10 Sep 2026 06:39:18 +0900 Subject: [PATCH 1/4] =?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(=EC=98=B5=EC=85=98?= =?UTF-8?q?=20=EA=B7=B8=EB=A3=B9=20=EA=B3=84=EC=95=BD=20=EC=84=A4=EB=AA=85?= =?UTF-8?q?=203=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 세 건 다 클라이언트가 그대로 따르면 유효한 주문을 막거나 무효한 조합을 만든다. - optionRequiresDescription / optionRequiresImage — "선택지에 설명·이미지를 필수로 만든다"고 적었으나, 옵션 아이템 생성은 null이어도 통과한다. 실제로는 order-checkout.service.ts:243-250이 이 플래그가 켜진 그룹의 선택을 통째로 거절한다(OPTION_CUSTOMIZATION_REQUIRED, 커스텀 체크아웃 확장 전 잠정 가드 — 이슈 #212). 판매자가 설명만 보고 켜면 그 옵션을 못 팔게 된다 - sortOrder 기본값 — "미지정 시 맨 뒤"라 적었으나 옵션 그룹·선택지·커스텀 토큰은 input.sortOrder ?? 0이라 0이 들어간다. 상품 이미지만 ?? count로 맨 뒤에 붙는다 (seller-product-image.service.ts:73). 이미지 문구를 나머지 셋에 복사한 것이고 네 자리 중 셋이 틀렸다 - isRequired / minSelect / maxSelect — order-checkout.service.ts:230-235가 count를 [min_select, max_select]로만 보고 is_required는 0개 허용 여부만 가른다. isRequired=true라도 minSelect=0이면 0개가 유효하고, min=max=2면 정확히 2개다. "true면 최소 1개", "minSelect와 같으면 단일 선택"은 둘 다 틀렸다 전체 1,874건 통과. --- src/features/seller/seller-product.graphql | 41 +++++++++++++--------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/features/seller/seller-product.graphql b/src/features/seller/seller-product.graphql index ff7314c2..32feac00 100644 --- a/src/features/seller/seller-product.graphql +++ b/src/features/seller/seller-product.graphql @@ -86,9 +86,9 @@ type SellerOptionItem { optionGroupId: ID! """선택지 이름.""" title: String! - """선택지 설명. 소속 그룹의 optionRequiresDescription이 true면 필수다.""" + """선택지 설명. 미등록 시 null.""" description: String - """선택지 이미지 URL. 소속 그룹의 optionRequiresImage가 true면 필수다.""" + """선택지 이미지 URL. 미등록 시 null.""" imageUrl: String """이 선택지를 고를 때 더해지는 금액(원). 음수면 할인으로 동작한다.""" priceDelta: Int! @@ -109,15 +109,22 @@ type SellerOptionGroup { name: String! """그룹 안내 문구(상품 상세 옵션 섹션 인트로).""" description: String - """필수 선택 여부. true면 이 그룹에서 최소 1개를 골라야 주문이 된다.""" + """미선택 허용 여부. false면 0개도 유효하고, true면 선택 개수가 minSelect~maxSelect 범위 안이어야 한다.""" isRequired: Boolean! """최소 선택 개수.""" minSelect: Int! - """최대 선택 개수. minSelect와 같으면 단일 선택처럼 동작한다.""" + """최대 선택 개수. minSelect와 함께 허용 개수를 정한다(둘 다 1이면 단일 선택).""" maxSelect: Int! - """true면 이 그룹의 선택지에 description이 필수다.""" + """ + 선택지에 구매자 커스텀 입력(설명)이 필요한 그룹인지. + 현재는 커스텀 체크아웃이 확장되기 전이라, 이 값이 true인 그룹의 옵션을 고르면 + 주문이 거절된다(잠정 가드). 켜기 전에 주문 가능 여부를 확인해야 한다. + """ optionRequiresDescription: Boolean! - """true면 이 그룹의 선택지에 imageUrl이 필수다.""" + """ + 선택지에 구매자 커스텀 입력(이미지)이 필요한 그룹인지. + optionRequiresDescription과 마찬가지로 현재는 true인 그룹의 옵션 선택이 거절된다. + """ optionRequiresImage: Boolean! """상품 안에서의 그룹 노출 순서. 오름차순.""" sortOrder: Int! @@ -328,17 +335,17 @@ input SellerCreateOptionGroupInput { name: String! """그룹 안내 문구.""" description: String - """필수 선택 여부. 기본 true.""" + """미선택 허용 여부. 기본 true(미선택 불가).""" isRequired: Boolean = true """최소 선택 개수. 기본 1.""" minSelect: Int = 1 - """최대 선택 개수. 기본 1(단일 선택).""" + """최대 선택 개수. 기본 1. minSelect와 함께 허용 개수를 정한다.""" maxSelect: Int = 1 - """true면 이 그룹의 선택지에 설명이 필수가 된다. 기본 false.""" + """구매자 커스텀 입력(설명)이 필요한 그룹으로 표시한다. 기본 false. 현재는 켜면 해당 옵션 주문이 거절된다.""" optionRequiresDescription: Boolean = false - """true면 이 그룹의 선택지에 이미지가 필수가 된다. 기본 false.""" + """구매자 커스텀 입력(이미지)이 필요한 그룹으로 표시한다. 기본 false. 현재는 켜면 해당 옵션 주문이 거절된다.""" optionRequiresImage: Boolean = false - """노출 순서. 미지정 시 맨 뒤.""" + """노출 순서. 미지정 시 0이 들어간다(맨 뒤가 아니다).""" sortOrder: Int """노출 여부. 기본 true.""" isActive: Boolean = true @@ -357,9 +364,9 @@ input SellerUpdateOptionGroupInput { minSelect: Int """최대 선택 개수.""" maxSelect: Int - """true면 이 그룹의 선택지에 설명이 필수가 된다.""" + """구매자 커스텀 입력(설명)이 필요한 그룹으로 표시한다.""" optionRequiresDescription: Boolean - """true면 이 그룹의 선택지에 이미지가 필수가 된다.""" + """구매자 커스텀 입력(이미지)이 필요한 그룹으로 표시한다.""" optionRequiresImage: Boolean """노출 순서.""" sortOrder: Int @@ -382,13 +389,13 @@ input SellerCreateOptionItemInput { optionGroupId: ID! """선택지 이름.""" title: String! - """선택지 설명. 그룹이 optionRequiresDescription이면 필수.""" + """선택지 설명.""" description: String - """선택지 이미지 URL. 그룹이 optionRequiresImage면 필수.""" + """선택지 이미지 URL.""" imageUrl: String """추가 금액(원). 기본 0이고 음수면 할인으로 동작한다.""" priceDelta: Int = 0 - """노출 순서. 미지정 시 맨 뒤.""" + """노출 순서. 미지정 시 0이 들어간다(맨 뒤가 아니다).""" sortOrder: Int """노출 여부. 기본 true.""" isActive: Boolean = true @@ -448,7 +455,7 @@ input SellerUpsertProductCustomTextTokenInput { defaultText: String! """입력 가능한 최대 글자 수. 기본 30.""" maxLength: Int = 30 - """입력 화면 노출 순서. 미지정 시 맨 뒤.""" + """입력 화면 노출 순서. 미지정 시 0이 들어간다(맨 뒤가 아니다).""" sortOrder: Int """필수 입력 여부. 기본 true.""" isRequired: Boolean = true From 561b314bd396d491b3d1282a7c90f7d964d523f6 Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Thu, 10 Sep 2026 06:41:17 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20isRequired=20=ED=91=9C=ED=98=84=20?= =?UTF-8?q?=EC=9E=94=EC=97=AC=EB=B6=84=EA=B3=BC=20isActive=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=20=EC=B6=9C=EC=B2=98=20=EB=AA=85=EC=8B=9C=20?= =?UTF-8?q?(=EC=9E=90=EC=B2=B4=20=EC=A0=90=EA=B2=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기계적 대조로 두 건을 더 찾았다. - SellerUpdateOptionGroupInput.isRequired만 '필수 선택 여부.'로 남아 있었다. 같은 필드의 다른 두 자리는 앞 커밋에서 '미선택 허용 여부'로 고쳤는데 표현 변형이 셋이라 하나를 놓쳤다. - SellerProductListInput.isActive는 SDL에 기본값이 없고 서비스가 ?? true로 기본을 적용한다. '기본 true'가 SDL 기본값처럼 읽혀 출처를 명시했다. 점검 방법: 설명이 말하는 '기본 X'와 SDL defaultValue를 전수 대조(불일치 1건), nullable 필드에 무조건 필수라 적은 것 탐색(2건, 그중 1건이 위 잔여분). --- src/features/seller/seller-product.graphql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/seller/seller-product.graphql b/src/features/seller/seller-product.graphql index 32feac00..fb75a3e3 100644 --- a/src/features/seller/seller-product.graphql +++ b/src/features/seller/seller-product.graphql @@ -229,7 +229,7 @@ input SellerProductListInput { limit: Int = 20 """이전 응답의 nextCursor. 첫 페이지는 생략한다.""" cursor: ID - """노출 여부 필터. 미지정 시 활성 상품만 반환한다(기본 true).""" + """노출 여부 필터. 생략하면 서버가 활성 상품만 반환한다(SDL 기본값이 아니라 서비스 기본 동작).""" isActive: Boolean """카테고리 필터. 해당 카테고리가 연결된 상품만.""" categoryId: ID @@ -358,7 +358,7 @@ input SellerUpdateOptionGroupInput { name: String """그룹 안내 문구.""" description: String - """필수 선택 여부.""" + """미선택 허용 여부. false면 0개도 유효하다.""" isRequired: Boolean """최소 선택 개수.""" minSelect: Int From 26d401672569ce13dde27ced4276846e3f80d6ed Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Thu, 10 Sep 2026 06:43:28 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20isRequired=20=EA=B7=B9=EC=84=B1=20?= =?UTF-8?q?=EB=B0=98=EC=A0=84=20=EC=A0=95=EC=A0=95=20=EB=B0=8F=20=EA=B5=AC?= =?UTF-8?q?=EB=A7=A4=EC=9E=90=20=EC=83=81=EC=84=B8=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20(Codex=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 앞 커밋에서 라벨을 '미선택 허용 여부'로 바꾸면서 극성을 뒤집었다. true가 필수인데 라벨은 반대를 가리킨다. 원래 라벨은 문제가 없었고 틀린 건 뒤의 '최소 1개를 골라야 한다'였는데, 라벨까지 손대며 새 오류를 넣었다. '필수 선택 여부'로 되돌리고 설명만 정확히 적는다 — true면 선택 개수가 minSelect~maxSelect 범위 안, false면 0개도 허용. 함께 발견: product-detail.graphql의 구매자 쪽 isRequired·maxSelect가 옛 설명 그대로 남아 있었다. seller만 고치고 구매자를 놓친 것이라 같은 계약으로 통일했다. --- src/features/product/product-detail.graphql | 4 ++-- src/features/seller/seller-product.graphql | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/product/product-detail.graphql b/src/features/product/product-detail.graphql index 3ba5da28..05689eee 100644 --- a/src/features/product/product-detail.graphql +++ b/src/features/product/product-detail.graphql @@ -39,11 +39,11 @@ type ProductDetailOptionGroup { name: String! """그룹 안내 문구(섹션 인트로).""" description: String - """필수 선택 여부. true면 이 그룹에서 최소 1개를 골라야 주문이 된다.""" + """필수 선택 여부. true면 선택 개수가 minSelect~maxSelect 범위 안이어야 하고, false면 0개 선택도 허용된다.""" isRequired: Boolean! """최소 선택 개수.""" minSelect: Int! - """최대 선택 개수. minSelect와 같으면 단일 선택처럼 동작한다.""" + """최대 선택 개수. minSelect와 함께 허용 개수를 정한다(둘 다 1이면 단일 선택).""" maxSelect: Int! """섹션 노출 순서. 오름차순.""" sortOrder: Int! diff --git a/src/features/seller/seller-product.graphql b/src/features/seller/seller-product.graphql index fb75a3e3..41e08764 100644 --- a/src/features/seller/seller-product.graphql +++ b/src/features/seller/seller-product.graphql @@ -109,7 +109,7 @@ type SellerOptionGroup { name: String! """그룹 안내 문구(상품 상세 옵션 섹션 인트로).""" description: String - """미선택 허용 여부. false면 0개도 유효하고, true면 선택 개수가 minSelect~maxSelect 범위 안이어야 한다.""" + """필수 선택 여부. true면 선택 개수가 minSelect~maxSelect 범위 안이어야 하고, false면 0개 선택도 허용된다.""" isRequired: Boolean! """최소 선택 개수.""" minSelect: Int! @@ -335,7 +335,7 @@ input SellerCreateOptionGroupInput { name: String! """그룹 안내 문구.""" description: String - """미선택 허용 여부. 기본 true(미선택 불가).""" + """필수 선택 여부. 기본 true. false면 0개 선택도 허용된다.""" isRequired: Boolean = true """최소 선택 개수. 기본 1.""" minSelect: Int = 1 @@ -358,7 +358,7 @@ input SellerUpdateOptionGroupInput { name: String """그룹 안내 문구.""" description: String - """미선택 허용 여부. false면 0개도 유효하다.""" + """필수 선택 여부. false면 0개 선택도 허용된다.""" isRequired: Boolean """최소 선택 개수.""" minSelect: Int From 7a05ad40ad8f10b72ecd859d1f89bd21bf5b1e16 Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Thu, 10 Sep 2026 06:48:35 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EC=98=B5=EC=85=98=20=EC=BB=A4?= =?UTF-8?q?=EC=8A=A4=ED=85=80=20=ED=94=8C=EB=9E=98=EA=B7=B8=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=A0=EB=A5=BC=20=EC=88=98=EC=A0=95=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=EC=97=90=EB=8F=84=20=EC=B6=94=EA=B0=80=20(Codex=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 생성 입력·출력 타입에는 '켜면 주문이 거절된다' 경고를 넣고 수정 입력에만 빠뜨렸다. 수정으로 켜는 경로도 동일하게 주문을 막는데 그 호출자만 모르는 상태였다. optionRequiresDescription/Image가 나오는 6자리(출력 2 · 생성 2 · 수정 2)를 전수로 맞췄다. 같은 개념이 여러 자리에 흩어져 있으면 한 곳만 고쳤을 때 설명끼리 어긋난다. --- src/features/seller/seller-product.graphql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/seller/seller-product.graphql b/src/features/seller/seller-product.graphql index 41e08764..20af6eef 100644 --- a/src/features/seller/seller-product.graphql +++ b/src/features/seller/seller-product.graphql @@ -364,9 +364,9 @@ input SellerUpdateOptionGroupInput { minSelect: Int """최대 선택 개수.""" maxSelect: Int - """구매자 커스텀 입력(설명)이 필요한 그룹으로 표시한다.""" + """구매자 커스텀 입력(설명)이 필요한 그룹으로 표시한다. 현재는 켜면 해당 옵션 주문이 거절된다.""" optionRequiresDescription: Boolean - """구매자 커스텀 입력(이미지)이 필요한 그룹으로 표시한다.""" + """구매자 커스텀 입력(이미지)이 필요한 그룹으로 표시한다. 현재는 켜면 해당 옵션 주문이 거절된다.""" optionRequiresImage: Boolean """노출 순서.""" sortOrder: Int