get(); $tokens = $this->getTokensQuery($request)->paginate(20); $stats = $this->getTokenStats($request->get('tenant_id')); return view('fcm.tokens', compact('tenants', 'tokens', 'stats')); } /** * FCM 토큰 목록 (HTMX partial) */ public function tokenList(Request $request): View { $tokens = $this->getTokensQuery($request)->paginate(20); return view('fcm.partials.token-table', compact('tokens')); } /** * 토큰 통계 (HTMX partial) */ public function tokenStats(Request $request): View { $stats = $this->getTokenStats($request->get('tenant_id')); return view('fcm.partials.token-stats', compact('stats')); } /** * 토큰 상태 변경 (활성/비활성) */ public function toggleToken(Request $request, int $id): View { $token = PushDeviceToken::withoutGlobalScopes()->findOrFail($id); $token->update([ 'is_active' => ! $token->is_active, ]); return view('fcm.partials.token-row', compact('token')); } /** * 토큰 삭제 */ public function deleteToken(int $id): Response { $token = PushDeviceToken::withoutGlobalScopes()->findOrFail($id); $token->delete(); return response('', 200) ->header('HX-Trigger', 'tokenDeleted'); } /** * FCM 테스트 발송 페이지 */ public function send(Request $request): View { $tenants = Tenant::orderBy('company_name')->get(); return view('fcm.send', compact('tenants')); } /** * FCM 발송 실행 (HTMX) */ public function sendPush(Request $request): View { $request->validate([ 'title' => 'required|string|max:255', 'body' => 'required|string|max:1000', 'tenant_id' => 'nullable|integer|exists:tenants,id', 'user_id' => 'nullable|integer', 'platform' => 'nullable|string|in:android,ios,web', 'channel_id' => 'nullable|string|max:50', 'type' => 'nullable|string|max:50', 'url' => 'nullable|string|max:500', 'sound_key' => 'nullable|string|max:50', ]); // 대상 토큰 조회 $query = PushDeviceToken::withoutGlobalScopes()->active(); if ($tenantId = $request->get('tenant_id')) { $query->forTenant($tenantId); } if ($userId = $request->get('user_id')) { $query->forUser($userId); } if ($platform = $request->get('platform')) { $query->platform($platform); } $tokens = $query->pluck('token')->toArray(); $tokenCount = count($tokens); if ($tokenCount === 0) { return view('fcm.partials.send-result', [ 'success' => false, 'message' => '발송 대상 토큰이 없습니다.', ]); } // 발송 로그 생성 $sendLog = FcmSendLog::create([ 'tenant_id' => $request->get('tenant_id'), 'user_id' => $request->get('user_id'), 'sender_id' => auth()->id(), 'title' => $request->get('title'), 'body' => $request->get('body'), 'channel_id' => $request->get('channel_id', 'push_default'), 'type' => $request->get('type'), 'platform' => $request->get('platform'), 'data' => array_filter([ 'type' => $request->get('type'), 'url' => $request->get('url'), 'sound_key' => $request->get('sound_key'), ]), 'status' => FcmSendLog::STATUS_SENDING, ]); try { // FCM 발송 $data = array_filter([ 'type' => $request->get('type'), 'url' => $request->get('url'), 'sound_key' => $request->get('sound_key'), ]); $result = $this->fcmSender->sendToMany( $tokens, $request->get('title'), $request->get('body'), $request->get('channel_id', 'push_default'), $data ); // 무효 토큰 비활성화 $invalidTokens = $result->getInvalidTokens(); if (count($invalidTokens) > 0) { foreach ($invalidTokens as $token) { $response = $result->getResponse($token); $errorCode = $response?->getErrorCode(); PushDeviceToken::withoutGlobalScopes() ->where('token', $token) ->update([ 'is_active' => false, 'last_error' => $errorCode, 'last_error_at' => now(), ]); } } // 발송 로그 업데이트 $summary = $result->toSummary(); $sendLog->markAsCompleted($summary); return view('fcm.partials.send-result', [ 'success' => true, 'message' => '발송 완료', 'data' => $summary, ]); } catch (\Exception $e) { $sendLog->markAsFailed($e->getMessage()); return view('fcm.partials.send-result', [ 'success' => false, 'message' => '발송 중 오류 발생: '.$e->getMessage(), ]); } } /** * FCM 발송 이력 페이지 */ public function history(Request $request): View { $logs = $this->getHistoryQuery($request)->paginate(20); return view('fcm.history', compact('logs')); } /** * FCM 발송 이력 목록 (HTMX partial) */ public function historyList(Request $request): View { $logs = $this->getHistoryQuery($request)->paginate(20); return view('fcm.partials.history-table', compact('logs')); } /** * 대상 토큰 수 미리보기 (HTMX) */ public function previewCount(Request $request): View { $query = PushDeviceToken::withoutGlobalScopes()->active(); if ($tenantId = $request->get('tenant_id')) { $query->forTenant($tenantId); } if ($userId = $request->get('user_id')) { $query->forUser($userId); } if ($platform = $request->get('platform')) { $query->platform($platform); } $count = $query->count(); return view('fcm.partials.preview-count', compact('count')); } /** * 토큰 쿼리 빌더 */ private function getTokensQuery(Request $request) { $query = PushDeviceToken::withoutGlobalScopes() ->with(['user:id,name,email', 'tenant:id,company_name']); if ($tenantId = $request->get('tenant_id')) { $query->where('tenant_id', $tenantId); } if ($platform = $request->get('platform')) { $query->where('platform', $platform); } if ($request->has('is_active') && $request->get('is_active') !== '') { $query->where('is_active', $request->boolean('is_active')); } if ($request->boolean('has_error')) { $query->hasError(); } if ($search = $request->get('search')) { $query->where(function ($q) use ($search) { $q->where('token', 'like', "%{$search}%") ->orWhere('device_name', 'like', "%{$search}%") ->orWhereHas('user', function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%"); }); }); } return $query->orderBy('created_at', 'desc'); } /** * 토큰 통계 가져오기 */ private function getTokenStats(?int $tenantId = null): array { $query = PushDeviceToken::withoutGlobalScopes(); if ($tenantId) { $query->where('tenant_id', $tenantId); } $total = (clone $query)->count(); $active = (clone $query)->where('is_active', true)->count(); $inactive = (clone $query)->where('is_active', false)->count(); $hasError = (clone $query)->whereNotNull('last_error')->count(); $byPlatform = (clone $query)->selectRaw('platform, count(*) as count') ->groupBy('platform') ->pluck('count', 'platform') ->toArray(); return [ 'total' => $total, 'active' => $active, 'inactive' => $inactive, 'has_error' => $hasError, 'by_platform' => $byPlatform, ]; } /** * 발송 이력 쿼리 빌더 */ private function getHistoryQuery(Request $request) { $query = FcmSendLog::with(['sender:id,name', 'tenant:id,company_name']); if ($tenantId = $request->get('tenant_id')) { $query->where('tenant_id', $tenantId); } if ($status = $request->get('status')) { $query->where('status', $status); } if ($from = $request->get('from')) { $query->whereDate('created_at', '>=', $from); } if ($to = $request->get('to')) { $query->whereDate('created_at', '<=', $to); } return $query->orderBy('created_at', 'desc'); } }