| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- using Application.Abstractions.Cache;
- using Application.Abstractions.Chat;
- using Application.Common;
- using SharedKernel.Helpers;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.AspNetCore.Mvc.RazorPages;
- using StackExchange.Redis;
- using System.ComponentModel;
- using System.ComponentModel.DataAnnotations;
- namespace Admin.Pages.Member;
- public class VisitorModel(
- IChatConnectionTracker tracker,
- IConnectionMultiplexer redis
- ) : PageModel {
- [BindProperty(SupportsGet = true)]
- public QueryParams Query { get; set; } = new();
- public sealed class QueryParams
- {
- [Range(1, int.MaxValue)]
- [DisplayName("페이지 번호")]
- public int PageNum { get; set; } = 1;
- [Range(1, 100)]
- [DisplayName("페이지 목록 수")]
- public ushort PerPage { get; set; } = 20;
- [Range(1, 2, ErrorMessage = "{0}이(가) 올바르지 않습니다.")]
- [DisplayName("검색 조건")]
- public int? Search { get; set; }
- [MaxLength(100, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
- [DisplayName("검색어")]
- public string? Keyword { get; set; }
- [DisplayName("검색 구분")]
- public int? Tab { get; set; }
- }
- public int TotalRows { get; set; }
- public int TotalMember { get; set; }
- public int TotalGuest { get; set; }
- public int TotalIps { get; set; }
- public List<(
- int Num,
- string ConnectionID,
- bool IsGuest,
- string? MemberID,
- string? Email,
- string IpAddress,
- string Browser,
- string OS,
- string Device,
- string ConnectedAt,
- string LogoutURL,
- int Connections
- )> List { get; set; } = [];
- public Pagination? Pagination { get; set; }
- public async Task OnGetAsync(CancellationToken _)
- {
- if (!ModelState.IsValid)
- {
- return;
- }
- var all = await tracker.GetAllAsync();
- TotalRows = all.Count;
- TotalMember = all.Count(x => !x.IsGuest);
- TotalGuest = all.Count(x => x.IsGuest);
- TotalIps = all.Select(x => x.IpAddress).Distinct().Count();
- if (Query.Tab == 3)
- {
- // IP 목록 탭
- var ipGroups = all
- .GroupBy(x => x.IpAddress)
- .Select(g => new
- {
- IpAddress = g.Key,
- ConnectedAt = g.Min(x => x.ConnectedAt),
- SampleUA = g.First().UserAgent,
- Connections = g.Count()
- })
- .OrderByDescending(x => x.Connections)
- .ToList();
- var skip = (Query.PageNum - 1) * Query.PerPage;
- var paged = ipGroups.Skip(skip).Take(Query.PerPage).ToList();
- int num = skip + 1;
- List = [..paged.Select(c => (
- Num: num++,
- ConnectionID: "",
- IsGuest: false,
- MemberID: (string?)null,
- Email: (string?)null,
- c.IpAddress,
- Browser: ParseUA(c.SampleUA, "browser"),
- OS: ParseUA(c.SampleUA, "os"),
- Device: ParseUA(c.SampleUA, "device"),
- ConnectedAt: c.ConnectedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"),
- LogoutURL: "",
- c.Connections
- ))];
- Pagination = new Pagination(ipGroups.Count, Query.PageNum, Query.PerPage);
- }
- else
- {
- // 검색 필터
- IEnumerable<ConnectedUser> filtered = all;
- if (!string.IsNullOrWhiteSpace(Query.Keyword))
- {
- filtered = Query.Search == 1
- ? filtered.Where(x => x.MemberID.HasValue && x.MemberID.Value.ToString() == Query.Keyword)
- : filtered.Where(x => x.Email != null && x.Email == Query.Keyword);
- }
- // 탭 필터
- filtered = Query.Tab switch
- {
- 1 => filtered.Where(x => !x.IsGuest),
- 2 => filtered.Where(x => x.IsGuest),
- _ => filtered
- };
- var sorted = filtered.OrderByDescending(x => x.ConnectedAt).ToList();
- var skip = (Query.PageNum - 1) * Query.PerPage;
- var paged = sorted.Skip(skip).Take(Query.PerPage).ToList();
- int num = skip + 1;
- var qs = Request.QueryString.ToString();
- List = [..paged.Select(c => (
- Num: num++,
- ConnectionID: c.ConnectionId,
- c.IsGuest,
- MemberID: c.MemberID?.ToString(),
- c.Email,
- c.IpAddress,
- Browser: ParseUA(c.UserAgent, "browser"),
- OS: ParseUA(c.UserAgent, "os"),
- Device: ParseUA(c.UserAgent, "device"),
- ConnectedAt: c.ConnectedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"),
- LogoutURL: $"/Member/Visitor?handler=Kick&connectionId={c.ConnectionId}",
- Connections: 0
- ))];
- Pagination = new Pagination(sorted.Count, Query.PageNum, Query.PerPage);
- }
- }
- public async Task<IActionResult> OnGetKickAsync(string connectionId, CancellationToken ct)
- {
- try
- {
- if (string.IsNullOrEmpty(connectionId))
- {
- throw new Exception("강제 종료할 대상이 없습니다.");
- }
- await PublishKick(connectionId);
- TempData["SuccessMessage"] = "강제 종료되었습니다.";
- }
- catch (Exception e)
- {
- TempData["ErrorMessages"] = e.Message;
- }
- return RedirectToPage(Query);
- }
- public async Task<IActionResult> OnPostKickAsync(string[] ids, CancellationToken ct)
- {
- try
- {
- if (ids.Length == 0)
- {
- throw new Exception("강제 종료할 항목을 선택해주세요.");
- }
- foreach (var id in ids)
- {
- await PublishKick(id);
- }
- TempData["SuccessMessage"] = $"{ids.Length}건이 강제 종료되었습니다.";
- }
- catch (Exception e)
- {
- TempData["ErrorMessages"] = e.Message;
- }
- return RedirectToPage(Query);
- }
- // Redis Pub/Sub을 통해 해당 ConnectionID를 구독 중인 클라이언트에게 강제 종료 메시지를 보냄
- private async Task PublishKick(string connectionId)
- {
- await redis.GetSubscriber().PublishAsync(
- RedisChannel.Literal(CacheKeys.ChatKickChannel), connectionId
- );
- }
- // User-Agent 문자열에서 브라우저, OS, 디바이스 정보를 추출하는 헬퍼 메서드
- private static string ParseUA(string? ua, string type)
- {
- if (string.IsNullOrWhiteSpace(ua))
- {
- return "-";
- }
- return type switch
- {
- "browser" => UserAgentParser.ExtractBrowser(ua),
- "os" => UserAgentParser.ExtractOS(ua),
- "device" => UserAgentParser.ExtractDevice(ua),
- _ => "-"
- };
- }
- }
|