tenantId(); $userId = $this->apiUserId(); // 동일 토큰이 있으면 업데이트, 없으면 생성 $token = PushDeviceToken::withoutGlobalScopes() ->where('token', $data['token']) ->first(); if ($token) { // 기존 토큰 업데이트 (다른 사용자의 토큰이면 이전 것은 비활성화) if ($token->user_id !== $userId || $token->tenant_id !== $tenantId) { $token->update([ 'tenant_id' => $tenantId, 'user_id' => $userId, 'platform' => $data['platform'], 'device_name' => $data['device_name'] ?? null, 'app_version' => $data['app_version'] ?? null, 'is_active' => true, 'last_used_at' => now(), 'deleted_at' => null, ]); } else { $token->update([ 'platform' => $data['platform'], 'device_name' => $data['device_name'] ?? null, 'app_version' => $data['app_version'] ?? null, 'is_active' => true, 'last_used_at' => now(), ]); } } else { // 새 토큰 생성 $token = PushDeviceToken::create([ 'tenant_id' => $tenantId, 'user_id' => $userId, 'token' => $data['token'], 'platform' => $data['platform'], 'device_name' => $data['device_name'] ?? null, 'app_version' => $data['app_version'] ?? null, 'is_active' => true, 'last_used_at' => now(), ]); } // 사용자 기본 알림 설정 초기화 (없는 경우) $this->initializeDefaultSettings($tenantId, $userId); Log::info('FCM token registered', [ 'user_id' => $userId, 'platform' => $data['platform'], 'token_id' => $token->id, ]); return $token; } /** * FCM 토큰 비활성화 */ public function unregisterToken(string $tokenValue): bool { $token = PushDeviceToken::withoutGlobalScopes() ->where('token', $tokenValue) ->first(); if ($token) { $token->update(['is_active' => false]); Log::info('FCM token unregistered', [ 'token_id' => $token->id, ]); return true; } return false; } /** * 사용자의 활성 토큰 목록 조회 */ public function getUserTokens(?int $userId = null): array { $userId = $userId ?? $this->apiUserId(); return PushDeviceToken::forUser($userId) ->active() ->get() ->toArray(); } /** * 알림 설정 조회 */ public function getSettings(?int $userId = null): array { $tenantId = $this->tenantId(); $userId = $userId ?? $this->apiUserId(); $settings = PushNotificationSetting::where('tenant_id', $tenantId) ->forUser($userId) ->get() ->keyBy('notification_type'); // 모든 알림 유형에 대한 설정 반환 (없으면 기본값) $result = []; foreach (PushNotificationSetting::getAllTypes() as $type) { if ($settings->has($type)) { $result[$type] = $settings->get($type)->toArray(); } else { $result[$type] = [ 'notification_type' => $type, 'is_enabled' => true, 'sound' => $this->getDefaultSound($type), 'vibrate' => true, 'show_preview' => true, ]; } } return $result; } /** * 알림 설정 업데이트 */ public function updateSettings(array $settings): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $updated = []; foreach ($settings as $setting) { $record = PushNotificationSetting::updateOrCreate( [ 'tenant_id' => $tenantId, 'user_id' => $userId, 'notification_type' => $setting['notification_type'], ], [ 'is_enabled' => $setting['is_enabled'], 'sound' => $setting['sound'] ?? $this->getDefaultSound($setting['notification_type']), 'vibrate' => $setting['vibrate'] ?? true, 'show_preview' => $setting['show_preview'] ?? true, ] ); $updated[] = $record->toArray(); } return $updated; } /** * 특정 사용자에게 푸시 알림 전송 (FCM HTTP v1 API) */ public function sendToUser(int $userId, string $notificationType, array $notification): bool { $tenantId = $this->tenantIdOrNull() ?? 0; // 사용자 알림 설정 확인 $setting = PushNotificationSetting::where('tenant_id', $tenantId) ->forUser($userId) ->ofType($notificationType) ->first(); // 알림이 비활성화된 경우 전송 안함 if ($setting && ! $setting->is_enabled) { Log::info('Push notification skipped (disabled)', [ 'user_id' => $userId, 'type' => $notificationType, ]); return false; } // 사용자의 활성 토큰 조회 $tokens = PushDeviceToken::withoutGlobalScopes() ->forUser($userId) ->active() ->get(); if ($tokens->isEmpty()) { Log::info('Push notification skipped (no tokens)', [ 'user_id' => $userId, ]); return false; } // 알림음 결정 $sound = $setting?->sound ?? $this->getDefaultSound($notificationType); $successCount = 0; foreach ($tokens as $token) { $result = $this->sendFcmMessage($token, $notification, $sound, $notificationType); if ($result) { $successCount++; } } return $successCount > 0; } /** * FCM 메시지 전송 (실제 구현) */ protected function sendFcmMessage( PushDeviceToken $token, array $notification, 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'] ?? '', 'sound' => $sound, 'type' => $notificationType, ]); return true; } /** * 기본 알림 설정 초기화 */ protected function initializeDefaultSettings(int $tenantId, int $userId): void { foreach (PushNotificationSetting::getAllTypes() as $type) { PushNotificationSetting::firstOrCreate( [ 'tenant_id' => $tenantId, 'user_id' => $userId, 'notification_type' => $type, ], [ 'is_enabled' => true, 'sound' => $this->getDefaultSound($type), 'vibrate' => true, 'show_preview' => true, ] ); } } /** * 알림 유형별 기본 알림음 */ protected function getDefaultSound(string $type): string { return match ($type) { PushNotificationSetting::TYPE_DEPOSIT => PushNotificationSetting::SOUND_DEPOSIT, PushNotificationSetting::TYPE_WITHDRAWAL => PushNotificationSetting::SOUND_WITHDRAWAL, PushNotificationSetting::TYPE_ORDER => PushNotificationSetting::SOUND_ORDER, PushNotificationSetting::TYPE_APPROVAL => PushNotificationSetting::SOUND_APPROVAL, default => PushNotificationSetting::SOUND_DEFAULT, }; } }