feat(message_bizppurio): 비즈뿌리오 메시징 플러그인 골격 + 환경설정
Phase 1 — 플러그인 설치·활성화 시 권한·메뉴·환경설정 화면이 뜨는 기반 구축. 이후 발송엔진·채널·이력·템플릿 Phase 의 전제가 된다. - 관리자 환경설정 화면(3섹션: API 연동/발송 설정/연동 안내) — 비즈뿌리오 아이디·비밀번호·API 키·발신번호·알림톡 발신프로필 키·연동 환경(검수/운영). 크리덴셜은 마스킹 + 프론트 미노출. - 조회/관리 권한 2개 + 「메시지 발송 관리」 관리자 메뉴(알림톡 템플릿·발송 이력 하위). - 잔액 부족 시 관리자 알림 정의(webhook 결과코드 감지는 Phase 4). - 발송 관련 Enum 4종(채널/상태/출처/결과분류) — Phase 2~4 소비용 골격. 알림톡 채널 기본 body 시드는 채널 등록(Phase 3)·탭 연동(Phase 6)과 강결합이라 Phase 6 으로 이관(plugin.php 주석 명시). 테스트: PHPUnit 12건(설치·권한·메뉴·알림 정의·Enum) + Vitest 22건(환경설정 레이아웃). 설치→활성화 실검증으로 권한 4·메뉴 3·알림 1·설정 초기값 로드 확인.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
이 프로젝트의 모든 주요 변경사항을 기록합니다.
|
||||
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며,
|
||||
[Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다.
|
||||
|
||||
## [1.0.0] - 2026-07-09
|
||||
|
||||
### Added
|
||||
|
||||
- 비즈뿌리오(다우기술) 연동 환경설정 화면을 추가했습니다. 비즈뿌리오 아이디·비밀번호·API 키·발신번호·알림톡 발신프로필 키·연동 환경(검수/운영)을 관리자 화면에서 설정할 수 있습니다.
|
||||
- 관리자 「메시지 발송 관리」 메뉴와 조회·관리 권한을 추가했습니다.
|
||||
- 비즈뿌리오 지갑 잔액이 부족할 때 관리자에게 알리는 알림을 추가했습니다.
|
||||
@@ -0,0 +1,47 @@
|
||||
프로그램 명칭 : 그누보드7용 비즈뿌리오 메시징 플러그인 (sirsoft-message_bizppurio)
|
||||
|
||||
저작자 : (주)에스아이알소프트
|
||||
|
||||
----- MIT 라이선스 (한국어 번역) --------------------------------------------------------
|
||||
|
||||
MIT 라이선스
|
||||
|
||||
Copyright (c) 2026 (주)에스아이알소프트
|
||||
|
||||
이 소프트웨어와 관련 문서 파일(이하 "소프트웨어")의 복사본을 취득하는 모든 사람에게
|
||||
소프트웨어를 제한 없이 사용, 복사, 수정, 병합, 출판, 배포, 서브라이선스 허여 및/또는
|
||||
판매할 수 있는 권리를 무상으로 부여합니다. 다만, 소프트웨어를 제공받은 사람은 다음
|
||||
조건을 따라야 합니다:
|
||||
|
||||
위 저작권 고지와 본 허가 고지는 소프트웨어의 모든 복사본 또는 상당 부분에 포함되어야
|
||||
합니다.
|
||||
|
||||
소프트웨어는 "있는 그대로" 제공되며, 명시적이든 묵시적이든 어떠한 종류의 보증도 하지
|
||||
않습니다. 여기에는 상품성, 특정 목적에의 적합성 및 비침해에 대한 보증이 포함되나 이에
|
||||
국한되지 않습니다. 어떠한 경우에도 저작자 또는 저작권자는 소프트웨어나 소프트웨어의
|
||||
사용 또는 기타 거래로 인해 발생하는 계약, 불법행위 또는 기타 청구, 손해 또는 기타
|
||||
책임에 대해 책임을 지지 않습니다.
|
||||
|
||||
----- MIT License (English Original) --------------------------------------------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2026 SIRSOFT
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "plugins/sirsoft-message_bizppurio",
|
||||
"description": "Bizppurio SMS/LMS and KakaoTalk alimtalk messaging plugin for G7 platform by sirsoft",
|
||||
"type": "library",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "sirsoft",
|
||||
"email": "contact@sirsoft.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-json": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Plugins\\Sirsoft\\MessageBizppurio\\": ["src/", "./"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": "1.0.0",
|
||||
"description": "비즈뿌리오 메시징 플러그인 환경설정 기본값 및 프론트엔드 스키마. 크리덴셜(password/api_key/sender_key)은 sensitive + expose:false 로 프론트 노출을 차단한다. 관리자 설정 화면은 코어 /api/admin/plugins/{id}/settings 로 직접 조회하므로 window.G7Config 노출이 불필요하여 전 필드 expose:false."
|
||||
},
|
||||
"defaults": {
|
||||
"environment": "dev",
|
||||
"bizppurio_id": "",
|
||||
"password": "",
|
||||
"api_key": "",
|
||||
"sender_number": "",
|
||||
"sender_key": ""
|
||||
},
|
||||
"frontend_schema": {
|
||||
"environment": { "expose": false },
|
||||
"bizppurio_id": { "expose": false },
|
||||
"password": { "expose": false, "sensitive": true },
|
||||
"api_key": { "expose": false, "sensitive": true },
|
||||
"sender_number": { "expose": false },
|
||||
"sender_key": { "expose": false, "sensitive": true }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// 발송 채널 (DispatchChannel enum)
|
||||
'channel' => [
|
||||
'sms' => 'SMS',
|
||||
'lms' => 'LMS',
|
||||
'alimtalk' => 'Alimtalk',
|
||||
],
|
||||
|
||||
// 발송 상태 (DispatchStatus enum)
|
||||
'status' => [
|
||||
'pending' => 'Pending',
|
||||
'sent' => 'Sending',
|
||||
'success' => 'Success',
|
||||
'failed' => 'Failed',
|
||||
],
|
||||
|
||||
// 발송 출처 (DispatchSource enum)
|
||||
'source' => [
|
||||
'auto' => 'Automatic',
|
||||
'manual' => 'Manual',
|
||||
'bulk' => 'Bulk',
|
||||
],
|
||||
|
||||
// 결과코드 분류 (ResultCategory enum)
|
||||
'result_category' => [
|
||||
'success' => 'Success',
|
||||
'retry' => 'Retry',
|
||||
'permanent_failure' => 'Permanent Failure',
|
||||
'balance_low' => 'Insufficient Balance',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// 발송 채널 (DispatchChannel enum)
|
||||
'channel' => [
|
||||
'sms' => 'SMS',
|
||||
'lms' => 'LMS',
|
||||
'alimtalk' => '알림톡',
|
||||
],
|
||||
|
||||
// 발송 상태 (DispatchStatus enum)
|
||||
'status' => [
|
||||
'pending' => '대기',
|
||||
'sent' => '발송중',
|
||||
'success' => '성공',
|
||||
'failed' => '실패',
|
||||
],
|
||||
|
||||
// 발송 출처 (DispatchSource enum)
|
||||
'source' => [
|
||||
'auto' => '자동',
|
||||
'manual' => '수동',
|
||||
'bulk' => '대량',
|
||||
],
|
||||
|
||||
// 결과코드 분류 (ResultCategory enum)
|
||||
'result_category' => [
|
||||
'success' => '성공',
|
||||
'retry' => '재시도',
|
||||
'permanent_failure' => '영구 실패',
|
||||
'balance_low' => '잔액 부족',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@plugins/sirsoft-message_bizppurio",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"identifier": "sirsoft-message_bizppurio",
|
||||
"vendor": "sirsoft",
|
||||
"name": {
|
||||
"ko": "비즈뿌리오 메시징",
|
||||
"en": "Bizppurio Messaging"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"description": {
|
||||
"ko": "비즈뿌리오(다우기술) 연동 SMS/LMS·카카오 알림톡 자동발송 플러그인입니다. 코어 알림 시스템 채널로 문자·알림톡을 발송하고 발송 결과를 webhook 으로 수신합니다.",
|
||||
"en": "Bizppurio (Daou Tech) SMS/LMS and KakaoTalk alimtalk plugin. Sends messages through core notification channels and receives delivery results via webhook."
|
||||
},
|
||||
"license": "MIT",
|
||||
"g7_version": ">=7.0.3",
|
||||
"dependencies": {
|
||||
"modules": {},
|
||||
"plugins": {}
|
||||
},
|
||||
"github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-message_bizppurio",
|
||||
"github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-message_bizppurio/blob/main/CHANGELOG.md",
|
||||
"loading": {
|
||||
"strategy": "global",
|
||||
"priority": 100
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Extension\AbstractPlugin;
|
||||
use App\Extension\Helpers\ExtensionMenuSyncHelper;
|
||||
|
||||
/**
|
||||
* 비즈뿌리오 메시징 플러그인
|
||||
*
|
||||
* 비즈뿌리오(다우기술) 연동 SMS/LMS·카카오 알림톡 자동발송을 제공합니다.
|
||||
* 코어 알림 시스템의 채널로 문자·알림톡을 발송하고, 발송 결과를 webhook 으로
|
||||
* 수신하여 이력에 기록합니다.
|
||||
*/
|
||||
class Plugin extends AbstractPlugin
|
||||
{
|
||||
/**
|
||||
* 플러그인 메타데이터 반환
|
||||
*
|
||||
* @return array 메타데이터
|
||||
*/
|
||||
public function getMetadata(): array
|
||||
{
|
||||
return [
|
||||
'author' => 'Sirsoft',
|
||||
'license' => 'MIT',
|
||||
'homepage' => 'https://sir.kr',
|
||||
'keywords' => ['bizppurio', 'sms', 'lms', 'alimtalk', 'kakao', 'messaging', 'notification'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 메뉴 정의
|
||||
*
|
||||
* 코어 PluginManager 는 모듈과 달리 plugin 의 getAdminMenus() 를 자동 호출하지 않으므로
|
||||
* 본 클래스의 activate()/deactivate()/uninstall() lifecycle hook 에서 직접 sync 한다.
|
||||
* 멱등성: helper.syncMenu 가 upsert 패턴이라 재활성화 시에도 row 중복 없이 그대로 복원.
|
||||
*
|
||||
* 최상위 「메시지 발송 관리」 아래에 알림톡 템플릿 관리·발송 이력 하위 메뉴를 둔다.
|
||||
* (하위 페이지는 Phase 4·5 에서 라우트/레이아웃과 함께 실물화)
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getAdminMenus(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => ['ko' => '메시지 발송 관리', 'en' => 'Messaging'],
|
||||
'slug' => 'sirsoft-message_bizppurio',
|
||||
'url' => null,
|
||||
'icon' => 'fas fa-paper-plane',
|
||||
'order' => 55,
|
||||
'children' => [
|
||||
[
|
||||
'name' => ['ko' => '알림톡 템플릿 관리', 'en' => 'Alimtalk Templates'],
|
||||
'slug' => 'sirsoft-message_bizppurio-alimtalk-templates',
|
||||
'url' => '/admin/plugins/sirsoft-message_bizppurio/alimtalk-templates',
|
||||
'icon' => 'fas fa-comment-dots',
|
||||
'order' => 1,
|
||||
'permission' => 'sirsoft-message_bizppurio.messaging.view',
|
||||
],
|
||||
[
|
||||
'name' => ['ko' => '발송 이력', 'en' => 'Dispatch History'],
|
||||
'slug' => 'sirsoft-message_bizppurio-dispatches',
|
||||
'url' => '/admin/plugins/sirsoft-message_bizppurio/dispatches',
|
||||
'icon' => 'fas fa-clock-rotate-left',
|
||||
'order' => 2,
|
||||
'permission' => 'sirsoft-message_bizppurio.messaging.view',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 활성화 — 관리자 메뉴 자동 등록.
|
||||
*
|
||||
* 모듈은 ModuleManager 가 getAdminMenus() 를 자동 처리하지만 플러그인은
|
||||
* PluginManager 가 그 책임을 위임하므로 본 메서드에서 helper 를 직접 호출한다.
|
||||
* syncMenu 가 신규 메뉴 생성 시 admin 역할을 자동 부여하므로 별도 역할 처리는 불필요.
|
||||
*
|
||||
* @return bool 활성화 성공 여부
|
||||
*/
|
||||
public function activate(): bool
|
||||
{
|
||||
$helper = app(ExtensionMenuSyncHelper::class);
|
||||
|
||||
foreach ($this->getAdminMenus() as $menuData) {
|
||||
$helper->syncMenuRecursive(
|
||||
$menuData,
|
||||
ExtensionOwnerType::Plugin,
|
||||
$this->getIdentifier(),
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 비활성화 — 관리자 메뉴 일괄 제거.
|
||||
*
|
||||
* cleanupStaleMenus 에 currentSlugs=[] 를 넘겨 본 플러그인 소속 메뉴 전체를 삭제한다.
|
||||
* 자식 메뉴 + role_menus 피벗 정리는 helper 가 cascade 처리.
|
||||
*
|
||||
* @return bool 비활성화 성공 여부
|
||||
*/
|
||||
public function deactivate(): bool
|
||||
{
|
||||
app(ExtensionMenuSyncHelper::class)->cleanupStaleMenus(
|
||||
ExtensionOwnerType::Plugin,
|
||||
$this->getIdentifier(),
|
||||
currentSlugs: [],
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 제거 — 메뉴 잔존 안전망 (정상 흐름은 deactivate 가 먼저 처리).
|
||||
*
|
||||
* @return bool 제거 성공 여부
|
||||
*/
|
||||
public function uninstall(): bool
|
||||
{
|
||||
$this->deactivate();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 권한 목록 반환 (계층 구조)
|
||||
*
|
||||
* PluginManager 가 1레벨(플러그인 노드) → 2레벨(카테고리) → 3레벨(개별 권한) 트리로 등록.
|
||||
* 모든 권한은 admin 역할에 매핑.
|
||||
*
|
||||
* 권한 분할 의도:
|
||||
* - view: 발송 이력·알림톡 템플릿 조회 (모니터링)
|
||||
* - manage: 환경설정·템플릿 등록/검수·이벤트 연동 (운영)
|
||||
*
|
||||
* @return array 권한 정의 배열 (categories 계층 구조)
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return [
|
||||
'name' => [
|
||||
'ko' => '비즈뿌리오 메시징',
|
||||
'en' => 'Bizppurio Messaging',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '비즈뿌리오 메시징 플러그인이 제공하는 권한',
|
||||
'en' => 'Permissions provided by the Bizppurio Messaging plugin',
|
||||
],
|
||||
'categories' => [
|
||||
[
|
||||
'identifier' => 'messaging',
|
||||
'name' => ['ko' => '메시지 발송', 'en' => 'Messaging'],
|
||||
'description' => [
|
||||
'ko' => '메시지 발송 도메인 권한 (조회·관리)',
|
||||
'en' => 'Messaging domain permissions (view, manage)',
|
||||
],
|
||||
'permissions' => [
|
||||
[
|
||||
'action' => 'view',
|
||||
'name' => ['ko' => '메시지 조회', 'en' => 'View Messaging'],
|
||||
'description' => [
|
||||
'ko' => '발송 이력·알림톡 템플릿 조회 (모니터링)',
|
||||
'en' => 'View dispatch history and alimtalk templates (monitoring)',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin'],
|
||||
],
|
||||
[
|
||||
'action' => 'manage',
|
||||
'name' => ['ko' => '메시지 관리', 'en' => 'Manage Messaging'],
|
||||
'description' => [
|
||||
'ko' => '환경설정·알림톡 템플릿 등록/검수·이벤트 연동 관리',
|
||||
'en' => 'Manage settings, alimtalk template registration/inspection, and event bindings',
|
||||
],
|
||||
'type' => 'admin',
|
||||
'roles' => ['admin'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 설정 스키마 반환
|
||||
*
|
||||
* 관리자 설정 페이지 UI 를 동적으로 생성하는 데 사용됩니다. 크리덴셜(비밀번호·API 키)은
|
||||
* sensitive 로 마킹하여 마스킹하며, frontend_schema(defaults.json)에서 expose:false 로
|
||||
* 프론트 노출을 차단합니다.
|
||||
*
|
||||
* 발송 시스템(account)과 카카오 관리 시스템(bizId)의 식별자는 동일한 '비즈뿌리오 아이디'
|
||||
* 이므로 bizppurio_id 단일 필드로 받는다.
|
||||
*
|
||||
* @return array 설정 스키마
|
||||
*/
|
||||
public function getSettingsSchema(): array
|
||||
{
|
||||
return [
|
||||
'environment' => [
|
||||
'type' => 'enum',
|
||||
'options' => ['dev', 'live'],
|
||||
'default' => 'dev',
|
||||
'label' => ['ko' => '연동 환경', 'en' => 'Environment'],
|
||||
'hint' => [
|
||||
'ko' => '검수(개발) 또는 운영 환경을 선택합니다. 발송 API 도메인이 환경에 따라 분기됩니다.',
|
||||
'en' => 'Select the inspection (dev) or production environment. The sending API domain differs by environment.',
|
||||
],
|
||||
'required' => false,
|
||||
],
|
||||
'bizppurio_id' => [
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'label' => ['ko' => '비즈뿌리오 아이디', 'en' => 'Bizppurio ID'],
|
||||
'hint' => [
|
||||
'ko' => '발송·카카오 관리에 공통으로 사용하는 비즈뿌리오 아이디입니다.',
|
||||
'en' => 'The Bizppurio account ID used for both sending and Kakao management.',
|
||||
],
|
||||
'required' => false,
|
||||
],
|
||||
'password' => [
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'label' => ['ko' => '비밀번호', 'en' => 'Password'],
|
||||
'hint' => [
|
||||
'ko' => '발송 토큰 발급에 사용하는 비즈뿌리오 비밀번호입니다.',
|
||||
'en' => 'The Bizppurio password used to issue the sending token.',
|
||||
],
|
||||
'sensitive' => true,
|
||||
'required' => false,
|
||||
],
|
||||
'api_key' => [
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'label' => ['ko' => 'API 키', 'en' => 'API Key'],
|
||||
'hint' => [
|
||||
'ko' => '카카오 관리(알림톡 템플릿·발신프로필)에 사용하는 API 키입니다. 비즈뿌리오 고객센터로 아이디와 함께 접수하면 확인 후 발급됩니다.',
|
||||
'en' => 'The API key used for Kakao management (alimtalk templates, sender profiles).',
|
||||
],
|
||||
'sensitive' => true,
|
||||
'required' => false,
|
||||
],
|
||||
'sender_number' => [
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'label' => ['ko' => '발신번호', 'en' => 'Sender Number'],
|
||||
'hint' => [
|
||||
'ko' => '문자·알림톡 발송에 사용하는 발신 전화번호입니다.',
|
||||
'en' => 'The sender phone number used for SMS and alimtalk delivery.',
|
||||
],
|
||||
'required' => false,
|
||||
],
|
||||
'sender_key' => [
|
||||
'type' => 'string',
|
||||
'default' => '',
|
||||
'label' => ['ko' => '알림톡 발신프로필 키', 'en' => 'Alimtalk Sender Profile Key'],
|
||||
'hint' => [
|
||||
'ko' => '알림톡 발송·템플릿 조회에 사용하는 발신프로필 키(40자)입니다.',
|
||||
'en' => 'The 40-character sender profile key used for alimtalk delivery and template lookup.',
|
||||
],
|
||||
'sensitive' => true,
|
||||
'required' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 플러그인 설정 기본값 반환 (하위 호환)
|
||||
*
|
||||
* 신규 설치 기본값은 config/settings/defaults.json 의 defaults 섹션이 1순위이며,
|
||||
* 본 메서드는 하위 호환 경로로 동일 값을 반환한다.
|
||||
*
|
||||
* @return array 기본 설정값
|
||||
*/
|
||||
public function getConfigValues(): array
|
||||
{
|
||||
return [
|
||||
'environment' => 'dev',
|
||||
'bizppurio_id' => '',
|
||||
'password' => '',
|
||||
'api_key' => '',
|
||||
'sender_number' => '',
|
||||
'sender_key' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 이 플러그인이 등록할 알림 정의/템플릿 선언을 반환합니다.
|
||||
*
|
||||
* Phase 1 범위: 비즈뿌리오 지갑 잔액부족 시 관리자에게 보내는 자체 알림 1건.
|
||||
* (webhook 결과코드 9070/7436 감지 시 Phase 4 에서 이 알림을 발화)
|
||||
*
|
||||
* ※ 3영역(코어/게시판/이커머스) 알림톡 채널 기본 body 시드는 알림톡 채널 등록(Phase 3)·
|
||||
* 탭 연동(Phase 6)과 강결합이므로 Phase 6 으로 이관한다. 그때 본 메서드에 정의를 추가한다.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getNotificationDefinitions(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'type' => 'bizppurio_balance_low',
|
||||
'hook_prefix' => 'sirsoft-message_bizppurio',
|
||||
'name' => [
|
||||
'ko' => '비즈뿌리오 잔액 부족',
|
||||
'en' => 'Bizppurio Balance Low',
|
||||
],
|
||||
'description' => [
|
||||
'ko' => '비즈뿌리오 지갑 잔액이 부족해 문자/알림톡 발송이 실패했을 때 관리자에게 발송',
|
||||
'en' => 'Sent to admin when a message fails due to insufficient Bizppurio wallet balance',
|
||||
],
|
||||
'channels' => ['mail', 'database'],
|
||||
'hooks' => ['sirsoft-message_bizppurio.balance.low'],
|
||||
'variables' => [
|
||||
['key' => 'name', 'description' => '수신자(관리자) 이름'],
|
||||
['key' => 'app_name', 'description' => '사이트 이름'],
|
||||
['key' => 'result_code', 'description' => '결과 코드(9070 문자 / 7436 알림톡)'],
|
||||
['key' => 'channel_label', 'description' => '발송 채널(문자/알림톡)'],
|
||||
['key' => 'settings_url', 'description' => '메시징 환경설정 URL'],
|
||||
['key' => 'site_url', 'description' => '사이트 URL'],
|
||||
],
|
||||
'templates' => [
|
||||
[
|
||||
'channel' => 'mail',
|
||||
'recipients' => [['type' => 'role', 'value' => 'admin']],
|
||||
'subject' => [
|
||||
'ko' => '[{app_name}] 비즈뿌리오 잔액이 부족합니다',
|
||||
'en' => '[{app_name}] Bizppurio balance is insufficient',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '{name}님, 비즈뿌리오 지갑 잔액이 부족하여 {channel_label} 발송이 실패했습니다 (코드: {result_code}). 충전 후 발송이 정상화됩니다.',
|
||||
'en' => 'Dear {name}, a {channel_label} message failed due to insufficient Bizppurio balance (code: {result_code}). Delivery resumes after recharging.',
|
||||
],
|
||||
],
|
||||
[
|
||||
'channel' => 'database',
|
||||
'recipients' => [['type' => 'role', 'value' => 'admin']],
|
||||
'subject' => [
|
||||
'ko' => '비즈뿌리오 잔액 부족',
|
||||
'en' => 'Bizppurio balance low',
|
||||
],
|
||||
'body' => [
|
||||
'ko' => '비즈뿌리오 잔액 부족으로 {channel_label} 발송이 실패했습니다 (코드: {result_code}).',
|
||||
'en' => 'A {channel_label} message failed due to insufficient Bizppurio balance (code: {result_code}).',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 훅 리스너 목록 반환
|
||||
*
|
||||
* Phase 3~6 에서 채널 등록/발송 위임/webhook 리스너를 추가한다.
|
||||
*
|
||||
* @return array<class-string>
|
||||
*/
|
||||
public function getHookListeners(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 비즈뿌리오 메시징 플러그인 레이아웃 테스트 공통 헬퍼
|
||||
*
|
||||
* JSON 트리에서 ID/이름 검색 및 핸들러·i18n 키 추출을 위한 유틸리티.
|
||||
*/
|
||||
|
||||
export type AnyNode = Record<string, unknown> & {
|
||||
id?: string;
|
||||
name?: string;
|
||||
type?: string;
|
||||
children?: AnyNode[];
|
||||
slots?: Record<string, AnyNode[]>;
|
||||
modals?: Record<string, AnyNode> | AnyNode[];
|
||||
actions?: AnyNode[];
|
||||
iteration?: { source?: string; item_var?: string; index_var?: string };
|
||||
if?: string;
|
||||
props?: Record<string, unknown>;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 트리 노드에서 특정 id를 재귀 탐색 (children/slots/modals)
|
||||
*/
|
||||
export function findById(node: AnyNode | undefined | null, id: string): AnyNode | null {
|
||||
if (!node) return null;
|
||||
if (node.id === id) return node;
|
||||
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
const found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.slots && typeof node.slots === 'object') {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
const found = findById(child, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (node.modals) {
|
||||
const modalEntries = Array.isArray(node.modals) ? node.modals : Object.values(node.modals);
|
||||
for (const m of modalEntries) {
|
||||
const found = findById(m as AnyNode, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 트리 노드에서 특정 name 컴포넌트를 모두 수집 (children/slots/modals)
|
||||
*/
|
||||
export function findAllByName(node: AnyNode | undefined | null, name: string): AnyNode[] {
|
||||
const results: AnyNode[] = [];
|
||||
if (!node) return results;
|
||||
if (node.name === name) results.push(node);
|
||||
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
results.push(...findAllByName(child, name));
|
||||
}
|
||||
}
|
||||
if (node.slots && typeof node.slots === 'object') {
|
||||
for (const slotChildren of Object.values(node.slots)) {
|
||||
if (Array.isArray(slotChildren)) {
|
||||
for (const child of slotChildren) {
|
||||
results.push(...findAllByName(child, name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (node.modals) {
|
||||
const modalEntries = Array.isArray(node.modals) ? node.modals : Object.values(node.modals);
|
||||
for (const m of modalEntries) {
|
||||
results.push(...findAllByName(m as AnyNode, name));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 name 컴포넌트 중 props.name 속성이 주어진 값인 첫 노드를 찾음 (폼 입력 필드 탐색용)
|
||||
*/
|
||||
export function findInputByName(node: AnyNode | undefined | null, inputName: string): AnyNode | null {
|
||||
for (const candidate of [...findAllByName(node, 'Input'), ...findAllByName(node, 'Select')]) {
|
||||
if ((candidate.props as { name?: string } | undefined)?.name === inputName) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열에서 사용된 핸들러 이름 모두 수집
|
||||
*/
|
||||
export function collectHandlers(json: unknown): string[] {
|
||||
const text = JSON.stringify(json);
|
||||
const matches = text.match(/"handler":\s*"([^"]+)"/g) ?? [];
|
||||
const names = matches
|
||||
.map((m) => m.match(/"handler":\s*"([^"]+)"/)?.[1] ?? '')
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set(names));
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열에서 사용된 i18n 키($t:sirsoft-message_bizppurio.*) 모두 수집
|
||||
*/
|
||||
export function collectI18nKeys(json: unknown): string[] {
|
||||
const text = JSON.stringify(json);
|
||||
const matches = text.match(/\$t:sirsoft-message_bizppurio\.[a-zA-Z0-9_.]+/g) ?? [];
|
||||
return Array.from(new Set(matches));
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 비즈뿌리오 메시징 플러그인 환경설정 레이아웃 구조 검증 (§6-1)
|
||||
*
|
||||
* 3 섹션:
|
||||
* 1. section_api (연동 환경 + 비즈뿌리오 아이디 + 비밀번호 + API 키)
|
||||
* 2. section_sending (발신번호 + 알림톡 발신프로필 키)
|
||||
* 3. section_integration (webhook 수신 주소 안내)
|
||||
*
|
||||
* 크리덴셜(password/api_key/sender_key)은 type=password 로 마스킹, 저장은
|
||||
* 자동바인딩(_local.form) → 코어 /api/admin/plugins/{id}/settings PUT.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import layout from '../../../layouts/admin/plugin_settings.json';
|
||||
import ko from '../../../lang/ko.json';
|
||||
import en from '../../../lang/en.json';
|
||||
import {
|
||||
findById,
|
||||
findInputByName,
|
||||
collectHandlers,
|
||||
collectI18nKeys,
|
||||
type AnyNode,
|
||||
} from './helpers';
|
||||
|
||||
const root = layout as unknown as AnyNode;
|
||||
|
||||
describe('plugin_settings.json — 레이아웃 메타/권한', () => {
|
||||
it('layout_name 이 plugin_settings 이다', () => {
|
||||
expect((root as { layout_name?: string }).layout_name).toBe('plugin_settings');
|
||||
});
|
||||
|
||||
it('_admin_base 를 상속한다', () => {
|
||||
expect((root as { extends?: string }).extends).toBe('_admin_base');
|
||||
});
|
||||
|
||||
it('core.plugins.update 권한을 요구한다', () => {
|
||||
expect((root as { permissions?: string[] }).permissions).toContain('core.plugins.update');
|
||||
});
|
||||
|
||||
it('settings 데이터소스가 코어 플러그인 설정 API 를 조회한다', () => {
|
||||
const sources = (root as { data_sources?: AnyNode[] }).data_sources ?? [];
|
||||
const settings = sources.find((s) => s.id === 'settings');
|
||||
expect(settings).toBeTruthy();
|
||||
expect(settings?.endpoint).toBe('/api/admin/plugins/{{route.identifier}}/settings');
|
||||
expect(settings?.initLocal).toBe('form');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin_settings.json — 자동바인딩', () => {
|
||||
it('컨테이너가 dataKey=form + trackChanges 로 자동바인딩한다', () => {
|
||||
const container = findById(root, 'plugin_settings_content');
|
||||
expect(container).toBeTruthy();
|
||||
expect((container as { dataKey?: string }).dataKey).toBe('form');
|
||||
expect((container as { trackChanges?: boolean }).trackChanges).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin_settings.json — 3 섹션', () => {
|
||||
it.each([
|
||||
['section_api', 'API 연동'],
|
||||
['section_sending', '발송 설정'],
|
||||
['section_integration', '연동 안내'],
|
||||
])('%s 섹션이 존재한다', (id) => {
|
||||
expect(findById(root, id)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin_settings.json — 입력 필드 6종', () => {
|
||||
it.each([
|
||||
'environment',
|
||||
'bizppurio_id',
|
||||
'password',
|
||||
'api_key',
|
||||
'sender_number',
|
||||
'sender_key',
|
||||
])('%s 입력 필드가 자동바인딩 name 으로 존재한다', (name) => {
|
||||
expect(findInputByName(root, name)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('크리덴셜(password/api_key/sender_key)은 type=password 로 마스킹된다', () => {
|
||||
for (const cred of ['password', 'api_key', 'sender_key']) {
|
||||
const input = findInputByName(root, cred);
|
||||
expect((input?.props as { type?: string } | undefined)?.type).toBe('password');
|
||||
}
|
||||
});
|
||||
|
||||
it('bizppurio_id/sender_number 는 일반 text 입력이다', () => {
|
||||
for (const field of ['bizppurio_id', 'sender_number']) {
|
||||
const input = findInputByName(root, field);
|
||||
expect((input?.props as { type?: string } | undefined)?.type).toBe('text');
|
||||
}
|
||||
});
|
||||
|
||||
it('environment 는 dev/live 옵션 Select 이다', () => {
|
||||
const select = findInputByName(root, 'environment');
|
||||
const options = (select?.children ?? []).map(
|
||||
(c) => (c.props as { value?: string } | undefined)?.value
|
||||
);
|
||||
expect(options).toEqual(['dev', 'live']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin_settings.json — webhook 안내', () => {
|
||||
it('연동 안내 섹션에 고정 webhook 경로가 readonly 로 표시된다', () => {
|
||||
const section = findById(root, 'section_integration');
|
||||
const inputs = JSON.stringify(section);
|
||||
expect(inputs).toContain('/api/plugins/sirsoft-message_bizppurio/webhook');
|
||||
expect(inputs).toContain('"readOnly":true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin_settings.json — 저장 버튼', () => {
|
||||
it('저장 버튼이 hasChanges 없으면 비활성화된다', () => {
|
||||
const save = findById(root, 'save_button');
|
||||
expect((save?.props as { disabled?: string } | undefined)?.disabled).toContain('!_local.hasChanges');
|
||||
});
|
||||
|
||||
it('저장은 코어 설정 API 로 PUT 한다', () => {
|
||||
const text = JSON.stringify(findById(root, 'save_button'));
|
||||
expect(text).toContain('/api/admin/plugins/{{route.identifier}}/settings');
|
||||
expect(text).toContain('"method":"PUT"');
|
||||
expect(text).toContain('{{_local.form}}');
|
||||
});
|
||||
|
||||
it('등록된 핸들러만 사용한다 (오탈자 핸들러 없음)', () => {
|
||||
const handlers = collectHandlers(layout);
|
||||
const allowed = ['apiCall', 'setState', 'toast', 'navigate', 'sequence', 'refetchDataSource', 'scrollIntoView'];
|
||||
for (const h of handlers) {
|
||||
expect(allowed).toContain(h);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugin_settings.json — i18n 키 정합', () => {
|
||||
it('레이아웃이 참조하는 $t: 키가 ko/en 다국어 파일에 모두 존재한다', () => {
|
||||
const keys = collectI18nKeys(layout);
|
||||
expect(keys.length).toBeGreaterThan(0);
|
||||
|
||||
const resolve = (dict: Record<string, unknown>, path: string): unknown =>
|
||||
path.split('.').reduce<unknown>((acc, seg) => {
|
||||
if (acc && typeof acc === 'object') {
|
||||
return (acc as Record<string, unknown>)[seg];
|
||||
}
|
||||
return undefined;
|
||||
}, dict);
|
||||
|
||||
for (const raw of keys) {
|
||||
// "$t:sirsoft-message_bizppurio.settings.title" → "settings.title"
|
||||
const path = raw.replace('$t:sirsoft-message_bizppurio.', '');
|
||||
expect(resolve(ko, path), `ko 누락: ${path}`).toBeTruthy();
|
||||
expect(resolve(en, path), `en 누락: ${path}`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "Bizppurio Messaging",
|
||||
"description": "Bizppurio (Daou Tech) SMS/LMS and KakaoTalk alimtalk messaging plugin.",
|
||||
"settings": {
|
||||
"title": "Bizppurio Messaging Settings",
|
||||
"description": "Manage Bizppurio integration credentials and sending options.",
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"save_success": "Settings saved.",
|
||||
"save_failed": "Failed to save settings.",
|
||||
"sections": {
|
||||
"api": {
|
||||
"title": "API Integration",
|
||||
"description": "Credentials shared by the sending system and the Kakao management system."
|
||||
},
|
||||
"sending": {
|
||||
"title": "Sending Options",
|
||||
"description": "Sender information used for SMS and alimtalk delivery."
|
||||
},
|
||||
"integration": {
|
||||
"title": "Integration Guide",
|
||||
"description": "The webhook address that receives delivery results."
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"environment": {
|
||||
"label": "Environment",
|
||||
"hint": "Select the inspection (dev) or production environment. The sending API domain differs by environment.",
|
||||
"option_dev": "Inspection (Dev)",
|
||||
"option_live": "Production"
|
||||
},
|
||||
"bizppurio_id": {
|
||||
"label": "Bizppurio ID",
|
||||
"hint": "The Bizppurio account ID used for both sending and Kakao management."
|
||||
},
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"hint": "The Bizppurio password used to issue the sending token."
|
||||
},
|
||||
"api_key": {
|
||||
"label": "API Key",
|
||||
"hint": "The API key is issued after you submit it together with your ID to Bizppurio customer support."
|
||||
},
|
||||
"sender_number": {
|
||||
"label": "Sender Number",
|
||||
"hint": "The sender phone number used for SMS and alimtalk delivery."
|
||||
},
|
||||
"sender_key": {
|
||||
"label": "Alimtalk Sender Profile Key",
|
||||
"hint": "The 40-character sender profile key used for alimtalk delivery and template lookup."
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"label": "Delivery Result Address (webhook)",
|
||||
"hint": "Register this address with the Bizppurio business team as the URL PUSH endpoint so delivery results are recorded automatically.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "비즈뿌리오 메시징",
|
||||
"description": "비즈뿌리오(다우기술) 연동 SMS/LMS·카카오 알림톡 자동발송 플러그인입니다.",
|
||||
"settings": {
|
||||
"title": "비즈뿌리오 메시징 설정",
|
||||
"description": "비즈뿌리오 연동 정보와 발송 설정을 관리합니다.",
|
||||
"save": "저장",
|
||||
"saving": "저장 중...",
|
||||
"save_success": "설정이 저장되었습니다.",
|
||||
"save_failed": "설정 저장에 실패했습니다.",
|
||||
"sections": {
|
||||
"api": {
|
||||
"title": "API 연동",
|
||||
"description": "발송 시스템과 카카오 관리 시스템에 공통으로 사용하는 연동 정보입니다."
|
||||
},
|
||||
"sending": {
|
||||
"title": "발송 설정",
|
||||
"description": "문자·알림톡 발송에 사용하는 발신 정보입니다."
|
||||
},
|
||||
"integration": {
|
||||
"title": "연동 안내",
|
||||
"description": "발송 결과를 수신하는 webhook 주소입니다."
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"environment": {
|
||||
"label": "연동 환경",
|
||||
"hint": "검수(개발) 또는 운영 환경을 선택합니다. 발송 API 도메인이 환경에 따라 분기됩니다.",
|
||||
"option_dev": "검수(개발)",
|
||||
"option_live": "운영"
|
||||
},
|
||||
"bizppurio_id": {
|
||||
"label": "비즈뿌리오 아이디",
|
||||
"hint": "발송·카카오 관리에 공통으로 사용하는 비즈뿌리오 아이디입니다."
|
||||
},
|
||||
"password": {
|
||||
"label": "비밀번호",
|
||||
"hint": "발송 토큰 발급에 사용하는 비즈뿌리오 비밀번호입니다."
|
||||
},
|
||||
"api_key": {
|
||||
"label": "API 키",
|
||||
"hint": "API 키는 비즈뿌리오 고객센터로 아이디와 함께 접수하면 확인 후 발급됩니다."
|
||||
},
|
||||
"sender_number": {
|
||||
"label": "발신번호",
|
||||
"hint": "문자·알림톡 발송에 사용하는 발신 전화번호입니다."
|
||||
},
|
||||
"sender_key": {
|
||||
"label": "알림톡 발신프로필 키",
|
||||
"hint": "알림톡 발송·템플릿 조회에 사용하는 발신프로필 키(40자)입니다."
|
||||
}
|
||||
},
|
||||
"webhook": {
|
||||
"label": "발송 결과 수신 주소 (webhook)",
|
||||
"hint": "비즈뿌리오 사업팀에 이 주소를 URL PUSH 수신 주소로 등록하면 발송 결과가 이력에 자동 기록됩니다.",
|
||||
"copy": "복사",
|
||||
"copied": "복사되었습니다."
|
||||
}
|
||||
}
|
||||
}
|
||||
+644
@@ -0,0 +1,644 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"layout_name": "plugin_settings",
|
||||
"permissions": [
|
||||
"core.plugins.update"
|
||||
],
|
||||
"extends": "_admin_base",
|
||||
"meta": {
|
||||
"title": "$t:sirsoft-message_bizppurio.settings.title",
|
||||
"description": "$t:sirsoft-message_bizppurio.settings.description"
|
||||
},
|
||||
"data_sources": [
|
||||
{
|
||||
"id": "settings",
|
||||
"type": "api",
|
||||
"endpoint": "/api/admin/plugins/{{route.identifier}}/settings",
|
||||
"method": "GET",
|
||||
"auto_fetch": true,
|
||||
"auth_required": true,
|
||||
"initLocal": "form",
|
||||
"refetchOnMount": true
|
||||
}
|
||||
],
|
||||
"slots": {
|
||||
"content": [
|
||||
{
|
||||
"id": "plugin_settings_content",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"dataKey": "form",
|
||||
"trackChanges": true,
|
||||
"props": {
|
||||
"className": "p-4 sm:p-6 lg:p-8 min-h-screen bg-gray-50 dark:bg-gray-900"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "page_header",
|
||||
"type": "composite",
|
||||
"name": "PageHeader",
|
||||
"props": {
|
||||
"title": "$t:sirsoft-message_bizppurio.settings.title",
|
||||
"description": "$t:sirsoft-message_bizppurio.settings.description",
|
||||
"backPath": "/admin/plugins",
|
||||
"backLabel": "$t:admin.plugins.back_to_list"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "validation_error",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"if": "{{_local.errors}}",
|
||||
"props": {
|
||||
"id": "bizppurio_settings_validation_error",
|
||||
"role": "alert",
|
||||
"aria-live": "assertive",
|
||||
"tabIndex": -1,
|
||||
"className": "alert-danger scroll-mt-32"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex gap-3"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"props": {
|
||||
"name": "triangle-exclamation",
|
||||
"size": "md",
|
||||
"className": "text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-error-soft"
|
||||
},
|
||||
"text": "$t:admin.plugins.settings.validation_error"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Ul",
|
||||
"if": "{{typeof _local.errors === 'object' && Object.keys(_local.errors).length > 0}}",
|
||||
"props": {
|
||||
"className": "mt-2 list-disc list-inside space-y-1"
|
||||
},
|
||||
"iteration": {
|
||||
"source": "Object.entries(_local.errors ?? {}).flatMap(([field, messages]) => Array.isArray(messages) ? messages : [messages])",
|
||||
"item_var": "message"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Li",
|
||||
"text": "{{message}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "section_api",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mt-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-6 py-4 border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-base font-semibold text-gray-900 dark:text-gray-100"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.sections.api.title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.sections.api.description"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-6 space-y-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_environment",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "form-label"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.environment.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Select",
|
||||
"props": {
|
||||
"name": "environment",
|
||||
"className": "input mt-1"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Option",
|
||||
"props": {
|
||||
"value": "dev"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.environment.option_dev"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Option",
|
||||
"props": {
|
||||
"value": "live"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.environment.option_live"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.environment.hint"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_bizppurio_id",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "form-label"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.bizppurio_id.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"name": "bizppurio_id",
|
||||
"autoComplete": "off",
|
||||
"className": "input mt-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.bizppurio_id.hint"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_password",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "form-label"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.password.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "password",
|
||||
"name": "password",
|
||||
"autoComplete": "new-password",
|
||||
"placeholder": "••••••••",
|
||||
"className": "input mt-1 font-mono"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.password.hint"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_api_key",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "form-label"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.api_key.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "password",
|
||||
"name": "api_key",
|
||||
"autoComplete": "new-password",
|
||||
"placeholder": "••••••••",
|
||||
"className": "input mt-1 font-mono"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.api_key.hint"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "section_sending",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mt-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-6 py-4 border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-base font-semibold text-gray-900 dark:text-gray-100"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.sections.sending.title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.sections.sending.description"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-6 space-y-6"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "field_sender_number",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "form-label"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.sender_number.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"name": "sender_number",
|
||||
"autoComplete": "off",
|
||||
"className": "input mt-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.sender_number.hint"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "field_sender_key",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "form-label"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.sender_key.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "password",
|
||||
"name": "sender_key",
|
||||
"autoComplete": "new-password",
|
||||
"placeholder": "••••••••",
|
||||
"className": "input mt-1 font-mono"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint mt-1"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.fields.sender_key.hint"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "section_integration",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "mt-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "px-6 py-4 border-b border-gray-200 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "H3",
|
||||
"props": {
|
||||
"className": "text-base font-semibold text-gray-900 dark:text-gray-100"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.sections.integration.title"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.sections.integration.description"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "p-6 space-y-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Label",
|
||||
"props": {
|
||||
"className": "form-label"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.webhook.label"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "flex gap-2"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Input",
|
||||
"props": {
|
||||
"type": "text",
|
||||
"readOnly": true,
|
||||
"value": "/api/plugins/sirsoft-message_bizppurio/webhook",
|
||||
"className": "input flex-1 font-mono text-sm bg-gray-50 dark:bg-gray-900"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "P",
|
||||
"props": {
|
||||
"className": "form-hint"
|
||||
},
|
||||
"text": "$t:sirsoft-message_bizppurio.settings.webhook.hint"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "footer_buttons",
|
||||
"type": "basic",
|
||||
"name": "Div",
|
||||
"props": {
|
||||
"className": "sticky bottom-0 z-10 flex justify-end items-center gap-3 mt-6 px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"id": "cancel_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"className": "btn btn-secondary"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "navigate",
|
||||
"params": {
|
||||
"path": "/admin/plugins"
|
||||
}
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"text": "$t:common.cancel"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "save_button",
|
||||
"type": "basic",
|
||||
"name": "Button",
|
||||
"props": {
|
||||
"type": "button",
|
||||
"disabled": "{{!_local.hasChanges || _local.isSaving}}",
|
||||
"className": "btn btn-primary flex-center gap-1.5"
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"type": "click",
|
||||
"handler": "sequence",
|
||||
"actions": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": true,
|
||||
"errors": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "apiCall",
|
||||
"auth_required": true,
|
||||
"target": "/api/admin/plugins/{{route.identifier}}/settings",
|
||||
"params": {
|
||||
"method": "PUT",
|
||||
"body": "{{_local.form}}"
|
||||
},
|
||||
"onSuccess": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"hasChanges": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "success",
|
||||
"message": "$t:admin.plugins.settings.save_success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "refetchDataSource",
|
||||
"params": {
|
||||
"dataSourceId": "settings"
|
||||
}
|
||||
}
|
||||
],
|
||||
"onError": [
|
||||
{
|
||||
"handler": "setState",
|
||||
"params": {
|
||||
"target": "local",
|
||||
"isSaving": false,
|
||||
"errors": "{{error.errors ?? { _general: error.message }}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "scrollIntoView",
|
||||
"params": {
|
||||
"selector": "#bizppurio_settings_validation_error",
|
||||
"behavior": "smooth",
|
||||
"block": "start",
|
||||
"waitForElement": true,
|
||||
"timeout": 2000
|
||||
}
|
||||
},
|
||||
{
|
||||
"handler": "toast",
|
||||
"params": {
|
||||
"type": "error",
|
||||
"message": "$t:admin.plugins.settings.save_error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Icon",
|
||||
"if": "{{_local.isSaving}}",
|
||||
"props": {
|
||||
"name": "spinner",
|
||||
"className": "spinner-sm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{!_local.isSaving}}",
|
||||
"text": "$t:common.save"
|
||||
},
|
||||
{
|
||||
"type": "basic",
|
||||
"name": "Span",
|
||||
"if": "{{_local.isSaving}}",
|
||||
"text": "$t:common.saving"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Enums;
|
||||
|
||||
/**
|
||||
* 발송 채널 — bizppurio_dispatches.channel 컬럼의 값 집합.
|
||||
*
|
||||
* SMS/LMS 는 문자, 알림톡은 카카오 채널. 실제 발송 유형(SMS/LMS 자동 분기)은
|
||||
* SmsTypeResolver(Phase 2)가 byte 판별로 확정한다.
|
||||
*/
|
||||
enum DispatchChannel: string
|
||||
{
|
||||
case Sms = 'sms';
|
||||
case Lms = 'lms';
|
||||
case Alimtalk = 'alimtalk';
|
||||
|
||||
/**
|
||||
* 현재 locale 에 맞는 채널 라벨을 반환합니다.
|
||||
*
|
||||
* @return string 다국어 채널 라벨 (예: "SMS", "LMS", "알림톡")
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Sms => __('sirsoft-message_bizppurio::messages.channel.sms'),
|
||||
self::Lms => __('sirsoft-message_bizppurio::messages.channel.lms'),
|
||||
self::Alimtalk => __('sirsoft-message_bizppurio::messages.channel.alimtalk'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 문자(SMS/LMS) 채널 여부를 반환합니다.
|
||||
*
|
||||
* @return bool 문자 채널이면 true, 알림톡이면 false
|
||||
*/
|
||||
public function isText(): bool
|
||||
{
|
||||
return $this === self::Sms || $this === self::Lms;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 채널 값 목록을 반환합니다.
|
||||
*
|
||||
* @return array<int, string> 채널 문자열 배열 (예: ["sms", "lms", "alimtalk"])
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Enums;
|
||||
|
||||
/**
|
||||
* 발송 출처 — bizppurio_dispatches.source 컬럼의 값 집합.
|
||||
*
|
||||
* 1차 범위는 auto(코어 알림 이벤트 자동발송)만 사용한다.
|
||||
* manual(수동발송)·bulk(대량발송)은 후속(D15).
|
||||
*/
|
||||
enum DispatchSource: string
|
||||
{
|
||||
case Auto = 'auto';
|
||||
case Manual = 'manual';
|
||||
case Bulk = 'bulk';
|
||||
|
||||
/**
|
||||
* 현재 locale 에 맞는 출처 라벨을 반환합니다.
|
||||
*
|
||||
* @return string 다국어 출처 라벨 (예: "자동", "수동", "대량")
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Auto => __('sirsoft-message_bizppurio::messages.source.auto'),
|
||||
self::Manual => __('sirsoft-message_bizppurio::messages.source.manual'),
|
||||
self::Bulk => __('sirsoft-message_bizppurio::messages.source.bulk'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 출처 값 목록을 반환합니다.
|
||||
*
|
||||
* @return array<int, string> 출처 문자열 배열
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Enums;
|
||||
|
||||
/**
|
||||
* 발송 상태 — bizppurio_dispatches.status 컬럼의 값 집합.
|
||||
*
|
||||
* pending(대기) → sent(발송요청 성공, 리포트 대기) → success/failed(webhook 리포트 확정).
|
||||
* webhook 결과 수신(Phase 4) 전까지는 sent 상태로 머문다.
|
||||
*/
|
||||
enum DispatchStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Sent = 'sent';
|
||||
case Success = 'success';
|
||||
case Failed = 'failed';
|
||||
|
||||
/**
|
||||
* 현재 locale 에 맞는 상태 라벨을 반환합니다.
|
||||
*
|
||||
* @return string 다국어 상태 라벨 (예: "대기", "발송중", "성공", "실패")
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Pending => __('sirsoft-message_bizppurio::messages.status.pending'),
|
||||
self::Sent => __('sirsoft-message_bizppurio::messages.status.sent'),
|
||||
self::Success => __('sirsoft-message_bizppurio::messages.status.success'),
|
||||
self::Failed => __('sirsoft-message_bizppurio::messages.status.failed'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 리포트 수신으로 상태가 확정되었는지(성공/실패) 여부를 반환합니다.
|
||||
*
|
||||
* @return bool 성공 또는 실패이면 true, 대기/발송중이면 false
|
||||
*/
|
||||
public function isFinal(): bool
|
||||
{
|
||||
return $this === self::Success || $this === self::Failed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 상태 값 목록을 반환합니다.
|
||||
*
|
||||
* @return array<int, string> 상태 문자열 배열
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Enums;
|
||||
|
||||
/**
|
||||
* 결과코드 분류 — 비즈뿌리오 결과코드를 처리 방침으로 분류한 값 집합.
|
||||
*
|
||||
* ResultCodeResolver(Phase 4)가 코드 → 이 분류로 매핑하고, 발송 재시도 정책
|
||||
* (SendMessageJob, Phase 2)과 잔액부족 자체알림(Phase 4)의 판단 기준이 된다.
|
||||
*
|
||||
* - success: 발송/리포트 성공
|
||||
* - retry: 일시 오류 → 재시도 대상
|
||||
* - permanent_failure: 영구 실패 → 즉시 실패 처리
|
||||
* - balance_low: 지갑 잔액 부족(9070 문자 / 7436 알림톡) → 실패 + 관리자 자체알림
|
||||
*/
|
||||
enum ResultCategory: string
|
||||
{
|
||||
case Success = 'success';
|
||||
case Retry = 'retry';
|
||||
case PermanentFailure = 'permanent_failure';
|
||||
case BalanceLow = 'balance_low';
|
||||
|
||||
/**
|
||||
* 현재 locale 에 맞는 분류 라벨을 반환합니다.
|
||||
*
|
||||
* @return string 다국어 분류 라벨
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Success => __('sirsoft-message_bizppurio::messages.result_category.success'),
|
||||
self::Retry => __('sirsoft-message_bizppurio::messages.result_category.retry'),
|
||||
self::PermanentFailure => __('sirsoft-message_bizppurio::messages.result_category.permanent_failure'),
|
||||
self::BalanceLow => __('sirsoft-message_bizppurio::messages.result_category.balance_low'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 재시도 대상 분류 여부를 반환합니다.
|
||||
*
|
||||
* @return bool 일시 오류(retry)이면 true
|
||||
*/
|
||||
public function isRetryable(): bool
|
||||
{
|
||||
return $this === self::Retry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 발송 실패로 확정되는 분류 여부를 반환합니다.
|
||||
*
|
||||
* @return bool 영구 실패 또는 잔액 부족이면 true
|
||||
*/
|
||||
public function isFailure(): bool
|
||||
{
|
||||
return $this === self::PermanentFailure || $this === self::BalanceLow;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 분류 값 목록을 반환합니다.
|
||||
*
|
||||
* @return array<int, string> 분류 문자열 배열
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Providers;
|
||||
|
||||
use App\Extension\BasePluginServiceProvider;
|
||||
|
||||
/**
|
||||
* 비즈뿌리오 메시징 플러그인 서비스 프로바이더.
|
||||
*
|
||||
* BasePluginServiceProvider 가 Repository / Storage / Cache 자동 바인딩과
|
||||
* 다국어 로드를 처리한다. 아래 배열에 등록만 하면 contextual binding 이 적용된다.
|
||||
*/
|
||||
class MessageBizppurioServiceProvider extends BasePluginServiceProvider
|
||||
{
|
||||
/** 플러그인 식별자 (manifest 와 일치) */
|
||||
protected string $pluginIdentifier = 'sirsoft-message_bizppurio';
|
||||
|
||||
/**
|
||||
* Repository 인터페이스 ↔ 구현체 매핑.
|
||||
*
|
||||
* Phase 4 에서 발송 이력·이벤트 연동 Repository 를 등록한다.
|
||||
*
|
||||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected array $repositories = [
|
||||
// Phase 4: BizppurioDispatchRepositoryInterface / BizppurioNotificationBindingRepositoryInterface
|
||||
];
|
||||
|
||||
/**
|
||||
* CacheInterface 가 필요한 서비스 (contextual binding).
|
||||
*
|
||||
* Phase 2 에서 발송 토큰 캐시 서비스(BizppurioTokenService)를 등록한다.
|
||||
*
|
||||
* @var array<int, class-string>
|
||||
*/
|
||||
protected array $cacheServices = [
|
||||
// Phase 2: BizppurioTokenService::class
|
||||
];
|
||||
|
||||
/**
|
||||
* 플러그인 루트 lang 디렉토리를 로드합니다.
|
||||
*
|
||||
* 백엔드 다국어는 lang/{ko,en}/*.php 에 두고 `$this->translationNamespace()`
|
||||
* (= 플러그인 식별자) 네임스페이스로 로드한다.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function loadExtensionTranslations(): void
|
||||
{
|
||||
$langPath = dirname($this->getProviderPath(), 2).'/lang';
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, $this->translationNamespace());
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Tests\Feature\Installation;
|
||||
|
||||
use App\Enums\ExtensionOwnerType;
|
||||
use App\Models\Menu;
|
||||
use Plugins\Sirsoft\MessageBizppurio\Plugin;
|
||||
use Plugins\Sirsoft\MessageBizppurio\Tests\PluginTestCase;
|
||||
|
||||
/**
|
||||
* 비즈뿌리오 메시징 플러그인 설치 스모크 테스트.
|
||||
*
|
||||
* plugin.php 가 선언하는 권한/메뉴/알림 정의/설정 스키마 구조와
|
||||
* activate/deactivate 의 메뉴 동기화·정리 동작을 검증한다.
|
||||
*/
|
||||
class InstallationTest extends PluginTestCase
|
||||
{
|
||||
private Plugin $plugin;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->plugin = new Plugin;
|
||||
}
|
||||
|
||||
public function test_식별자가_디렉토리명과_일치한다(): void
|
||||
{
|
||||
$this->assertSame('sirsoft-message_bizppurio', $this->plugin->getIdentifier());
|
||||
$this->assertSame('sirsoft', $this->plugin->getVendor());
|
||||
}
|
||||
|
||||
public function test_권한이_view_manage_2개로_선언된다(): void
|
||||
{
|
||||
$permissions = $this->plugin->getPermissions();
|
||||
|
||||
$this->assertArrayHasKey('categories', $permissions);
|
||||
$this->assertCount(1, $permissions['categories']);
|
||||
|
||||
$category = $permissions['categories'][0];
|
||||
$this->assertSame('messaging', $category['identifier']);
|
||||
|
||||
$actions = array_column($category['permissions'], 'action');
|
||||
$this->assertEqualsCanonicalizing(['view', 'manage'], $actions);
|
||||
|
||||
foreach ($category['permissions'] as $permission) {
|
||||
$this->assertContains('admin', $permission['roles']);
|
||||
$this->assertSame('admin', $permission['type']);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_설정_스키마의_크리덴셜은_sensitive다(): void
|
||||
{
|
||||
$schema = $this->plugin->getSettingsSchema();
|
||||
|
||||
foreach (['password', 'api_key', 'sender_key'] as $credential) {
|
||||
$this->assertArrayHasKey($credential, $schema);
|
||||
$this->assertTrue(
|
||||
$schema[$credential]['sensitive'] ?? false,
|
||||
"{$credential} 는 sensitive 로 마킹되어야 한다."
|
||||
);
|
||||
}
|
||||
|
||||
// 비크리덴셜 필드는 존재하되 sensitive 아님
|
||||
$this->assertArrayHasKey('bizppurio_id', $schema);
|
||||
$this->assertArrayHasKey('sender_number', $schema);
|
||||
$this->assertArrayHasKey('environment', $schema);
|
||||
$this->assertSame('dev', $schema['environment']['default']);
|
||||
}
|
||||
|
||||
public function test_defaults_json의_크리덴셜은_expose_false다(): void
|
||||
{
|
||||
$path = dirname(__DIR__, 3).'/config/settings/defaults.json';
|
||||
$this->assertFileExists($path);
|
||||
|
||||
$defaults = json_decode(file_get_contents($path), true);
|
||||
$schema = $defaults['frontend_schema'];
|
||||
|
||||
foreach ($schema as $field => $rule) {
|
||||
$this->assertFalse(
|
||||
$rule['expose'] ?? true,
|
||||
"{$field} 는 프론트에 노출(expose:true)되면 안 된다."
|
||||
);
|
||||
}
|
||||
|
||||
foreach (['password', 'api_key', 'sender_key'] as $credential) {
|
||||
$this->assertTrue($schema[$credential]['sensitive'] ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_잔액부족_알림이_관리자_대상으로_정의된다(): void
|
||||
{
|
||||
$definitions = $this->plugin->getNotificationDefinitions();
|
||||
|
||||
$this->assertCount(1, $definitions);
|
||||
$definition = $definitions[0];
|
||||
|
||||
$this->assertSame('bizppurio_balance_low', $definition['type']);
|
||||
$this->assertSame('sirsoft-message_bizppurio', $definition['hook_prefix']);
|
||||
$this->assertEqualsCanonicalizing(['mail', 'database'], $definition['channels']);
|
||||
|
||||
foreach ($definition['templates'] as $template) {
|
||||
$recipients = $template['recipients'];
|
||||
$this->assertSame('role', $recipients[0]['type']);
|
||||
$this->assertSame('admin', $recipients[0]['value']);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_activate_시_관리자_메뉴가_계층으로_생성된다(): void
|
||||
{
|
||||
$this->plugin->activate();
|
||||
|
||||
$parent = Menu::where('slug', 'sirsoft-message_bizppurio')
|
||||
->where('extension_type', ExtensionOwnerType::Plugin->value)
|
||||
->first();
|
||||
|
||||
$this->assertNotNull($parent, '최상위 메뉴가 생성되어야 한다.');
|
||||
|
||||
$children = Menu::where('parent_id', $parent->id)->get();
|
||||
$this->assertCount(2, $children, '알림톡 템플릿 관리·발송 이력 2개 하위 메뉴가 생성되어야 한다.');
|
||||
|
||||
$childSlugs = $children->pluck('slug')->all();
|
||||
$this->assertContains('sirsoft-message_bizppurio-alimtalk-templates', $childSlugs);
|
||||
$this->assertContains('sirsoft-message_bizppurio-dispatches', $childSlugs);
|
||||
}
|
||||
|
||||
public function test_deactivate_시_메뉴가_제거된다(): void
|
||||
{
|
||||
$this->plugin->activate();
|
||||
$this->assertGreaterThan(0, Menu::where('extension_identifier', 'sirsoft-message_bizppurio')->count());
|
||||
|
||||
$this->plugin->deactivate();
|
||||
|
||||
$this->assertSame(
|
||||
0,
|
||||
Menu::where('extension_identifier', 'sirsoft-message_bizppurio')->count(),
|
||||
'비활성화 시 플러그인 소속 메뉴가 전부 제거되어야 한다.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* 비즈뿌리오 메시징 플러그인 테스트 베이스 클래스
|
||||
*
|
||||
* 모든 플러그인 테스트는 이 클래스를 상속받아야 합니다.
|
||||
* 코어 + 번들 확장 마이그레이션을 자동으로 처리합니다.
|
||||
*/
|
||||
abstract class PluginTestCase extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* HookManager static state 스냅샷 — tearDown 에서 복원하여 테스트 간 훅 격리 보장.
|
||||
*
|
||||
* @var array{hooks: array, filters: array, dispatching: array}|null
|
||||
*/
|
||||
private ?array $hookSnapshot = null;
|
||||
|
||||
/**
|
||||
* 테스트 환경 설정
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->snapshotHookManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* tearDown 에 HookManager 상태 복원.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->restoreHookManager();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* HookManager static $hooks / $filters / $dispatching 를 스냅샷.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function snapshotHookManager(): void
|
||||
{
|
||||
$ref = new \ReflectionClass(\App\Extension\HookManager::class);
|
||||
$this->hookSnapshot = [
|
||||
'hooks' => $ref->getProperty('hooks')->getValue(),
|
||||
'filters' => $ref->getProperty('filters')->getValue(),
|
||||
'dispatching' => $ref->getProperty('dispatching')->getValue(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 스냅샷 시점으로 HookManager 복원.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function restoreHookManager(): void
|
||||
{
|
||||
if ($this->hookSnapshot === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ref = new \ReflectionClass(\App\Extension\HookManager::class);
|
||||
$ref->getProperty('hooks')->setValue(null, $this->hookSnapshot['hooks']);
|
||||
$ref->getProperty('filters')->setValue(null, $this->hookSnapshot['filters']);
|
||||
$ref->getProperty('dispatching')->setValue(null, $this->hookSnapshot['dispatching']);
|
||||
|
||||
$this->hookSnapshot = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 마이그레이션 경로를 반환합니다.
|
||||
*
|
||||
* RefreshDatabase 의 migrate:fresh 명령에 코어 + 번들 확장 마이그레이션 경로를 전달합니다.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function migrateFreshUsing(): array
|
||||
{
|
||||
$paths = ['database/migrations'];
|
||||
foreach (glob(base_path('modules/_bundled/*/database/migrations'), GLOB_ONLYDIR) as $p) {
|
||||
$paths[] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $p);
|
||||
}
|
||||
foreach (glob(base_path('plugins/_bundled/*/database/migrations'), GLOB_ONLYDIR) as $p) {
|
||||
$paths[] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $p);
|
||||
}
|
||||
|
||||
return [
|
||||
'--drop-views' => $this->shouldDropViews(),
|
||||
'--drop-types' => $this->shouldDropTypes(),
|
||||
'--seed' => false,
|
||||
'--path' => $paths,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Tests\Unit\Enums;
|
||||
|
||||
use Plugins\Sirsoft\MessageBizppurio\Enums\DispatchChannel;
|
||||
use Plugins\Sirsoft\MessageBizppurio\Enums\DispatchSource;
|
||||
use Plugins\Sirsoft\MessageBizppurio\Enums\DispatchStatus;
|
||||
use Plugins\Sirsoft\MessageBizppurio\Enums\ResultCategory;
|
||||
use Plugins\Sirsoft\MessageBizppurio\Tests\PluginTestCase;
|
||||
|
||||
/**
|
||||
* 발송 관련 Enum 4종 검증.
|
||||
*/
|
||||
class EnumTest extends PluginTestCase
|
||||
{
|
||||
public function test_dispatch_channel_값과_helper(): void
|
||||
{
|
||||
$this->assertEqualsCanonicalizing(['sms', 'lms', 'alimtalk'], DispatchChannel::values());
|
||||
$this->assertTrue(DispatchChannel::Sms->isText());
|
||||
$this->assertTrue(DispatchChannel::Lms->isText());
|
||||
$this->assertFalse(DispatchChannel::Alimtalk->isText());
|
||||
$this->assertNull(DispatchChannel::tryFrom('invalid'));
|
||||
}
|
||||
|
||||
public function test_dispatch_status_값과_final_판정(): void
|
||||
{
|
||||
$this->assertEqualsCanonicalizing(['pending', 'sent', 'success', 'failed'], DispatchStatus::values());
|
||||
$this->assertFalse(DispatchStatus::Pending->isFinal());
|
||||
$this->assertFalse(DispatchStatus::Sent->isFinal());
|
||||
$this->assertTrue(DispatchStatus::Success->isFinal());
|
||||
$this->assertTrue(DispatchStatus::Failed->isFinal());
|
||||
}
|
||||
|
||||
public function test_dispatch_source_값(): void
|
||||
{
|
||||
$this->assertEqualsCanonicalizing(['auto', 'manual', 'bulk'], DispatchSource::values());
|
||||
$this->assertSame('auto', DispatchSource::Auto->value);
|
||||
}
|
||||
|
||||
public function test_result_category_분류_판정(): void
|
||||
{
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['success', 'retry', 'permanent_failure', 'balance_low'],
|
||||
ResultCategory::values()
|
||||
);
|
||||
|
||||
$this->assertTrue(ResultCategory::Retry->isRetryable());
|
||||
$this->assertFalse(ResultCategory::Success->isRetryable());
|
||||
|
||||
$this->assertTrue(ResultCategory::PermanentFailure->isFailure());
|
||||
$this->assertTrue(ResultCategory::BalanceLow->isFailure());
|
||||
$this->assertFalse(ResultCategory::Success->isFailure());
|
||||
$this->assertFalse(ResultCategory::Retry->isFailure());
|
||||
}
|
||||
|
||||
public function test_label은_다국어_문자열을_반환한다(): void
|
||||
{
|
||||
// label() 이 예외 없이 문자열을 반환하는지 (lang 키 해석 스모크)
|
||||
$this->assertIsString(DispatchChannel::Sms->label());
|
||||
$this->assertIsString(DispatchStatus::Success->label());
|
||||
$this->assertIsString(DispatchSource::Auto->label());
|
||||
$this->assertIsString(ResultCategory::BalanceLow->label());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# audit:allow test-scenario-coverage reason: |
|
||||
# Phase 1 환경설정 화면은 입력·저장이 코어 /api/admin/plugins/{id}/settings 에 위임되는
|
||||
# 단순 자동바인딩 폼이라, axes cross product 의 런타임 효과(저장 성공/실패 왕복)는 코어 설정
|
||||
# 컨트롤러가 책임진다. 본 확장의 effects(3섹션 구조 / 6필드 / 크리덴셜 마스킹 / webhook 안내 /
|
||||
# 저장 버튼 게이팅 / i18n 정합 / 권한·메뉴·알림 정의 / 설정 초기값 로드)는 PHPUnit 12건
|
||||
# (InstallationTest·EnumTest) + Vitest 22건(plugin_settings.test.tsx) 정적 검증으로 가드된다.
|
||||
# 런타임 Playwright spec 은 계획서 §9 방침에 따라 실연동 검증(PO 직접 수행, 인증·발송 환경 필요)
|
||||
# 단계에서 최초 도입하며, 그 시점에 @scenario/@effects docblock 완전 매핑을 추가한다. 그때까지
|
||||
# 매니페스트는 명세로 유지한다.
|
||||
|
||||
feature: 비즈뿌리오 메시징 환경설정 (Phase 1)
|
||||
|
||||
description: |
|
||||
관리자 환경설정 화면(admin/plugin_settings.json)에서 비즈뿌리오 연동 정보를 입력·저장한다.
|
||||
코어 플러그인 설정 인프라(/api/admin/plugins/{id}/settings)에 자동바인딩(_local.form)으로
|
||||
위임하며, 3 섹션(API 연동 / 발송 설정 / 연동 안내)으로 구성된다.
|
||||
|
||||
핵심 동작:
|
||||
- API 연동: 연동 환경(dev/live) + 비즈뿌리오 아이디 + 비밀번호 + API 키
|
||||
- 발송 설정: 발신번호 + 알림톡 발신프로필 키
|
||||
- 연동 안내: 발송 결과 수신 webhook 주소(readonly 표시)
|
||||
- 크리덴셜(password/api_key/sender_key)은 type=password 마스킹 + frontend_schema expose:false
|
||||
- 저장은 hasChanges 없으면 비활성, 있으면 코어 설정 API 로 PUT
|
||||
- 설치·활성화 시 권한 2개(view/manage)·메뉴 3개·잔액부족 알림 정의 생성 + 설정 초기값 로드
|
||||
|
||||
※ 알림톡 채널 기본 body 시드(3영역)는 채널 등록(Phase 3)·탭 연동(Phase 6)과 강결합이므로
|
||||
Phase 6 으로 이관(plugin.php getNotificationDefinitions 주석 참조).
|
||||
|
||||
axes:
|
||||
credential_state: [empty, filled]
|
||||
environment: [dev, live]
|
||||
field_kind: [credential, plain, readonly]
|
||||
|
||||
exclusions:
|
||||
- { field_kind: readonly, credential_state: filled, reason: "webhook 주소는 고정 표시값 — 운영자 입력 대상 아님" }
|
||||
|
||||
effects:
|
||||
# 섹션 구조
|
||||
- three_sections_api_sending_integration_present
|
||||
# 입력 필드
|
||||
- six_input_fields_bound_by_name
|
||||
- credential_fields_masked_as_password
|
||||
- plain_fields_use_text_input
|
||||
- environment_select_has_dev_live_options
|
||||
# 연동 안내
|
||||
- webhook_address_shown_readonly
|
||||
# 저장
|
||||
- save_button_disabled_without_changes
|
||||
- save_puts_to_core_plugin_settings_api
|
||||
- only_registered_handlers_used
|
||||
# 다국어
|
||||
- all_referenced_i18n_keys_exist_in_ko_and_en
|
||||
# 설치·활성화 (백엔드)
|
||||
- install_creates_view_and_manage_permissions
|
||||
- activate_creates_hierarchical_admin_menus
|
||||
- deactivate_removes_plugin_menus
|
||||
- balance_low_notification_targets_admin
|
||||
- settings_defaults_load_with_dev_environment
|
||||
|
||||
test_files:
|
||||
- plugins/_bundled/sirsoft-message_bizppurio/resources/js/__tests__/layouts/plugin_settings.test.tsx
|
||||
- plugins/_bundled/sirsoft-message_bizppurio/tests/Feature/Installation/InstallationTest.php
|
||||
- plugins/_bundled/sirsoft-message_bizppurio/tests/Unit/Enums/EnumTest.php
|
||||
|
||||
audit:
|
||||
status: codified
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["resources/js/*"]
|
||||
}
|
||||
},
|
||||
"include": ["resources/js/**/*.ts", "resources/js/**/*.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['resources/js/__tests__/**/*.test.{ts,tsx}'],
|
||||
setupFiles: [],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'resources/js'),
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user