option('from'); if (! $from) { $this->error('--from 옵션은 필수입니다. 예: --from=2024-01-01'); return self::FAILURE; } $startDate = Carbon::parse($from); $endDate = $this->option('to') ? Carbon::parse($this->option('to')) : Carbon::yesterday(); $domain = $this->option('domain'); $tenantId = $this->option('tenant') ? (int) $this->option('tenant') : null; $totalDays = $startDate->diffInDays($endDate) + 1; $this->info("📊 백필 시작: {$startDate->format('Y-m-d')} ~ {$endDate->format('Y-m-d')} ({$totalDays}일)"); if ($domain) { $this->info(" 도메인 필터: {$domain}"); } if ($tenantId) { $this->info(" 테넌트 필터: {$tenantId}"); } $totalErrors = []; $totalDomainsProcessed = 0; $startTime = microtime(true); // 1. 차원 테이블 동기화 (최초 1회) if (! $this->option('skip-dimensions')) { $this->info(''); $this->info('🔄 차원 테이블 동기화...'); try { $tenants = $this->getTargetTenants($tenantId); foreach ($tenants as $tenant) { $clients = $dimensionSync->syncClients($tenant->id); $products = $dimensionSync->syncProducts($tenant->id); $this->line(" tenant={$tenant->id}: 고객 {$clients}건, 제품 {$products}건"); } } catch (\Throwable $e) { $this->warn(" 차원 동기화 실패: {$e->getMessage()}"); } } // 2. 일간 집계 $this->info(''); $this->info('📅 일간 집계 시작...'); $bar = $this->output->createProgressBar($totalDays); $bar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %message%'); $bar->setMessage(''); $period = CarbonPeriod::create($startDate, $endDate); foreach ($period as $date) { $bar->setMessage($date->format('Y-m-d')); try { $result = $aggregator->aggregateDaily($date, $domain, $tenantId); $totalDomainsProcessed += $result['domains_processed']; if (! empty($result['errors'])) { $totalErrors = array_merge($totalErrors, $result['errors']); } } catch (\Throwable $e) { $totalErrors[] = "daily {$date->format('Y-m-d')}: {$e->getMessage()}"; } $bar->advance(); } $bar->finish(); $this->newLine(); // 3. 월간 집계 if (! $this->option('skip-monthly')) { $this->info(''); $this->info('📆 월간 집계 시작...'); $months = $this->getMonthRange($startDate, $endDate); $monthBar = $this->output->createProgressBar(count($months)); foreach ($months as [$year, $month]) { $monthBar->setMessage("{$year}-".str_pad($month, 2, '0', STR_PAD_LEFT)); try { $result = $aggregator->aggregateMonthly($year, $month, $domain, $tenantId); $totalDomainsProcessed += $result['domains_processed']; if (! empty($result['errors'])) { $totalErrors = array_merge($totalErrors, $result['errors']); } } catch (\Throwable $e) { $totalErrors[] = "monthly {$year}-{$month}: {$e->getMessage()}"; } $monthBar->advance(); } $monthBar->finish(); $this->newLine(); } $durationSec = round(microtime(true) - $startTime, 1); $this->info(''); $this->info('✅ 백필 완료:'); $this->info(" - 기간: {$startDate->format('Y-m-d')} ~ {$endDate->format('Y-m-d')} ({$totalDays}일)"); $this->info(" - 처리 도메인-테넌트: {$totalDomainsProcessed}건"); $this->info(" - 소요 시간: {$durationSec}초"); if (! empty($totalErrors)) { $this->warn(' - 에러: '.count($totalErrors).'건'); foreach (array_slice($totalErrors, 0, 20) as $error) { $this->error(" - {$error}"); } if (count($totalErrors) > 20) { $this->warn(' ... 외 '.(count($totalErrors) - 20).'건'); } return self::FAILURE; } return self::SUCCESS; } private function getTargetTenants(?int $tenantId): \Illuminate\Database\Eloquent\Collection { $query = \App\Models\Tenants\Tenant::where('tenant_st_code', '!=', 'none'); if ($tenantId) { $query->where('id', $tenantId); } return $query->get(); } private function getMonthRange(Carbon $start, Carbon $end): array { $months = []; $current = $start->copy()->startOfMonth(); $endMonth = $end->copy()->startOfMonth(); while ($current->lte($endMonth)) { $months[] = [$current->year, $current->month]; $current->addMonth(); } return $months; } }