fix(message_bizppurio): 알림톡 템플릿 검색 결과 없음이 오류로 처리되던 문제 수정

카카오 kapi 결과코드 508("요청한 데이터가 없음")은 검색 결과 0건을 나타내는
정상 응답인데, 예외로 전파해 화면에 아무 반응 없이 이전 목록만 남는 문제가
있었다. 목록 조회에서만 508을 빈 결과로 흡수하도록 수정.
This commit is contained in:
chym1217
2026-07-28 12:40:17 +09:00
parent e8784854aa
commit 8521428636
5 changed files with 64 additions and 4 deletions
@@ -20,7 +20,7 @@
- 관리자 "알림 발송 이력" 화면에서 문자·알림톡의 발송 결과(성공/실패·사유·지갑 잔액 부족·SMS 대체발송 여부)를 각 발송 행에서 바로 확인할 수 있습니다. 문자·알림톡 탭에서만 결과가 표시되며, 행을 펼치면 상세 결과를 볼 수 있습니다. 대체발송을 실제로 요청한 건에서만 결과가 표시되어, 사용하지 않은 건에 잘못 표시되지 않습니다. 알림톡은 카카오 승인 템플릿의 실제 발송 내용을 별도로 함께 보여줘, 위에 표시되는 알림 기본 본문과 헷갈리지 않도록 합니다.
- 지갑 잔액 부족으로 발송이 실패하면 관리자에게 알립니다. 대량 실패 시 알림이 반복 발송되지 않도록 일정 시간 간격으로 한 번만 안내합니다.
- 설정 페이지의 알림톡 템플릿 탭에서 카카오 알림톡 템플릿의 목록·상태·내용을 확인할 수 있습니다. 템플릿 등록·수정·검수·상태변경은 비즈뿌리오 콘솔에서 하며, 이 화면은 목록·상태·내용 조회와 알림 연결을 담당합니다. 콘솔에서 등록한 템플릿도 이 목록에 그대로 표시되고 알림에 연결할 수 있습니다.
- 템플릿 목록을 상태(발송가능·검수중·반려·미검수·중지 등) 배지와 함께 보여주고, 상태·이름으로 필터·검색할 수 있습니다. 발송가능(승인) 상태의 템플릿만 알림에 연결해 발송할 수 있습니다.
- 템플릿 목록을 상태(발송가능·검수중·반려·미검수·중지 등) 배지와 함께 보여주고, 상태·이름으로 필터·검색할 수 있습니다. 검색 결과가 없으면 오류 없이 "등록된 알림톡 템플릿이 없습니다"로 표시됩니다. 발송가능(승인) 상태의 템플릿만 알림에 연결해 발송할 수 있습니다.
- 각 템플릿의 [내용] 버튼으로 템플릿명·코드·카테고리·유형·본문·버튼을 담은 상세 내용을 확인할 수 있습니다.
- 목록 상단에 배지 의미(발송가능·검수중·미검수·반려)와 함께, 템플릿 등록·수정·검수·상태변경은 비즈뿌리오 콘솔에서 한다는 안내와 콘솔 바로가기를 제공합니다.
- 환경설정 탭에 카카오 알림톡·문자(SMS/LMS) 사용 전 준비 사항(비즈뿌리오 가입·API 승인·발신프로필 등록·발신번호 사전 등록)을 안내하는 박스를 추가했습니다.
@@ -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<string, mixed> $filters status(templateStatus)·keyword·page·count
* @return array{templates: array<int, array<string, mixed>>, pagination: array<string, int>}
*
* @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'] ?? []);
@@ -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
*
@@ -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();
@@ -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