diff --git a/app/Http/Controllers/Video/TutorialVideoController.php b/app/Http/Controllers/Video/TutorialVideoController.php index a07beef5..d646b80e 100644 --- a/app/Http/Controllers/Video/TutorialVideoController.php +++ b/app/Http/Controllers/Video/TutorialVideoController.php @@ -185,19 +185,24 @@ public function download(int $id): BinaryFileResponse|RedirectResponse|JsonRespo } /** - * 영상 미리보기 (스트리밍) + * 영상 미리보기 (스트리밍 또는 서명URL) + * + * ?url=1 파라미터: JSON으로 서명URL 반환 (CORS 회피용) + * 그 외: 로컬 파일 직접 스트리밍 */ - public function preview(int $id): Response|RedirectResponse|JsonResponse + public function preview(Request $request, int $id): Response|RedirectResponse|JsonResponse { $tutorial = TutorialVideo::findOrFail($id); - if ($tutorial->gcs_path && $this->gcsService->isAvailable()) { + // ?url=1 → JSON으로 GCS 서명URL 반환 (video src에서 직접 사용) + if ($request->query('url') && $tutorial->gcs_path && $this->gcsService->isAvailable()) { $signedUrl = $this->gcsService->getSignedUrl($tutorial->gcs_path, 60); if ($signedUrl) { - return redirect()->away($signedUrl); + return response()->json(['url' => $signedUrl]); } } + // 로컬 파일 직접 스트리밍 if (! $tutorial->output_path || ! file_exists($tutorial->output_path)) { return response()->json(['message' => '영상 파일을 찾을 수 없습니다.'], 404); } diff --git a/resources/views/video/tutorial/index.blade.php b/resources/views/video/tutorial/index.blade.php index 53f9758e..e4b97329 100644 --- a/resources/views/video/tutorial/index.blade.php +++ b/resources/views/video/tutorial/index.blade.php @@ -314,6 +314,7 @@ className="px-6 py-2.5 bg-indigo-600 text-white rounded-lg font-medium hover:bg- const GenerationProgress = ({ tutorialId, onComplete, onReset }) => { const [status, setStatus] = useState(null); const [polling, setPolling] = useState(true); + const [videoSrc, setVideoSrc] = useState(null); const intervalRef = useRef(null); useEffect(() => { @@ -325,7 +326,16 @@ className="px-6 py-2.5 bg-indigo-600 text-white rounded-lg font-medium hover:bg- setStatus(data); if (data.status === 'completed' || data.status === 'failed') { setPolling(false); - if (data.status === 'completed') onComplete?.(); + if (data.status === 'completed') { + // GCS 서명URL 가져오기 (CORS 회피), 실패 시 로컬 스트리밍 fallback + try { + const urlData = await api(`/video/tutorial/preview/${tutorialId}?url=1`); + setVideoSrc(urlData.url); + } catch { + setVideoSrc(`/video/tutorial/preview/${tutorialId}`); + } + onComplete?.(); + } } } catch (err) { console.error('Polling error:', err); @@ -364,7 +374,7 @@ className={`progress-bar ${isCompleted ? 'bg-green-500' : isFailed ? 'bg-red-500
{progress}%
- {isCompleted && ( + {isCompleted && videoSrc && (
영상 생성 완료!
@@ -372,7 +382,7 @@ className={`progress-bar ${isCompleted ? 'bg-green-500' : isFailed ? 'bg-red-500