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:
@@ -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'] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Fcm;
|
||||
|
||||
class FcmBatchResult
|
||||
{
|
||||
/** @var array<string, FcmResponse> */
|
||||
private array $responses = [];
|
||||
|
||||
private int $successCount = 0;
|
||||
|
||||
private int $failureCount = 0;
|
||||
|
||||
/** @var array<string> 무효 토큰 목록 */
|
||||
private array $invalidTokens = [];
|
||||
|
||||
/**
|
||||
* 응답 추가
|
||||
*/
|
||||
public function addResponse(string $token, FcmResponse $response): void
|
||||
{
|
||||
$this->responses[$token] = $response;
|
||||
|
||||
if ($response->success) {
|
||||
$this->successCount++;
|
||||
} else {
|
||||
$this->failureCount++;
|
||||
|
||||
if ($response->isInvalidToken()) {
|
||||
$this->invalidTokens[] = $token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 발송 수
|
||||
*/
|
||||
public function getTotal(): int
|
||||
{
|
||||
return count($this->responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공 수
|
||||
*/
|
||||
public function getSuccessCount(): int
|
||||
{
|
||||
return $this->successCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 수
|
||||
*/
|
||||
public function getFailureCount(): int
|
||||
{
|
||||
return $this->failureCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 무효 토큰 목록 (비활성화 필요)
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getInvalidTokens(): array
|
||||
{
|
||||
return $this->invalidTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 응답
|
||||
*
|
||||
* @return array<string, FcmResponse>
|
||||
*/
|
||||
public function getResponses(): array
|
||||
{
|
||||
return $this->responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 토큰의 응답
|
||||
*/
|
||||
public function getResponse(string $token): ?FcmResponse
|
||||
{
|
||||
return $this->responses[$token] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 성공률 (%)
|
||||
*/
|
||||
public function getSuccessRate(): float
|
||||
{
|
||||
if ($this->getTotal() === 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return round(($this->successCount / $this->getTotal()) * 100, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 요약 정보
|
||||
*/
|
||||
public function toSummary(): array
|
||||
{
|
||||
return [
|
||||
'total' => $this->getTotal(),
|
||||
'success' => $this->successCount,
|
||||
'failure' => $this->failureCount,
|
||||
'invalid_tokens' => count($this->invalidTokens),
|
||||
'success_rate' => $this->getSuccessRate(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Fcm;
|
||||
|
||||
use Exception;
|
||||
|
||||
class FcmException extends Exception
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
public readonly int $httpStatusCode = 0,
|
||||
public readonly array $response = [],
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 응답에서 예외 생성
|
||||
*/
|
||||
public static function fromResponse(int $statusCode, array $response): self
|
||||
{
|
||||
$message = $response['error']['message'] ?? 'Unknown FCM error';
|
||||
|
||||
return new self($message, $statusCode, $response);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Fcm;
|
||||
|
||||
class FcmResponse
|
||||
{
|
||||
public function __construct(
|
||||
public readonly bool $success,
|
||||
public readonly ?string $messageId,
|
||||
public readonly ?string $error,
|
||||
public readonly int $statusCode,
|
||||
public readonly array $rawResponse
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 성공 응답 생성
|
||||
*/
|
||||
public static function success(?string $messageId, int $statusCode, array $rawResponse): self
|
||||
{
|
||||
return new self(
|
||||
success: true,
|
||||
messageId: $messageId,
|
||||
error: null,
|
||||
statusCode: $statusCode,
|
||||
rawResponse: $rawResponse
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 응답 생성
|
||||
*/
|
||||
public static function failure(string $error, int $statusCode, array $rawResponse): self
|
||||
{
|
||||
return new self(
|
||||
success: false,
|
||||
messageId: null,
|
||||
error: $error,
|
||||
statusCode: $statusCode,
|
||||
rawResponse: $rawResponse
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM 에러 코드 추출
|
||||
*/
|
||||
public function getErrorCode(): ?string
|
||||
{
|
||||
if ($this->success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->rawResponse['error']['details'][0]['errorCode'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰이 유효하지 않은지 확인 (삭제 필요)
|
||||
*/
|
||||
public function isInvalidToken(): bool
|
||||
{
|
||||
if ($this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FCM에서 반환하는 무효 토큰 에러 코드들
|
||||
$invalidTokenErrors = [
|
||||
'UNREGISTERED',
|
||||
'INVALID_ARGUMENT',
|
||||
'NOT_FOUND',
|
||||
];
|
||||
|
||||
return in_array($this->getErrorCode(), $invalidTokenErrors, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열로 변환
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'success' => $this->success,
|
||||
'message_id' => $this->messageId,
|
||||
'error' => $this->error,
|
||||
'status_code' => $this->statusCode,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Fcm;
|
||||
|
||||
use Google\Auth\Credentials\ServiceAccountCredentials;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FcmSender
|
||||
{
|
||||
private const FCM_SCOPE = 'https://www.googleapis.com/auth/firebase.messaging';
|
||||
|
||||
private ?string $accessToken = null;
|
||||
|
||||
private ?int $tokenExpiresAt = null;
|
||||
|
||||
/**
|
||||
* 단일 토큰에 푸시 발송
|
||||
*/
|
||||
public function sendToToken(
|
||||
string $token,
|
||||
string $title,
|
||||
string $body,
|
||||
string $channelId = 'push_default',
|
||||
array $data = []
|
||||
): FcmResponse {
|
||||
$message = $this->buildMessage($token, $title, $body, $channelId, $data);
|
||||
|
||||
return $this->send($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 다건 발송 (토큰 배열) - 단순 루프
|
||||
*
|
||||
* @param array<string> $tokens
|
||||
* @return array<FcmResponse>
|
||||
*/
|
||||
public function sendToTokens(
|
||||
array $tokens,
|
||||
string $title,
|
||||
string $body,
|
||||
string $channelId = 'push_default',
|
||||
array $data = []
|
||||
): array {
|
||||
$responses = [];
|
||||
|
||||
foreach ($tokens as $token) {
|
||||
$responses[] = $this->sendToToken($token, $title, $body, $channelId, $data);
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* 대량 발송 (chunk + rate limit 지원)
|
||||
*
|
||||
* @param array<string> $tokens
|
||||
*/
|
||||
public function sendToMany(
|
||||
array $tokens,
|
||||
string $title,
|
||||
string $body,
|
||||
string $channelId = 'push_default',
|
||||
array $data = [],
|
||||
?int $chunkSize = null,
|
||||
?int $delayMs = null
|
||||
): FcmBatchResult {
|
||||
$chunkSize = $chunkSize ?? config('fcm.batch.chunk_size', 200);
|
||||
$delayMs = $delayMs ?? config('fcm.batch.delay_ms', 100);
|
||||
|
||||
$result = new FcmBatchResult;
|
||||
$chunks = array_chunk($tokens, $chunkSize);
|
||||
$totalChunks = count($chunks);
|
||||
|
||||
foreach ($chunks as $index => $chunk) {
|
||||
foreach ($chunk as $token) {
|
||||
$response = $this->sendToToken($token, $title, $body, $channelId, $data);
|
||||
$result->addResponse($token, $response);
|
||||
}
|
||||
|
||||
// 마지막 chunk가 아니면 rate limit delay
|
||||
if ($index < $totalChunks - 1 && $delayMs > 0) {
|
||||
usleep($delayMs * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM 메시지 빌드
|
||||
*/
|
||||
private function buildMessage(
|
||||
string $token,
|
||||
string $title,
|
||||
string $body,
|
||||
string $channelId,
|
||||
array $data
|
||||
): array {
|
||||
$message = [
|
||||
'message' => [
|
||||
'token' => $token,
|
||||
'notification' => [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
],
|
||||
'android' => [
|
||||
'priority' => config('fcm.defaults.priority', 'high'),
|
||||
'ttl' => config('fcm.defaults.ttl', '86400s'),
|
||||
'notification' => [
|
||||
'channel_id' => $channelId,
|
||||
'sound' => 'default',
|
||||
],
|
||||
],
|
||||
'apns' => [
|
||||
'payload' => [
|
||||
'aps' => [
|
||||
'sound' => 'default',
|
||||
'badge' => 1,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// 커스텀 데이터 추가
|
||||
if (! empty($data)) {
|
||||
// FCM data는 모두 string이어야 함
|
||||
$stringData = array_map(fn ($v) => is_string($v) ? $v : json_encode($v), $data);
|
||||
$message['message']['data'] = $stringData;
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM HTTP v1 API로 메시지 발송
|
||||
*/
|
||||
private function send(array $message): FcmResponse
|
||||
{
|
||||
$projectId = config('fcm.project_id');
|
||||
|
||||
if (empty($projectId)) {
|
||||
return FcmResponse::failure('FCM_PROJECT_ID not configured', 0, $message);
|
||||
}
|
||||
|
||||
$endpoint = str_replace('{project_id}', $projectId, config('fcm.endpoint'));
|
||||
|
||||
try {
|
||||
$accessToken = $this->getAccessToken();
|
||||
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$accessToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->post($endpoint, $message);
|
||||
|
||||
$statusCode = $response->status();
|
||||
$responseBody = $response->json() ?? [];
|
||||
|
||||
$this->logRequest($message, $statusCode, $responseBody);
|
||||
|
||||
if ($response->successful()) {
|
||||
return FcmResponse::success($responseBody['name'] ?? null, $statusCode, $responseBody);
|
||||
}
|
||||
|
||||
$errorMessage = $responseBody['error']['message'] ?? 'Unknown FCM error';
|
||||
|
||||
return FcmResponse::failure($errorMessage, $statusCode, $responseBody);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logError($message, $e);
|
||||
|
||||
return FcmResponse::failure($e->getMessage(), 0, []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth2 Access Token 발급 (캐싱)
|
||||
*/
|
||||
private function getAccessToken(): string
|
||||
{
|
||||
// 토큰이 유효하면 재사용
|
||||
if ($this->accessToken && $this->tokenExpiresAt && time() < $this->tokenExpiresAt - 60) {
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
$saPath = $this->getServiceAccountPath();
|
||||
|
||||
if (! file_exists($saPath)) {
|
||||
throw new FcmException("Service account file not found: {$saPath}");
|
||||
}
|
||||
|
||||
$credentials = new ServiceAccountCredentials(
|
||||
self::FCM_SCOPE,
|
||||
json_decode(file_get_contents($saPath), true)
|
||||
);
|
||||
|
||||
$token = $credentials->fetchAuthToken();
|
||||
|
||||
if (empty($token['access_token'])) {
|
||||
throw new FcmException('Failed to fetch FCM access token');
|
||||
}
|
||||
|
||||
$this->accessToken = $token['access_token'];
|
||||
$this->tokenExpiresAt = time() + ($token['expires_in'] ?? 3600);
|
||||
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service Account JSON 파일 경로
|
||||
*/
|
||||
private function getServiceAccountPath(): string
|
||||
{
|
||||
$path = config('fcm.service_account_path');
|
||||
|
||||
// 절대 경로인 경우
|
||||
if (str_starts_with($path, '/')) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
// 상대 경로인 경우 storage_path 기준
|
||||
return storage_path($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 요청/응답 로깅
|
||||
*/
|
||||
private function logRequest(array $message, int $statusCode, array $response): void
|
||||
{
|
||||
if (! config('fcm.logging.enabled', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$channel = config('fcm.logging.channel', 'stack');
|
||||
$token = $message['message']['token'] ?? 'unknown';
|
||||
$maskedToken = substr($token, 0, 20).'...';
|
||||
|
||||
if ($statusCode >= 200 && $statusCode < 300) {
|
||||
Log::channel($channel)->info('FCM message sent', [
|
||||
'token' => $maskedToken,
|
||||
'status' => $statusCode,
|
||||
'message_name' => $response['name'] ?? null,
|
||||
]);
|
||||
} else {
|
||||
Log::channel($channel)->warning('FCM message failed', [
|
||||
'token' => $maskedToken,
|
||||
'status' => $statusCode,
|
||||
'error' => $response['error'] ?? $response,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 로깅
|
||||
*/
|
||||
private function logError(array $message, \Exception $e): void
|
||||
{
|
||||
if (! config('fcm.logging.enabled', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$channel = config('fcm.logging.channel', 'stack');
|
||||
$token = $message['message']['token'] ?? 'unknown';
|
||||
$maskedToken = substr($token, 0, 20).'...';
|
||||
|
||||
Log::channel($channel)->error('FCM send exception', [
|
||||
'token' => $maskedToken,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
114
app/Services/FcmApiService.php
Normal file
114
app/Services/FcmApiService.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* FCM 발송 API 서비스
|
||||
*
|
||||
* MNG에서 API 서버의 FCM 발송 엔드포인트를 호출합니다.
|
||||
*/
|
||||
class FcmApiService
|
||||
{
|
||||
private string $baseUrl;
|
||||
|
||||
private string $apiKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->baseUrl = rtrim(config('services.api.base_url'), '/');
|
||||
$this->apiKey = config('services.api.key');
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM 푸시 발송
|
||||
*
|
||||
* @param array $data 발송 데이터
|
||||
* @param int|null $senderId 발송자 ID (MNG 관리자 ID)
|
||||
* @return array{success: bool, data?: array, message?: string}
|
||||
*/
|
||||
public function send(array $data, ?int $senderId = null): array
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders([
|
||||
'X-API-KEY' => $this->apiKey,
|
||||
'X-Sender-Id' => $senderId,
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->post("{$this->baseUrl}/api/v1/admin/fcm/send", $data);
|
||||
|
||||
if ($response->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $response->json('data'),
|
||||
'message' => $response->json('message'),
|
||||
];
|
||||
}
|
||||
|
||||
Log::warning('[FcmApiService] Send failed', [
|
||||
'status' => $response->status(),
|
||||
'response' => $response->json(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => $response->json('message') ?? 'FCM 발송에 실패했습니다.',
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[FcmApiService] Send exception', [
|
||||
'exception' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'API 서버 연결에 실패했습니다: '.$e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 대상 토큰 수 미리보기
|
||||
*
|
||||
* @param array $filters 필터 조건
|
||||
* @return array{success: bool, count?: int, message?: string}
|
||||
*/
|
||||
public function previewCount(array $filters): array
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
->withHeaders([
|
||||
'X-API-KEY' => $this->apiKey,
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->get("{$this->baseUrl}/api/v1/admin/fcm/preview-count", $filters);
|
||||
|
||||
if ($response->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'count' => $response->json('data.count', 0),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'count' => 0,
|
||||
'message' => $response->json('message') ?? '미리보기 조회에 실패했습니다.',
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('[FcmApiService] Preview count exception', [
|
||||
'exception' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'count' => 0,
|
||||
'message' => 'API 서버 연결에 실패했습니다.',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,15 @@
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
// Capacitor 네이티브 환경(ios, android)에서만 실행
|
||||
const platform = window.Capacitor?.getPlatform?.();
|
||||
if (platform !== 'ios' && platform !== 'android') {
|
||||
console.log('[FCM] Not running in native app (platform:', platform || 'web', ')');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.Capacitor?.Plugins?.PushNotifications) {
|
||||
console.log('[FCM] Not running in Capacitor');
|
||||
console.log('[FCM] PushNotifications plugin not available');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none foc
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">사운드 키</label>
|
||||
<input type="text"
|
||||
name="sound_key"
|
||||
value="default"
|
||||
placeholder="예: default, alarm"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user