From 88116de788ccf5a4cf49692b385ca0e598436491 Mon Sep 17 00:00:00 2001 From: thisgun Date: Tue, 7 Jul 2026 18:19:17 +0900 Subject: [PATCH] =?UTF-8?q?fix(kginicis):=20=EC=84=A4=EC=A0=95=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EC=98=A4=EB=A5=98=20=EC=95=88=EB=82=B4=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sirsoft-pay_kginicis/CHANGELOG.md | 1 + .../sirsoft-pay_kginicis/lang/en/messages.php | 2 +- .../sirsoft-pay_kginicis/lang/ko/messages.php | 2 +- .../layouts/pluginSettingsSticky.test.tsx | 68 ++++++++++++++++ .../resources/lang/en.json | 2 +- .../resources/lang/ko.json | 2 +- .../layouts/admin/plugin_settings.json | 18 ++++- .../Listeners/ValidateCbtSettingsListener.php | 22 +++++- .../AdminPluginSettingsValidationTest.php | 79 +++++++++++++++++++ .../ValidateCbtSettingsListenerTest.php | 18 ++++- 10 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminPluginSettingsValidationTest.php diff --git a/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md b/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md index 9d135ab4..050a5d4a 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - 이커머스 주문설정 화면에서 KG 이니시스 플러그인이 테스트 모드일 때 운영 전환 경고와 설정 이동 버튼을 표시하도록 보강했습니다. +- KG 이니시스 설정 저장 실패 시 검증 오류가 화면 상단에만 남아 사용자가 원인을 놓치지 않도록 오류 영역으로 자동 이동하고, 일본 결제(CBT) 샘플값 안내를 한 번만 표시하도록 개선했습니다. ## [1.0.0] - 2026-07-01 diff --git a/plugins/_bundled/sirsoft-pay_kginicis/lang/en/messages.php b/plugins/_bundled/sirsoft-pay_kginicis/lang/en/messages.php index 04b6f559..3c00cef3 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/lang/en/messages.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/lang/en/messages.php @@ -30,7 +30,7 @@ return [ 'japan_contact_email_required' => 'Contact email for the live Japan payment window is required.', 'japan_contact_phone_required' => 'Contact phone for the live Japan payment window is required.', 'japan_contact_opening_hours_required' => 'Contact opening hours for the live Japan payment window are required.', - 'replace_sample_value' => 'Replace sample values with your real contract information in live mode.', + 'replace_sample_value' => 'Replace the sample Japan merchant display information with your real contract information in live mode.', ], 'defaults' => [ 'good_name' => 'Goods', diff --git a/plugins/_bundled/sirsoft-pay_kginicis/lang/ko/messages.php b/plugins/_bundled/sirsoft-pay_kginicis/lang/ko/messages.php index 80dcda6e..91395990 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/lang/ko/messages.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/lang/ko/messages.php @@ -30,7 +30,7 @@ return [ 'japan_contact_email_required' => '운영 일본 결제창에 표시할 문의 이메일이 필요합니다.', 'japan_contact_phone_required' => '운영 일본 결제창에 표시할 문의 전화번호가 필요합니다.', 'japan_contact_opening_hours_required' => '운영 일본 결제창에 표시할 문의 영업시간이 필요합니다.', - 'replace_sample_value' => '운영 모드에서는 샘플값 대신 실제 계약 정보를 입력하세요.', + 'replace_sample_value' => '운영 모드에서는 일본 가맹점 표시 정보의 샘플값을 실제 계약 정보로 변경하세요.', ], 'defaults' => [ 'good_name' => '상품', diff --git a/plugins/_bundled/sirsoft-pay_kginicis/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx b/plugins/_bundled/sirsoft-pay_kginicis/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx index 6f4c7ba8..34150d02 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx +++ b/plugins/_bundled/sirsoft-pay_kginicis/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx @@ -49,6 +49,21 @@ function collectClassNames(node: unknown, classNames: string[] = []): string[] { return classNames; } +function collectNodes(node: unknown, predicate: (value: Record) => boolean, nodes: Record[] = []): Record[] { + if (!node || typeof node !== 'object') { + return nodes; + } + const value = node as Record; + if (predicate(value)) { + nodes.push(value); + } + for (const child of Object.values(value)) { + collectNodes(child, predicate, nodes); + } + + return nodes; +} + describe('plugin_settings 하단 버튼 sticky 고정', () => { it('footer_buttons 에 sticky bottom 고정 클래스가 존재해야 한다', () => { const footer = findById(pluginSettingsLayout, 'footer_buttons'); @@ -80,4 +95,57 @@ describe('plugin_settings 하단 버튼 sticky 고정', () => { expect(className).not.toContain('row-stack'); } }); + + it('저장 실패 시 apiCall onError 에서 validation errors 를 error 컨텍스트에서 읽어야 한다', () => { + const saveActions = collectNodes(pluginSettingsLayout, (node) => { + return node.handler === 'apiCall' + && typeof node.target === 'string' + && node.target.includes('/api/admin/plugins/') + && node.target.includes('/settings'); + }); + + expect(saveActions).toHaveLength(1); + + const onError = saveActions[0].onError as Record[] | undefined; + expect(onError).toBeDefined(); + expect(onError?.[0]?.handler).toBe('setState'); + + const params = (onError?.[0]?.params ?? {}) as Record; + expect(params.errors).toBe('{{error.errors ?? { _general: error.message }}}'); + }); + + it('저장 실패 시 상단 검증 오류 영역으로 자동 스크롤해야 한다', () => { + const validationError = collectNodes(pluginSettingsLayout, (node) => { + return node.id === 'validation_error'; + })[0]; + + expect(validationError).toBeDefined(); + + const validationErrorProps = (validationError.props ?? {}) as Record; + expect(validationErrorProps.id).toBe('kginicis_settings_validation_error'); + expect(validationErrorProps.role).toBe('alert'); + expect(String(validationErrorProps.className)).toContain('scroll-mt-32'); + + const saveActions = collectNodes(pluginSettingsLayout, (node) => { + return node.handler === 'apiCall' + && typeof node.target === 'string' + && node.target.includes('/api/admin/plugins/') + && node.target.includes('/settings'); + }); + + expect(saveActions).toHaveLength(1); + + const onError = saveActions[0].onError as Record[] | undefined; + const scrollAction = onError?.find((action) => action.handler === 'scrollIntoView'); + + expect(scrollAction).toBeDefined(); + + const params = (scrollAction?.params ?? {}) as Record; + expect(params).toMatchObject({ + selector: '#kginicis_settings_validation_error', + behavior: 'smooth', + block: 'start', + waitForElement: true, + }); + }); }); diff --git a/plugins/_bundled/sirsoft-pay_kginicis/resources/lang/en.json b/plugins/_bundled/sirsoft-pay_kginicis/resources/lang/en.json index 12d265df..aba0f1ca 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/resources/lang/en.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/resources/lang/en.json @@ -53,7 +53,7 @@ "japan_contact_email_required": "Contact email for the live Japan payment window is required.", "japan_contact_phone_required": "Contact phone for the live Japan payment window is required.", "japan_contact_opening_hours_required": "Contact opening hours for the live Japan payment window are required.", - "replace_sample_value": "Replace sample values with your real contract information in live mode." + "replace_sample_value": "Replace the sample Japan merchant display information with your real contract information in live mode." } }, "payment_error": { diff --git a/plugins/_bundled/sirsoft-pay_kginicis/resources/lang/ko.json b/plugins/_bundled/sirsoft-pay_kginicis/resources/lang/ko.json index 006fa5a4..b19d870a 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/resources/lang/ko.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/resources/lang/ko.json @@ -53,7 +53,7 @@ "japan_contact_email_required": "운영 일본 결제창에 표시할 문의 이메일이 필요합니다.", "japan_contact_phone_required": "운영 일본 결제창에 표시할 문의 전화번호가 필요합니다.", "japan_contact_opening_hours_required": "운영 일본 결제창에 표시할 문의 영업시간이 필요합니다.", - "replace_sample_value": "운영 모드에서는 샘플값 대신 실제 계약 정보를 입력하세요." + "replace_sample_value": "운영 모드에서는 일본 가맹점 표시 정보의 샘플값을 실제 계약 정보로 변경하세요." } }, "payment_error": { diff --git a/plugins/_bundled/sirsoft-pay_kginicis/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-pay_kginicis/resources/layouts/admin/plugin_settings.json index 17391a71..a4df03c1 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/resources/layouts/admin/plugin_settings.json @@ -61,7 +61,11 @@ "name": "Div", "if": "{{_local.errors}}", "props": { - "className": "alert-danger" + "id": "kginicis_settings_validation_error", + "role": "alert", + "aria-live": "assertive", + "tabIndex": -1, + "className": "alert-danger scroll-mt-32" }, "children": [ { @@ -2688,7 +2692,17 @@ "params": { "target": "local", "isSaving": false, - "errors": "{{$error.errors}}" + "errors": "{{error.errors ?? { _general: error.message }}}" + } + }, + { + "handler": "scrollIntoView", + "params": { + "selector": "#kginicis_settings_validation_error", + "behavior": "smooth", + "block": "start", + "waitForElement": true, + "timeout": 2000 } }, { diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Listeners/ValidateCbtSettingsListener.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Listeners/ValidateCbtSettingsListener.php index c2314a25..67b49b40 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Listeners/ValidateCbtSettingsListener.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Listeners/ValidateCbtSettingsListener.php @@ -36,12 +36,15 @@ class ValidateCbtSettingsListener implements HookListenerInterface 'japan_contact_opening_hours' => '10:00-18:00', ]; + private const SAMPLE_VALUE_ERROR_FIELD = 'japan_contract_info'; + public static function getSubscribedHooks(): array { return [ 'core.plugin_settings.before_save' => [ 'method' => 'validateBeforeSave', 'priority' => 10, + 'sync' => true, ], ]; } @@ -73,10 +76,10 @@ class ValidateCbtSettingsListener implements HookListenerInterface } } - foreach (self::SAMPLE_VALUES as $field => $sampleValue) { - if (trim((string) ($settings[$field] ?? '')) === $sampleValue) { - $errors[$field][] = __('sirsoft-pay_kginicis::messages.settings_validation.replace_sample_value'); - } + if ($this->hasSampleDisplayValue($settings)) { + $errors[self::SAMPLE_VALUE_ERROR_FIELD][] = __( + 'sirsoft-pay_kginicis::messages.settings_validation.replace_sample_value' + ); } } @@ -103,4 +106,15 @@ class ValidateCbtSettingsListener implements HookListenerInterface { return trim((string) $value) === ''; } + + private function hasSampleDisplayValue(array $settings): bool + { + foreach (self::SAMPLE_VALUES as $field => $sampleValue) { + if (trim((string) ($settings[$field] ?? '')) === $sampleValue) { + return true; + } + } + + return false; + } } diff --git a/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminPluginSettingsValidationTest.php b/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminPluginSettingsValidationTest.php new file mode 100644 index 00000000..95b4ac7e --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminPluginSettingsValidationTest.php @@ -0,0 +1,79 @@ +setLocale('ko'); + + $admin = $this->createAdminUser(['core.plugins.update']); + $originalBeforeSaveHooks = HookManager::getHooks()['core.plugin_settings.before_save'] ?? []; + + HookManager::addAction( + 'core.plugin_settings.before_save', + fn (string $identifier, array $settings) => app(ValidateCbtSettingsListener::class) + ->validateBeforeSave($identifier, $settings), + 1 + ); + + try { + $response = $this->actingAs($admin) + ->withHeaders(['Accept-Language' => 'ko']) + ->putJson('/api/admin/plugins/sirsoft-pay_kginicis/settings', [ + 'is_test_mode' => false, + 'japan_enabled' => true, + 'test_japan_sign_key' => 'test-cbt-hash-key', + 'live_japan_mid' => '', + 'live_japan_sign_key' => '', + 'japan_merchant_name' => 'サンプルストア', + 'japan_merchant_name_kana' => 'サンプルストア', + 'japan_merchant_name_alphabet' => 'Sample Store', + 'japan_merchant_name_short' => 'サンプル', + 'japan_contact_name' => 'サポート窓口', + 'japan_contact_email' => 'support@example.com', + 'japan_contact_phone' => '0120-123-456', + 'japan_contact_opening_hours' => '10:00-18:00', + ]); + + $response->assertStatus(422) + ->assertJsonPath( + 'errors.live_japan_mid.0', + '운영 모드에서 일본 결제(CBT)를 사용하려면 라이브 일본 MID가 필요합니다.' + ) + ->assertJsonPath( + 'errors.japan_contract_info.0', + '운영 모드에서는 일본 가맹점 표시 정보의 샘플값을 실제 계약 정보로 변경하세요.' + ); + + $errors = $response->json('errors'); + $this->assertCount(1, $errors['japan_contract_info'] ?? []); + $this->assertArrayNotHasKey('japan_merchant_name', $errors); + $this->assertArrayNotHasKey('japan_merchant_name_alphabet', $errors); + $this->assertArrayNotHasKey('japan_contact_email', $errors); + } finally { + $this->restoreBeforeSaveHooks($originalBeforeSaveHooks); + } + } + + /** + * @param array> $hooks + */ + private function restoreBeforeSaveHooks(array $hooks): void + { + HookManager::clearAction('core.plugin_settings.before_save'); + + foreach ($hooks as $priority => $callbacks) { + foreach ($callbacks as $callback) { + HookManager::addAction('core.plugin_settings.before_save', $callback, (int) $priority); + } + } + } +} diff --git a/plugins/_bundled/sirsoft-pay_kginicis/tests/Unit/Listeners/ValidateCbtSettingsListenerTest.php b/plugins/_bundled/sirsoft-pay_kginicis/tests/Unit/Listeners/ValidateCbtSettingsListenerTest.php index a596ec12..69fd4d4f 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/tests/Unit/Listeners/ValidateCbtSettingsListenerTest.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/tests/Unit/Listeners/ValidateCbtSettingsListenerTest.php @@ -13,6 +13,11 @@ class ValidateCbtSettingsListenerTest extends PluginTestCase public function test_plugin_registers_cbt_settings_validation_listener(): void { $this->assertContains(ValidateCbtSettingsListener::class, (new Plugin())->getHookListeners()); + + $hooks = ValidateCbtSettingsListener::getSubscribedHooks(); + $this->assertSame('validateBeforeSave', $hooks['core.plugin_settings.before_save']['method'] ?? null); + $this->assertSame(10, $hooks['core.plugin_settings.before_save']['priority'] ?? null); + $this->assertTrue($hooks['core.plugin_settings.before_save']['sync'] ?? false); } public function test_plugin_schema_includes_jpy_payment_method_restriction_option(): void @@ -120,16 +125,27 @@ class ValidateCbtSettingsListenerTest extends PluginTestCase public function test_live_japan_payment_rejects_sample_jppg_display_values(): void { $this->mockCurrentSettings([]); + app()->setLocale('ko'); $listener = new ValidateCbtSettingsListener(); try { $listener->validateBeforeSave('sirsoft-pay_kginicis', $this->validLiveSettings([ 'japan_merchant_name' => 'サンプルストア', + 'japan_merchant_name_alphabet' => 'Sample Store', + 'japan_contact_email' => 'support@example.com', ])); $this->fail('ValidationException was not thrown.'); } catch (ValidationException $e) { - $this->assertArrayHasKey('japan_merchant_name', $e->errors()); + $errors = $e->errors(); + + $this->assertArrayHasKey('japan_contract_info', $errors); + $this->assertArrayNotHasKey('japan_merchant_name', $errors); + $this->assertArrayNotHasKey('japan_merchant_name_alphabet', $errors); + $this->assertArrayNotHasKey('japan_contact_email', $errors); + $this->assertSame([ + '운영 모드에서는 일본 가맹점 표시 정보의 샘플값을 실제 계약 정보로 변경하세요.', + ], $errors['japan_contract_info']); } }