feat: FCM HTTP v1 푸시 알림 발송 기능 구현

- google/auth 패키지 추가 (OAuth2 Service Account 인증)
- FcmSender: FCM HTTP v1 API 발송 서비스
- FcmResponse: 응답 DTO (성공/실패, 토큰 유효성 체크)
- FcmException: FCM 전용 예외 클래스
- fcm:test artisan 명령어 (테스트 발송)
- PushNotificationService에 FcmSender 연동
- config/fcm.php 설정 파일 추가
- 알림 유형별 채널 분리 (push_default, push_urgent)
This commit is contained in:
2025-12-18 22:06:26 +09:00
parent da7165a79f
commit 6e36d179a6
9 changed files with 735 additions and 11 deletions

View File

@@ -4,6 +4,7 @@
use App\Models\PushDeviceToken;
use App\Models\PushNotificationSetting;
use App\Services\Fcm\FcmSender;
use Illuminate\Support\Facades\Log;
class PushNotificationService extends Service
@@ -217,7 +218,7 @@ public function sendToUser(int $userId, string $notificationType, array $notific
}
/**
* FCM 메시지 전송 (실제 구현)
* FCM 메시지 전송 (FCM HTTP v1 API)
*/
protected function sendFcmMessage(
PushDeviceToken $token,
@@ -225,18 +226,75 @@ protected function sendFcmMessage(
string $sound,
string $notificationType
): bool {
// TODO: FCM HTTP v1 API 구현
// 현재는 로그만 기록
Log::info('FCM message would be sent', [
'token_id' => $token->id,
'platform' => $token->platform,
'title' => $notification['title'] ?? '',
'body' => $notification['body'] ?? '',
// FCM 설정이 없으면 로그만 기록
if (empty(config('fcm.project_id'))) {
Log::info('FCM message skipped (not configured)', [
'token_id' => $token->id,
'platform' => $token->platform,
'title' => $notification['title'] ?? '',
'body' => $notification['body'] ?? '',
'type' => $notificationType,
]);
return true; // 설정이 없어도 실패로 처리하지 않음
}
// 알림 유형에 따른 채널 결정
$channelId = $this->getChannelId($notificationType);
// 추가 데이터 구성
$data = array_merge($notification['data'] ?? [], [
'notification_type' => $notificationType,
'sound' => $sound,
'type' => $notificationType,
]);
return true;
try {
$sender = new FcmSender;
$response = $sender->sendToToken(
$token->token,
$notification['title'] ?? '',
$notification['body'] ?? '',
$channelId,
$data
);
// 유효하지 않은 토큰이면 비활성화
if ($response->isInvalidToken()) {
$token->update(['is_active' => false]);
Log::warning('FCM token invalidated', [
'token_id' => $token->id,
'error' => $response->error,
]);
return false;
}
return $response->success;
} catch (\Exception $e) {
Log::error('FCM send failed', [
'token_id' => $token->id,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* 알림 유형에 따른 채널 ID 결정
*/
protected function getChannelId(string $notificationType): string
{
// 긴급 알림 유형들
$urgentTypes = [
PushNotificationSetting::TYPE_APPROVAL,
PushNotificationSetting::TYPE_ORDER,
];
return in_array($notificationType, $urgentTypes, true)
? config('fcm.channels.urgent', 'push_urgent')
: config('fcm.channels.default', 'push_default');
}
/**