fix(message_bizppurio): 알림 채널 미저장 시 발송 차단 + 토글 저장·노출 정정
관리자가 SMS·알림톡 채널을 꺼도 발송이 계속되던 문제 수정. - 원인: 채널 토글 저장이 map-only 라 미등록 확장 채널(sms/alimtalk)의 on/off 가 저장되지 않았고, 백엔드는 미저장 채널을 활성으로 판정 - 프론트: 토글 저장을 upsert 로 전환(미저장 채널도 저장), 하단 채널 탭을 source 기준으로 필터(확장 채널은 미저장/OFF 시 숨김) — 코어·게시판·이커머스 - 백엔드: 플러그인이 core.notification.channel_enabled 훅을 구독해 확장 채널 미저장 시 OFF 로 게이트(코어 무수정, 코어 기본 채널은 하위호환 유지)
This commit is contained in:
1 parent
92f36e86b1
commit
eb0c72c32b
9 files changed
+309
-6
No files matched your search
+95
@@ -5,7 +5,9 @@ declare(strict_types=1);
|
||||
namespace Plugins\Sirsoft\MessageBizppurio\Listeners;
|
||||
|
||||
use App\Contracts\Extension\HookListenerInterface;
|
||||
use App\Services\ModuleSettingsService;
|
||||
use App\Services\PluginSettingsService;
|
||||
use App\Services\SettingsService;
|
||||
|
||||
/**
|
||||
* 비즈뿌리오 문자·알림톡 채널을 코어 알림 시스템에 등록하는 필터 리스너.
|
||||
@@ -70,6 +72,11 @@ class RegisterNotificationChannelsListener implements HookListenerInterface
|
||||
'priority' => 20,
|
||||
'type' => 'filter',
|
||||
],
|
||||
'core.notification.channel_enabled' => [
|
||||
'method' => 'gateChannelEnabled',
|
||||
'priority' => 20,
|
||||
'type' => 'filter',
|
||||
],
|
||||
];
|
||||
|
||||
foreach (self::CHANNEL_HOOK_PREFIXES as $prefix) {
|
||||
@@ -144,6 +151,94 @@ class RegisterNotificationChannelsListener implements HookListenerInterface
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 우리 채널(sms/alimtalk)의 "미저장=비활성(OFF)" 정책을 적용합니다
|
||||
* (core.notification.channel_enabled 필터).
|
||||
*
|
||||
* 코어의 기본 판정은 "채널 설정 엔트리가 없으면 활성(true)" 이다(하위호환). 하지만
|
||||
* sms·alimtalk 은 플러그인이 나중에 주입한 채널이라, 관리자가 명시적으로 켜기 전에는
|
||||
* 발송·기록하지 않아야 한다(opt-in). 게스트 발송 정책 isChannelGuestAllowed 의
|
||||
* "확장 채널은 미선언=차단" 과 같은 방향이다.
|
||||
*
|
||||
* 따라서 우리 채널이면서 해당 확장의 notifications.channels 저장소에 엔트리가 없을 때만
|
||||
* false 로 덮어쓴다. 엔트리가 저장되어 있으면(켜짐/꺼짐 모두) 코어가 이미 그 값을 반영해
|
||||
* $enabled 로 넘겨주므로 그대로 통과시킨다. 우리 채널이 아니면 항상 원본을 통과시킨다.
|
||||
*
|
||||
* @param bool $enabled 코어가 계산한 활성 여부
|
||||
* @param string $extensionType 확장 타입 (core/module/plugin)
|
||||
* @param string|null $extensionIdentifier 확장 식별자
|
||||
* @param string $channelId 채널 식별자
|
||||
* @return bool 최종 활성 여부
|
||||
*/
|
||||
public function gateChannelEnabled(
|
||||
bool $enabled,
|
||||
string $extensionType,
|
||||
?string $extensionIdentifier,
|
||||
string $channelId
|
||||
): bool {
|
||||
if (! in_array($channelId, self::CHANNEL_IDS, true)) {
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
// 저장 엔트리가 있으면 코어 판정($enabled)을 존중, 없으면 미저장 → OFF
|
||||
if ($this->hasSavedChannelEntry($extensionType, $extensionIdentifier, $channelId)) {
|
||||
return $enabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 해당 확장의 notifications.channels 저장소에 특정 채널 엔트리가 존재하는지 확인합니다.
|
||||
*
|
||||
* 저장소는 확장 타입별로 분리됩니다:
|
||||
* - core → SettingsService
|
||||
* - module → ModuleSettingsService
|
||||
* - plugin → PluginSettingsService
|
||||
*
|
||||
* 조회 실패(예외)나 미지원 타입은 "미저장"(false)으로 간주해 안전측(OFF)으로 처리한다.
|
||||
*
|
||||
* @param string $extensionType 확장 타입
|
||||
* @param string|null $extensionIdentifier 확장 식별자
|
||||
* @param string $channelId 채널 식별자
|
||||
* @return bool 저장 엔트리 존재 여부
|
||||
*/
|
||||
private function hasSavedChannelEntry(
|
||||
string $extensionType,
|
||||
?string $extensionIdentifier,
|
||||
string $channelId
|
||||
): bool {
|
||||
try {
|
||||
$channels = match ($extensionType) {
|
||||
'core' => app(SettingsService::class)
|
||||
->getSetting('notifications.channels', []),
|
||||
'module' => empty($extensionIdentifier)
|
||||
? []
|
||||
: app(ModuleSettingsService::class)
|
||||
->get($extensionIdentifier, 'notifications.channels', []),
|
||||
'plugin' => empty($extensionIdentifier)
|
||||
? []
|
||||
: app(PluginSettingsService::class)
|
||||
->get($extensionIdentifier, 'notifications.channels', []),
|
||||
default => [],
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! is_array($channels)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($channels as $entry) {
|
||||
if (is_array($entry) && ($entry['id'] ?? null) === $channelId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 레거시 다채널 자동 결정 경로에서 채널 후보에 sms·alimtalk 을 더합니다.
|
||||
*
|
||||
|
||||
+67
@@ -6,6 +6,7 @@ use App\Extension\HookManager;
|
||||
use App\Services\ChannelReadinessService;
|
||||
use App\Services\NotificationChannelService;
|
||||
use App\Services\PluginSettingsService;
|
||||
use App\Services\SettingsService;
|
||||
use Plugins\Sirsoft\MessageBizppurio\Listeners\RegisterNotificationChannelsListener;
|
||||
use Plugins\Sirsoft\MessageBizppurio\Tests\PluginTestCase;
|
||||
|
||||
@@ -48,6 +49,26 @@ class ChannelRegistrationHookTest extends PluginTestCase
|
||||
fn ($result, $channelId) => $listener->checkReadiness($result, $channelId),
|
||||
20,
|
||||
);
|
||||
HookManager::addFilter(
|
||||
'core.notification.channel_enabled',
|
||||
fn ($enabled, $extType, $extId, $channelId) => $listener->gateChannelEnabled($enabled, $extType, $extId, $channelId),
|
||||
20,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 코어 SettingsService 의 notifications.channels 저장값을 지정합니다.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $channels
|
||||
*/
|
||||
private function setCoreChannels(array $channels): void
|
||||
{
|
||||
$settings = \Mockery::mock(SettingsService::class);
|
||||
$settings->shouldReceive('getSetting')
|
||||
->with('notifications.channels', [])
|
||||
->andReturn($channels);
|
||||
$this->app->instance(SettingsService::class, $settings);
|
||||
app(NotificationChannelService::class)->clearChannelEnabledCache();
|
||||
}
|
||||
|
||||
public function test_코어_채널서비스가_sms_alimtalk을_노출한다(): void
|
||||
@@ -94,4 +115,50 @@ class ChannelRegistrationHookTest extends PluginTestCase
|
||||
|
||||
$this->assertTrue($result['ready']);
|
||||
}
|
||||
|
||||
public function test_channel_enabled_미저장_sms는_off로_덮인다(): void
|
||||
{
|
||||
$this->registerListener();
|
||||
// mail/database 만 저장, sms/alimtalk 엔트리 없음(미저장)
|
||||
$this->setCoreChannels([
|
||||
['id' => 'mail', 'is_active' => true],
|
||||
['id' => 'database', 'is_active' => true],
|
||||
]);
|
||||
|
||||
$service = app(NotificationChannelService::class);
|
||||
|
||||
// 우리 채널: 미저장 → OFF (opt-in)
|
||||
$this->assertFalse($service->isChannelEnabledForExtension('core', 'core', 'sms'));
|
||||
$service->clearChannelEnabledCache();
|
||||
$this->assertFalse($service->isChannelEnabledForExtension('core', 'core', 'alimtalk'));
|
||||
// 코어 기본 채널: 미저장이어도 ON 유지(하위호환)
|
||||
$service->clearChannelEnabledCache();
|
||||
$this->assertTrue($service->isChannelEnabledForExtension('core', 'core', 'mail'));
|
||||
}
|
||||
|
||||
public function test_channel_enabled_저장된_on_sms는_on이다(): void
|
||||
{
|
||||
$this->registerListener();
|
||||
$this->setCoreChannels([
|
||||
['id' => 'mail', 'is_active' => true],
|
||||
['id' => 'sms', 'is_active' => true],
|
||||
]);
|
||||
|
||||
$this->assertTrue(
|
||||
app(NotificationChannelService::class)->isChannelEnabledForExtension('core', 'core', 'sms')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_channel_enabled_저장된_off_sms는_off이다(): void
|
||||
{
|
||||
$this->registerListener();
|
||||
$this->setCoreChannels([
|
||||
['id' => 'mail', 'is_active' => true],
|
||||
['id' => 'sms', 'is_active' => false],
|
||||
]);
|
||||
|
||||
$this->assertFalse(
|
||||
app(NotificationChannelService::class)->isChannelEnabledForExtension('core', 'core', 'sms')
|
||||
);
|
||||
}
|
||||
}
|
||||
+11
@@ -204,6 +204,7 @@ class RegisterNotificationChannelsListenerTest extends PluginTestCase
|
||||
|
||||
$this->assertArrayHasKey('core.notification.filter_available_channels', $hooks);
|
||||
$this->assertArrayHasKey('core.notification.channel_readiness', $hooks);
|
||||
$this->assertArrayHasKey('core.notification.channel_enabled', $hooks);
|
||||
$this->assertArrayHasKey('core.auth.notification.channels', $hooks);
|
||||
$this->assertArrayHasKey('sirsoft-ecommerce.notification.channels', $hooks);
|
||||
$this->assertArrayHasKey('sirsoft-board.notification.channels', $hooks);
|
||||
@@ -213,4 +214,14 @@ class RegisterNotificationChannelsListenerTest extends PluginTestCase
|
||||
$this->assertSame('filter', $config['type']);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_channel_enabled_우리채널이_아니면_원본을_통과시킨다(): void
|
||||
{
|
||||
$listener = $this->makeListener();
|
||||
|
||||
// mail/database 등 타 채널은 저장소 조회 없이 코어 판정($enabled)을 그대로 반환
|
||||
$this->assertTrue($listener->gateChannelEnabled(true, 'core', 'core', 'mail'));
|
||||
$this->assertFalse($listener->gateChannelEnabled(false, 'core', 'core', 'database'));
|
||||
$this->assertTrue($listener->gateChannelEnabled(true, 'module', 'sirsoft-board', 'slack'));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user