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 },