feat: 관리자 대시보드 — 토큰·비용·응답시간 집계 API + 앱 /admin 페이지
- backend apps/stats: GET /api/v1/admin/stats?from&to → totals / byUser / byDay. ChatMessage(assistant) 집계만, is_superuser 아니면 403. 테스트 3개 - frontend features/admin: 요약 카드 4개(요청·토큰·비용·평균 응답), 일별 토큰 막대(div), 사용자별 표, 기간 프리셋(오늘/7/30/90일). 헤더에 ADMIN 만 아이콘. 테스트 2개 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
aa37cef13d
commit
d4a5f20787
@@ -0,0 +1,60 @@
|
||||
"""GET /api/v1/admin/stats — 관리자만, 토큰·비용·시간 집계."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from django.utils import timezone
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.chat.models import ChatMessage, ChatSession
|
||||
|
||||
|
||||
def _msg(session, role="assistant", days_ago=0, **kw):
|
||||
m = ChatMessage.objects.create(session=session, role=role, content="x", **kw)
|
||||
if days_ago:
|
||||
ChatMessage.objects.filter(id=m.id).update(created_at=timezone.now() - timedelta(days=days_ago))
|
||||
return m
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_api(db, django_user_model):
|
||||
admin = django_user_model.objects.create_superuser(username="a@x.com", email="a@x.com", password="x")
|
||||
c = APIClient()
|
||||
c.force_authenticate(admin)
|
||||
return c, admin
|
||||
|
||||
|
||||
def test_non_admin_forbidden(auth_api):
|
||||
res = auth_api.get("/api/v1/admin/stats")
|
||||
assert res.status_code == 403 and res.json()["code"] == "FORBIDDEN"
|
||||
|
||||
|
||||
def test_totals_by_user_by_day(admin_api, user):
|
||||
api, admin = admin_api
|
||||
s1 = ChatSession.objects.create(id="s1", user=user)
|
||||
s2 = ChatSession.objects.create(id="s2", user=admin)
|
||||
_msg(s1, role="user") # user 행은 집계 제외
|
||||
_msg(s1, input_tokens=100, output_tokens=50, cost_usd=0.01, elapsed_ms=1000)
|
||||
_msg(s1, input_tokens=200, output_tokens=100, cost_usd=0.02, elapsed_ms=3000, days_ago=1)
|
||||
_msg(s2, input_tokens=10, output_tokens=5, cost_usd=0.001, elapsed_ms=500)
|
||||
_msg(s2, input_tokens=999, output_tokens=999, cost_usd=9.0, elapsed_ms=9, days_ago=40) # 기간 밖
|
||||
|
||||
data = api.get("/api/v1/admin/stats").json()["data"]
|
||||
t = data["totals"]
|
||||
assert t["requests"] == 3 and t["inputTokens"] == 310 and t["outputTokens"] == 155 and t["totalTokens"] == 465
|
||||
assert t["costUsd"] == pytest.approx(0.031) and t["avgElapsedMs"] == 1500
|
||||
assert t["users"] == 2 and t["sessions"] == 2
|
||||
|
||||
by_user = {r["email"]: r for r in data["byUser"]}
|
||||
assert by_user[user.email]["requests"] == 2 and by_user[user.email]["totalTokens"] == 450
|
||||
assert by_user[user.email]["sessions"] == 1 and by_user[user.email]["avgElapsedMs"] == 2000
|
||||
assert data["byUser"][0]["email"] == user.email # 비용 큰 순
|
||||
assert len(data["byDay"]) == 2 and sum(r["requests"] for r in data["byDay"]) == 3
|
||||
|
||||
|
||||
def test_range_validation(admin_api):
|
||||
api, _ = admin_api
|
||||
assert api.get("/api/v1/admin/stats?from=2026-13-01").status_code == 400
|
||||
assert api.get("/api/v1/admin/stats?from=2026-09-10&to=2026-09-01").status_code == 400
|
||||
res = api.get("/api/v1/admin/stats?from=2026-09-01&to=2026-09-02")
|
||||
assert res.status_code == 200 and res.json()["data"]["from"] == "2026-09-01"
|
||||
Reference in New Issue
Block a user