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, '파일이 서버에 존재하지 않습니다.'); } }