| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- 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<Query, Response>
- {
- public async Task<Response> 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<LeaderboardItem>(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);
- }
|