ChatHub.cs 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. using Application.Abstractions.Chat;
  2. using Application.Abstractions.Data;
  3. using Microsoft.AspNetCore.Authorization;
  4. using Microsoft.AspNetCore.SignalR;
  5. using Microsoft.EntityFrameworkCore;
  6. using Microsoft.IdentityModel.JsonWebTokens;
  7. using System.Collections.Concurrent;
  8. namespace Web.Api.Hubs;
  9. [Authorize]
  10. public sealed class ChatHub(IChatMessageStore messageStore, IServiceScopeFactory scopeFactory) : Hub<IChatHubClient>
  11. {
  12. private static readonly ConcurrentDictionary<string, DateTime> _lastMessageTime = new();
  13. public override async Task OnConnectedAsync()
  14. {
  15. var messages = await messageStore.GetRecentMessagesAsync(ChatSettings.MaxMessages);
  16. await Clients.Caller.ReceiveHistory(messages);
  17. var memberName = GetMemberName();
  18. if (!string.IsNullOrEmpty(memberName))
  19. {
  20. await Clients.Others.ReceiveSystemMessage($"{memberName}님이 입장했습니다.");
  21. }
  22. await base.OnConnectedAsync();
  23. }
  24. public override async Task OnDisconnectedAsync(Exception? exception)
  25. {
  26. _lastMessageTime.TryRemove(Context.ConnectionId, out _);
  27. var memberName = GetMemberName();
  28. if (!string.IsNullOrEmpty(memberName))
  29. {
  30. await Clients.Others.ReceiveSystemMessage($"{memberName}님이 퇴장했습니다.");
  31. }
  32. await base.OnDisconnectedAsync(exception);
  33. }
  34. public async Task SendMessage(string content)
  35. {
  36. if (string.IsNullOrWhiteSpace(content))
  37. {
  38. return;
  39. }
  40. content = content.Trim();
  41. if (content.Length > ChatSettings.MaxContentLength)
  42. {
  43. return;
  44. }
  45. var now = DateTime.UtcNow;
  46. if (_lastMessageTime.TryGetValue(Context.ConnectionId, out var lastTime))
  47. {
  48. if ((now - lastTime).TotalSeconds < ChatSettings.RateLimitSeconds)
  49. {
  50. return;
  51. }
  52. }
  53. _lastMessageTime[Context.ConnectionId] = now;
  54. var memberID = GetMemberID();
  55. if (memberID is null)
  56. {
  57. return;
  58. }
  59. using var scope = scopeFactory.CreateScope();
  60. var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
  61. var member = await db.Member.AsNoTracking().Where(x => x.ID == memberID.Value).Select(x => new { x.SID, x.Name }).FirstOrDefaultAsync();
  62. if (member is null)
  63. {
  64. return;
  65. }
  66. var message = new ChatMessage(
  67. memberID.Value,
  68. member.SID,
  69. member.Name ?? "익명",
  70. content,
  71. now
  72. );
  73. // 채팅 기록 저장
  74. await messageStore.AddMessageAsync(message);
  75. // 모든 클라이언트에게 메시지 전송
  76. await Clients.All.ReceiveMessage(message);
  77. }
  78. private int? GetMemberID()
  79. {
  80. var sub = Context.User?.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
  81. if (int.TryParse(sub, out var memberID))
  82. {
  83. return memberID;
  84. }
  85. return null;
  86. }
  87. private string? GetMemberName()
  88. {
  89. return Context.User?.FindFirst(JwtRegisteredClaimNames.Name)?.Value;
  90. }
  91. }