using Application.Abstractions.Data; using Domain.Entities.Donations.ValueObject; using Web.Api.Common; using Microsoft.EntityFrameworkCore; namespace Web.Api.Endpoints.Widget; /// WidgetToken + configID 기반 크루 리더보드 (위젯 초기 로드용, 인증 불필요). /// configID로 CrewWidgetConfig → Crew 식별 → CrewMember 기반 base ranking + CrewDonationSummary로 수치 갱신. /// 후원 없는 크루원도 0원으로 표시. internal sealed class CrewRankingByToken : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) { app.MapGet("api/widget/crew/by-token/{widgetToken}", async ( string widgetToken, int? configID, IAppDbContext db, CancellationToken ct ) => { var channelID = await db.Channel.AsNoTracking() .Where(c => c.WidgetToken == widgetToken && c.IsActive) .Select(c => c.ID) .FirstOrDefaultAsync(ct); if (channelID == 0) { return ApiResponse.Ok(new { list = Array.Empty(), totalAmount = 0 }); } // CrewWidgetConfig 조회 var widgetCfgQuery = db.CrewWidgetConfig.AsNoTracking() .Where(w => w.ChannelID == channelID); if (configID.HasValue) { widgetCfgQuery = widgetCfgQuery.Where(w => w.ID == configID.Value); } else { widgetCfgQuery = widgetCfgQuery.Where(w => w.IsActive); } var widgetCfg = await widgetCfgQuery .OrderByDescending(w => w.ID) .Select(w => new { w.CrewID, w.MaxDisplayCount }) .FirstOrDefaultAsync(ct); if (widgetCfg is null) { return ApiResponse.Ok(new { list = Array.Empty(), totalAmount = 0 }); } // Base: 크루의 모든 active 크루원 (Channel.ThumbnailUrl 우선) var baseMembers = await db.CrewMember.AsNoTracking() .Where(m => m.CrewID == widgetCfg.CrewID && m.IsActive) .OrderBy(m => m.SortOrder).ThenBy(m => m.JoinedAt) .Select(m => new { ID = m.ID, Nickname = m.Nickname, Icon = m.Channel != null && m.Channel.ThumbnailUrl != null ? m.Channel.ThumbnailUrl : (m.Member != null ? m.Member.Thumb : null), ChannelName = m.Channel != null ? m.Channel.Name : null }) .ToListAsync(ct); // 가장 최근 CrewSession (active 우선, 없으면 ended) var sessionID = await db.CrewSession.AsNoTracking() .Where(s => s.CrewID == widgetCfg.CrewID && s.Status == CrewSessionStatus.Active) .OrderByDescending(s => s.CreatedAt) .Select(s => (int?)s.ID) .FirstOrDefaultAsync(ct); if (sessionID is null) { sessionID = await db.CrewSession.AsNoTracking() .Where(s => s.CrewID == widgetCfg.CrewID) .OrderByDescending(s => s.CreatedAt) .Select(s => (int?)s.ID) .FirstOrDefaultAsync(ct); } // CrewDonationSummary 데이터 (있으면) var summaryDict = sessionID.HasValue ? (await db.CrewDonationSummary.AsNoTracking() .Where(s => s.CrewSessionID == sessionID.Value) .Select(s => new { s.CrewMemberID, s.TotalAmount, s.DonationCount, s.ContributionRate }) .ToListAsync(ct)).ToDictionary(s => s.CrewMemberID) : new(); var sessionTotal = sessionID.HasValue ? await db.CrewSession.AsNoTracking() .Where(s => s.ID == sessionID.Value) .Select(s => s.TotalAmount) .FirstOrDefaultAsync(ct) : 0; // base + summary merge var merged = baseMembers.Select(m => { var s = summaryDict.GetValueOrDefault(m.ID); return new { crewMemberID = m.ID, nickname = m.Nickname, icon = m.Icon, channelName = m.ChannelName, totalAmount = s?.TotalAmount ?? 0, donationCount = s?.DonationCount ?? 0, contributionRate = s?.ContributionRate ?? 0m }; }) .OrderByDescending(x => x.totalAmount) .Take(widgetCfg.MaxDisplayCount) .ToList(); var list = merged.Select((x, i) => new { rank = i + 1, crewMemberID = x.crewMemberID, nickname = x.nickname, icon = x.icon, channelName = x.channelName, totalAmount = x.totalAmount, donationCount = x.donationCount, contributionRate = x.contributionRate }).ToList(); return ApiResponse.Ok(new { list, totalAmount = sessionTotal }); }) .WithTags("Widget") .AllowAnonymous(); } }