Handler.cs 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Hub;
  3. using Application.Abstractions.Messaging;
  4. using Application.Abstractions.Notification;
  5. using SharedKernel;
  6. using SharedKernel.Results;
  7. using Domain.Entities.Donations.ValueObject;
  8. using Domain.Entities.Notifications.ValueObject;
  9. using Microsoft.EntityFrameworkCore;
  10. namespace Application.Features.Api.Crew.DeclineInvite;
  11. internal sealed class Handler(
  12. IAppDbContext db,
  13. INotificationService notificationService,
  14. ICrewHubBroadcaster crewBroadcaster
  15. ) : ICommandHandler<Command, Result<Response>>
  16. {
  17. public async Task<Result<Response>> Handle(Command request, CancellationToken ct)
  18. {
  19. var invitation = await db.CrewInvitation.FirstOrDefaultAsync(i => i.ID == request.InvitationID, ct);
  20. if (invitation is null)
  21. {
  22. return Result.Failure<Response>(Error.NotFound("CrewInvitation.NotFound", "초대를 찾을 수 없습니다."));
  23. }
  24. if (invitation.TargetMemberID != request.MemberID)
  25. {
  26. return Result.Failure<Response>(Error.Forbidden("CrewInvitation.NotForYou", "본인에게 온 초대만 거절할 수 있습니다."));
  27. }
  28. if (invitation.Status != CrewInviteStatus.Pending)
  29. {
  30. return Result.Failure<Response>(Error.Conflict("CrewInvitation.NotPending", "대기 중인 초대만 거절할 수 있습니다."));
  31. }
  32. if (invitation.IsExpired())
  33. {
  34. invitation.MarkExpired();
  35. await db.SaveChangesAsync(ct);
  36. return Result.Failure<Response>(Error.Problem("CrewInvitation.Expired", "초대가 만료되었습니다."));
  37. }
  38. invitation.Decline();
  39. await db.SaveChangesAsync(ct);
  40. var crew = await db.Crew.AsNoTracking().FirstOrDefaultAsync(c => c.ID == invitation.CrewID, ct);
  41. // 운영자(초대 발송자)에게 알림 — AcceptInvite 와 일관되게 INotificationService 로 통일 (SignalR push 포함)
  42. await notificationService.SendAsync(
  43. memberID: invitation.InvitedByMemberID,
  44. type: NotificationType.CrewInvitation,
  45. title: $"크루 초대 거절",
  46. message: $"'{invitation.Nickname}' 님이 '{crew?.Name ?? "크루"}' 초대를 거절했습니다.",
  47. actionUrl: $"/studio/donation/crew/{invitation.CrewID}",
  48. relatedType: "CrewInvitation",
  49. relatedID: invitation.ID,
  50. imageUrl: null,
  51. ct: ct
  52. );
  53. // 운영자 그룹(crew:{crewID})에 초대 응답 broadcast → 멤버 리스트 자동 새로고침
  54. await crewBroadcaster.BroadcastMemberChangeAsync(
  55. crewID: invitation.CrewID,
  56. kind: "invitationStatusChanged",
  57. data: new
  58. {
  59. invitationID = invitation.ID,
  60. status = "Declined",
  61. crewID = invitation.CrewID
  62. },
  63. ct: ct
  64. );
  65. return Result.Success(new Response(invitation.ID));
  66. }
  67. }