From fa14a9fbec23225a1e140cdc78155bb9331d670d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 7 Feb 2026 13:51:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:AI=20=EC=9D=8C=EC=84=B1=EB=85=B9=EC=9D=8C?= =?UTF-8?q?=20GCS=20=ED=8C=8C=EC=9D=BC=20=EB=8B=A4=EC=9A=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GoogleCloudService에 downloadFromStorage 메서드 추가 (GCS REST API 사용) - AiVoiceRecordingController에 download 메서드 추가 (스트림 응답) - 다운로드 라우트 추가 (GET /{id}/download) - 파일명은 제목 기반으로 생성, Content-Disposition 헤더 설정 Co-Authored-By: Claude Opus 4.6 --- .../System/AiVoiceRecordingController.php | 37 +++++++++++++++ app/Services/GoogleCloudService.php | 47 +++++++++++++++++++ routes/web.php | 1 + 3 files changed, 85 insertions(+) diff --git a/app/Http/Controllers/System/AiVoiceRecordingController.php b/app/Http/Controllers/System/AiVoiceRecordingController.php index 991ffb07..62bf0b18 100644 --- a/app/Http/Controllers/System/AiVoiceRecordingController.php +++ b/app/Http/Controllers/System/AiVoiceRecordingController.php @@ -6,8 +6,10 @@ use App\Models\AiVoiceRecording; use App\Models\Interview\InterviewCategory; use App\Services\AiVoiceRecordingService; +use App\Services\GoogleCloudService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Illuminate\View\View; use Symfony\Component\HttpFoundation\Response; @@ -223,4 +225,39 @@ public function status(int $id): JsonResponse ], ]); } + + /** + * 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.'"'); + } } diff --git a/app/Services/GoogleCloudService.php b/app/Services/GoogleCloudService.php index d570976f..ed8fd35e 100644 --- a/app/Services/GoogleCloudService.php +++ b/app/Services/GoogleCloudService.php @@ -317,6 +317,53 @@ public function deleteFromStorage(string $objectName): bool } } + /** + * GCS에서 파일 다운로드 (스트림) + */ + public function downloadFromStorage(string $objectName): ?string + { + $token = $this->getAccessToken(); + if (! $token) { + Log::error('Google Cloud: 다운로드 토큰 획득 실패'); + + return null; + } + + $bucket = config('services.google.storage_bucket'); + if (! $bucket) { + Log::error('Google Cloud: Storage 버킷 설정 없음'); + + return null; + } + + try { + $url = 'https://storage.googleapis.com/storage/v1/b/'. + urlencode($bucket).'/o/'.urlencode($objectName).'?alt=media'; + + $response = Http::withToken($token)->get($url); + + if ($response->successful()) { + Log::info('Google Cloud: Storage 다운로드 성공', [ + 'object' => $objectName, + 'size' => strlen($response->body()), + ]); + + return $response->body(); + } + + Log::error('Google Cloud: Storage 다운로드 실패', [ + 'status' => $response->status(), + 'response' => $response->body(), + ]); + + return null; + } catch (\Exception $e) { + Log::error('Google Cloud: Storage 다운로드 예외', ['error' => $e->getMessage()]); + + return null; + } + } + /** * 서비스 사용 가능 여부 */ diff --git a/routes/web.php b/routes/web.php index 9c8fd95b..9de04a5f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -423,6 +423,7 @@ Route::post('/{id}/process', [AiVoiceRecordingController::class, 'processAudio'])->name('process'); Route::delete('/{id}', [AiVoiceRecordingController::class, 'destroy'])->name('destroy'); Route::get('/{id}/status', [AiVoiceRecordingController::class, 'status'])->name('status'); + Route::get('/{id}/download', [AiVoiceRecordingController::class, 'download'])->name('download'); }); // 명함 OCR API