Files
sam-api/app/Services/Stats/HrStatService.php
권혁성 595e3d59b4 feat: sam_stat P1 도메인 확장 (Phase 3)
- 차원 테이블: dim_client, dim_product 마이그레이션 + SCD Type 2 동기화 (DimensionSyncService)
- 재고 통계: stat_inventory_daily + InventoryStatService (stocks, stock_transactions, inspections)
- 견적/영업 통계: stat_quote_pipeline_daily + QuoteStatService (quotes, biddings, sales_prospects)
- 인사/근태 통계: stat_hr_attendance_daily + HrStatService (attendances, leaves, user_tenants)
- KPI/알림: stat_kpi_targets, stat_alerts + KpiAlertService + StatCheckKpiAlertsCommand
- StatAggregatorService에 inventory, quote, hr 도메인 추가 (총 6개 도메인)
- 스케줄러: stat:check-kpi-alerts 매일 09:00 등록

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 20:19:50 +09:00

88 lines
3.5 KiB
PHP

<?php
namespace App\Services\Stats;
use App\Models\Stats\Daily\StatHrAttendanceDaily;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class HrStatService implements StatDomainServiceInterface
{
public function aggregateDaily(int $tenantId, Carbon $date): int
{
$dateStr = $date->format('Y-m-d');
// 전체 직원 수 (tenant_user_profiles 기준)
$totalEmployees = DB::connection('mysql')
->table('user_tenants')
->where('tenant_id', $tenantId)
->where('is_active', true)
->count();
// 근태 (attendances)
$attendanceStats = DB::connection('mysql')
->table('attendances')
->where('tenant_id', $tenantId)
->where('base_date', $dateStr)
->whereNull('deleted_at')
->selectRaw("
COUNT(*) as total_count,
SUM(CASE WHEN status = 'onTime' THEN 1 ELSE 0 END) as on_time_count,
SUM(CASE WHEN status = 'late' THEN 1 ELSE 0 END) as late_count,
SUM(CASE WHEN status = 'absent' THEN 1 ELSE 0 END) as absent_count,
SUM(CASE WHEN status = 'overtime' THEN 1 ELSE 0 END) as overtime_count
")
->first();
$attendanceCount = ($attendanceStats->on_time_count ?? 0)
+ ($attendanceStats->late_count ?? 0)
+ ($attendanceStats->overtime_count ?? 0);
$attendanceRate = $totalEmployees > 0 ? ($attendanceCount / $totalEmployees) * 100 : 0;
// 휴가 (leaves)
$leaveStats = DB::connection('mysql')
->table('leaves')
->where('tenant_id', $tenantId)
->where('start_date', '<=', $dateStr)
->where('end_date', '>=', $dateStr)
->where('status', 'approved')
->whereNull('deleted_at')
->selectRaw("
COUNT(*) as total_count,
SUM(CASE WHEN leave_type = 'annual' THEN 1 ELSE 0 END) as annual_count,
SUM(CASE WHEN leave_type = 'sick' THEN 1 ELSE 0 END) as sick_count,
SUM(CASE WHEN leave_type NOT IN ('annual', 'sick') THEN 1 ELSE 0 END) as other_count
")
->first();
// 초과근무 (attendances status = 'overtime')
$overtimeCount = $attendanceStats->overtime_count ?? 0;
StatHrAttendanceDaily::updateOrCreate(
['tenant_id' => $tenantId, 'stat_date' => $dateStr],
[
'total_employees' => $totalEmployees,
'attendance_count' => $attendanceCount,
'late_count' => $attendanceStats->late_count ?? 0,
'absent_count' => $attendanceStats->absent_count ?? 0,
'attendance_rate' => $attendanceRate,
'leave_count' => $leaveStats->total_count ?? 0,
'leave_annual_count' => $leaveStats->annual_count ?? 0,
'leave_sick_count' => $leaveStats->sick_count ?? 0,
'leave_other_count' => $leaveStats->other_count ?? 0,
'overtime_hours' => 0, // attendances에 시간 정보 없음
'overtime_employee_count' => $overtimeCount,
'total_labor_cost' => 0, // 일간 인건비는 급여 정산 시 계산
]
);
return 1;
}
public function aggregateMonthly(int $tenantId, int $year, int $month): int
{
// 인사 도메인은 일간 테이블만 운영 (월간은 Phase 4에서 필요시 추가)
return 0;
}
}