feat: [additional] Notion 검색 기능 추가

- NotionService: Notion API 검색 + Gemini AI 답변
- AiConfig에 notion provider 추가
- 추가기능 > Notion 검색 채팅 UI
This commit is contained in:
김보곤
2026-02-22 23:04:16 +09:00
parent f8b0843763
commit aa3c9f4c3b
6 changed files with 600 additions and 33 deletions

View File

@@ -48,7 +48,7 @@ public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'provider' => 'required|string|in:gemini,claude,openai,gcs',
'provider' => 'required|string|in:gemini,claude,openai,gcs,notion',
'api_key' => 'nullable|string|max:255',
'model' => 'nullable|string|max:100',
'base_url' => 'nullable|string|max:255',
@@ -108,7 +108,7 @@ public function update(Request $request, int $id): JsonResponse
$validated = $request->validate([
'name' => 'required|string|max:50',
'provider' => 'required|string|in:gemini,claude,openai,gcs',
'provider' => 'required|string|in:gemini,claude,openai,gcs,notion',
'api_key' => 'nullable|string|max:255',
'model' => 'nullable|string|max:100',
'base_url' => 'nullable|string|max:255',
@@ -203,7 +203,7 @@ public function toggle(int $id): JsonResponse
public function test(Request $request): JsonResponse
{
$validated = $request->validate([
'provider' => 'required|string|in:gemini,claude,openai',
'provider' => 'required|string|in:gemini,claude,openai,notion',
'api_key' => 'nullable|string',
'model' => 'required|string',
'base_url' => 'nullable|string',
@@ -279,7 +279,7 @@ private function testGemini(string $baseUrl, string $model, string $apiKey): arr
return [
'ok' => false,
'error' => 'API 응답 오류: ' . $response->status(),
'error' => 'API 응답 오류: '.$response->status(),
];
}
@@ -297,19 +297,19 @@ private function testGeminiVertexAi(string $model, string $projectId, string $re
return ['ok' => false, 'error' => '서비스 계정 파일 경로가 필요합니다.'];
}
if (!file_exists($serviceAccountPath)) {
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'])) {
if (! $serviceAccount || empty($serviceAccount['client_email']) || empty($serviceAccount['private_key'])) {
return ['ok' => false, 'error' => '서비스 계정 파일 형식이 올바르지 않습니다.'];
}
// OAuth 토큰 획득
$accessToken = $this->getVertexAiAccessToken($serviceAccount);
if (!$accessToken) {
if (! $accessToken) {
return ['ok' => false, 'error' => 'OAuth 토큰 획득 실패. 서비스 계정 권한을 확인하세요.'];
}
@@ -318,7 +318,7 @@ private function testGeminiVertexAi(string $model, string $projectId, string $re
$response = \Illuminate\Support\Facades\Http::timeout(30)
->withHeaders([
'Authorization' => 'Bearer ' . $accessToken,
'Authorization' => 'Bearer '.$accessToken,
'Content-Type' => 'application/json',
])
->post($url, [
@@ -345,7 +345,7 @@ private function testGeminiVertexAi(string $model, string $projectId, string $re
// 상세 오류 메시지 추출
$errorBody = $response->json();
$errorMsg = $errorBody['error']['message'] ?? ('HTTP ' . $response->status());
$errorMsg = $errorBody['error']['message'] ?? ('HTTP '.$response->status());
return [
'ok' => false,
@@ -369,16 +369,16 @@ private function getVertexAiAccessToken(array $serviceAccount): ?string
]));
$privateKey = openssl_pkey_get_private($serviceAccount['private_key']);
if (!$privateKey) {
if (! $privateKey) {
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);
$response = \Illuminate\Support\Facades\Http::asForm()->post('https://oauth2.googleapis.com/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
@@ -408,13 +408,13 @@ public function testGcs(Request $request): JsonResponse
$serviceAccount = null;
// 서비스 계정 로드 (JSON 직접 입력 또는 파일 경로)
if (!empty($validated['service_account_json'])) {
if (! empty($validated['service_account_json'])) {
$serviceAccount = $validated['service_account_json'];
} elseif (!empty($validated['service_account_path']) && file_exists($validated['service_account_path'])) {
} elseif (! empty($validated['service_account_path']) && file_exists($validated['service_account_path'])) {
$serviceAccount = json_decode(file_get_contents($validated['service_account_path']), true);
}
if (!$serviceAccount) {
if (! $serviceAccount) {
return response()->json([
'ok' => false,
'error' => '서비스 계정 정보를 찾을 수 없습니다.',
@@ -423,7 +423,7 @@ public function testGcs(Request $request): JsonResponse
// OAuth 토큰 획득
$accessToken = $this->getGcsAccessToken($serviceAccount);
if (!$accessToken) {
if (! $accessToken) {
return response()->json([
'ok' => false,
'error' => 'OAuth 토큰 획득 실패',
@@ -432,7 +432,7 @@ public function testGcs(Request $request): JsonResponse
// 버킷 존재 확인
$response = \Illuminate\Support\Facades\Http::timeout(10)
->withHeaders(['Authorization' => 'Bearer ' . $accessToken])
->withHeaders(['Authorization' => 'Bearer '.$accessToken])
->get("https://storage.googleapis.com/storage/v1/b/{$bucketName}");
if ($response->successful()) {
@@ -444,7 +444,7 @@ public function testGcs(Request $request): JsonResponse
return response()->json([
'ok' => false,
'error' => '버킷 접근 실패: ' . $response->status(),
'error' => '버킷 접근 실패: '.$response->status(),
]);
} catch (\Exception $e) {
@@ -467,24 +467,24 @@ private function getGcsAccessToken(array $serviceAccount): ?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($serviceAccount['private_key']);
if (!$privateKey) {
if (! $privateKey) {
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);
$response = \Illuminate\Support\Facades\Http::asForm()->post('https://oauth2.googleapis.com/token', [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt
'assertion' => $jwt,
]);
if ($response->successful()) {