using Application.Abstractions.Chat; using Application.Abstractions.Data; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.JsonWebTokens; using System.Collections.Concurrent; namespace Web.Api.Hubs; [Authorize] public sealed class ChatHub(IChatMessageStore messageStore, 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 memberName = GetMemberName(); if (!string.IsNullOrEmpty(memberName)) { await Clients.Others.ReceiveSystemMessage($"{memberName}님이 입장했습니다."); } await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { _lastMessageTime.TryRemove(Context.ConnectionId, out _); var memberName = GetMemberName(); if (!string.IsNullOrEmpty(memberName)) { await Clients.Others.ReceiveSystemMessage($"{memberName}님이 퇴장했습니다."); } await base.OnDisconnectedAsync(exception); } 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; } 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; } var message = new ChatMessage( memberID.Value, member.SID, member.Name ?? "익명", content, now ); // 채팅 기록 저장 await messageStore.AddMessageAsync(message); // 모든 클라이언트에게 메시지 전송 await Clients.All.ReceiveMessage(message); } 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; } }