From 1f4be4ca4ed241906286b7f933da1682674fd7d0 Mon Sep 17 00:00:00 2001 From: chanwoo7 Date: Tue, 25 Aug 2026 06:19:17 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81=20(=EC=84=A4=EB=AA=85/=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=95=84=EC=88=98=20=EC=98=B5=EC=85=98=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=8B=9C=20=EC=A3=BC=EB=AC=B8=20=EA=B1=B0?= =?UTF-8?q?=EC=A0=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 릴리즈 PR #209 Codex 지적 반영. option_requires_description/image가 켜진 옵션 그룹은 판매자가 요구한 커스텀 정보 없이는 주문이 완성되지 않는데, 체크아웃이 플래그를 읽지 않아 스냅샷만으로 SUBMITTED가 됐다. - findProductDetailById에 두 플래그 로드 추가 - 해당 그룹 옵션을 선택한 주문은 커스텀 체크아웃 확장 전까지 거절 (미선택이면 기존대로 허용) - 회귀 테스트 1건 추가 --- .../order/constants/order-error-messages.ts | 2 ++ .../services/order-checkout.service.spec.ts | 36 +++++++++++++++++++ .../order/services/order-checkout.service.ts | 10 ++++++ .../repositories/product.repository.ts | 5 +++ 4 files changed, 53 insertions(+) diff --git a/src/features/order/constants/order-error-messages.ts b/src/features/order/constants/order-error-messages.ts index bde6a43..6714e25 100644 --- a/src/features/order/constants/order-error-messages.ts +++ b/src/features/order/constants/order-error-messages.ts @@ -6,6 +6,8 @@ export const ORDER_CHECKOUT_ERRORS = { DUPLICATE_OPTION_ITEM: '중복된 옵션 선택입니다.', INVALID_OPTION_ITEM: '해당 상품의 옵션이 아닙니다.', OPTION_GROUP_RULE_VIOLATION: '옵션 그룹의 선택 규칙을 충족하지 않습니다.', + OPTION_CUSTOMIZATION_REQUIRED: + '커스텀 정보가 필요한 옵션은 아직 주문할 수 없습니다.', PICKUP_NOT_AVAILABLE: '선택한 픽업 일시는 예약할 수 없습니다.', ORDER_AMOUNT_OUT_OF_RANGE: '주문 금액이 처리 가능한 범위를 벗어났습니다.', UNSUPPORTED_CURRENCY: 'KRW 상품만 주문할 수 있습니다.', diff --git a/src/features/order/services/order-checkout.service.spec.ts b/src/features/order/services/order-checkout.service.spec.ts index 00fc208..b69e8d8 100644 --- a/src/features/order/services/order-checkout.service.spec.ts +++ b/src/features/order/services/order-checkout.service.spec.ts @@ -313,6 +313,42 @@ describe('OrderCheckoutService (real DB)', () => { ).rejects.toThrow(BadRequestException); }); + it('설명/이미지 필수 옵션 선택은 커스텀 확장 전까지 거절한다', async () => { + const store = await makeOpenStore(); + const product = await createProduct(prisma, { store_id: store.id }); + const group = await prisma.productOptionGroup.create({ + data: { + product_id: product.id, + name: '레터링', + is_required: false, + min_select: 1, + max_select: 1, + option_requires_description: true, + }, + }); + const item = await prisma.productOptionItem.create({ + data: { option_group_id: group.id, title: '문구 입력', price_delta: 0 }, + }); + const buyer = await makeBuyer(); + + await expect( + service.createOrder( + buyer.id, + baseInput({ + productId: product.id.toString(), + optionItemIds: [item.id.toString()], + }), + ), + ).rejects.toThrow(BadRequestException); + + // 해당 그룹을 선택하지 않으면 주문 가능(선택 그룹이므로) + const ok = await service.createOrder( + buyer.id, + baseInput({ productId: product.id.toString() }), + ); + expect(ok.status).toBe('SUBMITTED'); + }); + it('없거나 비활성 상품·비활성 매장 상품은 NOT_FOUND다', async () => { const buyer = await makeBuyer(); const inactiveStore = await createStore(prisma, { is_active: false }); diff --git a/src/features/order/services/order-checkout.service.ts b/src/features/order/services/order-checkout.service.ts index 6f2a04d..814c857 100644 --- a/src/features/order/services/order-checkout.service.ts +++ b/src/features/order/services/order-checkout.service.ts @@ -220,6 +220,16 @@ export class OrderCheckoutService { ORDER_CHECKOUT_ERRORS.OPTION_GROUP_RULE_VIOLATION, ); } + // 설명/이미지 필수 옵션은 커스텀 입력 없이는 판매자 요구 정보가 빠진 채 + // 주문된다 — 커스텀 체크아웃 확장 전까지 해당 옵션 선택은 거절한다 + if ( + count > 0 && + (group.option_requires_description || group.option_requires_image) + ) { + throw new BadRequestException( + ORDER_CHECKOUT_ERRORS.OPTION_CUSTOMIZATION_REQUIRED, + ); + } } return selections; } diff --git a/src/features/product/repositories/product.repository.ts b/src/features/product/repositories/product.repository.ts index 3c300be..bd94260 100644 --- a/src/features/product/repositories/product.repository.ts +++ b/src/features/product/repositories/product.repository.ts @@ -79,6 +79,9 @@ export interface ProductDetailRow { is_required: boolean; min_select: number; max_select: number; + // 주문 생성(checkout)의 커스텀 필수 옵션 가드용 + option_requires_description: boolean; + option_requires_image: boolean; sort_order: number; option_items: { id: bigint; @@ -923,6 +926,8 @@ export class ProductRepository { is_required: true, min_select: true, max_select: true, + option_requires_description: true, + option_requires_image: true, sort_order: true, option_items: { where: { is_active: true, deleted_at: null },