관리자 조회 API 추가(경기별 투표·이메일 랭킹, 토큰 인증) + ops/ gitignore

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
develop
jwkim 2026-07-22 14:04:14 +09:00
parent e1fb1f6b1d
commit c73a9ad9f9
2 changed files with 72 additions and 0 deletions

3
.gitignore vendored
View File

@ -45,3 +45,6 @@ serviceAccount.json
# 로컬 전용 댓글 관리 도구 # 로컬 전용 댓글 관리 도구
tp-comment-admin/ tp-comment-admin/
# 개인 운영 대시보드 (관리자 토큰 포함 — 커밋 금지)
ops/

View File

@ -132,3 +132,72 @@ async def recount_crowd(db: AsyncSession = Depends(get_db)) -> GenericOk:
fixed += 1 fixed += 1
await db.commit() await db.commit()
return GenericOk(ok=True, detail="crowd recounted", extra={"matchesFixed": fixed}) 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)
]