diff --git a/plugins/_bundled/sirsoft-message_bizppurio/docs/api/dispatch-results.md b/plugins/_bundled/sirsoft-message_bizppurio/docs/api/dispatch-results.md index 530e1c45..81b4f96b 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/docs/api/dispatch-results.md +++ b/plugins/_bundled/sirsoft-message_bizppurio/docs/api/dispatch-results.md @@ -102,6 +102,7 @@ HTTP/1.1 200 | is_low_balance | boolean | 잔액 부족(9070 문자 / 7436 알림톡) 여부 | | fallback_status | string\|null | SMS 대체발송 결과 (webhook TELRES). 없으면 null | | channel | string\|null | 발송 채널: `sms` / `lms` / `alimtalk` | +| content | string\|null | 실제 비즈뿌리오에 발송한 본문. 알림톡은 코어 `notification_logs.body`(대체발송용 코어 템플릿 값)와 달리 카카오 승인 템플릿의 실제 내용이므로, 화면은 이 값을 채널별로 구분해 별도 노출한다 | **응답 예시** diff --git a/plugins/_bundled/sirsoft-message_bizppurio/resources/extensions/notification_log_result.json b/plugins/_bundled/sirsoft-message_bizppurio/resources/extensions/notification_log_result.json index 4e294559..fd8e707c 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/resources/extensions/notification_log_result.json +++ b/plugins/_bundled/sirsoft-message_bizppurio/resources/extensions/notification_log_result.json @@ -95,6 +95,41 @@ "text": "$t:sirsoft-message_bizppurio.dispatch_result.fallback|status={{(dispatchResults?.data?.results ?? {})[row.id]?.fallback_status}}" } ] + }, + { + "comment": "알림톡 실제 발송 내용 — 코어 '본문'(notification_logs.body)은 SMS 대체발송용 코어 템플릿 값이라 실제 카카오 발송 내용(승인 템플릿 전체)과 다르다. row.channel==='alimtalk'이고 실제 발송 내용이 있을 때만 별도 노출해 혼동을 방지한다.", + "type": "basic", + "name": "Div", + "if": "{{row.channel === 'alimtalk' && !!(dispatchResults?.data?.results ?? {})[row.id]?.content}}", + "props": { + "className": "pt-2 border-t border-gray-200 dark:border-gray-700" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "props": { + "className": "form-label" + }, + "text": "$t:sirsoft-message_bizppurio.dispatch_result.sent_content_label" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-sm text-gray-700 dark:text-gray-200 whitespace-pre-line admin-card" + }, + "text": "{{(dispatchResults?.data?.results ?? {})[row.id]?.content}}" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-xs text-gray-400 dark:text-gray-500 mt-1" + }, + "text": "$t:sirsoft-message_bizppurio.dispatch_result.sent_content_hint" + } + ] } ] } diff --git a/plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/notification_log_result.test.tsx b/plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/notification_log_result.test.tsx index 6616bbb8..fb0df8cb 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/notification_log_result.test.tsx +++ b/plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/notification_log_result.test.tsx @@ -36,6 +36,14 @@ function getResultCellChildren(): any[] { return column.cellChildren; } +/** overlay 에서 행 토글(expandChildren) 에 append 된 결과 블록을 추출한다. */ +function getResultExpandChildren(): any[] { + const inj = (overlay as any).injections.find( + (i: any) => i.target_id === 'notification_log_datagrid' && i.position === 'inject_props', + ); + return inj.props.expandChildren._append; +} + /** 결과 컬럼 cellChildren 을 iteration row 컨텍스트로 렌더하는 프로브 레이아웃. */ function buildProbe() { return { @@ -57,6 +65,27 @@ function buildProbe() { }; } +/** 행 토글(expandChildren) 결과 블록을 iteration row 컨텍스트로 렌더하는 프로브 레이아웃. */ +function buildExpandProbe() { + return { + version: '1.0.0', + layout_name: 'test/a2-dispatch-result-expand', + data_sources: [ + { id: 'notificationLogs', type: 'api', endpoint: '/api/test/logs', method: 'GET', auto_fetch: true }, + { id: 'dispatchResults', type: 'api', endpoint: '/api/test/results', method: 'POST', auto_fetch: true }, + ], + components: [ + { + type: 'basic', + name: 'Div', + iteration: { source: '{{notificationLogs.data?.data ?? []}}', item_var: 'row' }, + props: { 'data-testid': 'result-expand' }, + children: getResultExpandChildren(), + }, + ], + }; +} + let registry: MockComponentRegistry; beforeEach(() => { registry = createMockComponentRegistryWithBasics(); @@ -231,3 +260,80 @@ describe('A-2 결과 컬럼 주입 — 렌더', () => { utils.cleanup(); }); }); + +describe('A-2 행 토글 — 알림톡 실제 발송 내용', () => { + it('알림톡 채널 + 실제 발송 내용이 있으면 코어 "본문"과 별도로 렌더한다', async () => { + const utils = createLayoutTest(buildExpandProbe(), { componentRegistry: registry as any, locale: 'ko' }); + utils.mockApi('notificationLogs', { response: logs([7], 'alimtalk') }); + utils.mockApi('dispatchResults', { + response: { + data: { + results: { + 7: { + status: 'success', + status_label: '성공', + result_label: '성공 (7000)', + is_low_balance: false, + fallback_status: null, + channel: 'alimtalk', + content: '[그누보드7] 회원가입을 환영합니다\n\n김으네님, 가입이 완료되었습니다.', + }, + }, + }, + }, + }); + await utils.render(); + expect(screen.getByText(/회원가입을 환영합니다/)).toBeInTheDocument(); + utils.cleanup(); + }); + + it('sms 채널은 실제 발송 내용 값이 있어도 렌더하지 않는다(코어 본문과 동일하므로 중복 표시 불필요)', async () => { + const utils = createLayoutTest(buildExpandProbe(), { componentRegistry: registry as any, locale: 'ko' }); + utils.mockApi('notificationLogs', { response: logs([8], 'sms') }); + utils.mockApi('dispatchResults', { + response: { + data: { + results: { + 8: { + status: 'success', + status_label: '성공', + result_label: '성공 (4100)', + is_low_balance: false, + fallback_status: null, + channel: 'sms', + content: '문자 본문', + }, + }, + }, + }, + }); + await utils.render(); + expect(screen.queryByText('문자 본문')).not.toBeInTheDocument(); + utils.cleanup(); + }); + + it('알림톡 채널이지만 실제 발송 내용이 없으면(과거 이력 등) 렌더하지 않는다', async () => { + const utils = createLayoutTest(buildExpandProbe(), { componentRegistry: registry as any, locale: 'ko' }); + utils.mockApi('notificationLogs', { response: logs([9], 'alimtalk') }); + utils.mockApi('dispatchResults', { + response: { + data: { + results: { + 9: { + status: 'success', + status_label: '성공', + result_label: '성공 (7000)', + is_low_balance: false, + fallback_status: null, + channel: 'alimtalk', + content: null, + }, + }, + }, + }, + }); + await utils.render(); + expect(screen.queryByText('sirsoft-message_bizppurio.dispatch_result.sent_content_label')).not.toBeInTheDocument(); + utils.cleanup(); + }); +}); diff --git a/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/en.json b/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/en.json index 70751cd3..a28c9e3a 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/en.json +++ b/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/en.json @@ -113,7 +113,9 @@ "low_balance": "Low balance", "fallback": "SMS fallback {status}", "detail_title": "SMS/Alimtalk delivery result", - "channel_label": "Channel: {channel}" + "channel_label": "Channel: {channel}", + "sent_content_label": "Actual sent content", + "sent_content_hint": "Alimtalk messages are sent using the actual content of the approved Kakao template, which may differ from the \"Body\" above." }, "editor": { "data_source": { diff --git a/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/ko.json b/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/ko.json index 610edbc9..43e5b8f6 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/ko.json +++ b/plugins/_bundled/sirsoft-message_bizppurio/resources/lang/ko.json @@ -113,7 +113,9 @@ "low_balance": "잔액 부족", "fallback": "SMS 대체발송 {status}", "detail_title": "문자·알림톡 발송 결과", - "channel_label": "발송 채널: {channel}" + "channel_label": "발송 채널: {channel}", + "sent_content_label": "실제 발송 내용", + "sent_content_hint": "알림톡은 카카오 승인 템플릿의 실제 내용으로 발송되며, 위 \"본문\"과 다를 수 있습니다." }, "editor": { "data_source": { 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 377df1bc..744dfaf0 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 @@ -1467,7 +1467,7 @@ "comment": "조회 전용 — [내용] 버튼 하나. 상세 조회 후 내용 모달 열기(관리 액션 없음).", "type": "basic", "name": "Button", - "props": { "type": "button", "className": "btn btn-secondary btn-xs flex-center gap-1" }, + "props": { "type": "button", "className": "btn btn-secondary btn-xs flex-center gap-1 whitespace-nowrap" }, "actions": [ { "type": "click", @@ -1482,7 +1482,6 @@ } ], "children": [ - { "type": "basic", "name": "Icon", "props": { "name": "eye", "size": "sm" } }, { "type": "basic", "name": "Span", "text": "$t:sirsoft-message_bizppurio.templates.actions.detail" } ] } diff --git a/plugins/_bundled/sirsoft-message_bizppurio/src/Services/DispatchResultService.php b/plugins/_bundled/sirsoft-message_bizppurio/src/Services/DispatchResultService.php index 37fe546c..441379b9 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/src/Services/DispatchResultService.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/src/Services/DispatchResultService.php @@ -94,6 +94,10 @@ class DispatchResultService 'channel' => $dispatch->channel?->value, // 발송 시점 검수 모드 스냅샷. null=컬럼 신설 이전 이력(검수 여부 판단 불가 → 화면 미표시). 'is_test_mode' => $dispatch->is_test_mode, + // 실제 비즈뿌리오에 발송한 본문. 알림톡은 코어 notification_logs.body(대체발송용 코어 + // 템플릿 본문)와 다른 값(카카오 승인 템플릿 실제 내용)이라, 코어 "본문" 표시만으로는 + // 실제 발송 내용을 알 수 없다 — 화면이 채널별로 구분해 별도 노출할 수 있도록 포함. + 'content' => $dispatch->content, ]; } } diff --git a/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/DispatchResultServiceTest.php b/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/DispatchResultServiceTest.php index 5f1bbb5c..c6608454 100644 --- a/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/DispatchResultServiceTest.php +++ b/plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Services/DispatchResultServiceTest.php @@ -128,4 +128,18 @@ class DispatchResultServiceTest extends PluginTestCase $this->assertSame('success', $results[60]['status']); $this->assertTrue($results[61]['is_low_balance']); } + + /** + * 실제 발송 본문(dispatch.content)이 결과에 포함된다 — 알림톡은 코어 + * notification_logs.body(대체발송용 코어 템플릿 값)와 실제 카카오 발송 내용이 달라, + * 화면이 채널별로 구분해 실제 발송 내용을 보여줄 수 있도록 결과에 담아야 한다. + */ + public function test_실제_발송_본문이_결과에_포함된다(): void + { + $this->linkedDispatch(80, ['content' => '[그누보드7] 회원가입을 환영합니다\n\n김으네님, 가입이 완료되었습니다.']); + + $results = $this->service->resultsForLogIds([80]); + + $this->assertSame('[그누보드7] 회원가입을 환영합니다\n\n김으네님, 가입이 완료되었습니다.', $results[80]['content']); + } }