$data definition 데이터 (type, hook_prefix, extension_type, * extension_identifier, name, description, variables, * channels, hooks, is_active, is_default, templates 포함 가능) * @return NotificationDefinition 동기화된 definition */ public function syncDefinition(array $data): NotificationDefinition { return NotificationDefinition::syncOrCreateFromUpgrade( ['type' => $data['type']], [ 'hook_prefix' => $data['hook_prefix'], 'extension_type' => $data['extension_type'], 'extension_identifier' => $data['extension_identifier'], 'name' => $data['name'], 'description' => $data['description'] ?? null, 'variables' => $data['variables'] ?? [], 'channels' => $data['channels'] ?? ['mail'], 'hooks' => $data['hooks'] ?? [], 'is_active' => $data['is_active'] ?? true, 'is_default' => $data['is_default'] ?? true, ] ); } /** * 알림 템플릿을 동기화합니다 (user_overrides 보존 upsert). * * @param int $definitionId 연결된 definition id * @param array $data template 데이터 (channel, subject, body, * click_url, recipients, is_active, is_default) * @return NotificationTemplate 동기화된 template */ public function syncTemplate(int $definitionId, array $data): NotificationTemplate { return NotificationTemplate::syncOrCreateFromUpgrade( ['definition_id' => $definitionId, 'channel' => $data['channel']], [ 'subject' => $data['subject'] ?? null, 'body' => $data['body'], 'click_url' => $data['click_url'] ?? null, 'recipients' => $data['recipients'] ?? null, 'is_active' => $data['is_active'] ?? true, 'is_default' => $data['is_default'] ?? true, ] ); } /** * seeder/정의에 없는 stale 알림 정의를 삭제합니다 (완전 동기화 원칙). * * 정책: `user_overrides` 무관 — config/seeder 에 없는 정의는 삭제. * FK cascade 로 연관 `notification_templates` 도 자동 정리됩니다. * * @param string $extensionType 확장 타입: 'core', 'module', 'plugin' * @param string $extensionIdentifier 확장 식별자 * @param array $currentTypes 현재 유효한 definition type 목록 * @return int 삭제된 definition 수 */ public function cleanupStaleDefinitions( string $extensionType, string $extensionIdentifier, array $currentTypes, ): int { $query = NotificationDefinition::query() ->where('extension_type', $extensionType) ->where('extension_identifier', $extensionIdentifier) ->whereNotIn('type', $currentTypes); $targets = $query->get(['id', 'type']); foreach ($targets as $def) { $def->delete(); } $count = $targets->count(); if ($count > 0) { Log::info('stale 알림 정의 정리 완료', [ 'extension_type' => $extensionType, 'extension_identifier' => $extensionIdentifier, 'deleted' => $count, 'types' => $targets->pluck('type')->all(), ]); } return $count; } /** * 주어진 definition 의 channel 목록 기준으로 stale template 을 삭제합니다. * * 정책: `user_overrides` 무관 — seeder 에 없는 (definition_id, channel) * 조합은 삭제. definition 삭제 시 FK cascade 로 자동 정리되는 경로와 별개로, * definition 은 유지되지만 channel 이 재구성된 경우에 사용됩니다. * * @param int $definitionId * @param array $currentChannels 현재 유효한 channel 목록 * @return int 삭제된 template 수 */ public function cleanupStaleTemplates(int $definitionId, array $currentChannels): int { $query = NotificationTemplate::query() ->where('definition_id', $definitionId) ->whereNotIn('channel', $currentChannels); $targets = $query->get(['id', 'channel']); foreach ($targets as $template) { $template->delete(); } $count = $targets->count(); if ($count > 0) { Log::info('stale 알림 템플릿 정리 완료', [ 'definition_id' => $definitionId, 'deleted' => $count, 'channels' => $targets->pluck('channel')->all(), ]); } return $count; } }