CrewSessionEnd.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. using Application.Abstractions.Hub;
  2. using Application.Abstractions.Notification;
  3. using Domain.Entities.Notifications.ValueObject;
  4. using Infrastructure.Hubs;
  5. using Web.Api.Common;
  6. using MediatR;
  7. using Microsoft.AspNetCore.SignalR;
  8. namespace Web.Api.Endpoints.Donation;
  9. /// <summary>크루 방송 종료 — 정산 + 크루원에게 결과 쪽지/알림</summary>
  10. internal sealed class CrewSessionEnd : IEndpoint
  11. {
  12. public void MapEndpoint(IEndpointRouteBuilder app)
  13. {
  14. app.MapPost("api/crew/session/end", async (
  15. Application.Features.Api.Crew.EndSession.Command body,
  16. ISender sender,
  17. IHubContext<AppHub, IAppHubClient> appHub,
  18. INotificationService notificationService,
  19. Application.Abstractions.Data.IAppDbContext db,
  20. CancellationToken ct
  21. ) => {
  22. // 종료 전 세션 정보 미리 조회
  23. var session = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
  24. .FirstOrDefaultAsync(
  25. db.CrewSession.Where(s => s.ID == body.CrewSessionID), ct
  26. );
  27. if (session is null)
  28. {
  29. return CustomResults.Problem(SharedKernel.Results.Result.Failure(SharedKernel.Results.Error.NotFound("Session.NotFound", "세션을 찾을 수 없습니다.")));
  30. }
  31. var crew = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
  32. .FirstOrDefaultAsync(
  33. Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
  34. .AsNoTracking(db.Crew)
  35. .Where(c => c.ID == session.CrewID), ct
  36. );
  37. // 세션 종료 처리
  38. await sender.Send(body, ct);
  39. // 정산 결과 조회
  40. var summaries = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
  41. .ToListAsync(
  42. Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
  43. .AsNoTracking(db.CrewDonationSummary)
  44. .Where(s => s.CrewSessionID == body.CrewSessionID)
  45. .Join(db.CrewMember, s => s.CrewMemberID, m => m.ID, (s, m) => new {
  46. m.MemberID, m.Nickname, s.TotalAmount, s.DonationCount,
  47. s.ContributionRate, s.Rank
  48. })
  49. .OrderBy(x => x.Rank), ct
  50. );
  51. // 각 크루원에게 정산 알림 + 쪽지
  52. foreach (var summary in summaries)
  53. {
  54. var noteTitle = $"[{crew?.Name}] 크루 방송 정산 결과";
  55. var noteContent = $"방송: {session.Title}\n" +
  56. $"순위: {summary.Rank}위\n" +
  57. $"받은 후원: {summary.TotalAmount:N0}원 ({summary.DonationCount}건)\n" +
  58. $"기여율: {summary.ContributionRate:F1}%\n" +
  59. $"전체 후원: {session.TotalAmount:N0}원";
  60. // 시스템 쪽지 발송
  61. var note = Domain.Entities.Notes.Note.CreateSystem(
  62. summary.MemberID, noteTitle, noteContent,
  63. "CrewSession", body.CrewSessionID
  64. );
  65. db.Note.Add(note);
  66. // 알림 발송
  67. await notificationService.SendAsync(
  68. summary.MemberID,
  69. NotificationType.CrewEnded,
  70. "크루 방송 종료",
  71. $"'{session.Title}' 방송이 종료되었습니다. {summary.Rank}위, {summary.TotalAmount:N0}원",
  72. null, "CrewSession", body.CrewSessionID, null, ct
  73. );
  74. }
  75. await db.SaveChangesAsync(ct);
  76. // SignalR 브로드캐스트
  77. if (crew is not null)
  78. {
  79. var channel = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
  80. .FirstOrDefaultAsync(
  81. Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions
  82. .AsNoTracking(db.Channel)
  83. .Where(c => c.ID == crew.ChannelID), ct
  84. );
  85. if (channel is not null)
  86. {
  87. await appHub.Clients.Group($"channel:{channel.SID}").ReceiveCrewEnded(new
  88. {
  89. CrewSessionID = body.CrewSessionID,
  90. Title = session.Title,
  91. TotalAmount = session.TotalAmount,
  92. Summaries = summaries.Select(s => new {
  93. s.Nickname, s.Rank, s.TotalAmount, s.ContributionRate
  94. })
  95. });
  96. }
  97. }
  98. return ApiResponse.Ok();
  99. })
  100. .WithTags("Crew")
  101. .RequireAuthorization();
  102. }
  103. }