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; } }