관리자 조회 API 추가(경기별 투표·이메일 랭킹, 토큰 인증) + ops/ gitignore
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>develop
parent
e1fb1f6b1d
commit
c73a9ad9f9
|
|
@ -45,3 +45,6 @@ serviceAccount.json
|
|||
|
||||
# 로컬 전용 댓글 관리 도구
|
||||
tp-comment-admin/
|
||||
|
||||
# 개인 운영 대시보드 (관리자 토큰 포함 — 커밋 금지)
|
||||
ops/
|
||||
|
|
|
|||
|
|
@ -132,3 +132,72 @@ async def recount_crowd(db: AsyncSession = Depends(get_db)) -> GenericOk:
|
|||
fixed += 1
|
||||
await db.commit()
|
||||
return GenericOk(ok=True, detail="crowd recounted", extra={"matchesFixed": fixed})
|
||||
|
||||
|
||||
# ── 운영 대시보드용 읽기 API (개인 관리용 — 이메일 원본 노출 주의) ──
|
||||
@router.get("/matches/{match_id}/votes", dependencies=[Depends(require_admin)])
|
||||
async def match_votes(
|
||||
match_id: str, db: AsyncSession = Depends(get_db)
|
||||
) -> list[dict]:
|
||||
"""경기별 투표 전체 — 누가(이메일 원본) 어떻게 찍었고 몇 점 받았는지."""
|
||||
rows = (
|
||||
await db.execute(
|
||||
select(UserPrediction)
|
||||
.where(UserPrediction.match_id == match_id)
|
||||
.order_by(UserPrediction.created_at.desc())
|
||||
)
|
||||
).scalars().all()
|
||||
return [
|
||||
{
|
||||
"email": r.email,
|
||||
"outcome": r.outcome,
|
||||
"scoreA": r.score_a,
|
||||
"scoreB": r.score_b,
|
||||
"points": r.points,
|
||||
"notify": r.notify,
|
||||
"createdAt": r.created_at.isoformat() if r.created_at else None,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
@router.get("/leaderboard", dependencies=[Depends(require_admin)])
|
||||
async def full_leaderboard(
|
||||
league: str = "", db: AsyncSession = Depends(get_db)
|
||||
) -> list[dict]:
|
||||
"""이메일 원본 랭킹 — 공개 리더보드와 같은 집계(채점된 픽 재집계).
|
||||
배제 대상(운영진 도메인)도 표시하되 excluded 플래그로 구분."""
|
||||
from ..scoring import is_excluded
|
||||
|
||||
q = (
|
||||
select(UserPrediction, Match)
|
||||
.join(Match, Match.match_id == UserPrediction.match_id)
|
||||
.where(
|
||||
Match.result_outcome.is_not(None),
|
||||
UserPrediction.points.is_not(None),
|
||||
UserPrediction.email.is_not(None),
|
||||
)
|
||||
)
|
||||
if league:
|
||||
q = q.where(Match.league == league)
|
||||
picks = (await db.execute(q)).all()
|
||||
agg: dict[str, list[int]] = {}
|
||||
for pk, m in picks:
|
||||
row = agg.setdefault(pk.email, [0, 0, 0])
|
||||
row[0] += pk.points or 0
|
||||
row[1] += 1 if (
|
||||
pk.score_a == m.result_score_a and pk.score_b == m.result_score_b
|
||||
) else 0
|
||||
row[2] += 1
|
||||
ranked = sorted(agg.items(), key=lambda kv: (-kv[1][0], -kv[1][1], -kv[1][2]))
|
||||
return [
|
||||
{
|
||||
"rank": i + 1,
|
||||
"email": e,
|
||||
"totalPoints": v[0],
|
||||
"exactCount": v[1],
|
||||
"matchesPlayed": v[2],
|
||||
"excluded": is_excluded(e),
|
||||
}
|
||||
for i, (e, v) in enumerate(ranked)
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue