From b07d6c895349fca0e926399de73274a218f4fed8 Mon Sep 17 00:00:00 2001 From: HeuJung Date: Tue, 14 Jul 2026 14:57:18 +0900 Subject: [PATCH] =?UTF-8?q?fix(ecommerce,tosspayments,templates):=20?= =?UTF-8?q?=EC=97=90=EC=8A=A4=ED=81=AC=EB=A1=9C=20=ED=91=9C=EC=8B=9C=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=C2=B7=EA=B2=B0=EC=A0=9C=EC=88=98=EB=8B=A8=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=EC=8B=9C=20=EC=9E=85=EB=A0=A5=20=EC=9E=94?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 에스크로 표시가 영구히 렌더되지 않던 문제: is_escrow 가 DB·모델·부분취소 차단 로직에는 있으나 OrderPaymentResource 가 내보내지 않아, 이 값을 참조하는 주문 상세(관리자·구매자)의 에스크로 행이 항상 false 로 평가됐다. Resource 에 노출한다. - 결제수단을 바꿔도 이전 수단의 입력이 주문에 실리던 문제: 결제수단 버튼이 _local.paymentMethod 만 바꾸고 슬롯이 소유한 확장 페이로드·환불계좌는 비우지 않았다. 확장 슬롯과 환불계좌 블록은 결제수단 조건부로 언마운트되지만 _local 값은 남으므로, 무통장에서 현금영수증·환불계좌를 입력한 뒤 카드로 전환하면 그 값이 주문 생성 요청에 그대로 실렸다. 프론트는 결제수단 전환 시 슬롯 소유 상태를 초기화하고, 서버는 환불계좌가 쓰이지 않는 결제수단이면 저장하지 않도록 게이팅한다(현금영수증은 이미 dbank 게이트 보유). - Modal 이 받지 않는 size prop 을 쓰던 4곳을 width 로 교체(무시되어 의도한 폭이 나오지 않았다). 토스 에러 모달의 setState target 을 params 안으로 옮긴다. - 레이아웃 편집기 팔레트에서 체크박스를 넣으면 라벨 없이 네모 칸만 놓이던 문제: Checkbox 는 label prop 을 렌더하지 않으므로 Label 래핑 + Span 텍스트로 바꾸고, Accordion 이 Label 을 받도록 중첩 스펙을 맞춘다. --- .../_bundled/sirsoft-ecommerce/CHANGELOG.md | 4 +- .../extensions/mypage_order_cash_receipt.json | 2 +- .../_modal_issue_cash_receipt.json | 2 +- .../src/Enums/PaymentMethodEnum.php | 17 ++++++ .../Requests/Public/CreateOrderRequest.php | 7 +++ .../Http/Resources/OrderPaymentResource.php | 4 ++ .../CheckoutCashReceiptAndRefundBankTest.php | 56 +++++++++++++++++++ .../specs/shop/checkout-cash-receipt.spec.ts | 33 +++++++++++ .../Resources/OrderPaymentResourceTest.php | 25 +++++++++ .../cash-receipt-ui-and-refund-bank.yaml | 2 + .../sirsoft-tosspayments/CHANGELOG.md | 1 + .../extensions/checkout-payment-error.json | 6 +- .../layouts/editor-spec-default-node.test.tsx | 43 ++++++++++++++ .../editor-spec/componentPalette.json | 21 +++++-- .../editor-spec/nesting.json | 3 +- .../checkoutSummaryBodyPayload.test.tsx | 52 +++++++++++++++++ .../partials/shop/_checkout_payment.json | 28 +++++++++- 17 files changed, 292 insertions(+), 14 deletions(-) diff --git a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md index 5215c9d8..ed582d6c 100644 --- a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md +++ b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md @@ -31,14 +31,14 @@ #### 신청·발급 화면 -- 주문서의 무통장입금 결제 영역에서 현금영수증을 신청할 수 있습니다. 용도(소득공제/지출증빙)에 따라 고를 수 있는 발급 수단이 바뀌며, 그 용도에 쓸 수 없는 수단이 선택되어 있으면 자동으로 되돌립니다. 발급 업체를 설정하지 않은 상점에서는 신청란이 나타나지 않습니다. +- 주문서의 무통장입금 결제 영역에서 현금영수증을 신청할 수 있습니다. 용도(소득공제/지출증빙)에 따라 고를 수 있는 발급 수단이 바뀌며, 그 용도에 쓸 수 없는 수단이 선택되어 있으면 자동으로 되돌립니다. 발급 업체를 설정하지 않은 상점에서는 신청란이 나타나지 않습니다. 결제수단을 다른 것으로 바꾸면 입력했던 신청 내용은 지워집니다. - 구매자 주문 상세에 현금영수증 카드가 표시됩니다. 입금 전에는 안내만, 입금 후 미발급이면 발급 버튼, 발급 후에는 발급일시·용도·식별번호와 영수증 보기 링크가 나타납니다. 환불에 따른 재발급이 실패한 경우에는 안내 문구가 표시됩니다. - 관리자 주문 상세에 현금영수증 카드가 표시됩니다. 발급·발급취소·수동 재발급을 할 수 있고 발급·취소 이력을 함께 볼 수 있습니다. 재발급이 실패한 주문에는 경고와 함께 다시 발급하는 버튼이 나타납니다. - 이커머스 환경설정 주문설정 탭에서 현금영수증 발급 업체, 배송비 과세 방식, 자진발급 사용 여부를 설정할 수 있습니다. #### 환불 계좌 -- 주문서의 무통장입금·가상계좌 결제 영역에서 환불받을 계좌(은행·계좌번호·예금주)를 미리 입력할 수 있습니다. 입력하지 않아도 주문할 수 있지만, 세 칸 중 하나라도 입력하면 나머지도 입력해야 합니다. +- 주문서의 무통장입금·가상계좌 결제 영역에서 환불받을 계좌(은행·계좌번호·예금주)를 미리 입력할 수 있습니다. 입력하지 않아도 주문할 수 있지만, 세 칸 중 하나라도 입력하면 나머지도 입력해야 합니다. 무통장입금·가상계좌가 아닌 결제수단으로 주문하면 환불 계좌는 저장되지 않습니다 — 계좌를 입력한 뒤 카드 등으로 결제수단을 바꿔도 필요 없는 계좌정보가 주문에 남지 않습니다. - 관리자 주문 취소 화면에서 환불 계좌를 입력하거나 수정할 수 있습니다. 주문할 때 입력한 계좌가 있으면 미리 채워집니다. 입금이 완료된 가상계좌 주문은 환불에 계좌가 반드시 필요하므로 입력하지 않으면 취소할 수 없습니다. 무통장입금은 관리자가 직접 이체할 때 참고하도록 선택 입력이며, 카드·간편결제는 원래 결제수단으로 환불되므로 계좌란이 나타나지 않습니다. #### 금액·과세 diff --git a/modules/_bundled/sirsoft-ecommerce/resources/extensions/mypage_order_cash_receipt.json b/modules/_bundled/sirsoft-ecommerce/resources/extensions/mypage_order_cash_receipt.json index d932977b..ae3c827d 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/extensions/mypage_order_cash_receipt.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/extensions/mypage_order_cash_receipt.json @@ -11,7 +11,7 @@ "name": "Modal", "props": { "title": "$t:sirsoft-ecommerce.order.cash_receipt.issue_title", - "size": "small" + "width": "480px" }, "children": [ { diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_modal_issue_cash_receipt.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_modal_issue_cash_receipt.json index e517e2c2..e408ae1b 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_modal_issue_cash_receipt.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_modal_issue_cash_receipt.json @@ -8,7 +8,7 @@ "name": "Modal", "props": { "title": "$t:sirsoft-ecommerce.admin.order.detail.cash_receipt.issue_title", - "size": "small" + "width": "480px" }, "children": [ { diff --git a/modules/_bundled/sirsoft-ecommerce/src/Enums/PaymentMethodEnum.php b/modules/_bundled/sirsoft-ecommerce/src/Enums/PaymentMethodEnum.php index 91b2364c..2ceed8f3 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Enums/PaymentMethodEnum.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Enums/PaymentMethodEnum.php @@ -90,6 +90,23 @@ enum PaymentMethodEnum: string ]); } + /** + * 환불 계좌를 받아야 하는 결제수단인지 확인합니다. + * + * 무통장입금은 관리자가 수동으로 이체할 대상 계좌가 필요하고, 가상계좌는 PG 환불 API 의 + * refundReceiveAccount 로 계좌가 필요하다. 카드·계좌이체·휴대폰은 원거래 취소로 환불되고 + * 마일리지·예치금·무료는 내부 처리이므로 계좌가 필요 없다. + * + * 체크아웃 화면은 결제수단을 바꿔도 입력값을 비우지 않으므로, 계좌가 필요 없는 주문에 + * 계좌정보가 저장되지 않도록 서버가 이 축으로 게이팅한다. + * + * @return bool 환불 계좌 필요 여부 + */ + public function needsRefundBankAccount(): bool + { + return in_array($this, [self::DBANK, self::VBANK], true); + } + /** * 결제수단의 현금성(현금영수증 발급 대상) 금액을 산정합니다. * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/CreateOrderRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/CreateOrderRequest.php index af02c240..f3ad66f9 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/CreateOrderRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/CreateOrderRequest.php @@ -364,6 +364,13 @@ class CreateOrderRequest extends FormRequest */ public function getRefundBankInfo(): ?array { + // 환불 계좌가 쓰이지 않는 결제수단(카드·계좌이체·휴대폰·마일리지·예치금·무료)이면 저장하지 않는다. + // 체크아웃 화면은 결제수단을 바꿔도 입력값을 비우지 않으므로, 무통장에서 계좌를 넣고 + // 카드로 전환하면 그 값이 그대로 전송된다. getCashReceiptInfo() 와 같은 축으로 게이팅한다. + if (! PaymentMethodEnum::tryFrom((string) $this->input('payment_method'))?->needsRefundBankAccount()) { + return null; + } + $bankCode = $this->input('refund_bank.bank_code'); if (blank($bankCode)) { diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderPaymentResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderPaymentResource.php index 7e241259..3fedb59f 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderPaymentResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderPaymentResource.php @@ -72,6 +72,10 @@ class OrderPaymentResource extends BaseApiResource 'vbank_due_at' => $this->vbank_due_at?->toIso8601String(), // audit:allow datetime-display-user-timezone reason: machine ISO8601 parsed by PG plugin JS injectors, display uses *_formatted sibling 'vbank_due_at_formatted' => $this->formatDateTimeStringForUser($this->vbank_due_at), + // 에스크로 여부 — 결제 시점 스냅샷. 부분취소 차단(PaymentRefundListener)의 기준이자 + // 주문 상세(관리자/회원)에서 에스크로 거래임을 표시하는 값. + 'is_escrow' => (bool) $this->is_escrow, + // 무통장입금 정보 'dbank_name' => $this->dbank_name, 'dbank_account' => $this->dbank_account, diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/CheckoutCashReceiptAndRefundBankTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/CheckoutCashReceiptAndRefundBankTest.php index 3d4177a1..ed8d09ac 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/CheckoutCashReceiptAndRefundBankTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/CheckoutCashReceiptAndRefundBankTest.php @@ -348,6 +348,62 @@ class CheckoutCashReceiptAndRefundBankTest extends ModuleTestCase $this->assertNotNull($payment->refund_bank_name); } + /** + * 환불계좌가 의미를 갖지 않는 결제수단이면 저장하지 않는다. + * + * 환불계좌는 무통장(관리자 수동 이체 대상)과 가상계좌(PG 환불 API 의 refundReceiveAccount) + * 에서만 쓰인다. 카드/간편결제는 원거래 취소로 환불되므로 계좌가 필요 없다. + * + * 체크아웃 화면은 결제수단을 바꿔도 _local 의 환불계좌 입력값을 비우지 않으므로, + * 무통장에서 계좌를 넣고 카드로 전환하면 그 값이 그대로 전송된다. 발급될 수 없는 + * 주문에 계좌정보를 남기지 않도록 현금영수증(getCashReceiptInfo)과 같은 축으로 게이팅한다. + * + * @param string $paymentMethod 환불계좌를 쓰지 않는 결제수단 + */ + #[DataProvider('nonRefundBankPaymentMethodProvider')] + public function test_환불계좌를_쓰지_않는_결제수단이면_저장하지_않는다(string $paymentMethod): void + { + $this->postOrder([ + 'payment_method' => $paymentMethod, + 'dbank' => null, + 'refund_bank' => [ + 'bank_code' => '004', + 'account_number' => '110-123-456789', + 'holder' => '홍길동', + ], + ])->assertStatus(201); + + $payment = $this->latestPayment(); + + $this->assertNull( + $payment->refund_bank_code, + "{$paymentMethod} 주문에 환불계좌가 저장되어서는 안 된다" + ); + $this->assertNull($payment->refund_bank_account); + $this->assertNull($payment->refund_bank_holder); + $this->assertNull($payment->refund_bank_name); + } + + /** + * 환불계좌를 쓰지 않는 결제수단 (무통장·가상계좌 제외). + * + * @return array + */ + public static function nonRefundBankPaymentMethodProvider(): array + { + $cases = []; + + foreach (PaymentMethodEnum::cases() as $method) { + if (in_array($method, [PaymentMethodEnum::DBANK, PaymentMethodEnum::VBANK], true)) { + continue; + } + + $cases[$method->value] = [$method->value]; + } + + return $cases; + } + /** * @return array, array}> */ diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/checkout-cash-receipt.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/checkout-cash-receipt.spec.ts index 34512041..aa007335 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/checkout-cash-receipt.spec.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/checkout-cash-receipt.spec.ts @@ -20,6 +20,7 @@ * checkout_purpose_switch_resets_invalid_identifier, * checkout_identifier_type_options_by_purpose, * checkout_identifier_cleared_on_type_change, + * checkout_state_reset_on_payment_method_change, * new_ui_uses_portable_only, * new_ui_has_no_tailwind_breakpoint */ @@ -313,6 +314,38 @@ test.describe('체크아웃 현금영수증 신청 폼 (무통장 슬롯 주입) await expect(page.locator(IDENTIFIER)).toHaveValue(''); }); + test('결제수단을 바꾸면 현금영수증 입력이 초기화된다 (이전 수단 값 잔류 금지)', async ({ page }) => { + // 회귀: 결제수단 버튼이 _local.paymentMethod 만 바꾸고 슬롯이 소유한 _local + // (checkoutExtraPayload / refundBank*) 은 비우지 않아, 무통장에서 현금영수증을 입력한 뒤 + // 카드로 전환하면 그 값이 주문 생성 POST 에 그대로 실렸다. + await gotoCheckout(page); + await selectDbank(page); + + const others = page.locator('[data-testid^="checkout-payment-method-"]:not([data-testid$="-dbank"])'); + if ((await others.count()) === 0) { + test.info().annotations.push({ + type: 'coverage-gap', + description: '무통장 외 활성 결제수단이 없어 전환 시 초기화는 미검증 (환경설정 의존)', + }); + return; + } + + // 무통장에서 현금영수증 신청 + 번호 입력 + await page.locator(TOGGLE).check(); + await page.locator(IDENTIFIER).fill('01012345678'); + await expect(page.locator(IDENTIFIER)).toHaveValue('01012345678'); + + // 다른 결제수단으로 전환 → 슬롯 언마운트 + await others.first().click(); + await expect(page.locator(SLOT)).toHaveCount(0); + + // 무통장으로 돌아오면 이전 입력이 남아 있지 않다 (신청 토글이 꺼진 초기 상태) + await selectDbank(page); + await expect(page.locator(SLOT)).toHaveCount(1); + await expect(page.locator(TOGGLE)).not.toBeChecked(); + await expect(page.locator(FIELDS)).toHaveCount(0); + }); + test('발급수단을 바꾸면 이전에 입력한 번호가 비워진다', async ({ page }) => { await gotoCheckout(page); await selectDbank(page); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Resources/OrderPaymentResourceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Resources/OrderPaymentResourceTest.php index 2f609ece..1c1252ff 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Resources/OrderPaymentResourceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Resources/OrderPaymentResourceTest.php @@ -50,4 +50,29 @@ class OrderPaymentResourceTest extends ModuleTestCase $this->assertStringContainsString('신한카드', $array['account_info']); $this->assertStringContainsString('1234-****-****-5678', $array['account_info']); } + + /** + * 에스크로 여부가 API 로 노출된다. + * + * 회귀 배경(#454): is_escrow 는 DB·모델·부분취소 차단 로직(PaymentRefundListener)에는 + * 있었지만 Resource 가 내보내지 않아, 이 값을 참조하는 주문 상세 화면(관리자/회원)의 + * 에스크로 표시가 항상 false 로 평가되어 렌더되지 않았다. + */ + public function test_is_escrow_is_exposed(): void + { + $escrow = OrderPaymentFactory::new()->make([ + 'payment_method' => PaymentMethodEnum::VBANK, + 'is_escrow' => true, + ]); + + $normal = OrderPaymentFactory::new()->make([ + 'payment_method' => PaymentMethodEnum::VBANK, + 'is_escrow' => false, + ]); + + $request = Request::create('/'); + + $this->assertTrue((new OrderPaymentResource($escrow))->toArray($request)['is_escrow']); + $this->assertFalse((new OrderPaymentResource($normal))->toArray($request)['is_escrow']); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-ui-and-refund-bank.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-ui-and-refund-bank.yaml index 96600897..dfde18d1 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-ui-and-refund-bank.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-ui-and-refund-bank.yaml @@ -139,6 +139,8 @@ effects: - checkout_purpose_switch_resets_invalid_identifier # 소득공제 전환 시 business → phone 리셋 - checkout_identifier_type_options_by_purpose # 소득공제 2종 / 지출증빙 3종 - checkout_identifier_cleared_on_type_change # 발급수단 변경 시 번호 초기화 + - checkout_state_reset_on_payment_method_change # 결제수단 전환 시 슬롯 소유 _local 초기화 (이전 수단 값 잔류 금지) + - refund_bank_not_stored_for_non_bank_methods # 환불계좌를 쓰지 않는 결제수단이면 서버가 저장하지 않음 # 주문 생성 저장 (FormRequest 4단계 사슬) - checkout_cash_receipt_request_persisted # is_cash_receipt_requested + 용도 + 수단 diff --git a/plugins/_bundled/sirsoft-tosspayments/CHANGELOG.md b/plugins/_bundled/sirsoft-tosspayments/CHANGELOG.md index 4e3b4f37..5004aac6 100644 --- a/plugins/_bundled/sirsoft-tosspayments/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-tosspayments/CHANGELOG.md @@ -27,6 +27,7 @@ - 가상계좌·계좌이체 결제에 에스크로(구매안전서비스)를 적용할 수 있습니다. 사용 안 함 / 강제 사용 / 구매자 선택 중에서 고를 수 있습니다. - 에스크로 결제 시 결제창에 상품 정보(상품명·단가·수량)를 함께 전달합니다. 가상계좌와 계좌이체 모두에 적용됩니다. - 에스크로를 켜면 설정 화면에 운영 안내가 표시됩니다 — 에스크로 주문은 부분취소가 불가하다는 점과, 배송정보는 토스 상점관리자에서 등록해야 한다는 점을 안내하며 상점관리자로 바로 이동할 수 있습니다. +- 에스크로로 결제된 주문은 관리자 주문 상세와 구매자 주문 상세에 에스크로 거래임이 표시됩니다. #### 현금영수증 diff --git a/plugins/_bundled/sirsoft-tosspayments/resources/extensions/checkout-payment-error.json b/plugins/_bundled/sirsoft-tosspayments/resources/extensions/checkout-payment-error.json index 56c6bfd0..c04c48c4 100644 --- a/plugins/_bundled/sirsoft-tosspayments/resources/extensions/checkout-payment-error.json +++ b/plugins/_bundled/sirsoft-tosspayments/resources/extensions/checkout-payment-error.json @@ -7,7 +7,7 @@ "name": "Modal", "props": { "title": "$t:sirsoft-tosspayments.payment_error_title", - "size": "sm" + "width": "420px" }, "children": [ { @@ -54,8 +54,8 @@ { "type": "click", "handler": "setState", - "target": "local", "params": { + "target": "local", "paymentErrorMessage": null } }, @@ -79,7 +79,7 @@ "name": "Modal", "props": { "title": "$t:sirsoft-tosspayments.payment_cancel_title", - "size": "sm" + "width": "420px" }, "children": [ { diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/editor-spec-default-node.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/editor-spec-default-node.test.tsx index 5afc6653..c2daa142 100644 --- a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/editor-spec-default-node.test.tsx +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/editor-spec-default-node.test.tsx @@ -200,6 +200,49 @@ describe('sirsoft-admin_basic editor-spec defaultNode 공통 디자인', () => { }); }); + // Checkbox 라벨 렌더 회귀 가드. + // Checkbox 컴포넌트는 label prop 을 받기만 하고 렌더하지 않는다(부모 Label 이 표시 담당). + // 팔레트가 label prop 만 심으면 편집기 삽입 시 라벨 없는 빈 체크박스가 나온다. + describe('Checkbox defaultNode — 라벨이 실제로 렌더된다', () => { + it('Checkbox defaultNode 는 label prop 이 아니라 Label 래핑 + Span 텍스트를 쓴다', () => { + const node = entries.Checkbox?.defaultNode; + expect(node).toBeDefined(); + + // 최상위는 Label — Checkbox 를 감싸 클릭 영역과 표시를 함께 제공한다. + expect(node?.name).toBe('Label'); + + const children = node?.children ?? []; + const checkbox = children.find((c) => c.name === 'Checkbox'); + const span = children.find((c) => c.name === 'Span'); + + expect(checkbox, 'Checkbox 자식 누락').toBeDefined(); + expect(span, '라벨 텍스트 Span 누락').toBeDefined(); + expect(span?.text).toBe('$t:layout_editor.palette.checkbox.default_label'); + + // 렌더되지 않는 label prop 에 의존하지 않는다. + expect(node?.props?.label, 'Label 에 label prop 사용 금지').toBeUndefined(); + expect(checkbox?.props?.label, 'Checkbox 의 label prop 은 렌더되지 않는다').toBeUndefined(); + }); + + it('Checkbox defaultNode 를 렌더하면 라벨 텍스트가 DOM 에 나타난다', async () => { + const node = entries.Checkbox?.defaultNode as DefaultNode; + const registry = createMockComponentRegistryWithBasics(); + const t = createLayoutTest(wrapAsLayout(node), { + templateId: 'sirsoft-admin_basic', + componentRegistry: registry, + locale: 'ko', + }); + const { container } = await t.render(); + + // 체크박스 input 과 라벨 텍스트가 함께 렌더된다(빈 체크박스 회귀 차단). + const label = container.querySelector('label'); + expect(label, 'Label 래퍼가 렌더되어야 함').not.toBeNull(); + expect((label?.textContent ?? '').trim().length, '라벨 텍스트가 비어 있음').toBeGreaterThan(0); + + t.cleanup(); + }); + }); + // defaultNode prop shape 결함 회귀 가드. // 라이브 검증에서 발견한 defaultNode↔컴포넌트 shape 불일치 결함들이 되돌아가지 않게 한다. describe('defaultNode prop shape 정합', () => { diff --git a/templates/_bundled/sirsoft-admin_basic/editor-spec/componentPalette.json b/templates/_bundled/sirsoft-admin_basic/editor-spec/componentPalette.json index 771d02ee..8d7a0b7f 100644 --- a/templates/_bundled/sirsoft-admin_basic/editor-spec/componentPalette.json +++ b/templates/_bundled/sirsoft-admin_basic/editor-spec/componentPalette.json @@ -673,11 +673,24 @@ "requiresDefaultNode": true, "defaultNode": { "type": "basic", - "name": "Checkbox", + "name": "Label", "props": { - "label": "$t:layout_editor.palette.checkbox.default_label", - "className": "w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500" - } + "className": "flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300" + }, + "children": [ + { + "type": "basic", + "name": "Checkbox", + "props": { + "className": "w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500" + } + }, + { + "type": "basic", + "name": "Span", + "text": "$t:layout_editor.palette.checkbox.default_label" + } + ] } }, "RadioGroup": { diff --git a/templates/_bundled/sirsoft-admin_basic/editor-spec/nesting.json b/templates/_bundled/sirsoft-admin_basic/editor-spec/nesting.json index dd7ceb7c..2bb548ad 100644 --- a/templates/_bundled/sirsoft-admin_basic/editor-spec/nesting.json +++ b/templates/_bundled/sirsoft-admin_basic/editor-spec/nesting.json @@ -466,7 +466,8 @@ "Input", "Textarea", "Select", - "Checkbox" + "Checkbox", + "Label" ] } } diff --git a/templates/_bundled/sirsoft-basic/__tests__/layouts/checkoutSummaryBodyPayload.test.tsx b/templates/_bundled/sirsoft-basic/__tests__/layouts/checkoutSummaryBodyPayload.test.tsx index 3383cec3..10e9f4f2 100644 --- a/templates/_bundled/sirsoft-basic/__tests__/layouts/checkoutSummaryBodyPayload.test.tsx +++ b/templates/_bundled/sirsoft-basic/__tests__/layouts/checkoutSummaryBodyPayload.test.tsx @@ -18,6 +18,7 @@ import { describe, it, expect } from 'vitest'; import { DataBindingEngine } from '@core/template-engine/DataBindingEngine'; import checkoutSummaryJson from '../../layouts/partials/shop/_checkout_summary.json'; +import checkoutPaymentJson from '../../layouts/partials/shop/_checkout_payment.json'; import checkoutJson from '../../layouts/shop/checkout.json'; /** 객체 트리에서 조건을 만족하는 첫 노드를 깊이우선 탐색 */ @@ -231,6 +232,57 @@ describe('주문 생성 body — 확장 병합 칸 계약', () => { }); }); + // 결제수단 전환 시 이전 수단이 소유하던 입력값이 남아 payload 에 실리던 결함의 회귀 가드. + // 확장 슬롯(현금영수증)과 환불계좌 블록은 결제수단 조건부로 언마운트되지만 _local 값은 남는다. + // 무통장에서 입력 → 카드로 전환 시 그 값이 그대로 주문 생성 POST 에 실렸다. + describe('결제수단 전환 — 이전 수단 소유 상태 초기화', () => { + it('결제수단 선택 액션이 확장 칸과 환불계좌를 함께 비운다', () => { + const methodSelect = findNode( + checkoutPaymentJson, + (n: any) => + Array.isArray(n?.actions) && + n.actions.some( + (a: any) => + a?.type === 'click' && + JSON.stringify(a).includes('_local.paymentMethod'), + ), + ); + expect(methodSelect, '결제수단 선택 액션 노드를 찾지 못했다').toBeDefined(); + + const clickAction = methodSelect.actions.find((a: any) => a?.type === 'click'); + const inner = clickAction?.params?.actions ?? []; + const targets = inner.map((a: any) => a?.params?.target); + + // 결제수단 자체는 설정하고 + expect(targets).toContain('_local.paymentMethod'); + // 이전 수단이 소유하던 상태는 비운다 + expect(targets).toContain('_local.checkoutExtraPayload'); + expect(targets).toContain('_local.refundBankCode'); + expect(targets).toContain('_local.refundBankAccount'); + expect(targets).toContain('_local.refundBankHolder'); + }); + + it('초기화된 상태로 조립한 payload 에는 이전 수단의 값이 남지 않는다', () => { + // 무통장에서 현금영수증·환불계좌를 입력한 뒤 카드로 전환된 직후의 _local 상태 + const ctx = baseCtx({ + _local: { + ...baseCtx()._local, + paymentMethod: 'card', + checkoutExtraPayload: {}, + refundBankCode: '', + refundBankAccount: '', + refundBankHolder: '', + }, + }); + + const body = evalBody(ctx); + + expect(body.refund_bank).toBeNull(); + expect(body.cash_receipt_requested).toBeUndefined(); + expect(body.cash_receipt_identifier).toBeUndefined(); + }); + }); + // ───────────────────────────────────────────────────────────── // #454 — 플러그인 결제수단(toss_*)의 코어 결제수단 전송 // diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_payment.json b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_payment.json index 9bb0019a..224f0e9f 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_payment.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_payment.json @@ -146,9 +146,33 @@ ], "actions": [ { + "comment": "결제수단 전환 시 이전 수단이 소유하던 입력값을 함께 비운다. 확장 슬롯(현금영수증 등)과 환불계좌 블록은 결제수단 조건부로 언마운트되지만 _local 값은 남으므로, 초기화하지 않으면 무통장에서 입력한 값이 카드 주문 payload 에 그대로 실린다.", "type": "click", - "handler": "setState", - "params": { "target": "_local.paymentMethod", "value": "{{method.id}}" } + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { "target": "_local.paymentMethod", "value": "{{method.id}}" } + }, + { + "handler": "setState", + "params": { "target": "_local.checkoutExtraPayload", "value": {} } + }, + { + "handler": "setState", + "params": { "target": "_local.refundBankCode", "value": "" } + }, + { + "handler": "setState", + "params": { "target": "_local.refundBankAccount", "value": "" } + }, + { + "handler": "setState", + "params": { "target": "_local.refundBankHolder", "value": "" } + } + ] + } } ] }