get(); foreach ($tenants as $tenant) { try { $alertsCreated += $this->checkTenantKpi($tenant->id); } catch (\Throwable $e) { $errors[] = "tenant={$tenant->id}: {$e->getMessage()}"; Log::error('stat:check-kpi-alerts 실패', [ 'tenant_id' => $tenant->id, 'error' => $e->getMessage(), ]); } } return [ 'alerts_created' => $alertsCreated, 'errors' => $errors, ]; } private function checkTenantKpi(int $tenantId): int { $now = Carbon::now(); $year = $now->year; $month = $now->month; $alertsCreated = 0; $targets = StatKpiTarget::where('tenant_id', $tenantId) ->where('stat_year', $year) ->where(function ($q) use ($month) { $q->where('stat_month', $month)->orWhereNull('stat_month'); }) ->get(); foreach ($targets as $target) { $currentValue = $this->getCurrentValue($tenantId, $target->domain, $target->metric_code, $year, $month); if ($currentValue === null) { continue; } $ratio = $target->target_value > 0 ? ($currentValue / $target->target_value) * 100 : 0; if ($ratio < 50) { $severity = 'critical'; } elseif ($ratio < 80) { $severity = 'warning'; } else { continue; // 80% 이상이면 알림 불필요 } StatAlert::create([ 'tenant_id' => $tenantId, 'domain' => $target->domain, 'alert_type' => 'below_target', 'severity' => $severity, 'title' => "{$target->description} 목표 미달 (".number_format($ratio, 0).'%)', 'message' => "KPI '{$target->metric_code}' 현재값: {$currentValue}, 목표값: {$target->target_value} ({$target->unit})", 'metric_code' => $target->metric_code, 'current_value' => $currentValue, 'threshold_value' => $target->target_value, 'created_at' => now(), ]); $alertsCreated++; } return $alertsCreated; } private function getCurrentValue(int $tenantId, string $domain, string $metricCode, int $year, int $month): ?float { return match ($domain) { 'sales' => $this->getSalesMetric($tenantId, $metricCode, $year, $month), 'finance' => $this->getFinanceMetric($tenantId, $metricCode, $year, $month), default => null, }; } private function getSalesMetric(int $tenantId, string $metricCode, int $year, int $month): ?float { $monthly = StatSalesMonthly::where('tenant_id', $tenantId) ->where('stat_year', $year) ->where('stat_month', $month) ->first(); if (! $monthly) { // 월간 미집계 시 일간 합산 $daily = StatSalesDaily::where('tenant_id', $tenantId) ->whereYear('stat_date', $year) ->whereMonth('stat_date', $month) ->selectRaw('SUM(order_amount) as order_total, SUM(sales_amount) as sales_total') ->first(); return match ($metricCode) { 'monthly_order_amount' => (float) ($daily->order_total ?? 0), 'monthly_sales_amount' => (float) ($daily->sales_total ?? 0), default => null, }; } return match ($metricCode) { 'monthly_order_amount' => (float) $monthly->order_amount, 'monthly_sales_amount' => (float) $monthly->sales_amount, 'monthly_order_count' => (float) $monthly->order_count, default => null, }; } private function getFinanceMetric(int $tenantId, string $metricCode, int $year, int $month): ?float { $daily = StatFinanceDaily::where('tenant_id', $tenantId) ->whereYear('stat_date', $year) ->whereMonth('stat_date', $month) ->selectRaw('SUM(deposit_amount) as deposit_total, SUM(withdrawal_amount) as withdrawal_total') ->first(); return match ($metricCode) { 'monthly_deposit_amount' => (float) ($daily->deposit_total ?? 0), 'monthly_withdrawal_amount' => (float) ($daily->withdrawal_total ?? 0), default => null, }; } }