From 52b26c7216e97bfa72a0c424db73fcdaa7bbf82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 23 Feb 2026 09:55:07 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20[ai-config]=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?API=20=ED=82=A4=EB=A5=BC=20DB(ai=5Fconfigs)=EC=97=90=EC=84=9C?= =?UTF-8?q?=20.env=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AiConfig::getActiveGemini() → config('services.gemini') 기반 - AiConfig::getActiveGcs() → config('services.google') 기반 - AiConfig::getActiveClaude() → config('services.claude') 기반 - AiConfig::getActiveNotion() → config('services.notion') 기반 - GoogleCloudStorageService: DB 우선 로직 제거, .env만 사용 - 8개 서비스 파일은 수정 없이 동작 (AiConfig 인터페이스 유지) --- app/Models/System/AiConfig.php | 96 +++++++++++--- app/Services/GoogleCloudStorageService.php | 143 ++++++++------------- config/services.php | 2 + 3 files changed, 134 insertions(+), 107 deletions(-) diff --git a/app/Models/System/AiConfig.php b/app/Models/System/AiConfig.php index e47bab36..d771379d 100644 --- a/app/Models/System/AiConfig.php +++ b/app/Models/System/AiConfig.php @@ -82,43 +82,84 @@ class AiConfig extends Model public const API_SERVICE_PROVIDERS = ['notion']; /** - * 활성화된 Gemini 설정 조회 + * 활성화된 Gemini 설정 조회 (.env 기반) */ public static function getActiveGemini(): ?self { - return self::where('provider', 'gemini') - ->where('is_active', true) - ->first(); + $apiKey = config('services.gemini.api_key'); + if (! $apiKey) { + return null; + } + + $instance = new self; + $instance->provider = 'gemini'; + $instance->api_key = $apiKey; + $instance->model = config('services.gemini.model', 'gemini-2.0-flash'); + $instance->base_url = config('services.gemini.base_url', 'https://generativelanguage.googleapis.com/v1beta'); + $instance->is_active = true; + $instance->options = [ + 'auth_type' => 'api_key', + 'project_id' => config('services.gemini.project_id', 'codebridge-chatbot'), + 'region' => config('services.vertex_ai.location', 'us-central1'), + ]; + + return $instance; } /** - * 활성화된 Claude 설정 조회 + * 활성화된 Claude 설정 조회 (.env 기반) */ public static function getActiveClaude(): ?self { - return self::where('provider', 'claude') - ->where('is_active', true) - ->first(); + $apiKey = config('services.claude.api_key'); + if (! $apiKey) { + return null; + } + + $instance = new self; + $instance->provider = 'claude'; + $instance->api_key = $apiKey; + $instance->model = 'claude-sonnet-4-20250514'; + $instance->base_url = 'https://api.anthropic.com/v1'; + $instance->is_active = true; + $instance->options = []; + + return $instance; } /** - * 활성화된 Notion 설정 조회 + * 활성화된 Notion 설정 조회 (.env 기반) */ public static function getActiveNotion(): ?self { - return self::where('provider', 'notion') - ->where('is_active', true) - ->first(); + $apiKey = config('services.notion.api_key'); + if (! $apiKey) { + return null; + } + + $instance = new self; + $instance->provider = 'notion'; + $instance->api_key = $apiKey; + $instance->model = config('services.notion.version', '2025-09-03'); + $instance->base_url = config('services.notion.base_url', 'https://api.notion.com/v1'); + $instance->is_active = true; + $instance->options = []; + + return $instance; } /** - * Provider별 활성 설정 조회 + * Provider별 활성 설정 조회 (.env 기반) */ public static function getActive(string $provider): ?self { - return self::where('provider', $provider) - ->where('is_active', true) - ->first(); + return match ($provider) { + 'gemini' => self::getActiveGemini(), + 'claude' => self::getActiveClaude(), + 'notion' => self::getActiveNotion(), + 'gcs' => self::getActiveGcs(), + default => null, + }; } /** @@ -145,13 +186,28 @@ public function getProviderLabelAttribute(): string } /** - * 활성화된 GCS 설정 조회 + * 활성화된 GCS 설정 조회 (.env 기반) */ public static function getActiveGcs(): ?self { - return self::where('provider', 'gcs') - ->where('is_active', true) - ->first(); + $credentialsPath = config('services.google.credentials_path'); + $bucket = config('services.google.storage_bucket'); + if (! $bucket) { + return null; + } + + $instance = new self; + $instance->provider = 'gcs'; + $instance->api_key = 'gcs_service_account'; + $instance->model = '-'; + $instance->base_url = 'https://storage.googleapis.com'; + $instance->is_active = true; + $instance->options = [ + 'bucket_name' => $bucket, + 'service_account_path' => $credentialsPath, + ]; + + return $instance; } /** diff --git a/app/Services/GoogleCloudStorageService.php b/app/Services/GoogleCloudStorageService.php index e65e5357..fd145e0a 100644 --- a/app/Services/GoogleCloudStorageService.php +++ b/app/Services/GoogleCloudStorageService.php @@ -2,7 +2,6 @@ namespace App\Services; -use App\Models\System\AiConfig; use Illuminate\Support\Facades\Log; /** @@ -18,7 +17,9 @@ class GoogleCloudStorageService { private ?string $bucketName = null; + private ?array $serviceAccount = null; + private string $configSource = 'none'; public function __construct() @@ -27,67 +28,27 @@ public function __construct() } /** - * GCS 설정 로드 - * - * 우선순위: - * 1. DB 설정 (ai_configs 테이블의 활성화된 gcs provider) - * 2. 환경변수 (.env의 GCS_BUCKET_NAME, GCS_SERVICE_ACCOUNT_PATH) - * 3. 레거시 파일 설정 (/sales/apikey/) + * GCS 설정 로드 (.env 기반) */ private function loadConfig(): void { - // 1. DB 설정 확인 (GCS_USE_DB_CONFIG=true일 때만) - if (config('gcs.use_db_config', true)) { - $dbConfig = AiConfig::getActiveGcs(); + $this->bucketName = config('services.google.storage_bucket'); + $credentialsPath = config('services.google.credentials_path'); - if ($dbConfig) { - $this->bucketName = $dbConfig->getBucketName(); + if ($credentialsPath && file_exists($credentialsPath)) { + $this->serviceAccount = json_decode(file_get_contents($credentialsPath), true); + } - // 서비스 계정: JSON 직접 입력 또는 파일 경로 - if ($dbConfig->getServiceAccountJson()) { - $this->serviceAccount = $dbConfig->getServiceAccountJson(); - } elseif ($dbConfig->getServiceAccountPath() && file_exists($dbConfig->getServiceAccountPath())) { - $this->serviceAccount = json_decode(file_get_contents($dbConfig->getServiceAccountPath()), true); - } - - if ($this->bucketName && $this->serviceAccount) { - $this->configSource = 'db'; - Log::debug('GCS 설정 로드: DB (활성화된 설정: ' . $dbConfig->name . ')'); - return; - } + // fallback: 레거시 파일 경로 + if (! $this->serviceAccount) { + $legacyPath = '/var/www/sales/apikey/google_service_account.json'; + if (file_exists($legacyPath)) { + $this->serviceAccount = json_decode(file_get_contents($legacyPath), true); } } - // 2. 환경변수 (.env) 설정 - $envBucket = config('gcs.bucket_name'); - $envServiceAccountPath = config('gcs.service_account_path'); - - if ($envBucket && $envServiceAccountPath && file_exists($envServiceAccountPath)) { - $this->bucketName = $envBucket; - $this->serviceAccount = json_decode(file_get_contents($envServiceAccountPath), true); - - if ($this->serviceAccount) { - $this->configSource = 'env'; - Log::debug('GCS 설정 로드: 환경변수 (.env)'); - return; - } - } - - // 3. 레거시 파일 설정 (fallback) - $gcsConfigPath = base_path('../sales/apikey/gcs_config.txt'); - if (file_exists($gcsConfigPath)) { - $config = parse_ini_file($gcsConfigPath); - $this->bucketName = $config['bucket_name'] ?? null; - } - - $serviceAccountPath = base_path('../sales/apikey/google_service_account.json'); - if (file_exists($serviceAccountPath)) { - $this->serviceAccount = json_decode(file_get_contents($serviceAccountPath), true); - } - if ($this->bucketName && $this->serviceAccount) { - $this->configSource = 'legacy'; - Log::debug('GCS 설정 로드: 레거시 파일'); + $this->configSource = 'env'; } } @@ -110,25 +71,27 @@ public function isAvailable(): bool /** * GCS에 파일 업로드 * - * @param string $filePath 로컬 파일 경로 - * @param string $objectName GCS에 저장할 객체 이름 + * @param string $filePath 로컬 파일 경로 + * @param string $objectName GCS에 저장할 객체 이름 * @return string|null GCS URI (gs://bucket/object) 또는 실패 시 null */ public function upload(string $filePath, string $objectName): ?string { - if (!$this->isAvailable()) { + if (! $this->isAvailable()) { Log::warning('GCS 업로드 실패: 설정되지 않음'); + return null; } - if (!file_exists($filePath)) { - Log::error('GCS 업로드 실패: 파일 없음 - ' . $filePath); + if (! file_exists($filePath)) { + Log::error('GCS 업로드 실패: 파일 없음 - '.$filePath); + return null; } // OAuth 2.0 토큰 생성 $accessToken = $this->getAccessToken(); - if (!$accessToken) { + if (! $accessToken) { return null; } @@ -136,16 +99,16 @@ public function upload(string $filePath, string $objectName): ?string $fileContent = file_get_contents($filePath); $mimeType = mime_content_type($filePath) ?: 'application/octet-stream'; - $uploadUrl = 'https://storage.googleapis.com/upload/storage/v1/b/' . - urlencode($this->bucketName) . '/o?uploadType=media&name=' . + $uploadUrl = 'https://storage.googleapis.com/upload/storage/v1/b/'. + urlencode($this->bucketName).'/o?uploadType=media&name='. urlencode($objectName); $ch = curl_init($uploadUrl); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: Bearer ' . $accessToken, - 'Content-Type: ' . $mimeType, - 'Content-Length: ' . strlen($fileContent) + 'Authorization: Bearer '.$accessToken, + 'Content-Type: '.$mimeType, + 'Content-Length: '.strlen($fileContent), ]); curl_setopt($ch, CURLOPT_POSTFIELDS, $fileContent); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -157,25 +120,27 @@ public function upload(string $filePath, string $objectName): ?string curl_close($ch); if ($httpCode === 200) { - $gcsUri = 'gs://' . $this->bucketName . '/' . $objectName; - Log::info('GCS 업로드 성공: ' . $gcsUri); + $gcsUri = 'gs://'.$this->bucketName.'/'.$objectName; + Log::info('GCS 업로드 성공: '.$gcsUri); + return $gcsUri; } - Log::error('GCS 업로드 실패 (HTTP ' . $httpCode . '): ' . ($error ?: $response)); + Log::error('GCS 업로드 실패 (HTTP '.$httpCode.'): '.($error ?: $response)); + return null; } /** * GCS에서 서명된 다운로드 URL 생성 * - * @param string $objectName GCS 객체 이름 또는 gs:// URI - * @param int $expiresInMinutes URL 유효 시간 (분) + * @param string $objectName GCS 객체 이름 또는 gs:// URI + * @param int $expiresInMinutes URL 유효 시간 (분) * @return string|null 서명된 URL 또는 실패 시 null */ public function getSignedUrl(string $objectName, int $expiresInMinutes = 60): ?string { - if (!$this->isAvailable()) { + if (! $this->isAvailable()) { return null; } @@ -185,8 +150,9 @@ public function getSignedUrl(string $objectName, int $expiresInMinutes = 60): ?s $stringToSign = "GET\n\n\n{$expiration}\n/{$this->bucketName}/{$objectName}"; $privateKey = openssl_pkey_get_private($this->serviceAccount['private_key']); - if (!$privateKey) { + if (! $privateKey) { Log::error('GCS URL 서명 실패: 개인 키 읽기 오류'); + return null; } @@ -198,39 +164,39 @@ public function getSignedUrl(string $objectName, int $expiresInMinutes = 60): ?s $encodedSignature = urlencode(base64_encode($signature)); $clientEmail = urlencode($this->serviceAccount['client_email']); - return "https://storage.googleapis.com/{$this->bucketName}/{$objectName}" . - "?GoogleAccessId={$clientEmail}" . - "&Expires={$expiration}" . + return "https://storage.googleapis.com/{$this->bucketName}/{$objectName}". + "?GoogleAccessId={$clientEmail}". + "&Expires={$expiration}". "&Signature={$encodedSignature}"; } /** * GCS에서 파일 삭제 * - * @param string $objectName GCS 객체 이름 또는 gs:// URI + * @param string $objectName GCS 객체 이름 또는 gs:// URI * @return bool 성공 여부 */ public function delete(string $objectName): bool { - if (!$this->isAvailable()) { + if (! $this->isAvailable()) { return false; } $objectName = $this->stripGsPrefix($objectName); $accessToken = $this->getAccessToken(); - if (!$accessToken) { + if (! $accessToken) { return false; } - $deleteUrl = 'https://storage.googleapis.com/storage/v1/b/' . - urlencode($this->bucketName) . '/o/' . + $deleteUrl = 'https://storage.googleapis.com/storage/v1/b/'. + urlencode($this->bucketName).'/o/'. urlencode($objectName); $ch = curl_init($deleteUrl); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Authorization: Bearer ' . $accessToken, + 'Authorization: Bearer '.$accessToken, ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -254,28 +220,29 @@ private function getAccessToken(): ?string 'scope' => 'https://www.googleapis.com/auth/devstorage.full_control', 'aud' => 'https://oauth2.googleapis.com/token', 'exp' => $now + 3600, - 'iat' => $now + 'iat' => $now, ])); $privateKey = openssl_pkey_get_private($this->serviceAccount['private_key']); - if (!$privateKey) { + if (! $privateKey) { Log::error('GCS 토큰 실패: 개인 키 읽기 오류'); + return null; } - openssl_sign($jwtHeader . '.' . $jwtClaim, $signature, $privateKey, OPENSSL_ALGO_SHA256); + openssl_sign($jwtHeader.'.'.$jwtClaim, $signature, $privateKey, OPENSSL_ALGO_SHA256); if (PHP_VERSION_ID < 80000) { openssl_free_key($privateKey); } - $jwt = $jwtHeader . '.' . $jwtClaim . '.' . $this->base64UrlEncode($signature); + $jwt = $jwtHeader.'.'.$jwtClaim.'.'.$this->base64UrlEncode($signature); // OAuth 토큰 요청 $ch = curl_init('https://oauth2.googleapis.com/token'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', - 'assertion' => $jwt + 'assertion' => $jwt, ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']); @@ -285,11 +252,13 @@ private function getAccessToken(): ?string curl_close($ch); if ($httpCode !== 200) { - Log::error('GCS 토큰 실패: HTTP ' . $httpCode); + Log::error('GCS 토큰 실패: HTTP '.$httpCode); + return null; } $data = json_decode($response, true); + return $data['access_token'] ?? null; } @@ -308,7 +277,7 @@ private function base64UrlEncode(string $data): string */ private function stripGsPrefix(string $objectName): string { - $prefix = 'gs://' . $this->bucketName . '/'; + $prefix = 'gs://'.$this->bucketName.'/'; if (str_starts_with($objectName, $prefix)) { return substr($objectName, strlen($prefix)); } diff --git a/config/services.php b/config/services.php index 00e73855..d012d82e 100644 --- a/config/services.php +++ b/config/services.php @@ -37,6 +37,8 @@ 'gemini' => [ 'api_key' => env('GEMINI_API_KEY'), + 'model' => env('GEMINI_MODEL', 'gemini-2.0-flash'), + 'base_url' => env('GEMINI_BASE_URL', 'https://generativelanguage.googleapis.com/v1beta'), 'project_id' => env('GEMINI_PROJECT_ID', 'codebridge-chatbot'), ],