feat: Vertex AI 연결 테스트 기능 추가

- testGeminiVertexAi() 메서드 추가
- getVertexAiAccessToken() OAuth 토큰 획득 메서드 추가
- 모달에서 Vertex AI 파라미터 전송하도록 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
pro
2026-01-30 13:37:33 +09:00
parent a9637ba5c6
commit bf1e3ff5b7
2 changed files with 159 additions and 12 deletions

View File

@@ -204,19 +204,35 @@ public function test(Request $request): JsonResponse
{
$validated = $request->validate([
'provider' => 'required|string|in:gemini,claude,openai',
'api_key' => 'required|string',
'api_key' => 'nullable|string',
'model' => 'required|string',
'base_url' => 'nullable|string',
'auth_type' => 'nullable|string|in:api_key,vertex_ai',
'project_id' => 'nullable|string',
'region' => 'nullable|string',
'service_account_path' => 'nullable|string',
]);
try {
$provider = $validated['provider'];
$apiKey = $validated['api_key'];
$model = $validated['model'];
$baseUrl = $validated['base_url'] ?? AiConfig::DEFAULT_BASE_URLS[$provider];
$authType = $validated['auth_type'] ?? 'api_key';
if ($provider === 'gemini') {
$result = $this->testGemini($baseUrl, $model, $apiKey);
if ($authType === 'vertex_ai') {
// Vertex AI (서비스 계정) 방식
$result = $this->testGeminiVertexAi(
$model,
$validated['project_id'] ?? '',
$validated['region'] ?? 'us-central1',
$validated['service_account_path'] ?? ''
);
} else {
// API 키 방식
$apiKey = $validated['api_key'] ?? '';
$baseUrl = $validated['base_url'] ?? AiConfig::DEFAULT_BASE_URLS[$provider];
$result = $this->testGemini($baseUrl, $model, $apiKey);
}
} else {
return response()->json([
'ok' => false,
@@ -234,7 +250,7 @@ public function test(Request $request): JsonResponse
}
/**
* Gemini API 테스트
* Gemini API 테스트 (API 키 방식)
*/
private function testGemini(string $baseUrl, string $model, string $apiKey): array
{
@@ -267,6 +283,115 @@ private function testGemini(string $baseUrl, string $model, string $apiKey): arr
];
}
/**
* Gemini API 테스트 (Vertex AI 방식)
*/
private function testGeminiVertexAi(string $model, string $projectId, string $region, string $serviceAccountPath): array
{
// 필수 파라미터 검증
if (empty($projectId)) {
return ['ok' => false, 'error' => '프로젝트 ID가 필요합니다.'];
}
if (empty($serviceAccountPath)) {
return ['ok' => false, 'error' => '서비스 계정 파일 경로가 필요합니다.'];
}
if (!file_exists($serviceAccountPath)) {
return ['ok' => false, 'error' => "서비스 계정 파일을 찾을 수 없습니다: {$serviceAccountPath}"];
}
// 서비스 계정 JSON 로드
$serviceAccount = json_decode(file_get_contents($serviceAccountPath), true);
if (!$serviceAccount || empty($serviceAccount['client_email']) || empty($serviceAccount['private_key'])) {
return ['ok' => false, 'error' => '서비스 계정 파일 형식이 올바르지 않습니다.'];
}
// OAuth 토큰 획득
$accessToken = $this->getVertexAiAccessToken($serviceAccount);
if (!$accessToken) {
return ['ok' => false, 'error' => 'OAuth 토큰 획득 실패. 서비스 계정 권한을 확인하세요.'];
}
// Vertex AI 엔드포인트 URL 구성
$url = "https://{$region}-aiplatform.googleapis.com/v1/projects/{$projectId}/locations/{$region}/publishers/google/models/{$model}:generateContent";
$response = \Illuminate\Support\Facades\Http::timeout(30)
->withHeaders([
'Authorization' => 'Bearer ' . $accessToken,
'Content-Type' => 'application/json',
])
->post($url, [
'contents' => [
[
'role' => 'user',
'parts' => [
['text' => '안녕하세요. 테스트입니다. "OK"라고만 응답해주세요.'],
],
],
],
'generationConfig' => [
'temperature' => 0,
'maxOutputTokens' => 10,
],
]);
if ($response->successful()) {
return [
'ok' => true,
'message' => 'Vertex AI 연결 테스트 성공',
];
}
// 상세 오류 메시지 추출
$errorBody = $response->json();
$errorMsg = $errorBody['error']['message'] ?? ('HTTP ' . $response->status());
return [
'ok' => false,
'error' => "Vertex AI 오류: {$errorMsg}",
];
}
/**
* Vertex AI OAuth 토큰 획득
*/
private function getVertexAiAccessToken(array $serviceAccount): ?string
{
$now = time();
$jwtHeader = $this->base64UrlEncode(json_encode(['alg' => 'RS256', 'typ' => 'JWT']));
$jwtClaim = $this->base64UrlEncode(json_encode([
'iss' => $serviceAccount['client_email'],
'scope' => 'https://www.googleapis.com/auth/cloud-platform',
'aud' => 'https://oauth2.googleapis.com/token',
'exp' => $now + 3600,
'iat' => $now,
]));
$privateKey = openssl_pkey_get_private($serviceAccount['private_key']);
if (!$privateKey) {
return null;
}
openssl_sign($jwtHeader . '.' . $jwtClaim, $signature, $privateKey, OPENSSL_ALGO_SHA256);
if (PHP_VERSION_ID < 80000) {
openssl_free_key($privateKey);
}
$jwt = $jwtHeader . '.' . $jwtClaim . '.' . $this->base64UrlEncode($signature);
$response = \Illuminate\Support\Facades\Http::asForm()->post('https://oauth2.googleapis.com/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt,
]);
if ($response->successful()) {
return $response->json('access_token');
}
return null;
}
/**
* GCS 연결 테스트
*/

View File

@@ -602,16 +602,38 @@ function toggleAuthTypeUI(provider, authType) {
// 연결 테스트 (모달에서)
window.testConnectionFromModal = async function() {
const provider = document.getElementById('config-provider').value;
const authType = document.getElementById('config-auth-type').value;
const data = {
provider: document.getElementById('config-provider').value,
api_key: document.getElementById('config-api-key').value,
provider: provider,
model: document.getElementById('config-model').value,
base_url: document.getElementById('config-base-url').value || null
auth_type: authType
};
if (!data.api_key) {
showToast('API 키를 입력해주세요.', 'error');
return;
// Vertex AI 방식인 경우
if (provider === 'gemini' && authType === 'vertex_ai') {
data.project_id = document.getElementById('config-project-id').value;
data.region = document.getElementById('config-region').value;
data.service_account_path = document.getElementById('config-service-account-path').value;
if (!data.project_id) {
showToast('프로젝트 ID를 입력해주세요.', 'error');
return;
}
if (!data.service_account_path) {
showToast('서비스 계정 파일 경로를 입력해주세요.', 'error');
return;
}
} else {
// API 키 방식인 경우
data.api_key = document.getElementById('config-api-key').value;
data.base_url = document.getElementById('config-base-url').value || null;
if (!data.api_key) {
showToast('API 키를 입력해주세요.', 'error');
return;
}
}
showToast('연결 테스트 중...', 'info');
@@ -629,7 +651,7 @@ function toggleAuthTypeUI(provider, authType) {
const result = await response.json();
if (result.ok) {
showToast('연결 테스트 성공!', 'success');
showToast(result.message || '연결 테스트 성공!', 'success');
} else {
showToast(result.error || '연결 테스트 실패', 'error');
}