using Application.Abstractions.Data;
using Application.Abstractions.Messaging;
using Domain.Entities.Members;
using Microsoft.EntityFrameworkCore;
namespace Application.Features.Api.TrackRecord.GetLeaderboard;
///
/// 예측 적중률 리더보드 조회 (D2 M4). 필터 인덱스(IX_MemberTrackRecord_HitRate WHERE Predictions>=10)를 활용해
/// 채점 표본(Hits+Misses) 10+ 회원을 HitRate 내림차순으로 페이징한다. Redis 캐시는 v1 미사용(필터 인덱스로 충분).
///
internal sealed class Handler(IAppDbContext db) : IQueryHandler
{
private const int PageSize = 50;
/// 리더보드 게이트 — 채점 표본(Hits+Misses) 최소치 (d2 §③ 표본 10+).
private const int MinSample = TrackRecordTiers.MinSample;
public async Task Handle(Query request, CancellationToken ct)
{
var page = request.Page < 1 ? 1 : request.Page;
// 표본(채점 완료) = Hits + Misses. 필터 인덱스는 Predictions>=10 이지만 Void 가 껴 실제 표본이 10 미만일 수
// 있어 Hits+Misses>=10 을 명시적으로 재확인한다(정확성 우선, 인덱스는 후보 축소).
var query = db.MemberTrackRecord.AsNoTracking().Where(c => c.Hits + c.Misses >= MinSample);
var total = await query.CountAsync(ct);
var rows = await query
.OrderByDescending(c => c.HitRate).ThenByDescending(c => c.Hits + c.Misses).ThenBy(c => c.MemberID)
.Skip((page - 1) * PageSize)
.Take(PageSize)
.Select(c => new
{
c.Member.SID,
c.Member.Name,
c.Tier,
c.HitRate,
c.Predictions,
c.Hits,
c.Misses,
c.BestStreak
})
.ToListAsync(ct);
var baseRank = (page - 1) * PageSize;
return new Response
{
Total = total,
Period = "all",
List = [..rows.Select((c, i) => new Response.Row
{
Rank = baseRank + i + 1,
SID = c.SID,
Name = c.Name ?? string.Empty,
Tier = c.Tier.ToString(),
TierCode = (byte)c.Tier,
HitRate = c.HitRate,
Predictions = c.Predictions,
Hits = c.Hits,
Misses = c.Misses,
BestStreak = c.BestStreak
})]
};
}
}