Handler.cs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  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 list = new List<LeaderboardItem>(grouped.Count);
  65. var rank = 0;
  66. foreach (var g in grouped)
  67. {
  68. rank++;
  69. if (!memberRows.TryGetValue(g.MemberID, out var m))
  70. {
  71. continue;
  72. }
  73. list.Add(new LeaderboardItem(
  74. Rank: rank,
  75. MemberID: m.ID,
  76. MemberSID: m.SID,
  77. MemberName: m.Name ?? "익명",
  78. Xp: g.Xp,
  79. TitleIconUrl: null,
  80. GradeImageUrl: m.GradeImage,
  81. MemberIcon: m.Icon
  82. ));
  83. }
  84. MyRankItem? myRank = null;
  85. if (request.ViewerMemberID.HasValue)
  86. {
  87. var viewerID = request.ViewerMemberID.Value;
  88. var inTop = list.FirstOrDefault(x => x.MemberID == viewerID);
  89. if (inTop is not null)
  90. {
  91. myRank = new MyRankItem(inTop.Rank, inTop.Xp);
  92. }
  93. else
  94. {
  95. var myXp = await db.MemberExpLog.AsNoTracking()
  96. .Where(e => e.BroadcastSessionID == session.ID && e.MemberID == viewerID)
  97. .SumAsync(e => (int?)e.Amount, ct) ?? 0;
  98. if (myXp > 0)
  99. {
  100. var higher = await db.MemberExpLog.AsNoTracking()
  101. .Where(e => e.BroadcastSessionID == session.ID)
  102. .GroupBy(e => e.MemberID)
  103. .Select(g => g.Sum(x => x.Amount))
  104. .CountAsync(xp => xp > myXp, ct);
  105. myRank = new MyRankItem(higher + 1, myXp);
  106. }
  107. else
  108. {
  109. myRank = new MyRankItem(0, 0);
  110. }
  111. }
  112. }
  113. return new Response(session.ID, list, total, myRank);
  114. }
  115. private static MyRankItem? MyRank(MyRankItem? val, int fallback) => val ?? new MyRankItem(0, fallback);
  116. }