ListController.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596
  1. using System.Diagnostics;
  2. using Microsoft.AspNetCore.Mvc;
  3. using Microsoft.AspNetCore.Mvc.Filters;
  4. using Microsoft.AspNetCore.Mvc.Rendering;
  5. using Microsoft.AspNetCore.Authorization;
  6. using Microsoft.AspNetCore.WebUtilities;
  7. using Microsoft.EntityFrameworkCore;
  8. using bitforum.Helpers;
  9. using bitforum.Models;
  10. using bitforum.Models.Account;
  11. using bitforum.Services;
  12. using bitforum.Constants;
  13. namespace bitforum.Controllers.Member
  14. {
  15. [Authorize]
  16. [Route("Member")]
  17. public class ListController : Controller
  18. {
  19. private readonly ILogger<ListController> _logger;
  20. private readonly IFileUploadService _fileUploadService;
  21. private readonly DefaultDbContext _db;
  22. private readonly string _IndexViewPath = "~/Views/Member/List/Index.cshtml";
  23. private readonly string _WriteViewPath = "~/Views/Member/List/Write.cshtml";
  24. private readonly string _EditViewPath = "~/Views/Member/List/Edit.cshtml";
  25. private string? _queryString = null;
  26. public ListController(ILogger<ListController> logger, IFileUploadService fileUploadService, DefaultDbContext db)
  27. {
  28. _logger = logger;
  29. _fileUploadService = fileUploadService;
  30. _db = db;
  31. }
  32. [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
  33. public IActionResult Error()
  34. {
  35. return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
  36. }
  37. public override void OnActionExecuting(ActionExecutingContext context)
  38. {
  39. ViewBag.QueryString = _queryString = string.Join("&", QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).Select(r => $"{r.Key}={r.Value.FirstOrDefault()}"));
  40. base.OnActionExecuting(context);
  41. }
  42. [HttpGet("List")]
  43. public async Task<IActionResult> Index(
  44. [FromQuery]
  45. 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
  46. ) {
  47. var query = _db.Member.AsQueryable();
  48. // 검색 조건 적용
  49. if (!string.IsNullOrEmpty(keyword) && search.HasValue)
  50. {
  51. switch (search)
  52. {
  53. case 1: // ID 검색
  54. if (int.TryParse(keyword, out int memberID))
  55. {
  56. query = query.Where(m => m.ID == memberID);
  57. }
  58. else
  59. {
  60. query = query.Where(m => m.ID == 0);
  61. }
  62. break;
  63. case 2: // 이메일 검색
  64. query = query.Where(m => m.Email.Contains(keyword));
  65. break;
  66. case 3: // 별명 검색
  67. query = query.Where(m => m.Name != null && m.Name.Contains(keyword));
  68. break;
  69. case 4: // 이름 검색
  70. query = query.Where(m => m.FullName != null && m.FullName.Contains(keyword));
  71. break;
  72. case 5: // 연락처 검색
  73. query = query.Where(m => m.Phone != null && m.Phone.Contains(keyword));
  74. break;
  75. }
  76. }
  77. if (tab > 0)
  78. {
  79. switch (tab)
  80. {
  81. case 1: // 차단
  82. query = query.Where(m => m.IsDenied);
  83. break;
  84. case 2: // 탈퇴
  85. query = query.Where(m => m.IsWithdraw);
  86. break;
  87. case 3: // 관리자
  88. query = query.Where(m => m.IsAdmin);
  89. break;
  90. }
  91. }
  92. if (gender is not null) // 성별
  93. {
  94. query = query.Where(m => m.Gender == gender);
  95. }
  96. if (isEmailVerified > 0) // 이메일 인증 여부
  97. {
  98. query = query.Where(m => m.IsEmailVerified == true);
  99. }
  100. if (isAuthCertified > 0) // 본인 인증 여부
  101. {
  102. query = query.Where(m => m.IsAuthCertified == true);
  103. }
  104. // 가입 일시
  105. var today = DateTime.UtcNow.Date;
  106. if (startAt != null && DateTime.TryParse(startAt, out var st))
  107. {
  108. query = query.Where(log => log.CreatedAt >= st);
  109. }
  110. if (endAt != null && DateTime.TryParse(endAt, out var et))
  111. {
  112. query = query.Where(log => log.CreatedAt <= et);
  113. }
  114. query = query.OrderByDescending(c => c.CreatedAt);
  115. var members = await query
  116. .Skip((page - 1) * perPage)
  117. .Take(perPage)
  118. .Select(c => new
  119. {
  120. GradeName = _db.MemberGrade.Where(m => m.ID == c.GradeID).Select(m => m.KorName).FirstOrDefault(), // 회원등급 이름
  121. c.ID,
  122. c.SID,
  123. c.Email,
  124. c.Name,
  125. c.FullName,
  126. c.Icon,
  127. c.Coin,
  128. c.Exp,
  129. c.Phone,
  130. c.Gender,
  131. c.Birthday,
  132. c.Following,
  133. c.Followed,
  134. c.IsEmailVerified,
  135. c.IsAuthCertified,
  136. c.IsDenied,
  137. c.IsAdmin,
  138. c.IsWithdraw,
  139. c.LastLoginIp,
  140. c.LastLoginAt,
  141. c.CreatedAt,
  142. c.UpdatedAt
  143. })
  144. .ToListAsync();
  145. var data = new List<object>();
  146. if (members.Count > 0)
  147. {
  148. foreach (var row in members)
  149. {
  150. data.Add(new
  151. {
  152. row.GradeName,
  153. row.ID,
  154. row.Email,
  155. row.Name,
  156. row.FullName,
  157. row.Icon,
  158. Phone = row?.Phone ?? "-",
  159. Gender = row.Gender.HasValue ? row.Gender.ToString() : "-",
  160. Birthday = row.Birthday?.ToString("yyyy.MM.dd"),
  161. Coin = row.Coin.ToString("N0"),
  162. Exp = row.Exp.ToString("N0"),
  163. Following = row.Following.ToString("N0"),
  164. Followed = row.Followed.ToString("N0"),
  165. row.IsEmailVerified,
  166. row.IsAuthCertified,
  167. row.IsDenied,
  168. row.IsAdmin,
  169. row.IsWithdraw,
  170. row.LastLoginIp,
  171. LastLoginAt = row.LastLoginAt?.ToString("yyyy.MM.dd"),
  172. CreatedAt = row.CreatedAt.ToString("yyyy.MM.dd"),
  173. UpdatedAt = row.UpdatedAt?.ToString("yyyy.MM.dd"),
  174. ApproveURL = $"/Member/List/{row.ID}/Approve?{_queryString}",
  175. EditURL = $"/Member/List/{row.ID}/Edit?{_queryString}",
  176. DeleteURL = $"/Member/List/{row.ID}/Delete?{_queryString}"
  177. });
  178. }
  179. }
  180. var parameter = new
  181. {
  182. Page = page,
  183. PerPage = perPage,
  184. Search = search,
  185. Keyword = keyword,
  186. StartAt = startAt,
  187. EndAt = endAt,
  188. Tab = tab,
  189. IsEmailVerified = isEmailVerified,
  190. IsAuthCertified = isAuthCertified,
  191. Gender = gender,
  192. };
  193. ViewBag.Data = data;
  194. ViewBag.Total = await query.CountAsync();
  195. ViewBag.Parameter = parameter;
  196. ViewBag.Pagination = new Pagination(ViewBag.Total, page, perPage, parameter);
  197. return View(_IndexViewPath);
  198. }
  199. [HttpGet("List/Write")]
  200. public IActionResult Write()
  201. {
  202. ViewBag.MemberGrades = new SelectList(
  203. _db.MemberGrade.Where(c => c.IsActive).OrderByDescending(c => c.Order),
  204. "ID",
  205. "KorName",
  206. null
  207. );
  208. return View(_WriteViewPath);
  209. }
  210. [HttpPost("List/Create")]
  211. public async Task<IActionResult> Create([FromForm] Models.Account.Member request, IFormFile? photo, IFormFile? icon)
  212. {
  213. try
  214. {
  215. if (!ModelState.IsValid)
  216. {
  217. throw new Exception("유효성 검사에 실패하였습니다.");
  218. }
  219. if (request.FirstName != null || request.LastName != null)
  220. {
  221. request.FullName = $"{request.FirstName}{request.LastName}";
  222. }
  223. // 이메일 중복 확인
  224. if (await _db.Member.AnyAsync(c => c.Email == request.Email))
  225. {
  226. throw new ArgumentException("이미 등록된 이메일 주소입니다.");
  227. }
  228. // 별명 중복 확인
  229. if (await _db.Member.AnyAsync(c => c.Name == request.Name))
  230. {
  231. throw new ArgumentException("이미 등록된 별명입니다.");
  232. }
  233. request.Password = BCrypt.Net.BCrypt.HashPassword(request.Password);
  234. request.PasswordUpdatedAt = DateTime.UtcNow;
  235. request.SignupIP = HttpContext.GetClientIP();
  236. request.CreatedAt = DateTime.UtcNow;
  237. await _db.Member.AddAsync(request);
  238. int affectedRows = await _db.SaveChangesAsync();
  239. if (affectedRows <= 0)
  240. {
  241. throw new Exception("회원 등록 중 오류가 발생했습니다.");
  242. }
  243. request.Photo = await _fileUploadService.UploadImageAsync(photo, UploadFolder.Member, request.ID);
  244. request.Icon = await _fileUploadService.UploadImageAsync(icon, UploadFolder.Member, request.ID);
  245. request.EmailVerifiedAt = (request.IsEmailVerified ? DateTime.UtcNow : null);
  246. request.AuthCertifiedAt = (request.IsAuthCertified ? DateTime.UtcNow : null);
  247. request.DeletedAt = (request.IsWithdraw ? DateTime.UtcNow : null);
  248. // 회원의 약관 및 알림 동의 정보 생성
  249. await _db.MemberApprove.AddAsync(new MemberApprove
  250. {
  251. MemberID = request.ID,
  252. IsReceiveSMS = false,
  253. IsReceiveEmail = false,
  254. IsReceiveNote = false,
  255. IsDisclosureInvest = false
  256. });
  257. await _db.SaveChangesAsync();
  258. string message = "회원이 등록되었습니다.";
  259. TempData["SuccessMessage"] = message;
  260. _logger.LogInformation(message);
  261. return Redirect("/Member/List");
  262. }
  263. catch (ArgumentException e)
  264. {
  265. TempData["ErrorMessages"] = e.Message;
  266. _logger.LogError(e, e.Message);
  267. return Write();
  268. }
  269. catch (Exception e)
  270. {
  271. TempData["ErrorMessages"] = e.Message;
  272. _logger.LogError(e, e.Message);
  273. return Write();
  274. }
  275. }
  276. [HttpGet("List/{id}/Edit")]
  277. public async Task<IActionResult> Edit(int id)
  278. {
  279. try
  280. {
  281. if (id <= 0)
  282. {
  283. throw new Exception("유효하지 않은 접근입니다.");
  284. }
  285. var member = await _db.Member.Where(m => m.ID == id).FirstOrDefaultAsync();
  286. if (member is null)
  287. {
  288. throw new Exception("회원 정보를 찾을 수 없습니다.");
  289. }
  290. ViewBag.MemberGrades = new SelectList(
  291. _db.MemberGrade.Where(c => c.IsActive).OrderByDescending(c => c.Order),
  292. "ID",
  293. "KorName",
  294. member.GradeID
  295. );
  296. return View(_EditViewPath, member);
  297. }
  298. catch (Exception e)
  299. {
  300. TempData["ErrorMessages"] = e.Message;
  301. _logger.LogError(e, e.Message);
  302. return Redirect("/Member/List");
  303. }
  304. }
  305. [HttpPost("List/Update")]
  306. public async Task<IActionResult> Update([FromForm] Models.Account.Member request, IFormFile? photo, IFormFile? icon, [FromForm] bool IsPhotoRemove = false, bool IsIconRemove = false)
  307. {
  308. try
  309. {
  310. if (!ModelState.IsValid)
  311. {
  312. throw new Exception("유효성 검사에 실패하였습니다.");
  313. }
  314. var member = await _db.Member.FirstOrDefaultAsync(c => c.ID == request.ID);
  315. if (member is null)
  316. {
  317. throw new Exception("회원 정보를 찾을 수 없습니다.");
  318. }
  319. if (request.FirstName != null || request.LastName != null)
  320. {
  321. member.FullName = $"{request.FirstName}{request.LastName}";
  322. }
  323. // 이메일 중복 확인
  324. if (await _db.Member.AnyAsync(c => c.Email == request.Email && c.ID != member.ID))
  325. {
  326. throw new ArgumentException("이미 등록된 이메일 주소입니다.");
  327. }
  328. // 별명 중복 확인
  329. if (await _db.Member.AnyAsync(c => c.Name == request.Name && c.ID != member.ID))
  330. {
  331. throw new ArgumentException("이미 등록된 별명입니다.");
  332. }
  333. // 비밀번호 변경
  334. if (!string.IsNullOrEmpty(request.Password))
  335. {
  336. member.Password = BCrypt.Net.BCrypt.HashPassword(request.Password);
  337. member.PasswordUpdatedAt = DateTime.UtcNow;
  338. }
  339. // 사진 저장
  340. if (IsPhotoRemove)
  341. {
  342. _fileUploadService.RemoveFile(request.Photo);
  343. member.Photo = null;
  344. }
  345. else if (photo is not null)
  346. {
  347. member.Photo = await _fileUploadService.UploadImageAsync(photo, UploadFolder.Grade, member.ID);
  348. }
  349. // 아이콘 저장
  350. if (IsIconRemove)
  351. {
  352. _fileUploadService.RemoveFile(request.Photo);
  353. member.Icon = null;
  354. }
  355. else if (icon is not null)
  356. {
  357. member.Icon = await _fileUploadService.UploadImageAsync(icon, UploadFolder.Grade, member.ID);
  358. }
  359. member.GradeID = request.GradeID;
  360. member.Email = request.Email;
  361. member.LastEmailChangedAt = (member.Email != request.Email ? DateTime.UtcNow : null);
  362. member.Name = request.Name;
  363. member.LastNameChangedAt = (member.Name != request.Name ? DateTime.UtcNow : null);
  364. member.FirstName = request.FirstName;
  365. member.LastName = request.LastName;
  366. member.Intro = request.Intro;
  367. member.Summary = request.Summary;
  368. member.Phone = request.Phone;
  369. member.Birthday = request.Birthday;
  370. member.Gender = request.Gender;
  371. member.EmailVerifiedAt = (request.IsEmailVerified && !member.IsEmailVerified && member.IsEmailVerified != request.IsEmailVerified ? DateTime.UtcNow : (request.IsEmailVerified ? member.EmailVerifiedAt : null));
  372. member.IsEmailVerified = request.IsEmailVerified;
  373. member.AuthCertifiedAt = (request.IsAuthCertified && !member.IsAuthCertified && member.IsAuthCertified != request.IsAuthCertified ? DateTime.UtcNow : (request.IsAuthCertified ? member.AuthCertifiedAt : null));
  374. member.IsAuthCertified = request.IsAuthCertified;
  375. member.IsDenied = request.IsDenied;
  376. member.IsAdmin = request.IsAdmin;
  377. member.DeletedAt = (request.IsWithdraw && !member.IsWithdraw && member.IsWithdraw != request.IsWithdraw ? DateTime.UtcNow : (request.IsWithdraw ? member.DeletedAt : null));
  378. member.IsWithdraw = request.IsWithdraw;
  379. member.UpdatedAt = DateTime.UtcNow;
  380. _db.Member.Update(member);
  381. int affectedRows = await _db.SaveChangesAsync();
  382. if (affectedRows <= 0)
  383. {
  384. throw new Exception("회원 수정 중 오류가 발생했습니다.");
  385. }
  386. string message = "회원 정보가 수정되었습니다.";
  387. TempData["SuccessMessage"] = message;
  388. _logger.LogInformation(message);
  389. return Redirect($"/Member/List/{request.ID}/Edit?{_queryString}");
  390. }
  391. catch (Exception e)
  392. {
  393. TempData["ErrorMessages"] = e.Message;
  394. _logger.LogError(e, e.Message);
  395. return await Edit(request.ID);
  396. }
  397. }
  398. [HttpGet("List/{id}/Delete")]
  399. public async Task<IActionResult> Delete(int id)
  400. {
  401. try
  402. {
  403. if (id <= 0)
  404. {
  405. throw new Exception("유효하지 않은 접근입니다.");
  406. }
  407. var isMember = await _db.Member.AnyAsync(c => c.ID == id);
  408. if (!isMember)
  409. {
  410. throw new Exception("회원 정보를 찾을 수 없습니다.");
  411. }
  412. var member = await _db.Member.FindAsync(id);
  413. _db.Member.Remove(member);
  414. int affectedRows = await _db.SaveChangesAsync();
  415. if (affectedRows <= 0)
  416. {
  417. throw new Exception("회원 삭제 중 오류가 발생했습니다.");
  418. }
  419. _fileUploadService.RemoveFile(member.Photo);
  420. _fileUploadService.RemoveFile(member.Icon);
  421. string message = "회원이 정상적으로 삭제되었습니다.";
  422. TempData["SuccessMessage"] = message;
  423. _logger.LogInformation(message);
  424. }
  425. catch (Exception e)
  426. {
  427. TempData["ErrorMessages"] = e.Message;
  428. _logger.LogError(e, e.Message);
  429. }
  430. return Redirect($"/Member/List?{_queryString}");
  431. }
  432. [HttpPost("List/Delete")]
  433. public async Task<IActionResult> Delete([FromForm] int[] ids)
  434. {
  435. try
  436. {
  437. if (ids == null || ids.Length <= 0)
  438. {
  439. throw new Exception("유효하지 않은 접근입니다.");
  440. }
  441. foreach (var id in ids)
  442. {
  443. var member = await _db.Member.FindAsync(id);
  444. if (member == null)
  445. {
  446. throw new Exception($"{id}번 회원을 찾을 수 없습니다.");
  447. }
  448. _db.Member.Remove(member);
  449. int affectedRows = await _db.SaveChangesAsync();
  450. if (affectedRows <= 0)
  451. {
  452. throw new Exception($"{id}번 회원 삭제 중 오류가 발생했습니다.");
  453. }
  454. _fileUploadService.RemoveFile(member.Photo);
  455. _fileUploadService.RemoveFile(member.Icon);
  456. }
  457. string message = $"{ids.Length}건의 회원이 삭제되었습니다.";
  458. TempData["SuccessMessage"] = message;
  459. _logger.LogInformation(message);
  460. }
  461. catch (Exception e)
  462. {
  463. TempData["ErrorMessages"] = e.Message;
  464. _logger.LogError(e, e.Message);
  465. }
  466. return Redirect($"/Member/List?{_queryString}");
  467. }
  468. [HttpGet("List/{id}/Approve")]
  469. public async Task<IActionResult> Approve(int id)
  470. {
  471. try
  472. {
  473. if (id <= 0)
  474. {
  475. throw new Exception("유효하지 않은 접근입니다.");
  476. }
  477. var memberApprove = await _db.MemberApprove.FindAsync(id);
  478. if (memberApprove is null)
  479. {
  480. throw new Exception("회원 알림 및 동의 정보를 찾을 수 없습니다.");
  481. }
  482. return View("~/Views/Member/List/Approve.cshtml", memberApprove);
  483. }
  484. catch (Exception e)
  485. {
  486. TempData["ErrorMessages"] = e.Message;
  487. _logger.LogError(e, e.Message);
  488. return Redirect("/Member/List");
  489. }
  490. }
  491. [HttpPost("List/Approve")]
  492. public async Task<IActionResult> Approve([FromForm] MemberApprove request)
  493. {
  494. try
  495. {
  496. if (!ModelState.IsValid)
  497. {
  498. throw new Exception("유효성 검사에 실패하였습니다.");
  499. }
  500. if (!await _db.MemberApprove.AnyAsync(c => c.MemberID == request.MemberID))
  501. {
  502. throw new ArgumentException("회원 알림 및 동의 정보를 찾을 수 없습니다.");
  503. }
  504. _db.MemberApprove.Update(request);
  505. int affectedRows = await _db.SaveChangesAsync();
  506. if (affectedRows <= 0)
  507. {
  508. throw new Exception("수정 중 오류가 발생했습니다.");
  509. }
  510. string message = "회원 알림 및 동의 정보가 수정되었습니다.";
  511. TempData["SuccessMessage"] = message;
  512. _logger.LogInformation(message);
  513. return Redirect("/Member/List");
  514. }
  515. catch (Exception e)
  516. {
  517. TempData["ErrorMessages"] = e.Message;
  518. _logger.LogError(e, e.Message);
  519. return await Approve(request.MemberID);
  520. }
  521. }
  522. }
  523. }