feat: [hr] 입퇴사자 현황 페이지 구현

- EmployeeService에 근속기간 조회/통계/CSV 내보내기 메서드 추가
- API 컨트롤러에 tenure/tenureExport 엔드포인트 추가
- EmployeeTenureController 뷰 컨트롤러 생성
- 통계 카드 6개 (전체/재직/퇴직/평균근속/올해입사/올해퇴사)
- HTMX 테이블 (사원/부서/직책/상태/입사일/퇴사일/근속기간/근속일수)
- 필터: 이름검색, 부서, 상태, 입사기간 범위, 정렬
- CSV 엑셀 다운로드 기능
This commit is contained in:
김보곤
2026-02-27 08:24:26 +09:00
parent 57a2012a85
commit 2f739d0d55
7 changed files with 598 additions and 0 deletions

View File

@@ -6,11 +6,13 @@
use App\Models\Boards\File;
use App\Services\GoogleCloudStorageService;
use App\Services\HR\EmployeeService;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
class EmployeeController extends Controller
{
@@ -383,6 +385,119 @@ public function downloadFile(int $id, int $fileId, GoogleCloudStorageService $gc
abort(404, '파일이 서버에 존재하지 않습니다.');
}
/**
* 입퇴사자 현황 목록 (HTMX → HTML / 일반 → JSON)
*/
public function tenure(Request $request): JsonResponse|Response
{
$employees = $this->employeeService->getEmployeeTenure(
$request->all(),
$request->integer('per_page', 50)
);
// 근속기간 계산 추가
$employees->getCollection()->each(function ($employee) {
$hireDate = $employee->hire_date;
if ($hireDate) {
$hire = Carbon::parse($hireDate);
$end = $employee->resign_date ? Carbon::parse($employee->resign_date) : today();
$tenureDays = $hire->diffInDays($end);
$diff = $hire->diff($end);
$employee->tenure_days = $tenureDays;
$employee->tenure_label = $this->formatTenureLabel($diff);
} else {
$employee->tenure_days = 0;
$employee->tenure_label = '-';
}
});
if ($request->header('HX-Request')) {
$stats = $this->employeeService->getTenureStats();
return response(view('hr.employee-tenure.partials.table', compact('employees', 'stats')));
}
return response()->json([
'success' => true,
'data' => $employees->items(),
'meta' => [
'current_page' => $employees->currentPage(),
'last_page' => $employees->lastPage(),
'per_page' => $employees->perPage(),
'total' => $employees->total(),
],
]);
}
/**
* 입퇴사자 현황 CSV 내보내기
*/
public function tenureExport(Request $request): StreamedResponse
{
$employees = $this->employeeService->getTenureExportData($request->all());
$filename = '입퇴사자현황_'.now()->format('Ymd').'.csv';
return response()->streamDownload(function () use ($employees) {
$handle = fopen('php://output', 'w');
// BOM for Excel UTF-8
fwrite($handle, "\xEF\xBB\xBF");
// 헤더
fputcsv($handle, ['No.', '사원명', '부서', '직책', '상태', '입사일', '퇴사일', '근속기간', '근속일수']);
$index = 1;
foreach ($employees as $employee) {
$hireDate = $employee->hire_date;
$tenureDays = 0;
$tenureLabel = '-';
if ($hireDate) {
$hire = Carbon::parse($hireDate);
$end = $employee->resign_date ? Carbon::parse($employee->resign_date) : today();
$tenureDays = $hire->diffInDays($end);
$tenureLabel = $this->formatTenureLabel($hire->diff($end));
}
$statusMap = ['active' => '재직', 'leave' => '휴직', 'resigned' => '퇴직'];
fputcsv($handle, [
$index++,
$employee->display_name ?? $employee->user?->name ?? '-',
$employee->department?->name ?? '-',
$employee->position_label ?? '-',
$statusMap[$employee->employee_status] ?? $employee->employee_status,
$employee->hire_date ?? '-',
$employee->resign_date ?? '-',
$tenureLabel,
$tenureDays,
]);
}
fclose($handle);
}, $filename, [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
private function formatTenureLabel(\DateInterval $diff): string
{
$parts = [];
if ($diff->y > 0) {
$parts[] = "{$diff->y}";
}
if ($diff->m > 0) {
$parts[] = "{$diff->m}개월";
}
if ($diff->d > 0 || empty($parts)) {
$parts[] = "{$diff->d}";
}
return implode(' ', $parts);
}
/**
* 직급/직책 추가
*/