338 lines
9.8 KiB
PHP
338 lines
9.8 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Http\Controllers;
|
||
|
|
|
||
|
|
use App\Models\FcmSendLog;
|
||
|
|
use App\Models\PushDeviceToken;
|
||
|
|
use App\Models\Tenants\Tenant;
|
||
|
|
use App\Services\Fcm\FcmSender;
|
||
|
|
use Illuminate\Http\Request;
|
||
|
|
use Illuminate\Http\Response;
|
||
|
|
use Illuminate\View\View;
|
||
|
|
|
||
|
|
class FcmController extends Controller
|
||
|
|
{
|
||
|
|
public function __construct(
|
||
|
|
private readonly FcmSender $fcmSender
|
||
|
|
) {}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* FCM 토큰 관리 페이지
|
||
|
|
*/
|
||
|
|
public function tokens(Request $request): View
|
||
|
|
{
|
||
|
|
$tenants = Tenant::orderBy('company_name')->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');
|
||
|
|
}
|
||
|
|
}
|