using Application.Abstractions.Hub; using Infrastructure.Hubs; using Web.Api.Common; using Web.Api.Extensions; using MediatR; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using System.Security.Claims; namespace Web.Api.Endpoints.Donation; /// 후원 전송 internal sealed class Send : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) { /// 후원 전송 (지갑 차감 → Donation 생성 → Alert 생성 → OBS 위젯 브로드캐스트) app.MapPost("api/donation/send", async ( Application.Features.Api.Donation.Send.Command body, ClaimsPrincipal user, ISender sender, IHubContext appHub, IHubContext donationHub, Application.Abstractions.Data.IAppDbContext db, CancellationToken ct ) => { var memberID = user.GetRequiredMemberID(); var command = body with { SponsorMemberID = memberID }; var result = await sender.Send(command, ct); if (result.IsFailure) { return CustomResults.Problem(result); } var data = result.Value; // 채널 정보 (widgetToken, ChannelID, ChannelName) 조회 var channelID = body.ChannelID > 0 ? body.ChannelID : (body.ChannelSID != null ? await db.Channel.AsNoTracking().Where(c => c.SID == body.ChannelSID).Select(c => c.ID).FirstOrDefaultAsync(ct) : 0); var channelMeta = await db.Channel.AsNoTracking() .Where(c => c.ID == channelID) .Select(c => new { c.WidgetToken, c.Name }) .FirstOrDefaultAsync(ct); // 크루원 닉네임 조회 (크루 후원인 경우) string? crewMemberNickname = null; if (body.CrewMemberID.HasValue) { crewMemberNickname = await db.CrewMember.AsNoTracking() .Where(m => m.ID == body.CrewMemberID.Value) .Select(m => m.Nickname) .FirstOrDefaultAsync(ct); } // ── OBS 위젯 알림 브로드캐스트 (DonationHub.ReceiveAlert) ── if (channelMeta is not null && !string.IsNullOrEmpty(channelMeta.WidgetToken)) { var alertPayload = new { alertID = data.AlertID, donationID = data.DonationID, correlationID = data.CorrelationID, sponsorMemberID = memberID, sendName = body.SendName, amount = data.Amount, netAmount = data.NetAmount, message = body.Message, channelID = channelID, channelName = channelMeta.Name, crewMemberID = body.CrewMemberID, crewMemberNickname = crewMemberNickname, isTest = body.IsTest, createdAt = DateTime.UtcNow }; await donationHub.Clients.Group(channelMeta.WidgetToken).ReceiveAlert(alertPayload); // ⚠️ Goal/Rank/Crew broadcast는 알림 재생 종료 시점(DonationHub.AlertCompleted)에 처리됨 } // ── 크루 세션 후원인 경우 추가 브로드캐스트 ── if (body.CrewSessionID.HasValue && body.CrewMemberID.HasValue) { // 크루 순위 업데이트 → DonationHub (OBS 위젯) var rankResult = await sender.Send( new Application.Features.Api.Crew.GetCrewRanking.Query(body.CrewSessionID.Value), ct ); if (rankResult.IsSuccess && channelMeta is not null && !string.IsNullOrEmpty(channelMeta.WidgetToken)) { var rankData = rankResult.Value; await donationHub.Clients.Group(channelMeta.WidgetToken).ReceiveCrewUpdate(new { rankData.List, rankData.TotalAmount }); } // 크루원에게 토스트 알림 → AppHub var crewMember = await db.CrewMember.AsNoTracking() .Where(m => m.ID == body.CrewMemberID.Value).FirstOrDefaultAsync(ct); if (crewMember is not null) { await appHub.Clients.Group($"member:{crewMember.MemberID}").ReceiveCrewToast(new { SendName = body.SendName, Amount = body.Amount, Message = body.Message, CrewMemberNickname = crewMember.Nickname }); } } return ApiResponse.Ok(data); }) .WithTags("Donation") .RequireAuthorization(); } }