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();
}
}