using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Donations.ValueObject; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Studio.Dashboard.GetDashboard; internal sealed class Handler(IAppDbContext db) : IQueryHandler { private static readonly Response Empty = new(null, null, new FinancialSummary(0, 0, 0, 0, 0), [], [], [], []); public async Task Handle(Query request, CancellationToken ct) { // ── 채널 정보 ── var channel = await db.Channel.AsNoTracking() .Where(c => c.MemberID == request.MemberID && c.IsActive) .Select(c => new { c.ID, c.SID, c.Name, c.ThumbnailUrl, c.IsVerified, c.SubscriberCount, c.WidgetToken }) .FirstOrDefaultAsync(ct); if (channel is null) { return Empty; } // ── 위젯 URL ── var widgets = new WidgetUrls( channel.WidgetToken, $"/widget/alert/{channel.WidgetToken}", $"/widget/goal/{channel.WidgetToken}", $"/widget/rank/{channel.WidgetToken}", $"/widget/crew/{channel.WidgetToken}", $"/remote/{channel.WidgetToken}" ); var channelInfo = new ChannelInfo( channel.ID, channel.SID, channel.Name, channel.ThumbnailUrl, channel.IsVerified, channel.SubscriberCount ); // ── 재무 요약 ── var wallet = await db.Wallet.AsNoTracking() .Include(w => w.Balances) .FirstOrDefaultAsync(w => w.MemberID == request.MemberID, ct); var availableBalance = 0; if (wallet is not null) { var donationBal = wallet.Balances.FirstOrDefault(b => b.Type == Domain.Entities.Wallets.ValueObject.WalletBalanceType.Donation); if (donationBal is not null) { availableBalance = (int)donationBal.Amount.Value; } } var todayStart = DateTime.UtcNow.Date; var monthStart = new DateTime(todayStart.Year, todayStart.Month, 1, 0, 0, 0, DateTimeKind.Utc); var todayDonations = await db.Donation.AsNoTracking() .Where(d => d.ReceiverMemberID == request.MemberID && !d.IsTest && d.CreatedAt >= todayStart) .SumAsync(d => d.NetAmount, ct); var monthDonations = await db.Donation.AsNoTracking() .Where(d => d.ReceiverMemberID == request.MemberID && !d.IsTest && d.CreatedAt >= monthStart) .SumAsync(d => d.NetAmount, ct); var pendingWithdrawal = await db.WithdrawalRequest.AsNoTracking() .Where(w => w.MemberID == request.MemberID && (w.Status == WithdrawalStatus.Pending || w.Status == WithdrawalStatus.Processing)) .SumAsync(w => w.RequestedAmount, ct); // 이번 달 상점 판매 보상 합계 (OrderChannelReward 입금) var monthStoreSales = await db.WalletTransaction.AsNoTracking() .Where(t => t.Wallet!.MemberID == request.MemberID && t.BalanceType == Domain.Entities.Wallets.ValueObject.WalletBalanceType.StoreRevenue && t.TxType == Domain.Entities.Wallets.ValueObject.WalletTransactionType.OrderChannelReward && t.CreatedAt >= monthStart) .SumAsync(t => (int)t.Amount.Value, ct); var financial = new FinancialSummary(availableBalance, todayDonations, monthDonations, monthStoreSales, pendingWithdrawal); // ── 최근 후원 5건 ── var recentDonations = await db.Donation.AsNoTracking() .Where(d => d.ReceiverMemberID == request.MemberID && !d.IsTest) .OrderByDescending(d => d.CreatedAt) .Take(5) .Select(d => new RecentDonation(d.ID, d.SendName, d.Amount, d.Message, d.CreatedAt)) .ToListAsync(ct); // ── 활성 위젯 config 목록 (Goal/Rank — config별 별도 위젯 URL 노출용) ── var goalConfigs = await db.DonationGoalConfig.AsNoTracking() .Where(g => g.ChannelID == channel.ID && g.IsActive) .OrderByDescending(g => g.ID) .Select(g => new WidgetConfigItem(g.ID, g.Title, g.IsActive)) .ToListAsync(ct); var rankConfigs = await db.DonationRankConfig.AsNoTracking() .Where(r => r.ChannelID == channel.ID && r.IsActive) .OrderByDescending(r => r.ID) .Select(r => new WidgetConfigItem(r.ID, r.Title, r.IsActive)) .ToListAsync(ct); var crewConfigs = await db.CrewWidgetConfig.AsNoTracking() .Where(w => w.ChannelID == channel.ID && w.IsActive) .OrderByDescending(w => w.ID) .Select(w => new WidgetConfigItem( w.ID, w.Crew != null ? $"{w.Crew.Name} - {w.Title}" : w.Title, w.IsActive )) .ToListAsync(ct); return new Response(channelInfo, widgets, financial, recentDonations, goalConfigs, rankConfigs, crewConfigs); } }