2026-02-07 09:57:25 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\System;
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
2026-02-09 09:33:56 +09:00
|
|
|
use App\Models\System\AiPricingConfig;
|
2026-02-07 09:57:25 +09:00
|
|
|
use App\Models\System\AiTokenUsage;
|
|
|
|
|
use App\Models\Tenants\Tenant;
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Http\Response;
|
|
|
|
|
use Illuminate\View\View;
|
|
|
|
|
|
|
|
|
|
class AiTokenUsageController extends Controller
|
|
|
|
|
{
|
|
|
|
|
public function index(Request $request): View|Response
|
|
|
|
|
{
|
|
|
|
|
if ($request->header('HX-Request')) {
|
|
|
|
|
return response('', 200)->header('HX-Redirect', route('system.ai-token-usage.index'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return view('system.ai-token-usage.index');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function list(Request $request): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$perPage = $request->input('per_page', 20);
|
|
|
|
|
$startDate = $request->input('start_date');
|
|
|
|
|
$endDate = $request->input('end_date');
|
|
|
|
|
$tenantId = $request->input('tenant_id');
|
|
|
|
|
$menuName = $request->input('menu_name');
|
|
|
|
|
|
|
|
|
|
$query = AiTokenUsage::query()
|
|
|
|
|
->orderByDesc('created_at');
|
|
|
|
|
|
|
|
|
|
if ($tenantId) {
|
|
|
|
|
$query->where('tenant_id', $tenantId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($menuName) {
|
|
|
|
|
$query->where('menu_name', $menuName);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($startDate) {
|
|
|
|
|
$query->whereDate('created_at', '>=', $startDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($endDate) {
|
|
|
|
|
$query->whereDate('created_at', '<=', $endDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 통계 (필터 조건 동일하게 적용)
|
|
|
|
|
$statsQuery = clone $query;
|
|
|
|
|
$stats = $statsQuery->selectRaw('
|
|
|
|
|
COUNT(*) as total_count,
|
|
|
|
|
SUM(prompt_tokens) as total_prompt_tokens,
|
|
|
|
|
SUM(completion_tokens) as total_completion_tokens,
|
|
|
|
|
SUM(total_tokens) as total_total_tokens,
|
|
|
|
|
SUM(cost_usd) as total_cost_usd,
|
|
|
|
|
SUM(cost_krw) as total_cost_krw
|
|
|
|
|
')->first();
|
|
|
|
|
|
|
|
|
|
// 페이지네이션
|
|
|
|
|
$records = $query->paginate($perPage);
|
|
|
|
|
|
|
|
|
|
// 테넌트 이름 매핑
|
|
|
|
|
$tenantIds = $records->pluck('tenant_id')->unique();
|
|
|
|
|
$tenants = Tenant::whereIn('id', $tenantIds)->pluck('company_name', 'id');
|
|
|
|
|
|
|
|
|
|
$data = $records->through(function ($item) use ($tenants) {
|
|
|
|
|
return [
|
|
|
|
|
'id' => $item->id,
|
|
|
|
|
'tenant_id' => $item->tenant_id,
|
|
|
|
|
'tenant_name' => $tenants[$item->tenant_id] ?? '-',
|
|
|
|
|
'model' => $item->model,
|
|
|
|
|
'menu_name' => $item->menu_name,
|
|
|
|
|
'prompt_tokens' => $item->prompt_tokens,
|
|
|
|
|
'completion_tokens' => $item->completion_tokens,
|
|
|
|
|
'total_tokens' => $item->total_tokens,
|
|
|
|
|
'cost_usd' => (float) $item->cost_usd,
|
|
|
|
|
'cost_krw' => (float) $item->cost_krw,
|
|
|
|
|
'request_id' => $item->request_id,
|
|
|
|
|
'created_at' => $item->created_at->format('Y-m-d H:i:s'),
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 필터용 메뉴 목록
|
|
|
|
|
$menuNames = AiTokenUsage::select('menu_name')
|
|
|
|
|
->distinct()
|
|
|
|
|
->orderBy('menu_name')
|
|
|
|
|
->pluck('menu_name');
|
|
|
|
|
|
|
|
|
|
// 필터용 테넌트 목록
|
|
|
|
|
$allTenantIds = AiTokenUsage::select('tenant_id')
|
|
|
|
|
->distinct()
|
|
|
|
|
->pluck('tenant_id');
|
|
|
|
|
$allTenants = Tenant::whereIn('id', $allTenantIds)
|
|
|
|
|
->orderBy('company_name')
|
|
|
|
|
->get(['id', 'company_name']);
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'success' => true,
|
|
|
|
|
'data' => $data->items(),
|
|
|
|
|
'stats' => [
|
|
|
|
|
'total_count' => (int) ($stats->total_count ?? 0),
|
|
|
|
|
'total_prompt_tokens' => (int) ($stats->total_prompt_tokens ?? 0),
|
|
|
|
|
'total_completion_tokens' => (int) ($stats->total_completion_tokens ?? 0),
|
|
|
|
|
'total_total_tokens' => (int) ($stats->total_total_tokens ?? 0),
|
|
|
|
|
'total_cost_usd' => round((float) ($stats->total_cost_usd ?? 0), 6),
|
|
|
|
|
'total_cost_krw' => round((float) ($stats->total_cost_krw ?? 0), 2),
|
|
|
|
|
],
|
|
|
|
|
'filters' => [
|
|
|
|
|
'menu_names' => $menuNames,
|
|
|
|
|
'tenants' => $allTenants->map(fn ($t) => ['id' => $t->id, 'name' => $t->company_name]),
|
|
|
|
|
],
|
|
|
|
|
'pagination' => [
|
|
|
|
|
'current_page' => $records->currentPage(),
|
|
|
|
|
'last_page' => $records->lastPage(),
|
|
|
|
|
'per_page' => $records->perPage(),
|
|
|
|
|
'total' => $records->total(),
|
|
|
|
|
],
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-02-09 09:33:56 +09:00
|
|
|
|
|
|
|
|
public function pricingList(): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$configs = AiPricingConfig::orderBy('id')->get();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'success' => true,
|
|
|
|
|
'data' => $configs->map(fn ($c) => [
|
|
|
|
|
'id' => $c->id,
|
|
|
|
|
'provider' => $c->provider,
|
|
|
|
|
'model_name' => $c->model_name,
|
|
|
|
|
'input_price_per_million' => (float) $c->input_price_per_million,
|
|
|
|
|
'output_price_per_million' => (float) $c->output_price_per_million,
|
|
|
|
|
'unit_price' => (float) $c->unit_price,
|
|
|
|
|
'unit_description' => $c->unit_description,
|
|
|
|
|
'exchange_rate' => (float) $c->exchange_rate,
|
|
|
|
|
'is_active' => $c->is_active,
|
|
|
|
|
'description' => $c->description,
|
|
|
|
|
]),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function pricingUpdate(Request $request): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$validated = $request->validate([
|
|
|
|
|
'configs' => 'required|array',
|
|
|
|
|
'configs.*.id' => 'required|integer|exists:ai_pricing_configs,id',
|
|
|
|
|
'configs.*.model_name' => 'required|string|max:100',
|
|
|
|
|
'configs.*.input_price_per_million' => 'required|numeric|min:0',
|
|
|
|
|
'configs.*.output_price_per_million' => 'required|numeric|min:0',
|
|
|
|
|
'configs.*.unit_price' => 'required|numeric|min:0',
|
|
|
|
|
'configs.*.exchange_rate' => 'required|numeric|min:0',
|
|
|
|
|
'configs.*.description' => 'nullable|string|max:255',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
foreach ($validated['configs'] as $item) {
|
|
|
|
|
AiPricingConfig::where('id', $item['id'])->update([
|
|
|
|
|
'model_name' => $item['model_name'],
|
|
|
|
|
'input_price_per_million' => $item['input_price_per_million'],
|
|
|
|
|
'output_price_per_million' => $item['output_price_per_million'],
|
|
|
|
|
'unit_price' => $item['unit_price'],
|
|
|
|
|
'exchange_rate' => $item['exchange_rate'],
|
|
|
|
|
'description' => $item['description'] ?? null,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AiPricingConfig::clearCache();
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'success' => true,
|
|
|
|
|
'message' => '단가 설정이 저장되었습니다.',
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-02-07 09:57:25 +09:00
|
|
|
}
|