Files
sam-manage/app/Http/Controllers/Api/Admin/HR/AttendanceController.php
김보곤 474165ff67 feat: [attendance] 근태현황 Phase 1 구현
- 1-1: 등록/수정 버그 수정 (created_by 덮어쓰기 방지)
- 1-2: 엑셀(CSV) 다운로드 기능 추가
- 1-3: 체크박스 일괄 삭제 기능 추가
- 1-4: 월간 통계 연/월 선택 기능 추가
2026-02-26 20:45:19 +09:00

253 lines
8.1 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin\HR;
use App\Http\Controllers\Controller;
use App\Models\HR\Attendance;
use App\Services\HR\AttendanceService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
class AttendanceController extends Controller
{
public function __construct(
private AttendanceService $attendanceService
) {}
/**
* 근태 목록 조회 (HTMX → HTML / 일반 → JSON)
*/
public function index(Request $request): JsonResponse|Response
{
$attendances = $this->attendanceService->getAttendances(
$request->all(),
$request->integer('per_page', 20)
);
if ($request->header('HX-Request')) {
return response(view('hr.attendances.partials.table', compact('attendances')));
}
return response()->json([
'success' => true,
'data' => $attendances->items(),
'meta' => [
'current_page' => $attendances->currentPage(),
'last_page' => $attendances->lastPage(),
'per_page' => $attendances->perPage(),
'total' => $attendances->total(),
],
]);
}
/**
* 월간 통계 (HTMX → HTML / 일반 → JSON)
*/
public function stats(Request $request): JsonResponse|Response
{
$stats = $this->attendanceService->getMonthlyStats(
$request->integer('year') ?: null,
$request->integer('month') ?: null
);
if ($request->header('HX-Request')) {
return response(view('hr.attendances.partials.stats', compact('stats')));
}
return response()->json([
'success' => true,
'data' => $stats,
]);
}
/**
* 엑셀(CSV) 내보내기
*/
public function export(Request $request): StreamedResponse
{
$attendances = $this->attendanceService->getExportData($request->all());
$tenantId = session('selected_tenant_id');
$filename = '근태현황_'.now()->format('Ymd').'.csv';
return response()->streamDownload(function () use ($attendances) {
$file = fopen('php://output', 'w');
fwrite($file, "\xEF\xBB\xBF"); // UTF-8 BOM
fputcsv($file, ['날짜', '사원명', '부서', '상태', '출근', '퇴근', '비고']);
foreach ($attendances as $att) {
$profile = $att->user?->tenantProfiles?->first();
$displayName = $profile?->display_name ?? $att->user?->name ?? '-';
$department = $profile?->department?->name ?? '-';
$statusLabel = Attendance::STATUS_MAP[$att->status] ?? $att->status;
$checkIn = $att->check_in ? substr($att->check_in, 0, 5) : '';
$checkOut = $att->check_out ? substr($att->check_out, 0, 5) : '';
fputcsv($file, [
$att->base_date->format('Y-m-d'),
$displayName,
$department,
$statusLabel,
$checkIn,
$checkOut,
$att->remarks ?? '',
]);
}
fclose($file);
}, $filename, [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
/**
* 일괄 삭제
*/
public function bulkDestroy(Request $request): JsonResponse|Response
{
$validated = $request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'integer',
]);
try {
$count = $this->attendanceService->bulkDelete($validated['ids']);
if ($request->header('HX-Request')) {
$attendances = $this->attendanceService->getAttendances(
$request->except('ids'),
$request->integer('per_page', 20)
);
return response(view('hr.attendances.partials.table', compact('attendances')));
}
return response()->json([
'success' => true,
'message' => "{$count}건의 근태가 삭제되었습니다.",
]);
} catch (\Throwable $e) {
report($e);
return response()->json([
'success' => false,
'message' => '일괄 삭제 중 오류가 발생했습니다.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* 근태 등록
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'user_id' => 'required|integer|exists:users,id',
'base_date' => 'required|date',
'status' => 'required|string|in:onTime,late,absent,vacation,businessTrip,fieldWork,overtime,remote',
'check_in' => 'nullable|date_format:H:i',
'check_out' => 'nullable|date_format:H:i',
'remarks' => 'nullable|string|max:500',
]);
try {
$attendance = $this->attendanceService->storeAttendance($validated);
return response()->json([
'success' => true,
'message' => '근태가 등록되었습니다.',
'data' => $attendance,
], 201);
} catch (\Throwable $e) {
report($e);
return response()->json([
'success' => false,
'message' => '근태 등록 중 오류가 발생했습니다.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* 근태 수정
*/
public function update(Request $request, int $id): JsonResponse
{
$validated = $request->validate([
'status' => 'sometimes|required|string|in:onTime,late,absent,vacation,businessTrip,fieldWork,overtime,remote',
'check_in' => 'nullable|date_format:H:i',
'check_out' => 'nullable|date_format:H:i',
'remarks' => 'nullable|string|max:500',
]);
try {
$attendance = $this->attendanceService->updateAttendance($id, $validated);
if (! $attendance) {
return response()->json([
'success' => false,
'message' => '근태 정보를 찾을 수 없습니다.',
], 404);
}
return response()->json([
'success' => true,
'message' => '근태가 수정되었습니다.',
'data' => $attendance,
]);
} catch (\Throwable $e) {
report($e);
return response()->json([
'success' => false,
'message' => '근태 수정 중 오류가 발생했습니다.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* 근태 삭제
*/
public function destroy(Request $request, int $id): JsonResponse|Response
{
try {
$result = $this->attendanceService->deleteAttendance($id);
if (! $result) {
return response()->json([
'success' => false,
'message' => '근태 정보를 찾을 수 없습니다.',
], 404);
}
if ($request->header('HX-Request')) {
$attendances = $this->attendanceService->getAttendances(
$request->all(),
$request->integer('per_page', 20)
);
return response(view('hr.attendances.partials.table', compact('attendances')));
}
return response()->json([
'success' => true,
'message' => '근태가 삭제되었습니다.',
]);
} catch (\Throwable $e) {
report($e);
return response()->json([
'success' => false,
'message' => '근태 삭제 중 오류가 발생했습니다.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
}