Codes.cshtml.cs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. using Domain.Entities.Store.ValueObject;
  2. using SharedKernel.Attributes;
  3. using SharedKernel.Extensions;
  4. using SharedKernel.Helpers;
  5. using Application.Abstractions.Messaging;
  6. using Microsoft.AspNetCore.Mvc;
  7. using Microsoft.AspNetCore.Mvc.RazorPages;
  8. using System.ComponentModel;
  9. using System.ComponentModel.DataAnnotations;
  10. namespace Admin.Pages.Store.Coupon;
  11. public class CodesModel(IMediator mediator) : PageModel
  12. {
  13. [BindProperty(SupportsGet = true)]
  14. public int ID { get; set; }
  15. [BindProperty(SupportsGet = true)]
  16. public QueryParams Query { get; set; } = new();
  17. public string CouponName { get; private set; } = "";
  18. public string GameName { get; private set; } = "";
  19. public string ProductName { get; private set; } = "";
  20. public int Total { get; set; }
  21. public int AvailableCount { get; set; }
  22. public int ReservedCount { get; set; }
  23. public int IssuedCount { get; set; }
  24. public int UsedCount { get; set; }
  25. public int ExpiredCount { get; set; }
  26. public sealed class QueryParams
  27. {
  28. [Range(1, int.MaxValue)]
  29. public int PageNum { get; set; } = 1;
  30. [Range(1, 200)]
  31. public ushort PerPage { get; set; } = 50;
  32. [DisplayName("코드 상태")]
  33. public CouponCodeStatus? Status { get; set; }
  34. }
  35. public List<(
  36. int Num,
  37. int ID,
  38. string Code,
  39. string StatusText,
  40. int? IssuedToMemberID,
  41. string? IssuedToMemberName,
  42. string? IssuedToMemberEmail,
  43. string? IssuedAt,
  44. string? UsedAt,
  45. string CreatedAt
  46. )> List { get; set; } = [];
  47. public Pagination? Pagination { get; set; }
  48. [BindProperty]
  49. public UploadInput Upload { get; set; } = new();
  50. public sealed class UploadInput
  51. {
  52. [DisplayName("CSV 파일")]
  53. [Required(ErrorMessage = "{0}을(를) 선택하세요.")]
  54. [AllowedExtensions("csv,txt", ErrorMessage = "CSV 또는 TXT 파일만 허용됩니다.")]
  55. public IFormFile? CsvFile { get; set; }
  56. }
  57. public async Task OnGetAsync(CancellationToken ct)
  58. {
  59. if (!ModelState.IsValid)
  60. {
  61. return;
  62. }
  63. await LoadHeaderAsync(ct);
  64. await LoadListAsync(ct);
  65. }
  66. public async Task<IActionResult> OnPostUploadAsync(CancellationToken ct)
  67. {
  68. if (!ModelState.IsValid)
  69. {
  70. TempData["ErrorMessages"] = ModelState.GetErrorMessages();
  71. return RedirectToPage("/Store/Coupon/Codes", new { id = ID });
  72. }
  73. var result = await mediator.Send(new ImportCouponCodes.Command(ID, Upload.CsvFile!), ct);
  74. if (result.IsFailure)
  75. {
  76. TempData["ErrorMessages"] = result.Error.Description;
  77. }
  78. else
  79. {
  80. var r = result.Value;
  81. TempData["SuccessMessage"] =
  82. $"CSV 업로드 완료. 총 {r.TotalLines}줄 / 추가 {r.Added}개 (미사용 {r.AddedAvailable} + 사용됨 {r.AddedUsed}) / 중복 skip {r.SkippedDuplicate}개 / 회원미존재 skip {r.SkippedMissingMember}개 / 무효 skip {r.SkippedInvalid}개.";
  83. }
  84. return RedirectToPage("/Store/Coupon/Codes", new { id = ID });
  85. }
  86. public async Task<IActionResult> OnGetExportAsync(int id, CancellationToken ct)
  87. {
  88. var result = await mediator.Send(new ExportCouponCodes.Query(id), ct);
  89. if (result.IsFailure)
  90. {
  91. TempData["ErrorMessages"] = result.Error.Description;
  92. return RedirectToPage("/Store/Coupon/Codes", new { id });
  93. }
  94. var data = result.Value;
  95. return File(data.CsvBytes, "text/csv; charset=utf-8", data.FileName);
  96. }
  97. public async Task<IActionResult> OnPostDeleteCodesAsync(int id, int[] codeIDs, CancellationToken ct)
  98. {
  99. if (codeIDs is null || codeIDs.Length == 0)
  100. {
  101. TempData["ErrorMessages"] = "삭제할 코드를 선택해 주세요.";
  102. return RedirectToPage("/Store/Coupon/Codes", new { id });
  103. }
  104. var result = await mediator.Send(new DeleteCouponCodes.Command(id, codeIDs), ct);
  105. if (result.IsFailure)
  106. {
  107. TempData["ErrorMessages"] = result.Error.Description;
  108. }
  109. else
  110. {
  111. TempData["SuccessMessage"] = $"{result.Value.Deleted}개 코드가 삭제되었습니다.";
  112. }
  113. return RedirectToPage("/Store/Coupon/Codes", new { id });
  114. }
  115. public async Task<IActionResult> OnPostExpireCodesAsync(int id, int[] codeIDs, CancellationToken ct)
  116. {
  117. if (codeIDs is null || codeIDs.Length == 0)
  118. {
  119. TempData["ErrorMessages"] = "만료 처리할 코드를 선택해 주세요.";
  120. return RedirectToPage("/Store/Coupon/Codes", new { id });
  121. }
  122. var result = await mediator.Send(new ExpireCouponCodes.Command(id, codeIDs), ct);
  123. if (result.IsFailure)
  124. {
  125. TempData["ErrorMessages"] = result.Error.Description;
  126. }
  127. else
  128. {
  129. TempData["SuccessMessage"] = $"{result.Value.Expired}개 코드가 만료 처리되었습니다.";
  130. }
  131. return RedirectToPage("/Store/Coupon/Codes", new { id });
  132. }
  133. public async Task<IActionResult> OnPostRestoreCodesAsync(int id, int[] codeIDs, CancellationToken ct)
  134. {
  135. if (codeIDs is null || codeIDs.Length == 0)
  136. {
  137. TempData["ErrorMessages"] = "복귀 처리할 코드를 선택해 주세요.";
  138. return RedirectToPage("/Store/Coupon/Codes", new { id });
  139. }
  140. var result = await mediator.Send(new RestoreCouponCodes.Command(id, codeIDs), ct);
  141. if (result.IsFailure)
  142. {
  143. TempData["ErrorMessages"] = result.Error.Description;
  144. }
  145. else
  146. {
  147. TempData["SuccessMessage"] = $"{result.Value.Restored}개 코드가 복귀 처리되었습니다.";
  148. }
  149. return RedirectToPage("/Store/Coupon/Codes", new { id });
  150. }
  151. public IActionResult OnGetTemplate()
  152. {
  153. // 헤더만 담긴 빈 양식
  154. var header = new[]
  155. {
  156. "ID",
  157. "Code",
  158. "Status",
  159. "IssuedToMemberID",
  160. "IssuedToMemberName",
  161. "IssuedToMemberEmail",
  162. "IssuedAt",
  163. "UsedAt",
  164. "CreatedAt"
  165. };
  166. var bytes = SharedKernel.Helpers.CsvHelper.WriteUtf8Bom([header]);
  167. return File(bytes, "text/csv; charset=utf-8", "coupon-codes-template.csv");
  168. }
  169. private async Task LoadHeaderAsync(CancellationToken ct)
  170. {
  171. var head = await mediator.Send(new GetCoupon.Query(ID), ct);
  172. if (head.IsFailure)
  173. {
  174. TempData["ErrorMessages"] = head.Error.Description;
  175. return;
  176. }
  177. CouponName = head.Value.Name;
  178. GameName = head.Value.GameName;
  179. ProductName = head.Value.ProductName;
  180. }
  181. private async Task LoadListAsync(CancellationToken ct)
  182. {
  183. var result = await mediator.Send(new ListCouponCodes.Query(ID, Query.Status, Query.PageNum, Query.PerPage), ct);
  184. Total = result.Total;
  185. AvailableCount = result.AvailableCount;
  186. ReservedCount = result.ReservedCount;
  187. IssuedCount = result.IssuedCount;
  188. UsedCount = result.UsedCount;
  189. ExpiredCount = result.ExpiredCount;
  190. List = [.. result.List.Select(c => (
  191. c.Num,
  192. c.ID,
  193. c.Code,
  194. StatusText: c.Status switch
  195. {
  196. CouponCodeStatus.Available => "사용 가능",
  197. CouponCodeStatus.Reserved => "결제 진행 중",
  198. CouponCodeStatus.Issued => "발급됨",
  199. CouponCodeStatus.Used => "사용됨",
  200. CouponCodeStatus.Expired => "만료",
  201. _ => "?"
  202. },
  203. c.IssuedToMemberID,
  204. c.IssuedToMemberName,
  205. c.IssuedToMemberEmail,
  206. c.IssuedAt?.ToString("yyyy-MM-dd HH:mm"),
  207. c.UsedAt?.ToString("yyyy-MM-dd HH:mm"),
  208. c.CreatedAt.GetDateAt()
  209. ))];
  210. Pagination = new Pagination(result.Total, Query.PageNum, Query.PerPage);
  211. }
  212. }