using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Microsoft.EntityFrameworkCore; namespace Application.Features.Admin.Donation.Rank.Search; public sealed class Handler(IAppDbContext db) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { // 탭 → 기간 범위 (롤링 윈도우) var now = DateTime.UtcNow; var periodStart = request.Tab switch { 0 => now.AddHours(-24), // 오늘 1 => now.AddHours(-72), // 3일 2 => now.AddDays(-7), // 1주일 3 => now.AddDays(-30), // 1개월 4 => now.AddDays(-90), // 3개월 5 => now.AddDays(-180), // 6개월 6 => now.AddDays(-365), // 1년 _ => now.AddHours(-24) }; var donations = db.Donation.AsNoTracking().Where(d => d.CreatedAt >= periodStart && d.CreatedAt <= now); // 키워드 검색 (검색 창은 후원 내역과 동일) if (!string.IsNullOrWhiteSpace(request.Keyword)) { donations = request.Search switch { 1 => donations.Where(x => x.Sponsor != null && x.Sponsor.ID.ToString().Contains(request.Keyword)), 2 => donations.Where(x => x.Sponsor != null && x.Sponsor.SID.Contains(request.Keyword)), 3 => donations.Where(x => x.Sponsor != null && x.Sponsor.Name != null && x.Sponsor.Name.Contains(request.Keyword)), 4 => donations.Where(x => x.Receiver != null && x.Receiver.ID.ToString().Contains(request.Keyword)), 5 => donations.Where(x => x.Receiver != null && x.Receiver.SID.Contains(request.Keyword)), 6 => donations.Where(x => x.Receiver != null && x.Receiver.Name != null && x.Receiver.Name.Contains(request.Keyword)), 7 => donations.Where(x => x.SendName.Contains(request.Keyword)), _ => donations }; } // 기간 추가 필터 (StartAt/EndAt) — 탭 기간 내에서 추가로 좁히기 if (!string.IsNullOrWhiteSpace(request.StartAt) && DateTime.TryParse(request.StartAt, out var startAt)) { donations = donations.Where(x => x.CreatedAt >= startAt); } if (!string.IsNullOrWhiteSpace(request.EndAt) && DateTime.TryParse(request.EndAt, out var endAt)) { donations = donations.Where(x => x.CreatedAt <= endAt); } // 후원자별 집계 var grouped = donations.GroupBy(d => d.SponsorMemberID).Select(g => new { SponsorMemberID = g.Key, TotalAmount = g.Sum(x => (long)x.Amount), DonationCount = g.Count(), LastDonatedAt = g.Max(x => (DateTime?)x.CreatedAt) }); // 정렬 + 순위 grouped = request.Sort switch { 1 => grouped.OrderByDescending(x => x.TotalAmount).ThenByDescending(x => x.DonationCount), 2 => grouped.OrderBy(x => x.TotalAmount).ThenBy(x => x.DonationCount), 3 => grouped.OrderByDescending(x => x.DonationCount).ThenByDescending(x => x.TotalAmount), 4 => grouped.OrderBy(x => x.DonationCount).ThenBy(x => x.TotalAmount), _ => grouped.OrderByDescending(x => x.TotalAmount).ThenByDescending(x => x.DonationCount) }; var total = await grouped.CountAsync(ct); var skip = (request.Page - 1) * request.PerPage; var paged = await grouped.Skip(skip).Take(request.PerPage).ToListAsync(ct); // 마지막 피후원자 lookup — 후원자별 마지막 Donation의 Receiver var sponsorIDs = paged.Select(p => p.SponsorMemberID).ToArray(); var lastReceivers = await donations .Where(d => sponsorIDs.Contains(d.SponsorMemberID)) .GroupBy(d => d.SponsorMemberID) .Select(g => new { SponsorMemberID = g.Key, LastDonationID = g.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.ID).First().ID }) .ToListAsync(ct); var lastDonationIDs = lastReceivers.Select(r => r.LastDonationID).ToArray(); var lastDonations = await db.Donation.AsNoTracking() .Where(d => lastDonationIDs.Contains(d.ID)) .Include(d => d.Receiver) .Select(d => new { d.ID, d.SponsorMemberID, ReceiverID = d.Receiver != null ? d.Receiver.ID : (int?)null, ReceiverEmail = d.Receiver != null ? d.Receiver.Email : null, ReceiverName = d.Receiver != null ? d.Receiver.Name : null }) .ToListAsync(ct); var lastBySponsor = lastDonations.ToDictionary(x => x.SponsorMemberID); // Sponsor 정보 lookup var sponsors = await db.Member.AsNoTracking() .Where(m => sponsorIDs.Contains(m.ID)) .Select(m => new { m.ID, m.Email, m.Name }) .ToListAsync(ct); var sponsorByID = sponsors.ToDictionary(x => x.ID); // Rank 번호 — 현재 페이지에서 skip+1 부터 순서대로 var rows = paged.Select((x, idx) => { sponsorByID.TryGetValue(x.SponsorMemberID, out var sponsor); lastBySponsor.TryGetValue(x.SponsorMemberID, out var last); return new Response.Row { Num = total - skip - idx, Rank = skip + idx + 1, SponsorMemberID = x.SponsorMemberID, SponsorEmail = sponsor?.Email ?? string.Empty, SponsorName = sponsor?.Name, TotalAmount = x.TotalAmount, DonationCount = x.DonationCount, LastReceiverID = last?.ReceiverID, LastReceiverEmail = last?.ReceiverEmail, LastReceiverName = last?.ReceiverName, LastDonatedAt = x.LastDonatedAt }; }).ToList(); return new Response { Total = total, List = rows }; } }