input('scenario_type', 'sales'); $stepId = $request->input('step_id'); // DB에서 상담 기록 조회 $consultations = SalesConsultation::getByTenantAndType($tenantId, $scenarioType, $stepId); return view('sales.modals.consultation-log', [ 'tenant' => $tenant, 'consultations' => $consultations, 'scenarioType' => $scenarioType, 'stepId' => $stepId, ]); } /** * 텍스트 상담 기록 저장 */ public function store(Request $request): JsonResponse { $request->validate([ 'tenant_id' => 'required|integer|exists:tenants,id', 'scenario_type' => 'required|in:sales,manager', 'step_id' => 'nullable|integer', 'content' => 'required|string|max:5000', ]); $consultation = SalesConsultation::createText( $request->input('tenant_id'), $request->input('scenario_type'), $request->input('step_id'), $request->input('content') ); $consultation->load('creator'); return response()->json([ 'success' => true, 'consultation' => [ 'id' => $consultation->id, 'type' => $consultation->consultation_type, 'content' => $consultation->content, 'created_by_name' => $consultation->creator->name, 'created_at' => $consultation->created_at->format('Y-m-d H:i'), ], ]); } /** * 상담 기록 삭제 */ public function destroy(int $consultationId, Request $request): JsonResponse { $consultation = SalesConsultation::findOrFail($consultationId); // 파일이 있으면 함께 삭제 $consultation->deleteWithFile(); return response()->json([ 'success' => true, ]); } /** * 음성 파일 업로드 * * 10MB 이상 파일은 Google Cloud Storage에 업로드하여 본사 연구용으로 보관합니다. */ public function uploadAudio(Request $request, GoogleCloudStorageService $gcs): JsonResponse { $request->validate([ 'tenant_id' => 'required|integer|exists:tenants,id', 'scenario_type' => 'required|in:sales,manager', 'step_id' => 'nullable|integer', 'audio' => 'required|file|mimes:webm,mp3,wav,ogg|max:51200', // 50MB 'transcript' => 'nullable|string|max:10000', 'duration' => 'nullable|integer', ]); $tenantId = $request->input('tenant_id'); $scenarioType = $request->input('scenario_type'); $stepId = $request->input('step_id'); $transcript = $request->input('transcript'); $duration = $request->input('duration'); // 파일 저장 $file = $request->file('audio'); $fileName = 'audio_' . now()->format('Ymd_His') . '_' . uniqid() . '.' . $file->getClientOriginalExtension(); $localPath = $file->storeAs("tenant/consultations/{$tenantId}", $fileName, 'local'); $fileSize = $file->getSize(); // 10MB 이상 파일은 GCS에도 업로드 (본사 연구용) $gcsUri = null; $maxLocalSize = 10 * 1024 * 1024; // 10MB if ($fileSize > $maxLocalSize && $gcs->isAvailable()) { $gcsObjectName = "consultations/{$tenantId}/{$scenarioType}/{$fileName}"; $localFullPath = Storage::disk('local')->path($localPath); $gcsUri = $gcs->upload($localFullPath, $gcsObjectName); } // DB에 저장 $consultation = SalesConsultation::createAudio( $tenantId, $scenarioType, $stepId, $localPath, $fileName, $fileSize, $transcript, $duration, $gcsUri ); $consultation->load('creator'); return response()->json([ 'success' => true, 'consultation' => [ 'id' => $consultation->id, 'type' => $consultation->consultation_type, 'file_name' => $consultation->file_name, 'transcript' => $consultation->transcript, 'duration' => $consultation->duration, 'formatted_duration' => $consultation->formatted_duration, 'created_by_name' => $consultation->creator->name, 'created_at' => $consultation->created_at->format('Y-m-d H:i'), 'has_gcs' => !empty($gcsUri), ], ]); } /** * 첨부파일 업로드 */ public function uploadFile(Request $request): JsonResponse { $request->validate([ 'tenant_id' => 'required|integer|exists:tenants,id', 'scenario_type' => 'required|in:sales,manager', 'step_id' => 'nullable|integer', 'file' => 'required|file|max:20480', // 20MB ]); $tenantId = $request->input('tenant_id'); $scenarioType = $request->input('scenario_type'); $stepId = $request->input('step_id'); // 파일 저장 $file = $request->file('file'); $originalName = $file->getClientOriginalName(); $fileName = now()->format('Ymd_His') . '_' . uniqid() . '_' . $originalName; $path = $file->storeAs("tenant/attachments/{$tenantId}", $fileName, 'local'); // DB에 저장 $consultation = SalesConsultation::createFile( $tenantId, $scenarioType, $stepId, $path, $originalName, $file->getSize(), $file->getMimeType() ); $consultation->load('creator'); return response()->json([ 'success' => true, 'consultation' => [ 'id' => $consultation->id, 'type' => $consultation->consultation_type, 'file_name' => $consultation->file_name, 'file_size' => $consultation->file_size, 'formatted_file_size' => $consultation->formatted_file_size, 'created_by_name' => $consultation->creator->name, 'created_at' => $consultation->created_at->format('Y-m-d H:i'), ], ]); } /** * 파일 삭제 */ public function deleteFile(int $fileId, Request $request): JsonResponse { return $this->destroy($fileId, $request); } /** * 오디오 파일 다운로드 */ public function downloadAudio(int $consultationId, GoogleCloudStorageService $gcs): BinaryFileResponse|RedirectResponse { $consultation = SalesConsultation::findOrFail($consultationId); if ($consultation->consultation_type !== 'audio') { abort(400, '오디오 파일이 아닙니다.'); } // GCS에 저장된 경우 서명된 URL로 리다이렉트 if ($consultation->gcs_uri) { $objectName = str_replace('gs://' . $gcs->getBucketName() . '/', '', $consultation->gcs_uri); $signedUrl = $gcs->getSignedUrl($objectName, 60); if ($signedUrl) { return redirect()->away($signedUrl); } } // 로컬 파일 다운로드 $localPath = Storage::disk('local')->path($consultation->file_path); if (!file_exists($localPath)) { abort(404, '파일을 찾을 수 없습니다.'); } $extension = pathinfo($consultation->file_name, PATHINFO_EXTENSION) ?: 'webm'; $mimeTypes = [ 'webm' => 'audio/webm', 'wav' => 'audio/wav', 'mp3' => 'audio/mpeg', 'ogg' => 'audio/ogg', 'm4a' => 'audio/mp4' ]; $contentType = $mimeTypes[$extension] ?? 'audio/webm'; $downloadFileName = '상담녹음_' . $consultation->created_at->format('Ymd_His') . '.' . $extension; return response()->download($localPath, $downloadFileName, [ 'Content-Type' => $contentType, ]); } /** * 첨부파일 다운로드 */ public function downloadFile(int $consultationId): BinaryFileResponse { $consultation = SalesConsultation::findOrFail($consultationId); if ($consultation->consultation_type !== 'file') { abort(400, '첨부파일이 아닙니다.'); } $localPath = Storage::disk('local')->path($consultation->file_path); if (!file_exists($localPath)) { abort(404, '파일을 찾을 수 없습니다.'); } return response()->download($localPath, $consultation->file_name); } // ======================================== // Prospect(가망고객) 관련 메서드 // ======================================== /** * 가망고객 상담 기록 목록 (HTMX 부분 뷰) */ public function prospectIndex(int $prospectId, Request $request): View { $prospect = TenantProspect::findOrFail($prospectId); $scenarioType = $request->input('scenario_type', 'sales'); $stepId = $request->input('step_id'); $consultations = SalesConsultation::getByProspectAndType($prospectId, $scenarioType, $stepId); return view('sales.modals.consultation-log', [ 'prospect' => $prospect, 'isProspect' => true, 'consultations' => $consultations, 'scenarioType' => $scenarioType, 'stepId' => $stepId, ]); } /** * 가망고객 텍스트 상담 기록 저장 */ public function prospectStore(Request $request): JsonResponse { $request->validate([ 'prospect_id' => 'required|integer|exists:tenant_prospects,id', 'scenario_type' => 'required|in:sales,manager', 'step_id' => 'nullable|integer', 'content' => 'required|string|max:5000', ]); $consultation = SalesConsultation::createTextByProspect( $request->input('prospect_id'), $request->input('scenario_type'), $request->input('step_id'), $request->input('content') ); $consultation->load('creator'); return response()->json([ 'success' => true, 'consultation' => [ 'id' => $consultation->id, 'type' => $consultation->consultation_type, 'content' => $consultation->content, 'created_by_name' => $consultation->creator->name, 'created_at' => $consultation->created_at->format('Y-m-d H:i'), ], ]); } /** * 가망고객 음성 파일 업로드 */ public function prospectUploadAudio(Request $request, GoogleCloudStorageService $gcs): JsonResponse { $request->validate([ 'prospect_id' => 'required|integer|exists:tenant_prospects,id', 'scenario_type' => 'required|in:sales,manager', 'step_id' => 'nullable|integer', 'audio' => 'required|file|mimes:webm,mp3,wav,ogg|max:51200', 'transcript' => 'nullable|string|max:10000', 'duration' => 'nullable|integer', ]); $prospectId = $request->input('prospect_id'); $scenarioType = $request->input('scenario_type'); $stepId = $request->input('step_id'); $transcript = $request->input('transcript'); $duration = $request->input('duration'); $file = $request->file('audio'); $fileName = 'audio_' . now()->format('Ymd_His') . '_' . uniqid() . '.' . $file->getClientOriginalExtension(); $localPath = $file->storeAs("prospect/consultations/{$prospectId}", $fileName, 'local'); $fileSize = $file->getSize(); $gcsUri = null; $maxLocalSize = 10 * 1024 * 1024; if ($fileSize > $maxLocalSize && $gcs->isAvailable()) { $gcsObjectName = "consultations/prospect/{$prospectId}/{$scenarioType}/{$fileName}"; $localFullPath = Storage::disk('local')->path($localPath); $gcsUri = $gcs->upload($localFullPath, $gcsObjectName); } $consultation = SalesConsultation::createAudioByProspect( $prospectId, $scenarioType, $stepId, $localPath, $fileName, $fileSize, $transcript, $duration, $gcsUri ); $consultation->load('creator'); return response()->json([ 'success' => true, 'consultation' => [ 'id' => $consultation->id, 'type' => $consultation->consultation_type, 'file_name' => $consultation->file_name, 'transcript' => $consultation->transcript, 'duration' => $consultation->duration, 'formatted_duration' => $consultation->formatted_duration, 'created_by_name' => $consultation->creator->name, 'created_at' => $consultation->created_at->format('Y-m-d H:i'), 'has_gcs' => !empty($gcsUri), ], ]); } /** * 가망고객 첨부파일 업로드 */ public function prospectUploadFile(Request $request): JsonResponse { $request->validate([ 'prospect_id' => 'required|integer|exists:tenant_prospects,id', 'scenario_type' => 'required|in:sales,manager', 'step_id' => 'nullable|integer', 'file' => 'required|file|max:20480', ]); $prospectId = $request->input('prospect_id'); $scenarioType = $request->input('scenario_type'); $stepId = $request->input('step_id'); $file = $request->file('file'); $originalName = $file->getClientOriginalName(); $fileName = now()->format('Ymd_His') . '_' . uniqid() . '_' . $originalName; $path = $file->storeAs("prospect/attachments/{$prospectId}", $fileName, 'local'); $consultation = SalesConsultation::createFileByProspect( $prospectId, $scenarioType, $stepId, $path, $originalName, $file->getSize(), $file->getMimeType() ); $consultation->load('creator'); return response()->json([ 'success' => true, 'consultation' => [ 'id' => $consultation->id, 'type' => $consultation->consultation_type, 'file_name' => $consultation->file_name, 'file_size' => $consultation->file_size, 'formatted_file_size' => $consultation->formatted_file_size, 'created_by_name' => $consultation->creator->name, 'created_at' => $consultation->created_at->format('Y-m-d H:i'), ], ]); } }