'integer', 'file_size' => 'integer', 'duration' => 'integer', ]; /** * 상담 유형 상수 */ const TYPE_TEXT = 'text'; const TYPE_AUDIO = 'audio'; const TYPE_FILE = 'file'; /** * 테넌트 관계 */ public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class); } /** * 가망고객 관계 */ public function prospect(): BelongsTo { return $this->belongsTo(TenantProspect::class, 'tenant_prospect_id'); } /** * 작성자 관계 */ public function creator(): BelongsTo { return $this->belongsTo(User::class, 'created_by'); } /** * 텍스트 상담 기록 생성 */ public static function createText(int $tenantId, string $scenarioType, ?int $stepId, string $content): self { return self::create([ 'tenant_id' => $tenantId, 'scenario_type' => $scenarioType, 'step_id' => $stepId, 'consultation_type' => self::TYPE_TEXT, 'content' => $content, 'created_by' => auth()->id(), ]); } /** * 음성 상담 기록 생성 * * @param int $tenantId 테넌트 ID * @param string $scenarioType 시나리오 타입 (sales/manager) * @param int|null $stepId 단계 ID * @param string $filePath 로컬 파일 경로 * @param string $fileName 파일명 * @param int $fileSize 파일 크기 * @param string|null $transcript 음성 텍스트 변환 결과 * @param int|null $duration 녹음 시간 (초) * @param string|null $gcsUri GCS URI (본사 연구용 백업) */ public static function createAudio( int $tenantId, string $scenarioType, ?int $stepId, string $filePath, string $fileName, int $fileSize, ?string $transcript = null, ?int $duration = null, ?string $gcsUri = null ): self { return self::create([ 'tenant_id' => $tenantId, '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 createFile( int $tenantId, string $scenarioType, ?int $stepId, string $filePath, string $fileName, int $fileSize, string $fileType ): self { return self::create([ 'tenant_id' => $tenantId, '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(), ]); } /** * 파일 삭제 (storage 포함) */ public function deleteWithFile(): bool { if ($this->file_path && Storage::disk('local')->exists($this->file_path)) { Storage::disk('local')->delete($this->file_path); } return $this->delete(); } /** * 포맷된 duration Accessor */ public function getFormattedDurationAttribute(): ?string { if (! $this->duration) { return null; } $minutes = floor($this->duration / 60); $seconds = $this->duration % 60; return sprintf('%02d:%02d', $minutes, $seconds); } /** * 포맷된 file size Accessor */ public function getFormattedFileSizeAttribute(): ?string { if (! $this->file_size) { return null; } if ($this->file_size < 1024) { return $this->file_size.' B'; } elseif ($this->file_size < 1024 * 1024) { return round($this->file_size / 1024, 1).' KB'; } else { return round($this->file_size / (1024 * 1024), 1).' MB'; } } /** * 테넌트 + 시나리오 타입으로 조회 */ public static function getByTenantAndType(int $tenantId, string $scenarioType, ?int $stepId = null) { $query = self::where('tenant_id', $tenantId) ->where('scenario_type', $scenarioType) ->with('creator') ->orderBy('created_at', 'desc'); if ($stepId !== null) { $query->where('step_id', $stepId); } return $query->get(); } /** * 시나리오 타입 스코프 */ public function scopeByScenarioType($query, string $type) { return $query->where('scenario_type', $type); } /** * 상담 유형 스코프 */ public function scopeByType($query, string $type) { return $query->where('consultation_type', $type); } /** * 텍스트만 스코프 */ public function scopeTextOnly($query) { return $query->where('consultation_type', self::TYPE_TEXT); } /** * 오디오만 스코프 */ public function scopeAudioOnly($query) { return $query->where('consultation_type', self::TYPE_AUDIO); } /** * 파일만 스코프 */ 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(); } }