using Application.Abstractions.Data; using Application.Abstractions.Hub; using Domain.Entities.Donations.ValueObject; using Infrastructure.Hubs; using Web.Api.Common; using Web.Api.Extensions; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using System.Security.Claims; namespace Web.Api.Endpoints.Donation; /// 리모콘 — 큐 순서 변경 (두 알림 위치 swap, 위젯+리모콘 broadcast) internal sealed class RemoteReorder : IEndpoint { public sealed record Request(string WidgetToken, int AlertIDA, int AlertIDB); public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("api/donation/remote/reorder", async ( Request request, ClaimsPrincipal user, IAppDbContext db, IHubContext donationHub, CancellationToken ct ) => { var memberID = user.GetRequiredMemberID(); if (string.IsNullOrWhiteSpace(request.WidgetToken) || request.AlertIDA == request.AlertIDB) { return ApiResponse.Fail(StatusCodes.Status400BadRequest, "잘못된 요청입니다."); } // 권한 확인 — 본인 채널의 WidgetToken 만 reorder 가능 var channel = await db.Channel.AsNoTracking() .Where(c => c.WidgetToken == request.WidgetToken) .Select(c => new { c.ID, c.MemberID }) .FirstOrDefaultAsync(ct); if (channel is null || channel.MemberID != memberID) { return ApiResponse.Fail(StatusCodes.Status403Forbidden, "권한이 없습니다."); } // 방어적 검증 — 두 alertID 가 본 채널의 Queued 상태인지 확인 (재생/완료/스킵 항목 순서 조작 차단) var alerts = await db.DonationAlert.AsNoTracking() .Where(a => (a.ID == request.AlertIDA || a.ID == request.AlertIDB) && a.Donation!.ChannelID == channel.ID) .Select(a => new { a.ID, a.Status }) .ToListAsync(ct); if (alerts.Count != 2 || alerts.Any(a => a.Status != AlertStatus.Queued)) { return ApiResponse.Fail(StatusCodes.Status400BadRequest, "대기 중인 알림만 순서 변경이 가능합니다."); } // Widget+Remote 양쪽 그룹에 swap broadcast — 클라이언트가 자기 큐/표시 순서 swap await donationHub.Clients.Group(request.WidgetToken).ReceiveQueueReorder(new { swap = new[] { request.AlertIDA, request.AlertIDB } }); return ApiResponse.Ok(); }) .WithTags("DonationRemote") .RequireAuthorization(); } }