Write.cshtml.cs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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 WriteModel(IMediator mediator) : PageModel
  12. {
  13. public List<SelectListItem> Games { get; set; } = [];
  14. [BindProperty]
  15. public InputModel Input { get; set; } = new();
  16. public sealed class InputModel
  17. {
  18. [DisplayName("게임")]
  19. public int? GameID { get; set; }
  20. [DisplayName("상품명")]
  21. [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
  22. [StringLength(200, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
  23. public string Name { get; set; } = null!;
  24. [DisplayName("썸네일")]
  25. [AllowedExtensions("jpg,jpeg,png,gif,webp", ErrorMessage = "이미지 형식은 jpg, jpeg, png, gif, webp 만 허용합니다.")]
  26. public IFormFile? ThumbnailFile { get; set; }
  27. [DisplayName("설명")]
  28. public string? Description { get; set; }
  29. [DisplayName("상품 유형")]
  30. [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
  31. public ProductType Type { get; set; } = ProductType.Physical;
  32. [DisplayName("가격(P)")]
  33. [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
  34. [Range(0, int.MaxValue, ErrorMessage = "{0}은(는) 0 이상이어야 합니다.")]
  35. public int Price { get; set; }
  36. [DisplayName("할인 모드")]
  37. public DiscountType DiscountType { get; set; } = DiscountType.None;
  38. [DisplayName("할인 값")]
  39. [Range(0, int.MaxValue)]
  40. public int DiscountValue { get; set; }
  41. [DisplayName("재고 (-1=무제한)")]
  42. [Range(-1, int.MaxValue, ErrorMessage = "{0}은(는) -1(무제한) 또는 0 이상이어야 합니다.")]
  43. public int Stock { get; set; } = -1;
  44. [DisplayName("최소 구매 수량")]
  45. [Range(1, 999, ErrorMessage = "{0}은(는) {1} 이상 {2} 이하로 입력하세요.")]
  46. public int MinPurchase { get; set; } = 1;
  47. [DisplayName("최대 구매 수량")]
  48. [Range(1, 999, ErrorMessage = "{0}은(는) {1} 이상 {2} 이하로 입력하세요.")]
  49. public int MaxPurchase { get; set; } = 999;
  50. [DisplayName("순서")]
  51. [Range(0, 9999)]
  52. public int Order { get; set; } = 0;
  53. [DisplayName("사용 여부")]
  54. public bool IsActive { get; set; } = true;
  55. [DisplayName("판매 시작")]
  56. [DataType(DataType.DateTime)]
  57. public DateTime? SaleStartAt { get; set; }
  58. [DisplayName("판매 종료")]
  59. [DataType(DataType.DateTime)]
  60. public DateTime? SaleEndAt { get; set; }
  61. [DisplayName("후원 채널 필수")]
  62. public bool RequireDonationChannel { get; set; }
  63. [DisplayName("소모품 종류")]
  64. public ItemKind? ItemKind { get; set; }
  65. [DisplayName("소모품 효과 지속일")]
  66. [Range(0, 3650)]
  67. public int DurationDays { get; set; }
  68. }
  69. public async Task OnGetAsync(CancellationToken ct)
  70. {
  71. await LoadGamesAsync(ct);
  72. }
  73. public async Task<IActionResult> OnPostAsync(CancellationToken ct)
  74. {
  75. if (!ModelState.IsValid)
  76. {
  77. TempData["ErrorMessages"] = ModelState.GetErrorMessages();
  78. await LoadGamesAsync(ct);
  79. return Page();
  80. }
  81. // datetime-local input은 사용자 로컬 시각(Kind=Unspecified). DB는 UTC 저장 규약 → 명시 변환.
  82. var saleStartUtc = Input.SaleStartAt.HasValue
  83. ? DateTime.SpecifyKind(Input.SaleStartAt.Value, DateTimeKind.Local).ToUniversalTime()
  84. : (DateTime?)null;
  85. var saleEndUtc = Input.SaleEndAt.HasValue
  86. ? DateTime.SpecifyKind(Input.SaleEndAt.Value, DateTimeKind.Local).ToUniversalTime()
  87. : (DateTime?)null;
  88. var result = await mediator.Send(new CreateProduct.Command(
  89. Input.GameID,
  90. Input.Name,
  91. Input.ThumbnailFile,
  92. Input.Description,
  93. Input.Type,
  94. Input.Price,
  95. Input.DiscountType,
  96. Input.DiscountValue,
  97. Input.Stock,
  98. Input.MinPurchase,
  99. Input.MaxPurchase,
  100. Input.Order,
  101. Input.IsActive,
  102. saleStartUtc,
  103. saleEndUtc,
  104. Input.RequireDonationChannel,
  105. Input.ItemKind,
  106. Input.DurationDays
  107. ), ct);
  108. if (result.IsFailure)
  109. {
  110. TempData["ErrorMessages"] = result.Error.Description;
  111. await LoadGamesAsync(ct);
  112. return Page();
  113. }
  114. TempData["SuccessMessage"] = $"{Input.Name} 상품이 등록되었습니다.";
  115. return RedirectToPage("/Store/Product/Index");
  116. }
  117. private async Task LoadGamesAsync(CancellationToken ct)
  118. {
  119. var result = await mediator.Send(new GetGames.Query(true), ct);
  120. Games = [.. result.List.Select(g => new SelectListItem
  121. {
  122. Value = g.ID.ToString(),
  123. Text = $"{g.KorName} ({g.Publisher})"
  124. })];
  125. }
  126. }