From e9faac5c9d9ee28351312bc63685b4a3c59ad94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 9 Feb 2026 09:33:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:AI=20=ED=86=A0=ED=81=B0=20=EB=8B=A8?= =?UTF-8?q?=EA=B0=80=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(DB=20=EA=B8=B0=EB=B0=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai_pricing_configs 테이블 마이그레이션 생성 (기본 시드 데이터 포함) - AiPricingConfig 모델 추가 (캐시 적용 단가/환율 조회) - AiReportService 하드코딩 단가를 DB 조회로 변경 Co-Authored-By: Claude Opus 4.6 --- app/Models/Tenants/AiPricingConfig.php | 67 +++++++++++++ app/Services/AiReportService.php | 10 +- ...100000_create_ai_pricing_configs_table.php | 97 +++++++++++++++++++ 3 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 app/Models/Tenants/AiPricingConfig.php create mode 100644 database/migrations/2026_02_09_100000_create_ai_pricing_configs_table.php diff --git a/app/Models/Tenants/AiPricingConfig.php b/app/Models/Tenants/AiPricingConfig.php new file mode 100644 index 0000000..e1fc250 --- /dev/null +++ b/app/Models/Tenants/AiPricingConfig.php @@ -0,0 +1,67 @@ + 'decimal:4', + 'output_price_per_million' => 'decimal:4', + 'unit_price' => 'decimal:6', + 'exchange_rate' => 'decimal:2', + 'is_active' => 'boolean', + ]; + + /** + * 활성 단가 설정 조회 (캐시 적용) + */ + public static function getActivePricing(string $provider): ?self + { + return Cache::remember("ai_pricing_{$provider}", 3600, function () use ($provider) { + return self::where('provider', $provider) + ->where('is_active', true) + ->first(); + }); + } + + /** + * 환율 조회 (첫 번째 활성 레코드 기준) + */ + public static function getExchangeRate(): float + { + return Cache::remember('ai_pricing_exchange_rate', 3600, function () { + $config = self::where('is_active', true)->first(); + + return $config ? (float) $config->exchange_rate : 1400.0; + }); + } + + /** + * 캐시 초기화 + */ + public static function clearCache(): void + { + $providers = ['gemini', 'claude', 'google-stt', 'google-gcs']; + foreach ($providers as $provider) { + Cache::forget("ai_pricing_{$provider}"); + } + Cache::forget('ai_pricing_exchange_rate'); + } +} diff --git a/app/Services/AiReportService.php b/app/Services/AiReportService.php index ca71c32..8a65a0e 100644 --- a/app/Services/AiReportService.php +++ b/app/Services/AiReportService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Models\Tenants\AiPricingConfig; use App\Models\Tenants\AiReport; use App\Models\Tenants\AiTokenUsage; use App\Models\Tenants\Card; @@ -398,12 +399,13 @@ private function saveTokenUsage(array $apiResult, string $model, string $menuNam $completionTokens = $usage['candidatesTokenCount'] ?? 0; $totalTokens = $usage['totalTokenCount'] ?? 0; - // Gemini 2.0 Flash 기준 단가 (USD per token) - $inputPricePerToken = 0.10 / 1_000_000; // $0.10 / 1M tokens - $outputPricePerToken = 0.40 / 1_000_000; // $0.40 / 1M tokens + // DB 단가 조회 (fallback: 하드코딩 기본값) + $pricing = AiPricingConfig::getActivePricing('gemini'); + $inputPricePerToken = $pricing ? (float) $pricing->input_price_per_million / 1_000_000 : 0.10 / 1_000_000; + $outputPricePerToken = $pricing ? (float) $pricing->output_price_per_million / 1_000_000 : 0.40 / 1_000_000; $costUsd = ($promptTokens * $inputPricePerToken) + ($completionTokens * $outputPricePerToken); - $exchangeRate = (float) config('services.gemini.exchange_rate', 1400); + $exchangeRate = AiPricingConfig::getExchangeRate(); $costKrw = $costUsd * $exchangeRate; AiTokenUsage::create([ diff --git a/database/migrations/2026_02_09_100000_create_ai_pricing_configs_table.php b/database/migrations/2026_02_09_100000_create_ai_pricing_configs_table.php new file mode 100644 index 0000000..0b58226 --- /dev/null +++ b/database/migrations/2026_02_09_100000_create_ai_pricing_configs_table.php @@ -0,0 +1,97 @@ +id(); + $table->string('provider', 50)->comment('제공자 (gemini, claude, google-stt, google-gcs)'); + $table->string('model_name', 100)->comment('모델명 (gemini-2.0-flash, claude-3-haiku 등)'); + $table->decimal('input_price_per_million', 10, 4)->default(0)->comment('입력 토큰 백만개당 가격 (USD)'); + $table->decimal('output_price_per_million', 10, 4)->default(0)->comment('출력 토큰 백만개당 가격 (USD)'); + $table->decimal('unit_price', 10, 6)->default(0)->comment('단위 가격 (STT: 15초당, GCS: 1000건당)'); + $table->string('unit_description', 100)->nullable()->comment('단위 설명 (예: per 15 seconds)'); + $table->decimal('exchange_rate', 8, 2)->default(1400)->comment('USD→KRW 환율'); + $table->boolean('is_active')->default(true)->comment('활성 여부'); + $table->string('description', 255)->nullable()->comment('설명'); + $table->timestamps(); + + $table->unique('provider', 'uq_ai_pricing_provider'); + $table->index('is_active', 'idx_ai_pricing_active'); + }); + + // 기본 시드 데이터 삽입 + $now = now(); + DB::table('ai_pricing_configs')->insert([ + [ + 'provider' => 'gemini', + 'model_name' => 'gemini-2.0-flash', + 'input_price_per_million' => 0.1000, + 'output_price_per_million' => 0.4000, + 'unit_price' => 0, + 'unit_description' => null, + 'exchange_rate' => 1400, + 'is_active' => true, + 'description' => 'Gemini 2.0 Flash - 입력 $0.10/1M, 출력 $0.40/1M', + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'provider' => 'claude', + 'model_name' => 'claude-3-haiku', + 'input_price_per_million' => 0.2500, + 'output_price_per_million' => 1.2500, + 'unit_price' => 0, + 'unit_description' => null, + 'exchange_rate' => 1400, + 'is_active' => true, + 'description' => 'Claude 3 Haiku - 입력 $0.25/1M, 출력 $1.25/1M', + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'provider' => 'google-stt', + 'model_name' => 'latest_long', + 'input_price_per_million' => 0, + 'output_price_per_million' => 0, + 'unit_price' => 0.009000, + 'unit_description' => 'per 15 seconds', + 'exchange_rate' => 1400, + 'is_active' => true, + 'description' => 'Google Speech-to-Text - $0.009/15초', + 'created_at' => $now, + 'updated_at' => $now, + ], + [ + 'provider' => 'google-gcs', + 'model_name' => 'cloud-storage', + 'input_price_per_million' => 0, + 'output_price_per_million' => 0, + 'unit_price' => 0.005000, + 'unit_description' => 'per 1000 operations', + 'exchange_rate' => 1400, + 'is_active' => true, + 'description' => 'Google Cloud Storage - $0.005/1000건', + 'created_at' => $now, + 'updated_at' => $now, + ], + ]); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ai_pricing_configs'); + } +};