feat: FCM HTTP v1 푸시 알림 발송 기능 구현
- google/auth 패키지 추가 (OAuth2 Service Account 인증) - FcmSender: FCM HTTP v1 API 발송 서비스 - FcmResponse: 응답 DTO (성공/실패, 토큰 유효성 체크) - FcmException: FCM 전용 예외 클래스 - fcm:test artisan 명령어 (테스트 발송) - PushNotificationService에 FcmSender 연동 - config/fcm.php 설정 파일 추가 - 알림 유형별 채널 분리 (push_default, push_urgent)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -153,3 +153,4 @@ _ide_helper_models.php
|
||||
!**/data/
|
||||
# 그리고 .gitkeep은 예외로 추적
|
||||
!**/data/.gitkeep
|
||||
storage/secrets/
|
||||
|
||||
134
app/Console/Commands/FcmTestCommand.php
Normal file
134
app/Console/Commands/FcmTestCommand.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Fcm\FcmSender;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class FcmTestCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'fcm:test
|
||||
{--token= : FCM 디바이스 토큰 (필수)}
|
||||
{--channel=push_default : 알림 채널 ID (push_default, push_urgent)}
|
||||
{--title=테스트 알림 : 알림 제목}
|
||||
{--body=FCM 테스트 메시지입니다. : 알림 내용}
|
||||
{--data= : 추가 데이터 (JSON 형식)}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'FCM 푸시 알림 테스트 발송';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$token = $this->option('token');
|
||||
|
||||
if (empty($token)) {
|
||||
$this->error('--token 옵션은 필수입니다.');
|
||||
$this->line('');
|
||||
$this->line('사용법:');
|
||||
$this->line(' php artisan fcm:test --token=YOUR_FCM_TOKEN');
|
||||
$this->line(' php artisan fcm:test --token=YOUR_FCM_TOKEN --channel=push_urgent --title="긴급 알림"');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$channel = $this->option('channel');
|
||||
$title = $this->option('title');
|
||||
$body = $this->option('body');
|
||||
$dataJson = $this->option('data');
|
||||
|
||||
// 추가 데이터 파싱
|
||||
$data = [];
|
||||
if ($dataJson) {
|
||||
$data = json_decode($dataJson, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$this->error('--data 옵션은 유효한 JSON이어야 합니다.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// 설정 확인
|
||||
$this->info('FCM 테스트 발송 시작...');
|
||||
$this->line('');
|
||||
$this->table(
|
||||
['항목', '값'],
|
||||
[
|
||||
['Project ID', config('fcm.project_id') ?: '(미설정)'],
|
||||
['Token', substr($token, 0, 30).'...'],
|
||||
['Channel', $channel],
|
||||
['Title', $title],
|
||||
['Body', mb_substr($body, 0, 50).(mb_strlen($body) > 50 ? '...' : '')],
|
||||
['Data', $data ? json_encode($data, JSON_UNESCAPED_UNICODE) : '(없음)'],
|
||||
]
|
||||
);
|
||||
$this->line('');
|
||||
|
||||
// Project ID 확인
|
||||
if (empty(config('fcm.project_id'))) {
|
||||
$this->error('FCM_PROJECT_ID가 설정되지 않았습니다.');
|
||||
$this->line('.env 파일에 FCM_PROJECT_ID를 설정해주세요.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Service Account 파일 확인
|
||||
$saPath = config('fcm.service_account_path');
|
||||
$fullPath = str_starts_with($saPath, '/') ? $saPath : storage_path($saPath);
|
||||
|
||||
if (! file_exists($fullPath)) {
|
||||
$this->error("Service Account 파일을 찾을 수 없습니다: {$fullPath}");
|
||||
$this->line('.env 파일에 FCM_SA_PATH를 확인해주세요.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('Service Account 파일 확인됨: '.basename($fullPath));
|
||||
|
||||
// FCM 발송
|
||||
try {
|
||||
$sender = new FcmSender;
|
||||
$response = $sender->sendToToken($token, $title, $body, $channel, $data);
|
||||
|
||||
$this->line('');
|
||||
|
||||
if ($response->success) {
|
||||
$this->info('✅ FCM 발송 성공!');
|
||||
$this->line("Message ID: {$response->messageId}");
|
||||
$this->line("Status Code: {$response->statusCode}");
|
||||
} else {
|
||||
$this->error('❌ FCM 발송 실패');
|
||||
$this->line("Error: {$response->error}");
|
||||
$this->line("Status Code: {$response->statusCode}");
|
||||
|
||||
if ($response->isInvalidToken()) {
|
||||
$this->warn('토큰이 유효하지 않습니다. 디바이스에서 새 토큰을 발급받아야 합니다.');
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
$this->line('Response:');
|
||||
$this->line(json_encode($response->rawResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->error('FCM 발송 중 예외 발생: '.$e->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
27
app/Services/Fcm/FcmException.php
Normal file
27
app/Services/Fcm/FcmException.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
76
app/Services/Fcm/FcmResponse.php
Normal file
76
app/Services/Fcm/FcmResponse.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰이 유효하지 않은지 확인 (삭제 필요)
|
||||
*/
|
||||
public function isInvalidToken(): bool
|
||||
{
|
||||
if ($this->success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// FCM에서 반환하는 무효 토큰 에러 코드들
|
||||
$invalidTokenErrors = [
|
||||
'UNREGISTERED',
|
||||
'INVALID_ARGUMENT',
|
||||
'NOT_FOUND',
|
||||
];
|
||||
|
||||
$errorCode = $this->rawResponse['error']['details'][0]['errorCode'] ?? null;
|
||||
|
||||
return in_array($errorCode, $invalidTokenErrors, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열로 변환
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'success' => $this->success,
|
||||
'message_id' => $this->messageId,
|
||||
'error' => $this->error,
|
||||
'status_code' => $this->statusCode,
|
||||
];
|
||||
}
|
||||
}
|
||||
237
app/Services/Fcm/FcmSender.php
Normal file
237
app/Services/Fcm/FcmSender.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use App\Models\PushDeviceToken;
|
||||
use App\Models\PushNotificationSetting;
|
||||
use App\Services\Fcm\FcmSender;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PushNotificationService extends Service
|
||||
@@ -217,7 +218,7 @@ public function sendToUser(int $userId, string $notificationType, array $notific
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM 메시지 전송 (실제 구현)
|
||||
* FCM 메시지 전송 (FCM HTTP v1 API)
|
||||
*/
|
||||
protected function sendFcmMessage(
|
||||
PushDeviceToken $token,
|
||||
@@ -225,18 +226,75 @@ protected function sendFcmMessage(
|
||||
string $sound,
|
||||
string $notificationType
|
||||
): bool {
|
||||
// TODO: FCM HTTP v1 API 구현
|
||||
// 현재는 로그만 기록
|
||||
Log::info('FCM message would be sent', [
|
||||
// FCM 설정이 없으면 로그만 기록
|
||||
if (empty(config('fcm.project_id'))) {
|
||||
Log::info('FCM message skipped (not configured)', [
|
||||
'token_id' => $token->id,
|
||||
'platform' => $token->platform,
|
||||
'title' => $notification['title'] ?? '',
|
||||
'body' => $notification['body'] ?? '',
|
||||
'sound' => $sound,
|
||||
'type' => $notificationType,
|
||||
]);
|
||||
|
||||
return true;
|
||||
return true; // 설정이 없어도 실패로 처리하지 않음
|
||||
}
|
||||
|
||||
// 알림 유형에 따른 채널 결정
|
||||
$channelId = $this->getChannelId($notificationType);
|
||||
|
||||
// 추가 데이터 구성
|
||||
$data = array_merge($notification['data'] ?? [], [
|
||||
'notification_type' => $notificationType,
|
||||
'sound' => $sound,
|
||||
]);
|
||||
|
||||
try {
|
||||
$sender = new FcmSender;
|
||||
$response = $sender->sendToToken(
|
||||
$token->token,
|
||||
$notification['title'] ?? '',
|
||||
$notification['body'] ?? '',
|
||||
$channelId,
|
||||
$data
|
||||
);
|
||||
|
||||
// 유효하지 않은 토큰이면 비활성화
|
||||
if ($response->isInvalidToken()) {
|
||||
$token->update(['is_active' => false]);
|
||||
Log::warning('FCM token invalidated', [
|
||||
'token_id' => $token->id,
|
||||
'error' => $response->error,
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $response->success;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('FCM send failed', [
|
||||
'token_id' => $token->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 알림 유형에 따른 채널 ID 결정
|
||||
*/
|
||||
protected function getChannelId(string $notificationType): string
|
||||
{
|
||||
// 긴급 알림 유형들
|
||||
$urgentTypes = [
|
||||
PushNotificationSetting::TYPE_APPROVAL,
|
||||
PushNotificationSetting::TYPE_ORDER,
|
||||
];
|
||||
|
||||
return in_array($notificationType, $urgentTypes, true)
|
||||
? config('fcm.channels.urgent', 'push_urgent')
|
||||
: config('fcm.channels.default', 'push_default');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"php": "^8.2",
|
||||
"darkaonline/l5-swagger": "^9.0",
|
||||
"doctrine/dbal": "^4.3",
|
||||
"google/auth": "^1.49",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/mcp": "^0.1.1",
|
||||
"laravel/sanctum": "^4.0",
|
||||
|
||||
127
composer.lock
generated
127
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "3678e3392030cdc9d6cbbab85da3d350",
|
||||
"content-hash": "340d586f1c4e3f7bd0728229300967da",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -1042,6 +1042,69 @@
|
||||
},
|
||||
"time": "2024-11-01T03:51:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
"version": "v6.11.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/firebase/php-jwt.git",
|
||||
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
|
||||
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psr/cache": "^2.0||^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-sodium": "Support EdDSA (Ed25519) signatures",
|
||||
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Firebase\\JWT\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Neuman Vong",
|
||||
"email": "neuman+pear@twilio.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Anant Narayanan",
|
||||
"email": "anant@php.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
|
||||
"homepage": "https://github.com/firebase/php-jwt",
|
||||
"keywords": [
|
||||
"jwt",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/firebase/php-jwt/issues",
|
||||
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
|
||||
},
|
||||
"time": "2025-04-09T20:32:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/php-cors",
|
||||
"version": "v1.3.0",
|
||||
@@ -1113,6 +1176,68 @@
|
||||
],
|
||||
"time": "2023-10-12T05:21:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/auth",
|
||||
"version": "v1.49.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/googleapis/google-auth-library-php.git",
|
||||
"reference": "68e3d88cb59a49f713e3db25d4f6bb3cc0b70764"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/68e3d88cb59a49f713e3db25d4f6bb3cc0b70764",
|
||||
"reference": "68e3d88cb59a49f713e3db25d4f6bb3cc0b70764",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"firebase/php-jwt": "^6.0",
|
||||
"guzzlehttp/guzzle": "^7.4.5",
|
||||
"guzzlehttp/psr7": "^2.4.5",
|
||||
"php": "^8.1",
|
||||
"psr/cache": "^2.0||^3.0",
|
||||
"psr/http-message": "^1.1||^2.0",
|
||||
"psr/log": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/promises": "^2.0",
|
||||
"kelvinmo/simplejwt": "0.7.1",
|
||||
"phpseclib/phpseclib": "^3.0.35",
|
||||
"phpspec/prophecy-phpunit": "^2.1",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"sebastian/comparator": ">=1.2.3",
|
||||
"squizlabs/php_codesniffer": "^4.0",
|
||||
"symfony/filesystem": "^6.3||^7.3",
|
||||
"symfony/process": "^6.0||^7.0",
|
||||
"webmozart/assert": "^1.11"
|
||||
},
|
||||
"suggest": {
|
||||
"phpseclib/phpseclib": "May be used in place of OpenSSL for signing strings or for token management. Please require version ^2."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\Auth\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "Google Auth Library for PHP",
|
||||
"homepage": "https://github.com/google/google-auth-library-php",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"google",
|
||||
"oauth2"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://cloud.google.com/php/docs/reference/auth/latest",
|
||||
"issues": "https://github.com/googleapis/google-auth-library-php/issues",
|
||||
"source": "https://github.com/googleapis/google-auth-library-php/tree/v1.49.0"
|
||||
},
|
||||
"time": "2025-11-06T21:27:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"version": "v1.1.3",
|
||||
|
||||
65
config/fcm.php
Normal file
65
config/fcm.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| FCM Project ID
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Firebase 프로젝트 ID (Firebase Console에서 확인)
|
||||
|
|
||||
*/
|
||||
'project_id' => env('FCM_PROJECT_ID'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Service Account JSON Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Firebase Admin SDK 서비스 계정 JSON 파일 경로
|
||||
| storage_path() 기준 상대 경로 또는 절대 경로
|
||||
|
|
||||
*/
|
||||
'service_account_path' => env('FCM_SA_PATH', 'app/firebase-service-account.json'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| FCM HTTP v1 Endpoint
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'endpoint' => 'https://fcm.googleapis.com/v1/projects/{project_id}/messages:send',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Android Notification Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 앱에서 정의된 알림 채널 ID 매핑
|
||||
|
|
||||
*/
|
||||
'channels' => [
|
||||
'default' => 'push_default',
|
||||
'urgent' => 'push_urgent',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Settings
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'defaults' => [
|
||||
'channel_id' => 'push_default',
|
||||
'priority' => 'high',
|
||||
'ttl' => '86400s', // 24시간
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Logging
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'logging' => [
|
||||
'enabled' => env('FCM_LOGGING_ENABLED', true),
|
||||
'channel' => env('FCM_LOG_CHANNEL', 'stack'),
|
||||
],
|
||||
];
|
||||
Reference in New Issue
Block a user