ChatHub.cs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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 BroadcastParticipantCountAsync();
  46. await base.OnConnectedAsync();
  47. }
  48. public override async Task OnDisconnectedAsync(Exception? exception)
  49. {
  50. _lastMessageTime.TryRemove(Context.ConnectionId, out _);
  51. await tracker.RemoveAsync(Context.ConnectionId);
  52. if (Context.Items["user"] is ConnectedUser user && !user.IsGuest)
  53. {
  54. await Clients.Others.ReceiveSystemMessage($"{user.MemberName}님이 퇴장했습니다.");
  55. }
  56. await BroadcastParticipantCountAsync();
  57. await base.OnDisconnectedAsync(exception);
  58. }
  59. /// <summary>
  60. /// 클라이언트가 로그아웃 시 호출 (invoke('Logout'))
  61. /// </summary>
  62. public async Task Logout()
  63. {
  64. await tracker.RemoveAsync(Context.ConnectionId);
  65. if (Context.Items["user"] is ConnectedUser user && !user.IsGuest)
  66. {
  67. await Clients.Others.ReceiveSystemMessage($"{user.MemberName}님이 퇴장했습니다.");
  68. }
  69. await Clients.Caller.Logout("로그아웃 되었습니다.");
  70. }
  71. /// <summary>
  72. /// 채팅 메시지 전송 시
  73. /// </summary>
  74. public async Task SendMessage(string content)
  75. {
  76. if (string.IsNullOrWhiteSpace(content))
  77. {
  78. return;
  79. }
  80. content = content.Trim();
  81. if (content.Length > ChatSettings.MaxContentLength)
  82. {
  83. return;
  84. }
  85. var now = DateTime.UtcNow;
  86. if (_lastMessageTime.TryGetValue(Context.ConnectionId, out var lastTime))
  87. {
  88. if ((now - lastTime).TotalSeconds < ChatSettings.RateLimitSeconds)
  89. {
  90. return;
  91. }
  92. }
  93. _lastMessageTime[Context.ConnectionId] = now;
  94. var memberID = GetMemberID();
  95. if (memberID is null)
  96. {
  97. return;
  98. }
  99. ChatMessage message;
  100. using (var scope = scopeFactory.CreateScope())
  101. {
  102. var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
  103. var member = await db.Member.AsNoTracking().Where(x => x.ID == memberID.Value).Select(x => new { x.SID, x.Name }).FirstOrDefaultAsync();
  104. if (member is null)
  105. {
  106. return;
  107. }
  108. message = new ChatMessage(
  109. memberID.Value,
  110. member.SID,
  111. member.Name ?? "익명",
  112. content,
  113. now
  114. );
  115. }
  116. // 채팅 기록 저장
  117. await messageStore.AddMessageAsync(message);
  118. // 모든 클라이언트에게 메시지 전송
  119. await Clients.All.ReceiveMessage(message);
  120. }
  121. // 회원 ID 조회
  122. private int? GetMemberID()
  123. {
  124. var sub = Context.User?.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
  125. if (int.TryParse(sub, out var memberID))
  126. {
  127. return memberID;
  128. }
  129. return null;
  130. }
  131. // 회원 이름 조회
  132. private string? GetMemberName()
  133. {
  134. return Context.User?.FindFirst(JwtRegisteredClaimNames.Name)?.Value;
  135. }
  136. /// <summary>
  137. /// 클라이언트가 채팅 기록 요청 시 호출
  138. /// </summary>
  139. public async Task RequestHistory()
  140. {
  141. var messages = await messageStore.GetRecentMessagesAsync(ChatSettings.MaxMessages);
  142. await Clients.Caller.ReceiveHistory(messages);
  143. }
  144. /// <summary>
  145. /// 클라이언트가 접속자 수 요청 시 호출
  146. /// </summary>
  147. public async Task RequestParticipantCount()
  148. {
  149. var users = await tracker.GetAllAsync();
  150. await Clients.Caller.ReceiveParticipantCount(users.Count);
  151. }
  152. /// <summary>
  153. /// 클라이언트가 참여자 목록 요청 시 호출
  154. /// </summary>
  155. public async Task RequestParticipants()
  156. {
  157. var users = await tracker.GetAllAsync();
  158. var participants = users
  159. .Where(u => !u.IsGuest && !string.IsNullOrEmpty(u.MemberName))
  160. .Select(u => new ChatParticipant(u.MemberName!, u.IsGuest))
  161. .DistinctBy(p => p.MemberName)
  162. .OrderBy(p => p.MemberName)
  163. .ToList();
  164. await Clients.Caller.ReceiveParticipants(participants);
  165. }
  166. // 전체 접속자 수 브로드캐스트
  167. private async Task BroadcastParticipantCountAsync()
  168. {
  169. var users = await tracker.GetAllAsync();
  170. await Clients.All.ReceiveParticipantCount(users.Count);
  171. }
  172. }