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 searchUsers(Request $request): JsonResponse { $users = $this->employeeService->searchTenantUsers($request->get('q') ?? ''); return response()->json([ 'success' => true, 'data' => $users, ]); } /** * 사원 통계 */ public function stats(): JsonResponse { $stats = $this->employeeService->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', ]; // 신규 사용자일 때만 이메일 unique 검증 if (! $request->filled('existing_user_id')) { $rules['email'] = 'nullable|email|max:100|unique:users,email'; } $validated = $request->validate($rules); try { $employee = $this->employeeService->createEmployee($validated); return response()->json([ 'success' => true, 'message' => '사원이 등록되었습니다.', 'data' => $employee, ], 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 { $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', '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', ]); try { $employee = $this->employeeService->updateEmployee($id, $validated); if (! $employee) { return response()->json([ 'success' => false, 'message' => '사원 정보를 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'message' => '사원 정보가 수정되었습니다.', 'data' => $employee, ]); } 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->employeeService->deleteEmployee($id); if (! $result) { if ($request->header('HX-Request')) { return response()->json([ 'success' => false, 'message' => '사원 정보를 찾을 수 없습니다.', ], 404); } 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' => '퇴직 처리되었습니다.', ]); } 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): JsonResponse { $employee = $this->employeeService->getEmployeeById($id); if (! $employee) { 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(); $path = "tenants/{$tenantId}/employees/{$employee->id}"; $file->storeAs($path, $storedName, 'tenant'); $fileRecord = File::create([ 'tenant_id' => $tenantId, 'document_id' => $employee->id, 'document_type' => 'employee_profile', 'original_name' => $file->getClientOriginalName(), 'stored_name' => $storedName, 'file_path' => $path.'/'.$storedName, 'mime_type' => $file->getMimeType(), 'file_size' => $file->getSize(), 'file_type' => strtolower($file->getClientOriginalExtension()), '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): JsonResponse { $tenantId = session('selected_tenant_id'); $file = File::where('id', $fileId) ->where('document_id', $id) ->where('document_type', 'employee_profile') ->where('tenant_id', $tenantId) ->first(); if (! $file) { return response()->json(['success' => false, 'message' => '파일을 찾을 수 없습니다.'], 404); } Storage::disk('tenant')->delete($file->file_path); $file->delete(); return response()->json(['success' => true, 'message' => '파일이 삭제되었습니다.']); } /** * 사원 첨부파일 다운로드 */ public function downloadFile(int $id, int $fileId) { $tenantId = session('selected_tenant_id'); $file = File::where('id', $fileId) ->where('document_id', $id) ->where('document_type', 'employee_profile') ->where('tenant_id', $tenantId) ->first(); if (! $file) { abort(404, '파일을 찾을 수 없습니다.'); } $disk = Storage::disk('tenant'); if (! $disk->exists($file->file_path)) { abort(404, '파일이 서버에 존재하지 않습니다.'); } return $disk->download($file->file_path, $file->original_name); } /** * 직급/직책 추가 */ public function storePosition(Request $request): JsonResponse { $validated = $request->validate([ 'type' => 'required|string|in:rank,title', 'name' => 'required|string|max:50', ]); $position = $this->employeeService->createPosition($validated['type'], $validated['name']); return response()->json([ 'success' => true, 'message' => ($validated['type'] === 'rank' ? '직급' : '직책').'이 추가되었습니다.', 'data' => [ 'id' => $position->id, 'key' => $position->key, 'name' => $position->name, ], ], 201); } }