Handler.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. using Application.Abstractions.Cache;
  2. using Application.Abstractions.Channels;
  3. using Application.Abstractions.Data;
  4. using Application.Abstractions.Messaging;
  5. using Application.Helpers;
  6. using Microsoft.EntityFrameworkCore;
  7. namespace Application.Features.Api.Channel.GetLiveLeaderboard;
  8. internal sealed class Handler(IAppDbContext db, ICacheService cache) : IQueryHandler<Query, Response>
  9. {
  10. public async Task<Response> Handle(Query request, CancellationToken ct)
  11. {
  12. var cfg = await ChatExpConfigLoader.GetAsync(cache, db, ct);
  13. var maxRank = request.MaxRank ?? cfg.LeaderboardSize;
  14. var resolved = await ChannelIdentifierResolver.ResolveAsync(db, request.Identifier, ct);
  15. if (resolved.ChannelSID is null)
  16. {
  17. return new Response(null, [], 0, null);
  18. }
  19. var channelID = await db.Channel.AsNoTracking()
  20. .Where(c => c.SID == resolved.ChannelSID)
  21. .Select(c => (int?)c.ID)
  22. .FirstOrDefaultAsync(ct);
  23. if (channelID is null)
  24. {
  25. return new Response(null, [], 0, null);
  26. }
  27. // 현재 활성 방송 세션 (IsFinalized=false, CreatedAt desc)
  28. var session = await db.BroadcastSession.AsNoTracking()
  29. .Where(s => s.ChannelID == channelID.Value && !s.IsFinalized)
  30. .OrderByDescending(s => s.CreatedAt)
  31. .Select(s => new { s.ID })
  32. .FirstOrDefaultAsync(ct);
  33. if (session is null)
  34. {
  35. return new Response(null, [], 0, null);
  36. }
  37. // 방송 세션 단위 XP 집계
  38. var grouped = await db.MemberExpLog.AsNoTracking()
  39. .Where(e => e.BroadcastSessionID == session.ID)
  40. .GroupBy(e => e.MemberID)
  41. .Select(g => new { MemberID = g.Key, Xp = g.Sum(x => x.Amount) })
  42. .Where(x => x.Xp > 0)
  43. .OrderByDescending(x => x.Xp)
  44. .Take(maxRank)
  45. .ToListAsync(ct);
  46. var total = grouped.Count;
  47. if (total == 0)
  48. {
  49. return new Response(session.ID, [], 0, MyRank(null, 0));
  50. }
  51. var memberIDs = grouped.Select(g => g.MemberID).ToArray();
  52. // 멤버 정보 + 뱃지 조회 (칭호는 채널 문맥)
  53. var memberRows = await db.Member.AsNoTracking()
  54. .Where(m => memberIDs.Contains(m.ID))
  55. .Select(m => new
  56. {
  57. m.ID,
  58. m.SID,
  59. m.Name,
  60. m.Icon,
  61. GradeImage = m.MemberGrade != null ? m.MemberGrade.Image : null
  62. })
  63. .ToDictionaryAsync(x => x.ID, ct);
  64. var titleRows = await db.DonorTitleSelection.AsNoTracking()
  65. .Where(d => memberIDs.Contains(d.DonorMemberID)
  66. && d.ChannelID == channelID.Value
  67. && d.SelectedTitleID != null
  68. && d.SelectedTitle!.IsActive)
  69. .ToDictionaryAsync(d => d.DonorMemberID, d => d.SelectedTitle!.IconUrl, ct);
  70. var list = new List<LeaderboardItem>(grouped.Count);
  71. var rank = 0;
  72. foreach (var g in grouped)
  73. {
  74. rank++;
  75. if (!memberRows.TryGetValue(g.MemberID, out var m))
  76. {
  77. continue;
  78. }
  79. titleRows.TryGetValue(g.MemberID, out var titleIcon);
  80. list.Add(new LeaderboardItem(
  81. Rank: rank,
  82. MemberID: m.ID,
  83. MemberSID: m.SID,
  84. MemberName: m.Name ?? "익명",
  85. Xp: g.Xp,
  86. TitleIconUrl: titleIcon,
  87. GradeImageUrl: m.GradeImage,
  88. MemberIcon: m.Icon
  89. ));
  90. }
  91. MyRankItem? myRank = null;
  92. if (request.ViewerMemberID.HasValue)
  93. {
  94. var viewerID = request.ViewerMemberID.Value;
  95. var inTop = list.FirstOrDefault(x => x.MemberID == viewerID);
  96. if (inTop is not null)
  97. {
  98. myRank = new MyRankItem(inTop.Rank, inTop.Xp);
  99. }
  100. else
  101. {
  102. var myXp = await db.MemberExpLog.AsNoTracking()
  103. .Where(e => e.BroadcastSessionID == session.ID && e.MemberID == viewerID)
  104. .SumAsync(e => (int?)e.Amount, ct) ?? 0;
  105. if (myXp > 0)
  106. {
  107. var higher = await db.MemberExpLog.AsNoTracking()
  108. .Where(e => e.BroadcastSessionID == session.ID)
  109. .GroupBy(e => e.MemberID)
  110. .Select(g => g.Sum(x => x.Amount))
  111. .CountAsync(xp => xp > myXp, ct);
  112. myRank = new MyRankItem(higher + 1, myXp);
  113. }
  114. else
  115. {
  116. myRank = new MyRankItem(0, 0);
  117. }
  118. }
  119. }
  120. return new Response(session.ID, list, total, myRank);
  121. }
  122. private static MyRankItem? MyRank(MyRankItem? val, int fallback) => val ?? new MyRankItem(0, fallback);
  123. }