LimitConfig.cshtml.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. using Domain.Entities.Store.ValueObject;
  2. using SharedKernel.Extensions;
  3. using MediatR;
  4. using Microsoft.AspNetCore.Mvc;
  5. using Microsoft.AspNetCore.Mvc.RazorPages;
  6. using System.ComponentModel;
  7. using System.ComponentModel.DataAnnotations;
  8. namespace Admin.Pages.Store.Product;
  9. /// <summary>
  10. /// 상품 판매 정책 설정 (fdmbox tb_box_limit_config 패턴).
  11. /// /Store/Product/LimitConfig/{id} — Product 와 1:1, row 없으면 기본값으로 폼 초기화.
  12. /// Custom 기간은 절대 datetime 범위 (PeriodStartAt ~ PeriodEndAt).
  13. /// </summary>
  14. public class LimitConfigModel(IMediator mediator) : PageModel
  15. {
  16. public int ProductID { get; private set; }
  17. public string? ProductName { get; private set; }
  18. public string? ProductThumbnail { get; private set; }
  19. public string? QueryString { get; set; }
  20. [BindProperty]
  21. public InputModel Input { get; set; } = new();
  22. public sealed class InputModel
  23. {
  24. [DisplayName("PK")]
  25. [Required]
  26. public int ProductID { get; set; }
  27. [DisplayName("수량 제한")]
  28. public bool UseQuantityLimit { get; set; }
  29. [DisplayName("최대 구매 수량")]
  30. [Range(0, 9999, ErrorMessage = "{0}은(는) {1}~{2} 사이여야 합니다.")]
  31. public int MaxQuantity { get; set; }
  32. [DisplayName("기간 제한")]
  33. public bool UsePeriodLimit { get; set; }
  34. [DisplayName("기간 유형")]
  35. public LimitPeriodType PeriodType { get; set; } = LimitPeriodType.None;
  36. [DisplayName("시작 시각")]
  37. [DataType(DataType.DateTime)]
  38. public DateTime? PeriodStartAt { get; set; }
  39. [DisplayName("종료 시각")]
  40. [DataType(DataType.DateTime)]
  41. public DateTime? PeriodEndAt { get; set; }
  42. }
  43. public async Task<IActionResult> OnGetAsync(int id, CancellationToken ct)
  44. {
  45. var result = await mediator.Send(new GetProductLimitConfig.Query(id), ct);
  46. if (result.IsFailure)
  47. {
  48. TempData["ErrorMessages"] = result.Error.Description;
  49. return RedirectToPage("/Store/Product/Index");
  50. }
  51. var data = result.Value;
  52. ProductID = data.ProductID;
  53. ProductName = data.ProductName;
  54. ProductThumbnail = data.ProductThumbnail;
  55. QueryString = Request.QueryString.ToString();
  56. // DB는 UTC 저장 규약. datetime-local input은 사용자 로컬 시각을 기대하므로 표시 시점에 변환.
  57. Input = new InputModel
  58. {
  59. ProductID = data.ProductID,
  60. UseQuantityLimit = data.UseQuantityLimit,
  61. MaxQuantity = data.MaxQuantity,
  62. UsePeriodLimit = data.UsePeriodLimit,
  63. PeriodType = data.PeriodType,
  64. PeriodStartAt = data.PeriodStartAt.HasValue
  65. ? DateTime.SpecifyKind(data.PeriodStartAt.Value, DateTimeKind.Utc).ToLocalTime()
  66. : null,
  67. PeriodEndAt = data.PeriodEndAt.HasValue
  68. ? DateTime.SpecifyKind(data.PeriodEndAt.Value, DateTimeKind.Utc).ToLocalTime()
  69. : null
  70. };
  71. return Page();
  72. }
  73. public async Task<IActionResult> OnPostAsync(CancellationToken ct)
  74. {
  75. if (!ModelState.IsValid)
  76. {
  77. TempData["ErrorMessages"] = ModelState.GetErrorMessages();
  78. return Redirect($"/Store/Product/LimitConfig/{Input.ProductID}");
  79. }
  80. // UI 안전망: PeriodType 이 None 이외이면 UsePeriodLimit 강제 ON
  81. // (사용자가 라디오만 바꾸고 체크박스 토글을 잊은 경우 처리).
  82. // 반대로 UsePeriodLimit=false 면 PeriodType 도 None 으로 정규화.
  83. if (Input.PeriodType != LimitPeriodType.None)
  84. {
  85. Input.UsePeriodLimit = true;
  86. }
  87. else if (!Input.UsePeriodLimit)
  88. {
  89. Input.PeriodType = LimitPeriodType.None;
  90. }
  91. // datetime-local input은 사용자 로컬 시각(Kind=Unspecified). DB는 UTC 저장 규약 → 명시 변환.
  92. var periodStartUtc = Input.PeriodStartAt.HasValue
  93. ? DateTime.SpecifyKind(Input.PeriodStartAt.Value, DateTimeKind.Local).ToUniversalTime()
  94. : (DateTime?)null;
  95. var periodEndUtc = Input.PeriodEndAt.HasValue
  96. ? DateTime.SpecifyKind(Input.PeriodEndAt.Value, DateTimeKind.Local).ToUniversalTime()
  97. : (DateTime?)null;
  98. var result = await mediator.Send(new UpdateProductLimitConfig.Command(
  99. Input.ProductID,
  100. Input.UseQuantityLimit,
  101. Input.MaxQuantity,
  102. Input.UsePeriodLimit,
  103. Input.PeriodType,
  104. periodStartUtc,
  105. periodEndUtc
  106. ), ct);
  107. if (result.IsFailure)
  108. {
  109. TempData["ErrorMessages"] = result.Error.Description;
  110. }
  111. else
  112. {
  113. TempData["SuccessMessage"] = "판매 정책이 저장되었습니다.";
  114. }
  115. return Redirect($"/Store/Product/LimitConfig/{Input.ProductID}");
  116. }
  117. }