using Application.Abstractions.Hub; using Application.Abstractions.Notification; using Domain.Entities.Notifications.ValueObject; using Infrastructure.Hubs; using Web.Api.Common; using MediatR; using Microsoft.AspNetCore.SignalR; namespace Web.Api.Endpoints.Donation; /// 크루 방송 종료 — 정산 + 크루원에게 결과 쪽지/알림 internal sealed class CrewSessionEnd : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("api/crew/session/end", async ( Application.Features.Api.Crew.EndSession.Command body, ISender sender, IHubContext appHub, INotificationService notificationService, Application.Abstractions.Data.IAppDbContext db, CancellationToken ct ) => { // 종료 전 세션 정보 미리 조회 var session = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions .FirstOrDefaultAsync( db.CrewSession.Where(s => s.ID == body.CrewSessionID), ct ); if (session is null) { return CustomResults.Problem(SharedKernel.Results.Result.Failure(SharedKernel.Results.Error.NotFound("Session.NotFound", "세션을 찾을 수 없습니다."))); } var crew = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions .FirstOrDefaultAsync( Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions .AsNoTracking(db.Crew) .Where(c => c.ID == session.CrewID), ct ); // 세션 종료 처리 await sender.Send(body, ct); // 정산 결과 조회 var summaries = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions .ToListAsync( Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions .AsNoTracking(db.CrewDonationSummary) .Where(s => s.CrewSessionID == body.CrewSessionID) .Join(db.CrewMember, s => s.CrewMemberID, m => m.ID, (s, m) => new { m.MemberID, m.Nickname, s.TotalAmount, s.DonationCount, s.ContributionRate, s.Rank }) .OrderBy(x => x.Rank), ct ); // 각 크루원에게 정산 알림 + 쪽지 foreach (var summary in summaries) { var noteTitle = $"[{crew?.Name}] 크루 방송 정산 결과"; var noteContent = $"방송: {session.Title}\n" + $"순위: {summary.Rank}위\n" + $"받은 후원: {summary.TotalAmount:N0}원 ({summary.DonationCount}건)\n" + $"기여율: {summary.ContributionRate:F1}%\n" + $"전체 후원: {session.TotalAmount:N0}원"; // 시스템 쪽지 발송 var note = Domain.Entities.Notes.Note.CreateSystem( summary.MemberID, noteTitle, noteContent, "CrewSession", body.CrewSessionID ); db.Note.Add(note); // 알림 발송 await notificationService.SendAsync( summary.MemberID, NotificationType.CrewEnded, "크루 방송 종료", $"'{session.Title}' 방송이 종료되었습니다. {summary.Rank}위, {summary.TotalAmount:N0}원", null, "CrewSession", body.CrewSessionID, null, ct ); } await db.SaveChangesAsync(ct); // SignalR 브로드캐스트 if (crew is not null) { var channel = await Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions .FirstOrDefaultAsync( Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions .AsNoTracking(db.Channel) .Where(c => c.ID == crew.ChannelID), ct ); if (channel is not null) { await appHub.Clients.Group($"channel:{channel.SID}").ReceiveCrewEnded(new { CrewSessionID = body.CrewSessionID, Title = session.Title, TotalAmount = session.TotalAmount, Summaries = summaries.Select(s => new { s.Nickname, s.Rank, s.TotalAmount, s.ContributionRate }) }); } } return ApiResponse.Ok(); }) .WithTags("Crew") .RequireAuthorization(); } }