Files
sam-api/app/Services/AdminFcmService.php
kent 8a5c7b5298 feat(API): Service 로직 개선
- EstimateService, ItemService 기능 추가
- OrderService 공정 연동 개선
- SalaryService, ReceivablesService 수정
- HandoverReportService, SiteBriefingService 추가
- Pricing 서비스 추가

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-13 19:49:06 +09:00

265 lines
7.7 KiB
PHP

<?php
namespace App\Services;
use App\Models\FcmSendLog;
use App\Models\PushDeviceToken;
use App\Services\Fcm\FcmSender;
class AdminFcmService extends Service
{
public function __construct(
private readonly FcmSender $fcmSender
) {}
/**
* 관리자 FCM 발송
*
* @param array $data 발송 데이터
* @param int|null $senderId 발송자 ID (MNG 관리자)
*/
public function send(array $data, ?int $senderId = null): array
{
// 대상 토큰 조회
$query = PushDeviceToken::withoutGlobalScopes()->active();
if (! empty($data['tenant_id'])) {
$query->forTenant($data['tenant_id']);
}
if (! empty($data['user_id'])) {
$query->forUser($data['user_id']);
}
if (! empty($data['platform'])) {
$query->platform($data['platform']);
}
$tokens = $query->pluck('token')->toArray();
$tokenCount = count($tokens);
if ($tokenCount === 0) {
return [
'success' => false,
'message' => __('error.fcm.no_tokens'),
'data' => null,
];
}
// 발송 로그 생성
$sendLog = FcmSendLog::create([
'tenant_id' => $data['tenant_id'] ?? null,
'user_id' => $data['user_id'] ?? null,
'sender_id' => $senderId,
'title' => $data['title'],
'body' => $data['body'],
'channel_id' => $data['channel_id'] ?? 'push_default',
'type' => $data['type'] ?? null,
'platform' => $data['platform'] ?? null,
'data' => array_filter([
'type' => $data['type'] ?? null,
'url' => $data['url'] ?? null,
'sound_key' => $data['sound_key'] ?? null,
]),
'status' => FcmSendLog::STATUS_SENDING,
]);
try {
// FCM 발송
$fcmData = array_filter([
'type' => $data['type'] ?? null,
'url' => $data['url'] ?? null,
'sound_key' => $data['sound_key'] ?? null,
]);
$result = $this->fcmSender->sendToMany(
$tokens,
$data['title'],
$data['body'],
$data['channel_id'] ?? 'push_default',
$fcmData
);
// 무효 토큰 비활성화
$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 [
'success' => true,
'message' => __('message.fcm.sent'),
'data' => $summary,
'log_id' => $sendLog->id,
];
} catch (\Exception $e) {
$sendLog->markAsFailed($e->getMessage());
return [
'success' => false,
'message' => __('error.fcm.send_failed'),
'error' => $e->getMessage(),
'log_id' => $sendLog->id,
];
}
}
/**
* 대상 토큰 수 미리보기
*/
public function previewCount(array $filters): int
{
$query = PushDeviceToken::withoutGlobalScopes()->active();
if (! empty($filters['tenant_id'])) {
$query->forTenant($filters['tenant_id']);
}
if (! empty($filters['user_id'])) {
$query->forUser($filters['user_id']);
}
if (! empty($filters['platform'])) {
$query->platform($filters['platform']);
}
return $query->count();
}
/**
* 토큰 목록 조회 (관리자용)
*/
public function getTokens(array $filters, int $perPage = 20): array
{
$query = PushDeviceToken::withoutGlobalScopes()
->with(['user:id,name,email', 'tenant:id,company_name']);
if (! empty($filters['tenant_id'])) {
$query->where('tenant_id', $filters['tenant_id']);
}
if (! empty($filters['platform'])) {
$query->where('platform', $filters['platform']);
}
if (isset($filters['is_active'])) {
$query->where('is_active', $filters['is_active']);
}
if (! empty($filters['has_error'])) {
$query->hasError();
}
if (! empty($filters['search'])) {
$search = $filters['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')
->paginate($perPage)
->toArray();
}
/**
* 토큰 통계
*/
public 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,
];
}
/**
* 토큰 상태 토글
*/
public function toggleToken(int $tokenId): PushDeviceToken
{
$token = PushDeviceToken::withoutGlobalScopes()->findOrFail($tokenId);
$token->update(['is_active' => ! $token->is_active]);
return $token;
}
/**
* 토큰 삭제
*/
public function deleteToken(int $tokenId): bool
{
$token = PushDeviceToken::withoutGlobalScopes()->findOrFail($tokenId);
return $token->delete();
}
/**
* 발송 이력 조회
*/
public function getHistory(array $filters, int $perPage = 20): array
{
$query = FcmSendLog::with(['sender:id,name', 'tenant:id,company_name']);
if (! empty($filters['tenant_id'])) {
$query->where('tenant_id', $filters['tenant_id']);
}
if (! empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (! empty($filters['from'])) {
$query->whereDate('created_at', '>=', $filters['from']);
}
if (! empty($filters['to'])) {
$query->whereDate('created_at', '<=', $filters['to']);
}
return $query->orderBy('created_at', 'desc')
->paginate($perPage)
->toArray();
}
}