using Application.Abstractions.Data; using Application.Abstractions.Messaging; using SharedKernel.Results; using Domain.Entities.Donations.ValueObject; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.DonationRemote.GetCrewMembers; internal sealed class Handler(IAppDbContext db) : IQueryHandler> { public async Task> Handle(Query request, CancellationToken ct) { var channel = await db.Channel.AsNoTracking() .Where(c => c.WidgetToken == request.WidgetToken && c.IsActive) .Select(c => new { c.ID, c.MemberID }) .FirstOrDefaultAsync(ct); if (channel is null) { return Result.Failure(Error.NotFound("Channel.NotFound", "채널을 찾을 수 없습니다.")); } if (channel.MemberID != request.RequesterMemberID) { return Result.Failure(Error.Forbidden("Remote.Forbidden", "권한이 없습니다.")); } var crew = await db.Crew.AsNoTracking() .Where(c => c.ID == request.CrewID && c.ChannelID == channel.ID && c.IsActive) .Select(c => new { c.ID, c.Name }) .FirstOrDefaultAsync(ct); if (crew is null) { return Result.Failure(Error.NotFound("Crew.NotFound", "크루를 찾을 수 없습니다.")); } var activeSessionID = await db.CrewSession.AsNoTracking() .Where(s => s.CrewID == crew.ID && s.Status == CrewSessionStatus.Active) .OrderByDescending(s => s.CreatedAt) .Select(s => (int?)s.ID) .FirstOrDefaultAsync(ct); // ID > 0 방어: 시드/수동 SQL로 ID=0 row가 들어간 케이스 차단 var members = await db.CrewMember.AsNoTracking() .Where(m => m.CrewID == crew.ID && m.IsActive && m.ID > 0) .OrderBy(m => m.SortOrder).ThenBy(m => m.JoinedAt) .Select(m => new CrewMemberItem( m.ID, m.MemberID, m.Nickname, m.Role, m.SortOrder )) .ToListAsync(ct); return Result.Success(new Response(crew.ID, crew.Name, activeSessionID, members)); } }