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:
117
app/Http/Controllers/Api/V1/Admin/FcmController.php
Normal file
117
app/Http/Controllers/Api/V1/Admin/FcmController.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\FcmHistoryRequest;
|
||||
use App\Http\Requests\Admin\FcmTokenListRequest;
|
||||
use App\Http\Requests\Admin\SendFcmRequest;
|
||||
use App\Services\AdminFcmService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FcmController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminFcmService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* FCM 발송
|
||||
*/
|
||||
public function send(SendFcmRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
// sender_id는 MNG 관리자 ID로, 헤더에서 전달받음
|
||||
$senderId = $request->header('X-Sender-Id');
|
||||
|
||||
$result = $this->service->send(
|
||||
$request->validated(),
|
||||
$senderId ? (int) $senderId : null
|
||||
);
|
||||
|
||||
if (! $result['success']) {
|
||||
return ApiResponse::error($result['message'], 422, $result);
|
||||
}
|
||||
|
||||
return $result['data'];
|
||||
}, __('message.fcm.sent'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 대상 토큰 수 미리보기
|
||||
*/
|
||||
public function previewCount(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
$count = $this->service->previewCount($request->only([
|
||||
'tenant_id',
|
||||
'user_id',
|
||||
'platform',
|
||||
]));
|
||||
|
||||
return ['count' => $count];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 목록 조회
|
||||
*/
|
||||
public function tokens(FcmTokenListRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->getTokens(
|
||||
$request->validated(),
|
||||
$request->integer('per_page', 20)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 통계
|
||||
*/
|
||||
public function tokenStats(Request $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->getTokenStats(
|
||||
$request->integer('tenant_id')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 상태 토글
|
||||
*/
|
||||
public function toggleToken(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
return $this->service->toggleToken($id);
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 삭제
|
||||
*/
|
||||
public function deleteToken(int $id): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($id) {
|
||||
$this->service->deleteToken($id);
|
||||
|
||||
return ['deleted' => true];
|
||||
}, __('message.deleted'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 발송 이력 조회
|
||||
*/
|
||||
public function history(FcmHistoryRequest $request): JsonResponse
|
||||
{
|
||||
return ApiResponse::handle(function () use ($request) {
|
||||
return $this->service->getHistory(
|
||||
$request->validated(),
|
||||
$request->integer('per_page', 20)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -122,13 +122,22 @@ public function handle(Request $request, Closure $next)
|
||||
'api/v1/refresh',
|
||||
'api/v1/debug-apikey',
|
||||
'api/v1/internal/exchange-token', // 내부 서버간 토큰 교환 (HMAC 인증 사용)
|
||||
// 추가적으로 허용하고 싶은 라우트
|
||||
'api/v1/admin/fcm/*', // Admin FCM API (MNG에서 API Key만으로 접근)
|
||||
];
|
||||
|
||||
// 현재 라우트 확인 (경로 또는 이름)
|
||||
$currentRoute = $request->route()?->uri() ?? $request->path();
|
||||
|
||||
if (! in_array($currentRoute, $allowWithoutAuth)) {
|
||||
// 화이트리스트 패턴 매칭 (와일드카드 지원)
|
||||
$isAllowedWithoutAuth = false;
|
||||
foreach ($allowWithoutAuth as $pattern) {
|
||||
if ($pattern === $currentRoute || fnmatch($pattern, $currentRoute)) {
|
||||
$isAllowedWithoutAuth = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $isAllowedWithoutAuth) {
|
||||
// 인증정보(api_user, tenant_id) 없으면 튕김
|
||||
if (! app()->bound('api_user')) {
|
||||
throw new AuthenticationException('회원정보 정보 없음');
|
||||
|
||||
24
app/Http/Requests/Admin/FcmHistoryRequest.php
Normal file
24
app/Http/Requests/Admin/FcmHistoryRequest.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class FcmHistoryRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => 'nullable|integer|exists:tenants,id',
|
||||
'status' => 'nullable|string|in:pending,sending,completed,failed',
|
||||
'from' => 'nullable|date',
|
||||
'to' => 'nullable|date|after_or_equal:from',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
25
app/Http/Requests/Admin/FcmTokenListRequest.php
Normal file
25
app/Http/Requests/Admin/FcmTokenListRequest.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class FcmTokenListRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'tenant_id' => 'nullable|integer|exists:tenants,id',
|
||||
'platform' => 'nullable|string|in:android,ios,web',
|
||||
'is_active' => 'nullable|boolean',
|
||||
'has_error' => 'nullable|boolean',
|
||||
'search' => 'nullable|string|max:100',
|
||||
'per_page' => 'nullable|integer|min:1|max:100',
|
||||
];
|
||||
}
|
||||
}
|
||||
37
app/Http/Requests/Admin/SendFcmRequest.php
Normal file
37
app/Http/Requests/Admin/SendFcmRequest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SendFcmRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'required|string|max:255',
|
||||
'body' => 'required|string|max:1000',
|
||||
'tenant_id' => 'nullable|integer|exists:tenants,id',
|
||||
'user_id' => 'nullable|integer|exists:users,id',
|
||||
'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',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'title.required' => __('validation.required', ['attribute' => '제목']),
|
||||
'body.required' => __('validation.required', ['attribute' => '내용']),
|
||||
'platform.in' => __('validation.in', ['attribute' => '플랫폼']),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user