where('tenant_id', $tenantId) ->pluck('user_id') ->toArray(); if (empty($userIds)) { $this->command->warn(' ⚠ attendances: 사용자가 없습니다'); return; } // 최근 30일간의 근태 데이터 생성 $startDate = now()->subDays(30); $endDate = now(); $count = 0; // 상태 분포 (enum: onTime, late, absent, vacation, businessTrip, fieldWork, overtime, remote) $statuses = [ 'onTime' => 60, // 정시 출근 60% 'late' => 10, // 지각 10% 'vacation' => 10, // 휴가 10% 'absent' => 5, // 결근 5% 'businessTrip' => 5, // 출장 5% 'remote' => 5, // 재택 5% 'overtime' => 5, // 초과근무 5% ]; foreach ($userIds as $uId) { $currentDate = clone $startDate; while ($currentDate <= $endDate) { // 주말 제외 if ($currentDate->isWeekend()) { $currentDate->addDay(); continue; } $baseDate = $currentDate->format('Y-m-d'); // 이미 존재하는지 확인 $exists = Attendance::where('tenant_id', $tenantId) ->where('user_id', $uId) ->where('base_date', $baseDate) ->exists(); if ($exists) { $currentDate->addDay(); continue; } // 상태 결정 (가중치 기반 랜덤) $status = $this->getRandomStatus($statuses); // 출퇴근 시간 생성 $jsonDetails = $this->generateTimeDetails($status); Attendance::create([ 'tenant_id' => $tenantId, 'user_id' => $uId, 'base_date' => $baseDate, 'status' => $status, 'json_details' => $jsonDetails, 'remarks' => $this->getRemarks($status), 'created_by' => $userId, ]); $count++; $currentDate->addDay(); } } $this->command->info(' ✓ attendances: '.$count.'건 생성'); } private function getRandomStatus(array $weights): string { $total = array_sum($weights); $rand = rand(1, $total); $cumulative = 0; foreach ($weights as $status => $weight) { $cumulative += $weight; if ($rand <= $cumulative) { return $status; } } return 'normal'; } private function generateTimeDetails(string $status): array { $checkIn = null; $checkOut = null; $workMinutes = 0; $lateMinutes = 0; $earlyLeaveMinutes = 0; $overtimeMinutes = 0; $standardStart = 9 * 60; // 09:00 in minutes $standardEnd = 18 * 60; // 18:00 in minutes $breakMinutes = 60; switch ($status) { case 'onTime': $startVariance = rand(-10, 10); // ±10분 $endVariance = rand(-5, 30); // -5분 ~ +30분 $checkIn = sprintf('%02d:%02d:00', 9, max(0, $startVariance)); $checkOut = sprintf('%02d:%02d:00', 18 + intdiv($endVariance, 60), abs($endVariance) % 60); $workMinutes = ($standardEnd - $standardStart - $breakMinutes) + $endVariance; break; case 'late': $lateMinutes = rand(10, 60); // 10분 ~ 1시간 지각 $checkIn = sprintf('%02d:%02d:00', 9 + intdiv($lateMinutes, 60), $lateMinutes % 60); $checkOut = '18:00:00'; $workMinutes = ($standardEnd - $standardStart - $breakMinutes) - $lateMinutes; break; case 'overtime': $checkIn = '09:00:00'; $overtimeMinutes = rand(60, 180); // 1시간 ~ 3시간 초과근무 $endTime = $standardEnd + $overtimeMinutes; $checkOut = sprintf('%02d:%02d:00', intdiv($endTime, 60), $endTime % 60); $workMinutes = ($standardEnd - $standardStart - $breakMinutes) + $overtimeMinutes; break; case 'remote': $checkIn = sprintf('%02d:%02d:00', 9, rand(0, 10)); $checkOut = sprintf('%02d:%02d:00', 18, rand(0, 30)); $workMinutes = ($standardEnd - $standardStart - $breakMinutes); break; case 'businessTrip': case 'fieldWork': $checkIn = sprintf('%02d:%02d:00', 8, rand(0, 30)); // 출장은 일찍 시작 $checkOut = sprintf('%02d:%02d:00', 19, rand(0, 60)); // 늦게 종료 $workMinutes = 10 * 60 - $breakMinutes; // 10시간 근무 break; case 'vacation': case 'absent': // 출퇴근 기록 없음 break; } return [ 'check_in' => $checkIn, 'check_out' => $checkOut, 'work_minutes' => max(0, $workMinutes), 'late_minutes' => $lateMinutes, 'early_leave_minutes' => $earlyLeaveMinutes, 'overtime_minutes' => $overtimeMinutes, 'vacation_type' => $status === 'vacation' ? 'annual' : null, 'gps_data' => $checkIn ? [ 'check_in' => ['lat' => 37.5012 + (rand(-10, 10) / 10000), 'lng' => 127.0396 + (rand(-10, 10) / 10000)], 'check_out' => $checkOut ? ['lat' => 37.5012 + (rand(-10, 10) / 10000), 'lng' => 127.0396 + (rand(-10, 10) / 10000)] : null, ] : null, ]; } private function getRemarks(string $status): ?string { return match ($status) { 'late' => '지각 - 교통 체증', 'vacation' => '연차 휴가', 'absent' => '결근', 'businessTrip' => '출장 - 외부 미팅', 'fieldWork' => '외근 - 현장 방문', 'overtime' => '초과근무 - 프로젝트 마감', 'remote' => '재택근무', default => null, }; } }