diff --git a/app/Http/Controllers/Video/Veo3Controller.php b/app/Http/Controllers/Video/Veo3Controller.php index a7958528..f206241c 100644 --- a/app/Http/Controllers/Video/Veo3Controller.php +++ b/app/Http/Controllers/Video/Veo3Controller.php @@ -41,18 +41,60 @@ public function index(Request $request): View|Response */ public function fetchTrending(): JsonResponse { - $keywords = Cache::remember('health_trending', 1800, function () { - $rawKeywords = $this->trendingService->fetchTrendingKeywords(20); + // 1단계: 원본 트렌딩 키워드 수집 (자체 캐시 있음) + $rawKeywords = $this->trendingService->fetchTrendingKeywords(20); - $healthKeywords = $this->geminiService->filterHealthTrending($rawKeywords); + if (empty($rawKeywords)) { + return response()->json([ + 'success' => false, + 'keywords' => [], + 'reason' => 'rss_fail', + 'message' => 'Google 트렌드 데이터를 가져올 수 없습니다. 잠시 후 다시 시도해주세요.', + ]); + } - // Gemini 필터링 결과 반환 (빈 배열이면 빈 배열 — 원본 노출 안 함) - return $healthKeywords; - }); + // 2단계: Gemini 필터링 (성공 결과만 캐시) + $cached = Cache::get('health_trending'); + if ($cached !== null && ! empty($cached)) { + return response()->json([ + 'success' => true, + 'keywords' => $cached, + ]); + } + + // 직접 필터링 시도 + $healthKeywords = $this->geminiService->filterHealthTrending($rawKeywords); + + // 건강 키워드가 없으면 리프레이밍 폴백 시도 + if (empty($healthKeywords)) { + $healthKeywords = $this->geminiService->reframeAsHealthTrending($rawKeywords); + } + + if (! empty($healthKeywords)) { + Cache::put('health_trending', $healthKeywords, 1800); + + return response()->json([ + 'success' => true, + 'keywords' => $healthKeywords, + ]); + } + + // 최종 폴백: 원본 키워드 상위 5개를 건강 없이 반환 + $fallback = collect($rawKeywords)->take(5)->map(fn ($item) => [ + 'keyword' => $item['keyword'], + 'health_angle' => '', + 'suggested_topic' => $item['keyword'], + 'traffic' => $item['traffic'] ?? '', + 'news_title' => $item['news_title'] ?? '', + 'pub_date' => $item['pub_date'] ?? null, + 'is_raw' => true, + ])->values()->toArray(); return response()->json([ 'success' => true, - 'keywords' => $keywords, + 'keywords' => $fallback, + 'reason' => 'no_health_match', + 'message' => '건강 관련 트렌딩이 없어 인기 키워드를 표시합니다.', ]); } diff --git a/app/Services/Video/GeminiScriptService.php b/app/Services/Video/GeminiScriptService.php index 93a3db94..b7975e67 100644 --- a/app/Services/Video/GeminiScriptService.php +++ b/app/Services/Video/GeminiScriptService.php @@ -109,6 +109,80 @@ public function filterHealthTrending(array $trendingKeywords): array })->values()->toArray(); } + /** + * 폴백: 트렌딩 키워드를 건강 관점에서 리프레이밍 + * (filterHealthTrending에서 0개 반환 시 호출) + */ + public function reframeAsHealthTrending(array $trendingKeywords): array + { + if (empty($trendingKeywords)) { + return []; + } + + $keywordList = collect($trendingKeywords)->take(10)->map(function ($item, $i) { + $news = ! empty($item['news_title']) ? " (뉴스: {$item['news_title']})" : ''; + + return ($i + 1) . ". {$item['keyword']}{$news}"; + })->implode("\n"); + + $prompt = <<callGemini($prompt); + + if (! $result) { + return []; + } + + $parsed = $this->parseJsonResponse($result); + + if (! $parsed || empty($parsed['keywords'])) { + return []; + } + + $trendingMap = collect($trendingKeywords)->keyBy('keyword'); + + return collect($parsed['keywords'])->map(function ($item) use ($trendingMap) { + $original = $trendingMap->get($item['keyword']); + + return [ + 'keyword' => $item['keyword'], + 'health_angle' => $item['health_angle'] ?? '', + 'suggested_topic' => $item['suggested_topic'] ?? $item['keyword'], + 'traffic' => $original['traffic'] ?? '', + 'news_title' => $original['news_title'] ?? '', + 'pub_date' => $original['pub_date'] ?? null, + 'is_reframed' => true, + ]; + })->values()->toArray(); + } + /** * 키워드 → 트렌딩 제목 5개 생성 (기본) */ diff --git a/resources/views/video/veo3/index.blade.php b/resources/views/video/veo3/index.blade.php index 49a9e5cc..01856673 100644 --- a/resources/views/video/veo3/index.blade.php +++ b/resources/views/video/veo3/index.blade.php @@ -123,17 +123,22 @@ if (keyword.trim()) onSubmit(keyword.trim(), trendingContext); }; + const [trendingInfo, setTrendingInfo] = useState(''); + const fetchTrending = async () => { setTrendingLoading(true); setTrendingError(''); + setTrendingInfo(''); try { const data = await api('/video/veo3/trending'); setTrendingKeywords(data.keywords || []); if ((data.keywords || []).length === 0) { - setTrendingError('트렌딩 키워드를 가져오지 못했습니다.'); + setTrendingError(data.message || '트렌딩 키워드를 가져오지 못했습니다.'); + } else if (data.reason === 'no_health_match') { + setTrendingInfo(data.message || '건강 관련 트렌딩이 없어 인기 키워드를 표시합니다.'); } } catch (err) { - setTrendingError(err.message); + setTrendingError(err.message || '네트워크 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'); } finally { setTrendingLoading(false); } @@ -186,10 +191,17 @@ className="inline-flex items-center gap-2 px-4 py-2 border border-green-300 bg-g
{trendingError}
)} + {/* Trending Info (non-error message) */} + {trendingInfo && !trendingError && ( +
{trendingInfo}
+ )} + {/* Trending Keyword Chips */} {trendingKeywords.length > 0 && (
-
💚 건강 트렌드 키워드 (클릭하여 선택)
+
+ {trendingKeywords.some(k => k.is_raw) ? '🔥 실시간 인기 키워드 (클릭하여 선택)' : trendingKeywords.some(k => k.is_reframed) ? '💡 트렌드 × 건강 키워드 (클릭하여 선택)' : '\u{1F49A} 건강 트렌드 키워드 (클릭하여 선택)'} +
{trendingKeywords.map((item, i) => { const isSelected = trendingContext?.keyword === item.keyword;