feat: [fcm] Admin FCM API 추가 - MNG에서 API 호출로 FCM 발송

- AdminFcmController, AdminFcmService 추가
- FormRequest 검증 (AdminFcmSendRequest 등)
- Swagger 문서 추가 (AdminFcmApi.php)
- ApiKeyMiddleware: admin/fcm/* 화이트리스트 추가
- FCM 에러 메시지 i18n 추가
This commit is contained in:
2025-12-23 12:42:58 +09:00
parent 03cf96d4bb
commit f5ec9d502c
8 changed files with 146 additions and 586 deletions

View File

@@ -5,7 +5,7 @@
use App\Models\FcmSendLog;
use App\Models\PushDeviceToken;
use App\Models\Tenants\Tenant;
use App\Services\Fcm\FcmSender;
use App\Services\FcmApiService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
@@ -13,7 +13,7 @@
class FcmController extends Controller
{
public function __construct(
private readonly FcmSender $fcmSender
private readonly FcmApiService $fcmApiService
) {}
/**
@@ -86,6 +86,8 @@ public function send(Request $request): View
/**
* FCM 발송 실행 (HTMX)
*
* API 서버를 통해 FCM을 발송합니다.
*/
public function sendPush(Request $request): View
{
@@ -101,100 +103,34 @@ public function sendPush(Request $request): View
'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([
// API 서버로 발송 요청
$result = $this->fcmApiService->send(
array_filter([
'title' => $request->get('title'),
'body' => $request->get('body'),
'tenant_id' => $request->get('tenant_id'),
'user_id' => $request->get('user_id'),
'platform' => $request->get('platform'),
'channel_id' => $request->get('channel_id'),
'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());
auth()->id()
);
if (! $result['success']) {
return view('fcm.partials.send-result', [
'success' => false,
'message' => '발송 중 오류 발생: '.$e->getMessage(),
'message' => $result['message'] ?? 'FCM 발송에 실패했습니다.',
]);
}
return view('fcm.partials.send-result', [
'success' => true,
'message' => $result['message'] ?? '발송 완료',
'data' => $result['data'] ?? [],
]);
}
/**