- 영업(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>
150 lines
5.9 KiB
PHP
150 lines
5.9 KiB
PHP
<?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;
|
|
}
|
|
}
|