feat: sam_stat P0 도메인 집계 구현 (Phase 2)
- 영업(Sales), 재무(Finance), 생산(Production) 3개 도메인 구현 - 일간/월간 통계 테이블 6개 마이그레이션 생성 - 도메인별 StatService (SalesStatService, FinanceStatService, ProductionStatService) - Daily/Monthly 6개 Eloquent 모델 생성 - StatAggregatorService에 도메인 서비스 매핑 활성화 - StatJobLog duration_ms abs() 처리 - 스케줄러 등록 (일간 02:00, 월간 1일 03:00) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
26
app/Models/Stats/Daily/StatFinanceDaily.php
Normal file
26
app/Models/Stats/Daily/StatFinanceDaily.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatFinanceDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_finance_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'deposit_amount' => 'decimal:2',
|
||||
'withdrawal_amount' => 'decimal:2',
|
||||
'net_cashflow' => 'decimal:2',
|
||||
'purchase_amount' => 'decimal:2',
|
||||
'purchase_tax_amount' => 'decimal:2',
|
||||
'receivable_balance' => 'decimal:2',
|
||||
'payable_balance' => 'decimal:2',
|
||||
'overdue_receivable' => 'decimal:2',
|
||||
'bill_issued_amount' => 'decimal:2',
|
||||
'bill_matured_amount' => 'decimal:2',
|
||||
'card_transaction_amount' => 'decimal:2',
|
||||
'bank_balance_total' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
21
app/Models/Stats/Daily/StatProductionDaily.php
Normal file
21
app/Models/Stats/Daily/StatProductionDaily.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatProductionDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_production_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'production_qty' => 'decimal:2',
|
||||
'defect_qty' => 'decimal:2',
|
||||
'defect_rate' => 'decimal:2',
|
||||
'planned_hours' => 'decimal:2',
|
||||
'actual_hours' => 'decimal:2',
|
||||
'efficiency_rate' => 'decimal:2',
|
||||
'delivery_rate' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
18
app/Models/Stats/Daily/StatSalesDaily.php
Normal file
18
app/Models/Stats/Daily/StatSalesDaily.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Daily;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatSalesDaily extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_sales_daily';
|
||||
|
||||
protected $casts = [
|
||||
'stat_date' => 'date',
|
||||
'order_amount' => 'decimal:2',
|
||||
'sales_amount' => 'decimal:2',
|
||||
'sales_tax_amount' => 'decimal:2',
|
||||
'shipment_amount' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
22
app/Models/Stats/Monthly/StatFinanceMonthly.php
Normal file
22
app/Models/Stats/Monthly/StatFinanceMonthly.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Monthly;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatFinanceMonthly extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_finance_monthly';
|
||||
|
||||
protected $casts = [
|
||||
'deposit_total' => 'decimal:2',
|
||||
'withdrawal_total' => 'decimal:2',
|
||||
'net_cashflow' => 'decimal:2',
|
||||
'purchase_total' => 'decimal:2',
|
||||
'card_total' => 'decimal:2',
|
||||
'receivable_end' => 'decimal:2',
|
||||
'payable_end' => 'decimal:2',
|
||||
'bank_balance_end' => 'decimal:2',
|
||||
'mom_cashflow_change' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
20
app/Models/Stats/Monthly/StatProductionMonthly.php
Normal file
20
app/Models/Stats/Monthly/StatProductionMonthly.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Monthly;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatProductionMonthly extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_production_monthly';
|
||||
|
||||
protected $casts = [
|
||||
'production_qty' => 'decimal:2',
|
||||
'defect_qty' => 'decimal:2',
|
||||
'avg_defect_rate' => 'decimal:2',
|
||||
'avg_efficiency_rate' => 'decimal:2',
|
||||
'avg_delivery_rate' => 'decimal:2',
|
||||
'total_planned_hours' => 'decimal:2',
|
||||
'total_actual_hours' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
20
app/Models/Stats/Monthly/StatSalesMonthly.php
Normal file
20
app/Models/Stats/Monthly/StatSalesMonthly.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Stats\Monthly;
|
||||
|
||||
use App\Models\Stats\BaseStatModel;
|
||||
|
||||
class StatSalesMonthly extends BaseStatModel
|
||||
{
|
||||
protected $table = 'stat_sales_monthly';
|
||||
|
||||
protected $casts = [
|
||||
'order_amount' => 'decimal:2',
|
||||
'sales_amount' => 'decimal:2',
|
||||
'shipment_amount' => 'decimal:2',
|
||||
'avg_order_amount' => 'decimal:2',
|
||||
'top_client_amount' => 'decimal:2',
|
||||
'mom_growth_rate' => 'decimal:2',
|
||||
'yoy_growth_rate' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public function markRunning(): void
|
||||
public function markCompleted(int $recordsProcessed = 0): void
|
||||
{
|
||||
$durationMs = $this->started_at
|
||||
? (int) now()->diffInMilliseconds($this->started_at)
|
||||
? abs((int) now()->diffInMilliseconds($this->started_at))
|
||||
: null;
|
||||
|
||||
$this->update([
|
||||
@@ -40,7 +40,7 @@ public function markCompleted(int $recordsProcessed = 0): void
|
||||
public function markFailed(string $errorMessage): void
|
||||
{
|
||||
$durationMs = $this->started_at
|
||||
? (int) now()->diffInMilliseconds($this->started_at)
|
||||
? abs((int) now()->diffInMilliseconds($this->started_at))
|
||||
: null;
|
||||
|
||||
$this->update([
|
||||
|
||||
172
app/Services/Stats/FinanceStatService.php
Normal file
172
app/Services/Stats/FinanceStatService.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Stats;
|
||||
|
||||
use App\Models\Stats\Daily\StatFinanceDaily;
|
||||
use App\Models\Stats\Monthly\StatFinanceMonthly;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FinanceStatService implements StatDomainServiceInterface
|
||||
{
|
||||
public function aggregateDaily(int $tenantId, Carbon $date): int
|
||||
{
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
|
||||
// 입금 (deposits)
|
||||
$depositStats = DB::connection('mysql')
|
||||
->table('deposits')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('deposit_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
// 출금 (withdrawals)
|
||||
$withdrawalStats = DB::connection('mysql')
|
||||
->table('withdrawals')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('withdrawal_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
// 매입 (purchases)
|
||||
$purchaseStats = DB::connection('mysql')
|
||||
->table('purchases')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('purchase_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('
|
||||
COUNT(*) as cnt,
|
||||
COALESCE(SUM(supply_amount), 0) as supply_total,
|
||||
COALESCE(SUM(tax_amount), 0) as tax_total
|
||||
')
|
||||
->first();
|
||||
|
||||
// 어음 발행 (bills - issued on this date)
|
||||
$billIssuedStats = DB::connection('mysql')
|
||||
->table('bills')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('issue_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
// 어음 만기 (bills - matured on this date)
|
||||
$billMaturedStats = DB::connection('mysql')
|
||||
->table('bills')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('maturity_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
// 카드 거래 (withdrawals with card_id)
|
||||
$cardStats = DB::connection('mysql')
|
||||
->table('withdrawals')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('withdrawal_date', $dateStr)
|
||||
->whereNotNull('card_id')
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('COUNT(*) as cnt, COALESCE(SUM(amount), 0) as total')
|
||||
->first();
|
||||
|
||||
// 은행 잔액 합계 (bank_transactions - 계좌별 최신 잔액)
|
||||
$bankBalance = DB::connection('mysql')
|
||||
->query()
|
||||
->fromSub(function ($query) use ($tenantId, $dateStr) {
|
||||
$query->from('bank_transactions')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('transaction_date', '<=', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('bank_account_id, balance_after as latest_balance,
|
||||
ROW_NUMBER() OVER(PARTITION BY bank_account_id ORDER BY transaction_date DESC, id DESC) as rn');
|
||||
}, 'sub')
|
||||
->where('rn', 1)
|
||||
->selectRaw('COALESCE(SUM(latest_balance), 0) as total_balance')
|
||||
->first();
|
||||
|
||||
$depositAmount = $depositStats->total ?? 0;
|
||||
$withdrawalAmount = $withdrawalStats->total ?? 0;
|
||||
|
||||
StatFinanceDaily::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
|
||||
[
|
||||
'deposit_count' => $depositStats->cnt ?? 0,
|
||||
'deposit_amount' => $depositAmount,
|
||||
'withdrawal_count' => $withdrawalStats->cnt ?? 0,
|
||||
'withdrawal_amount' => $withdrawalAmount,
|
||||
'net_cashflow' => $depositAmount - $withdrawalAmount,
|
||||
'purchase_count' => $purchaseStats->cnt ?? 0,
|
||||
'purchase_amount' => $purchaseStats->supply_total ?? 0,
|
||||
'purchase_tax_amount' => $purchaseStats->tax_total ?? 0,
|
||||
'receivable_balance' => 0, // Phase 3에서 미수금 로직 추가
|
||||
'payable_balance' => 0,
|
||||
'overdue_receivable' => 0,
|
||||
'bill_issued_count' => $billIssuedStats->cnt ?? 0,
|
||||
'bill_issued_amount' => $billIssuedStats->total ?? 0,
|
||||
'bill_matured_count' => $billMaturedStats->cnt ?? 0,
|
||||
'bill_matured_amount' => $billMaturedStats->total ?? 0,
|
||||
'card_transaction_count' => $cardStats->cnt ?? 0,
|
||||
'card_transaction_amount' => $cardStats->total ?? 0,
|
||||
'bank_balance_total' => $bankBalance->total_balance ?? 0,
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function aggregateMonthly(int $tenantId, int $year, int $month): int
|
||||
{
|
||||
$dailySum = 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,
|
||||
SUM(net_cashflow) as net_cashflow,
|
||||
SUM(purchase_amount) as purchase_total,
|
||||
SUM(card_transaction_amount) as card_total
|
||||
')
|
||||
->first();
|
||||
|
||||
// 월말 데이터 (해당 월의 마지막 일간 레코드)
|
||||
$lastDay = StatFinanceDaily::where('tenant_id', $tenantId)
|
||||
->whereYear('stat_date', $year)
|
||||
->whereMonth('stat_date', $month)
|
||||
->orderByDesc('stat_date')
|
||||
->first();
|
||||
|
||||
// 전월 대비 현금흐름 변화
|
||||
$prevMonth = StatFinanceMonthly::where('tenant_id', $tenantId)
|
||||
->where(function ($q) use ($year, $month) {
|
||||
$prev = Carbon::create($year, $month, 1)->subMonth();
|
||||
$q->where('stat_year', $prev->year)->where('stat_month', $prev->month);
|
||||
})
|
||||
->first();
|
||||
|
||||
$netCashflow = $dailySum->net_cashflow ?? 0;
|
||||
$momChange = null;
|
||||
if ($prevMonth && $prevMonth->net_cashflow != 0) {
|
||||
$momChange = (($netCashflow - $prevMonth->net_cashflow) / abs($prevMonth->net_cashflow)) * 100;
|
||||
}
|
||||
|
||||
StatFinanceMonthly::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_year' => $year, 'stat_month' => $month],
|
||||
[
|
||||
'deposit_total' => $dailySum->deposit_total ?? 0,
|
||||
'withdrawal_total' => $dailySum->withdrawal_total ?? 0,
|
||||
'net_cashflow' => $netCashflow,
|
||||
'purchase_total' => $dailySum->purchase_total ?? 0,
|
||||
'card_total' => $dailySum->card_total ?? 0,
|
||||
'receivable_end' => $lastDay->receivable_balance ?? 0,
|
||||
'payable_end' => $lastDay->payable_balance ?? 0,
|
||||
'bank_balance_end' => $lastDay->bank_balance_total ?? 0,
|
||||
'mom_cashflow_change' => $momChange,
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
149
app/Services/Stats/ProductionStatService.php
Normal file
149
app/Services/Stats/ProductionStatService.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Stats;
|
||||
|
||||
use App\Models\Stats\Daily\StatProductionDaily;
|
||||
use App\Models\Stats\Monthly\StatProductionMonthly;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ProductionStatService implements StatDomainServiceInterface
|
||||
{
|
||||
public function aggregateDaily(int $tenantId, Carbon $date): int
|
||||
{
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
|
||||
// 작업지시 (work_orders)
|
||||
$woCreated = DB::connection('mysql')
|
||||
->table('work_orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
$woCompleted = DB::connection('mysql')
|
||||
->table('work_orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('completed_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
$woInProgress = DB::connection('mysql')
|
||||
->table('work_orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('status', 'in_progress')
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
// 납기 초과 (scheduled_date < today && status not completed/shipped)
|
||||
$woOverdue = DB::connection('mysql')
|
||||
->table('work_orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('scheduled_date', '<', $dateStr)
|
||||
->whereNotIn('status', ['completed', 'shipped', 'cancelled'])
|
||||
->whereNull('deleted_at')
|
||||
->count();
|
||||
|
||||
// 생산 실적 (work_results)
|
||||
$productionStats = DB::connection('mysql')
|
||||
->table('work_results')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('work_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('
|
||||
COALESCE(SUM(production_qty), 0) as production_qty,
|
||||
COALESCE(SUM(defect_qty), 0) as defect_qty,
|
||||
COUNT(DISTINCT worker_id) as active_worker_count
|
||||
')
|
||||
->first();
|
||||
|
||||
$productionQty = $productionStats->production_qty ?? 0;
|
||||
$defectQty = $productionStats->defect_qty ?? 0;
|
||||
$defectRate = $productionQty > 0 ? ($defectQty / $productionQty) * 100 : 0;
|
||||
|
||||
// 납기 준수 (당일 완료된 작업지시 중 scheduled_date >= completed_at인 것)
|
||||
$onTimeCount = DB::connection('mysql')
|
||||
->table('work_orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('completed_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->whereRaw('DATE(completed_at) <= scheduled_date')
|
||||
->count();
|
||||
|
||||
$lateCount = $woCompleted - $onTimeCount;
|
||||
$deliveryRate = $woCompleted > 0 ? ($onTimeCount / $woCompleted) * 100 : 0;
|
||||
|
||||
StatProductionDaily::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
|
||||
[
|
||||
'wo_created_count' => $woCreated,
|
||||
'wo_completed_count' => $woCompleted,
|
||||
'wo_in_progress_count' => $woInProgress,
|
||||
'wo_overdue_count' => $woOverdue,
|
||||
'production_qty' => $productionQty,
|
||||
'defect_qty' => $defectQty,
|
||||
'defect_rate' => round($defectRate, 2),
|
||||
'planned_hours' => 0, // 계획 공수 필드 없음 - 추후 확장
|
||||
'actual_hours' => 0,
|
||||
'efficiency_rate' => 0,
|
||||
'active_worker_count' => $productionStats->active_worker_count ?? 0,
|
||||
'issue_count' => 0, // work_order_issues 테이블 확인 필요
|
||||
'on_time_delivery_count' => $onTimeCount,
|
||||
'late_delivery_count' => max(0, $lateCount),
|
||||
'delivery_rate' => round($deliveryRate, 2),
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function aggregateMonthly(int $tenantId, int $year, int $month): int
|
||||
{
|
||||
$dailyData = StatProductionDaily::where('tenant_id', $tenantId)
|
||||
->whereYear('stat_date', $year)
|
||||
->whereMonth('stat_date', $month)
|
||||
->selectRaw('
|
||||
SUM(wo_created_count) as wo_total_count,
|
||||
SUM(wo_completed_count) as wo_completed_count,
|
||||
SUM(production_qty) as production_qty,
|
||||
SUM(defect_qty) as defect_qty,
|
||||
SUM(planned_hours) as total_planned_hours,
|
||||
SUM(actual_hours) as total_actual_hours,
|
||||
SUM(issue_count) as issue_total_count
|
||||
')
|
||||
->first();
|
||||
|
||||
$productionQty = $dailyData->production_qty ?? 0;
|
||||
$defectQty = $dailyData->defect_qty ?? 0;
|
||||
$avgDefectRate = $productionQty > 0 ? ($defectQty / $productionQty) * 100 : 0;
|
||||
|
||||
// 월평균 효율/납기 (일간 데이터의 평균)
|
||||
$avgRates = StatProductionDaily::where('tenant_id', $tenantId)
|
||||
->whereYear('stat_date', $year)
|
||||
->whereMonth('stat_date', $month)
|
||||
->where('wo_completed_count', '>', 0)
|
||||
->selectRaw('
|
||||
AVG(efficiency_rate) as avg_efficiency_rate,
|
||||
AVG(delivery_rate) as avg_delivery_rate
|
||||
')
|
||||
->first();
|
||||
|
||||
StatProductionMonthly::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_year' => $year, 'stat_month' => $month],
|
||||
[
|
||||
'wo_total_count' => $dailyData->wo_total_count ?? 0,
|
||||
'wo_completed_count' => $dailyData->wo_completed_count ?? 0,
|
||||
'production_qty' => $productionQty,
|
||||
'defect_qty' => $defectQty,
|
||||
'avg_defect_rate' => round($avgDefectRate, 2),
|
||||
'avg_efficiency_rate' => round($avgRates->avg_efficiency_rate ?? 0, 2),
|
||||
'avg_delivery_rate' => round($avgRates->avg_delivery_rate ?? 0, 2),
|
||||
'total_planned_hours' => $dailyData->total_planned_hours ?? 0,
|
||||
'total_actual_hours' => $dailyData->total_actual_hours ?? 0,
|
||||
'issue_total_count' => $dailyData->issue_total_count ?? 0,
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
192
app/Services/Stats/SalesStatService.php
Normal file
192
app/Services/Stats/SalesStatService.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Stats;
|
||||
|
||||
use App\Models\Stats\Daily\StatSalesDaily;
|
||||
use App\Models\Stats\Monthly\StatSalesMonthly;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SalesStatService implements StatDomainServiceInterface
|
||||
{
|
||||
public function aggregateDaily(int $tenantId, Carbon $date): int
|
||||
{
|
||||
$dateStr = $date->format('Y-m-d');
|
||||
|
||||
// 수주 집계 (orders)
|
||||
$orderStats = DB::connection('mysql')
|
||||
->table('orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('
|
||||
COUNT(*) as order_count,
|
||||
COALESCE(SUM(total_amount), 0) as order_amount,
|
||||
COALESCE(SUM(quantity), 0) as order_item_count,
|
||||
SUM(CASE WHEN status_code = ? THEN 1 ELSE 0 END) as order_draft_count,
|
||||
SUM(CASE WHEN status_code = ? THEN 1 ELSE 0 END) as order_confirmed_count,
|
||||
SUM(CASE WHEN status_code = ? THEN 1 ELSE 0 END) as order_in_progress_count,
|
||||
SUM(CASE WHEN status_code = ? THEN 1 ELSE 0 END) as order_completed_count,
|
||||
SUM(CASE WHEN status_code = ? THEN 1 ELSE 0 END) as order_cancelled_count
|
||||
', ['draft', 'confirmed', 'in_progress', 'completed', 'cancelled'])
|
||||
->first();
|
||||
|
||||
// 매출 집계 (sales)
|
||||
$salesStats = DB::connection('mysql')
|
||||
->table('sales')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('sale_date', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('
|
||||
COUNT(*) as sales_count,
|
||||
COALESCE(SUM(supply_amount), 0) as sales_amount,
|
||||
COALESCE(SUM(tax_amount), 0) as sales_tax_amount
|
||||
')
|
||||
->first();
|
||||
|
||||
// 신규 고객 (당일 생성된 고객)
|
||||
$newClientCount = DB::connection('mysql')
|
||||
->table('clients')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->count();
|
||||
|
||||
// 활성 고객 (당일 수주/매출에 연결된 고유 고객)
|
||||
$activeClientCount = DB::connection('mysql')
|
||||
->table('orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereDate('created_at', $dateStr)
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('client_id')
|
||||
->distinct('client_id')
|
||||
->count('client_id');
|
||||
|
||||
// 출하 집계 (shipments)
|
||||
$shipmentStats = DB::connection('mysql')
|
||||
->table('shipments')
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('scheduled_date', $dateStr)
|
||||
->where('status', 'completed')
|
||||
->whereNull('deleted_at')
|
||||
->selectRaw('
|
||||
COUNT(*) as shipment_count,
|
||||
COALESCE(SUM(shipping_cost), 0) as shipment_amount
|
||||
')
|
||||
->first();
|
||||
|
||||
StatSalesDaily::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
|
||||
[
|
||||
'order_count' => $orderStats->order_count ?? 0,
|
||||
'order_amount' => $orderStats->order_amount ?? 0,
|
||||
'order_item_count' => $orderStats->order_item_count ?? 0,
|
||||
'sales_count' => $salesStats->sales_count ?? 0,
|
||||
'sales_amount' => $salesStats->sales_amount ?? 0,
|
||||
'sales_tax_amount' => $salesStats->sales_tax_amount ?? 0,
|
||||
'new_client_count' => $newClientCount,
|
||||
'active_client_count' => $activeClientCount,
|
||||
'order_draft_count' => $orderStats->order_draft_count ?? 0,
|
||||
'order_confirmed_count' => $orderStats->order_confirmed_count ?? 0,
|
||||
'order_in_progress_count' => $orderStats->order_in_progress_count ?? 0,
|
||||
'order_completed_count' => $orderStats->order_completed_count ?? 0,
|
||||
'order_cancelled_count' => $orderStats->order_cancelled_count ?? 0,
|
||||
'shipment_count' => $shipmentStats->shipment_count ?? 0,
|
||||
'shipment_amount' => $shipmentStats->shipment_amount ?? 0,
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function aggregateMonthly(int $tenantId, int $year, int $month): int
|
||||
{
|
||||
// 일간 데이터를 합산하여 월간 통계 생성
|
||||
$dailySum = StatSalesDaily::where('tenant_id', $tenantId)
|
||||
->whereYear('stat_date', $year)
|
||||
->whereMonth('stat_date', $month)
|
||||
->selectRaw('
|
||||
SUM(order_count) as order_count,
|
||||
SUM(order_amount) as order_amount,
|
||||
SUM(sales_count) as sales_count,
|
||||
SUM(sales_amount) as sales_amount,
|
||||
SUM(shipment_count) as shipment_count,
|
||||
SUM(shipment_amount) as shipment_amount
|
||||
')
|
||||
->first();
|
||||
|
||||
// 월간 고유 거래 고객 수
|
||||
$startDate = Carbon::create($year, $month, 1)->format('Y-m-d');
|
||||
$endDate = Carbon::create($year, $month, 1)->endOfMonth()->format('Y-m-d');
|
||||
|
||||
$uniqueClientCount = DB::connection('mysql')
|
||||
->table('orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereBetween(DB::raw('DATE(created_at)'), [$startDate, $endDate])
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('client_id')
|
||||
->distinct('client_id')
|
||||
->count('client_id');
|
||||
|
||||
// 평균 수주 금액
|
||||
$orderCount = $dailySum->order_count ?? 0;
|
||||
$orderAmount = $dailySum->order_amount ?? 0;
|
||||
$avgOrderAmount = $orderCount > 0 ? $orderAmount / $orderCount : 0;
|
||||
|
||||
// 최다 거래 고객
|
||||
$topClient = DB::connection('mysql')
|
||||
->table('orders')
|
||||
->where('tenant_id', $tenantId)
|
||||
->whereBetween(DB::raw('DATE(created_at)'), [$startDate, $endDate])
|
||||
->whereNull('deleted_at')
|
||||
->whereNotNull('client_id')
|
||||
->groupBy('client_id')
|
||||
->orderByRaw('SUM(total_amount) DESC')
|
||||
->selectRaw('client_id, SUM(total_amount) as total')
|
||||
->first();
|
||||
|
||||
// 전월 대비 성장률
|
||||
$prevMonth = StatSalesMonthly::where('tenant_id', $tenantId)
|
||||
->where(function ($q) use ($year, $month) {
|
||||
$prev = Carbon::create($year, $month, 1)->subMonth();
|
||||
$q->where('stat_year', $prev->year)->where('stat_month', $prev->month);
|
||||
})
|
||||
->first();
|
||||
|
||||
$salesAmount = $dailySum->sales_amount ?? 0;
|
||||
$momGrowthRate = null;
|
||||
if ($prevMonth && $prevMonth->sales_amount > 0) {
|
||||
$momGrowthRate = (($salesAmount - $prevMonth->sales_amount) / $prevMonth->sales_amount) * 100;
|
||||
}
|
||||
|
||||
// 전년동월 대비
|
||||
$prevYear = StatSalesMonthly::where('tenant_id', $tenantId)
|
||||
->where('stat_year', $year - 1)
|
||||
->where('stat_month', $month)
|
||||
->first();
|
||||
|
||||
$yoyGrowthRate = null;
|
||||
if ($prevYear && $prevYear->sales_amount > 0) {
|
||||
$yoyGrowthRate = (($salesAmount - $prevYear->sales_amount) / $prevYear->sales_amount) * 100;
|
||||
}
|
||||
|
||||
StatSalesMonthly::updateOrCreate(
|
||||
['tenant_id' => $tenantId, 'stat_year' => $year, 'stat_month' => $month],
|
||||
[
|
||||
'order_count' => $dailySum->order_count ?? 0,
|
||||
'order_amount' => $orderAmount,
|
||||
'sales_count' => $dailySum->sales_count ?? 0,
|
||||
'sales_amount' => $salesAmount,
|
||||
'shipment_count' => $dailySum->shipment_count ?? 0,
|
||||
'shipment_amount' => $dailySum->shipment_amount ?? 0,
|
||||
'unique_client_count' => $uniqueClientCount,
|
||||
'avg_order_amount' => $avgOrderAmount,
|
||||
'top_client_id' => $topClient->client_id ?? null,
|
||||
'top_client_amount' => $topClient->total ?? 0,
|
||||
'mom_growth_rate' => $momGrowthRate,
|
||||
'yoy_growth_rate' => $yoyGrowthRate,
|
||||
]
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,14 @@
|
||||
class StatAggregatorService
|
||||
{
|
||||
/**
|
||||
* 일간 도메인 서비스 매핑 (Phase 2에서 구현체 추가)
|
||||
* 일간 도메인 서비스 매핑
|
||||
*/
|
||||
private function getDailyDomainServices(): array
|
||||
{
|
||||
return [
|
||||
// 'sales' => SalesStatService::class,
|
||||
// 'finance' => FinanceStatService::class,
|
||||
// 'production' => ProductionStatService::class,
|
||||
'sales' => SalesStatService::class,
|
||||
'finance' => FinanceStatService::class,
|
||||
'production' => ProductionStatService::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ private function getDailyDomainServices(): array
|
||||
private function getMonthlyDomainServices(): array
|
||||
{
|
||||
return [
|
||||
// 'sales' => SalesStatService::class,
|
||||
// 'finance' => FinanceStatService::class,
|
||||
// 'production' => ProductionStatService::class,
|
||||
'sales' => SalesStatService::class,
|
||||
'finance' => FinanceStatService::class,
|
||||
'production' => ProductionStatService::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_sales_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 수주
|
||||
$table->unsignedInteger('order_count')->default(0)->comment('신규 수주 건수');
|
||||
$table->decimal('order_amount', 18, 2)->default(0)->comment('수주 금액');
|
||||
$table->unsignedInteger('order_item_count')->default(0)->comment('수주 품목 수');
|
||||
|
||||
// 매출
|
||||
$table->unsignedInteger('sales_count')->default(0)->comment('매출 건수');
|
||||
$table->decimal('sales_amount', 18, 2)->default(0)->comment('매출 금액');
|
||||
$table->decimal('sales_tax_amount', 18, 2)->default(0)->comment('세액');
|
||||
|
||||
// 고객
|
||||
$table->unsignedInteger('new_client_count')->default(0)->comment('신규 고객 수');
|
||||
$table->unsignedInteger('active_client_count')->default(0)->comment('활성 고객 수');
|
||||
|
||||
// 수주 상태별 건수
|
||||
$table->unsignedInteger('order_draft_count')->default(0);
|
||||
$table->unsignedInteger('order_confirmed_count')->default(0);
|
||||
$table->unsignedInteger('order_in_progress_count')->default(0);
|
||||
$table->unsignedInteger('order_completed_count')->default(0);
|
||||
$table->unsignedInteger('order_cancelled_count')->default(0);
|
||||
|
||||
// 출하
|
||||
$table->unsignedInteger('shipment_count')->default(0);
|
||||
$table->decimal('shipment_amount', 18, 2)->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
$table->index('tenant_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_sales_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_finance_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 입출금
|
||||
$table->unsignedInteger('deposit_count')->default(0);
|
||||
$table->decimal('deposit_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('withdrawal_count')->default(0);
|
||||
$table->decimal('withdrawal_amount', 18, 2)->default(0);
|
||||
$table->decimal('net_cashflow', 18, 2)->default(0)->comment('입금 - 출금');
|
||||
|
||||
// 매입
|
||||
$table->unsignedInteger('purchase_count')->default(0);
|
||||
$table->decimal('purchase_amount', 18, 2)->default(0);
|
||||
$table->decimal('purchase_tax_amount', 18, 2)->default(0);
|
||||
|
||||
// 미수/미지급
|
||||
$table->decimal('receivable_balance', 18, 2)->default(0)->comment('미수금 잔액');
|
||||
$table->decimal('payable_balance', 18, 2)->default(0)->comment('미지급 잔액');
|
||||
$table->decimal('overdue_receivable', 18, 2)->default(0)->comment('연체 미수금');
|
||||
|
||||
// 어음
|
||||
$table->unsignedInteger('bill_issued_count')->default(0);
|
||||
$table->decimal('bill_issued_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('bill_matured_count')->default(0);
|
||||
$table->decimal('bill_matured_amount', 18, 2)->default(0);
|
||||
|
||||
// 카드
|
||||
$table->unsignedInteger('card_transaction_count')->default(0);
|
||||
$table->decimal('card_transaction_amount', 18, 2)->default(0);
|
||||
|
||||
// 은행
|
||||
$table->decimal('bank_balance_total', 18, 2)->default(0)->comment('전 계좌 잔액 합계');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_finance_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_finance_monthly', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->smallInteger('stat_year');
|
||||
$table->tinyInteger('stat_month');
|
||||
|
||||
$table->decimal('deposit_total', 18, 2)->default(0);
|
||||
$table->decimal('withdrawal_total', 18, 2)->default(0);
|
||||
$table->decimal('net_cashflow', 18, 2)->default(0);
|
||||
$table->decimal('purchase_total', 18, 2)->default(0);
|
||||
$table->decimal('card_total', 18, 2)->default(0);
|
||||
|
||||
$table->decimal('receivable_end', 18, 2)->default(0)->comment('월말 미수금');
|
||||
$table->decimal('payable_end', 18, 2)->default(0)->comment('월말 미지급');
|
||||
$table->decimal('bank_balance_end', 18, 2)->default(0)->comment('월말 잔액');
|
||||
|
||||
$table->decimal('mom_cashflow_change', 8, 2)->nullable()->comment('전월 대비 현금흐름 변화 (%)');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_year', 'stat_month'], 'uk_tenant_year_month');
|
||||
$table->index(['stat_year', 'stat_month']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_finance_monthly');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_sales_monthly', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->smallInteger('stat_year');
|
||||
$table->tinyInteger('stat_month');
|
||||
|
||||
// 일일 합산
|
||||
$table->unsignedInteger('order_count')->default(0);
|
||||
$table->decimal('order_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('sales_count')->default(0);
|
||||
$table->decimal('sales_amount', 18, 2)->default(0);
|
||||
$table->unsignedInteger('shipment_count')->default(0);
|
||||
$table->decimal('shipment_amount', 18, 2)->default(0);
|
||||
|
||||
// 월간 고유 지표
|
||||
$table->unsignedInteger('unique_client_count')->default(0)->comment('거래 고객 수');
|
||||
$table->decimal('avg_order_amount', 18, 2)->default(0)->comment('평균 수주 금액');
|
||||
$table->unsignedBigInteger('top_client_id')->nullable()->comment('최다 거래 고객');
|
||||
$table->decimal('top_client_amount', 18, 2)->default(0);
|
||||
$table->decimal('mom_growth_rate', 8, 2)->nullable()->comment('전월 대비 성장률 (%)');
|
||||
$table->decimal('yoy_growth_rate', 8, 2)->nullable()->comment('전년동월 대비 (%)');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_year', 'stat_month'], 'uk_tenant_year_month');
|
||||
$table->index(['stat_year', 'stat_month']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_sales_monthly');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_production_daily', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->date('stat_date');
|
||||
|
||||
// 작업지시
|
||||
$table->unsignedInteger('wo_created_count')->default(0)->comment('신규 작업지시');
|
||||
$table->unsignedInteger('wo_completed_count')->default(0)->comment('완료 작업지시');
|
||||
$table->unsignedInteger('wo_in_progress_count')->default(0)->comment('진행중');
|
||||
$table->unsignedInteger('wo_overdue_count')->default(0)->comment('납기 초과');
|
||||
|
||||
// 생산량
|
||||
$table->decimal('production_qty', 18, 2)->default(0)->comment('생산 수량');
|
||||
$table->decimal('defect_qty', 18, 2)->default(0)->comment('불량 수량');
|
||||
$table->decimal('defect_rate', 5, 2)->default(0)->comment('불량률 (%)');
|
||||
|
||||
// 작업 효율
|
||||
$table->decimal('planned_hours', 10, 2)->default(0)->comment('계획 공수');
|
||||
$table->decimal('actual_hours', 10, 2)->default(0)->comment('실적 공수');
|
||||
$table->decimal('efficiency_rate', 5, 2)->default(0)->comment('효율 (%)');
|
||||
|
||||
// 작업자
|
||||
$table->unsignedInteger('active_worker_count')->default(0);
|
||||
$table->unsignedInteger('issue_count')->default(0)->comment('발생 이슈 수');
|
||||
|
||||
// 납기
|
||||
$table->unsignedInteger('on_time_delivery_count')->default(0);
|
||||
$table->unsignedInteger('late_delivery_count')->default(0);
|
||||
$table->decimal('delivery_rate', 5, 2)->default(0)->comment('납기준수율 (%)');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_date'], 'uk_tenant_date');
|
||||
$table->index('stat_date');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_production_daily');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
protected $connection = 'sam_stat';
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Schema::connection($this->connection)->create('stat_production_monthly', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('tenant_id');
|
||||
$table->smallInteger('stat_year');
|
||||
$table->tinyInteger('stat_month');
|
||||
|
||||
$table->unsignedInteger('wo_total_count')->default(0);
|
||||
$table->unsignedInteger('wo_completed_count')->default(0);
|
||||
$table->decimal('production_qty', 18, 2)->default(0);
|
||||
$table->decimal('defect_qty', 18, 2)->default(0);
|
||||
$table->decimal('avg_defect_rate', 5, 2)->default(0);
|
||||
$table->decimal('avg_efficiency_rate', 5, 2)->default(0);
|
||||
$table->decimal('avg_delivery_rate', 5, 2)->default(0);
|
||||
$table->decimal('total_planned_hours', 10, 2)->default(0);
|
||||
$table->decimal('total_actual_hours', 10, 2)->default(0);
|
||||
$table->unsignedInteger('issue_total_count')->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['tenant_id', 'stat_year', 'stat_month'], 'uk_tenant_year_month');
|
||||
$table->index(['stat_year', 'stat_month']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::connection($this->connection)->dropIfExists('stat_production_monthly');
|
||||
}
|
||||
};
|
||||
@@ -93,3 +93,29 @@
|
||||
->onFailure(function () {
|
||||
\Illuminate\Support\Facades\Log::error('❌ storage:record-usage 스케줄러 실행 실패', ['time' => now()]);
|
||||
});
|
||||
|
||||
// ─── 통계 집계 (sam_stat DB) ───
|
||||
|
||||
// 매일 새벽 02:00에 일간 통계 집계 (전일 데이터)
|
||||
Schedule::command('stat:aggregate-daily')
|
||||
->dailyAt('02:00')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/scheduler.log'))
|
||||
->onSuccess(function () {
|
||||
\Illuminate\Support\Facades\Log::info('✅ stat:aggregate-daily 스케줄러 실행 성공', ['time' => now()]);
|
||||
})
|
||||
->onFailure(function () {
|
||||
\Illuminate\Support\Facades\Log::error('❌ stat:aggregate-daily 스케줄러 실행 실패', ['time' => now()]);
|
||||
});
|
||||
|
||||
// 매월 1일 새벽 03:00에 월간 통계 집계 (전월 데이터)
|
||||
Schedule::command('stat:aggregate-monthly')
|
||||
->monthlyOn(1, '03:00')
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/scheduler.log'))
|
||||
->onSuccess(function () {
|
||||
\Illuminate\Support\Facades\Log::info('✅ stat:aggregate-monthly 스케줄러 실행 성공', ['time' => now()]);
|
||||
})
|
||||
->onFailure(function () {
|
||||
\Illuminate\Support\Facades\Log::error('❌ stat:aggregate-monthly 스케줄러 실행 실패', ['time' => now()]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user