diff --git a/app/Http/Controllers/Api/V1/PushNotificationController.php b/app/Http/Controllers/Api/V1/PushNotificationController.php index 8f71413..9ee744d 100644 --- a/app/Http/Controllers/Api/V1/PushNotificationController.php +++ b/app/Http/Controllers/Api/V1/PushNotificationController.php @@ -11,15 +11,17 @@ class PushNotificationController extends Controller { + public function __construct( + private readonly PushNotificationService $service + ) {} + /** * FCM 토큰 등록 */ public function registerToken(RegisterTokenRequest $request) { return ApiResponse::handle(function () use ($request) { - $service = new PushNotificationService; - - return $service->registerToken($request->validated()); + return $this->service->registerToken($request->validated()); }, __('message.push.token_registered')); } @@ -34,9 +36,7 @@ public function unregisterToken(Request $request) throw new \InvalidArgumentException(__('error.push.token_required')); } - $service = new PushNotificationService; - - return ['unregistered' => $service->unregisterToken($token)]; + return ['unregistered' => $this->service->unregisterToken($token)]; }, __('message.push.token_unregistered')); } @@ -46,9 +46,7 @@ public function unregisterToken(Request $request) public function getTokens() { return ApiResponse::handle(function () { - $service = new PushNotificationService; - - return $service->getUserTokens(); + return $this->service->getUserTokens(); }); } @@ -58,9 +56,7 @@ public function getTokens() public function getSettings() { return ApiResponse::handle(function () { - $service = new PushNotificationService; - - return $service->getSettings(); + return $this->service->getSettings(); }); } @@ -70,9 +66,7 @@ public function getSettings() public function updateSettings(UpdateSettingsRequest $request) { return ApiResponse::handle(function () use ($request) { - $service = new PushNotificationService; - - return $service->updateSettings($request->validated()['settings']); + return $this->service->updateSettings($request->validated()['settings']); }, __('message.push.settings_updated')); } diff --git a/app/Services/PushNotificationService.php b/app/Services/PushNotificationService.php index cbd841f..f77375e 100644 --- a/app/Services/PushNotificationService.php +++ b/app/Services/PushNotificationService.php @@ -3,11 +3,15 @@ namespace App\Services; use App\Models\PushDeviceToken; +use App\Models\PushNotificationSetting; use App\Services\Fcm\FcmSender; use Illuminate\Support\Facades\Log; /** - * 비즈니스 이벤트 기반 푸시 알림 서비스 + * 푸시 알림 서비스 + * - 토큰 관리 (등록/해제/조회) + * - 알림 설정 관리 + * - 비즈니스 이벤트 기반 푸시 발송 */ class PushNotificationService extends Service { @@ -15,6 +19,211 @@ public function __construct( private readonly FcmSender $fcmSender ) {} + // ============================================================ + // 토큰 관리 + // ============================================================ + + /** + * FCM 토큰 등록/갱신 + */ + public function registerToken(array $data): PushDeviceToken + { + $tenantId = $this->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('[PushNotificationService] 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('[PushNotificationService] 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; + } + + /** + * 기본 알림 설정 초기화 + */ + 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, + }; + } + + // ============================================================ + // 비즈니스 이벤트 기반 푸시 발송 + // ============================================================ + /** * 비즈니스 이벤트에 따른 푸시 발송 * @@ -195,4 +404,4 @@ private function getChannelForEvent(string $event): string default => 'push_default', }; } -} +} \ No newline at end of file