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 { private static readonly ConcurrentDictionary _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(); 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); } /// /// 클라이언트가 로그아웃 시 호출 (invoke('Logout')) /// 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("로그아웃 되었습니다."); } /// /// 채팅 메시지 전송 시 /// 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(); 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; } /// /// 클라이언트가 채팅 기록 요청 시 호출 /// public async Task RequestHistory() { var messages = await messageStore.GetRecentMessagesAsync(ChatSettings.MaxMessages); await Clients.Caller.ReceiveHistory(messages); } /// /// 클라이언트가 접속자 수 요청 시 호출 /// public async Task RequestParticipantCount() { var users = await tracker.GetAllAsync(); await Clients.Caller.ReceiveParticipantCount(users.Count); } /// /// 클라이언트가 참여자 목록 요청 시 호출 /// 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); } }