googleCloud = $googleCloud; $this->config = AiConfig::getActiveGemini(); } /** * 키워드 → 트렌딩 제목 5개 생성 */ public function generateTrendingTitles(string $keyword): array { $prompt = <<callGemini($prompt); if (! $result) { return []; } $parsed = $this->parseJsonResponse($result); return $parsed['titles'] ?? []; } /** * 제목 → 장면별 시나리오 생성 */ public function generateScenario(string $title, string $keyword = ''): array { $prompt = <<callGemini($prompt); if (! $result) { return []; } return $this->parseJsonResponse($result) ?: []; } /** * Gemini API 호출 (AiConfig 기반 - API Key / Vertex AI 자동 분기) */ private function callGemini(string $prompt): ?string { if (! $this->config) { Log::error('GeminiScriptService: 활성화된 Gemini 설정이 없습니다. (시스템 > AI 설정 확인)'); return null; } try { $model = $this->config->model ?: 'gemini-3.0-flash'; $body = [ 'contents' => [ [ 'parts' => [ ['text' => $prompt], ], ], ], 'generationConfig' => [ 'temperature' => 0.9, 'maxOutputTokens' => 4096, 'responseMimeType' => 'application/json', ], ]; if ($this->config->isVertexAi()) { // Vertex AI 방식 (서비스 계정 OAuth) $accessToken = $this->googleCloud->getAccessToken(); if (! $accessToken) { Log::error('GeminiScriptService: Vertex AI 액세스 토큰 획득 실패'); return null; } $projectId = $this->config->getProjectId(); $region = $this->config->getRegion(); $url = "https://{$region}-aiplatform.googleapis.com/v1/projects/{$projectId}/locations/{$region}/publishers/google/models/{$model}:generateContent"; // Vertex AI에서는 role 필수 $body['contents'][0]['role'] = 'user'; $response = Http::withToken($accessToken) ->timeout(60) ->post($url, $body); } else { // API Key 방식 $apiKey = $this->config->api_key; $baseUrl = $this->config->base_url ?? 'https://generativelanguage.googleapis.com/v1beta'; $url = "{$baseUrl}/models/{$model}:generateContent?key={$apiKey}"; $response = Http::timeout(60)->post($url, $body); } if (! $response->successful()) { Log::error('GeminiScriptService: API 호출 실패', [ 'status' => $response->status(), 'body' => $response->body(), 'auth_type' => $this->config->isVertexAi() ? 'vertex_ai' : 'api_key', ]); return null; } $data = $response->json(); $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null; return $text; } catch (\Exception $e) { Log::error('GeminiScriptService: 예외 발생', ['error' => $e->getMessage()]); return null; } } /** * JSON 응답 파싱 (코드블록 제거 포함) */ private function parseJsonResponse(string $text): ?array { // 코드블록 제거 (```json ... ```) $text = preg_replace('/^```(?:json)?\s*/m', '', $text); $text = preg_replace('/```\s*$/m', '', $text); $text = trim($text); $decoded = json_decode($text, true); if (json_last_error() !== JSON_ERROR_NONE) { Log::warning('GeminiScriptService: JSON 파싱 실패', [ 'error' => json_last_error_msg(), 'text' => substr($text, 0, 500), ]); return null; } return $decoded; } }