header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('system.ai-voice-recording.index')); } return view('system.ai-voice-recording.index'); } /** * JSON 목록 */ public function list(Request $request): JsonResponse { $params = $request->only(['search', 'status', 'per_page']); $recordings = $this->service->getList($params); return response()->json([ 'success' => true, 'data' => $recordings, ]); } /** * 카테고리 + 템플릿 목록 */ public function categories(): JsonResponse { $categories = InterviewCategory::with(['templates' => function ($q) { $q->where('is_active', true)->orderBy('sort_order'); }]) ->where('is_active', true) ->orderBy('sort_order') ->get(['id', 'name', 'description']); return response()->json([ 'success' => true, 'data' => $categories, ]); } /** * 새 녹음 생성 */ public function store(Request $request): JsonResponse { $validated = $request->validate([ 'title' => 'nullable|string|max:200', 'interview_template_id' => 'nullable|integer|exists:interview_templates,id', ]); $recording = $this->service->create($validated); return response()->json([ 'success' => true, 'message' => '녹음이 생성되었습니다.', 'data' => $recording, ], 201); } /** * Base64 오디오 업로드 + 처리 */ public function processAudio(Request $request, int $id): JsonResponse { $recording = AiVoiceRecording::find($id); if (! $recording) { return response()->json([ 'success' => false, 'message' => '녹음을 찾을 수 없습니다.', ], 404); } $validated = $request->validate([ 'audio' => 'required|string', 'duration' => 'required|integer|min:1', ]); $result = $this->service->processAudio( $recording, $validated['audio'], $validated['duration'] ); if (! $result['ok']) { return response()->json([ 'success' => false, 'message' => $result['error'] ?? '처리 중 오류가 발생했습니다.', ], 500); } return response()->json([ 'success' => true, 'message' => '음성 분석이 완료되었습니다.', 'data' => $result['recording'], ]); } /** * 파일 업로드 + 처리 */ public function uploadFile(Request $request): JsonResponse { $validated = $request->validate([ 'audio_file' => 'required|file|mimes:webm,wav,mp3,ogg,m4a,mp4|max:102400', 'title' => 'nullable|string|max:200', 'interview_template_id' => 'nullable|integer|exists:interview_templates,id', ]); $recording = $this->service->create([ 'title' => $validated['title'] ?? '업로드된 음성녹음', 'interview_template_id' => $validated['interview_template_id'] ?? null, ]); $result = $this->service->processUploadedFile( $recording, $request->file('audio_file') ); if (! $result['ok']) { return response()->json([ 'success' => false, 'message' => $result['error'] ?? '처리 중 오류가 발생했습니다.', ], 500); } return response()->json([ 'success' => true, 'message' => '음성 분석이 완료되었습니다.', 'data' => $result['recording'], ]); } /** * 상세 조회 */ public function show(int $id): JsonResponse { $recording = $this->service->getById($id); if (! $recording) { return response()->json([ 'success' => false, 'message' => '녹음을 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'data' => $recording, ]); } /** * 삭제 */ public function destroy(int $id): JsonResponse { $recording = AiVoiceRecording::find($id); if (! $recording) { return response()->json([ 'success' => false, 'message' => '녹음을 찾을 수 없습니다.', ], 404); } $this->service->delete($recording); return response()->json([ 'success' => true, 'message' => '녹음이 삭제되었습니다.', ]); } /** * 처리 상태 폴링용 */ public function status(int $id): JsonResponse { $recording = AiVoiceRecording::find($id); if (! $recording) { return response()->json([ 'success' => false, 'message' => '녹음을 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'data' => [ 'id' => $recording->id, 'status' => $recording->status, 'status_label' => $recording->status_label, 'is_completed' => $recording->isCompleted(), 'is_processing' => $recording->isProcessing(), 'transcript_text' => $recording->transcript_text, 'analysis_text' => $recording->analysis_text, ], ]); } /** * GCS 음성파일 다운로드 */ public function download(int $id): Response|JsonResponse { $recording = AiVoiceRecording::find($id); if (! $recording || ! $recording->audio_file_path) { return response()->json([ 'success' => false, 'message' => '파일을 찾을 수 없습니다.', ], 404); } $googleCloudService = app(GoogleCloudService::class); $content = $googleCloudService->downloadFromStorage($recording->audio_file_path); if (! $content) { return response()->json([ 'success' => false, 'message' => '파일 다운로드에 실패했습니다.', ], 500); } // 파일 확장자 추출 $extension = pathinfo($recording->audio_file_path, PATHINFO_EXTENSION) ?: 'webm'; // 파일명 생성 (제목 기반, URL 안전하게) $filename = Str::slug($recording->title ?: 'recording').'.'. $extension; return response($content) ->header('Content-Type', 'audio/'.$extension) ->header('Content-Disposition', 'attachment; filename="'.$filename.'"'); } }