Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
275 lines
9.3 KiB
C#
275 lines
9.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using D3ROVoice.Api.Data;
|
|
using D3ROVoice.Api.Dtos;
|
|
using D3ROVoice.Api.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace D3ROVoice.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class AdminController : ControllerBase
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly ISttProxyService _sttService;
|
|
private static readonly DateTime _serverStartTime = DateTime.UtcNow;
|
|
|
|
public AdminController(AppDbContext db, ISttProxyService sttService)
|
|
{
|
|
_db = db;
|
|
_sttService = sttService;
|
|
}
|
|
|
|
[HttpGet("stats")]
|
|
public async Task<IActionResult> GetStats()
|
|
{
|
|
var totalUsers = await _db.Users.CountAsync();
|
|
var today = DateTime.UtcNow.Date;
|
|
var activeUsersToday = await _db.Users.CountAsync(u => u.LastLoginAt >= today);
|
|
var totalRequests = await _db.UsageLogs.CountAsync() + await _db.SttUsageLogs.CountAsync();
|
|
var totalLlmCost = await _db.UsageLogs.SumAsync(u => (decimal?)u.CalculatedCost) ?? 0m;
|
|
var totalSttCost = await _db.SttUsageLogs.SumAsync(u => (decimal?)u.CalculatedCost) ?? 0m;
|
|
var totalCost = totalLlmCost + totalSttCost;
|
|
var errorCount = await _db.ErrorLogs.CountAsync();
|
|
|
|
var recentErrors = await _db.ErrorLogs
|
|
.OrderByDescending(e => e.CreatedAt)
|
|
.Take(10)
|
|
.Select(e => new ServerErrorLogDto(e.Id, e.ErrorType, e.Message, e.Endpoint, e.CreatedAt))
|
|
.ToListAsync();
|
|
|
|
var uptime = (DateTime.UtcNow - _serverStartTime).TotalSeconds;
|
|
|
|
var stats = new ServerStatsDto(
|
|
totalUsers,
|
|
activeUsersToday,
|
|
totalRequests,
|
|
totalCost,
|
|
uptime,
|
|
errorCount,
|
|
recentErrors
|
|
);
|
|
|
|
return Ok(stats);
|
|
}
|
|
|
|
[HttpGet("users")]
|
|
public async Task<IActionResult> GetUsers()
|
|
{
|
|
var users = await _db.Users
|
|
.OrderByDescending(u => u.CreatedAt)
|
|
.Select(u => new UserInfoDto(u.Id, u.Email, u.Role, u.CreatedAt, u.LastLoginAt, u.IsActive))
|
|
.ToListAsync();
|
|
|
|
return Ok(users);
|
|
}
|
|
|
|
// ── LLM Model Endpoints ───────────────────────────────────────────────
|
|
|
|
[HttpGet("endpoints")]
|
|
public async Task<IActionResult> GetEndpoints()
|
|
{
|
|
var endpoints = await _db.ModelEndpoints
|
|
.OrderBy(m => m.Id)
|
|
.ToListAsync();
|
|
|
|
return Ok(endpoints);
|
|
}
|
|
|
|
[HttpPost("endpoints")]
|
|
public async Task<IActionResult> CreateEndpoint([FromBody] CreateModelEndpointDto dto)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dto.ModelId) || string.IsNullOrWhiteSpace(dto.ModelName))
|
|
{
|
|
return BadRequest(new { message = "ModelId와 ModelName은 필수 항목입니다." });
|
|
}
|
|
|
|
var existing = await _db.ModelEndpoints.FirstOrDefaultAsync(m => m.ModelId == dto.ModelId);
|
|
if (existing != null)
|
|
{
|
|
return Conflict(new { message = "이미 존재하는 ModelId입니다." });
|
|
}
|
|
|
|
var endpoint = new ServiceModelEndpoint
|
|
{
|
|
ModelId = dto.ModelId.Trim(),
|
|
ModelName = dto.ModelName.Trim(),
|
|
Provider = dto.Provider.Trim(),
|
|
EndpointUrl = dto.EndpointUrl.Trim(),
|
|
ApiKey = dto.ApiKey ?? "",
|
|
CostPer1kPromptTokens = dto.CostPer1kPromptTokens,
|
|
CostPer1kCompletionTokens = dto.CostPer1kCompletionTokens,
|
|
IsActive = true,
|
|
CreatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_db.ModelEndpoints.Add(endpoint);
|
|
await _db.SaveChangesAsync();
|
|
|
|
return Ok(endpoint);
|
|
}
|
|
|
|
[HttpPut("endpoints/{id}")]
|
|
public async Task<IActionResult> UpdateEndpoint(int id, [FromBody] UpdateModelEndpointDto dto)
|
|
{
|
|
var endpoint = await _db.ModelEndpoints.FindAsync(id);
|
|
if (endpoint == null) return NotFound();
|
|
|
|
endpoint.ModelName = dto.ModelName.Trim();
|
|
endpoint.Provider = dto.Provider.Trim();
|
|
endpoint.EndpointUrl = dto.EndpointUrl.Trim();
|
|
endpoint.ApiKey = dto.ApiKey ?? "";
|
|
endpoint.CostPer1kPromptTokens = dto.CostPer1kPromptTokens;
|
|
endpoint.CostPer1kCompletionTokens = dto.CostPer1kCompletionTokens;
|
|
endpoint.IsActive = dto.IsActive;
|
|
|
|
await _db.SaveChangesAsync();
|
|
return Ok(endpoint);
|
|
}
|
|
|
|
[HttpDelete("endpoints/{id}")]
|
|
public async Task<IActionResult> DeleteEndpoint(int id)
|
|
{
|
|
var endpoint = await _db.ModelEndpoints.FindAsync(id);
|
|
if (endpoint == null) return NotFound();
|
|
|
|
_db.ModelEndpoints.Remove(endpoint);
|
|
await _db.SaveChangesAsync();
|
|
|
|
return Ok(new { message = "삭제되었습니다." });
|
|
}
|
|
|
|
// ── STT / Transcription Provider Endpoints ────────────────────────────
|
|
|
|
[HttpGet("stt-endpoints")]
|
|
public async Task<IActionResult> GetSttEndpoints()
|
|
{
|
|
var endpoints = await _sttService.GetAllEndpointsAsync();
|
|
return Ok(endpoints);
|
|
}
|
|
|
|
[HttpGet("stt-endpoints/{id}")]
|
|
public async Task<IActionResult> GetSttEndpoint(int id)
|
|
{
|
|
var endpoints = await _sttService.GetAllEndpointsAsync();
|
|
var endpoint = endpoints.FirstOrDefault(e => e.Id == id);
|
|
if (endpoint == null) return NotFound(new { message = $"STT Endpoint {id} not found." });
|
|
return Ok(endpoint);
|
|
}
|
|
|
|
[HttpPost("stt-endpoints")]
|
|
public async Task<IActionResult> CreateSttEndpoint([FromBody] CreateSttEndpointDto dto)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dto.Name) || string.IsNullOrWhiteSpace(dto.EndpointUrl))
|
|
{
|
|
return BadRequest(new { message = "이름과 Endpoint URL은 필수입니다." });
|
|
}
|
|
|
|
var endpoint = await _sttService.CreateEndpointAsync(dto);
|
|
return Ok(endpoint);
|
|
}
|
|
|
|
[HttpPut("stt-endpoints/{id}")]
|
|
public async Task<IActionResult> UpdateSttEndpoint(int id, [FromBody] UpdateSttEndpointDto dto)
|
|
{
|
|
try
|
|
{
|
|
var endpoint = await _sttService.UpdateEndpointAsync(id, dto);
|
|
return Ok(endpoint);
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
return NotFound(new { message = $"STT Endpoint {id} not found." });
|
|
}
|
|
}
|
|
|
|
[HttpDelete("stt-endpoints/{id}")]
|
|
public async Task<IActionResult> DeleteSttEndpoint(int id)
|
|
{
|
|
var deleted = await _sttService.DeleteEndpointAsync(id);
|
|
if (!deleted) return NotFound();
|
|
return Ok(new { message = "STT 엔드포인트가 성공적으로 삭제되었습니다." });
|
|
}
|
|
|
|
[HttpPost("stt-endpoints/{id}/set-default")]
|
|
public async Task<IActionResult> SetDefaultSttEndpoint(int id)
|
|
{
|
|
var success = await _sttService.SetDefaultEndpointAsync(id);
|
|
if (!success) return NotFound();
|
|
return Ok(new { id, isDefault = true, success = true, message = "기본 클라우드 전사 프로바이더로 설정되었습니다." });
|
|
}
|
|
|
|
[HttpPost("stt-endpoints/{id}/test")]
|
|
public async Task<IActionResult> TestSttEndpoint(int id)
|
|
{
|
|
var result = await _sttService.TestEndpointAsync(id);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpPost("stt-endpoints/test-direct")]
|
|
public async Task<IActionResult> TestSttDirect([FromQuery] string endpointUrl, [FromQuery] string? apiKey)
|
|
{
|
|
var result = await _sttService.TestEndpointAsync(0, apiKey, endpointUrl);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("stt-usage")]
|
|
public async Task<IActionResult> GetSttUsageReport()
|
|
{
|
|
var report = await _sttService.GetUsageReportAsync();
|
|
return Ok(report);
|
|
}
|
|
|
|
// ── LLM Usage Report ──────────────────────────────────────────────────
|
|
|
|
[HttpGet("usage")]
|
|
public async Task<IActionResult> GetUsageReport()
|
|
{
|
|
var logs = await _db.UsageLogs.AsNoTracking().ToListAsync();
|
|
|
|
var totalRequests = logs.Count;
|
|
var totalPromptTokens = logs.Sum(l => l.PromptTokens);
|
|
var totalCompletionTokens = logs.Sum(l => l.CompletionTokens);
|
|
var totalCost = logs.Sum(l => l.CalculatedCost);
|
|
|
|
var userSummaries = logs
|
|
.GroupBy(l => new { l.UserId, l.UserEmail })
|
|
.Select(g => new UserUsageSummaryDto(
|
|
g.Key.UserId,
|
|
g.Key.UserEmail,
|
|
g.Count(),
|
|
g.Sum(x => x.TotalTokens),
|
|
g.Sum(x => x.CalculatedCost)
|
|
))
|
|
.OrderByDescending(u => u.TotalCost)
|
|
.ToList();
|
|
|
|
var modelSummaries = logs
|
|
.GroupBy(l => l.ModelId)
|
|
.Select(g => new ModelUsageSummaryDto(
|
|
g.Key,
|
|
g.Key,
|
|
g.Count(),
|
|
g.Sum(x => x.TotalTokens),
|
|
g.Sum(x => x.CalculatedCost)
|
|
))
|
|
.OrderByDescending(m => m.TotalCost)
|
|
.ToList();
|
|
|
|
var report = new UsageReportDto(
|
|
totalRequests,
|
|
totalPromptTokens,
|
|
totalCompletionTokens,
|
|
totalCost,
|
|
userSummaries,
|
|
modelSummaries
|
|
);
|
|
|
|
return Ok(report);
|
|
}
|
|
}
|