ChatHub.cs 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. using Application.Abstractions.Chat;
  2. using Application.Abstractions.Data;
  3. using Microsoft.AspNetCore.SignalR;
  4. using Microsoft.EntityFrameworkCore;
  5. using Microsoft.IdentityModel.JsonWebTokens;
  6. using SharedKernel.Extensions;
  7. using System.Collections.Concurrent;
  8. namespace Web.Api.Hubs;
  9. public sealed class ChatHub(IChatMessageStore messageStore, IChatConnectionTracker tracker, IServiceScopeFactory scopeFactory) : Hub<IChatHubClient>
  10. {
  11. private static readonly ConcurrentDictionary<string, DateTime> _lastMessageTime = new();
  12. public override async Task OnConnectedAsync()
  13. {
  14. var messages = await messageStore.GetRecentMessagesAsync(ChatSettings.MaxMessages);
  15. await Clients.Caller.ReceiveHistory(messages);
  16. var ip = Context.GetHttpContext()?.GetClientIP() ?? "Unknown";
  17. var ua = Context.GetHttpContext()?.GetUserAgent() ?? "Unknown";
  18. ConnectedUser user;
  19. if (Context.User?.Identity?.IsAuthenticated == true)
  20. {
  21. var memberID = GetMemberID();
  22. var memberName = GetMemberName();
  23. string? email = null;
  24. if (memberID.HasValue)
  25. {
  26. using (var scope = scopeFactory.CreateScope())
  27. {
  28. var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
  29. email = await db.Member.AsNoTracking().Where(x => x.ID == memberID.Value).Select(x => x.Email).FirstOrDefaultAsync();
  30. }
  31. }
  32. user = new ConnectedUser(Context.ConnectionId, memberID, email, memberName, ip, ua, false, DateTime.UtcNow);
  33. if (!string.IsNullOrEmpty(memberName))
  34. {
  35. await Clients.Caller.Connected($"{memberName}님, 환영합니다.");
  36. await Clients.Others.ReceiveSystemMessage($"{memberName}님이 입장했습니다.");
  37. }
  38. }
  39. else
  40. {
  41. user = new ConnectedUser(Context.ConnectionId, null, null, null, ip, ua, true, DateTime.UtcNow);
  42. }
  43. Context.Items["user"] = user;
  44. await tracker.AddAsync(user);
  45. await base.OnConnectedAsync();
  46. }
  47. public override async Task OnDisconnectedAsync(Exception? exception)
  48. {
  49. _lastMessageTime.TryRemove(Context.ConnectionId, out _);
  50. await tracker.RemoveAsync(Context.ConnectionId);
  51. if (Context.Items["user"] is ConnectedUser user && !user.IsGuest)
  52. {
  53. await Clients.Others.ReceiveSystemMessage($"{user.MemberName}님이 퇴장했습니다.");
  54. }
  55. await base.OnDisconnectedAsync(exception);
  56. }
  57. /// <summary>
  58. /// 클라이언트가 로그아웃 시 호출 (invoke('Logout'))
  59. /// </summary>
  60. public async Task Logout()
  61. {
  62. await tracker.RemoveAsync(Context.ConnectionId);
  63. if (Context.Items["user"] is ConnectedUser user && !user.IsGuest)
  64. {
  65. await Clients.Others.ReceiveSystemMessage($"{user.MemberName}님이 퇴장했습니다.");
  66. }
  67. await Clients.Caller.Logout("로그아웃 되었습니다.");
  68. }
  69. /// <summary>
  70. /// 채팅 메시지 전송 시
  71. /// </summary>
  72. public async Task SendMessage(string content)
  73. {
  74. if (string.IsNullOrWhiteSpace(content))
  75. {
  76. return;
  77. }
  78. content = content.Trim();
  79. if (content.Length > ChatSettings.MaxContentLength)
  80. {
  81. return;
  82. }
  83. var now = DateTime.UtcNow;
  84. if (_lastMessageTime.TryGetValue(Context.ConnectionId, out var lastTime))
  85. {
  86. if ((now - lastTime).TotalSeconds < ChatSettings.RateLimitSeconds)
  87. {
  88. return;
  89. }
  90. }
  91. _lastMessageTime[Context.ConnectionId] = now;
  92. var memberID = GetMemberID();
  93. if (memberID is null)
  94. {
  95. return;
  96. }
  97. ChatMessage message;
  98. using (var scope = scopeFactory.CreateScope())
  99. {
  100. var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
  101. var member = await db.Member.AsNoTracking().Where(x => x.ID == memberID.Value).Select(x => new { x.SID, x.Name }).FirstOrDefaultAsync();
  102. if (member is null)
  103. {
  104. return;
  105. }
  106. message = new ChatMessage(
  107. memberID.Value,
  108. member.SID,
  109. member.Name ?? "익명",
  110. content,
  111. now
  112. );
  113. }
  114. // 채팅 기록 저장
  115. await messageStore.AddMessageAsync(message);
  116. // 모든 클라이언트에게 메시지 전송
  117. await Clients.All.ReceiveMessage(message);
  118. }
  119. // 회원 ID 조회
  120. private int? GetMemberID()
  121. {
  122. var sub = Context.User?.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
  123. if (int.TryParse(sub, out var memberID))
  124. {
  125. return memberID;
  126. }
  127. return null;
  128. }
  129. // 회원 이름 조회
  130. private string? GetMemberName()
  131. {
  132. return Context.User?.FindFirst(JwtRegisteredClaimNames.Name)?.Value;
  133. }
  134. }