Send.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. using Application.Abstractions.Hub;
  2. using Infrastructure.Hubs;
  3. using Web.Api.Common;
  4. using Web.Api.Extensions;
  5. using MediatR;
  6. using Microsoft.AspNetCore.SignalR;
  7. using Microsoft.EntityFrameworkCore;
  8. using System.Security.Claims;
  9. namespace Web.Api.Endpoints.Donation;
  10. /// <summary>후원 전송</summary>
  11. internal sealed class Send : IEndpoint
  12. {
  13. public void MapEndpoint(IEndpointRouteBuilder app)
  14. {
  15. /// 후원 전송 (지갑 차감 → Donation 생성 → Alert 생성 → OBS 위젯 브로드캐스트)
  16. app.MapPost("api/donation/send", async (
  17. Application.Features.Api.Donation.Send.Command body,
  18. ClaimsPrincipal user,
  19. ISender sender,
  20. IHubContext<AppHub, IAppHubClient> appHub,
  21. IHubContext<DonationHub, IDonationHubClient> donationHub,
  22. Application.Abstractions.Data.IAppDbContext db,
  23. CancellationToken ct
  24. ) => {
  25. var memberID = user.GetRequiredMemberID();
  26. var command = body with { SponsorMemberID = memberID };
  27. var result = await sender.Send(command, ct);
  28. if (result.IsFailure)
  29. {
  30. return CustomResults.Problem(result);
  31. }
  32. var data = result.Value;
  33. // 채널 정보 (widgetToken, ChannelID, ChannelName) 조회
  34. var channelID = body.ChannelID > 0 ? body.ChannelID :
  35. (body.ChannelSID != null ?
  36. await db.Channel.AsNoTracking().Where(c => c.SID == body.ChannelSID).Select(c => c.ID).FirstOrDefaultAsync(ct) : 0);
  37. var channelMeta = await db.Channel.AsNoTracking()
  38. .Where(c => c.ID == channelID)
  39. .Select(c => new { c.WidgetToken, c.Name })
  40. .FirstOrDefaultAsync(ct);
  41. // 크루원 닉네임 조회 (크루 후원인 경우)
  42. string? crewMemberNickname = null;
  43. if (body.CrewMemberID.HasValue)
  44. {
  45. crewMemberNickname = await db.CrewMember.AsNoTracking()
  46. .Where(m => m.ID == body.CrewMemberID.Value)
  47. .Select(m => m.Nickname)
  48. .FirstOrDefaultAsync(ct);
  49. }
  50. // ── OBS 위젯 알림 브로드캐스트 (DonationHub.ReceiveAlert) ──
  51. if (channelMeta is not null && !string.IsNullOrEmpty(channelMeta.WidgetToken))
  52. {
  53. var alertPayload = new
  54. {
  55. alertID = data.AlertID,
  56. donationID = data.DonationID,
  57. correlationID = data.CorrelationID,
  58. sponsorMemberID = memberID,
  59. sendName = body.SendName,
  60. amount = data.Amount,
  61. netAmount = data.NetAmount,
  62. message = body.Message,
  63. channelID = channelID,
  64. channelName = channelMeta.Name,
  65. crewMemberID = body.CrewMemberID,
  66. crewMemberNickname = crewMemberNickname,
  67. isTest = body.IsTest,
  68. createdAt = DateTime.UtcNow
  69. };
  70. await donationHub.Clients.Group(channelMeta.WidgetToken).ReceiveAlert(alertPayload);
  71. // ⚠️ Goal/Rank/Crew broadcast는 알림 재생 종료 시점(DonationHub.AlertCompleted)에 처리됨
  72. }
  73. // ── 크루 세션 후원인 경우 추가 브로드캐스트 ──
  74. if (body.CrewSessionID.HasValue && body.CrewMemberID.HasValue)
  75. {
  76. // 크루 순위 업데이트 → DonationHub (OBS 위젯)
  77. var rankResult = await sender.Send(
  78. new Application.Features.Api.Crew.GetCrewRanking.Query(body.CrewSessionID.Value), ct
  79. );
  80. if (rankResult.IsSuccess && channelMeta is not null && !string.IsNullOrEmpty(channelMeta.WidgetToken))
  81. {
  82. var rankData = rankResult.Value;
  83. await donationHub.Clients.Group(channelMeta.WidgetToken).ReceiveCrewUpdate(new
  84. {
  85. rankData.List,
  86. rankData.TotalAmount
  87. });
  88. }
  89. // 크루원에게 토스트 알림 → AppHub
  90. var crewMember = await db.CrewMember.AsNoTracking()
  91. .Where(m => m.ID == body.CrewMemberID.Value).FirstOrDefaultAsync(ct);
  92. if (crewMember is not null)
  93. {
  94. await appHub.Clients.Group($"member:{crewMember.MemberID}").ReceiveCrewToast(new
  95. {
  96. SendName = body.SendName,
  97. Amount = body.Amount,
  98. Message = body.Message,
  99. CrewMemberNickname = crewMember.Nickname
  100. });
  101. }
  102. }
  103. return ApiResponse.Ok(data);
  104. })
  105. .WithTags("Donation")
  106. .RequireAuthorization();
  107. }
  108. }