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);
}
/**
* 직급/직책 추가
*/

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\HR;
use App\Http\Controllers\Controller;
use App\Services\HR\EmployeeService;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class EmployeeTenureController extends Controller
{
public function __construct(
private EmployeeService $employeeService
) {}
/**
* 입퇴사자 현황 페이지
*/
public function index(Request $request): View|Response
{
if ($request->header('HX-Request')) {
return response('', 200)->header('HX-Redirect', route('hr.employee-tenure'));
}
$stats = $this->employeeService->getTenureStats();
$departments = $this->employeeService->getDepartments();
return view('hr.employee-tenure.index', [
'stats' => $stats,
'departments' => $departments,
]);
}
}

View File

@@ -6,7 +6,9 @@
use App\Models\HR\Position;
use App\Models\Tenants\Department;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
@@ -380,6 +382,191 @@ public function getPositions(string $type = 'rank'): \Illuminate\Database\Eloque
->get(['id', 'key', 'name']);
}
/**
* 입퇴사자 현황 조회 (페이지네이션)
*/
public function getEmployeeTenure(array $filters = [], int $perPage = 50): LengthAwarePaginator
{
$tenantId = session('selected_tenant_id');
$query = Employee::query()
->with(['user', 'department'])
->forTenant($tenantId)
->whereNotNull('json_extra->hire_date');
// 이름 검색
if (! empty($filters['q'])) {
$search = $filters['q'];
$query->where(function ($q) use ($search) {
$q->where('display_name', 'like', "%{$search}%")
->orWhereHas('user', function ($uq) use ($search) {
$uq->where('name', 'like', "%{$search}%");
});
});
}
// 부서 필터
if (! empty($filters['department_id'])) {
$query->where('department_id', $filters['department_id']);
}
// 상태 필터
if (! empty($filters['status'])) {
if ($filters['status'] === 'active') {
$query->where('employee_status', 'active');
} elseif ($filters['status'] === 'resigned') {
$query->where('employee_status', 'resigned');
}
}
// 입사기간 범위
if (! empty($filters['hire_from'])) {
$query->where('json_extra->hire_date', '>=', $filters['hire_from']);
}
if (! empty($filters['hire_to'])) {
$query->where('json_extra->hire_date', '<=', $filters['hire_to']);
}
// 정렬
$sortBy = $filters['sort_by'] ?? 'hire_date_desc';
switch ($sortBy) {
case 'hire_date_asc':
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) ASC");
break;
case 'tenure_desc':
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) ASC");
break;
case 'tenure_asc':
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC");
break;
default: // hire_date_desc
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC");
break;
}
return $query->paginate($perPage);
}
/**
* 입퇴사자 통계
*/
public function getTenureStats(): array
{
$tenantId = session('selected_tenant_id');
$baseQuery = Employee::query()
->forTenant($tenantId)
->whereNotNull('json_extra->hire_date');
$total = (clone $baseQuery)->count();
$active = (clone $baseQuery)->where('employee_status', 'active')->count();
$resigned = (clone $baseQuery)->where('employee_status', 'resigned')->count();
// 올해 입사자
$thisYear = now()->year;
$hiredThisYear = (clone $baseQuery)
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) LIKE ?", ["{$thisYear}%"])
->count();
// 올해 퇴사자
$resignedThisYear = Employee::query()
->forTenant($tenantId)
->whereNotNull('json_extra->resign_date')
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) LIKE ?", ["{$thisYear}%"])
->count();
// 평균 근속기간 (재직자 기준)
$activeEmployees = (clone $baseQuery)
->where('employee_status', 'active')
->get(['json_extra']);
$avgTenureDays = 0;
if ($activeEmployees->isNotEmpty()) {
$totalDays = 0;
$count = 0;
foreach ($activeEmployees as $emp) {
$hireDate = $emp->json_extra['hire_date'] ?? null;
if ($hireDate) {
$totalDays += Carbon::parse($hireDate)->diffInDays(today());
$count++;
}
}
$avgTenureDays = $count > 0 ? (int) round($totalDays / $count) : 0;
}
return [
'total' => $total,
'active' => $active,
'resigned' => $resigned,
'hired_this_year' => $hiredThisYear,
'resigned_this_year' => $resignedThisYear,
'avg_tenure_days' => $avgTenureDays,
'avg_tenure_label' => $this->formatTenure($avgTenureDays),
];
}
/**
* CSV 내보내기용 전체 데이터
*/
public function getTenureExportData(array $filters = []): Collection
{
$tenantId = session('selected_tenant_id');
$query = Employee::query()
->with(['user', 'department'])
->forTenant($tenantId)
->whereNotNull('json_extra->hire_date');
if (! empty($filters['department_id'])) {
$query->where('department_id', $filters['department_id']);
}
if (! empty($filters['status'])) {
if ($filters['status'] === 'active') {
$query->where('employee_status', 'active');
} elseif ($filters['status'] === 'resigned') {
$query->where('employee_status', 'resigned');
}
}
if (! empty($filters['hire_from'])) {
$query->where('json_extra->hire_date', '>=', $filters['hire_from']);
}
if (! empty($filters['hire_to'])) {
$query->where('json_extra->hire_date', '<=', $filters['hire_to']);
}
return $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC")->get();
}
/**
* 근속일수 → "N년 M개월 D일" 변환
*/
public function formatTenure(int $days): string
{
if ($days <= 0) {
return '0일';
}
$years = intdiv($days, 365);
$remaining = $days % 365;
$months = intdiv($remaining, 30);
$d = $remaining % 30;
$parts = [];
if ($years > 0) {
$parts[] = "{$years}";
}
if ($months > 0) {
$parts[] = "{$months}개월";
}
if ($d > 0 || empty($parts)) {
$parts[] = "{$d}";
}
return implode(' ', $parts);
}
/**
* 직급/직책 추가
*/

View File

@@ -0,0 +1,141 @@
@extends('layouts.app')
@section('title', '입퇴사자 현황')
@section('content')
<div class="px-4 py-6">
{{-- 페이지 헤더 --}}
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
<div>
<h1 class="text-2xl font-bold text-gray-800">입퇴사자 현황</h1>
<p class="text-sm text-gray-500 mt-1">{{ now()->format('Y년 n월 j일') }} 현재</p>
</div>
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
<a href="{{ route('api.admin.hr.employees.tenure-export') }}"
class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium rounded-lg transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
엑셀 다운로드
</a>
</div>
</div>
{{-- 통계 카드 --}}
<div class="grid gap-4 mb-6" style="grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));">
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">전체 사원</div>
<div class="text-2xl font-bold text-gray-800">{{ $stats['total'] }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">재직</div>
<div class="text-2xl font-bold text-emerald-600">{{ $stats['active'] }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">퇴직</div>
<div class="text-2xl font-bold text-red-600">{{ $stats['resigned'] }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">평균 근속기간</div>
<div class="text-2xl font-bold text-blue-600">{{ $stats['avg_tenure_label'] }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">올해 입사</div>
<div class="text-2xl font-bold text-indigo-600">{{ $stats['hired_this_year'] }}</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">올해 퇴사</div>
<div class="text-2xl font-bold text-orange-600">{{ $stats['resigned_this_year'] }}</div>
</div>
</div>
{{-- 테이블 컨테이너 --}}
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
{{-- 필터 --}}
<div class="px-6 py-4 border-b border-gray-200">
<x-filter-collapsible id="tenureFilter">
<form id="tenureFilterForm" class="flex flex-wrap gap-3 items-end">
<div style="flex: 1 1 200px; max-width: 280px;">
<label class="block text-xs text-gray-500 mb-1">이름 검색</label>
<input type="text" name="q" placeholder="사원명..."
value="{{ request('q') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div style="flex: 0 1 160px;">
<label class="block text-xs text-gray-500 mb-1">부서</label>
<select name="department_id"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">전체 부서</option>
@foreach($departments as $dept)
<option value="{{ $dept->id }}" {{ request('department_id') == $dept->id ? 'selected' : '' }}>
{{ $dept->name }}
</option>
@endforeach
</select>
</div>
<div style="flex: 0 1 130px;">
<label class="block text-xs text-gray-500 mb-1">상태</label>
<select name="status"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="">전체</option>
<option value="active" {{ request('status') === 'active' ? 'selected' : '' }}>재직</option>
<option value="resigned" {{ request('status') === 'resigned' ? 'selected' : '' }}>퇴직</option>
</select>
</div>
<div style="flex: 0 1 150px;">
<label class="block text-xs text-gray-500 mb-1">입사일(시작)</label>
<input type="date" name="hire_from"
value="{{ request('hire_from') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div style="flex: 0 1 150px;">
<label class="block text-xs text-gray-500 mb-1">입사일(종료)</label>
<input type="date" name="hire_to"
value="{{ request('hire_to') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div style="flex: 0 1 160px;">
<label class="block text-xs text-gray-500 mb-1">정렬</label>
<select name="sort_by"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="hire_date_desc" {{ request('sort_by', 'hire_date_desc') === 'hire_date_desc' ? 'selected' : '' }}>입사일 최신순</option>
<option value="hire_date_asc" {{ request('sort_by') === 'hire_date_asc' ? 'selected' : '' }}>입사일 빠른순</option>
<option value="tenure_desc" {{ request('sort_by') === 'tenure_desc' ? 'selected' : '' }}>근속기간 긴순</option>
<option value="tenure_asc" {{ request('sort_by') === 'tenure_asc' ? 'selected' : '' }}>근속기간 짧은순</option>
</select>
</div>
<div class="shrink-0">
<button type="submit"
hx-get="{{ route('api.admin.hr.employees.tenure') }}"
hx-target="#tenure-table"
hx-include="#tenureFilterForm"
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm rounded-lg transition-colors">
검색
</button>
</div>
</form>
</x-filter-collapsible>
</div>
{{-- HTMX 테이블 영역 --}}
<div id="tenure-table"
hx-get="{{ route('api.admin.hr.employees.tenure') }}"
hx-trigger="load"
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
class="min-h-[200px]">
<div class="flex justify-center items-center p-12">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.getElementById('tenureFilterForm')?.addEventListener('submit', function(e) {
e.preventDefault();
htmx.trigger('#tenure-table', 'htmx:trigger');
});
</script>
@endpush

View File

@@ -0,0 +1,116 @@
{{-- 입퇴사자 현황 테이블 (HTMX로 로드) --}}
<x-table-swipe>
<table class="min-w-full">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-600" style="width: 50px;">No.</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-600">사원</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-600">부서</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-600">직책</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-600">상태</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-600">입사일</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-600">퇴사일</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-600">근속기간</th>
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-600">근속일수</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
@forelse($employees as $employee)
<tr class="hover:bg-gray-50 transition-colors {{ $employee->employee_status === 'resigned' ? 'opacity-60' : '' }}">
{{-- No. --}}
<td class="px-4 py-3 whitespace-nowrap text-center text-sm text-gray-500">
{{ ($employees->currentPage() - 1) * $employees->perPage() + $loop->iteration }}
</td>
{{-- 사원 --}}
<td class="px-4 py-3 whitespace-nowrap">
<a href="{{ route('hr.employees.show', $employee->id) }}"
class="flex items-center gap-3 group">
<div class="shrink-0 w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-sm font-medium">
{{ mb_substr($employee->display_name ?? $employee->user?->name ?? '?', 0, 1) }}
</div>
<span class="text-sm font-medium text-gray-900 group-hover:text-blue-600">
{{ $employee->display_name ?? $employee->user?->name ?? '-' }}
</span>
</a>
</td>
{{-- 부서 --}}
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700">
{{ $employee->department?->name ?? '-' }}
</td>
{{-- 직책 --}}
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700">
{{ $employee->position_label ?? '-' }}
</td>
{{-- 상태 --}}
<td class="px-4 py-3 whitespace-nowrap text-center">
@if($employee->employee_status === 'active')
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700">
재직
</span>
@elseif($employee->employee_status === 'resigned')
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-700">
퇴직
</span>
@else
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-700">
{{ $employee->employee_status === 'leave' ? '휴직' : ($employee->employee_status ?? '-') }}
</span>
@endif
</td>
{{-- 입사일 --}}
<td class="px-4 py-3 whitespace-nowrap text-center text-sm text-gray-500">
{{ $employee->hire_date ?? '-' }}
</td>
{{-- 퇴사일 --}}
<td class="px-4 py-3 whitespace-nowrap text-center text-sm text-gray-500">
{{ $employee->resign_date ?? '-' }}
</td>
{{-- 근속기간 --}}
<td class="px-4 py-3 whitespace-nowrap text-center text-sm font-medium {{ $employee->employee_status === 'active' ? 'text-blue-700' : 'text-gray-500' }}">
{{ $employee->tenure_label ?? '-' }}
</td>
{{-- 근속일수 --}}
<td class="px-4 py-3 whitespace-nowrap text-center text-sm text-gray-500">
{{ number_format($employee->tenure_days ?? 0) }}
</td>
</tr>
@empty
<tr>
<td colspan="9" class="px-6 py-12 text-center">
<div class="flex flex-col items-center gap-2">
<svg class="w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
</svg>
<p class="text-gray-500">입사일이 등록된 사원이 없습니다.</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</x-table-swipe>
{{-- 하단 요약 + 페이지네이션 --}}
@if($employees->isNotEmpty())
<div class="px-6 py-3 border-t border-gray-200 bg-gray-50">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="text-sm text-gray-600">
<strong>{{ number_format($employees->total()) }}</strong>
@if(isset($stats))
&middot; 평균 근속 <strong class="text-blue-600">{{ $stats['avg_tenure_label'] }}</strong>
@endif
</div>
@if($employees->hasPages())
<div>{{ $employees->links() }}</div>
@endif
</div>
</div>
@endif

View File

@@ -1043,6 +1043,8 @@
Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/hr/employees')->name('api.admin.hr.employees.')->group(function () {
Route::get('/search-users', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'searchUsers'])->name('search-users');
Route::get('/stats', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'stats'])->name('stats');
Route::get('/tenure', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'tenure'])->name('tenure');
Route::get('/tenure-export', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'tenureExport'])->name('tenure-export');
Route::get('/', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'index'])->name('index');
Route::post('/', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'store'])->name('store');
Route::get('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'show'])->name('show');

View File

@@ -897,6 +897,9 @@
Route::get('/{id}/edit', [\App\Http\Controllers\HR\EmployeeController::class, 'edit'])->name('edit');
});
// 입퇴사자 현황
Route::get('/employee-tenure', [\App\Http\Controllers\HR\EmployeeTenureController::class, 'index'])->name('employee-tenure');
// 근태현황
Route::prefix('attendances')->name('attendances.')->group(function () {
Route::get('/', [\App\Http\Controllers\HR\AttendanceController::class, 'index'])->name('index');