feat: [hr] 인사관리 사원관리 Phase 1 구현
- Employee, Position 모델 생성 (tenant_user_profiles, positions 테이블) - EmployeeService 생성 (CRUD, 통계, 필터/검색/페이지네이션) - 뷰 컨트롤러(HR/EmployeeController) + API 컨트롤러 생성 - Blade 뷰: index(통계카드+HTMX테이블), create, edit, show, partials/table - 라우트: web.php(/hr/employees/*), api.php(/admin/hr/employees/*)
This commit is contained in:
237
app/Services/HR/EmployeeService.php
Normal file
237
app/Services/HR/EmployeeService.php
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\HR;
|
||||
|
||||
use App\Models\HR\Employee;
|
||||
use App\Models\HR\Position;
|
||||
use App\Models\Tenants\Department;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class EmployeeService
|
||||
{
|
||||
/**
|
||||
* 사원 목록 조회 (페이지네이션)
|
||||
*/
|
||||
public function getEmployees(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$query = Employee::query()
|
||||
->with(['user', 'department'])
|
||||
->forTenant($tenantId);
|
||||
|
||||
// 검색 필터 (이름, 사번, 이메일)
|
||||
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}%")
|
||||
->orWhere('email', 'like', "%{$search}%")
|
||||
->orWhere('phone', 'like', "%{$search}%");
|
||||
})
|
||||
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.employee_code')) LIKE ?", ["%{$search}%"]);
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (! empty($filters['status'])) {
|
||||
$query->where('employee_status', $filters['status']);
|
||||
}
|
||||
|
||||
// 부서 필터
|
||||
if (! empty($filters['department_id'])) {
|
||||
$query->where('department_id', $filters['department_id']);
|
||||
}
|
||||
|
||||
// 정렬
|
||||
$query->orderByRaw("FIELD(employee_status, 'active', 'leave', 'resigned')")
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
return $query->paginate($perPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 상세 조회
|
||||
*/
|
||||
public function getEmployeeById(int $id): ?Employee
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
return Employee::query()
|
||||
->with(['user', 'department', 'manager'])
|
||||
->forTenant($tenantId)
|
||||
->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 통계
|
||||
*/
|
||||
public function getStats(): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$baseQuery = Employee::query()->forTenant($tenantId);
|
||||
|
||||
return [
|
||||
'total' => (clone $baseQuery)->count(),
|
||||
'active' => (clone $baseQuery)->where('employee_status', 'active')->count(),
|
||||
'leave' => (clone $baseQuery)->where('employee_status', 'leave')->count(),
|
||||
'resigned' => (clone $baseQuery)->where('employee_status', 'resigned')->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 등록 (User + TenantUserProfile 동시 생성)
|
||||
*/
|
||||
public function createEmployee(array $data): Employee
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
return DB::transaction(function () use ($data, $tenantId) {
|
||||
// User 생성
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'] ?? null,
|
||||
'phone' => $data['phone'] ?? null,
|
||||
'password' => bcrypt($data['password'] ?? 'sam1234!'),
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
// json_extra 구성
|
||||
$jsonExtra = [];
|
||||
if (! empty($data['employee_code'])) {
|
||||
$jsonExtra['employee_code'] = $data['employee_code'];
|
||||
}
|
||||
if (! empty($data['hire_date'])) {
|
||||
$jsonExtra['hire_date'] = $data['hire_date'];
|
||||
}
|
||||
if (! empty($data['address'])) {
|
||||
$jsonExtra['address'] = $data['address'];
|
||||
}
|
||||
if (! empty($data['emergency_contact'])) {
|
||||
$jsonExtra['emergency_contact'] = $data['emergency_contact'];
|
||||
}
|
||||
|
||||
// Employee(TenantUserProfile) 생성
|
||||
$employee = Employee::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $user->id,
|
||||
'department_id' => $data['department_id'] ?? null,
|
||||
'position_key' => $data['position_key'] ?? null,
|
||||
'job_title_key' => $data['job_title_key'] ?? null,
|
||||
'work_location_key' => $data['work_location_key'] ?? null,
|
||||
'employment_type_key' => $data['employment_type_key'] ?? null,
|
||||
'employee_status' => $data['employee_status'] ?? 'active',
|
||||
'manager_user_id' => $data['manager_user_id'] ?? null,
|
||||
'display_name' => $data['display_name'] ?? $data['name'],
|
||||
'json_extra' => ! empty($jsonExtra) ? $jsonExtra : null,
|
||||
]);
|
||||
|
||||
return $employee->load(['user', 'department']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 정보 수정
|
||||
*/
|
||||
public function updateEmployee(int $id, array $data): ?Employee
|
||||
{
|
||||
$employee = $this->getEmployeeById($id);
|
||||
if (! $employee) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 기본 필드 업데이트
|
||||
$updateData = array_filter([
|
||||
'department_id' => $data['department_id'] ?? null,
|
||||
'position_key' => $data['position_key'] ?? null,
|
||||
'job_title_key' => $data['job_title_key'] ?? null,
|
||||
'work_location_key' => $data['work_location_key'] ?? null,
|
||||
'employment_type_key' => $data['employment_type_key'] ?? null,
|
||||
'employee_status' => $data['employee_status'] ?? null,
|
||||
'manager_user_id' => $data['manager_user_id'] ?? null,
|
||||
'display_name' => $data['display_name'] ?? null,
|
||||
], fn ($v) => $v !== null);
|
||||
|
||||
// json_extra 업데이트
|
||||
$jsonExtraKeys = ['employee_code', 'hire_date', 'address', 'emergency_contact', 'salary', 'bank_account'];
|
||||
$extra = $employee->json_extra ?? [];
|
||||
foreach ($jsonExtraKeys as $key) {
|
||||
if (array_key_exists($key, $data)) {
|
||||
if ($data[$key] === null || $data[$key] === '') {
|
||||
unset($extra[$key]);
|
||||
} else {
|
||||
$extra[$key] = $data[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
$updateData['json_extra'] = ! empty($extra) ? $extra : null;
|
||||
|
||||
$employee->update($updateData);
|
||||
|
||||
// User 기본정보 동기화
|
||||
if ($employee->user) {
|
||||
$userUpdate = [];
|
||||
if (! empty($data['name'])) {
|
||||
$userUpdate['name'] = $data['name'];
|
||||
}
|
||||
if (! empty($data['email'])) {
|
||||
$userUpdate['email'] = $data['email'];
|
||||
}
|
||||
if (! empty($data['phone'])) {
|
||||
$userUpdate['phone'] = $data['phone'];
|
||||
}
|
||||
if (! empty($userUpdate)) {
|
||||
$employee->user->update($userUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
return $employee->fresh(['user', 'department']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 삭제 (퇴직 처리)
|
||||
*/
|
||||
public function deleteEmployee(int $id): bool
|
||||
{
|
||||
$employee = $this->getEmployeeById($id);
|
||||
if (! $employee) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$employee->update(['employee_status' => 'resigned']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 목록 (드롭다운용)
|
||||
*/
|
||||
public function getDepartments(): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
return Department::query()
|
||||
->where('is_active', true)
|
||||
->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId))
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 직급 목록 (드롭다운용)
|
||||
*/
|
||||
public function getPositions(string $type = 'rank'): \Illuminate\Database\Eloquent\Collection
|
||||
{
|
||||
return Position::query()
|
||||
->forTenant()
|
||||
->where('type', $type)
|
||||
->where('is_active', true)
|
||||
->ordered()
|
||||
->get(['id', 'key', 'name']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user