feat(sirsoft-board,sirsoft-ecommerce,message_bizppurio): 게시판·이커머스 알림 설정에 카카오 알림톡 연동 UI 추가
코어 알림 설정 화면에만 있던 비즈뿌리오 카카오 알림톡 템플릿 연결 UI(연결 버튼, 검수모드 배너, 안내박스)가 게시판·이커머스 알림 설정 화면에는 빠져 있었다. Phase 6 재설계 시 "후속 작업"으로 남겨두고 실제 반영이 되지 않았던 부분이다. - 게시판·이커머스 알림 정의 행에 코어와 동일한 notification_definition_row_footer 확장 슬롯을 추가하고, 게시판 레이아웃에는 코어/이커머스와 동일한 채널 콘텐츠 wrapper id를 신설해 구조를 맞췄다. - 비즈뿌리오 플러그인에 게시판·이커머스 전용 overlay 확장 파일을 추가해 배너· 안내박스·연결모달·데이터소스를 각 레이아웃에 주입한다. 백엔드는 notification_type을 확장 구분 없는 문자열 키로 이미 3영역 공용 처리하고 있어 프론트 진입점만 보완했다. - 회귀 테스트: 이커머스 화면에서 최초 구현 시 발견된 위치 버그(연결 버튼이 변수 뱃지 아래가 아니라 토글/편집 버튼 옆으로 붙던 문제)를 fail 재현 후 수정, 같은 회귀를 잡는 구조 검증 테스트를 게시판·이커머스 양쪽에 추가했다.
This commit is contained in:
@@ -9,6 +9,6 @@
|
||||
### Added
|
||||
|
||||
- 비즈뿌리오 연동 환경설정 화면과 문자(SMS/LMS)·카카오 알림톡 발송 채널을 추가했습니다. 회원가입·주문 등 코어 알림에 자동 연결되며, 검수/운영 환경을 구분해 운영합니다.
|
||||
- 설정 화면에서 카카오 알림톡 템플릿을 조회해 알림에 연결하고 실제로 발송할 수 있습니다. 템플릿 등록·검수는 비즈뿌리오 콘솔에서 진행합니다.
|
||||
- 설정 화면에서 카카오 알림톡 템플릿을 조회해 알림에 연결하고 실제로 발송할 수 있습니다. 템플릿 등록·검수는 비즈뿌리오 콘솔에서 진행합니다. 게시판·이커머스 알림 설정 화면에서도 코어와 동일하게 연결할 수 있습니다.
|
||||
- 비즈뿌리오 webhook 으로 발송 결과를 수신해 "알림 발송 이력" 화면에 성공/실패와 사유를 기록하며, 지갑 잔액 부족 등으로 발송이 막히면 관리자에게 알립니다.
|
||||
- 플러그인을 삭제하면 추가했던 발송 채널이 함께 정리되고, 재설치 시 자동 복원됩니다.
|
||||
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
{
|
||||
"target_layout": "admin_board_settings",
|
||||
"comment": "게시판 알림 설정 알림톡 탭에 비즈뿌리오 연동 UI 를 얹는다(notification_tab_core.json 과 동일 패턴, target_layout/target_id/모달 id 만 게시판 전용). 코어 목록·편집 모달·저장 버튼은 건드리지 않는다(무오염). ① 탭 상단 상태 배너(문제 있을 때만) ② 각 알림 행 하단(notification_definition_row_footer 확장 슬롯, notification_row_footer.json 이 채널 무관 전역 매칭)에 연결 상태 줄 + [연결/변경] 버튼 ③ [연결] 클릭 시 이 모듈 전용 연결 모달에서 승인 템플릿 선택·SMS 대체 설정 후 [저장]. 저장은 우리 API(notification_type 은 게시판 알림 type 값 그대로 전달, 백엔드는 확장 구분 없는 문자열 키).",
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "bizppurioBindings",
|
||||
"comment": "행 연결 상태 표시 + 모달 프리필용. auto_fetch:false — 설정 페이지 전 탭 자동 호출 방지. 알림톡 탭 진입 시 init_actions 로 조회한다.",
|
||||
"type": "api",
|
||||
"endpoint": "/api/plugins/sirsoft-message_bizppurio/admin/notification-bindings",
|
||||
"method": "GET",
|
||||
"auto_fetch": false,
|
||||
"auth_required": true,
|
||||
"fallback": {
|
||||
"data": {
|
||||
"bindings": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bizppurioApprovedTemplates",
|
||||
"comment": "연결 모달 드롭다운 옵션(카카오 관리 API 위임). auto_fetch:false — 자격증명 미설정 시 카카오 API 422 가 전 탭에서 발화하던 문제 차단. 연결 모달 열 때만 조회. errorHandling.default:suppress — 자격증명 미설정 시 422(카카오 API 요청 실패)를 조용히 무시한다(fallback 사용). 조회 실패와 '진짜 0건'을 화면에서 구분하기 위해 fallback.data.load_failed:true 마커를 싣는다.",
|
||||
"type": "api",
|
||||
"endpoint": "/api/plugins/sirsoft-message_bizppurio/admin/notification-bindings/approved-templates",
|
||||
"method": "GET",
|
||||
"auto_fetch": false,
|
||||
"auth_required": true,
|
||||
"errorHandling": {
|
||||
"default": {
|
||||
"handler": "suppress"
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
"data": {
|
||||
"templates": [],
|
||||
"load_failed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"injections": [
|
||||
{
|
||||
"target_id": "board_notif_channel_content",
|
||||
"position": "prepend_child",
|
||||
"comment": "알림톡·SMS 탭 상단 상태 배너. 문제 있을 때만 노출(정상=배너 없음). readiness 미충족 🔴 설정하기 / is_test_mode 🟡 테스트 모드.",
|
||||
"components": [
|
||||
{
|
||||
"id": "bizppurio_board_status_banner",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{['sms','alimtalk'].includes(_local.activeNotificationChannel ?? query.channel ?? '') && ((availableChannels?.data?.channels ?? []).find(c => c.id === (_local.activeNotificationChannel ?? query.channel))?.readiness?.ready === false || (availableChannels?.data?.channels ?? []).find(c => c.id === (_local.activeNotificationChannel ?? query.channel))?.is_test_mode === true)}}",
|
||||
"props": {
|
||||
"className": "mt-4 mb-3 space-y-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bizppurio_board_banner_not_ready",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(availableChannels?.data?.channels ?? []).find(c => c.id === (_local.activeNotificationChannel ?? query.channel))?.readiness?.ready === false}}",
|
||||
"props": {
|
||||
"className": "flex-between gap-3 p-3 rounded-lg bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fas fa-circle-exclamation",
|
||||
"size": "sm",
|
||||
"className": "text-red-600 dark:text-red-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-red-800 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.banner.not_ready"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-xs font-medium px-3 py-1.5 rounded-md bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 whitespace-nowrap"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.banner.setup_action",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/plugins/sirsoft-message_bizppurio/settings"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bizppurio_board_banner_test_mode",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(availableChannels?.data?.channels ?? []).find(c => c.id === (_local.activeNotificationChannel ?? query.channel))?.is_test_mode === true}}",
|
||||
"props": {
|
||||
"className": "flex-center gap-2 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fas fa-flask",
|
||||
"size": "sm",
|
||||
"className": "text-amber-600 dark:text-amber-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-amber-800 dark:text-amber-200"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.banner.test_mode"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bizppurio_board_alimtalk_guide",
|
||||
"comment": "알림톡 탭 상시 안내 박스 — 이 화면에서 무엇을 하는지(각 알림에 승인 템플릿을 연결). alimtalk 탭일 때만. 배너(빨강/노랑)와 구분되게 연한 파랑 정보 박스.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_local.activeNotificationChannel ?? query.channel) === 'alimtalk'}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mt-3 mb-1 p-3 rounded-lg bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fas fa-circle-info",
|
||||
"size": "sm",
|
||||
"className": "text-blue-500 dark:text-blue-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-blue-800 dark:text-blue-200"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.list_guide"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"modals": [
|
||||
{
|
||||
"id": "modal_bizppurio_binding",
|
||||
"comment": "알림톡 연결 전용 모달(우리 소유). 코어 편집 모달과 분리. 승인 템플릿 드롭다운·SMS 대체 토글·변수 안내 + [취소][저장]. 저장은 우리 API store. 모달 컨텍스트 분리 대응으로 값은 _global.bizppurio_binding_modal 에 담는다.",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-message_bizppurio.binding.modal_title|name={{_global.bizppurio_binding_modal?.notification_name ?? ''}}",
|
||||
"size": "md",
|
||||
"closeOnOverlayClick": false
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bizppurio_board_binding_modal_body",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"blur_until_loaded": "{{_global.bizppurio_binding_modal?.isSaving}}",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 p-3 rounded-lg bg-teal-50 dark:bg-teal-950 border border-teal-200 dark:border-teal-800"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fas fa-circle-info",
|
||||
"size": "sm",
|
||||
"className": "text-teal-600 dark:text-teal-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-teal-700 dark:text-teal-300"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.section_hint"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-secondary block text-xs font-medium mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.connected_template"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"value": "{{_global.bizppurio_binding_modal?.template_code ?? ''}}",
|
||||
"className": "w-full text-sm",
|
||||
"options": "{{[{ value: '', label: $t('sirsoft-message_bizppurio.binding.none') }, ...(bizppurioApprovedTemplates?.data?.templates ?? []).map(t => ({ value: t.template_code, label: t.template_name + ' (' + t.template_code + ')' }))]}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.template_code": "{{$event.target.value}}",
|
||||
"bizppurio_binding_modal.template_name": "{{((bizppurioApprovedTemplates?.data?.templates ?? []).find(t => t.template_code === $event.target.value)?.template_name) ?? ''}}",
|
||||
"bizppurio_binding_modal.fallback_sms": "{{$event.target.value === '' ? false : (_global.bizppurio_binding_modal?.fallback_sms ?? false)}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "드롭다운이 비었고 조회 실패(fallback 마커)일 때 — 키 오류 등을 '0건'과 구분해 설정 확인을 안내.",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{(bizppurioApprovedTemplates?.data?.templates ?? []).length === 0 && bizppurioApprovedTemplates?.data?.load_failed === true}}",
|
||||
"props": {
|
||||
"className": "text-xs text-red-600 dark:text-red-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.templates_load_failed"
|
||||
},
|
||||
{
|
||||
"comment": "드롭다운이 비었고 조회는 정상(load_failed 없음)일 때 — 실제 승인 템플릿 0건.",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{(bizppurioApprovedTemplates?.data?.templates ?? []).length === 0 && !(bizppurioApprovedTemplates?.data?.load_failed)}}",
|
||||
"props": {
|
||||
"className": "text-xs text-amber-600 dark:text-amber-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.no_approved_templates"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"checked": "{{_global.bizppurio_binding_modal?.fallback_sms ?? false}}",
|
||||
"disabled": "{{(_global.bizppurio_binding_modal?.template_code ?? '') === ''}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.fallback_sms": "{{!(_global.bizppurio_binding_modal?.fallback_sms ?? false)}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-secondary text-xs"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.fallback_sms"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bizppurio_board_binding_fallback_hint",
|
||||
"comment": "SMS 대체발송 시 무엇이 발송되는지 안내 — 이 알림의 본문이 문자로 발송됨.",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 pl-11 -mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.fallback_hint"
|
||||
},
|
||||
{
|
||||
"id": "bizppurio_board_binding_variables",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.bizppurio_binding_modal?.variables ?? []).length > 0}}",
|
||||
"props": {
|
||||
"className": "rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-600 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.variables_hint"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-wrap gap-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"iteration": {
|
||||
"source": "{{_global.bizppurio_binding_modal?.variables ?? []}}",
|
||||
"item_var": "v"
|
||||
},
|
||||
"props": {
|
||||
"className": "text-xs font-mono px-1.5 py-0.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "{{'#{' + (v.key ?? '') + '}'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-2 pt-4 mt-2 border-t border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-sm px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50",
|
||||
"disabled": "{{_global.bizppurio_binding_modal?.isSaving ?? false}}"
|
||||
},
|
||||
"text": "$t:common.cancel",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal",
|
||||
"target": "modal_bizppurio_binding"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-sm px-4 py-2 rounded-md bg-teal-600 text-white hover:bg-teal-700 dark:bg-teal-500 dark:hover:bg-teal-600 disabled:opacity-50",
|
||||
"disabled": "{{_global.bizppurio_binding_modal?.isSaving ?? false}}"
|
||||
},
|
||||
"text": "$t:common.save",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"params": {
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.isSaving": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/plugins/sirsoft-message_bizppurio/admin/notification-bindings",
|
||||
"params": {
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"notification_type": "{{_global.bizppurio_binding_modal?.notification_type}}",
|
||||
"template_code": "{{_global.bizppurio_binding_modal?.template_code ?? ''}}",
|
||||
"template_name": "{{_global.bizppurio_binding_modal?.template_name ?? ''}}",
|
||||
"fallback_sms_enabled": "{{_global.bizppurio_binding_modal?.fallback_sms ?? false}}"
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "bizppurioBindings"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"message": "$t:sirsoft-message_bizppurio.binding.saved",
|
||||
"type": "success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "closeModal",
|
||||
"target": "modal_bizppurio_binding"
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.isSaving": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.isSaving": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"message": "$t:sirsoft-message_bizppurio.binding.save_error",
|
||||
"type": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"init_actions": [
|
||||
{
|
||||
"comment": "알림톡 탭 진입 시 연결 맵 조회(행 연결 상태 표시용). 다른 탭에선 자동 호출 안 됨(auto_fetch:false).",
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "bizppurioBindings"
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 320
|
||||
}
|
||||
+483
@@ -0,0 +1,483 @@
|
||||
{
|
||||
"target_layout": "admin_ecommerce_settings",
|
||||
"comment": "이커머스 알림 설정 알림톡 탭에 비즈뿌리오 연동 UI 를 얹는다(notification_tab_core.json 과 동일 패턴, target_layout/target_id/모달 id 만 이커머스 전용). 코어 목록·편집 모달·저장 버튼은 건드리지 않는다(무오염). ① 탭 상단 상태 배너(문제 있을 때만) ② 각 알림 행 하단(notification_definition_row_footer 확장 슬롯, notification_row_footer.json 이 채널 무관 전역 매칭)에 연결 상태 줄 + [연결/변경] 버튼 ③ [연결] 클릭 시 이 모듈 전용 연결 모달에서 승인 템플릿 선택·SMS 대체 설정 후 [저장]. 저장은 우리 API(notification_type 은 이커머스 알림 type 값 그대로 전달, 백엔드는 확장 구분 없는 문자열 키).",
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "bizppurioBindings",
|
||||
"comment": "행 연결 상태 표시 + 모달 프리필용. auto_fetch:false — 설정 페이지 전 탭 자동 호출 방지. 알림톡 탭 진입 시 init_actions 로 조회한다.",
|
||||
"type": "api",
|
||||
"endpoint": "/api/plugins/sirsoft-message_bizppurio/admin/notification-bindings",
|
||||
"method": "GET",
|
||||
"auto_fetch": false,
|
||||
"auth_required": true,
|
||||
"fallback": {
|
||||
"data": {
|
||||
"bindings": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bizppurioApprovedTemplates",
|
||||
"comment": "연결 모달 드롭다운 옵션(카카오 관리 API 위임). auto_fetch:false — 자격증명 미설정 시 카카오 API 422 가 전 탭에서 발화하던 문제 차단. 연결 모달 열 때만 조회. errorHandling.default:suppress — 자격증명 미설정 시 422(카카오 API 요청 실패)를 조용히 무시한다(fallback 사용). 조회 실패와 '진짜 0건'을 화면에서 구분하기 위해 fallback.data.load_failed:true 마커를 싣는다.",
|
||||
"type": "api",
|
||||
"endpoint": "/api/plugins/sirsoft-message_bizppurio/admin/notification-bindings/approved-templates",
|
||||
"method": "GET",
|
||||
"auto_fetch": false,
|
||||
"auth_required": true,
|
||||
"errorHandling": {
|
||||
"default": {
|
||||
"handler": "suppress"
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
"data": {
|
||||
"templates": [],
|
||||
"load_failed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"injections": [
|
||||
{
|
||||
"target_id": "ecommerce_notif_channel_content",
|
||||
"position": "prepend_child",
|
||||
"comment": "알림톡·SMS 탭 상단 상태 배너. 문제 있을 때만 노출(정상=배너 없음). readiness 미충족 🔴 설정하기 / is_test_mode 🟡 테스트 모드.",
|
||||
"components": [
|
||||
{
|
||||
"id": "bizppurio_ecommerce_status_banner",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{['sms','alimtalk'].includes(_local.activeNotificationChannel ?? query.channel ?? '') && ((availableChannels?.data?.channels ?? []).find(c => c.id === (_local.activeNotificationChannel ?? query.channel))?.readiness?.ready === false || (availableChannels?.data?.channels ?? []).find(c => c.id === (_local.activeNotificationChannel ?? query.channel))?.is_test_mode === true)}}",
|
||||
"props": {
|
||||
"className": "mt-4 mb-3 space-y-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bizppurio_ecommerce_banner_not_ready",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(availableChannels?.data?.channels ?? []).find(c => c.id === (_local.activeNotificationChannel ?? query.channel))?.readiness?.ready === false}}",
|
||||
"props": {
|
||||
"className": "flex-between gap-3 p-3 rounded-lg bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fas fa-circle-exclamation",
|
||||
"size": "sm",
|
||||
"className": "text-red-600 dark:text-red-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-red-800 dark:text-red-200"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.banner.not_ready"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-xs font-medium px-3 py-1.5 rounded-md bg-red-600 text-white hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 whitespace-nowrap"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.banner.setup_action",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/plugins/sirsoft-message_bizppurio/settings"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bizppurio_ecommerce_banner_test_mode",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(availableChannels?.data?.channels ?? []).find(c => c.id === (_local.activeNotificationChannel ?? query.channel))?.is_test_mode === true}}",
|
||||
"props": {
|
||||
"className": "flex-center gap-2 p-3 rounded-lg bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fas fa-flask",
|
||||
"size": "sm",
|
||||
"className": "text-amber-600 dark:text-amber-400"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-amber-800 dark:text-amber-200"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.banner.test_mode"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bizppurio_ecommerce_alimtalk_guide",
|
||||
"comment": "알림톡 탭 상시 안내 박스 — 이 화면에서 무엇을 하는지(각 알림에 승인 템플릿을 연결). alimtalk 탭일 때만. 배너(빨강/노랑)와 구분되게 연한 파랑 정보 박스.",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_local.activeNotificationChannel ?? query.channel) === 'alimtalk'}}",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 mt-3 mb-1 p-3 rounded-lg bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fas fa-circle-info",
|
||||
"size": "sm",
|
||||
"className": "text-blue-500 dark:text-blue-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-sm text-blue-800 dark:text-blue-200"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.list_guide"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"modals": [
|
||||
{
|
||||
"id": "modal_bizppurio_binding",
|
||||
"comment": "알림톡 연결 전용 모달(우리 소유). 코어 편집 모달과 분리. 승인 템플릿 드롭다운·SMS 대체 토글·변수 안내 + [취소][저장]. 저장은 우리 API store. 모달 컨텍스트 분리 대응으로 값은 _global.bizppurio_binding_modal 에 담는다.",
|
||||
"type": "composite",
|
||||
"name": "Modal",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-message_bizppurio.binding.modal_title|name={{_global.bizppurio_binding_modal?.notification_name ?? ''}}",
|
||||
"size": "md",
|
||||
"closeOnOverlayClick": false
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "bizppurio_ecommerce_binding_modal_body",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"blur_until_loaded": "{{_global.bizppurio_binding_modal?.isSaving}}",
|
||||
"props": {
|
||||
"className": "space-y-4"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex items-center gap-2 p-3 rounded-lg bg-teal-50 dark:bg-teal-950 border border-teal-200 dark:border-teal-800"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fas fa-circle-info",
|
||||
"size": "sm",
|
||||
"className": "text-teal-600 dark:text-teal-400 flex-shrink-0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-xs text-teal-700 dark:text-teal-300"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.section_hint"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "text-secondary block text-xs font-medium mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.connected_template"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"value": "{{_global.bizppurio_binding_modal?.template_code ?? ''}}",
|
||||
"className": "w-full text-sm",
|
||||
"options": "{{[{ value: '', label: $t('sirsoft-message_bizppurio.binding.none') }, ...(bizppurioApprovedTemplates?.data?.templates ?? []).map(t => ({ value: t.template_code, label: t.template_name + ' (' + t.template_code + ')' }))]}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.template_code": "{{$event.target.value}}",
|
||||
"bizppurio_binding_modal.template_name": "{{((bizppurioApprovedTemplates?.data?.templates ?? []).find(t => t.template_code === $event.target.value)?.template_name) ?? ''}}",
|
||||
"bizppurio_binding_modal.fallback_sms": "{{$event.target.value === '' ? false : (_global.bizppurio_binding_modal?.fallback_sms ?? false)}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "드롭다운이 비었고 조회 실패(fallback 마커)일 때 — 키 오류 등을 '0건'과 구분해 설정 확인을 안내.",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{(bizppurioApprovedTemplates?.data?.templates ?? []).length === 0 && bizppurioApprovedTemplates?.data?.load_failed === true}}",
|
||||
"props": {
|
||||
"className": "text-xs text-red-600 dark:text-red-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.templates_load_failed"
|
||||
},
|
||||
{
|
||||
"comment": "드롭다운이 비었고 조회는 정상(load_failed 없음)일 때 — 실제 승인 템플릿 0건.",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"if": "{{(bizppurioApprovedTemplates?.data?.templates ?? []).length === 0 && !(bizppurioApprovedTemplates?.data?.load_failed)}}",
|
||||
"props": {
|
||||
"className": "text-xs text-amber-600 dark:text-amber-400 mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.no_approved_templates"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Toggle",
|
||||
"props": {
|
||||
"checked": "{{_global.bizppurio_binding_modal?.fallback_sms ?? false}}",
|
||||
"disabled": "{{(_global.bizppurio_binding_modal?.template_code ?? '') === ''}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.fallback_sms": "{{!(_global.bizppurio_binding_modal?.fallback_sms ?? false)}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-secondary text-xs"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.fallback_sms"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bizppurio_ecommerce_binding_fallback_hint",
|
||||
"comment": "SMS 대체발송 시 무엇이 발송되는지 안내 — 이 알림의 본문이 문자로 발송됨.",
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-500 dark:text-gray-400 pl-11 -mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.fallback_hint"
|
||||
},
|
||||
{
|
||||
"id": "bizppurio_ecommerce_binding_variables",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(_global.bizppurio_binding_modal?.variables ?? []).length > 0}}",
|
||||
"props": {
|
||||
"className": "rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 p-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-xs text-gray-600 dark:text-gray-400 mb-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.binding.variables_hint"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex flex-wrap gap-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"iteration": {
|
||||
"source": "{{_global.bizppurio_binding_modal?.variables ?? []}}",
|
||||
"item_var": "v"
|
||||
},
|
||||
"props": {
|
||||
"className": "text-xs font-mono px-1.5 py-0.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded text-gray-700 dark:text-gray-300"
|
||||
},
|
||||
"text": "{{'#{' + (v.key ?? '') + '}'}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-end gap-2 pt-4 mt-2 border-t border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-sm px-4 py-2 rounded-md border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50",
|
||||
"disabled": "{{_global.bizppurio_binding_modal?.isSaving ?? false}}"
|
||||
},
|
||||
"text": "$t:common.cancel",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "closeModal",
|
||||
"target": "modal_bizppurio_binding"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "text-sm px-4 py-2 rounded-md bg-teal-600 text-white hover:bg-teal-700 dark:bg-teal-500 dark:hover:bg-teal-600 disabled:opacity-50",
|
||||
"disabled": "{{_global.bizppurio_binding_modal?.isSaving ?? false}}"
|
||||
},
|
||||
"text": "$t:common.save",
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"params": {
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.isSaving": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/plugins/sirsoft-message_bizppurio/admin/notification-bindings",
|
||||
"params": {
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"notification_type": "{{_global.bizppurio_binding_modal?.notification_type}}",
|
||||
"template_code": "{{_global.bizppurio_binding_modal?.template_code ?? ''}}",
|
||||
"template_name": "{{_global.bizppurio_binding_modal?.template_name ?? ''}}",
|
||||
"fallback_sms_enabled": "{{_global.bizppurio_binding_modal?.fallback_sms ?? false}}"
|
||||
}
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "bizppurioBindings"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"message": "$t:sirsoft-message_bizppurio.binding.saved",
|
||||
"type": "success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "closeModal",
|
||||
"target": "modal_bizppurio_binding"
|
||||
},
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.isSaving": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "global",
|
||||
"bizppurio_binding_modal.isSaving": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"message": "$t:sirsoft-message_bizppurio.binding.save_error",
|
||||
"type": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"init_actions": [
|
||||
{
|
||||
"comment": "알림톡 탭 진입 시 연결 맵 조회(행 연결 상태 표시용). 다른 탭에선 자동 호출 안 됨(auto_fetch:false).",
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "bizppurioBindings"
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 320
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// e2e:allow 게시판·이커머스 배너/버튼/모달 노출은 Chrome MCP 로 실브라우저 확인·수정까지 완료
|
||||
// (2026-07-28). 정식 E2E는 비즈뿌리오 발송 인프라 의존이 커서 별도 계획에서 다룸(코어와 동일 사유).
|
||||
/**
|
||||
* 게시판·이커머스 알림 설정 알림톡 탭 연동 UI 구조 검증 (이슈 #28 후속)
|
||||
*
|
||||
* 배경: notification_tab_core.json(target_layout=admin_settings) + notification_row_footer.json
|
||||
* (extension_point, 전역 매칭)은 코어 알림 설정 화면에만 연동 버튼·배너를 노출했다.
|
||||
* extension_point 는 이름만 같으면 어느 레이아웃에서도 매칭되지만, target_id 기반 overlay
|
||||
* injection 은 레이아웃별로 독립이라 게시판·이커머스는 배너/안내박스/연결모달이 뜨지 않았다.
|
||||
*
|
||||
* notification_tab_board.json / notification_tab_ecommerce.json 을 신설해
|
||||
* target_layout=admin_board_settings / admin_ecommerce_settings 로 각각 등록했다.
|
||||
* notification_row_footer.json 은 그대로(전역 매칭)이므로 재사용된다.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import boardOverlay from '../../../extensions/notification_tab_board.json';
|
||||
import ecommerceOverlay from '../../../extensions/notification_tab_ecommerce.json';
|
||||
import ko from '../../../lang/ko.json';
|
||||
import en from '../../../lang/en.json';
|
||||
import { findById, type AnyNode } from './helpers';
|
||||
|
||||
type OverlayFixture = {
|
||||
label: string;
|
||||
overlay: typeof boardOverlay;
|
||||
targetLayout: string;
|
||||
targetId: string;
|
||||
bannerId: string;
|
||||
notReadyId: string;
|
||||
testModeId: string;
|
||||
guideId: string;
|
||||
modalBodyId: string;
|
||||
};
|
||||
|
||||
const FIXTURES: OverlayFixture[] = [
|
||||
{
|
||||
label: '게시판',
|
||||
overlay: boardOverlay,
|
||||
targetLayout: 'admin_board_settings',
|
||||
targetId: 'board_notif_channel_content',
|
||||
bannerId: 'bizppurio_board_status_banner',
|
||||
notReadyId: 'bizppurio_board_banner_not_ready',
|
||||
testModeId: 'bizppurio_board_banner_test_mode',
|
||||
guideId: 'bizppurio_board_alimtalk_guide',
|
||||
modalBodyId: 'bizppurio_board_binding_modal_body',
|
||||
},
|
||||
{
|
||||
label: '이커머스',
|
||||
overlay: ecommerceOverlay,
|
||||
targetLayout: 'admin_ecommerce_settings',
|
||||
targetId: 'ecommerce_notif_channel_content',
|
||||
bannerId: 'bizppurio_ecommerce_status_banner',
|
||||
notReadyId: 'bizppurio_ecommerce_banner_not_ready',
|
||||
testModeId: 'bizppurio_ecommerce_banner_test_mode',
|
||||
guideId: 'bizppurio_ecommerce_alimtalk_guide',
|
||||
modalBodyId: 'bizppurio_ecommerce_binding_modal_body',
|
||||
},
|
||||
];
|
||||
|
||||
const collectPluginKeys = (json: unknown): string[] => {
|
||||
const text = JSON.stringify(json);
|
||||
const prefixed = text.match(/\$t:sirsoft-message_bizppurio\.[a-zA-Z0-9_.]+/g) ?? [];
|
||||
const called = text.match(/\$t\('sirsoft-message_bizppurio\.[a-zA-Z0-9_.]+'\)/g) ?? [];
|
||||
return Array.from(new Set([
|
||||
...prefixed.map((m) => m.replace('$t:', '')),
|
||||
...called.map((m) => m.replace(/^\$t\('/, '').replace(/'\)$/, '')),
|
||||
]));
|
||||
};
|
||||
|
||||
describe.each(FIXTURES)('$label 알림톡 연동 overlay', ({
|
||||
overlay, targetLayout, targetId, bannerId, notReadyId, testModeId, guideId, modalBodyId,
|
||||
}) => {
|
||||
const injectionRoot = {
|
||||
children: ((overlay as { injections?: Array<{ components?: AnyNode[] }> }).injections ?? [])
|
||||
.flatMap((i) => i.components ?? []),
|
||||
} as AnyNode;
|
||||
const modalRoot = { children: (overlay as { modals?: AnyNode[] }).modals ?? [] } as AnyNode;
|
||||
|
||||
it(`target_layout=${targetLayout} 이고 extension_point 키가 없다(overlay 전용)`, () => {
|
||||
expect((overlay as { target_layout?: string }).target_layout).toBe(targetLayout);
|
||||
expect((overlay as Record<string, unknown>).extension_point).toBeUndefined();
|
||||
});
|
||||
|
||||
it(`배너·안내박스는 target_id=${targetId} 에 prepend_child 로 주입된다`, () => {
|
||||
const injections = (overlay as { injections?: Array<Record<string, unknown>> }).injections ?? [];
|
||||
expect(injections).toHaveLength(1);
|
||||
expect(injections[0].target_id).toBe(targetId);
|
||||
expect(injections[0].position).toBe('prepend_child');
|
||||
});
|
||||
|
||||
it('연결 맵·승인 템플릿 데이터소스를 등록한다(코어와 동일 endpoint)', () => {
|
||||
const ids = ((overlay as { data_sources?: Array<{ id: string }> }).data_sources ?? []).map((d) => d.id);
|
||||
expect(ids).toContain('bizppurioBindings');
|
||||
expect(ids).toContain('bizppurioApprovedTemplates');
|
||||
});
|
||||
|
||||
it('상태 배너는 sms·alimtalk 탭에서 문제(readiness 미충족 / test_mode)일 때만 노출된다', () => {
|
||||
const banner = findById(injectionRoot, bannerId);
|
||||
expect(banner).toBeTruthy();
|
||||
const cond = (banner as { if?: string }).if ?? '';
|
||||
expect(cond).toContain("'sms'");
|
||||
expect(cond).toContain("'alimtalk'");
|
||||
expect(cond).toContain('readiness?.ready === false');
|
||||
expect(cond).toContain('is_test_mode === true');
|
||||
});
|
||||
|
||||
it('readiness 미충족 배너에 설정하기 이동 버튼이 있다', () => {
|
||||
const raw = JSON.stringify(findById(injectionRoot, notReadyId));
|
||||
expect(raw).toContain('banner.not_ready');
|
||||
expect(raw).toContain('banner.setup_action');
|
||||
expect(raw).toContain('/admin/plugins/sirsoft-message_bizppurio/settings');
|
||||
});
|
||||
|
||||
it('검수 모드 배너는 readiness 와 무관하게 is_test_mode 만으로 노출된다', () => {
|
||||
const banner = findById(injectionRoot, testModeId);
|
||||
expect(banner).toBeTruthy();
|
||||
const cond = (banner as { if?: string }).if ?? '';
|
||||
expect(cond).not.toContain('readiness');
|
||||
expect(cond).toContain('is_test_mode === true');
|
||||
});
|
||||
|
||||
it('알림톡 탭 상시 안내 박스가 있다', () => {
|
||||
const guide = findById(injectionRoot, guideId);
|
||||
expect(guide).toBeTruthy();
|
||||
expect((guide as { if?: string }).if).toContain("=== 'alimtalk'");
|
||||
expect(JSON.stringify(guide)).toContain('binding.list_guide');
|
||||
});
|
||||
|
||||
it('연결 전용 모달(modal_bizppurio_binding)이 modals 로 등록된다', () => {
|
||||
const modal = findById(modalRoot, 'modal_bizppurio_binding');
|
||||
expect(modal).toBeTruthy();
|
||||
expect((modal as { name?: string }).name).toBe('Modal');
|
||||
});
|
||||
|
||||
it('[저장] 은 우리 API store 로 저장하고 toast + 모달 닫힘 + 목록 갱신한다', () => {
|
||||
const modal = findById(modalRoot, 'modal_bizppurio_binding');
|
||||
const raw = JSON.stringify(modal);
|
||||
expect(raw).toContain('/api/plugins/sirsoft-message_bizppurio/admin/notification-bindings');
|
||||
expect(raw).toContain('"method":"POST"');
|
||||
expect(raw).toContain('binding.saved');
|
||||
expect(raw).toContain('"closeModal"');
|
||||
expect(raw).toContain('bizppurioBindings');
|
||||
});
|
||||
|
||||
it('연결 템플릿이 없으면 SMS 대체 토글이 비활성이다(코어와 동일 규칙)', () => {
|
||||
const raw = JSON.stringify(findById(modalRoot, modalBodyId));
|
||||
expect(raw).toContain('"disabled"');
|
||||
expect(raw).toContain("=== ''");
|
||||
});
|
||||
|
||||
it('overlay 는 코어 편집 모달 저장 body(notification-templates PUT)를 건드리지 않는다', () => {
|
||||
const raw = JSON.stringify(overlay);
|
||||
expect(raw).not.toContain('/api/admin/notification-templates/');
|
||||
expect(raw).not.toContain('notification_template_form_modal');
|
||||
});
|
||||
|
||||
it('참조하는 모든 플러그인 i18n 키가 ko·en 에 존재한다', () => {
|
||||
const keys = collectPluginKeys(overlay);
|
||||
expect(keys.length).toBeGreaterThan(0);
|
||||
const resolve = (root: unknown, path: string): unknown =>
|
||||
path.split('.').slice(1).reduce<unknown>((acc, seg) => (acc as Record<string, unknown>)?.[seg], root);
|
||||
for (const key of Array.from(new Set(keys))) {
|
||||
expect(resolve(ko, key), `ko 누락: ${key}`).toBeTruthy();
|
||||
expect(resolve(en, key), `en 누락: ${key}`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user