Edit.cshtml.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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. }
  70. public async Task OnGetAsync(int id, CancellationToken ct)
  71. {
  72. await LoadGamesAsync(ct);
  73. var result = await mediator.Send(new GetProduct.Query(id), ct);
  74. if (result.IsFailure)
  75. {
  76. TempData["ErrorMessages"] = result.Error.Description;
  77. QueryString = Request.QueryString.ToString();
  78. return;
  79. }
  80. var data = result.Value;
  81. CurrentThumbnail = data.Thumbnail;
  82. // DB는 UTC 저장 규약. datetime-local input은 사용자 로컬 시각을 기대하므로 표시 시점에 변환.
  83. Input = new InputModel
  84. {
  85. ID = data.ID,
  86. GameID = data.GameID > 0 ? data.GameID : (int?)null,
  87. Name = data.Name,
  88. Description = data.Description,
  89. Type = data.Type,
  90. Price = data.Price,
  91. DiscountType = data.DiscountType,
  92. DiscountValue = data.DiscountValue,
  93. Stock = data.Stock,
  94. MinPurchase = data.MinPurchase,
  95. MaxPurchase = data.MaxPurchase,
  96. Order = data.Order,
  97. IsActive = data.IsActive,
  98. SaleStartAt = data.SaleStartAt.HasValue
  99. ? DateTime.SpecifyKind(data.SaleStartAt.Value, DateTimeKind.Utc).ToLocalTime()
  100. : null,
  101. SaleEndAt = data.SaleEndAt.HasValue
  102. ? DateTime.SpecifyKind(data.SaleEndAt.Value, DateTimeKind.Utc).ToLocalTime()
  103. : null,
  104. RequireDonationChannel = data.RequireDonationChannel
  105. };
  106. QueryString = Request.QueryString.ToString();
  107. }
  108. public async Task<IActionResult> OnPostAsync(CancellationToken ct)
  109. {
  110. if (!ModelState.IsValid)
  111. {
  112. TempData["ErrorMessages"] = ModelState.GetErrorMessages();
  113. return Redirect($"/Store/Product/Edit/{Input.ID}{Request.QueryString}");
  114. }
  115. // datetime-local input은 사용자 로컬 시각(Kind=Unspecified). DB는 UTC 저장 규약 → 명시 변환.
  116. var saleStartUtc = Input.SaleStartAt.HasValue
  117. ? DateTime.SpecifyKind(Input.SaleStartAt.Value, DateTimeKind.Local).ToUniversalTime()
  118. : (DateTime?)null;
  119. var saleEndUtc = Input.SaleEndAt.HasValue
  120. ? DateTime.SpecifyKind(Input.SaleEndAt.Value, DateTimeKind.Local).ToUniversalTime()
  121. : (DateTime?)null;
  122. var result = await mediator.Send(new UpdateProduct.Command(
  123. Input.ID,
  124. Input.GameID,
  125. Input.Name,
  126. Input.ThumbnailFile,
  127. Input.RemoveThumbnail,
  128. Input.Description,
  129. Input.Type,
  130. Input.Price,
  131. Input.DiscountType,
  132. Input.DiscountValue,
  133. Input.Stock,
  134. Input.MinPurchase,
  135. Input.MaxPurchase,
  136. Input.Order,
  137. Input.IsActive,
  138. saleStartUtc,
  139. saleEndUtc,
  140. Input.RequireDonationChannel
  141. ), ct);
  142. if (result.IsFailure)
  143. {
  144. TempData["ErrorMessages"] = result.Error.Description;
  145. }
  146. else
  147. {
  148. TempData["SuccessMessage"] = $"{Input.Name} 상품이 수정되었습니다.";
  149. }
  150. return Redirect($"/Store/Product/Edit/{Input.ID}{Request.QueryString}");
  151. }
  152. private async Task LoadGamesAsync(CancellationToken ct)
  153. {
  154. var result = await mediator.Send(new GetGames.Query(false), ct);
  155. Games = [.. result.List.Select(g => new SelectListItem
  156. {
  157. Value = g.ID.ToString(),
  158. Text = $"{g.KorName} ({g.Publisher})"
  159. })];
  160. }
  161. }