From fc00e30fc483c684269f8af69aad757480898702 Mon Sep 17 00:00:00 2001 From: chym1217 Date: Mon, 20 Jul 2026 15:30:06 +0900 Subject: [PATCH] =?UTF-8?q?fix(message=5Fbizppurio):=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=ED=86=A1=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=82=AC=EC=9C=A0=20=EB=85=B8=EC=B6=9C=20+=20=ED=83=AD=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=C2=B7=EB=AA=A9=EB=A1=9D=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카카오 kapi HTTP 실패 시 응답 body의 message/code를 예외에 실어 운영자에게 실제 사유(IP 차단·반려 등)를 노출 (기존 고정 문구로 뭉개지던 문제 해소) - 설정 탭 전환을 replaceUrl→navigate(replace)로 교체 (URL만 바뀌고 화면이 전환되지 않던 문제 해소) - 템플릿 목록 조회 실패 시 카카오 사유를 빨간 배너로 표시 - report_url 데이터소스 if 조건으로 templates 탭 불필요 재조회 억제 --- .../layouts/plugin_settings.test.tsx | 56 ++++++++++++++++++ .../resources/lang/en.json | 3 + .../resources/lang/ko.json | 3 + .../layouts/admin/plugin_settings.json | 59 ++++++++++++++++--- .../src/Services/BizppurioKakaoApiClient.php | 36 ++++++++--- .../Services/BizppurioKakaoApiClientTest.php | 36 +++++++++++ .../tests/scenarios/plugin-settings.yaml | 3 + 7 files changed, 179 insertions(+), 17 deletions(-) diff --git a/plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/plugin_settings.test.tsx b/plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/plugin_settings.test.tsx index 7f63768d..b233e1bb 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/plugin_settings.test.tsx +++ b/plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/plugin_settings.test.tsx @@ -188,3 +188,59 @@ describe('plugin_settings.json — i18n 키 정합', () => { } }); }); + +describe('plugin_settings.json — 탭 전환', () => { + // 탭 버튼은 query.tab 을 바꾸며 화면(if 조건부 패널)을 다시 그려야 하므로 + // replaceUrl(URL만 변경, if 재평가 없음) 이 아니라 navigate 를 써야 한다. + const tabButtonHandlers = (id: string): string[] => { + const btn = findById(layout, id); + const handlers: string[] = []; + const walk = (node: unknown): void => { + if (!node || typeof node !== 'object') return; + const n = node as Record; + if (typeof n.handler === 'string') handlers.push(n.handler); + for (const v of Object.values(n)) { + if (Array.isArray(v)) v.forEach(walk); + else if (v && typeof v === 'object') walk(v); + } + }; + walk((btn as { actions?: unknown })?.actions); + return handlers; + }; + + it('환경설정 탭 버튼은 navigate 로 화면을 전환한다 (replaceUrl 금지)', () => { + const handlers = tabButtonHandlers('tab_connection'); + expect(handlers).toContain('navigate'); + expect(handlers).not.toContain('replaceUrl'); + }); + + it('알림톡 템플릿 탭 버튼은 navigate 로 화면을 전환한다 (replaceUrl 금지)', () => { + const handlers = tabButtonHandlers('tab_templates'); + expect(handlers).toContain('navigate'); + expect(handlers).not.toContain('replaceUrl'); + }); +}); + +describe('plugin_settings.json — 목록 조회 실패 표시', () => { + it('alimtalk_templates 데이터소스가 실패 사유를 _local.templateListError 에 담는다', () => { + const sources = (root as { data_sources?: AnyNode[] }).data_sources ?? []; + const ds = sources.find((s) => s.id === 'alimtalk_templates') as + | Record + | undefined; + expect(ds).toBeTruthy(); + const onError = JSON.stringify(ds?.onError ?? {}); + // 카카오가 준 사유(kakao_message)를 우선 노출, 없으면 error.message 폴백 + expect(onError).toContain('templateListError'); + expect(onError).toContain('kakao_message'); + }); + + it('목록 오류 배너가 templateListError 조건으로 존재한다', () => { + const banner = findById(layout, 'templates_list_error') as + | Record + | undefined; + expect(banner).toBeTruthy(); + expect(banner?.if).toBe('{{_local.templateListError}}'); + // 사유 본문을 그대로 렌더한다 + expect(JSON.stringify(banner)).toContain('{{_local.templateListError}}'); + }); +}); diff --git a/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/en.json b/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/en.json index 25622ee0..a1d65e73 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/en.json +++ b/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/en.json @@ -95,6 +95,9 @@ "sender_key_missing": "Alimtalk sender profile key", "note": "Enter the items above on the Settings tab to browse and register templates. Actual delivery requires turning off test mode and using templates approved by Kakao." }, + "list_error": { + "title": "Failed to load the template list" + }, "list": { "new": "New Template", "refresh": "Refresh", diff --git a/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/ko.json b/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/ko.json index e38085a6..42137dc1 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/ko.json +++ b/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/ko.json @@ -95,6 +95,9 @@ "sender_key_missing": "알림톡 발신프로필 키", "note": "위 항목을 환경설정 탭에서 입력하면 템플릿 조회·등록이 가능합니다. 실제 발송은 검수 모드를 끄고 운영으로 전환한 뒤 카카오 승인을 받은 템플릿만 사용할 수 있습니다." }, + "list_error": { + "title": "템플릿 목록을 불러오지 못했습니다" + }, "list": { "new": "새 템플릿", "refresh": "새로고침", diff --git a/plugins/_bundled/sirsoft-message_bizppurio/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-message_bizppurio/resources/layouts/admin/plugin_settings.json index 317f819a..52175757 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-message_bizppurio/resources/layouts/admin/plugin_settings.json @@ -34,7 +34,8 @@ "method": "GET", "auto_fetch": true, "auth_required": true, - "initLocal": "reportInfo" + "initLocal": "reportInfo", + "if": "{{(query.tab ?? 'connection') === 'connection'}}" }, { "id": "alimtalk_templates", @@ -46,6 +47,14 @@ "params": { "status": "{{_local.templateStatus ?? ''}}", "keyword": "{{_local.templateKeyword ?? ''}}" + }, + "onSuccess": { + "handler": "setState", + "params": { "target": "local", "templateListError": null } + }, + "onError": { + "handler": "setState", + "params": { "target": "local", "templateListError": "{{error.errors?.kakao_message ?? error.message}}" } } }, { @@ -95,8 +104,8 @@ "actions": [ { "type": "click", - "handler": "replaceUrl", - "params": { "query": { "tab": "connection" }, "mergeQuery": true } + "handler": "navigate", + "params": { "path": "/admin/plugins/{{route.identifier}}/settings", "query": { "tab": "connection" }, "mergeQuery": true, "replace": true } } ], "children": [ @@ -116,14 +125,14 @@ "type": "click", "handler": "sequence", "actions": [ - { - "handler": "replaceUrl", - "params": { "query": { "tab": "templates" }, "mergeQuery": true } - }, { "handler": "setState", "params": { "target": "local", "templateView": "list" } }, + { + "handler": "navigate", + "params": { "path": "/admin/plugins/{{route.identifier}}/settings", "query": { "tab": "templates" }, "mergeQuery": true, "replace": true } + }, { "handler": "refetchDataSource", "params": { "dataSourceId": "alimtalk_templates" } @@ -150,7 +159,7 @@ "type": "basic", "name": "Div", "props": { - "className": "alert-info" + "className": "mt-6 alert-info" }, "children": [ { @@ -919,6 +928,38 @@ "if": "{{(query.tab ?? 'connection') === 'templates'}}", "props": { "className": "mt-6" }, "children": [ + { + "id": "templates_list_error", + "type": "basic", + "name": "Div", + "if": "{{_local.templateListError}}", + "props": { "className": "mb-4 rounded-lg border border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-950 p-4 flex gap-2 items-start" }, + "children": [ + { + "type": "basic", + "name": "Icon", + "props": { "name": "circle-exclamation", "size": "md", "className": "text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" } + }, + { + "type": "basic", + "name": "Div", + "children": [ + { + "type": "basic", + "name": "P", + "props": { "className": "text-sm font-medium text-red-800 dark:text-red-200" }, + "text": "$t:sirsoft-message_bizppurio.templates.list_error.title" + }, + { + "type": "basic", + "name": "P", + "props": { "className": "text-xs text-red-700 dark:text-red-300 mt-1 leading-relaxed" }, + "text": "{{_local.templateListError}}" + } + ] + } + ] + }, { "id": "templates_readiness", "type": "basic", @@ -981,7 +1022,7 @@ "name": "Button", "props": { "type": "button", "className": "btn btn-secondary btn-sm whitespace-nowrap" }, "actions": [ - { "type": "click", "handler": "replaceUrl", "params": { "query": { "tab": "connection" }, "mergeQuery": true } } + { "type": "click", "handler": "navigate", "params": { "path": "/admin/plugins/{{route.identifier}}/settings", "query": { "tab": "connection" }, "mergeQuery": true, "replace": true } } ], "children": [ { "type": "basic", "name": "Span", "text": "$t:sirsoft-message_bizppurio.templates.readiness.go_settings" } diff --git a/plugins/_bundled/sirsoft-message_bizppurio/src/Services/BizppurioKakaoApiClient.php b/plugins/_bundled/sirsoft-message_bizppurio/src/Services/BizppurioKakaoApiClient.php index 7edad0f8..53cf7dc8 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/src/Services/BizppurioKakaoApiClient.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/src/Services/BizppurioKakaoApiClient.php @@ -6,6 +6,7 @@ namespace Plugins\Sirsoft\MessageBizppurio\Services; use App\Services\PluginSettingsService; use Illuminate\Http\Client\PendingRequest; +use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; use Plugins\Sirsoft\MessageBizppurio\Exceptions\BizppurioApiException; @@ -111,10 +112,7 @@ class BizppurioKakaoApiClient ]); if ($response->failed()) { - throw new BizppurioApiException( - __('sirsoft-message_bizppurio::messages.error.kakao_request_failed'), - httpStatus: $response->status(), - ); + throw $this->failureException($response); } $result = $response->json(); @@ -178,10 +176,7 @@ class BizppurioKakaoApiClient ])); if ($response->failed()) { - throw new BizppurioApiException( - __('sirsoft-message_bizppurio::messages.error.kakao_request_failed'), - httpStatus: $response->status(), - ); + throw $this->failureException($response); } $result = $response->json(); @@ -196,6 +191,31 @@ class BizppurioKakaoApiClient return $result; } + /** + * HTTP 실패 응답에서 카카오가 준 사유(message)와 결과코드(code)를 추출해 예외를 만듭니다. + * + * 카카오 관리 API 는 실패 시에도 `{code, message}` 봉투를 반환하므로, body 를 파싱해 + * 운영자에게 실제 사유(접근 불가 IP·반려 사유·계정 오류 등)를 노출한다. body 파싱이 + * 불가하거나 message 가 비어 있으면 일반 실패 문구로 폴백한다. + * + * @param Response $response 실패한 HTTP 응답 + * @return BizppurioApiException 카카오 사유·결과코드·HTTP 상태가 담긴 예외 + */ + private function failureException(Response $response): BizppurioApiException + { + $body = $response->json(); + $message = is_array($body) ? (string) ($body['message'] ?? '') : ''; + $code = is_array($body) ? (string) ($body['code'] ?? '') : ''; + + return new BizppurioApiException( + $message !== '' + ? $message + : __('sirsoft-message_bizppurio::messages.error.kakao_request_failed'), + resultCode: $code !== '' ? $code : null, + httpStatus: $response->status(), + ); + } + /** * 환경설정에서 bizId(=bizppurio_id)와 apiKey 를 조회합니다. * diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/BizppurioKakaoApiClientTest.php b/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/BizppurioKakaoApiClientTest.php index 347fa5c0..795b27ad 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/BizppurioKakaoApiClientTest.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/BizppurioKakaoApiClientTest.php @@ -134,6 +134,42 @@ class BizppurioKakaoApiClientTest extends PluginTestCase $this->client()->getSenderProfiles(); } + public function test_http_실패시_카카오_message와_code를_예외에_싣는다(): void + { + Http::fake([ + 'kapi.ppurio.com/*' => Http::response( + ['code' => '403', 'message' => '접근할 수 없는 IP 입니다. (114.207.113.206)'], + 403, + ), + ]); + + try { + $this->client()->getSenderProfiles(); + $this->fail('BizppurioApiException 이 발생해야 한다.'); + } catch (BizppurioApiException $e) { + $this->assertSame('접근할 수 없는 IP 입니다. (114.207.113.206)', $e->getMessage()); + $this->assertSame('403', $e->getResultCode()); + $this->assertSame(403, $e->getHttpStatus()); + } + } + + public function test_http_실패_body에_message가_없으면_일반문구로_폴백한다(): void + { + Http::fake(['kapi.ppurio.com/*' => Http::response([], 500)]); + + try { + $this->client()->getSenderProfiles(); + $this->fail('BizppurioApiException 이 발생해야 한다.'); + } catch (BizppurioApiException $e) { + $this->assertSame( + __('sirsoft-message_bizppurio::messages.error.kakao_request_failed'), + $e->getMessage(), + ); + $this->assertNull($e->getResultCode()); + $this->assertSame(500, $e->getHttpStatus()); + } + } + protected function tearDown(): void { Mockery::close(); diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/plugin-settings.yaml b/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/plugin-settings.yaml index 02ae94e0..d1fc3093 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/plugin-settings.yaml +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/plugin-settings.yaml @@ -62,6 +62,9 @@ effects: - save_button_disabled_without_changes - save_puts_to_core_plugin_settings_api - only_registered_handlers_used + # 탭 전환 (환경설정 ↔ 알림톡 템플릿) — query.tab 변경 시 if 조건부 패널 재평가 필요 + - tab_buttons_use_navigate_not_replaceurl # replaceUrl 은 URL만 바꿔 if 재평가 없음 → 화면 미전환 회귀 + - alimtalk_template_list_error_shown_with_kakao_reason # 목록 조회 실패 시 카카오 사유(kakao_message)를 배너로 노출 # 다국어 - all_referenced_i18n_keys_exist_in_ko_and_en # 설치·활성화 (백엔드)