diff --git a/app/Http/Controllers/Sales/ConsultationController.php b/app/Http/Controllers/Sales/ConsultationController.php index 1f27f719..40265ddb 100644 --- a/app/Http/Controllers/Sales/ConsultationController.php +++ b/app/Http/Controllers/Sales/ConsultationController.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Controller; use App\Models\Sales\SalesConsultation; +use App\Models\Sales\TenantProspect; use App\Models\Tenants\Tenant; use App\Services\GoogleCloudStorageService; use Illuminate\Http\JsonResponse; @@ -279,4 +280,172 @@ public function downloadFile(int $consultationId): BinaryFileResponse 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'), + ], + ]); + } } diff --git a/app/Models/Sales/SalesConsultation.php b/app/Models/Sales/SalesConsultation.php index 0f141e40..7d92c798 100644 --- a/app/Models/Sales/SalesConsultation.php +++ b/app/Models/Sales/SalesConsultation.php @@ -2,6 +2,7 @@ namespace App\Models\Sales; +use App\Models\Sales\TenantProspect; use App\Models\Tenants\Tenant; use App\Models\User; use Illuminate\Database\Eloquent\Model; @@ -13,7 +14,8 @@ * 영업 상담 기록 모델 (텍스트, 음성, 첨부파일) * * @property int $id - * @property int $tenant_id + * @property int|null $tenant_id + * @property int|null $tenant_prospect_id * @property string $scenario_type (sales/manager) * @property int|null $step_id * @property string $consultation_type (text/audio/file) @@ -34,6 +36,7 @@ class SalesConsultation extends Model protected $fillable = [ 'tenant_id', + 'tenant_prospect_id', 'scenario_type', 'step_id', 'consultation_type', @@ -69,6 +72,14 @@ public function tenant(): BelongsTo return $this->belongsTo(Tenant::class); } + /** + * 가망고객 관계 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(TenantProspect::class, 'tenant_prospect_id'); + } + /** * 작성자 관계 */ @@ -258,4 +269,95 @@ public function scopeFileOnly($query) { return $query->where('consultation_type', self::TYPE_FILE); } + + // ======================================== + // Prospect(가망고객) 관련 메서드 + // ======================================== + + /** + * 가망고객 텍스트 상담 기록 생성 + */ + public static function createTextByProspect(int $prospectId, string $scenarioType, ?int $stepId, string $content): self + { + return self::create([ + 'tenant_prospect_id' => $prospectId, + 'scenario_type' => $scenarioType, + 'step_id' => $stepId, + 'consultation_type' => self::TYPE_TEXT, + 'content' => $content, + 'created_by' => auth()->id(), + ]); + } + + /** + * 가망고객 음성 상담 기록 생성 + */ + public static function createAudioByProspect( + int $prospectId, + string $scenarioType, + ?int $stepId, + string $filePath, + string $fileName, + int $fileSize, + ?string $transcript = null, + ?int $duration = null, + ?string $gcsUri = null + ): self { + return self::create([ + 'tenant_prospect_id' => $prospectId, + 'scenario_type' => $scenarioType, + 'step_id' => $stepId, + 'consultation_type' => self::TYPE_AUDIO, + 'file_path' => $filePath, + 'file_name' => $fileName, + 'file_size' => $fileSize, + 'file_type' => 'audio/webm', + 'transcript' => $transcript, + 'duration' => $duration, + 'gcs_uri' => $gcsUri, + 'created_by' => auth()->id(), + ]); + } + + /** + * 가망고객 파일 상담 기록 생성 + */ + public static function createFileByProspect( + int $prospectId, + string $scenarioType, + ?int $stepId, + string $filePath, + string $fileName, + int $fileSize, + string $fileType + ): self { + return self::create([ + 'tenant_prospect_id' => $prospectId, + 'scenario_type' => $scenarioType, + 'step_id' => $stepId, + 'consultation_type' => self::TYPE_FILE, + 'file_path' => $filePath, + 'file_name' => $fileName, + 'file_size' => $fileSize, + 'file_type' => $fileType, + 'created_by' => auth()->id(), + ]); + } + + /** + * 가망고객 + 시나리오 타입으로 조회 + */ + public static function getByProspectAndType(int $prospectId, string $scenarioType, ?int $stepId = null) + { + $query = self::where('tenant_prospect_id', $prospectId) + ->where('scenario_type', $scenarioType) + ->with('creator') + ->orderBy('created_at', 'desc'); + + if ($stepId !== null) { + $query->where('step_id', $stepId); + } + + return $query->get(); + } } diff --git a/resources/views/sales/modals/consultation-log.blade.php b/resources/views/sales/modals/consultation-log.blade.php index 41241d7c..8c2dce30 100644 --- a/resources/views/sales/modals/consultation-log.blade.php +++ b/resources/views/sales/modals/consultation-log.blade.php @@ -1,5 +1,9 @@ {{-- 상담 기록 컴포넌트 --}} -