tenantId(); $query = Attendance::query() ->where('tenant_id', $tenantId) ->with(['user:id,name,email']); // 사용자 필터 if (! empty($params['user_id'])) { $query->where('user_id', $params['user_id']); } // 날짜 필터 (단일) if (! empty($params['date'])) { $query->whereDate('base_date', $params['date']); } // 날짜 범위 필터 if (! empty($params['date_from'])) { $query->whereDate('base_date', '>=', $params['date_from']); } if (! empty($params['date_to'])) { $query->whereDate('base_date', '<=', $params['date_to']); } // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 부서 필터 (사용자의 부서) if (! empty($params['department_id'])) { $query->whereHas('user.tenantProfile', function ($q) use ($params) { $q->where('department_id', $params['department_id']); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'base_date'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); // 페이지네이션 $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 근태 상세 조회 */ public function show(int $id): Attendance { $tenantId = $this->tenantId(); $attendance = Attendance::query() ->where('tenant_id', $tenantId) ->with(['user:id,name,email']) ->findOrFail($id); return $attendance; } /** * 근태 등록 */ public function store(array $data): Attendance { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 기존 기록 확인 (같은 날 같은 사용자) $existing = Attendance::query() ->where('tenant_id', $tenantId) ->where('user_id', $data['user_id']) ->whereDate('base_date', $data['base_date']) ->first(); if ($existing) { throw new \Exception(__('error.attendance.already_exists')); } // json_details 구성 // json_details 객체가 직접 전달된 경우 그대로 사용, 아니면 개별 필드에서 구성 $jsonDetails = isset($data['json_details']) && is_array($data['json_details']) ? $data['json_details'] : $this->buildJsonDetails($data); $attendance = Attendance::create([ 'tenant_id' => $tenantId, 'user_id' => $data['user_id'], 'base_date' => $data['base_date'], 'status' => $data['status'] ?? 'onTime', 'json_details' => $jsonDetails, 'remarks' => $data['remarks'] ?? null, 'created_by' => $userId, 'updated_by' => $userId, ]); return $attendance->fresh(['user:id,name,email']); }); } /** * 근태 수정 */ public function update(int $id, array $data): Attendance { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $attendance = Attendance::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 기본 필드 업데이트 if (isset($data['status'])) { $attendance->status = $data['status']; } if (array_key_exists('remarks', $data)) { $attendance->remarks = $data['remarks']; } // json_details 업데이트 $jsonDetails = $attendance->json_details ?? []; // json_details 객체가 직접 전달된 경우 그대로 병합, 아니면 개별 필드에서 구성 $detailsUpdate = isset($data['json_details']) && is_array($data['json_details']) ? $data['json_details'] : $this->buildJsonDetails($data); $attendance->json_details = array_merge($jsonDetails, $detailsUpdate); $attendance->updated_by = $userId; $attendance->save(); return $attendance->fresh(['user:id,name,email']); }); } /** * 근태 삭제 */ public function destroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $attendance = Attendance::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $attendance->deleted_by = $userId; $attendance->save(); $attendance->delete(); return true; } /** * 일괄 삭제 */ public function bulkDelete(array $ids): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $deletedCount = 0; DB::transaction(function () use ($ids, $tenantId, $userId, &$deletedCount) { $attendances = Attendance::query() ->where('tenant_id', $tenantId) ->whereIn('id', $ids) ->get(); foreach ($attendances as $attendance) { $attendance->deleted_by = $userId; $attendance->save(); $attendance->delete(); $deletedCount++; } }); return ['deleted_count' => $deletedCount]; } /** * 출근 기록 (체크인) */ public function checkIn(array $data): Attendance { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $targetUserId = $data['user_id'] ?? $userId; $today = now()->toDateString(); return DB::transaction(function () use ($data, $tenantId, $userId, $targetUserId, $today) { // 오늘 기록 확인 $attendance = Attendance::query() ->where('tenant_id', $tenantId) ->where('user_id', $targetUserId) ->whereDate('base_date', $today) ->first(); $checkInTime = $data['check_in'] ?? now()->format('H:i:s'); $gpsData = $data['gps_data'] ?? null; if ($attendance) { // 기존 기록 업데이트 $jsonDetails = $attendance->json_details ?? []; $jsonDetails['check_in'] = $checkInTime; if ($gpsData) { $jsonDetails['gps_data'] = array_merge( $jsonDetails['gps_data'] ?? [], ['check_in' => $gpsData] ); } $attendance->json_details = $jsonDetails; $attendance->updated_by = $userId; $attendance->save(); } else { // 새 기록 생성 $jsonDetails = ['check_in' => $checkInTime]; if ($gpsData) { $jsonDetails['gps_data'] = ['check_in' => $gpsData]; } $attendance = Attendance::create([ 'tenant_id' => $tenantId, 'user_id' => $targetUserId, 'base_date' => $today, 'status' => $this->determineStatus($checkInTime, null), 'json_details' => $jsonDetails, 'created_by' => $userId, 'updated_by' => $userId, ]); } return $attendance->fresh(['user:id,name,email']); }); } /** * 퇴근 기록 (체크아웃) */ public function checkOut(array $data): Attendance { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $targetUserId = $data['user_id'] ?? $userId; $today = now()->toDateString(); return DB::transaction(function () use ($data, $tenantId, $userId, $targetUserId, $today) { $attendance = Attendance::query() ->where('tenant_id', $tenantId) ->where('user_id', $targetUserId) ->whereDate('base_date', $today) ->first(); if (! $attendance) { throw new \Exception(__('error.attendance.no_check_in')); } $checkOutTime = $data['check_out'] ?? now()->format('H:i:s'); $gpsData = $data['gps_data'] ?? null; $jsonDetails = $attendance->json_details ?? []; $jsonDetails['check_out'] = $checkOutTime; // 근무 시간 계산 if (isset($jsonDetails['check_in'])) { $checkIn = \Carbon\Carbon::createFromFormat('H:i:s', $jsonDetails['check_in']); $checkOut = \Carbon\Carbon::createFromFormat('H:i:s', $checkOutTime); $jsonDetails['work_minutes'] = $checkOut->diffInMinutes($checkIn); } if ($gpsData) { $jsonDetails['gps_data'] = array_merge( $jsonDetails['gps_data'] ?? [], ['check_out' => $gpsData] ); } $attendance->json_details = $jsonDetails; $attendance->status = $this->determineStatus( $jsonDetails['check_in'] ?? null, $checkOutTime ); $attendance->updated_by = $userId; $attendance->save(); return $attendance->fresh(['user:id,name,email']); }); } /** * 월간 통계 조회 */ public function monthlyStats(array $params): array { $tenantId = $this->tenantId(); $year = $params['year'] ?? now()->year; $month = $params['month'] ?? now()->month; $userId = $params['user_id'] ?? null; $startDate = \Carbon\Carbon::create($year, $month, 1)->startOfMonth(); $endDate = $startDate->copy()->endOfMonth(); $query = Attendance::query() ->where('tenant_id', $tenantId) ->whereBetween('base_date', [$startDate, $endDate]); if ($userId) { $query->where('user_id', $userId); } $attendances = $query->get(); $stats = [ 'year' => $year, 'month' => $month, 'total_days' => $attendances->count(), 'by_status' => [ 'onTime' => $attendances->where('status', 'onTime')->count(), 'late' => $attendances->where('status', 'late')->count(), 'absent' => $attendances->where('status', 'absent')->count(), 'vacation' => $attendances->where('status', 'vacation')->count(), 'businessTrip' => $attendances->where('status', 'businessTrip')->count(), 'fieldWork' => $attendances->where('status', 'fieldWork')->count(), 'overtime' => $attendances->where('status', 'overtime')->count(), 'remote' => $attendances->where('status', 'remote')->count(), ], 'total_work_minutes' => $attendances->sum(function ($a) { return $a->json_details['work_minutes'] ?? 0; }), 'total_overtime_minutes' => $attendances->sum(function ($a) { return $a->json_details['overtime_minutes'] ?? 0; }), ]; return $stats; } /** * json_details 구성 */ private function buildJsonDetails(array $data): array { $details = []; $detailKeys = [ 'check_in', 'check_out', 'gps_data', 'external_work', 'multiple_entries', 'work_minutes', 'overtime_minutes', 'late_minutes', 'early_leave_minutes', 'vacation_type', ]; foreach ($detailKeys as $key) { if (isset($data[$key])) { $details[$key] = $data[$key]; } } return $details; } /** * 상태 자동 결정 * 실제 업무에서는 회사별 출근 시간 설정을 참조해야 함 */ private function determineStatus(?string $checkIn, ?string $checkOut): string { if (! $checkIn) { return 'absent'; } // 기본 출근 시간: 09:00 $standardCheckIn = '09:00:00'; if ($checkIn > $standardCheckIn) { return 'late'; } return 'onTime'; } }