diff --git a/plugins/_bundled/sirsoft-message_bizppurio/CHANGELOG.md b/plugins/_bundled/sirsoft-message_bizppurio/CHANGELOG.md index 1997c91c..020423cb 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-message_bizppurio/CHANGELOG.md @@ -20,7 +20,7 @@ - 관리자 "알림 발송 이력" 화면에서 문자·알림톡의 발송 결과(성공/실패·사유·지갑 잔액 부족·SMS 대체발송 여부)를 각 발송 행에서 바로 확인할 수 있습니다. 문자·알림톡 탭에서만 결과가 표시되며, 행을 펼치면 상세 결과를 볼 수 있습니다. 대체발송을 실제로 요청한 건에서만 결과가 표시되어, 사용하지 않은 건에 잘못 표시되지 않습니다. 알림톡은 카카오 승인 템플릿의 실제 발송 내용을 별도로 함께 보여줘, 위에 표시되는 알림 기본 본문과 헷갈리지 않도록 합니다. - 지갑 잔액 부족으로 발송이 실패하면 관리자에게 알립니다. 대량 실패 시 알림이 반복 발송되지 않도록 일정 시간 간격으로 한 번만 안내합니다. - 설정 페이지의 알림톡 템플릿 탭에서 카카오 알림톡 템플릿의 목록·상태·내용을 확인할 수 있습니다. 템플릿 등록·수정·검수·상태변경은 비즈뿌리오 콘솔에서 하며, 이 화면은 목록·상태·내용 조회와 알림 연결을 담당합니다. 콘솔에서 등록한 템플릿도 이 목록에 그대로 표시되고 알림에 연결할 수 있습니다. -- 템플릿 목록을 상태(발송가능·검수중·반려·미검수·중지 등) 배지와 함께 보여주고, 상태·이름으로 필터·검색할 수 있습니다. 발송가능(승인) 상태의 템플릿만 알림에 연결해 발송할 수 있습니다. +- 템플릿 목록을 상태(발송가능·검수중·반려·미검수·중지 등) 배지와 함께 보여주고, 상태·이름으로 필터·검색할 수 있습니다. 검색 결과가 없으면 오류 없이 "등록된 알림톡 템플릿이 없습니다"로 표시됩니다. 발송가능(승인) 상태의 템플릿만 알림에 연결해 발송할 수 있습니다. - 각 템플릿의 [내용] 버튼으로 템플릿명·코드·카테고리·유형·본문·버튼을 담은 상세 내용을 확인할 수 있습니다. - 목록 상단에 배지 의미(발송가능·검수중·미검수·반려)와 함께, 템플릿 등록·수정·검수·상태변경은 비즈뿌리오 콘솔에서 한다는 안내와 콘솔 바로가기를 제공합니다. - 환경설정 탭에 카카오 알림톡·문자(SMS/LMS) 사용 전 준비 사항(비즈뿌리오 가입·API 승인·발신프로필 등록·발신번호 사전 등록)을 안내하는 박스를 추가했습니다. diff --git a/plugins/_bundled/sirsoft-message_bizppurio/src/Services/AlimtalkTemplateService.php b/plugins/_bundled/sirsoft-message_bizppurio/src/Services/AlimtalkTemplateService.php index 2321a85f..1280b8b5 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/src/Services/AlimtalkTemplateService.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/src/Services/AlimtalkTemplateService.php @@ -29,6 +29,9 @@ class AlimtalkTemplateService /** 목록 조회 기본 페이지 크기 */ private const DEFAULT_COUNT = 20; + /** kapi 결과코드: 요청한 데이터가 없음(검색 결과 0건 포함, 13.응답코드정의.md) */ + private const NOT_FOUND_CODE = '508'; + /** * serviceStatus → 상태 배지 매핑. * @@ -63,7 +66,7 @@ class AlimtalkTemplateService * @param array $filters status(templateStatus)·keyword·page·count * @return array{templates: array>, pagination: array} * - * @throws BizppurioApiException 자격증명 미설정·조회 실패 시 + * @throws BizppurioApiException 자격증명 미설정·조회 실패 시(결과코드 508 제외) */ public function list(array $filters = []): array { @@ -82,7 +85,25 @@ class AlimtalkTemplateService $response = $this->kakao->getTemplateList($this->senderKey(), $params); - $this->assertSuccess($response); + try { + $this->assertSuccess($response); + } catch (BizppurioApiException $e) { + // 508 = "요청한 데이터가 없음"(검색 결과 0건 포함). 카카오는 이 경우를 목록 + // 조회 실패로 응답하지만, 실제로는 정상적인 빈 결과이므로 예외로 취급하지 않는다. + if ($e->getResultCode() === self::NOT_FOUND_CODE) { + return [ + 'templates' => [], + 'pagination' => [ + 'total' => 0, + 'total_page' => 1, + 'current_page' => $params['page'], + 'per_page' => $params['count'], + ], + ]; + } + + throw $e; + } $rows = (array) ($response['data']['list'] ?? $response['data'] ?? []); diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/AlimtalkTemplate/AlimtalkTemplateControllerTest.php b/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/AlimtalkTemplate/AlimtalkTemplateControllerTest.php index 54825362..7548335e 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/AlimtalkTemplate/AlimtalkTemplateControllerTest.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/AlimtalkTemplate/AlimtalkTemplateControllerTest.php @@ -191,6 +191,26 @@ class AlimtalkTemplateControllerTest extends PluginTestCase $response->assertJsonPath('errors.kakao_message', '발신프로필을 찾을 수 없습니다.'); } + /** + * @scenario auth=view,kapi_result=not_found + * + * @effects list_treats_kapi_508_as_empty_result_not_error + */ + public function test_kapi_508은_에러가_아니라_빈_목록으로_응답한다(): void + { + $this->mockService()->shouldReceive('list')->once()->andReturn([ + 'templates' => [], + 'pagination' => ['total' => 0, 'total_page' => 1, 'current_page' => 1, 'per_page' => 20], + ]); + + $response = $this->withHeaders($this->authHeaders(['sirsoft-message_bizppurio.messaging.view'])) + ->getJson(self::BASE.'?keyword=대글'); + + $response->assertStatus(200); + $response->assertJsonPath('data.templates', []); + $response->assertJsonPath('data.pagination.total', 0); + } + /** * @scenario auth=manage,action=clear_cache * diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/AlimtalkTemplateServiceTest.php b/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/AlimtalkTemplateServiceTest.php index 036de8ca..4c2093de 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/AlimtalkTemplateServiceTest.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/AlimtalkTemplateServiceTest.php @@ -150,6 +150,21 @@ class AlimtalkTemplateServiceTest extends PluginTestCase } } + public function test_목록조회는_kapi_508을_빈_목록으로_처리한다(): void + { + // 카카오 결과코드 508 = "요청한 데이터가 없음"(13.응답코드정의.md). 목록 검색에서 + // 매칭 결과가 0건일 때 카카오가 이 코드로 응답하므로, 진짜 에러가 아니라 빈 목록으로 + // 취급해야 한다(PO 실측: "댓글"은 200 정상 필터링, "대글"처럼 매칭 없는 키워드만 508). + Http::fake([ + 'kapi.ppurio.com/*' => Http::response(['code' => '508', 'message' => '요청한 데이타가 없습니다.'], 200), + ]); + + $result = $this->service()->list(['keyword' => '대글']); + + $this->assertSame([], $result['templates']); + $this->assertSame(0, $result['pagination']['total']); + } + protected function tearDown(): void { Mockery::close(); diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/alimtalk-templates.yaml b/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/alimtalk-templates.yaml index fda48040..48bb8ddb 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/alimtalk-templates.yaml +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/scenarios/alimtalk-templates.yaml @@ -27,11 +27,13 @@ description: | axes: auth: [view, forbidden, guest] - kapi_result: [ok, fail] + kapi_result: [ok, fail, not_found] exclusions: - { auth: guest, kapi_result: fail, reason: "비인증은 결과 무관 401 — kapi 도달 전 차단" } - { auth: forbidden, kapi_result: fail, reason: "권한 없으면 403 — kapi 도달 전 차단" } + - { auth: guest, kapi_result: not_found, reason: "비인증은 결과 무관 401 — kapi 도달 전 차단" } + - { auth: forbidden, kapi_result: not_found, reason: "권한 없으면 403 — kapi 도달 전 차단" } effects: # 탭 네비게이션 (UI) @@ -77,6 +79,8 @@ effects: # kapi 실패 전파 (백엔드) - kapi_failure_is_surfaced_as_422_with_result_code - sender_key_missing_throws + # kapi 508(요청한 데이터가 없음=검색 결과 0건) 은 에러가 아니라 빈 목록으로 처리 (백엔드) + - list_treats_kapi_508_as_empty_result_not_error # 다국어 - all_referenced_i18n_keys_exist_in_ko_and_en