feat:영상 파일 GCS 저장 및 삭제 연동
- VideoGenerationJob: 최종 합성 후 GCS 업로드, gcs_path 저장 - Veo3Controller: download/preview GCS 서명URL 사용, destroy GCS 파일 삭제 - VideoGeneration 모델: gcs_path fillable 추가 - GCS 불가 시 로컬 파일로 폴백 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,10 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\VideoGenerationJob;
|
||||
use App\Models\VideoGeneration;
|
||||
use App\Services\GoogleCloudStorageService;
|
||||
use App\Services\Video\GeminiScriptService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
@@ -15,7 +17,8 @@
|
||||
class Veo3Controller extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GeminiScriptService $geminiService
|
||||
private readonly GeminiScriptService $geminiService,
|
||||
private readonly GoogleCloudStorageService $gcsService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -164,7 +167,7 @@ public function status(int $id): JsonResponse
|
||||
/**
|
||||
* 완성 영상 다운로드
|
||||
*/
|
||||
public function download(int $id): BinaryFileResponse|JsonResponse
|
||||
public function download(int $id): BinaryFileResponse|RedirectResponse|JsonResponse
|
||||
{
|
||||
$video = VideoGeneration::findOrFail($id);
|
||||
|
||||
@@ -172,6 +175,15 @@ public function download(int $id): BinaryFileResponse|JsonResponse
|
||||
return response()->json(['message' => '아직 영상이 완성되지 않았습니다.'], 404);
|
||||
}
|
||||
|
||||
// GCS 서명URL로 리다이렉트
|
||||
if ($video->gcs_path && $this->gcsService->isAvailable()) {
|
||||
$signedUrl = $this->gcsService->getSignedUrl($video->gcs_path, 30);
|
||||
if ($signedUrl) {
|
||||
return redirect()->away($signedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// 로컬 파일 폴백
|
||||
if (! file_exists($video->output_path)) {
|
||||
return response()->json(['message' => '영상 파일을 찾을 수 없습니다.'], 404);
|
||||
}
|
||||
@@ -187,10 +199,19 @@ public function download(int $id): BinaryFileResponse|JsonResponse
|
||||
/**
|
||||
* 영상 미리보기 (스트리밍)
|
||||
*/
|
||||
public function preview(int $id): Response|JsonResponse
|
||||
public function preview(int $id): Response|RedirectResponse|JsonResponse
|
||||
{
|
||||
$video = VideoGeneration::findOrFail($id);
|
||||
|
||||
// GCS 서명URL로 리다이렉트
|
||||
if ($video->gcs_path && $this->gcsService->isAvailable()) {
|
||||
$signedUrl = $this->gcsService->getSignedUrl($video->gcs_path, 60);
|
||||
if ($signedUrl) {
|
||||
return redirect()->away($signedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// 로컬 파일 폴백
|
||||
if (! $video->output_path || ! file_exists($video->output_path)) {
|
||||
return response()->json(['message' => '영상 파일을 찾을 수 없습니다.'], 404);
|
||||
}
|
||||
@@ -237,6 +258,11 @@ public function destroy(Request $request): JsonResponse
|
||||
|
||||
$deleted = 0;
|
||||
foreach ($videos as $video) {
|
||||
// GCS 파일 삭제
|
||||
if ($video->gcs_path && $this->gcsService->isAvailable()) {
|
||||
$this->gcsService->delete($video->gcs_path);
|
||||
}
|
||||
|
||||
// 로컬 작업 디렉토리 삭제 (클립, 나레이션, 최종 영상 등)
|
||||
$workDir = storage_path("app/video_gen/{$video->id}");
|
||||
if (is_dir($workDir)) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use App\Services\Video\TtsService;
|
||||
use App\Services\Video\VeoVideoService;
|
||||
use App\Services\Video\VideoAssemblyService;
|
||||
use App\Services\GoogleCloudStorageService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
@@ -41,7 +42,8 @@ public function handle(
|
||||
VeoVideoService $veo,
|
||||
TtsService $tts,
|
||||
BgmService $bgm,
|
||||
VideoAssemblyService $assembly
|
||||
VideoAssemblyService $assembly,
|
||||
GoogleCloudStorageService $gcs
|
||||
): void {
|
||||
$video = VideoGeneration::withoutGlobalScopes()->find($this->videoGenerationId);
|
||||
|
||||
@@ -264,6 +266,28 @@ public function handle(
|
||||
return;
|
||||
}
|
||||
|
||||
// === GCS 업로드 ===
|
||||
$gcsPath = null;
|
||||
|
||||
if ($gcs->isAvailable()) {
|
||||
$video->updateProgress(VideoGeneration::STATUS_ASSEMBLING, 95, 'GCS 업로드 중...');
|
||||
|
||||
$objectName = "video_gen/{$video->id}/final_{$video->id}.mp4";
|
||||
$gcsUri = $gcs->upload($finalPath, $objectName);
|
||||
|
||||
if ($gcsUri) {
|
||||
$gcsPath = $objectName;
|
||||
Log::info('VideoGenerationJob: GCS 업로드 성공', [
|
||||
'id' => $video->id,
|
||||
'gcs_path' => $objectName,
|
||||
]);
|
||||
} else {
|
||||
Log::warning('VideoGenerationJob: GCS 업로드 실패, 로컬 파일로 계속', [
|
||||
'id' => $video->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// === 완료 ===
|
||||
$stepMsg = empty($skippedScenes)
|
||||
? '완료'
|
||||
@@ -274,6 +298,7 @@ public function handle(
|
||||
'progress' => 100,
|
||||
'current_step' => $stepMsg,
|
||||
'output_path' => $finalPath,
|
||||
'gcs_path' => $gcsPath,
|
||||
'cost_usd' => $totalCost,
|
||||
]);
|
||||
|
||||
@@ -283,6 +308,7 @@ public function handle(
|
||||
Log::info('VideoGenerationJob: 영상 생성 완료', [
|
||||
'id' => $video->id,
|
||||
'output' => $finalPath,
|
||||
'gcs_path' => $gcsPath,
|
||||
'cost' => $totalCost,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -18,6 +18,7 @@ class VideoGeneration extends Model
|
||||
'current_step',
|
||||
'clips_data',
|
||||
'output_path',
|
||||
'gcs_path',
|
||||
'cost_usd',
|
||||
'error_message',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user