From efe1a81cb819d843b48475e477d4fc8669148ae5 Mon Sep 17 00:00:00 2001 From: chym1217 Date: Fri, 24 Jul 2026 19:33:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(message=5Fbizppurio):=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=ED=86=A1=20=EC=8B=A4=EC=A0=9C=20=EB=B0=9C=EC=86=A1=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=EC=9D=84=20=EB=B0=9C=EC=86=A1=20=EC=9D=B4=EB=A0=A5=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=90=20=EB=B3=84=EB=8F=84=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코어 notification_logs.body는 알림톡의 경우 SMS 대체발송용 코어 템플릿 본문이라, 실제 카카오에 발송된 내용(승인 템플릿 전체)과 다르다. 코어 공용 파일은 다른 채널도 함께 쓰므로 직접 수정하지 않고, 결과 컬럼 오버레이 방식을 확장해 알림톡 채널일 때만 실제 발송 내용을 별도로 보여준다. --- .../docs/api/dispatch-results.md | 1 + .../extensions/notification_log_result.json | 35 ++++++ .../layouts/notification_log_result.test.tsx | 106 ++++++++++++++++++ .../resources/lang/en.json | 4 +- .../resources/lang/ko.json | 4 +- .../layouts/admin/plugin_settings.json | 3 +- .../src/Services/DispatchResultService.php | 4 + .../Services/DispatchResultServiceTest.php | 14 +++ 8 files changed, 167 insertions(+), 4 deletions(-) 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']); + } }