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:
@@ -10,6 +10,7 @@
|
||||
|
||||
- 사이트맵에 담을 게시판·게시글 수의 안전 상한을 지원합니다. 상한을 설정하면 게시판 목록·게시판·게시글 어느 항목이든 그 수를 넘지 않으며, 상한에 걸려 일부가 빠진 경우 기록으로 남습니다.
|
||||
- 게시글·게시판을 공개/비공개로 바꾸거나 삭제하면 사이트맵에 해당 항목만 자동으로 반영됩니다. 전체를 다시 만들지 않고 바뀐 부분만 갱신하므로 게시글이 많은 사이트에서도 빠르게 최신 상태가 유지됩니다.
|
||||
- 게시판 알림 설정 화면에서도 코어와 동일하게 카카오 알림톡 템플릿을 연결할 수 있습니다(비즈뿌리오 플러그인 설치 시).
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// e2e:allow 레이아웃 트리 위치 검증(정적 구조 회귀 가드)이라 vitest 로 충분. 실제 화면 렌더는
|
||||
// Chrome MCP 로 확인·수정까지 완료(2026-07-28). 알림설정 화면 Playwright 인프라 부재 (#28)
|
||||
/**
|
||||
* 게시판 알림 정의 행 하단 확장 슬롯(notification_definition_row_footer) 위치 회귀 가드 (#28)
|
||||
*
|
||||
* @description
|
||||
* 회귀 시나리오: extension_point 를 행 정보 컬럼(`flex-1 min-w-0`) 밖에 형제로 잘못 넣으면,
|
||||
* 비즈뿌리오 플러그인이 주입하는 [연결/변경] 버튼이 정보 컬럼과 토글·편집 버튼 컬럼
|
||||
* (`flex-center gap-3 flex-shrink-0`) 사이의 3번째 flex 아이템이 되어 옆으로 붙어 보인다
|
||||
* (변수 뱃지 아래 새 줄이 아니라 오른쪽 버튼 옆으로 밀림).
|
||||
*
|
||||
* extension_point 는 반드시 정보 컬럼의 children 배열 마지막 원소(variables 뱃지의 형제)여야
|
||||
* 하며, 토글/편집 버튼을 담는 형제 Div 의 children 에는 나타나지 않아야 한다.
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import tab from '../../../layouts/admin/partials/admin_board_settings/_tab_notification_definitions.json';
|
||||
|
||||
type AnyNode = Record<string, unknown> & {
|
||||
id?: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
props?: { className?: string };
|
||||
children?: AnyNode[];
|
||||
};
|
||||
|
||||
function findAll(node: AnyNode | undefined, pred: (n: AnyNode) => boolean, results: AnyNode[] = []): AnyNode[] {
|
||||
if (!node) return results;
|
||||
if (pred(node)) results.push(node);
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const c of node.children) findAll(c, pred, results);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('게시판 알림 행 footer extension_point 위치 (#28)', () => {
|
||||
it('extension_point 는 정보 컬럼(flex-1 min-w-0)의 children 이다', () => {
|
||||
const infoColumns = findAll(tab as AnyNode, (n) => n.props?.className === 'flex-1 min-w-0');
|
||||
expect(infoColumns.length).toBeGreaterThan(0);
|
||||
|
||||
const infoColumnHasEp = infoColumns.some((col) =>
|
||||
(col.children ?? []).some((c) => c.type === 'extension_point' && c.name === 'notification_definition_row_footer'),
|
||||
);
|
||||
expect(infoColumnHasEp).toBe(true);
|
||||
});
|
||||
|
||||
it('extension_point 는 토글·편집 버튼 컬럼(flex-center gap-3 flex-shrink-0)의 children 이 아니다', () => {
|
||||
const buttonColumns = findAll(
|
||||
tab as AnyNode,
|
||||
(n) => n.props?.className === 'flex-center gap-3 flex-shrink-0',
|
||||
);
|
||||
expect(buttonColumns.length).toBeGreaterThan(0);
|
||||
|
||||
const buttonColumnHasEp = buttonColumns.some((col) =>
|
||||
(col.children ?? []).some((c) => c.type === 'extension_point'),
|
||||
);
|
||||
expect(buttonColumnHasEp).toBe(false);
|
||||
});
|
||||
|
||||
it('extension_point 가 정보 컬럼과 버튼 컬럼의 공통 부모(행 wrapper)에 직접 걸려 있지 않다', () => {
|
||||
const rowWrappers = findAll(
|
||||
tab as AnyNode,
|
||||
(n) =>
|
||||
Array.isArray(n.children) &&
|
||||
n.children.some((c) => c.props?.className === 'flex-1 min-w-0') &&
|
||||
n.children.some((c) => c.props?.className === 'flex-center gap-3 flex-shrink-0'),
|
||||
);
|
||||
expect(rowWrappers.length).toBeGreaterThan(0);
|
||||
|
||||
for (const wrapper of rowWrappers) {
|
||||
const directEp = (wrapper.children ?? []).some((c) => c.type === 'extension_point');
|
||||
expect(directEp, 'extension_point 가 행 wrapper 의 3번째 flex 형제가 되면 옆으로 붙는 회귀').toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
+115
-99
@@ -196,26 +196,16 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_notif_list_info",
|
||||
"id": "board_notif_channel_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-between py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-label-subtle"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.notification_definitions.total_count|count={{boardNotificationDefinitions?.data?.pagination?.total ?? 0}}"
|
||||
},
|
||||
{
|
||||
"id": "board_notif_list_info",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex-center gap-2"
|
||||
"className": "flex-between py-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
@@ -224,55 +214,70 @@
|
||||
"props": {
|
||||
"className": "text-label-subtle"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.notification_definitions.per_page_label"
|
||||
"text": "$t:sirsoft-board.admin.settings.notification_definitions.total_count|count={{boardNotificationDefinitions?.data?.pagination?.total ?? 0}}"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"name": "notifPerPage",
|
||||
"className": "w-20 px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm",
|
||||
"value": "{{String(query.per_page || 20)}}",
|
||||
"options": [
|
||||
{
|
||||
"value": "10",
|
||||
"label": "10"
|
||||
},
|
||||
{
|
||||
"value": "20",
|
||||
"label": "20"
|
||||
},
|
||||
{
|
||||
"value": "50",
|
||||
"label": "50"
|
||||
}
|
||||
]
|
||||
"className": "flex-center gap-2"
|
||||
},
|
||||
"actions": [
|
||||
"children": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/settings",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": {
|
||||
"per_page": "{{$event.target.value}}",
|
||||
"page": 1
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"props": {
|
||||
"className": "text-label-subtle"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.notification_definitions.per_page_label"
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "notifPerPage",
|
||||
"className": "w-20 px-3 py-1.5 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-sm",
|
||||
"value": "{{String(query.per_page || 20)}}",
|
||||
"options": [
|
||||
{
|
||||
"value": "10",
|
||||
"label": "10"
|
||||
},
|
||||
{
|
||||
"value": "20",
|
||||
"label": "20"
|
||||
},
|
||||
{
|
||||
"value": "50",
|
||||
"label": "50"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "change",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/settings",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": {
|
||||
"per_page": "{{$event.target.value}}",
|
||||
"page": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_notif_definitions_list",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
},
|
||||
{
|
||||
"id": "board_notif_definitions_list",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
@@ -493,6 +498,15 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "알림 정의 행 하단 확장 슬롯(범용). 각 소유 확장이 알림별 부가 UI(예: 채널 연동)를 이 자리에 주입한다. 특정 확장을 알지 못하며, 확장 미설치 시 빈 자리로 남는다. def·현재 채널을 extensionPointProps 로 전달한다.",
|
||||
"type": "extension_point",
|
||||
"name": "notification_definition_row_footer",
|
||||
"props": {
|
||||
"definition": "{{def}}",
|
||||
"activeChannel": "{{_local.activeNotificationChannel ?? query.channel ?? 'mail'}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -657,70 +671,72 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_notif_empty",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(boardNotificationDefinitions?.data?.data ?? []).length === 0}}",
|
||||
"props": {
|
||||
"className": "admin-card"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "board_notif_empty",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{(boardNotificationDefinitions?.data?.data ?? []).length === 0}}",
|
||||
"props": {
|
||||
"className": "row-stack"
|
||||
"className": "admin-card"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"name": "fa-solid fa-envelope",
|
||||
"className": "text-4xl text-gray-300 dark:text-gray-600 mb-3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-label-subtle"
|
||||
"className": "row-stack"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.notification_definitions.empty_message"
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "fa-solid fa-envelope",
|
||||
"className": "text-4xl text-gray-300 dark:text-gray-600 mb-3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "text-label-subtle"
|
||||
},
|
||||
"text": "$t:sirsoft-board.admin.settings.notification_definitions.empty_message"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "board_notif_pagination",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex justify-center"
|
||||
},
|
||||
"children": [
|
||||
},
|
||||
{
|
||||
"type": "composite",
|
||||
"name": "Pagination",
|
||||
"id": "board_notif_pagination",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"currentPage": "{{boardNotificationDefinitions?.data?.pagination?.current_page || 1}}",
|
||||
"totalPages": "{{boardNotificationDefinitions?.data?.pagination?.last_page || 1}}"
|
||||
"className": "flex justify-center"
|
||||
},
|
||||
"actions": [
|
||||
"children": [
|
||||
{
|
||||
"event": "onPageChange",
|
||||
"type": "action",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/settings",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": {
|
||||
"page": "{{$args[0]}}"
|
||||
"type": "composite",
|
||||
"name": "Pagination",
|
||||
"props": {
|
||||
"currentPage": "{{boardNotificationDefinitions?.data?.pagination?.current_page || 1}}",
|
||||
"totalPages": "{{boardNotificationDefinitions?.data?.pagination?.last_page || 1}}"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"event": "onPageChange",
|
||||
"type": "action",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/boards/settings",
|
||||
"replace": true,
|
||||
"mergeQuery": true,
|
||||
"query": {
|
||||
"page": "{{$args[0]}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
- 사이트맵에 담을 상품·카테고리 수의 안전 상한을 지원합니다. 상한을 설정하면 상품 목록·카테고리·상품 어느 항목이든 그 수를 넘지 않으며, 상한에 걸려 일부가 빠진 경우 기록으로 남습니다.
|
||||
- 상품·카테고리를 전시/숨김으로 바꾸거나 삭제하면 사이트맵에 해당 항목만 자동으로 반영됩니다. 전체를 다시 만들지 않고 바뀐 부분만 갱신하므로 상품이 많은 쇼핑몰에서도 빠르게 최신 상태가 유지됩니다.
|
||||
- 이커머스 알림 설정 화면에서도 코어와 동일하게 카카오 알림톡 템플릿을 연결할 수 있습니다(비즈뿌리오 플러그인 설치 시).
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// e2e:allow 레이아웃 트리 위치 검증(정적 구조 회귀 가드)이라 vitest 로 충분. 실제 화면 렌더는
|
||||
// Chrome MCP 로 확인·수정까지 완료(2026-07-28). 알림설정 화면 Playwright 인프라 부재 (#28)
|
||||
/**
|
||||
* 이커머스 알림 정의 행 하단 확장 슬롯(notification_definition_row_footer) 위치 회귀 가드 (#28)
|
||||
*
|
||||
* @description
|
||||
* 회귀 시나리오: extension_point 를 행 정보 컬럼(`flex-1 min-w-0`) 밖에 형제로 잘못 넣으면,
|
||||
* 비즈뿌리오 플러그인이 주입하는 [연결/변경] 버튼이 정보 컬럼과 토글·편집 버튼 컬럼
|
||||
* (`flex-center gap-3 flex-shrink-0`) 사이의 3번째 flex 아이템이 되어 옆으로 붙어 보인다
|
||||
* (변수 뱃지 아래 새 줄이 아니라 오른쪽 버튼 옆으로 밀림).
|
||||
*
|
||||
* extension_point 는 반드시 정보 컬럼의 children 배열 마지막 원소(variables 뱃지의 형제)여야
|
||||
* 하며, 토글/편집 버튼을 담는 형제 Div 의 children 에는 나타나지 않아야 한다.
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import tab from '../../../layouts/admin/partials/admin_ecommerce_settings/_tab_notification_definitions.json';
|
||||
|
||||
type AnyNode = Record<string, unknown> & {
|
||||
id?: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
props?: { className?: string };
|
||||
children?: AnyNode[];
|
||||
};
|
||||
|
||||
function findAll(node: AnyNode | undefined, pred: (n: AnyNode) => boolean, results: AnyNode[] = []): AnyNode[] {
|
||||
if (!node) return results;
|
||||
if (pred(node)) results.push(node);
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const c of node.children) findAll(c, pred, results);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
describe('이커머스 알림 행 footer extension_point 위치 (#28)', () => {
|
||||
it('extension_point 는 정보 컬럼(flex-1 min-w-0)의 children 이다', () => {
|
||||
const infoColumns = findAll(tab as AnyNode, (n) => n.props?.className === 'flex-1 min-w-0');
|
||||
expect(infoColumns.length).toBeGreaterThan(0);
|
||||
|
||||
const infoColumnHasEp = infoColumns.some((col) =>
|
||||
(col.children ?? []).some((c) => c.type === 'extension_point' && c.name === 'notification_definition_row_footer'),
|
||||
);
|
||||
expect(infoColumnHasEp).toBe(true);
|
||||
});
|
||||
|
||||
it('extension_point 는 토글·편집 버튼 컬럼(flex-center gap-3 flex-shrink-0)의 children 이 아니다', () => {
|
||||
const buttonColumns = findAll(
|
||||
tab as AnyNode,
|
||||
(n) => n.props?.className === 'flex-center gap-3 flex-shrink-0',
|
||||
);
|
||||
expect(buttonColumns.length).toBeGreaterThan(0);
|
||||
|
||||
const buttonColumnHasEp = buttonColumns.some((col) =>
|
||||
(col.children ?? []).some((c) => c.type === 'extension_point'),
|
||||
);
|
||||
expect(buttonColumnHasEp).toBe(false);
|
||||
});
|
||||
|
||||
it('extension_point 가 정보 컬럼과 버튼 컬럼의 공통 부모(행 wrapper)에 직접 걸려 있지 않다', () => {
|
||||
const rowWrappers = findAll(
|
||||
tab as AnyNode,
|
||||
(n) =>
|
||||
Array.isArray(n.children) &&
|
||||
n.children.some((c) => c.props?.className === 'flex-1 min-w-0') &&
|
||||
n.children.some((c) => c.props?.className === 'flex-center gap-3 flex-shrink-0'),
|
||||
);
|
||||
expect(rowWrappers.length).toBeGreaterThan(0);
|
||||
|
||||
for (const wrapper of rowWrappers) {
|
||||
const directEp = (wrapper.children ?? []).some((c) => c.type === 'extension_point');
|
||||
expect(directEp, 'extension_point 가 행 wrapper 의 3번째 flex 형제가 되면 옆으로 붙는 회귀').toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
+9
@@ -498,6 +498,15 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "알림 정의 행 하단 확장 슬롯(범용). 각 소유 확장이 알림별 부가 UI(예: 채널 연동)를 이 자리에 주입한다. 특정 확장을 알지 못하며, 확장 미설치 시 빈 자리로 남는다. def·현재 채널을 extensionPointProps 로 전달한다.",
|
||||
"type": "extension_point",
|
||||
"name": "notification_definition_row_footer",
|
||||
"props": {
|
||||
"definition": "{{def}}",
|
||||
"activeChannel": "{{_local.activeNotificationChannel ?? query.channel ?? 'mail'}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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