using System.Diagnostics; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.WebUtilities; using DeviceDetectorNET; using bitforum.Helpers; using bitforum.Models; namespace bitforum.Controllers.Member.Log { [Authorize] [Route("Member/Log")] public class LoginController : Controller { private readonly ILogger _logger; private readonly DefaultDbContext _db; private string? _queryString = null; public LoginController(ILogger logger, DefaultDbContext db) { _logger = logger; _db = db; } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } public override void OnActionExecuting(ActionExecutingContext context) { ViewBag.QueryString = _queryString = string.Join("&", QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).Select(r => $"{r.Key}={r.Value.FirstOrDefault()}")); base.OnActionExecuting(context); } [HttpGet("Login")] public async Task Index([FromQuery] int page = 1, int perPage = 10, byte? search = null, string? keyword = null, string startAt = null!, string endAt = null!) { var query = _db.LoginLog.AsQueryable(); // 기본 정렬 유지 // 검색 조건 적용 if (!string.IsNullOrEmpty(keyword) && search.HasValue) { if (search == 1) // ID 검색 { if (int.TryParse(keyword, out int memberID)) { query = query.Where(log => log.MemberID == memberID); } else { query = query.Where(log => log.MemberID == 0); } } else if (search == 2) { query = query.Where(log => _db.LoginLog.Any(c => c.Account.Contains(keyword))); // 이메일 검색 } else if (search == 3) { query = query.Where(log => _db.Member.Any(m => m.Name != null && m.Name.Contains(keyword))); // 사용자명 검색 } } var today = DateTime.UtcNow.Date; if (startAt != null && DateTime.TryParse(startAt, out var st)) { query = query.Where(log => log.CreatedAt >= st); } if (endAt != null && DateTime.TryParse(endAt, out var et)) { query = query.Where(log => log.CreatedAt <= et); } query = query.OrderByDescending(c => c.CreatedAt); // 페이징 처리 var logs = await query .Skip((page - 1) * perPage) .Take(perPage) .Select(c => new { Name = _db.Member.Where(m => m.ID == c.MemberID).Select(m => m.Name).FirstOrDefault(), // 사용자명 추가 c.ID, c.MemberID, c.Account, c.Success, c.Reason, c.Referer, c.Url, c.IpAddress, c.UserAgent, c.CreatedAt }) .ToListAsync(); var data = new List(); if (logs.Count > 0) { foreach (var row in logs) { var deviceDetector = new DeviceDetector(row.UserAgent); deviceDetector.Parse(); var browser = deviceDetector.GetClient(); var osInfo = deviceDetector.GetOs(); var device = deviceDetector.GetModel(); data.Add(new { row.ID, row.MemberID, row.Name, row.Account, Success = row.Success ? 'Y' : 'N', row.Reason, row.Referer, row.Url, row.IpAddress, Browser = browser, OS = osInfo, Device = device, ViewURL = $"/Member/Log/Login/{row.ID}?{_queryString}", DeleteURL = $"/Member/Log/Login/{row.ID}/Delete?{_queryString}", CreatedAt = row.CreatedAt.GetDateAt() }); } } var parameter = new { Page = page, PerPage = perPage, Search = search, Keyword = keyword, StartAt = startAt, EndAt = endAt }; ViewBag.Data = data; ViewBag.Total = await query.CountAsync(); ViewBag.Parameter = parameter; ViewBag.Pagination = new Pagination(ViewBag.Total, page, perPage, parameter); return View("~/Views/Member/Log/Login/Index.cshtml"); } [HttpGet("Login/{id}")] public async Task View([FromRoute] int id) { try { if (id <= 0) { throw new Exception("유효하지 않은 접근입니다."); } var loginData = await _db.LoginLog.FindAsync(id); if (loginData == null) { throw new Exception("로그인 정보를 찾을 수 없습니다."); } var deviceDetector = new DeviceDetector(loginData.UserAgent); deviceDetector.Parse(); var botInfo = deviceDetector.GetBot(); var clientInfo = deviceDetector.GetClient(); var osInfo = deviceDetector.GetOs(); var device = deviceDetector.GetDeviceName(); var brand = deviceDetector.GetBrandName(); var model = deviceDetector.GetModel(); ViewBag.Data = new { loginData.ID, loginData.MemberID, Name = _db.Member.Where(m => m.ID == loginData.MemberID).Select(m => m.Name).FirstOrDefault(), // 사용자명 추가 Success = loginData.Success ? 'Y' : 'N', loginData.Account, loginData.Reason, loginData.Referer, loginData.Url, loginData.IpAddress, loginData.UserAgent, loginData.CreatedAt, Bot = botInfo, Browser = clientInfo, OS = osInfo, Device = device, Brand = brand, Model = model, }; } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); } return View("~/Views/Member/Log/Login/View.cshtml"); } [HttpGet("Login/{id}/Delete")] public async Task Delete(int id) { try { if (id <= 0) { throw new Exception("유효하지 않은 접근입니다."); } var data = await _db.LoginLog.FindAsync(id); if (data == null) { throw new Exception("로그인 정보를 찾을 수 없습니다."); } _db.LoginLog.Remove(data); int affectedRows = await _db.SaveChangesAsync(); if (affectedRows <= 0) { throw new Exception("로그인 내역 삭제 중 오류가 발생했습니다."); } string message = "로그인 내역이 삭제되었습니다."; TempData["SuccessMessage"] = message; _logger.LogInformation(message); } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); } return Redirect($"/Member/Log/Login?{_queryString}"); } [HttpPost("Login/Delete")] public async Task Delete([FromForm] int[] ids) { try { if (ids == null || ids.Length <= 0) { throw new Exception("유효하지 않은 접근입니다."); } foreach (var id in ids) { var data = await _db.LoginLog.FindAsync(id); if (data == null) { throw new Exception($"{id}번 로그인 정보를 찾을 수 없습니다."); } _db.LoginLog.Remove(data); int affectedRows = await _db.SaveChangesAsync(); if (affectedRows <= 0) { throw new Exception($"{id}번 로그인 내역 삭제 중 오류가 발생했습니다."); } } string message = $"{ids.Length}건의 로그인 내역이 삭제되었습니다."; TempData["SuccessMessage"] = message; _logger.LogInformation(message); } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); } return Redirect($"/Member/Log/Login?{_queryString}"); } } }