buildContext($notifiable, $notification, $channel); // Before 훅: 발송 전 처리 (로깅 준비, 필터링 등) HookManager::doAction('core.notification.before_channel_send', $channel, $context); try { $result = parent::sendToNotifiable($notifiable, $id, $notification, $channel); // After 훅: 발송 성공 (로깅, 통계 등) HookManager::doAction('core.notification.after_channel_send', $channel, $context); return $result; } catch (\Exception $e) { // Failed 훅: 발송 실패 (에러 로깅 등) $context['error'] = $e->getMessage(); HookManager::doAction('core.notification.channel_send_failed', $channel, $context); Log::warning("알림 채널 '{$channel}' 발송 실패", [ 'channel' => $channel, 'notification' => get_class($notification), 'notifiable_id' => $notifiable->getKey(), 'error' => $e->getMessage(), ]); return null; } } /** * 훅에 전달할 공통 컨텍스트를 구성합니다. * * @param mixed $notifiable * @param mixed $notification * @param string $channel * @return array */ private function buildContext($notifiable, $notification, string $channel): array { $context = [ 'notifiable' => $notifiable, 'notifiable_id' => $notifiable->getKey(), 'notifiable_type' => get_class($notifiable), 'notifiable_uuid' => $notifiable->uuid ?? null, 'notification_class' => get_class($notification), 'recipient_identifier' => $notifiable->email ?? (string) ($notifiable->getKey() ?? ''), 'recipient_name' => $notifiable->name ?? null, 'recipient_user_id' => $notifiable->getKey() ?? null, ]; // GenericNotification인 경우 추가 메타데이터 if ($notification instanceof GenericNotification) { $context['notification_type'] = $notification->getType(); $context['extension_type'] = $notification->getExtensionType(); $context['extension_identifier'] = $notification->getExtensionIdentifier(); $context['data'] = $notification->getData(); // 렌더링된 subject/body를 context에 포함 (로깅용) $rendered = $this->resolveRenderedContent($notification, $notifiable, $channel); if ($rendered) { $context['subject'] = $rendered['subject'] ?? null; $context['body'] = $rendered['body'] ?? null; } } return $context; } /** * 채널별 렌더링된 제목/본문을 조회합니다. * * @param GenericNotification $notification * @param mixed $notifiable * @param string $channel * @return array{subject: string|null, body: string|null}|null */ private function resolveRenderedContent(GenericNotification $notification, $notifiable, string $channel): ?array { try { $templateService = app(NotificationTemplateService::class); $template = $templateService->resolve($notification->getType(), $channel); if (! $template || ! $template->is_active) { return null; } $locale = $notifiable->locale ?? app()->getLocale(); return $template->replaceVariables($notification->getData(), $locale); } catch (\Throwable $e) { Log::debug('NotificationDispatcher: 렌더링된 콘텐츠 조회 실패', [ 'type' => $notification->getType(), 'channel' => $channel, 'error' => $e->getMessage(), ]); return null; } } }