fix(kginicis): 설정 저장 오류 안내 개선
This commit is contained in:
1 parent
c6cab1f668
commit
88116de788
10 files changed
+203
-11
No files matched your search
@@ -9,6 +9,7 @@
|
||||
### Fixed
|
||||
|
||||
- 이커머스 주문설정 화면에서 KG 이니시스 플러그인이 테스트 모드일 때 운영 전환 경고와 설정 이동 버튼을 표시하도록 보강했습니다.
|
||||
- KG 이니시스 설정 저장 실패 시 검증 오류가 화면 상단에만 남아 사용자가 원인을 놓치지 않도록 오류 영역으로 자동 이동하고, 일본 결제(CBT) 샘플값 안내를 한 번만 표시하도록 개선했습니다.
|
||||
|
||||
## [1.0.0] - 2026-07-01
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '상품',
|
||||
|
||||
+68
@@ -49,6 +49,21 @@ function collectClassNames(node: unknown, classNames: string[] = []): string[] {
|
||||
return classNames;
|
||||
}
|
||||
|
||||
function collectNodes(node: unknown, predicate: (value: Record<string, unknown>) => boolean, nodes: Record<string, unknown>[] = []): Record<string, unknown>[] {
|
||||
if (!node || typeof node !== 'object') {
|
||||
return nodes;
|
||||
}
|
||||
const value = node as Record<string, unknown>;
|
||||
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<string, unknown>[] | undefined;
|
||||
expect(onError).toBeDefined();
|
||||
expect(onError?.[0]?.handler).toBe('setState');
|
||||
|
||||
const params = (onError?.[0]?.params ?? {}) as Record<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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<string, unknown>[] | undefined;
|
||||
const scrollAction = onError?.find((action) => action.handler === 'scrollIntoView');
|
||||
|
||||
expect(scrollAction).toBeDefined();
|
||||
|
||||
const params = (scrollAction?.params ?? {}) as Record<string, unknown>;
|
||||
expect(params).toMatchObject({
|
||||
selector: '#kginicis_settings_validation_error',
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
waitForElement: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+18
-4
@@ -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;
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\PayKginicis\Tests\Feature\Controllers;
|
||||
|
||||
use App\Extension\HookManager;
|
||||
use Plugins\Sirsoft\PayKginicis\Listeners\ValidateCbtSettingsListener;
|
||||
use Plugins\Sirsoft\PayKginicis\Tests\PluginTestCase;
|
||||
|
||||
class AdminPluginSettingsValidationTest extends PluginTestCase
|
||||
{
|
||||
public function test_live_cbt_settings_validation_returns_field_errors_on_save(): void
|
||||
{
|
||||
app()->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<int, array<int, callable>> $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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-1
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user