diff --git a/app/Http/Controllers/System/AiConfigController.php b/app/Http/Controllers/System/AiConfigController.php index 3c74fdbb..4f7c052e 100644 --- a/app/Http/Controllers/System/AiConfigController.php +++ b/app/Http/Controllers/System/AiConfigController.php @@ -36,13 +36,27 @@ public function store(Request $request): JsonResponse $validated = $request->validate([ 'name' => 'required|string|max:50', 'provider' => 'required|string|in:gemini,claude,openai', - 'api_key' => 'required|string|max:255', + 'api_key' => 'nullable|string|max:255', 'model' => 'required|string|max:100', - 'base_url' => 'nullable|string|max:255|url', + 'base_url' => 'nullable|string|max:255', 'description' => 'nullable|string', 'is_active' => 'boolean', + 'options' => 'nullable|array', + 'options.auth_type' => 'nullable|string|in:api_key,vertex_ai', + 'options.project_id' => 'nullable|string|max:100', + 'options.region' => 'nullable|string|max:50', + 'options.service_account_path' => 'nullable|string|max:500', ]); + // Vertex AI가 아닌 경우 API 키 필수 + $authType = $validated['options']['auth_type'] ?? 'api_key'; + if ($authType !== 'vertex_ai' && empty($validated['api_key'])) { + return response()->json([ + 'ok' => false, + 'message' => 'API 키를 입력해주세요.', + ], 422); + } + // 활성화 시 동일 provider의 다른 설정 비활성화 if ($validated['is_active'] ?? false) { AiConfig::where('provider', $validated['provider']) @@ -68,13 +82,27 @@ public function update(Request $request, int $id): JsonResponse $validated = $request->validate([ 'name' => 'required|string|max:50', 'provider' => 'required|string|in:gemini,claude,openai', - 'api_key' => 'required|string|max:255', + 'api_key' => 'nullable|string|max:255', 'model' => 'required|string|max:100', 'base_url' => 'nullable|string|max:255', 'description' => 'nullable|string', 'is_active' => 'boolean', + 'options' => 'nullable|array', + 'options.auth_type' => 'nullable|string|in:api_key,vertex_ai', + 'options.project_id' => 'nullable|string|max:100', + 'options.region' => 'nullable|string|max:50', + 'options.service_account_path' => 'nullable|string|max:500', ]); + // Vertex AI가 아닌 경우 API 키 필수 + $authType = $validated['options']['auth_type'] ?? 'api_key'; + if ($authType !== 'vertex_ai' && empty($validated['api_key'])) { + return response()->json([ + 'ok' => false, + 'message' => 'API 키를 입력해주세요.', + ], 422); + } + // 활성화 시 동일 provider의 다른 설정 비활성화 if ($validated['is_active'] ?? false) { AiConfig::where('provider', $validated['provider']) diff --git a/app/Models/System/AiConfig.php b/app/Models/System/AiConfig.php index 7fe5b9fd..98d11863 100644 --- a/app/Models/System/AiConfig.php +++ b/app/Models/System/AiConfig.php @@ -140,4 +140,47 @@ public function getMaskedApiKeyAttribute(): string return substr($this->api_key, 0, 8) . str_repeat('*', 8) . '...'; } + + /** + * Vertex AI 사용 여부 + */ + public function isVertexAi(): bool + { + return ($this->options['auth_type'] ?? 'api_key') === 'vertex_ai'; + } + + /** + * Vertex AI 프로젝트 ID + */ + public function getProjectId(): ?string + { + return $this->options['project_id'] ?? null; + } + + /** + * Vertex AI 리전 + */ + public function getRegion(): string + { + return $this->options['region'] ?? 'us-central1'; + } + + /** + * 서비스 계정 파일 경로 + */ + public function getServiceAccountPath(): ?string + { + return $this->options['service_account_path'] ?? null; + } + + /** + * 인증 방식 라벨 + */ + public function getAuthTypeLabelAttribute(): string + { + if ($this->isVertexAi()) { + return 'Vertex AI (서비스 계정)'; + } + return 'API 키'; + } } diff --git a/app/Services/BusinessCardOcrService.php b/app/Services/BusinessCardOcrService.php index 50fe53b9..d3ff331b 100644 --- a/app/Services/BusinessCardOcrService.php +++ b/app/Services/BusinessCardOcrService.php @@ -9,17 +9,6 @@ class BusinessCardOcrService { - /** - * Vertex AI 서비스 계정 파일 경로 - */ - private const SERVICE_ACCOUNT_PATH = '/var/www/html/storage/app/google_service_account.json'; - - /** - * Vertex AI 설정 - */ - private const VERTEX_AI_PROJECT_ID = 'codebridge-chatbot'; - private const VERTEX_AI_REGION = 'us-central1'; - /** * 명함 이미지에서 정보 추출 */ @@ -31,20 +20,29 @@ public function extractFromImage(string $base64Image): array throw new \RuntimeException('Gemini API 설정이 없습니다. 시스템 설정에서 AI 설정을 추가해주세요.'); } - return $this->callVertexAiApi($config, $base64Image); + // 인증 방식에 따라 다른 API 호출 + if ($config->isVertexAi()) { + return $this->callVertexAiApi($config, $base64Image); + } + + return $this->callGoogleAiStudioApi($config, $base64Image); } /** - * Vertex AI API 호출 (Google Cloud) + * Vertex AI API 호출 (Google Cloud - 서비스 계정 인증) */ private function callVertexAiApi(AiConfig $config, string $base64Image): array { $model = $config->model; - $projectId = self::VERTEX_AI_PROJECT_ID; - $region = self::VERTEX_AI_REGION; + $projectId = $config->getProjectId(); + $region = $config->getRegion(); + + if (!$projectId) { + throw new \RuntimeException('Vertex AI 프로젝트 ID가 설정되지 않았습니다.'); + } // 액세스 토큰 가져오기 - $accessToken = $this->getAccessToken(); + $accessToken = $this->getAccessToken($config); if (!$accessToken) { throw new \RuntimeException('Google Cloud 인증 실패'); } @@ -52,6 +50,33 @@ private function callVertexAiApi(AiConfig $config, string $base64Image): array // Vertex AI 엔드포인트 $url = "https://{$region}-aiplatform.googleapis.com/v1/projects/{$projectId}/locations/{$region}/publishers/google/models/{$model}:generateContent"; + return $this->callGeminiApi($url, $base64Image, [ + 'Authorization' => 'Bearer ' . $accessToken, + 'Content-Type' => 'application/json', + ]); + } + + /** + * Google AI Studio API 호출 (API 키 인증) + */ + private function callGoogleAiStudioApi(AiConfig $config, string $base64Image): array + { + $model = $config->model; + $apiKey = $config->api_key; + $baseUrl = $config->base_url ?? 'https://generativelanguage.googleapis.com/v1beta'; + + $url = "{$baseUrl}/models/{$model}:generateContent?key={$apiKey}"; + + return $this->callGeminiApi($url, $base64Image, [ + 'Content-Type' => 'application/json', + ]); + } + + /** + * Gemini API 공통 호출 로직 + */ + private function callGeminiApi(string $url, string $base64Image, array $headers): array + { // Base64 데이터에서 prefix 제거 $imageData = $base64Image; $mimeType = 'image/jpeg'; @@ -65,10 +90,7 @@ private function callVertexAiApi(AiConfig $config, string $base64Image): array try { $response = Http::timeout(30) - ->withHeaders([ - 'Authorization' => 'Bearer ' . $accessToken, - 'Content-Type' => 'application/json', - ]) + ->withHeaders($headers) ->post($url, [ 'contents' => [ [ @@ -95,7 +117,7 @@ private function callVertexAiApi(AiConfig $config, string $base64Image): array ]); if (! $response->successful()) { - Log::error('Vertex AI API error', [ + Log::error('Gemini API error', [ 'status' => $response->status(), 'body' => $response->body(), ]); @@ -118,7 +140,7 @@ private function callVertexAiApi(AiConfig $config, string $base64Image): array 'raw_response' => $text, ]; } catch (ConnectionException $e) { - Log::error('Vertex AI API connection failed', ['error' => $e->getMessage()]); + Log::error('Gemini API connection failed', ['error' => $e->getMessage()]); throw new \RuntimeException('AI API 연결 실패'); } } @@ -126,20 +148,21 @@ private function callVertexAiApi(AiConfig $config, string $base64Image): array /** * 서비스 계정으로 OAuth2 액세스 토큰 가져오기 */ - private function getAccessToken(): ?string + private function getAccessToken(AiConfig $config): ?string { - // 여러 경로에서 서비스 계정 파일 찾기 (Docker 컨테이너 및 호스트) - $possiblePaths = [ - '/var/www/sales/apikey/google_service_account.json', // Docker 컨테이너 내 sales 볼륨 - self::SERVICE_ACCOUNT_PATH, + // DB에서 서비스 계정 경로 가져오기 + $configuredPath = $config->getServiceAccountPath(); + + // 여러 경로에서 서비스 계정 파일 찾기 + $possiblePaths = array_filter([ + $configuredPath, // DB에 설정된 경로 우선 + '/var/www/sales/apikey/google_service_account.json', storage_path('app/google_service_account.json'), - base_path('../sales/apikey/google_service_account.json'), - '/home/aweso/sam/sales/apikey/google_service_account.json', - ]; + ]); $serviceAccountPath = null; foreach ($possiblePaths as $path) { - if (file_exists($path)) { + if ($path && file_exists($path)) { $serviceAccountPath = $path; break; } diff --git a/resources/views/system/ai-config/index.blade.php b/resources/views/system/ai-config/index.blade.php index 6f0b2378..ca37c24c 100644 --- a/resources/views/system/ai-config/index.blade.php +++ b/resources/views/system/ai-config/index.blade.php @@ -94,7 +94,12 @@
모델: {{ $config->model }}
+인증: {{ $config->auth_type_label }}
+ @if($config->isVertexAi()) +프로젝트: {{ $config->getProjectId() }} ({{ $config->getRegion() }})
+ @elseAPI 키: {{ $config->masked_api_key }}
+ @endif @if($config->description)설명: {{ $config->description }}
@endif @@ -168,9 +173,42 @@유료 플랜은 Vertex AI를 선택하세요
+Docker 컨테이너 내부 경로로 입력
+기본값: gemini-2.0-flash