fix(nhnkcp): 주문설정 테스트결제 경고 표시 개선

This commit is contained in:
thisgun
2026-07-07 16:38:32 +09:00
parent b9355f051c
commit f744b816b6
10 changed files with 352 additions and 1 deletions
@@ -9,3 +9,4 @@
### Added
- 환불 진행 중 안내(`refund.in_progress`)와 KCP CLI 바이너리 누락·실행 권한 부족 안내(`errors.cli_binary_missing`, `errors.cli_binary_not_executable`) 일본어 번역 추가 — 환불 중복 요청·CLI 실행 환경 오류 안내가 일본어 로케일에서 자연스럽게 표시됩니다.
- 주문 설정 화면의 NHN KCP 테스트모드 경고(`nhnkcp_test_mode_status`, `test_mode_settings_warning_*`) 일본어 번역 추가 — 테스트결제 상태 안내와 실결제 설정 이동 버튼이 일본어 로케일에서 자연스럽게 표시됩니다.
@@ -5,6 +5,7 @@
"kcp_escrow_delivery": "エスクロー配送",
"kcp_status": "決済状態",
"nhnkcp_easy_pay_config": "簡便決済設定",
"nhnkcp_test_mode_status": "テストモード状態",
"nhnkcp_test_map": "テスト決済マップ",
"nicepay_test_map": "テスト決済マップ",
"settings": "決済設定",
@@ -48,6 +49,10 @@
"admin": {
"test_mode_badge": "テスト決済",
"test_mode_order_notice": "この注文はテスト決済として処理されました。",
"test_mode_settings_warning_plugin": "NHN KCP",
"test_mode_settings_warning_title": "(注意)NHN KCP決済設定が現在テスト決済になっています。",
"test_mode_settings_warning_body": "テスト決済では実際の決済は発生しません。ショップ運営中の場合は必ず実決済に設定して運営してください。",
"test_mode_settings_warning_action": "実決済に設定",
"order_info_title": "NHN KCP決済情報",
"order_info_description": "DBに保存されたKCP決済情報を表示します。",
"pay_status_approved": "承認",
@@ -12,6 +12,7 @@
- 관리자 주문상세의 NHN KCP 결제 정보 패널이 원승인 정보와 현재 취소/환불 상태를 함께 표시하도록 보강했습니다.
- PC 결제창 close-report와 승인 콜백이 경쟁할 때 결제 성공 주문이 실패 처리로 덮이지 않도록 주문번호 단위 콜백 락과 최신 결제 상태 가드를 적용했습니다.
- NHN KCP 부분취소 요청에서 `rem_mny`를 취소 후 잔액이 아닌 현재 부분취소 가능 금액으로 전달해 큰 금액 부분취소가 거절될 수 있던 문제를 수정했습니다.
- 이커머스 주문설정 화면에서 NHN KCP 플러그인이 테스트 모드일 때 운영 전환 경고와 설정 이동 버튼을 표시하도록 보강했습니다.
## [1.0.0-beta.5] - 2026-06-23
@@ -4,6 +4,7 @@
"health": "Health",
"kcp_escrow_delivery": "Escrow Delivery",
"kcp_status": "Payment Status",
"nhnkcp_test_mode_status": "Test Mode Status",
"nhnkcp_easy_pay_config": "Easy Pay Config",
"nhnkcp_easy_pay_display_map": "Easy Pay Display Map",
"nhnkcp_test_map": "Test Payment Map",
@@ -49,6 +50,10 @@
"admin": {
"test_mode_badge": "Test Payment",
"test_mode_order_notice": "This order was processed as a test payment.",
"test_mode_settings_warning_plugin": "NHN KCP",
"test_mode_settings_warning_title": "Warning: NHN KCP payments are currently set to test mode.",
"test_mode_settings_warning_body": "Test payments do not create real payments. If the shop is live, switch this plugin to live payment mode before operating.",
"test_mode_settings_warning_action": "Switch to live mode",
"order_info_title": "NHN KCP Payment Info",
"order_info_description": "Displays stored KCP original approval and current payment status.",
"pay_status_approved": "Approved",
@@ -4,6 +4,7 @@
"health": "연동 상태",
"kcp_escrow_delivery": "에스크로 배송",
"kcp_status": "결제 상태",
"nhnkcp_test_mode_status": "테스트 모드 상태",
"nhnkcp_easy_pay_config": "간편결제 설정",
"nhnkcp_easy_pay_display_map": "간편결제 표시 맵",
"nhnkcp_test_map": "테스트 결제 맵",
@@ -49,6 +50,10 @@
"admin": {
"test_mode_badge": "테스트 결제",
"test_mode_order_notice": "이 주문은 테스트 결제로 처리되었습니다.",
"test_mode_settings_warning_plugin": "NHN KCP",
"test_mode_settings_warning_title": "(주의!) NHN KCP 결제 설정이 현재 테스트결제로 되어 있습니다.",
"test_mode_settings_warning_body": "테스트결제 시 실제 결제가 되지 않으므로, 쇼핑몰 운영 중이면 반드시 실결제로 설정하여 운영하세요.",
"test_mode_settings_warning_action": "실결제로 설정",
"order_info_title": "NHN KCP 결제 정보",
"order_info_description": "DB에 저장된 KCP 원승인 및 현재 결제 상태를 표시합니다.",
"pay_status_approved": "승인",
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayNhnkcp\Controllers;
use App\Helpers\ResponseHelper;
use App\Services\PluginSettingsService;
use Illuminate\Http\JsonResponse;
class AdminSettingsStatusController
{
private const PLUGIN_IDENTIFIER = 'sirsoft-pay_nhnkcp';
public function __construct(private readonly PluginSettingsService $settingsService) {}
/**
* 테스트 모드 활성 여부만 반환한다.
*
* @return JsonResponse
*/
public function testMode(): JsonResponse
{
return ResponseHelper::success('messages.success', [
'is_test_mode' => (bool) $this->settingsService->get(self::PLUGIN_IDENTIFIER, 'is_test_mode', true),
]);
}
}
@@ -13,6 +13,14 @@ class AdjustEcommercePaymentMethodsLayoutListener implements HookListenerInterfa
{
private const TARGET_LAYOUT = 'admin_ecommerce_settings';
private const TEST_MODE_DATA_SOURCE_ID = 'nhnkcp_test_mode_status';
private const TEST_MODE_NOTICE_ID = 'payment_test_mode_order_settings_notice';
private const TEST_MODE_NOTICE_ROW_ID = 'nhnkcp_test_mode_order_settings_notice';
private const TEST_MODE_CONDITION = 'nhnkcp_test_mode_status.data?.is_test_mode === true';
private const CORE_NO_PG_METHODS = ['point', 'deposit', 'free', 'dbank'];
private const NHNKCP_NO_PG_METHODS = [
@@ -44,6 +52,7 @@ class AdjustEcommercePaymentMethodsLayoutListener implements HookListenerInterfa
/**
* @param array<string, mixed> $layout
* @param int $templateId
* @return array<string, mixed>
*/
public function markEasyPayMethodsAsPgNotRequired(array $layout, int $templateId): array
@@ -52,7 +61,9 @@ class AdjustEcommercePaymentMethodsLayoutListener implements HookListenerInterfa
return $layout;
}
return $this->replaceNoPgMethodExpressions($layout);
$layout = $this->replaceNoPgMethodExpressions($layout);
return $this->ensureTestModeWarning($layout);
}
/**
@@ -100,4 +111,222 @@ class AdjustEcommercePaymentMethodsLayoutListener implements HookListenerInterfa
$expression
) ?? $expression;
}
/**
* @param array<string, mixed> $layout
* @return array<string, mixed>
*/
private function ensureTestModeWarning(array $layout): array
{
$layout = $this->ensureTestModeDataSource($layout);
return $this->insertTestModeNotice($layout);
}
/**
* @param array<string, mixed> $layout
* @return array<string, mixed>
*/
private function ensureTestModeDataSource(array $layout): array
{
$dataSources = is_array($layout['data_sources'] ?? null) ? $layout['data_sources'] : [];
foreach ($dataSources as $source) {
if (is_array($source) && ($source['id'] ?? null) === self::TEST_MODE_DATA_SOURCE_ID) {
$layout['data_sources'] = $dataSources;
return $layout;
}
}
$dataSources[] = [
'id' => self::TEST_MODE_DATA_SOURCE_ID,
'label_key' => '$t:sirsoft-pay_nhnkcp.editor.data_source.nhnkcp_test_mode_status',
'type' => 'api',
'endpoint' => '/api/plugins/sirsoft-pay_nhnkcp/admin/settings/test-mode-status',
'method' => 'GET',
'auto_fetch' => true,
'auth_required' => true,
];
$layout['data_sources'] = $dataSources;
return $layout;
}
/**
* @param array<string, mixed> $node
* @return array<string, mixed>
*/
private function insertTestModeNotice(array $node): array
{
if (($node['id'] ?? null) === 'tab_content_order_settings' && is_array($node['children'] ?? null)) {
$noticeIndex = $this->findChildIndexById($node['children'], self::TEST_MODE_NOTICE_ID);
if ($noticeIndex === null) {
$insertAt = $this->findChildIndexById($node['children'], 'payment_methods_card') ?? count($node['children']);
array_splice($node['children'], $insertAt, 0, [$this->testModeNoticeContainerNode()]);
$noticeIndex = $insertAt;
}
if (is_array($node['children'][$noticeIndex] ?? null)) {
$notice = $node['children'][$noticeIndex];
$notice['if'] = $this->mergeTestModeCondition((string) ($notice['if'] ?? ''));
$node['children'][$noticeIndex] = $this->appendTestModeNoticeRow($notice);
}
return $node;
}
foreach ($node as $key => $value) {
if (is_array($value)) {
$node[$key] = $this->insertTestModeNotice($value);
}
}
return $node;
}
/**
* @param array<int, mixed> $children
*/
private function findChildIndexById(array $children, string $id): ?int
{
foreach ($children as $index => $child) {
if (is_array($child) && ($child['id'] ?? null) === $id) {
return $index;
}
}
return null;
}
/**
* @param array<int, mixed> $children
*/
private function hasChildWithId(array $children, string $id): bool
{
return $this->findChildIndexById($children, $id) !== null;
}
private function mergeTestModeCondition(string $condition): string
{
if (str_contains($condition, self::TEST_MODE_CONDITION)) {
return $condition !== '' ? $condition : '{{' . self::TEST_MODE_CONDITION . '}}';
}
$inner = trim($condition);
if (str_starts_with($inner, '{{') && str_ends_with($inner, '}}')) {
$inner = trim(substr($inner, 2, -2));
}
if ($inner === '') {
return '{{' . self::TEST_MODE_CONDITION . '}}';
}
return '{{' . $inner . ' || ' . self::TEST_MODE_CONDITION . '}}';
}
/**
* @param array<string, mixed> $notice
* @return array<string, mixed>
*/
private function appendTestModeNoticeRow(array $notice): array
{
$children = is_array($notice['children'] ?? null) ? $notice['children'] : [];
if (! $this->hasChildWithId($children, self::TEST_MODE_NOTICE_ROW_ID)) {
$children[] = $this->testModeNoticeRowNode();
}
$notice['children'] = $children;
return $notice;
}
/**
* @return array<string, mixed>
*/
private function testModeNoticeContainerNode(): array
{
return [
'id' => self::TEST_MODE_NOTICE_ID,
'type' => 'basic',
'name' => 'Div',
'if' => '{{' . self::TEST_MODE_CONDITION . '}}',
'props' => [
'className' => 'mb-4 space-y-3 rounded-lg border border-orange-200 bg-orange-50 p-4 dark:border-orange-700 dark:bg-orange-900/20',
],
'children' => [
$this->testModeNoticeRowNode(),
],
];
}
/**
* @return array<string, mixed>
*/
private function testModeNoticeRowNode(): array
{
return [
'id' => self::TEST_MODE_NOTICE_ROW_ID,
'type' => 'basic',
'name' => 'Div',
'if' => '{{' . self::TEST_MODE_CONDITION . '}}',
'props' => [
'className' => 'flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between',
],
'children' => [
[
'type' => 'basic',
'name' => 'Div',
'props' => [
'className' => 'min-w-0',
],
'children' => [
[
'type' => 'basic',
'name' => 'Div',
'props' => [
'className' => 'text-sm font-medium text-orange-950 dark:text-orange-50',
],
'text' => '$t:sirsoft-pay_nhnkcp.admin.test_mode_settings_warning_plugin',
],
[
'type' => 'basic',
'name' => 'P',
'props' => [
'className' => 'mt-0.5 text-sm leading-6 text-orange-800 dark:text-orange-200',
],
'text' => '$t:sirsoft-pay_nhnkcp.admin.test_mode_settings_warning_body',
],
],
],
[
'type' => 'basic',
'name' => 'Button',
'props' => [
'type' => 'button',
'className' => 'btn btn-outline btn-sm w-full flex-shrink-0 border-orange-300 text-orange-900 hover:bg-orange-100 sm:w-auto dark:border-orange-600 dark:text-orange-100 dark:hover:bg-orange-900/40',
],
'actions' => [
[
'type' => 'click',
'handler' => 'navigate',
'params' => [
'path' => '/admin/plugins/sirsoft-pay_nhnkcp/settings',
],
],
],
'children' => [
[
'type' => 'basic',
'name' => 'Span',
'text' => '$t:sirsoft-pay_nhnkcp.admin.test_mode_settings_warning_action',
],
],
],
],
];
}
}
@@ -3,6 +3,7 @@
use Illuminate\Support\Facades\Route;
use Plugins\Sirsoft\PayNhnkcp\Controllers\AdminEscrowDeliveryController;
use Plugins\Sirsoft\PayNhnkcp\Controllers\AdminOrderListController;
use Plugins\Sirsoft\PayNhnkcp\Controllers\AdminSettingsStatusController;
use Plugins\Sirsoft\PayNhnkcp\Controllers\AdminTransactionController;
use Plugins\Sirsoft\PayNhnkcp\Controllers\HealthCheckController;
use Plugins\Sirsoft\PayNhnkcp\Controllers\MobileApprovalController;
@@ -53,6 +54,10 @@ Route::prefix('admin')->name('admin.')->middleware(['auth:sanctum', 'admin'])->g
],
]))->name('vbank.notify.url');
Route::get('/settings/test-mode-status', [AdminSettingsStatusController::class, 'testMode'])
->middleware('permission:admin,sirsoft-ecommerce.settings.read')
->name('settings.test-mode-status');
// 테스트 모드 주문 맵 (관리자 주문목록 배지 표시용)
Route::get('/orders/test-mode-map', [AdminOrderListController::class, 'testModeMap'])
->name('orders.test-mode-map');
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Plugins\Sirsoft\PayNhnkcp\Tests\Feature\Controllers;
use App\Services\PluginSettingsService;
use Plugins\Sirsoft\PayNhnkcp\Tests\PluginTestCase;
class AdminSettingsStatusControllerTest extends PluginTestCase
{
public function test_test_mode_status_returns_current_setting(): void
{
$settings = $this->createMock(PluginSettingsService::class);
$settings->expects($this->once())
->method('get')
->with('sirsoft-pay_nhnkcp', 'is_test_mode', true)
->willReturn(true);
$this->app->instance(PluginSettingsService::class, $settings);
$admin = $this->createAdminUser(['sirsoft-ecommerce.settings.read']);
$response = $this->actingAs($admin)
->getJson('/api/plugins/sirsoft-pay_nhnkcp/admin/settings/test-mode-status');
$response->assertOk()
->assertJsonPath('data.is_test_mode', true);
$data = $response->json('data');
$this->assertArrayNotHasKey('test_site_cd', $data);
$this->assertArrayNotHasKey('live_site_cd', $data);
}
}
@@ -58,6 +58,45 @@ class AdjustEcommercePaymentMethodsLayoutListenerTest extends TestCase
$this->assertStringNotContainsString("['point','deposit','free','dbank'].includes", $json);
}
public function test_adds_test_mode_warning_to_order_settings_tab(): void
{
$listener = new AdjustEcommercePaymentMethodsLayoutListener();
$layout = [
'layout_name' => 'admin_ecommerce_settings',
'children' => [
[
'id' => 'tab_content_order_settings',
'children' => [
['id' => 'default_pg_card'],
['id' => 'payment_methods_card'],
],
],
],
];
$result = $listener->markEasyPayMethodsAsPgNotRequired($layout, 1);
$result = $listener->markEasyPayMethodsAsPgNotRequired($result, 1);
$json = json_encode($result, JSON_UNESCAPED_SLASHES);
$this->assertIsString($json);
$this->assertStringContainsString('nhnkcp_test_mode_status', $json);
$this->assertStringContainsString('/api/plugins/sirsoft-pay_nhnkcp/admin/settings/test-mode-status', $json);
$this->assertStringContainsString('payment_test_mode_order_settings_notice', $json);
$this->assertStringContainsString('nhnkcp_test_mode_order_settings_notice', $json);
$this->assertStringNotContainsString('sirsoft-pay_nhnkcp.admin.test_mode_settings_summary_title', $json);
$this->assertStringNotContainsString('sirsoft-pay_nhnkcp.admin.test_mode_settings_summary_body', $json);
$this->assertStringContainsString('sirsoft-pay_nhnkcp.admin.test_mode_settings_warning_plugin', $json);
$this->assertStringContainsString('sirsoft-pay_nhnkcp.admin.test_mode_settings_warning_body', $json);
$this->assertStringContainsString('/admin/plugins/sirsoft-pay_nhnkcp/settings', $json);
$this->assertSame(1, substr_count($json, 'payment_test_mode_order_settings_notice'));
$this->assertSame(1, substr_count($json, 'nhnkcp_test_mode_order_settings_notice'));
$this->assertLessThan(
strpos($json, 'payment_methods_card'),
strpos($json, 'payment_test_mode_order_settings_notice')
);
}
public function test_leaves_other_layouts_unchanged(): void
{
$listener = new AdjustEcommercePaymentMethodsLayoutListener();