From bb81d07d61b79fc608ecb91f4837198b7802be8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Sat, 7 Feb 2026 11:27:53 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EC=A0=84=EC=B2=B4=20AI=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=EC=97=90=20=ED=86=A0=ED=81=B0=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=9F=89=20=EA=B8=B0=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AiTokenHelper 공통 헬퍼 생성 (Gemini/Claude 응답 파서) - BizCertOcrService (Claude) 토큰 기록 추가 - BusinessCardOcrService (Gemini) 토큰 기록 추가 - MeetingLogService (Claude) 토큰 기록 추가 Co-Authored-By: Claude Opus 4.6 --- app/Helpers/AiTokenHelper.php | 92 +++++++++++++++++++++++++ app/Services/BizCertOcrService.php | 5 ++ app/Services/BusinessCardOcrService.php | 5 ++ app/Services/MeetingLogService.php | 4 ++ 4 files changed, 106 insertions(+) create mode 100644 app/Helpers/AiTokenHelper.php diff --git a/app/Helpers/AiTokenHelper.php b/app/Helpers/AiTokenHelper.php new file mode 100644 index 00000000..11c1350e --- /dev/null +++ b/app/Helpers/AiTokenHelper.php @@ -0,0 +1,92 @@ + $e->getMessage()]); + } + } + + /** + * Claude API 응답에서 토큰 사용량 저장 + */ + public static function saveClaudeUsage(array $apiResult, string $model, string $menuName): void + { + try { + $usage = $apiResult['usage'] ?? null; + if (! $usage) { + return; + } + + $promptTokens = $usage['input_tokens'] ?? 0; + $completionTokens = $usage['output_tokens'] ?? 0; + $totalTokens = $promptTokens + $completionTokens; + + // Claude 3 Haiku 기준 단가 + $inputPrice = 0.25 / 1_000_000; + $outputPrice = 1.25 / 1_000_000; + + self::save($model, $menuName, $promptTokens, $completionTokens, $totalTokens, $inputPrice, $outputPrice); + } catch (\Exception $e) { + Log::warning('AI token usage save failed (Claude)', ['error' => $e->getMessage()]); + } + } + + /** + * 공통 저장 로직 + */ + private static function save( + string $model, + string $menuName, + int $promptTokens, + int $completionTokens, + int $totalTokens, + float $inputPricePerToken, + float $outputPricePerToken, + ): void { + $costUsd = ($promptTokens * $inputPricePerToken) + ($completionTokens * $outputPricePerToken); + $exchangeRate = (float) config('services.gemini.exchange_rate', 1400); + $costKrw = $costUsd * $exchangeRate; + + $tenantId = session('selected_tenant_id', 1); + + AiTokenUsage::create([ + 'tenant_id' => $tenantId, + 'model' => $model, + 'menu_name' => $menuName, + 'prompt_tokens' => $promptTokens, + 'completion_tokens' => $completionTokens, + 'total_tokens' => $totalTokens, + 'cost_usd' => $costUsd, + 'cost_krw' => $costKrw, + 'request_id' => Str::uuid()->toString(), + 'created_by' => auth()->id(), + ]); + } +} diff --git a/app/Services/BizCertOcrService.php b/app/Services/BizCertOcrService.php index 8140aeb7..871a8e55 100644 --- a/app/Services/BizCertOcrService.php +++ b/app/Services/BizCertOcrService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Helpers\AiTokenHelper; use App\Models\BizCert; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -88,6 +89,10 @@ public function processWithClaude(string $imageBase64, ?string $rawText = null): } $apiResponse = $response->json(); + + // 토큰 사용량 저장 + AiTokenHelper::saveClaudeUsage($apiResponse, 'claude-3-haiku-20240307', '사업자등록증OCR'); + $claudeText = $apiResponse['content'][0]['text'] ?? ''; if (empty($claudeText)) { diff --git a/app/Services/BusinessCardOcrService.php b/app/Services/BusinessCardOcrService.php index 15bf9034..0fb16654 100644 --- a/app/Services/BusinessCardOcrService.php +++ b/app/Services/BusinessCardOcrService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Helpers\AiTokenHelper; use App\Models\System\AiConfig; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; @@ -130,6 +131,10 @@ private function callGeminiApi(string $url, string $base64Image, array $headers, } $result = $response->json(); + + // 토큰 사용량 저장 + AiTokenHelper::saveGeminiUsage($result, $result['modelVersion'] ?? 'gemini', '명함OCR'); + $text = $result['candidates'][0]['content']['parts'][0]['text'] ?? ''; // JSON 파싱 diff --git a/app/Services/MeetingLogService.php b/app/Services/MeetingLogService.php index 85754ab0..780ee80e 100644 --- a/app/Services/MeetingLogService.php +++ b/app/Services/MeetingLogService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Helpers\AiTokenHelper; use App\Models\MeetingLog; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Auth; @@ -171,6 +172,9 @@ private function generateSummary(string $transcript, string $summaryType = 'meet if ($response->successful()) { $data = $response->json(); + // 토큰 사용량 저장 + AiTokenHelper::saveClaudeUsage($data, 'claude-3-haiku-20240307', '회의록AI요약'); + return $data['content'][0]['text'] ?? null; }