feat:음성 녹음 GCS 업로드 및 다운로드 기능 추가

- GoogleCloudStorageService 생성 (레거시 방식 JWT 인증)
- 10MB 이상 파일은 Google Cloud Storage에 백업 (본사 연구용)
- 오디오/파일 다운로드 라우트 추가
- voice-recorder.blade.php 인라인 x-data로 변경 (HTMX 호환)
- SalesConsultation 모델에 gcs_uri 필드 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
pro
2026-01-29 09:15:13 +09:00
parent 13418cd3ac
commit dd86d70503
5 changed files with 601 additions and 286 deletions

View File

@@ -5,8 +5,10 @@
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesConsultation;
use App\Models\Tenants\Tenant;
use App\Services\GoogleCloudStorageService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
@@ -88,8 +90,10 @@ public function destroy(int $consultationId, Request $request): JsonResponse
/**
* 음성 파일 업로드
*
* 10MB 이상 파일은 Google Cloud Storage에 업로드하여 본사 연구용으로 보관합니다.
*/
public function uploadAudio(Request $request): JsonResponse
public function uploadAudio(Request $request, GoogleCloudStorageService $gcs): JsonResponse
{
$request->validate([
'tenant_id' => 'required|integer|exists:tenants,id',
@@ -109,18 +113,30 @@ public function uploadAudio(Request $request): JsonResponse
// 파일 저장
$file = $request->file('audio');
$fileName = 'audio_' . now()->format('Ymd_His') . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
$path = $file->storeAs("tenant/consultations/{$tenantId}", $fileName, 'local');
$localPath = $file->storeAs("tenant/consultations/{$tenantId}", $fileName, 'local');
$fileSize = $file->getSize();
// 10MB 이상 파일은 GCS에도 업로드 (본사 연구용)
$gcsUri = null;
$maxLocalSize = 10 * 1024 * 1024; // 10MB
if ($fileSize > $maxLocalSize && $gcs->isAvailable()) {
$gcsObjectName = "consultations/{$tenantId}/{$scenarioType}/{$fileName}";
$localFullPath = Storage::disk('local')->path($localPath);
$gcsUri = $gcs->upload($localFullPath, $gcsObjectName);
}
// DB에 저장
$consultation = SalesConsultation::createAudio(
$tenantId,
$scenarioType,
$stepId,
$path,
$localPath,
$fileName,
$file->getSize(),
$fileSize,
$transcript,
$duration
$duration,
$gcsUri
);
$consultation->load('creator');
@@ -136,6 +152,7 @@ public function uploadAudio(Request $request): JsonResponse
'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),
],
]);
}
@@ -196,4 +213,69 @@ public function deleteFile(int $fileId, Request $request): JsonResponse
{
return $this->destroy($fileId, $request);
}
/**
* 오디오 파일 다운로드
*/
public function downloadAudio(int $consultationId, GoogleCloudStorageService $gcs): Response
{
$consultation = SalesConsultation::findOrFail($consultationId);
if ($consultation->consultation_type !== 'audio') {
abort(400, '오디오 파일이 아닙니다.');
}
// GCS에 저장된 경우 서명된 URL로 리다이렉트
if ($consultation->gcs_uri) {
$objectName = str_replace('gs://' . $gcs->getBucketName() . '/', '', $consultation->gcs_uri);
$signedUrl = $gcs->getSignedUrl($objectName, 60);
if ($signedUrl) {
return response()->redirectTo($signedUrl);
}
}
// 로컬 파일 다운로드
$localPath = Storage::disk('local')->path($consultation->file_path);
if (!file_exists($localPath)) {
abort(404, '파일을 찾을 수 없습니다.');
}
$extension = pathinfo($consultation->file_name, PATHINFO_EXTENSION) ?: 'webm';
$mimeTypes = [
'webm' => 'audio/webm',
'wav' => 'audio/wav',
'mp3' => 'audio/mpeg',
'ogg' => 'audio/ogg',
'm4a' => 'audio/mp4'
];
$contentType = $mimeTypes[$extension] ?? 'audio/webm';
$downloadFileName = '상담녹음_' . $consultation->created_at->format('Ymd_His') . '.' . $extension;
return response()->download($localPath, $downloadFileName, [
'Content-Type' => $contentType,
]);
}
/**
* 첨부파일 다운로드
*/
public function downloadFile(int $consultationId): Response
{
$consultation = SalesConsultation::findOrFail($consultationId);
if ($consultation->consultation_type !== 'file') {
abort(400, '첨부파일이 아닙니다.');
}
$localPath = Storage::disk('local')->path($consultation->file_path);
if (!file_exists($localPath)) {
abort(404, '파일을 찾을 수 없습니다.');
}
return response()->download($localPath, $consultation->file_name);
}
}