| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207 |
- using Application.Abstractions.Chat;
- using Application.Abstractions.Data;
- using Microsoft.AspNetCore.SignalR;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.IdentityModel.JsonWebTokens;
- using SharedKernel.Extensions;
- using System.Collections.Concurrent;
- namespace Web.Api.Hubs;
- public sealed class ChatHub(IChatMessageStore messageStore, IChatConnectionTracker tracker, IServiceScopeFactory scopeFactory) : Hub<IChatHubClient>
- {
- private static readonly ConcurrentDictionary<string, DateTime> _lastMessageTime = new();
- public override async Task OnConnectedAsync()
- {
- var messages = await messageStore.GetRecentMessagesAsync(ChatSettings.MaxMessages);
- await Clients.Caller.ReceiveHistory(messages);
- var ip = Context.GetHttpContext()?.GetClientIP() ?? "Unknown";
- var ua = Context.GetHttpContext()?.GetUserAgent() ?? "Unknown";
- ConnectedUser user;
- if (Context.User?.Identity?.IsAuthenticated == true)
- {
- var memberID = GetMemberID();
- var memberName = GetMemberName();
- string? email = null;
- if (memberID.HasValue)
- {
- using (var scope = scopeFactory.CreateScope())
- {
- var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
- email = await db.Member.AsNoTracking().Where(x => x.ID == memberID.Value).Select(x => x.Email).FirstOrDefaultAsync();
- }
- }
- user = new ConnectedUser(Context.ConnectionId, memberID, email, memberName, ip, ua, false, DateTime.UtcNow);
- if (!string.IsNullOrEmpty(memberName))
- {
- await Clients.Caller.Connected($"{memberName}님, 환영합니다.");
- await Clients.Others.ReceiveSystemMessage($"{memberName}님이 입장했습니다.");
- }
- }
- else
- {
- user = new ConnectedUser(Context.ConnectionId, null, null, null, ip, ua, true, DateTime.UtcNow);
- }
- Context.Items["user"] = user;
- await tracker.AddAsync(user);
- await BroadcastParticipantCountAsync();
- await base.OnConnectedAsync();
- }
- public override async Task OnDisconnectedAsync(Exception? exception)
- {
- _lastMessageTime.TryRemove(Context.ConnectionId, out _);
- await tracker.RemoveAsync(Context.ConnectionId);
- if (Context.Items["user"] is ConnectedUser user && !user.IsGuest)
- {
- await Clients.Others.ReceiveSystemMessage($"{user.MemberName}님이 퇴장했습니다.");
- }
- await BroadcastParticipantCountAsync();
- await base.OnDisconnectedAsync(exception);
- }
- /// <summary>
- /// 클라이언트가 로그아웃 시 호출 (invoke('Logout'))
- /// </summary>
- public async Task Logout()
- {
- await tracker.RemoveAsync(Context.ConnectionId);
- if (Context.Items["user"] is ConnectedUser user && !user.IsGuest)
- {
- await Clients.Others.ReceiveSystemMessage($"{user.MemberName}님이 퇴장했습니다.");
- }
- await Clients.Caller.Logout("로그아웃 되었습니다.");
- }
- /// <summary>
- /// 채팅 메시지 전송 시
- /// </summary>
- public async Task SendMessage(string content)
- {
- if (string.IsNullOrWhiteSpace(content))
- {
- return;
- }
- content = content.Trim();
- if (content.Length > ChatSettings.MaxContentLength)
- {
- return;
- }
- var now = DateTime.UtcNow;
- if (_lastMessageTime.TryGetValue(Context.ConnectionId, out var lastTime))
- {
- if ((now - lastTime).TotalSeconds < ChatSettings.RateLimitSeconds)
- {
- return;
- }
- }
- _lastMessageTime[Context.ConnectionId] = now;
- var memberID = GetMemberID();
- if (memberID is null)
- {
- return;
- }
- ChatMessage message;
- using (var scope = scopeFactory.CreateScope())
- {
- var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
- var member = await db.Member.AsNoTracking().Where(x => x.ID == memberID.Value).Select(x => new { x.SID, x.Name }).FirstOrDefaultAsync();
- if (member is null)
- {
- return;
- }
- message = new ChatMessage(
- memberID.Value,
- member.SID,
- member.Name ?? "익명",
- content,
- now
- );
- }
- // 채팅 기록 저장
- await messageStore.AddMessageAsync(message);
- // 모든 클라이언트에게 메시지 전송
- await Clients.All.ReceiveMessage(message);
- }
- // 회원 ID 조회
- private int? GetMemberID()
- {
- var sub = Context.User?.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
- if (int.TryParse(sub, out var memberID))
- {
- return memberID;
- }
- return null;
- }
- // 회원 이름 조회
- private string? GetMemberName()
- {
- return Context.User?.FindFirst(JwtRegisteredClaimNames.Name)?.Value;
- }
- /// <summary>
- /// 클라이언트가 채팅 기록 요청 시 호출
- /// </summary>
- public async Task RequestHistory()
- {
- var messages = await messageStore.GetRecentMessagesAsync(ChatSettings.MaxMessages);
- await Clients.Caller.ReceiveHistory(messages);
- }
- /// <summary>
- /// 클라이언트가 접속자 수 요청 시 호출
- /// </summary>
- public async Task RequestParticipantCount()
- {
- var users = await tracker.GetAllAsync();
- await Clients.Caller.ReceiveParticipantCount(users.Count);
- }
- /// <summary>
- /// 클라이언트가 참여자 목록 요청 시 호출
- /// </summary>
- public async Task RequestParticipants()
- {
- var users = await tracker.GetAllAsync();
- var participants = users
- .Where(u => !u.IsGuest && !string.IsNullOrEmpty(u.MemberName))
- .Select(u => new ChatParticipant(u.MemberName!, u.IsGuest))
- .DistinctBy(p => p.MemberName)
- .OrderBy(p => p.MemberName)
- .ToList();
- await Clients.Caller.ReceiveParticipants(participants);
- }
- // 전체 접속자 수 브로드캐스트
- private async Task BroadcastParticipantCountAsync()
- {
- var users = await tracker.GetAllAsync();
- await Clients.All.ReceiveParticipantCount(users.Count);
- }
- }
|