From f02e96d4fd3616c941bca25d6bce72eb47946ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Fri, 27 Feb 2026 13:46:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[hr]=20=EC=82=AC=EC=97=85=EC=86=8C?= =?UTF-8?q?=EB=93=9D=EC=9E=90=EA=B4=80=EB=A6=AC=20=EB=A9=94=EB=89=B4=20?= =?UTF-8?q?=EC=8B=A0=EC=84=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BusinessIncomeEarner 모델 생성 (worker_type 글로벌 스코프) - Employee 모델에 worker_type 글로벌 스코프 추가 (기존 사원 격리) - BusinessIncomeEarnerService 생성 (등록/수정/삭제/조회) - Web/API 컨트롤러 생성 (CRUD + 파일 업로드) - 라우트 추가 (web.php, api.php) - View 5개 생성 (index, create, show, edit, partials/table) - 사업장등록정보 6개 필드 (사업자등록번호, 상호, 대표자명, 업태, 종목, 소재지) --- .../HR/BusinessIncomeEarnerController.php | 389 ++++++++++++ .../HR/BusinessIncomeEarnerController.php | 88 +++ app/Models/HR/BusinessIncomeEarner.php | 236 ++++++++ app/Models/HR/Employee.php | 12 + .../HR/BusinessIncomeEarnerService.php | 394 +++++++++++++ .../business-income-earners/create.blade.php | 533 +++++++++++++++++ .../hr/business-income-earners/edit.blade.php | 554 ++++++++++++++++++ .../business-income-earners/index.blade.php | 139 +++++ .../partials/table.blade.php | 153 +++++ .../hr/business-income-earners/show.blade.php | 282 +++++++++ routes/api.php | 16 + routes/web.php | 8 + 12 files changed, 2804 insertions(+) create mode 100644 app/Http/Controllers/Api/Admin/HR/BusinessIncomeEarnerController.php create mode 100644 app/Http/Controllers/HR/BusinessIncomeEarnerController.php create mode 100644 app/Models/HR/BusinessIncomeEarner.php create mode 100644 app/Services/HR/BusinessIncomeEarnerService.php create mode 100644 resources/views/hr/business-income-earners/create.blade.php create mode 100644 resources/views/hr/business-income-earners/edit.blade.php create mode 100644 resources/views/hr/business-income-earners/index.blade.php create mode 100644 resources/views/hr/business-income-earners/partials/table.blade.php create mode 100644 resources/views/hr/business-income-earners/show.blade.php diff --git a/app/Http/Controllers/Api/Admin/HR/BusinessIncomeEarnerController.php b/app/Http/Controllers/Api/Admin/HR/BusinessIncomeEarnerController.php new file mode 100644 index 00000000..51e580f3 --- /dev/null +++ b/app/Http/Controllers/Api/Admin/HR/BusinessIncomeEarnerController.php @@ -0,0 +1,389 @@ +service->getBusinessIncomeEarners( + $request->all(), + $request->integer('per_page', 20) + ); + + if ($request->header('HX-Request')) { + return response(view('hr.business-income-earners.partials.table', compact('earners'))); + } + + return response()->json([ + 'success' => true, + 'data' => $earners->items(), + 'meta' => [ + 'current_page' => $earners->currentPage(), + 'last_page' => $earners->lastPage(), + 'per_page' => $earners->perPage(), + 'total' => $earners->total(), + ], + ]); + } + + /** + * 기존 사용자 검색 (사업소득자 미등록, 테넌트 소속) + */ + public function searchUsers(Request $request): JsonResponse + { + $users = $this->service->searchTenantUsers($request->get('q') ?? ''); + + return response()->json([ + 'success' => true, + 'data' => $users, + ]); + } + + /** + * 사업소득자 통계 + */ + public function stats(): JsonResponse + { + $stats = $this->service->getStats(); + + return response()->json([ + 'success' => true, + 'data' => $stats, + ]); + } + + /** + * 사업소득자 등록 + */ + public function store(Request $request): JsonResponse + { + $rules = [ + 'existing_user_id' => 'nullable|integer|exists:users,id', + 'name' => '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', + 'hire_date' => 'nullable|date', + 'resign_date' => 'nullable|date', + 'address' => 'nullable|string|max:200', + 'emergency_contact' => 'nullable|string|max:100', + 'resident_number' => 'nullable|string|max:14', + 'bank_account.bank_code' => 'nullable|string|max:20', + 'bank_account.bank_name' => 'nullable|string|max:50', + 'bank_account.account_holder' => 'nullable|string|max:50', + 'bank_account.account_number' => 'nullable|string|max:30', + 'dependents' => 'nullable|array', + 'dependents.*.name' => 'required_with:dependents|string|max:50', + 'dependents.*.nationality' => 'nullable|string|in:korean,foreigner', + 'dependents.*.resident_number' => 'nullable|string|max:14', + 'dependents.*.relationship' => 'nullable|string|max:20', + 'dependents.*.is_disabled' => 'nullable|boolean', + 'dependents.*.is_dependent' => 'nullable|boolean', + // 사업장등록정보 + 'business_registration_number' => 'nullable|string|max:12', + 'business_name' => 'nullable|string|max:100', + 'business_representative' => 'nullable|string|max:50', + 'business_type' => 'nullable|string|max:50', + 'business_category' => 'nullable|string|max:50', + 'business_address' => 'nullable|string|max:200', + ]; + + if (! $request->filled('existing_user_id')) { + $rules['email'] = 'nullable|email|max:100|unique:users,email'; + } + + $validated = $request->validate($rules); + + try { + $earner = $this->service->create($validated); + + return response()->json([ + 'success' => true, + 'message' => '사업소득자가 등록되었습니다.', + 'data' => $earner, + ], 201); + } catch (\Throwable $e) { + report($e); + + return response()->json([ + 'success' => false, + 'message' => '사업소득자 등록 중 오류가 발생했습니다.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * 사업소득자 상세 조회 + */ + public function show(int $id): JsonResponse + { + $earner = $this->service->getById($id); + + if (! $earner) { + return response()->json([ + 'success' => false, + 'message' => '사업소득자 정보를 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $earner, + ]); + } + + /** + * 사업소득자 수정 + */ + 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', + 'hire_date' => 'nullable|date', + 'resign_date' => 'nullable|date', + 'address' => 'nullable|string|max:200', + 'emergency_contact' => 'nullable|string|max:100', + 'resident_number' => 'nullable|string|max:14', + 'bank_account.bank_code' => 'nullable|string|max:20', + 'bank_account.bank_name' => 'nullable|string|max:50', + 'bank_account.account_holder' => 'nullable|string|max:50', + 'bank_account.account_number' => 'nullable|string|max:30', + 'dependents' => 'nullable|array', + 'dependents.*.name' => 'required_with:dependents|string|max:50', + 'dependents.*.nationality' => 'nullable|string|in:korean,foreigner', + 'dependents.*.resident_number' => 'nullable|string|max:14', + 'dependents.*.relationship' => 'nullable|string|max:20', + 'dependents.*.is_disabled' => 'nullable|boolean', + 'dependents.*.is_dependent' => 'nullable|boolean', + // 사업장등록정보 + 'business_registration_number' => 'nullable|string|max:12', + 'business_name' => 'nullable|string|max:100', + 'business_representative' => 'nullable|string|max:50', + 'business_type' => 'nullable|string|max:50', + 'business_category' => 'nullable|string|max:50', + 'business_address' => 'nullable|string|max:200', + ]); + + if ($request->has('dependents_submitted') && ! array_key_exists('dependents', $validated)) { + $validated['dependents'] = []; + } + + try { + $earner = $this->service->update($id, $validated); + + if (! $earner) { + return response()->json([ + 'success' => false, + 'message' => '사업소득자 정보를 찾을 수 없습니다.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'message' => '사업소득자 정보가 수정되었습니다.', + 'data' => $earner, + ]); + } 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->service->delete($id); + + if (! $result) { + return response()->json([ + 'success' => false, + 'message' => '사업소득자 정보를 찾을 수 없습니다.', + ], 404); + } + + if ($request->header('HX-Request')) { + $earners = $this->service->getBusinessIncomeEarners($request->all(), $request->integer('per_page', 20)); + + return response(view('hr.business-income-earners.partials.table', compact('earners'))); + } + + 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); + } + } + + /** + * 첨부파일 업로드 + */ + public function uploadFile(Request $request, int $id, GoogleCloudStorageService $gcs): JsonResponse + { + $earner = $this->service->getById($id); + if (! $earner) { + return response()->json(['success' => false, 'message' => '사업소득자 정보를 찾을 수 없습니다.'], 404); + } + + $request->validate([ + 'files' => 'required|array|max:10', + 'files.*' => 'file|max:20480', + ]); + + $tenantId = session('selected_tenant_id'); + $uploaded = []; + + foreach ($request->file('files') as $file) { + $storedName = Str::random(40).'.'.$file->getClientOriginalExtension(); + $storagePath = "business-income-earners/{$tenantId}/{$earner->id}/{$storedName}"; + + Storage::disk('tenant')->put($storagePath, file_get_contents($file)); + + $gcsUri = null; + $gcsObjectName = null; + if ($gcs->isAvailable()) { + $gcsObjectName = $storagePath; + $gcsUri = $gcs->upload($file->getRealPath(), $gcsObjectName); + } + + $fileRecord = File::create([ + 'tenant_id' => $tenantId, + 'document_id' => $earner->id, + 'document_type' => 'business_income_earner_profile', + 'original_name' => $file->getClientOriginalName(), + 'stored_name' => $storedName, + 'file_path' => $storagePath, + 'mime_type' => $file->getMimeType(), + 'file_size' => $file->getSize(), + 'file_type' => strtolower($file->getClientOriginalExtension()), + 'gcs_object_name' => $gcsObjectName, + 'gcs_uri' => $gcsUri, + 'uploaded_by' => auth()->id(), + 'created_by' => auth()->id(), + ]); + + $uploaded[] = $fileRecord; + } + + return response()->json([ + 'success' => true, + 'message' => count($uploaded).'개 파일이 업로드되었습니다.', + 'data' => $uploaded, + ], 201); + } + + /** + * 첨부파일 삭제 + */ + public function deleteFile(int $id, int $fileId, GoogleCloudStorageService $gcs): JsonResponse + { + $tenantId = session('selected_tenant_id'); + + $file = File::where('id', $fileId) + ->where('document_id', $id) + ->where('document_type', 'business_income_earner_profile') + ->where('tenant_id', $tenantId) + ->first(); + + if (! $file) { + return response()->json(['success' => false, 'message' => '파일을 찾을 수 없습니다.'], 404); + } + + if ($gcs->isAvailable() && $file->gcs_object_name) { + $gcs->delete($file->gcs_object_name); + } + + if ($file->file_path && Storage::disk('tenant')->exists($file->file_path)) { + Storage::disk('tenant')->delete($file->file_path); + } + + $file->deleted_by = auth()->id(); + $file->save(); + $file->delete(); + + return response()->json(['success' => true, 'message' => '파일이 삭제되었습니다.']); + } + + /** + * 첨부파일 다운로드 + */ + public function downloadFile(int $id, int $fileId, GoogleCloudStorageService $gcs) + { + $tenantId = session('selected_tenant_id'); + + $file = File::where('id', $fileId) + ->where('document_id', $id) + ->where('document_type', 'business_income_earner_profile') + ->where('tenant_id', $tenantId) + ->first(); + + if (! $file) { + abort(404, '파일을 찾을 수 없습니다.'); + } + + if ($gcs->isAvailable() && $file->gcs_object_name) { + $signedUrl = $gcs->getSignedUrl($file->gcs_object_name, 60); + if ($signedUrl) { + return redirect($signedUrl); + } + } + + $disk = Storage::disk('tenant'); + if ($file->file_path && $disk->exists($file->file_path)) { + return $disk->download($file->file_path, $file->original_name); + } + + abort(404, '파일이 서버에 존재하지 않습니다.'); + } +} diff --git a/app/Http/Controllers/HR/BusinessIncomeEarnerController.php b/app/Http/Controllers/HR/BusinessIncomeEarnerController.php new file mode 100644 index 00000000..a931e006 --- /dev/null +++ b/app/Http/Controllers/HR/BusinessIncomeEarnerController.php @@ -0,0 +1,88 @@ +service->getStats(); + $departments = $this->service->getDepartments(); + + return view('hr.business-income-earners.index', [ + 'stats' => $stats, + 'departments' => $departments, + ]); + } + + public function create(): View + { + $departments = $this->service->getDepartments(); + $ranks = $this->service->getPositions('rank'); + $titles = $this->service->getPositions('title'); + + return view('hr.business-income-earners.create', [ + 'departments' => $departments, + 'ranks' => $ranks, + 'titles' => $titles, + 'banks' => config('banks', []), + ]); + } + + public function show(int $id): View + { + $earner = $this->service->getById($id); + + if (! $earner) { + abort(404, '사업소득자 정보를 찾을 수 없습니다.'); + } + + $files = File::where('document_type', 'business_income_earner_profile') + ->where('document_id', $earner->id) + ->where('tenant_id', session('selected_tenant_id')) + ->orderBy('created_at', 'desc') + ->get(); + + return view('hr.business-income-earners.show', [ + 'earner' => $earner, + 'files' => $files, + ]); + } + + public function edit(int $id): View + { + $earner = $this->service->getById($id); + + if (! $earner) { + abort(404, '사업소득자 정보를 찾을 수 없습니다.'); + } + + $departments = $this->service->getDepartments(); + $ranks = $this->service->getPositions('rank'); + $titles = $this->service->getPositions('title'); + + $files = File::where('document_type', 'business_income_earner_profile') + ->where('document_id', $earner->id) + ->where('tenant_id', session('selected_tenant_id')) + ->orderBy('created_at', 'desc') + ->get(); + + return view('hr.business-income-earners.edit', [ + 'earner' => $earner, + 'departments' => $departments, + 'ranks' => $ranks, + 'titles' => $titles, + 'banks' => config('banks', []), + 'files' => $files, + ]); + } +} diff --git a/app/Models/HR/BusinessIncomeEarner.php b/app/Models/HR/BusinessIncomeEarner.php new file mode 100644 index 00000000..941e1bbf --- /dev/null +++ b/app/Models/HR/BusinessIncomeEarner.php @@ -0,0 +1,236 @@ + 'array', + 'tenant_id' => 'int', + 'user_id' => 'int', + 'department_id' => 'int', + 'manager_user_id' => 'int', + ]; + + protected $appends = [ + 'hire_date', + 'resign_date', + 'position_label', + 'job_title_label', + ]; + + // ========================================================================= + // 글로벌 스코프 + 이벤트 + // ========================================================================= + + protected static function booted(): void + { + static::addGlobalScope('business_income', function (Builder $builder) { + $builder->where('worker_type', 'business_income'); + }); + + static::creating(function (self $model) { + $model->worker_type = 'business_income'; + }); + } + + // ========================================================================= + // 관계 정의 + // ========================================================================= + + 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 (Employee 동일) + // ========================================================================= + + public function getHireDateAttribute(): ?string + { + return $this->json_extra['hire_date'] ?? null; + } + + public function getResignDateAttribute(): ?string + { + return $this->json_extra['resign_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 getResidentNumberAttribute(): ?string + { + return $this->json_extra['resident_number'] ?? null; + } + + public function getBankAccountAttribute(): ?array + { + return $this->json_extra['bank_account'] ?? null; + } + + public function getDependentsAttribute(): array + { + return $this->json_extra['dependents'] ?? []; + } + + // ========================================================================= + // 사업장등록정보 Accessor (json_extra에서 읽기) + // ========================================================================= + + public function getBusinessRegistrationNumberAttribute(): ?string + { + return $this->json_extra['business_registration_number'] ?? null; + } + + public function getBusinessNameAttribute(): ?string + { + return $this->json_extra['business_name'] ?? null; + } + + public function getBusinessRepresentativeAttribute(): ?string + { + return $this->json_extra['business_representative'] ?? null; + } + + public function getBusinessTypeAttribute(): ?string + { + return $this->json_extra['business_type'] ?? null; + } + + public function getBusinessCategoryAttribute(): ?string + { + return $this->json_extra['business_category'] ?? null; + } + + public function getBusinessAddressAttribute(): ?string + { + return $this->json_extra['business_address'] ?? 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/Employee.php b/app/Models/HR/Employee.php index 1a6a8633..8437e732 100644 --- a/app/Models/HR/Employee.php +++ b/app/Models/HR/Employee.php @@ -5,6 +5,7 @@ use App\Models\Tenants\Department; use App\Models\User; use App\Traits\ModelTrait; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -23,12 +24,23 @@ class Employee extends Model 'work_location_key', 'employment_type_key', 'employee_status', + 'worker_type', 'manager_user_id', 'json_extra', 'profile_photo_path', 'display_name', ]; + protected static function booted(): void + { + static::addGlobalScope('employee', function (Builder $builder) { + $builder->where(function ($q) { + $q->where('worker_type', 'employee') + ->orWhereNull('worker_type'); + }); + }); + } + protected $casts = [ 'json_extra' => 'array', 'tenant_id' => 'int', diff --git a/app/Services/HR/BusinessIncomeEarnerService.php b/app/Services/HR/BusinessIncomeEarnerService.php new file mode 100644 index 00000000..80f8741c --- /dev/null +++ b/app/Services/HR/BusinessIncomeEarnerService.php @@ -0,0 +1,394 @@ +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}%"); + }); + }); + } + + if (! empty($filters['status'])) { + $query->where('employee_status', $filters['status']); + } + + if (! empty($filters['department_id'])) { + $query->where('department_id', $filters['department_id']); + } + + $sortBy = $filters['sort_by'] ?? 'hire_date_asc'; + switch ($sortBy) { + case 'hire_date_asc': + $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) ASC"); + break; + case 'hire_date_desc': + $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC"); + break; + case 'resign_date_asc': + $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) ASC"); + break; + case 'resign_date_desc': + $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) DESC"); + break; + default: + $query->orderByRaw("FIELD(employee_status, 'active', 'leave', 'resigned')") + ->orderBy('created_at', 'desc'); + break; + } + + return $query->paginate($perPage); + } + + /** + * 사업소득자 상세 조회 + */ + public function getById(int $id): ?BusinessIncomeEarner + { + $tenantId = session('selected_tenant_id'); + + return BusinessIncomeEarner::query() + ->with(['user', 'department', 'manager']) + ->forTenant($tenantId) + ->find($id); + } + + /** + * 사업소득자 통계 + */ + public function getStats(): array + { + $tenantId = session('selected_tenant_id'); + + $baseQuery = BusinessIncomeEarner::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(), + ]; + } + + /** + * 테넌트 소속이지만 사업소득자 미등록인 사용자 검색 + */ + public function searchTenantUsers(string $query): array + { + $tenantId = session('selected_tenant_id'); + + $builder = User::query() + ->select('users.id', 'users.name', 'users.email', 'users.phone') + ->join('user_tenants as ut', function ($join) use ($tenantId) { + $join->on('users.id', '=', 'ut.user_id') + ->where('ut.tenant_id', $tenantId) + ->whereNull('ut.deleted_at'); + }) + ->leftJoin('tenant_user_profiles as tup', function ($join) use ($tenantId) { + $join->on('users.id', '=', 'tup.user_id') + ->where('tup.tenant_id', $tenantId); + }) + ->whereNull('tup.id') + ->whereNull('users.deleted_at'); + + if ($query !== '') { + $like = "%{$query}%"; + $builder->where(function ($q) use ($like) { + $q->where('users.name', 'like', $like) + ->orWhere('users.email', 'like', $like) + ->orWhere('users.phone', 'like', $like); + }); + } + + return $builder->orderBy('users.name')->limit(20)->get()->toArray(); + } + + /** + * 사업소득자 등록 (User is_active=false + TenantUserProfile 생성) + */ + public function create(array $data): BusinessIncomeEarner + { + $tenantId = session('selected_tenant_id'); + + return DB::transaction(function () use ($data, $tenantId) { + if (! empty($data['existing_user_id'])) { + $user = User::findOrFail($data['existing_user_id']); + + $isMember = $user->tenants() + ->wherePivot('tenant_id', $tenantId) + ->wherePivotNull('deleted_at') + ->exists(); + + if (! $isMember) { + throw new \RuntimeException('해당 사용자는 현재 테넌트에 소속되어 있지 않습니다.'); + } + + $alreadyRegistered = BusinessIncomeEarner::where('tenant_id', $tenantId) + ->where('user_id', $user->id) + ->exists(); + + if ($alreadyRegistered) { + throw new \RuntimeException('이미 사업소득자로 등록된 사용자입니다.'); + } + } else { + // 신규 사용자 생성 (is_active=false, 로그인 불가) + $loginId = ! empty($data['email']) + ? Str::before($data['email'], '@') + : 'BIZ_'.strtolower(Str::random(6)); + + while (User::where('user_id', $loginId)->exists()) { + $loginId = $loginId.'_'.Str::random(3); + } + + $email = ! empty($data['email']) + ? $data['email'] + : $loginId.'@placeholder.local'; + + while (User::where('email', $email)->exists()) { + $email = $loginId.'_'.Str::random(3).'@placeholder.local'; + } + + $user = User::create([ + 'user_id' => $loginId, + 'name' => $data['name'], + 'email' => $email, + 'phone' => $data['phone'] ?? null, + 'password' => Hash::make(Str::random(32)), + 'role' => 'ops', + 'is_active' => false, + 'must_change_password' => false, + 'created_by' => auth()->id(), + ]); + + if ($tenantId) { + $user->tenants()->attach($tenantId, [ + 'is_active' => true, + 'is_default' => true, + 'joined_at' => now(), + ]); + } + } + + // json_extra 구성 + $jsonExtra = []; + $scalarKeys = [ + 'hire_date', 'resign_date', 'address', 'emergency_contact', 'resident_number', + 'business_registration_number', 'business_name', 'business_representative', + 'business_type', 'business_category', 'business_address', + ]; + foreach ($scalarKeys as $key) { + if (! empty($data[$key])) { + $jsonExtra[$key] = $data[$key]; + } + } + + // 급여이체정보 + if (! empty($data['bank_account']) && is_array($data['bank_account'])) { + $bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== ''); + if (! empty($bankAccount)) { + $jsonExtra['bank_account'] = $bankAccount; + } + } + + // 부양가족 정보 + if (! empty($data['dependents']) && is_array($data['dependents'])) { + $dependents = array_values(array_filter($data['dependents'], function ($dep) { + return ! empty($dep['name']); + })); + $dependents = array_map(function ($dep) { + $dep['is_disabled'] = filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN); + $dep['is_dependent'] = filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN); + + return $dep; + }, $dependents); + if (! empty($dependents)) { + $jsonExtra['dependents'] = $dependents; + } + } + + $earner = BusinessIncomeEarner::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 $earner->load(['user', 'department']); + }); + } + + /** + * 사업소득자 정보 수정 + */ + public function update(int $id, array $data): ?BusinessIncomeEarner + { + $earner = $this->getById($id); + if (! $earner) { + 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 = [ + 'hire_date', 'resign_date', 'address', 'emergency_contact', 'salary', 'resident_number', + 'business_registration_number', 'business_name', 'business_representative', + 'business_type', 'business_category', 'business_address', + ]; + $extra = $earner->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]; + } + } + } + + // 급여이체정보 + if (array_key_exists('bank_account', $data)) { + if (! empty($data['bank_account']) && is_array($data['bank_account'])) { + $bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== ''); + if (! empty($bankAccount)) { + $extra['bank_account'] = $bankAccount; + } else { + unset($extra['bank_account']); + } + } else { + unset($extra['bank_account']); + } + } + + // 부양가족 정보 + if (array_key_exists('dependents', $data)) { + if (! empty($data['dependents']) && is_array($data['dependents'])) { + $dependents = array_values(array_filter($data['dependents'], function ($dep) { + return ! empty($dep['name']); + })); + $dependents = array_map(function ($dep) { + $dep['is_disabled'] = filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN); + $dep['is_dependent'] = filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN); + + return $dep; + }, $dependents); + if (! empty($dependents)) { + $extra['dependents'] = $dependents; + } else { + unset($extra['dependents']); + } + } else { + unset($extra['dependents']); + } + } + + $updateData['json_extra'] = ! empty($extra) ? $extra : null; + + $earner->update($updateData); + + // User 기본정보 동기화 + if ($earner->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)) { + $earner->user->update($userUpdate); + } + } + + return $earner->fresh(['user', 'department']); + } + + /** + * 사업소득자 삭제 (퇴직 처리) + */ + public function delete(int $id): bool + { + $earner = $this->getById($id); + if (! $earner) { + return false; + } + + $earner->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/business-income-earners/create.blade.php b/resources/views/hr/business-income-earners/create.blade.php new file mode 100644 index 00000000..77dab886 --- /dev/null +++ b/resources/views/hr/business-income-earners/create.blade.php @@ -0,0 +1,533 @@ +@extends('layouts.app') + +@section('title', '사업소득자 등록') + +@section('content') +
+ {{-- 페이지 헤더 --}} +
+ + + + + 사업소득자 목록으로 + +

사업소득자 등록

+
+ + {{-- 등록 폼 --}} +
+
+ +
+ + {{-- 기존 직원 불러오기 --}} +
+

기존 사용자 불러오기

+ + + + + + +

이미 시스템에 등록된 사용자를 사업소득자로 추가할 수 있습니다.

+
+ + {{-- 기본 정보 --}} +
+

기본 정보

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

근무 정보

+
+ +
+ + +
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + {{-- 사업장등록정보 --}} +
+

사업장등록정보

+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + {{-- 급여이체정보 --}} +
+

급여이체정보

+
+ +
+ + + +
+ +
+
+ + +
+
+ + +
+
+ + {{-- 부양가족 정보 --}} +
+

부양가족 정보

+
+ +
+ + + +
+ + {{-- 첨부파일 안내 --}} +
+

첨부파일

+
+
+

등록 후 상세/수정 페이지에서 파일을 업로드할 수 있습니다.

+
+ + {{-- 버튼 --}} +
+ + 취소 + + +
+
+
+
+{{-- 직급/직책 추가 모달 --}} +@include('hr.employees.partials.position-add-modal') +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/hr/business-income-earners/edit.blade.php b/resources/views/hr/business-income-earners/edit.blade.php new file mode 100644 index 00000000..d7745cf1 --- /dev/null +++ b/resources/views/hr/business-income-earners/edit.blade.php @@ -0,0 +1,554 @@ +@extends('layouts.app') + +@section('title', '사업소득자 수정') + +@section('content') +
+ {{-- 페이지 헤더 --}} +
+ + + + + 사업소득자 목록으로 + +

사업소득자 수정

+
+ + {{-- 수정 폼 --}} +
+
+ + +
+ + {{-- 기본 정보 --}} +
+

기본 정보

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

근무 정보

+
+ +
+ + +
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ + {{-- 사업장등록정보 --}} +
+

사업장등록정보

+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + {{-- 급여이체정보 --}} +
+

급여이체정보

+
+ + @php $bankAccount = $earner->bank_account ?? []; @endphp + +
+ + + +
+ +
+
+ + +
+
+ + +
+
+ + {{-- 부양가족 정보 --}} +
+

부양가족 정보

+
+ +
+ + + + +
+ + {{-- 버튼 --}} +
+ + 취소 + + +
+
+
+ + {{-- 첨부파일 --}} +
+
+

첨부파일

+
+ +
+ @forelse($files ?? [] as $file) +
+
+ + + + + {{ $file->original_name }} + + {{ number_format(($file->file_size ?? 0) / 1024, 0) }}KB +
+ +
+ @empty +

등록된 파일이 없습니다.

+ @endforelse +
+ +
+ + + + +

파일을 드래그하거나 클릭하여 업로드

+

파일당 최대 20MB

+
+ +
업로드 중...
+
+
+ +{{-- 직급/직책 추가 모달 --}} +@include('hr.employees.partials.position-add-modal') +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/hr/business-income-earners/index.blade.php b/resources/views/hr/business-income-earners/index.blade.php new file mode 100644 index 00000000..314ae445 --- /dev/null +++ b/resources/views/hr/business-income-earners/index.blade.php @@ -0,0 +1,139 @@ +@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/business-income-earners/partials/table.blade.php b/resources/views/hr/business-income-earners/partials/table.blade.php new file mode 100644 index 00000000..946d2920 --- /dev/null +++ b/resources/views/hr/business-income-earners/partials/table.blade.php @@ -0,0 +1,153 @@ +{{-- 사업소득자 목록 테이블 (HTMX로 로드) --}} + + + + + + + + + + + + + + + @forelse($earners as $earner) + + {{-- 사업소득자 정보 --}} + + + {{-- 부서 --}} + + + {{-- 직급/직책 --}} + + + {{-- 상태 --}} + + + {{-- 계약일 --}} + + + {{-- 연락처 --}} + + + {{-- 작업 --}} + + + @empty + + + + @endforelse + +
사업소득자부서직급/직책상태 + + 연락처작업
+ +
+ {{ mb_substr($earner->display_name ?? $earner->user?->name ?? '?', 0, 1) }} +
+
+
+ {{ $earner->display_name ?? $earner->user?->name ?? '-' }} +
+
+
+
+ {{ $earner->department?->name ?? '-' }} + +
{{ $earner->position_label ?? '-' }}
+ @if($earner->job_title_label) +
{{ $earner->job_title_label }}
+ @endif +
+ @switch($earner->employee_status) + @case('active') + + 계약중 + + @break + @case('leave') + + 휴직 + + @break + @case('resigned') + + 계약종료 + + @break + @default + + {{ $earner->employee_status ?? '-' }} + + @endswitch + + {{ $earner->hire_date ?? '-' }} + + {{ $earner->user?->phone ?? $earner->user?->email ?? '-' }} + +
+ + + + + + + + + + + + + + @if($earner->employee_status !== 'resigned') + + @endif +
+
+
+ + + +

등록된 사업소득자가 없습니다.

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

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

+
+ @if($earner->department) + {{ $earner->department->name }} + | + @endif + {{ $earner->position_label ?? '-' }} + @if($earner->job_title_label) + | + {{ $earner->job_title_label }} + @endif +
+
+ @switch($earner->employee_status) + @case('active') + 계약중 + @break + @case('leave') + 휴직 + @break + @case('resigned') + 계약종료 + @break + @endswitch +
+
+
+
+ + {{-- 상세 정보 --}} +
+
+

상세 정보

+
+
+ {{-- 기본 정보 --}} +
+
이름
+
{{ $earner->user?->name ?? '-' }}
+
+
+
표시 이름
+
{{ $earner->display_name ?? '-' }}
+
+
+
이메일
+
{{ $earner->user?->email ?? '-' }}
+
+
+
연락처
+
{{ $earner->user?->phone ?? '-' }}
+
+
+
주민등록번호
+
+ @if($earner->resident_number) + {{ Str::mask($earner->resident_number, '*', 8) }} + @else + - + @endif +
+
+ + {{-- 근무 정보 --}} +
+ 근무 정보 +
+
+
부서
+
{{ $earner->department?->name ?? '-' }}
+
+
+
직급
+
{{ $earner->position_label ?? '-' }}
+
+
+
직책
+
{{ $earner->job_title_label ?? '-' }}
+
+
+
계약일
+
{{ $earner->hire_date ?? '-' }}
+
+
+
계약종료일
+
{{ $earner->resign_date ?? '-' }}
+
+
+
상태
+
+ @switch($earner->employee_status) + @case('active') 계약중 @break + @case('leave') 휴직 @break + @case('resigned') 계약종료 @break + @default {{ $earner->employee_status }} @break + @endswitch +
+
+ + {{-- 사업장등록정보 --}} +
+ 사업장등록정보 +
+
+
사업자등록번호
+
{{ $earner->business_registration_number ?? '-' }}
+
+
+
상호
+
{{ $earner->business_name ?? '-' }}
+
+
+
대표자명
+
{{ $earner->business_representative ?? '-' }}
+
+
+
업태
+
{{ $earner->business_type ?? '-' }}
+
+
+
종목
+
{{ $earner->business_category ?? '-' }}
+
+
+
사업장소재지
+
{{ $earner->business_address ?? '-' }}
+
+ + {{-- 추가 정보 --}} +
+ 추가 정보 +
+
+
주소
+
{{ $earner->address ?? '-' }}
+
+
+
비상연락처
+
{{ $earner->emergency_contact ?? '-' }}
+
+
+
등록일
+
{{ $earner->created_at?->format('Y-m-d H:i') ?? '-' }}
+
+ + {{-- 급여이체정보 --}} +
+ 급여이체정보 +
+ @php $bankAccount = $earner->bank_account ?? []; @endphp +
+
이체은행
+
{{ $bankAccount['bank_name'] ?? '-' }}
+
+
+
예금주
+
{{ $bankAccount['account_holder'] ?? '-' }}
+
+
+
계좌번호
+
{{ $bankAccount['account_number'] ?? '-' }}
+
+ + {{-- 부양가족 정보 --}} +
+ 부양가족 정보 +
+ @php $dependents = $earner->dependents ?? []; @endphp + @if(count($dependents) > 0) +
+
+ + + + + + + + + + + + + @foreach($dependents as $dep) + + + + + + + + + @endforeach + +
이름내/외국인주민등록번호관계장애인피부양자
{{ $dep['name'] ?? '-' }}{{ ($dep['nationality'] ?? 'korean') === 'korean' ? '내국인' : '외국인' }} + @if(!empty($dep['resident_number'])) + {{ Str::mask($dep['resident_number'], '*', 8) }} + @else + - + @endif + + @switch($dep['relationship'] ?? '') + @case('spouse') 배우자 @break + @case('child') 자녀 @break + @case('parent') 부모 @break + @case('sibling') 형제자매 @break + @case('grandparent') 조부모 @break + @case('other') 기타 @break + @default - @break + @endswitch + {{ filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN) ? 'Y' : 'N' }}{{ filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN) ? 'Y' : 'N' }}
+
+
+ @else +
+

등록된 부양가족이 없습니다.

+
+ @endif + + {{-- 첨부파일 --}} +
+ 첨부파일 +
+ @if(!empty($files) && count($files) > 0) +
+ @foreach($files as $file) +
+ + + + + {{ $file->original_name }} + + {{ number_format(($file->file_size ?? 0) / 1024, 0) }}KB +
+ @endforeach +
+ @else +
+

등록된 파일이 없습니다.

+
+ @endif +
+
+
+@endsection diff --git a/routes/api.php b/routes/api.php index a8bc3ddb..0bf0bfd9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1057,6 +1057,22 @@ Route::get('/{id}/files/{fileId}/download', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'downloadFile'])->name('download-file'); }); +// 사업소득자관리 API +Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/hr/business-income-earners')->name('api.admin.hr.business-income-earners.')->group(function () { + Route::get('/search-users', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'searchUsers'])->name('search-users'); + Route::get('/stats', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'stats'])->name('stats'); + Route::get('/', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'index'])->name('index'); + Route::post('/', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'store'])->name('store'); + Route::get('/{id}', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'show'])->name('show'); + Route::put('/{id}', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'update'])->name('update'); + Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'destroy'])->name('destroy'); + + // 첨부파일 + Route::post('/{id}/files', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'uploadFile'])->name('upload-file'); + Route::delete('/{id}/files/{fileId}', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'deleteFile'])->name('delete-file'); + Route::get('/{id}/files/{fileId}/download', [\App\Http\Controllers\Api\Admin\HR\BusinessIncomeEarnerController::class, 'downloadFile'])->name('download-file'); +}); + // 직급/직책 관리 API Route::middleware(['web', 'auth', 'hq.member'])->prefix('admin/hr/positions')->name('api.admin.hr.positions.')->group(function () { Route::post('/', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'storePosition'])->name('store'); diff --git a/routes/web.php b/routes/web.php index 01448b7e..e3852dd1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -897,6 +897,14 @@ Route::get('/{id}/edit', [\App\Http\Controllers\HR\EmployeeController::class, 'edit'])->name('edit'); }); + // 사업소득자관리 + Route::prefix('business-income-earners')->name('business-income-earners.')->group(function () { + Route::get('/', [\App\Http\Controllers\HR\BusinessIncomeEarnerController::class, 'index'])->name('index'); + Route::get('/create', [\App\Http\Controllers\HR\BusinessIncomeEarnerController::class, 'create'])->name('create'); + Route::get('/{id}', [\App\Http\Controllers\HR\BusinessIncomeEarnerController::class, 'show'])->name('show'); + Route::get('/{id}/edit', [\App\Http\Controllers\HR\BusinessIncomeEarnerController::class, 'edit'])->name('edit'); + }); + // 입퇴사자 현황 Route::get('/employee-tenure', [\App\Http\Controllers\HR\EmployeeTenureController::class, 'index'])->name('employee-tenure');