149 lines
5.1 KiB
PHP
149 lines
5.1 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services\Stats;
|
||
|
|
|
||
|
|
use App\Models\Stats\Daily\StatFinanceDaily;
|
||
|
|
use App\Models\Stats\Daily\StatSalesDaily;
|
||
|
|
use App\Models\Stats\Monthly\StatSalesMonthly;
|
||
|
|
use App\Models\Stats\StatAlert;
|
||
|
|
use App\Models\Stats\StatKpiTarget;
|
||
|
|
use App\Models\Tenants\Tenant;
|
||
|
|
use Carbon\Carbon;
|
||
|
|
use Illuminate\Support\Facades\Log;
|
||
|
|
|
||
|
|
class KpiAlertService
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* KPI 목표 대비 실적 체크 → 알림 생성
|
||
|
|
*/
|
||
|
|
public function checkKpiAlerts(): array
|
||
|
|
{
|
||
|
|
$errors = [];
|
||
|
|
$alertsCreated = 0;
|
||
|
|
|
||
|
|
$tenants = Tenant::where('tenant_st_code', '!=', 'none')->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,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|