using System.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.WebUtilities; using Microsoft.EntityFrameworkCore; using bitforum.Helpers; using bitforum.Models; using bitforum.Models.Account; using bitforum.Services; using bitforum.Constants; namespace bitforum.Controllers.Member { [Authorize] [Route("Member")] public class ListController : Controller { private readonly ILogger _logger; private readonly IFileUploadService _fileUploadService; private readonly DefaultDbContext _db; private readonly string _IndexViewPath = "~/Views/Member/List/Index.cshtml"; private readonly string _WriteViewPath = "~/Views/Member/List/Write.cshtml"; private readonly string _EditViewPath = "~/Views/Member/List/Edit.cshtml"; private string? _queryString = null; public ListController(ILogger logger, IFileUploadService fileUploadService, DefaultDbContext db) { _logger = logger; _fileUploadService = fileUploadService; _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("List")] public async Task Index( [FromQuery] int page = 1, int perPage = 10, byte? search = null, string? keyword = null, string startAt = null!, string endAt = null!, sbyte tab = 0, sbyte isEmailVerified = 0, sbyte isAuthCertified = 0, Gender? gender = null ) { var query = _db.Member.AsQueryable(); // 검색 조건 적용 if (!string.IsNullOrEmpty(keyword) && search.HasValue) { switch (search) { case 1: // ID 검색 if (int.TryParse(keyword, out int memberID)) { query = query.Where(m => m.ID == memberID); } else { query = query.Where(m => m.ID == 0); } break; case 2: // 이메일 검색 query = query.Where(m => m.Email.Contains(keyword)); break; case 3: // 별명 검색 query = query.Where(m => m.Name != null && m.Name.Contains(keyword)); break; case 4: // 이름 검색 query = query.Where(m => m.FullName != null && m.FullName.Contains(keyword)); break; case 5: // 연락처 검색 query = query.Where(m => m.Phone != null && m.Phone.Contains(keyword)); break; } } if (tab > 0) { switch (tab) { case 1: // 차단 query = query.Where(m => m.IsDenied); break; case 2: // 탈퇴 query = query.Where(m => m.IsWithdraw); break; case 3: // 관리자 query = query.Where(m => m.IsAdmin); break; } } if (gender is not null) // 성별 { query = query.Where(m => m.Gender == gender); } if (isEmailVerified > 0) // 이메일 인증 여부 { query = query.Where(m => m.IsEmailVerified == true); } if (isAuthCertified > 0) // 본인 인증 여부 { query = query.Where(m => m.IsAuthCertified == true); } // 가입 일시 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 members = await query .Skip((page - 1) * perPage) .Take(perPage) .Select(c => new { GradeName = _db.MemberGrade.Where(m => m.ID == c.GradeID).Select(m => m.KorName).FirstOrDefault(), // 회원등급 이름 c.ID, c.SID, c.Email, c.Name, c.FullName, c.Icon, c.Coin, c.Exp, c.Phone, c.Gender, c.Birthday, c.Following, c.Followed, c.IsEmailVerified, c.IsAuthCertified, c.IsDenied, c.IsAdmin, c.IsWithdraw, c.LastLoginIp, c.LastLoginAt, c.CreatedAt, c.UpdatedAt }) .ToListAsync(); var data = new List(); if (members.Count > 0) { foreach (var row in members) { data.Add(new { row.GradeName, row.ID, row.Email, row.Name, row.FullName, row.Icon, Phone = row?.Phone ?? "-", Gender = row.Gender.HasValue ? row.Gender.ToString() : "-", Birthday = row.Birthday?.ToString("yyyy.MM.dd"), Coin = row.Coin.ToString("N0"), Exp = row.Exp.ToString("N0"), Following = row.Following.ToString("N0"), Followed = row.Followed.ToString("N0"), row.IsEmailVerified, row.IsAuthCertified, row.IsDenied, row.IsAdmin, row.IsWithdraw, row.LastLoginIp, LastLoginAt = row.LastLoginAt?.ToString("yyyy.MM.dd"), CreatedAt = row.CreatedAt.ToString("yyyy.MM.dd"), UpdatedAt = row.UpdatedAt?.ToString("yyyy.MM.dd"), ApproveURL = $"/Member/List/{row.ID}/Approve?{_queryString}", EditURL = $"/Member/List/{row.ID}/Edit?{_queryString}", DeleteURL = $"/Member/List/{row.ID}/Delete?{_queryString}" }); } } var parameter = new { Page = page, PerPage = perPage, Search = search, Keyword = keyword, StartAt = startAt, EndAt = endAt, Tab = tab, IsEmailVerified = isEmailVerified, IsAuthCertified = isAuthCertified, Gender = gender, }; ViewBag.Data = data; ViewBag.Total = await query.CountAsync(); ViewBag.Parameter = parameter; ViewBag.Pagination = new Pagination(ViewBag.Total, page, perPage, parameter); return View(_IndexViewPath); } [HttpGet("List/Write")] public IActionResult Write() { ViewBag.MemberGrades = new SelectList( _db.MemberGrade.Where(c => c.IsActive).OrderByDescending(c => c.Order), "ID", "KorName", null ); return View(_WriteViewPath); } [HttpPost("List/Create")] public async Task Create([FromForm] Models.Account.Member request, IFormFile? photo, IFormFile? icon) { try { if (!ModelState.IsValid) { throw new Exception("유효성 검사에 실패하였습니다."); } if (request.FirstName != null || request.LastName != null) { request.FullName = $"{request.FirstName}{request.LastName}"; } // 이메일 중복 확인 if (await _db.Member.AnyAsync(c => c.Email == request.Email)) { throw new ArgumentException("이미 등록된 이메일 주소입니다."); } // 별명 중복 확인 if (await _db.Member.AnyAsync(c => c.Name == request.Name)) { throw new ArgumentException("이미 등록된 별명입니다."); } request.Password = BCrypt.Net.BCrypt.HashPassword(request.Password); request.PasswordUpdatedAt = DateTime.UtcNow; request.SignupIP = HttpContext.GetClientIP(); request.CreatedAt = DateTime.UtcNow; await _db.Member.AddAsync(request); int affectedRows = await _db.SaveChangesAsync(); if (affectedRows <= 0) { throw new Exception("회원 등록 중 오류가 발생했습니다."); } request.Photo = await _fileUploadService.UploadImageAsync(photo, UploadFolder.Member, request.ID); request.Icon = await _fileUploadService.UploadImageAsync(icon, UploadFolder.Member, request.ID); request.EmailVerifiedAt = (request.IsEmailVerified ? DateTime.UtcNow : null); request.AuthCertifiedAt = (request.IsAuthCertified ? DateTime.UtcNow : null); request.DeletedAt = (request.IsWithdraw ? DateTime.UtcNow : null); // 회원의 약관 및 알림 동의 정보 생성 await _db.MemberApprove.AddAsync(new MemberApprove { MemberID = request.ID, IsReceiveSMS = false, IsReceiveEmail = false, IsReceiveNote = false, IsDisclosureInvest = false }); await _db.SaveChangesAsync(); string message = "회원이 등록되었습니다."; TempData["SuccessMessage"] = message; _logger.LogInformation(message); return Redirect("/Member/List"); } catch (ArgumentException e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); return Write(); } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); return Write(); } } [HttpGet("List/{id}/Edit")] public async Task Edit(int id) { try { if (id <= 0) { throw new Exception("유효하지 않은 접근입니다."); } var member = await _db.Member.Where(m => m.ID == id).FirstOrDefaultAsync(); if (member is null) { throw new Exception("회원 정보를 찾을 수 없습니다."); } ViewBag.MemberGrades = new SelectList( _db.MemberGrade.Where(c => c.IsActive).OrderByDescending(c => c.Order), "ID", "KorName", member.GradeID ); return View(_EditViewPath, member); } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); return Redirect("/Member/List"); } } [HttpPost("List/Update")] public async Task Update([FromForm] Models.Account.Member request, IFormFile? photo, IFormFile? icon, [FromForm] bool IsPhotoRemove = false, bool IsIconRemove = false) { try { if (!ModelState.IsValid) { throw new Exception("유효성 검사에 실패하였습니다."); } var member = await _db.Member.FirstOrDefaultAsync(c => c.ID == request.ID); if (member is null) { throw new Exception("회원 정보를 찾을 수 없습니다."); } if (request.FirstName != null || request.LastName != null) { member.FullName = $"{request.FirstName}{request.LastName}"; } // 이메일 중복 확인 if (await _db.Member.AnyAsync(c => c.Email == request.Email && c.ID != member.ID)) { throw new ArgumentException("이미 등록된 이메일 주소입니다."); } // 별명 중복 확인 if (await _db.Member.AnyAsync(c => c.Name == request.Name && c.ID != member.ID)) { throw new ArgumentException("이미 등록된 별명입니다."); } // 비밀번호 변경 if (!string.IsNullOrEmpty(request.Password)) { member.Password = BCrypt.Net.BCrypt.HashPassword(request.Password); member.PasswordUpdatedAt = DateTime.UtcNow; } // 사진 저장 if (IsPhotoRemove) { _fileUploadService.RemoveFile(request.Photo); member.Photo = null; } else if (photo is not null) { member.Photo = await _fileUploadService.UploadImageAsync(photo, UploadFolder.Grade, member.ID); } // 아이콘 저장 if (IsIconRemove) { _fileUploadService.RemoveFile(request.Photo); member.Icon = null; } else if (icon is not null) { member.Icon = await _fileUploadService.UploadImageAsync(icon, UploadFolder.Grade, member.ID); } member.GradeID = request.GradeID; member.Email = request.Email; member.LastEmailChangedAt = (member.Email != request.Email ? DateTime.UtcNow : null); member.Name = request.Name; member.LastNameChangedAt = (member.Name != request.Name ? DateTime.UtcNow : null); member.FirstName = request.FirstName; member.LastName = request.LastName; member.Intro = request.Intro; member.Summary = request.Summary; member.Phone = request.Phone; member.Birthday = request.Birthday; member.Gender = request.Gender; member.EmailVerifiedAt = (request.IsEmailVerified && !member.IsEmailVerified && member.IsEmailVerified != request.IsEmailVerified ? DateTime.UtcNow : (request.IsEmailVerified ? member.EmailVerifiedAt : null)); member.IsEmailVerified = request.IsEmailVerified; member.AuthCertifiedAt = (request.IsAuthCertified && !member.IsAuthCertified && member.IsAuthCertified != request.IsAuthCertified ? DateTime.UtcNow : (request.IsAuthCertified ? member.AuthCertifiedAt : null)); member.IsAuthCertified = request.IsAuthCertified; member.IsDenied = request.IsDenied; member.IsAdmin = request.IsAdmin; member.DeletedAt = (request.IsWithdraw && !member.IsWithdraw && member.IsWithdraw != request.IsWithdraw ? DateTime.UtcNow : (request.IsWithdraw ? member.DeletedAt : null)); member.IsWithdraw = request.IsWithdraw; member.UpdatedAt = DateTime.UtcNow; _db.Member.Update(member); int affectedRows = await _db.SaveChangesAsync(); if (affectedRows <= 0) { throw new Exception("회원 수정 중 오류가 발생했습니다."); } string message = "회원 정보가 수정되었습니다."; TempData["SuccessMessage"] = message; _logger.LogInformation(message); return Redirect($"/Member/List/{request.ID}/Edit?{_queryString}"); } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); return await Edit(request.ID); } } [HttpGet("List/{id}/Delete")] public async Task Delete(int id) { try { if (id <= 0) { throw new Exception("유효하지 않은 접근입니다."); } var isMember = await _db.Member.AnyAsync(c => c.ID == id); if (!isMember) { throw new Exception("회원 정보를 찾을 수 없습니다."); } var member = await _db.Member.FindAsync(id); _db.Member.Remove(member); int affectedRows = await _db.SaveChangesAsync(); if (affectedRows <= 0) { throw new Exception("회원 삭제 중 오류가 발생했습니다."); } _fileUploadService.RemoveFile(member.Photo); _fileUploadService.RemoveFile(member.Icon); string message = "회원이 정상적으로 삭제되었습니다."; TempData["SuccessMessage"] = message; _logger.LogInformation(message); } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); } return Redirect($"/Member/List?{_queryString}"); } [HttpPost("List/Delete")] public async Task Delete([FromForm] int[] ids) { try { if (ids == null || ids.Length <= 0) { throw new Exception("유효하지 않은 접근입니다."); } foreach (var id in ids) { var member = await _db.Member.FindAsync(id); if (member == null) { throw new Exception($"{id}번 회원을 찾을 수 없습니다."); } _db.Member.Remove(member); int affectedRows = await _db.SaveChangesAsync(); if (affectedRows <= 0) { throw new Exception($"{id}번 회원 삭제 중 오류가 발생했습니다."); } _fileUploadService.RemoveFile(member.Photo); _fileUploadService.RemoveFile(member.Icon); } 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/List?{_queryString}"); } [HttpGet("List/{id}/Approve")] public async Task Approve(int id) { try { if (id <= 0) { throw new Exception("유효하지 않은 접근입니다."); } var memberApprove = await _db.MemberApprove.FindAsync(id); if (memberApprove is null) { throw new Exception("회원 알림 및 동의 정보를 찾을 수 없습니다."); } return View("~/Views/Member/List/Approve.cshtml", memberApprove); } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); return Redirect("/Member/List"); } } [HttpPost("List/Approve")] public async Task Approve([FromForm] MemberApprove request) { try { if (!ModelState.IsValid) { throw new Exception("유효성 검사에 실패하였습니다."); } if (!await _db.MemberApprove.AnyAsync(c => c.MemberID == request.MemberID)) { throw new ArgumentException("회원 알림 및 동의 정보를 찾을 수 없습니다."); } _db.MemberApprove.Update(request); int affectedRows = await _db.SaveChangesAsync(); if (affectedRows <= 0) { throw new Exception("수정 중 오류가 발생했습니다."); } string message = "회원 알림 및 동의 정보가 수정되었습니다."; TempData["SuccessMessage"] = message; _logger.LogInformation(message); return Redirect("/Member/List"); } catch (Exception e) { TempData["ErrorMessages"] = e.Message; _logger.LogError(e, e.Message); return await Approve(request.MemberID); } } } }