refactor: [ai-config] 모든 API 키를 DB(ai_configs)에서 .env로 전환
- 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 인터페이스 유지)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user