using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Ranking.GetRising; /// /// 최근 7일 vs 이전 7일 후원 증가율 기준 급상승 크리에이터. /// 집계는 실시간 Donation 테이블로 계산 (최근성이 중요하므로 스냅샷 아님). /// internal sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { var limit = request.Limit is < 1 or > 100 ? 20 : request.Limit; var now = DateTime.UtcNow; var recent7End = now; var recent7Start = now.AddDays(-7); var prev7Start = now.AddDays(-14); // 최근 7일 집계 var recent = await db.Donation.AsNoTracking() .Where(d => d.CreatedAt >= recent7Start && d.CreatedAt < recent7End && !d.IsTest) .GroupBy(d => d.ReceiverMemberID) .Select(g => new { MemberID = g.Key, Amount = g.Sum(d => (long)d.NetAmount), DonorCount = g.Select(d => d.SponsorMemberID).Distinct().Count() }) .ToListAsync(ct); if (recent.Count == 0) { return new Response([], now); } // 이전 7일 집계 var memberIds = recent.Select(r => r.MemberID).ToList(); var previous = await db.Donation.AsNoTracking() .Where(d => d.CreatedAt >= prev7Start && d.CreatedAt < recent7Start && !d.IsTest && memberIds.Contains(d.ReceiverMemberID)) .GroupBy(d => d.ReceiverMemberID) .Select(g => new { MemberID = g.Key, Amount = g.Sum(d => (long)d.NetAmount) }) .ToDictionaryAsync(g => g.MemberID, g => g.Amount, ct); var rising = recent.Select(r => { var prevAmount = previous.GetValueOrDefault(r.MemberID); double rate; if (prevAmount <= 0) { rate = r.Amount > 0 ? 1000.0 : 0.0; // 신규 유입 — 1000% 취급 } else { rate = ((double)(r.Amount - prevAmount) / prevAmount) * 100.0; } return new { r.MemberID, r.Amount, r.DonorCount, PreviousAmount = prevAmount, Rate = rate }; }) .Where(r => r.Rate > 0) .OrderByDescending(r => r.Rate).ThenByDescending(r => r.Amount) .Take(limit) .ToList(); var memberInfo = await db.Member.AsNoTracking() .Where(m => rising.Select(r => r.MemberID).Contains(m.ID)) .Select(m => new { m.ID, m.Name, m.Thumb }) .ToListAsync(ct); var memberDict = memberInfo.ToDictionary(m => m.ID, m => m); var channels = await db.Channel.AsNoTracking() .Where(c => rising.Select(r => r.MemberID).Contains(c.MemberID)) .Select(c => new { c.MemberID, c.Name, c.ThumbnailUrl }) .ToListAsync(ct); var channelDict = channels.GroupBy(c => c.MemberID).ToDictionary(g => g.Key, g => g.First()); var entries = rising.Select(r => { var m = memberDict.GetValueOrDefault(r.MemberID); var ch = channelDict.GetValueOrDefault(r.MemberID); // Member.Thumb 우선, 없으면 Channel.ThumbnailUrl(YouTube 채널 썸네일) 폴백 return new RisingEntry( r.MemberID, m?.Name ?? "", m?.Thumb ?? ch?.ThumbnailUrl, ch?.Name, r.Amount, r.PreviousAmount, r.Rate, r.DonorCount ); }).ToList(); return new Response(entries, now); } }