106 lines
3.4 KiB
PHP
106 lines
3.4 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Console\Commands;
|
||
|
|
|
||
|
|
use App\Models\PushDeviceToken;
|
||
|
|
use Illuminate\Console\Command;
|
||
|
|
|
||
|
|
class FcmPruneInvalidCommand extends Command
|
||
|
|
{
|
||
|
|
protected $signature = 'fcm:prune-invalid
|
||
|
|
{--days=30 : N일 이상 된 무효 토큰 삭제}
|
||
|
|
{--inactive : 비활성(is_active=0) 토큰도 삭제}
|
||
|
|
{--dry-run : 실제 삭제 없이 대상 수만 출력}
|
||
|
|
{--force : 확인 없이 강제 실행}';
|
||
|
|
|
||
|
|
protected $description = 'FCM 무효 토큰 정리 (soft delete)';
|
||
|
|
|
||
|
|
public function handle(): int
|
||
|
|
{
|
||
|
|
$days = (int) $this->option('days');
|
||
|
|
$includeInactive = $this->option('inactive');
|
||
|
|
$dryRun = $this->option('dry-run');
|
||
|
|
|
||
|
|
$this->info('FCM 무효 토큰 정리');
|
||
|
|
$this->line('');
|
||
|
|
|
||
|
|
// 1. 에러가 있는 토큰 (N일 이상)
|
||
|
|
$errorQuery = PushDeviceToken::withoutGlobalScopes()
|
||
|
|
->hasError()
|
||
|
|
->where('last_error_at', '<', now()->subDays($days));
|
||
|
|
|
||
|
|
$errorCount = $errorQuery->count();
|
||
|
|
|
||
|
|
// 2. 비활성 토큰 (옵션)
|
||
|
|
$inactiveCount = 0;
|
||
|
|
if ($includeInactive) {
|
||
|
|
$inactiveQuery = PushDeviceToken::withoutGlobalScopes()
|
||
|
|
->where('is_active', false)
|
||
|
|
->whereNull('last_error') // 에러 없이 비활성화된 것만
|
||
|
|
->where('updated_at', '<', now()->subDays($days));
|
||
|
|
|
||
|
|
$inactiveCount = $inactiveQuery->count();
|
||
|
|
}
|
||
|
|
|
||
|
|
$totalCount = $errorCount + $inactiveCount;
|
||
|
|
|
||
|
|
// 현황 출력
|
||
|
|
$this->table(['구분', '대상 수'], [
|
||
|
|
["에러 토큰 ({$days}일 이상)", $errorCount],
|
||
|
|
['비활성 토큰', $includeInactive ? $inactiveCount : '(미포함)'],
|
||
|
|
['합계', $totalCount],
|
||
|
|
]);
|
||
|
|
$this->line('');
|
||
|
|
|
||
|
|
if ($totalCount === 0) {
|
||
|
|
$this->info('정리할 토큰이 없습니다.');
|
||
|
|
|
||
|
|
return self::SUCCESS;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Dry-run 모드
|
||
|
|
if ($dryRun) {
|
||
|
|
$this->info("🔍 Dry-run: {$totalCount}개 토큰 삭제 예정");
|
||
|
|
|
||
|
|
return self::SUCCESS;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 확인
|
||
|
|
if (! $this->option('force') && ! $this->confirm("정말 {$totalCount}개 토큰을 삭제하시겠습니까?")) {
|
||
|
|
$this->info('취소되었습니다.');
|
||
|
|
|
||
|
|
return self::SUCCESS;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 삭제 실행
|
||
|
|
$deletedError = 0;
|
||
|
|
$deletedInactive = 0;
|
||
|
|
|
||
|
|
// 에러 토큰 삭제
|
||
|
|
if ($errorCount > 0) {
|
||
|
|
$deletedError = PushDeviceToken::withoutGlobalScopes()
|
||
|
|
->hasError()
|
||
|
|
->where('last_error_at', '<', now()->subDays($days))
|
||
|
|
->delete();
|
||
|
|
|
||
|
|
$this->info(" 에러 토큰 삭제: {$deletedError}개");
|
||
|
|
}
|
||
|
|
|
||
|
|
// 비활성 토큰 삭제
|
||
|
|
if ($includeInactive && $inactiveCount > 0) {
|
||
|
|
$deletedInactive = PushDeviceToken::withoutGlobalScopes()
|
||
|
|
->where('is_active', false)
|
||
|
|
->whereNull('last_error')
|
||
|
|
->where('updated_at', '<', now()->subDays($days))
|
||
|
|
->delete();
|
||
|
|
|
||
|
|
$this->info(" 비활성 토큰 삭제: {$deletedInactive}개");
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->line('');
|
||
|
|
$this->info('✅ 총 '.($deletedError + $deletedInactive).'개 토큰 삭제 완료 (soft delete)');
|
||
|
|
|
||
|
|
return self::SUCCESS;
|
||
|
|
}
|
||
|
|
}
|