Files
sam-manage/app/Services/HR/AttendanceRequestService.php
김보곤 adc587292f feat: [attendance] 근태관리 2차 고도화 8개 기능 구현
- 월간 캘린더 뷰 (사원별 필터, 날짜 클릭 등록, HTMX 월 이동)
- 일괄 등록 (다수 사원 체크박스 선택 후 일괄 등록, upsert 처리)
- 사원별 월간 요약 (상태별 카운트 + 총 근무시간 집계 테이블)
- 초과근무 알림 (주 48h 경고 / 52h 위험 배너)
- 근태 승인 워크플로우 (신청→승인→근태 레코드 자동 생성)
- 자동 결근 처리 (매일 23:50 스케줄러, 주말 제외)
- 연차 관리 연동 (휴가 등록 시 leave_balances 자동 차감)
- GPS 출퇴근 UI (테이블 GPS 아이콘 + 상세 모달)
- 탭 네비게이션 (목록/캘린더/요약/승인) HTMX 기반 전환
2026-02-26 21:29:25 +09:00

149 lines
4.2 KiB
PHP

<?php
namespace App\Services\HR;
use App\Models\HR\Attendance;
use App\Models\HR\AttendanceRequest;
use Carbon\CarbonPeriod;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class AttendanceRequestService
{
/**
* 신청 목록 조회
*/
public function getRequests(array $filters = [], int $perPage = 20): LengthAwarePaginator
{
$tenantId = session('selected_tenant_id');
$query = AttendanceRequest::query()
->with(['user', 'user.tenantProfiles' => fn ($q) => $q->where('tenant_id', $tenantId), 'approver'])
->forTenant($tenantId)
->orderByRaw("FIELD(status, 'pending', 'approved', 'rejected')")
->orderBy('created_at', 'desc');
if (! empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (! empty($filters['user_id'])) {
$query->where('user_id', $filters['user_id']);
}
return $query->paginate($perPage);
}
/**
* 신청 등록
*/
public function storeRequest(array $data): AttendanceRequest
{
$tenantId = session('selected_tenant_id');
return AttendanceRequest::create([
'tenant_id' => $tenantId,
'user_id' => $data['user_id'],
'request_type' => $data['request_type'],
'start_date' => $data['start_date'],
'end_date' => $data['end_date'],
'reason' => $data['reason'] ?? null,
'status' => 'pending',
'json_details' => $data['json_details'] ?? null,
]);
}
/**
* 승인 처리
*/
public function approve(int $id): ?AttendanceRequest
{
$tenantId = session('selected_tenant_id');
$request = AttendanceRequest::query()
->forTenant($tenantId)
->where('status', 'pending')
->find($id);
if (! $request) {
return null;
}
return DB::transaction(function () use ($request, $tenantId) {
$request->update([
'status' => 'approved',
'approved_by' => auth()->id(),
'approved_at' => now(),
]);
// 승인 시 해당 기간의 근태 레코드 자동 생성
$this->createAttendanceRecords($request, $tenantId);
return $request->fresh(['user', 'approver']);
});
}
/**
* 반려 처리
*/
public function reject(int $id, ?string $reason = null): ?AttendanceRequest
{
$tenantId = session('selected_tenant_id');
$request = AttendanceRequest::query()
->forTenant($tenantId)
->where('status', 'pending')
->find($id);
if (! $request) {
return null;
}
$request->update([
'status' => 'rejected',
'approved_by' => auth()->id(),
'approved_at' => now(),
'reject_reason' => $reason,
]);
return $request->fresh(['user', 'approver']);
}
/**
* 승인 후 근태 레코드 자동 생성
*/
private function createAttendanceRecords(AttendanceRequest $request, int $tenantId): void
{
$statusMap = [
'vacation' => 'vacation',
'businessTrip' => 'businessTrip',
'remote' => 'remote',
'fieldWork' => 'fieldWork',
];
$status = $statusMap[$request->request_type] ?? $request->request_type;
$period = CarbonPeriod::create($request->start_date, $request->end_date);
foreach ($period as $date) {
// 주말 제외
if ($date->isWeekend()) {
continue;
}
Attendance::updateOrCreate(
[
'tenant_id' => $tenantId,
'user_id' => $request->user_id,
'base_date' => $date->toDateString(),
],
[
'status' => $status,
'remarks' => $request->reason ? mb_substr($request->reason, 0, 100) : null,
'updated_by' => auth()->id(),
]
);
}
}
}