diff --git a/app/Http/Controllers/Video/Veo3Controller.php b/app/Http/Controllers/Video/Veo3Controller.php index 93ef3801..a458dbab 100644 --- a/app/Http/Controllers/Video/Veo3Controller.php +++ b/app/Http/Controllers/Video/Veo3Controller.php @@ -7,6 +7,7 @@ use App\Models\VideoGeneration; use App\Services\GoogleCloudStorageService; use App\Services\Video\GeminiScriptService; +use App\Services\Video\TrendingKeywordService; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -18,7 +19,8 @@ class Veo3Controller extends Controller { public function __construct( private readonly GeminiScriptService $geminiService, - private readonly GoogleCloudStorageService $gcsService + private readonly GoogleCloudStorageService $gcsService, + private readonly TrendingKeywordService $trendingService ) {} /** @@ -34,16 +36,36 @@ public function index(Request $request): View|Response } /** - * 키워드 → 제목 후보 생성 + * 실시간 급상승 키워드 목록 + */ + public function fetchTrending(): JsonResponse + { + $keywords = $this->trendingService->fetchTrendingKeywords(10); + + return response()->json([ + 'success' => true, + 'keywords' => $keywords, + ]); + } + + /** + * 키워드 → 제목 후보 생성 (trending_context 옵션 지원) */ public function generateTitles(Request $request): JsonResponse { $request->validate([ 'keyword' => 'required|string|max:100', + 'trending_context' => 'nullable|array', ]); $keyword = $request->input('keyword'); - $titles = $this->geminiService->generateTrendingTitles($keyword); + $trendingContext = $request->input('trending_context'); + + if ($trendingContext) { + $titles = $this->geminiService->generateTrendingHookTitles($keyword, $trendingContext); + } else { + $titles = $this->geminiService->generateTrendingTitles($keyword); + } if (empty($titles)) { return response()->json([ diff --git a/app/Services/Video/GeminiScriptService.php b/app/Services/Video/GeminiScriptService.php index 7af3a6c5..4b7fd348 100644 --- a/app/Services/Video/GeminiScriptService.php +++ b/app/Services/Video/GeminiScriptService.php @@ -20,7 +20,7 @@ public function __construct(GoogleCloudService $googleCloud) } /** - * 키워드 → 트렌딩 제목 5개 생성 + * 키워드 → 트렌딩 제목 5개 생성 (기본) */ public function generateTrendingTitles(string $keyword): array { @@ -60,33 +60,121 @@ public function generateTrendingTitles(string $keyword): array } /** - * 제목 → 장면별 시나리오 생성 + * 트렌딩 키워드 + 컨텍스트 → 후킹 제목 5개 생성 + */ + public function generateTrendingHookTitles(string $keyword, array $context = []): array + { + $contextBlock = ''; + if (! empty($context)) { + $newsTitle = $context['news_title'] ?? ''; + $traffic = $context['traffic'] ?? ''; + $contextBlock = <<callGemini($prompt); + + if (! $result) { + return []; + } + + $parsed = $this->parseJsonResponse($result); + + return $parsed['titles'] ?? []; + } + + /** + * 제목 → 장면별 시나리오 생성 (Veo 3.1 공식 가이드 기반 프롬프트) */ public function generateScenario(string $title, string $keyword = ''): array { $prompt = <<withHeaders(['Accept' => 'application/xml']) + ->get(self::RSS_URL); + + if (! $response->successful()) { + Log::warning('TrendingKeywordService: RSS 호출 실패', [ + 'status' => $response->status(), + ]); + + return []; + } + + $keywords = $this->parseGoogleTrendsRss($response->body()); + + return array_slice($keywords, 0, $limit); + } catch (\Exception $e) { + Log::error('TrendingKeywordService: 예외 발생', [ + 'error' => $e->getMessage(), + ]); + + return []; + } + }); + } + + /** + * RSS XML 파싱 + */ + private function parseGoogleTrendsRss(string $xml): array + { + $keywords = []; + + try { + // XML namespace 처리를 위해 ht: 접두사 등록 + $doc = new \SimpleXMLElement($xml); + $namespaces = $doc->getNamespaces(true); + + $items = $doc->channel->item ?? []; + + foreach ($items as $item) { + $keyword = (string) $item->title; + + if (empty($keyword)) { + continue; + } + + $traffic = ''; + $newsTitle = ''; + + if (isset($namespaces['ht'])) { + $htData = $item->children($namespaces['ht']); + $traffic = (string) ($htData->approx_traffic ?? ''); + + if (isset($htData->news_item)) { + $newsTitle = (string) ($htData->news_item->news_item_title ?? ''); + } + } + + $pubDate = (string) ($item->pubDate ?? ''); + + $keywords[] = [ + 'keyword' => $keyword, + 'traffic' => $traffic, + 'news_title' => $newsTitle, + 'pub_date' => $pubDate ? date('c', strtotime($pubDate)) : null, + ]; + } + } catch (\Exception $e) { + Log::error('TrendingKeywordService: XML 파싱 실패', [ + 'error' => $e->getMessage(), + ]); + } + + return $keywords; + } +} diff --git a/routes/web.php b/routes/web.php index d1fb5c63..ae9639c2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1464,6 +1464,7 @@ */ Route::prefix('video/veo3')->name('video.veo3.')->middleware('auth')->group(function () { Route::get('/', [\App\Http\Controllers\Video\Veo3Controller::class, 'index'])->name('index'); + Route::get('/trending', [\App\Http\Controllers\Video\Veo3Controller::class, 'fetchTrending'])->name('trending'); Route::post('/titles', [\App\Http\Controllers\Video\Veo3Controller::class, 'generateTitles'])->name('titles'); Route::post('/scenario', [\App\Http\Controllers\Video\Veo3Controller::class, 'generateScenario'])->name('scenario'); Route::post('/generate', [\App\Http\Controllers\Video\Veo3Controller::class, 'generate'])->name('generate');