From 29ca02232198afc8ed72541f3ef4e9a7a4ca4d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Thu, 26 Feb 2026 16:43:52 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[hr]=20=EC=9D=B8=EC=82=AC=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EC=82=AC=EC=9B=90=EA=B4=80=EB=A6=AC=20Phase=201=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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/*) --- .../Api/Admin/HR/EmployeeController.php | 173 +++++++++++++ .../Controllers/HR/EmployeeController.php | 83 ++++++ app/Models/HR/Employee.php | 166 ++++++++++++ app/Models/HR/Position.php | 61 +++++ app/Services/HR/EmployeeService.php | 237 ++++++++++++++++++ resources/views/hr/employees/create.blade.php | 185 ++++++++++++++ resources/views/hr/employees/edit.blade.php | 188 ++++++++++++++ resources/views/hr/employees/index.blade.php | 112 +++++++++ .../hr/employees/partials/table.blade.php | 146 +++++++++++ resources/views/hr/employees/show.blade.php | 152 +++++++++++ routes/api.php | 15 ++ routes/web.php | 15 ++ 12 files changed, 1533 insertions(+) create mode 100644 app/Http/Controllers/Api/Admin/HR/EmployeeController.php create mode 100644 app/Http/Controllers/HR/EmployeeController.php create mode 100644 app/Models/HR/Employee.php create mode 100644 app/Models/HR/Position.php create mode 100644 app/Services/HR/EmployeeService.php create mode 100644 resources/views/hr/employees/create.blade.php create mode 100644 resources/views/hr/employees/edit.blade.php create mode 100644 resources/views/hr/employees/index.blade.php create mode 100644 resources/views/hr/employees/partials/table.blade.php create mode 100644 resources/views/hr/employees/show.blade.php diff --git a/app/Http/Controllers/Api/Admin/HR/EmployeeController.php b/app/Http/Controllers/Api/Admin/HR/EmployeeController.php new file mode 100644 index 00000000..85252b8f --- /dev/null +++ b/app/Http/Controllers/Api/Admin/HR/EmployeeController.php @@ -0,0 +1,173 @@ +employeeService->getEmployees( + $request->all(), + $request->integer('per_page', 20) + ); + + if ($request->header('HX-Request')) { + return response(view('hr.employees.partials.table', compact('employees'))); + } + + return response()->json([ + 'success' => true, + 'data' => $employees->items(), + 'meta' => [ + 'current_page' => $employees->currentPage(), + 'last_page' => $employees->lastPage(), + 'per_page' => $employees->perPage(), + 'total' => $employees->total(), + ], + ]); + } + + /** + * 사원 통계 + */ + public function stats(): JsonResponse + { + $stats = $this->employeeService->getStats(); + + return response()->json([ + 'success' => true, + 'data' => $stats, + ]); + } + + /** + * 사원 등록 + */ + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'name' => 'required|string|max:50', + 'email' => 'nullable|email|max:100|unique:users,email', + 'phone' => 'nullable|string|max:20', + 'password' => 'nullable|string|min:6', + 'department_id' => 'nullable|integer|exists:departments,id', + 'position_key' => 'nullable|string|max:50', + 'job_title_key' => 'nullable|string|max:50', + 'work_location_key' => 'nullable|string|max:50', + 'employment_type_key' => 'nullable|string|max:50', + 'employee_status' => 'nullable|string|in:active,leave,resigned', + 'manager_user_id' => 'nullable|integer|exists:users,id', + 'display_name' => 'nullable|string|max:50', + 'employee_code' => 'nullable|string|max:30', + 'hire_date' => 'nullable|date', + 'address' => 'nullable|string|max:200', + 'emergency_contact' => 'nullable|string|max:100', + ]); + + $employee = $this->employeeService->createEmployee($validated); + + return response()->json([ + 'success' => true, + 'message' => '사원이 등록되었습니다.', + 'data' => $employee, + ], 201); + } + + /** + * 사원 상세 조회 + */ + public function show(int $id): JsonResponse + { + $employee = $this->employeeService->getEmployeeById($id); + + if (! $employee) { + return response()->json([ + 'success' => false, + 'message' => '사원 정보를 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $employee, + ]); + } + + /** + * 사원 수정 + */ + public function update(Request $request, int $id): JsonResponse + { + $validated = $request->validate([ + 'name' => 'sometimes|required|string|max:50', + 'email' => 'nullable|email|max:100', + 'phone' => 'nullable|string|max:20', + 'department_id' => 'nullable|integer|exists:departments,id', + 'position_key' => 'nullable|string|max:50', + 'job_title_key' => 'nullable|string|max:50', + 'work_location_key' => 'nullable|string|max:50', + 'employment_type_key' => 'nullable|string|max:50', + 'employee_status' => 'nullable|string|in:active,leave,resigned', + 'manager_user_id' => 'nullable|integer|exists:users,id', + 'display_name' => 'nullable|string|max:50', + 'employee_code' => 'nullable|string|max:30', + 'hire_date' => 'nullable|date', + 'address' => 'nullable|string|max:200', + 'emergency_contact' => 'nullable|string|max:100', + ]); + + $employee = $this->employeeService->updateEmployee($id, $validated); + + if (! $employee) { + return response()->json([ + 'success' => false, + 'message' => '사원 정보를 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'message' => '사원 정보가 수정되었습니다.', + 'data' => $employee, + ]); + } + + /** + * 사원 삭제 (퇴직 처리) + */ + public function destroy(Request $request, int $id): JsonResponse|Response + { + $result = $this->employeeService->deleteEmployee($id); + + if (! $result) { + return response()->json([ + 'success' => false, + 'message' => '사원 정보를 찾을 수 없습니다.', + ], 404); + } + + if ($request->header('HX-Request')) { + $employees = $this->employeeService->getEmployees($request->all(), $request->integer('per_page', 20)); + + return response(view('hr.employees.partials.table', compact('employees'))); + } + + return response()->json([ + 'success' => true, + 'message' => '퇴직 처리되었습니다.', + ]); + } +} diff --git a/app/Http/Controllers/HR/EmployeeController.php b/app/Http/Controllers/HR/EmployeeController.php new file mode 100644 index 00000000..73bc42bc --- /dev/null +++ b/app/Http/Controllers/HR/EmployeeController.php @@ -0,0 +1,83 @@ +employeeService->getStats(); + $departments = $this->employeeService->getDepartments(); + + return view('hr.employees.index', [ + 'stats' => $stats, + 'departments' => $departments, + ]); + } + + /** + * 사원 등록 폼 + */ + public function create(): View + { + $departments = $this->employeeService->getDepartments(); + $ranks = $this->employeeService->getPositions('rank'); + $titles = $this->employeeService->getPositions('title'); + + return view('hr.employees.create', [ + 'departments' => $departments, + 'ranks' => $ranks, + 'titles' => $titles, + ]); + } + + /** + * 사원 상세 페이지 + */ + public function show(int $id): View + { + $employee = $this->employeeService->getEmployeeById($id); + + if (! $employee) { + abort(404, '사원 정보를 찾을 수 없습니다.'); + } + + return view('hr.employees.show', [ + 'employee' => $employee, + ]); + } + + /** + * 사원 수정 폼 + */ + public function edit(int $id): View + { + $employee = $this->employeeService->getEmployeeById($id); + + if (! $employee) { + abort(404, '사원 정보를 찾을 수 없습니다.'); + } + + $departments = $this->employeeService->getDepartments(); + $ranks = $this->employeeService->getPositions('rank'); + $titles = $this->employeeService->getPositions('title'); + + return view('hr.employees.edit', [ + 'employee' => $employee, + 'departments' => $departments, + 'ranks' => $ranks, + 'titles' => $titles, + ]); + } +} diff --git a/app/Models/HR/Employee.php b/app/Models/HR/Employee.php new file mode 100644 index 00000000..b576837b --- /dev/null +++ b/app/Models/HR/Employee.php @@ -0,0 +1,166 @@ + 'array', + 'tenant_id' => 'int', + 'user_id' => 'int', + 'department_id' => 'int', + 'manager_user_id' => 'int', + ]; + + protected $appends = [ + 'employee_code', + 'hire_date', + 'position_label', + 'job_title_label', + ]; + + // ========================================================================= + // 관계 정의 + // ========================================================================= + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function department(): BelongsTo + { + return $this->belongsTo(Department::class, 'department_id'); + } + + public function manager(): BelongsTo + { + return $this->belongsTo(User::class, 'manager_user_id'); + } + + // ========================================================================= + // json_extra Accessor + // ========================================================================= + + public function getEmployeeCodeAttribute(): ?string + { + return $this->json_extra['employee_code'] ?? null; + } + + public function getHireDateAttribute(): ?string + { + return $this->json_extra['hire_date'] ?? null; + } + + public function getAddressAttribute(): ?string + { + return $this->json_extra['address'] ?? null; + } + + public function getEmergencyContactAttribute(): ?string + { + return $this->json_extra['emergency_contact'] ?? null; + } + + public function getPositionLabelAttribute(): ?string + { + if (! $this->position_key || ! $this->tenant_id) { + return $this->position_key; + } + + $position = Position::where('tenant_id', $this->tenant_id) + ->where('type', Position::TYPE_RANK) + ->where('key', $this->position_key) + ->first(); + + return $position?->name ?? $this->position_key; + } + + public function getJobTitleLabelAttribute(): ?string + { + if (! $this->job_title_key || ! $this->tenant_id) { + return $this->job_title_key; + } + + $position = Position::where('tenant_id', $this->tenant_id) + ->where('type', Position::TYPE_TITLE) + ->where('key', $this->job_title_key) + ->first(); + + return $position?->name ?? $this->job_title_key; + } + + // ========================================================================= + // json_extra 헬퍼 + // ========================================================================= + + public function getJsonExtraValue(string $key, mixed $default = null): mixed + { + return $this->json_extra[$key] ?? $default; + } + + public function setJsonExtraValue(string $key, mixed $value): void + { + $extra = $this->json_extra ?? []; + if ($value === null) { + unset($extra[$key]); + } else { + $extra[$key] = $value; + } + $this->json_extra = $extra; + } + + // ========================================================================= + // 스코프 + // ========================================================================= + + public function scopeForTenant($query, ?int $tenantId = null) + { + $tenantId = $tenantId ?? session('selected_tenant_id'); + if ($tenantId) { + return $query->where('tenant_id', $tenantId); + } + + return $query; + } + + public function scopeActiveEmployees($query) + { + return $query->where('employee_status', 'active'); + } + + public function scopeOnLeave($query) + { + return $query->where('employee_status', 'leave'); + } + + public function scopeResigned($query) + { + return $query->where('employee_status', 'resigned'); + } +} diff --git a/app/Models/HR/Position.php b/app/Models/HR/Position.php new file mode 100644 index 00000000..4152fd26 --- /dev/null +++ b/app/Models/HR/Position.php @@ -0,0 +1,61 @@ + 'int', + 'sort_order' => 'int', + 'is_active' => 'bool', + ]; + + public const TYPE_RANK = 'rank'; + + public const TYPE_TITLE = 'title'; + + // ========================================================================= + // 스코프 + // ========================================================================= + + public function scopeForTenant($query, ?int $tenantId = null) + { + $tenantId = $tenantId ?? session('selected_tenant_id'); + if ($tenantId) { + return $query->where('tenant_id', $tenantId); + } + + return $query; + } + + public function scopeRanks($query) + { + return $query->where('type', self::TYPE_RANK); + } + + public function scopeTitles($query) + { + return $query->where('type', self::TYPE_TITLE); + } + + public function scopeOrdered($query) + { + return $query->orderBy('sort_order'); + } +} diff --git a/app/Services/HR/EmployeeService.php b/app/Services/HR/EmployeeService.php new file mode 100644 index 00000000..42441d5a --- /dev/null +++ b/app/Services/HR/EmployeeService.php @@ -0,0 +1,237 @@ +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']); + } +} diff --git a/resources/views/hr/employees/create.blade.php b/resources/views/hr/employees/create.blade.php new file mode 100644 index 00000000..9def6fc0 --- /dev/null +++ b/resources/views/hr/employees/create.blade.php @@ -0,0 +1,185 @@ +@extends('layouts.app') + +@section('title', '사원 등록') + +@section('content') +
+ {{-- 페이지 헤더 --}} + + + {{-- 등록 폼 --}} +
+
+ +
+ + {{-- 기본 정보 --}} +
+

기본 정보

+
+ + {{-- 이름 --}} +
+ + +
+ + {{-- 사번 --}} +
+ + +
+ + {{-- 이메일 / 연락처 --}} +
+
+ + +
+
+ + +
+
+ + {{-- 비밀번호 --}} +
+ + +

미입력 시 기본 비밀번호가 설정됩니다.

+
+ + {{-- 근무 정보 --}} +
+

근무 정보

+
+ + {{-- 부서 --}} +
+ + +
+ + {{-- 직급 / 직책 --}} +
+
+ + +
+
+ + +
+
+ + {{-- 입사일 / 상태 --}} +
+
+ + +
+
+ + +
+
+ + {{-- 주소 --}} +
+ + +
+ + {{-- 비상연락처 --}} +
+ + +
+ + {{-- 버튼 --}} +
+ + 취소 + + +
+
+
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/hr/employees/edit.blade.php b/resources/views/hr/employees/edit.blade.php new file mode 100644 index 00000000..9f11c2fc --- /dev/null +++ b/resources/views/hr/employees/edit.blade.php @@ -0,0 +1,188 @@ +@extends('layouts.app') + +@section('title', '사원 수정') + +@section('content') +
+ {{-- 페이지 헤더 --}} + + + {{-- 수정 폼 --}} +
+
+ +
+ + {{-- 기본 정보 --}} +
+

기본 정보

+
+ + {{-- 이름 --}} +
+ + +
+ + {{-- 표시 이름 --}} +
+ + +
+ + {{-- 사번 --}} +
+ + +
+ + {{-- 이메일 / 연락처 --}} +
+
+ + +
+
+ + +
+
+ + {{-- 근무 정보 --}} +
+

근무 정보

+
+ + {{-- 부서 --}} +
+ + +
+ + {{-- 직급 / 직책 --}} +
+
+ + +
+
+ + +
+
+ + {{-- 입사일 / 상태 --}} +
+
+ + +
+
+ + +
+
+ + {{-- 주소 --}} +
+ + +
+ + {{-- 비상연락처 --}} +
+ + +
+ + {{-- 버튼 --}} +
+ + 취소 + + +
+
+
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/hr/employees/index.blade.php b/resources/views/hr/employees/index.blade.php new file mode 100644 index 00000000..dfd5bee5 --- /dev/null +++ b/resources/views/hr/employees/index.blade.php @@ -0,0 +1,112 @@ +@extends('layouts.app') + +@section('title', '사원관리') + +@section('content') +
+ {{-- 페이지 헤더 --}} +
+
+

사원관리

+

{{ now()->format('Y년 n월 j일') }} 현재

+
+ +
+ + {{-- 통계 카드 --}} +
+
+
전체
+
{{ $stats['total'] }}명
+
+
+
재직
+
{{ $stats['active'] }}명
+
+
+
휴직
+
{{ $stats['leave'] }}명
+
+
+
퇴직
+
{{ $stats['resigned'] }}명
+
+
+ + {{-- 테이블 컨테이너 --}} +
+ {{-- 필터 --}} +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + {{-- HTMX 테이블 영역 --}} +
+
+
+
+
+
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/hr/employees/partials/table.blade.php b/resources/views/hr/employees/partials/table.blade.php new file mode 100644 index 00000000..cad11e51 --- /dev/null +++ b/resources/views/hr/employees/partials/table.blade.php @@ -0,0 +1,146 @@ +{{-- 사원 목록 테이블 (HTMX로 로드) --}} + + + + + + + + + + + + + + + @forelse($employees as $employee) + + {{-- 사원 정보 --}} + + + {{-- 부서 --}} + + + {{-- 직급/직책 --}} + + + {{-- 상태 --}} + + + {{-- 입사일 --}} + + + {{-- 연락처 --}} + + + {{-- 작업 --}} + + + @empty + + + + @endforelse + +
사원부서직급/직책상태입사일연락처작업
+ +
+ {{ mb_substr($employee->display_name ?? $employee->user?->name ?? '?', 0, 1) }} +
+
+
+ {{ $employee->display_name ?? $employee->user?->name ?? '-' }} +
+ @if($employee->employee_code) +
{{ $employee->employee_code }}
+ @endif +
+
+
+ {{ $employee->department?->name ?? '-' }} + +
{{ $employee->position_label ?? '-' }}
+ @if($employee->job_title_label) +
{{ $employee->job_title_label }}
+ @endif +
+ @switch($employee->employee_status) + @case('active') + + 재직 + + @break + @case('leave') + + 휴직 + + @break + @case('resigned') + + 퇴직 + + @break + @default + + {{ $employee->employee_status ?? '-' }} + + @endswitch + + {{ $employee->hire_date ?? '-' }} + + {{ $employee->user?->phone ?? $employee->user?->email ?? '-' }} + +
+ {{-- 상세 --}} + + + + + + + + {{-- 수정 --}} + + + + + + + {{-- 퇴직 처리 --}} + @if($employee->employee_status !== 'resigned') + + @endif +
+
+
+ + + +

등록된 사원이 없습니다.

+ + 첫 번째 사원 등록하기 → + +
+
+
+ +{{-- 페이지네이션 --}} +@if($employees->hasPages()) +
+ {{ $employees->links() }} +
+@endif diff --git a/resources/views/hr/employees/show.blade.php b/resources/views/hr/employees/show.blade.php new file mode 100644 index 00000000..0af5b653 --- /dev/null +++ b/resources/views/hr/employees/show.blade.php @@ -0,0 +1,152 @@ +@extends('layouts.app') + +@section('title', '사원 상세') + +@section('content') +
+ {{-- 페이지 헤더 --}} + + + {{-- 프로필 카드 --}} +
+
+
+ {{ mb_substr($employee->display_name ?? $employee->user?->name ?? '?', 0, 1) }} +
+
+

+ {{ $employee->display_name ?? $employee->user?->name ?? '-' }} +

+
+ @if($employee->employee_code) + {{ $employee->employee_code }} + | + @endif + @if($employee->department) + {{ $employee->department->name }} + | + @endif + {{ $employee->position_label ?? '-' }} + @if($employee->job_title_label) + | + {{ $employee->job_title_label }} + @endif +
+
+ @switch($employee->employee_status) + @case('active') + 재직 + @break + @case('leave') + 휴직 + @break + @case('resigned') + 퇴직 + @break + @endswitch +
+
+
+
+ + {{-- 상세 정보 --}} +
+
+

상세 정보

+
+
+ {{-- 기본 정보 --}} +
+
이름
+
{{ $employee->user?->name ?? '-' }}
+
+
+
표시 이름
+
{{ $employee->display_name ?? '-' }}
+
+
+
사번
+
{{ $employee->employee_code ?? '-' }}
+
+
+
이메일
+
{{ $employee->user?->email ?? '-' }}
+
+
+
연락처
+
{{ $employee->user?->phone ?? '-' }}
+
+ + {{-- 근무 정보 --}} +
+ 근무 정보 +
+
+
부서
+
{{ $employee->department?->name ?? '-' }}
+
+
+
직급
+
{{ $employee->position_label ?? '-' }}
+
+
+
직책
+
{{ $employee->job_title_label ?? '-' }}
+
+
+
입사일
+
{{ $employee->hire_date ?? '-' }}
+
+
+
재직상태
+
+ @switch($employee->employee_status) + @case('active') 재직 @break + @case('leave') 휴직 @break + @case('resigned') 퇴직 @break + @default {{ $employee->employee_status }} @break + @endswitch +
+
+ + {{-- 추가 정보 --}} +
+ 추가 정보 +
+
+
주소
+
{{ $employee->address ?? '-' }}
+
+
+
비상연락처
+
{{ $employee->emergency_contact ?? '-' }}
+
+
+
등록일
+
{{ $employee->created_at?->format('Y-m-d H:i') ?? '-' }}
+
+
+
수정일
+
{{ $employee->updated_at?->format('Y-m-d H:i') ?? '-' }}
+
+
+
+
+@endsection diff --git a/routes/api.php b/routes/api.php index 06ad8be2..51fa040a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1034,3 +1034,18 @@ // 요약 결과 조회 (HTMX 지원) Route::get('/{id}/summary', [MeetingLogController::class, 'summary'])->name('summary'); }); + +/* +|-------------------------------------------------------------------------- +| HR (인사관리) API +|-------------------------------------------------------------------------- +*/ +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/hr/employees')->name('api.admin.hr.employees.')->group(function () { + Route::get('/stats', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'stats'])->name('stats'); + 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'); + Route::put('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'update'])->name('update'); + Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'destroy'])->name('destroy'); +}); + diff --git a/routes/web.php b/routes/web.php index 20d9f105..d4a795cc 100644 --- a/routes/web.php +++ b/routes/web.php @@ -883,6 +883,21 @@ return response()->file(public_path('일일자금일보.html')); }); +/* +|-------------------------------------------------------------------------- +| HR Routes (인사관리) +|-------------------------------------------------------------------------- +*/ +Route::middleware('auth')->prefix('hr')->name('hr.')->group(function () { + // 사원관리 + Route::prefix('employees')->name('employees.')->group(function () { + Route::get('/', [\App\Http\Controllers\HR\EmployeeController::class, 'index'])->name('index'); + Route::get('/create', [\App\Http\Controllers\HR\EmployeeController::class, 'create'])->name('create'); + Route::get('/{id}', [\App\Http\Controllers\HR\EmployeeController::class, 'show'])->name('show'); + Route::get('/{id}/edit', [\App\Http\Controllers\HR\EmployeeController::class, 'edit'])->name('edit'); + }); +}); + /* |-------------------------------------------------------------------------- | Finance Routes (재무 관리)