header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('juil.meeting-minutes.index')); } return view('juil.meeting-minutes'); } public function list(Request $request): JsonResponse { $params = $request->only(['search', 'date_from', 'date_to', 'status', 'per_page']); $meetings = $this->service->getList($params); return response()->json([ 'success' => true, 'data' => $meetings, ]); } public function show(int $id): JsonResponse { $meeting = MeetingMinute::with(['user', 'segments'])->find($id); if (! $meeting) { return response()->json([ 'success' => false, 'message' => '회의록을 찾을 수 없습니다.', ], 404); } return response()->json([ 'success' => true, 'data' => $meeting, ]); } public function store(Request $request): JsonResponse { $validated = $request->validate([ 'title' => 'nullable|string|max:300', 'folder' => 'nullable|string|max:100', 'participants' => 'nullable|array', 'meeting_date' => 'nullable|date', 'meeting_time' => 'nullable', 'stt_language' => 'nullable|string|max:10', ]); $meeting = $this->service->create($validated); return response()->json([ 'success' => true, 'message' => '회의록이 생성되었습니다.', 'data' => $meeting, ], 201); } public function update(Request $request, int $id): JsonResponse { $meeting = MeetingMinute::find($id); if (! $meeting) { return response()->json([ 'success' => false, 'message' => '회의록을 찾을 수 없습니다.', ], 404); } $validated = $request->validate([ 'title' => 'nullable|string|max:300', 'folder' => 'nullable|string|max:100', 'participants' => 'nullable|array', 'meeting_date' => 'nullable|date', 'meeting_time' => 'nullable', ]); $meeting = $this->service->update($meeting, $validated); return response()->json([ 'success' => true, 'message' => '회의록이 수정되었습니다.', 'data' => $meeting, ]); } public function destroy(int $id): JsonResponse { $meeting = MeetingMinute::find($id); if (! $meeting) { return response()->json([ 'success' => false, 'message' => '회의록을 찾을 수 없습니다.', ], 404); } $this->service->delete($meeting); return response()->json([ 'success' => true, 'message' => '회의록이 삭제되었습니다.', ]); } public function saveSegments(Request $request, int $id): JsonResponse { $meeting = MeetingMinute::find($id); if (! $meeting) { return response()->json([ 'success' => false, 'message' => '회의록을 찾을 수 없습니다.', ], 404); } $validated = $request->validate([ 'segments' => 'required|array', 'segments.*.speaker_name' => 'required|string|max:100', 'segments.*.speaker_label' => 'nullable|string|max:20', 'segments.*.text' => 'required|string', 'segments.*.start_time_ms' => 'nullable|integer|min:0', 'segments.*.end_time_ms' => 'nullable|integer|min:0', 'segments.*.is_manual_speaker' => 'nullable|boolean', ]); $meeting = $this->service->saveSegments($meeting, $validated['segments']); return response()->json([ 'success' => true, 'message' => '세그먼트가 저장되었습니다.', 'data' => $meeting, ]); } public function uploadAudio(Request $request, int $id): JsonResponse { $meeting = MeetingMinute::find($id); if (! $meeting) { return response()->json([ 'success' => false, 'message' => '회의록을 찾을 수 없습니다.', ], 404); } $validated = $request->validate([ 'audio' => 'required|file|max:102400', 'duration_seconds' => 'required|integer|min:0', ]); $result = $this->service->uploadAudio($meeting, $request->file('audio'), $validated['duration_seconds']); if (! $result) { return response()->json([ 'success' => false, 'message' => '오디오 업로드에 실패했습니다.', ], 500); } return response()->json([ 'success' => true, 'message' => '오디오가 업로드되었습니다.', 'data' => $meeting->fresh(), ]); } public function summarize(int $id): JsonResponse { $meeting = MeetingMinute::find($id); if (! $meeting) { return response()->json([ 'success' => false, 'message' => '회의록을 찾을 수 없습니다.', ], 404); } if (empty($meeting->full_transcript)) { return response()->json([ 'success' => false, 'message' => '요약할 텍스트가 없습니다. 먼저 녹음을 진행해주세요.', ], 422); } $result = $this->service->generateSummary($meeting); if (! $result) { return response()->json([ 'success' => false, 'message' => 'AI 요약에 실패했습니다.', ], 500); } return response()->json([ 'success' => true, 'message' => 'AI 요약이 완료되었습니다.', 'data' => $meeting->fresh(), ]); } public function downloadAudio(Request $request, int $id): Response|JsonResponse { $meeting = MeetingMinute::find($id); if (! $meeting) { return response()->json([ 'success' => false, 'message' => '회의록을 찾을 수 없습니다.', ], 404); } if (! $meeting->audio_file_path) { return response()->json([ 'success' => false, 'message' => '오디오 파일이 없습니다.', ], 404); } $googleCloudService = app(GoogleCloudService::class); $content = $googleCloudService->downloadFromStorage($meeting->audio_file_path); if (! $content) { return response()->json([ 'success' => false, 'message' => '파일 다운로드에 실패했습니다.', ], 500); } $extension = pathinfo($meeting->audio_file_path, PATHINFO_EXTENSION) ?: 'webm'; $mimeType = 'audio/' . $extension; $safeTitle = preg_replace('/[\/\\\\:*?"<>|]/', '_', $meeting->title); $filename = "{$safeTitle}.{$extension}"; $encodedFilename = rawurlencode($filename); return response($content) ->header('Content-Type', $mimeType) ->header('Content-Length', strlen($content)) ->header('Content-Disposition', "attachment; filename=\"{$encodedFilename}\"; filename*=UTF-8''{$encodedFilename}") ->header('Cache-Control', 'private, max-age=3600'); } public function logSttUsage(Request $request): JsonResponse { $validated = $request->validate([ 'duration_seconds' => 'required|integer|min:1', ]); $this->service->logSttUsage($validated['duration_seconds']); return response()->json(['success' => true]); } }