using Application.Abstractions.Cache; using Application.Abstractions.Channels; using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Helpers; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Channel.GetLiveLeaderboard; internal sealed class Handler(IAppDbContext db, ICacheService cache) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { var cfg = await ChatExpConfigLoader.GetAsync(cache, db, ct); var maxRank = request.MaxRank ?? cfg.LeaderboardSize; var resolved = await ChannelIdentifierResolver.ResolveAsync(db, request.Identifier, ct); if (resolved.ChannelSID is null) { return new Response(null, [], 0, null); } var channelID = await db.Channel.AsNoTracking() .Where(c => c.SID == resolved.ChannelSID) .Select(c => (int?)c.ID) .FirstOrDefaultAsync(ct); if (channelID is null) { return new Response(null, [], 0, null); } // 현재 활성 방송 세션 (IsFinalized=false, CreatedAt desc) var session = await db.BroadcastSession.AsNoTracking() .Where(s => s.ChannelID == channelID.Value && !s.IsFinalized) .OrderByDescending(s => s.CreatedAt) .Select(s => new { s.ID }) .FirstOrDefaultAsync(ct); if (session is null) { return new Response(null, [], 0, null); } // 방송 세션 단위 XP 집계 var grouped = await db.MemberExpLog.AsNoTracking() .Where(e => e.BroadcastSessionID == session.ID) .GroupBy(e => e.MemberID) .Select(g => new { MemberID = g.Key, Xp = g.Sum(x => x.Amount) }) .Where(x => x.Xp > 0) .OrderByDescending(x => x.Xp) .Take(maxRank) .ToListAsync(ct); var total = grouped.Count; if (total == 0) { return new Response(session.ID, [], 0, MyRank(null, 0)); } var memberIDs = grouped.Select(g => g.MemberID).ToArray(); // 멤버 정보 + 뱃지 조회 (칭호는 채널 문맥) var memberRows = await db.Member.AsNoTracking() .Where(m => memberIDs.Contains(m.ID)) .Select(m => new { m.ID, m.SID, m.Name, m.Icon, GradeImage = m.MemberGrade != null ? m.MemberGrade.Image : null }) .ToDictionaryAsync(x => x.ID, ct); var list = new List(grouped.Count); var rank = 0; foreach (var g in grouped) { rank++; if (!memberRows.TryGetValue(g.MemberID, out var m)) { continue; } list.Add(new LeaderboardItem( Rank: rank, MemberID: m.ID, MemberSID: m.SID, MemberName: m.Name ?? "익명", Xp: g.Xp, TitleIconUrl: null, GradeImageUrl: m.GradeImage, MemberIcon: m.Icon )); } MyRankItem? myRank = null; if (request.ViewerMemberID.HasValue) { var viewerID = request.ViewerMemberID.Value; var inTop = list.FirstOrDefault(x => x.MemberID == viewerID); if (inTop is not null) { myRank = new MyRankItem(inTop.Rank, inTop.Xp); } else { var myXp = await db.MemberExpLog.AsNoTracking() .Where(e => e.BroadcastSessionID == session.ID && e.MemberID == viewerID) .SumAsync(e => (int?)e.Amount, ct) ?? 0; if (myXp > 0) { var higher = await db.MemberExpLog.AsNoTracking() .Where(e => e.BroadcastSessionID == session.ID) .GroupBy(e => e.MemberID) .Select(g => g.Sum(x => x.Amount)) .CountAsync(xp => xp > myXp, ct); myRank = new MyRankItem(higher + 1, myXp); } else { myRank = new MyRankItem(0, 0); } } } return new Response(session.ID, list, total, myRank); } private static MyRankItem? MyRank(MyRankItem? val, int fallback) => val ?? new MyRankItem(0, fallback); }