Edit.cshtml.cs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  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. public int? GameID { get; set; }
  26. [DisplayName("상품명")]
  27. [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
  28. [StringLength(200, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
  29. public string Name { get; set; } = null!;
  30. [DisplayName("새 썸네일")]
  31. [AllowedExtensions("jpg,jpeg,png,gif,webp", ErrorMessage = "이미지 형식은 jpg, jpeg, png, gif, webp 만 허용합니다.")]
  32. public IFormFile? ThumbnailFile { get; set; }
  33. [DisplayName("썸네일 삭제")]
  34. public bool RemoveThumbnail { get; set; }
  35. [DisplayName("설명")]
  36. public string? Description { get; set; }
  37. [DisplayName("상품 유형")]
  38. public ProductType Type { get; set; }
  39. [DisplayName("가격(P)")]
  40. [Range(0, int.MaxValue)]
  41. public int Price { get; set; }
  42. [DisplayName("할인 모드")]
  43. public DiscountType DiscountType { get; set; } = DiscountType.None;
  44. [DisplayName("할인 값")]
  45. [Range(0, int.MaxValue)]
  46. public int DiscountValue { get; set; }
  47. [DisplayName("재고 (-1=무제한)")]
  48. [Range(-1, int.MaxValue)]
  49. public int Stock { get; set; }
  50. [DisplayName("최소 구매 수량")]
  51. [Range(1, 999, ErrorMessage = "{0}은(는) {1} 이상 {2} 이하로 입력하세요.")]
  52. public int MinPurchase { get; set; } = 1;
  53. [DisplayName("최대 구매 수량")]
  54. [Range(1, 999, ErrorMessage = "{0}은(는) {1} 이상 {2} 이하로 입력하세요.")]
  55. public int MaxPurchase { get; set; } = 999;
  56. [DisplayName("순서")]
  57. [Range(0, 9999)]
  58. public int Order { get; set; }
  59. [DisplayName("사용 여부")]
  60. public bool IsActive { get; set; }
  61. [DisplayName("판매 시작")]
  62. [DataType(DataType.DateTime)]
  63. public DateTime? SaleStartAt { get; set; }
  64. [DisplayName("판매 종료")]
  65. [DataType(DataType.DateTime)]
  66. public DateTime? SaleEndAt { get; set; }
  67. [DisplayName("후원 채널 필수")]
  68. public bool RequireDonationChannel { get; set; }
  69. [DisplayName("아이템 종류")]
  70. public ItemKind? ItemKind { get; set; }
  71. [DisplayName("아이템 효과 지속일")]
  72. [Range(0, 3650)]
  73. public int DurationDays { get; set; }
  74. [DisplayName("효과 페이로드(JSON)")]
  75. public string? EffectPayload { get; set; }
  76. [DisplayName("선물하기 가능")]
  77. public bool IsGiftable { get; set; }
  78. }
  79. public async Task OnGetAsync(int id, CancellationToken ct)
  80. {
  81. await LoadGamesAsync(ct);
  82. var result = await mediator.Send(new GetProduct.Query(id), ct);
  83. if (result.IsFailure)
  84. {
  85. TempData["ErrorMessages"] = result.Error.Description;
  86. QueryString = Request.QueryString.ToString();
  87. return;
  88. }
  89. var data = result.Value;
  90. CurrentThumbnail = data.Thumbnail;
  91. // DB는 UTC 저장 규약. datetime-local input은 사용자 로컬 시각을 기대하므로 표시 시점에 변환.
  92. Input = new InputModel
  93. {
  94. ID = data.ID,
  95. GameID = data.GameID > 0 ? data.GameID : (int?)null,
  96. Name = data.Name,
  97. Description = data.Description,
  98. Type = data.Type,
  99. Price = data.Price,
  100. DiscountType = data.DiscountType,
  101. DiscountValue = data.DiscountValue,
  102. Stock = data.Stock,
  103. MinPurchase = data.MinPurchase,
  104. MaxPurchase = data.MaxPurchase,
  105. Order = data.Order,
  106. IsActive = data.IsActive,
  107. SaleStartAt = data.SaleStartAt.HasValue
  108. ? DateTime.SpecifyKind(data.SaleStartAt.Value, DateTimeKind.Utc).ToLocalTime()
  109. : null,
  110. SaleEndAt = data.SaleEndAt.HasValue
  111. ? DateTime.SpecifyKind(data.SaleEndAt.Value, DateTimeKind.Utc).ToLocalTime()
  112. : null,
  113. RequireDonationChannel = data.RequireDonationChannel,
  114. ItemKind = data.ItemKind,
  115. DurationDays = data.DurationDays,
  116. EffectPayload = data.EffectPayload,
  117. IsGiftable = data.IsGiftable
  118. };
  119. QueryString = Request.QueryString.ToString();
  120. }
  121. public async Task<IActionResult> OnPostAsync(CancellationToken ct)
  122. {
  123. if (!ModelState.IsValid)
  124. {
  125. TempData["ErrorMessages"] = ModelState.GetErrorMessages();
  126. return Redirect($"/Store/Product/Edit/{Input.ID}{Request.QueryString}");
  127. }
  128. // datetime-local input은 사용자 로컬 시각(Kind=Unspecified). DB는 UTC 저장 규약 → 명시 변환.
  129. var saleStartUtc = Input.SaleStartAt.HasValue
  130. ? DateTime.SpecifyKind(Input.SaleStartAt.Value, DateTimeKind.Local).ToUniversalTime()
  131. : (DateTime?)null;
  132. var saleEndUtc = Input.SaleEndAt.HasValue
  133. ? DateTime.SpecifyKind(Input.SaleEndAt.Value, DateTimeKind.Local).ToUniversalTime()
  134. : (DateTime?)null;
  135. var result = await mediator.Send(new UpdateProduct.Command(
  136. Input.ID,
  137. Input.GameID,
  138. Input.Name,
  139. Input.ThumbnailFile,
  140. Input.RemoveThumbnail,
  141. Input.Description,
  142. Input.Type,
  143. Input.Price,
  144. Input.DiscountType,
  145. Input.DiscountValue,
  146. Input.Stock,
  147. Input.MinPurchase,
  148. Input.MaxPurchase,
  149. Input.Order,
  150. Input.IsActive,
  151. saleStartUtc,
  152. saleEndUtc,
  153. Input.RequireDonationChannel,
  154. Input.ItemKind,
  155. Input.DurationDays,
  156. Input.EffectPayload,
  157. Input.IsGiftable
  158. ), ct);
  159. if (result.IsFailure)
  160. {
  161. TempData["ErrorMessages"] = result.Error.Description;
  162. }
  163. else
  164. {
  165. TempData["SuccessMessage"] = $"{Input.Name} 상품이 수정되었습니다.";
  166. }
  167. return Redirect($"/Store/Product/Edit/{Input.ID}{Request.QueryString}");
  168. }
  169. private async Task LoadGamesAsync(CancellationToken ct)
  170. {
  171. var result = await mediator.Send(new GetGames.Query(false), ct);
  172. Games = [.. result.List.Select(g => new SelectListItem
  173. {
  174. Value = g.ID.ToString(),
  175. Text = $"{g.KorName} ({g.Publisher})"
  176. })];
  177. }
  178. }