Edit.cshtml.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. using Domain.Entities.Store.ValueObject;
  2. using SharedKernel.Attributes;
  3. using SharedKernel.Extensions;
  4. using Application.Abstractions.Messaging;
  5. using Microsoft.AspNetCore.Mvc;
  6. using Microsoft.AspNetCore.Mvc.RazorPages;
  7. using Microsoft.AspNetCore.Mvc.Rendering;
  8. using System.ComponentModel;
  9. using System.ComponentModel.DataAnnotations;
  10. namespace Admin.Pages.Store.Product;
  11. public class EditModel(IMediator mediator) : PageModel
  12. {
  13. [BindProperty]
  14. public string? QueryString { get; set; }
  15. public List<SelectListItem> Games { get; set; } = [];
  16. public string? CurrentThumbnail { get; private set; }
  17. [BindProperty]
  18. public InputModel Input { get; set; } = new();
  19. public sealed class InputModel
  20. {
  21. [DisplayName("ID")]
  22. [Required]
  23. public int ID { get; set; }
  24. [DisplayName("게임")]
  25. [Required(ErrorMessage = "{0}을(를) 선택하세요.")]
  26. [Range(1, int.MaxValue, ErrorMessage = "게임을 선택해 주세요.")]
  27. public int GameID { get; set; }
  28. [DisplayName("상품명")]
  29. [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
  30. [StringLength(200, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
  31. public string Name { get; set; } = null!;
  32. [DisplayName("새 썸네일")]
  33. [AllowedExtensions("jpg,jpeg,png,gif,webp", ErrorMessage = "이미지 형식은 jpg, jpeg, png, gif, webp 만 허용합니다.")]
  34. public IFormFile? ThumbnailFile { get; set; }
  35. [DisplayName("썸네일 삭제")]
  36. public bool RemoveThumbnail { get; set; }
  37. [DisplayName("설명")]
  38. public string? Description { get; set; }
  39. [DisplayName("상품 유형")]
  40. public ProductType Type { get; set; }
  41. [DisplayName("가격(P)")]
  42. [Range(0, int.MaxValue)]
  43. public int Price { get; set; }
  44. [DisplayName("할인 모드")]
  45. public DiscountType DiscountType { get; set; } = DiscountType.None;
  46. [DisplayName("할인 값")]
  47. [Range(0, int.MaxValue)]
  48. public int DiscountValue { get; set; }
  49. [DisplayName("재고 (-1=무제한)")]
  50. [Range(-1, int.MaxValue)]
  51. public int Stock { get; set; }
  52. [DisplayName("최소 구매 수량")]
  53. [Range(1, 999, ErrorMessage = "{0}은(는) {1} 이상 {2} 이하로 입력하세요.")]
  54. public int MinPurchase { get; set; } = 1;
  55. [DisplayName("최대 구매 수량")]
  56. [Range(1, 999, ErrorMessage = "{0}은(는) {1} 이상 {2} 이하로 입력하세요.")]
  57. public int MaxPurchase { get; set; } = 999;
  58. [DisplayName("순서")]
  59. [Range(0, 9999)]
  60. public int Order { get; set; }
  61. [DisplayName("사용 여부")]
  62. public bool IsActive { get; set; }
  63. [DisplayName("판매 시작")]
  64. [DataType(DataType.DateTime)]
  65. public DateTime? SaleStartAt { get; set; }
  66. [DisplayName("판매 종료")]
  67. [DataType(DataType.DateTime)]
  68. public DateTime? SaleEndAt { get; set; }
  69. [DisplayName("후원 채널 필수")]
  70. public bool RequireDonationChannel { get; set; }
  71. }
  72. public async Task OnGetAsync(int id, CancellationToken ct)
  73. {
  74. await LoadGamesAsync(ct);
  75. var result = await mediator.Send(new GetProduct.Query(id), ct);
  76. if (result.IsFailure)
  77. {
  78. TempData["ErrorMessages"] = result.Error.Description;
  79. QueryString = Request.QueryString.ToString();
  80. return;
  81. }
  82. var data = result.Value;
  83. CurrentThumbnail = data.Thumbnail;
  84. // DB는 UTC 저장 규약. datetime-local input은 사용자 로컬 시각을 기대하므로 표시 시점에 변환.
  85. Input = new InputModel
  86. {
  87. ID = data.ID,
  88. GameID = data.GameID,
  89. Name = data.Name,
  90. Description = data.Description,
  91. Type = data.Type,
  92. Price = data.Price,
  93. DiscountType = data.DiscountType,
  94. DiscountValue = data.DiscountValue,
  95. Stock = data.Stock,
  96. MinPurchase = data.MinPurchase,
  97. MaxPurchase = data.MaxPurchase,
  98. Order = data.Order,
  99. IsActive = data.IsActive,
  100. SaleStartAt = data.SaleStartAt.HasValue
  101. ? DateTime.SpecifyKind(data.SaleStartAt.Value, DateTimeKind.Utc).ToLocalTime()
  102. : null,
  103. SaleEndAt = data.SaleEndAt.HasValue
  104. ? DateTime.SpecifyKind(data.SaleEndAt.Value, DateTimeKind.Utc).ToLocalTime()
  105. : null,
  106. RequireDonationChannel = data.RequireDonationChannel
  107. };
  108. QueryString = Request.QueryString.ToString();
  109. }
  110. public async Task<IActionResult> OnPostAsync(CancellationToken ct)
  111. {
  112. if (!ModelState.IsValid)
  113. {
  114. TempData["ErrorMessages"] = ModelState.GetErrorMessages();
  115. return Redirect($"/Store/Product/Edit/{Input.ID}{Request.QueryString}");
  116. }
  117. // datetime-local input은 사용자 로컬 시각(Kind=Unspecified). DB는 UTC 저장 규약 → 명시 변환.
  118. var saleStartUtc = Input.SaleStartAt.HasValue
  119. ? DateTime.SpecifyKind(Input.SaleStartAt.Value, DateTimeKind.Local).ToUniversalTime()
  120. : (DateTime?)null;
  121. var saleEndUtc = Input.SaleEndAt.HasValue
  122. ? DateTime.SpecifyKind(Input.SaleEndAt.Value, DateTimeKind.Local).ToUniversalTime()
  123. : (DateTime?)null;
  124. var result = await mediator.Send(new UpdateProduct.Command(
  125. Input.ID,
  126. Input.GameID,
  127. Input.Name,
  128. Input.ThumbnailFile,
  129. Input.RemoveThumbnail,
  130. Input.Description,
  131. Input.Type,
  132. Input.Price,
  133. Input.DiscountType,
  134. Input.DiscountValue,
  135. Input.Stock,
  136. Input.MinPurchase,
  137. Input.MaxPurchase,
  138. Input.Order,
  139. Input.IsActive,
  140. saleStartUtc,
  141. saleEndUtc,
  142. Input.RequireDonationChannel
  143. ), ct);
  144. if (result.IsFailure)
  145. {
  146. TempData["ErrorMessages"] = result.Error.Description;
  147. }
  148. else
  149. {
  150. TempData["SuccessMessage"] = $"{Input.Name} 상품이 수정되었습니다.";
  151. }
  152. return Redirect($"/Store/Product/Edit/{Input.ID}{Request.QueryString}");
  153. }
  154. private async Task LoadGamesAsync(CancellationToken ct)
  155. {
  156. var result = await mediator.Send(new GetGames.Query(false), ct);
  157. Games = [.. result.List.Select(g => new SelectListItem
  158. {
  159. Value = g.ID.ToString(),
  160. Text = $"{g.KorName} ({g.Publisher})"
  161. })];
  162. }
  163. }