using Domain.Entities.Store.ValueObject; using SharedKernel.Extensions; using Application.Abstractions.Messaging; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Admin.Pages.Store.Product; /// /// 상품 판매 정책 설정 (fdmbox tb_box_limit_config 패턴). /// /Store/Product/LimitConfig/{id} — Product 와 1:1, row 없으면 기본값으로 폼 초기화. /// Custom 기간은 절대 datetime 범위 (PeriodStartAt ~ PeriodEndAt). /// public class LimitConfigModel(IMediator mediator) : PageModel { public int ProductID { get; private set; } public string? ProductName { get; private set; } public string? ProductThumbnail { get; private set; } public string? QueryString { get; set; } [BindProperty] public InputModel Input { get; set; } = new(); public sealed class InputModel { [DisplayName("PK")] [Required] public int ProductID { get; set; } [DisplayName("수량 제한")] public bool UseQuantityLimit { get; set; } [DisplayName("최대 구매 수량")] [Range(0, 9999, ErrorMessage = "{0}은(는) {1}~{2} 사이여야 합니다.")] public int MaxQuantity { get; set; } [DisplayName("기간 제한")] public bool UsePeriodLimit { get; set; } [DisplayName("기간 유형")] public LimitPeriodType PeriodType { get; set; } = LimitPeriodType.None; [DisplayName("시작 시각")] [DataType(DataType.DateTime)] public DateTime? PeriodStartAt { get; set; } [DisplayName("종료 시각")] [DataType(DataType.DateTime)] public DateTime? PeriodEndAt { get; set; } } public async Task OnGetAsync(int id, CancellationToken ct) { var result = await mediator.Send(new GetProductLimitConfig.Query(id), ct); if (result.IsFailure) { TempData["ErrorMessages"] = result.Error.Description; return RedirectToPage("/Store/Product/Index"); } var data = result.Value; ProductID = data.ProductID; ProductName = data.ProductName; ProductThumbnail = data.ProductThumbnail; QueryString = Request.QueryString.ToString(); // DB는 UTC 저장 규약. datetime-local input은 사용자 로컬 시각을 기대하므로 표시 시점에 변환. Input = new InputModel { ProductID = data.ProductID, UseQuantityLimit = data.UseQuantityLimit, MaxQuantity = data.MaxQuantity, UsePeriodLimit = data.UsePeriodLimit, PeriodType = data.PeriodType, PeriodStartAt = data.PeriodStartAt.HasValue ? DateTime.SpecifyKind(data.PeriodStartAt.Value, DateTimeKind.Utc).ToLocalTime() : null, PeriodEndAt = data.PeriodEndAt.HasValue ? DateTime.SpecifyKind(data.PeriodEndAt.Value, DateTimeKind.Utc).ToLocalTime() : null }; return Page(); } public async Task OnPostAsync(CancellationToken ct) { if (!ModelState.IsValid) { TempData["ErrorMessages"] = ModelState.GetErrorMessages(); return Redirect($"/Store/Product/LimitConfig/{Input.ProductID}"); } // UI 안전망: PeriodType 이 None 이외이면 UsePeriodLimit 강제 ON // (사용자가 라디오만 바꾸고 체크박스 토글을 잊은 경우 처리). // 반대로 UsePeriodLimit=false 면 PeriodType 도 None 으로 정규화. if (Input.PeriodType != LimitPeriodType.None) { Input.UsePeriodLimit = true; } else if (!Input.UsePeriodLimit) { Input.PeriodType = LimitPeriodType.None; } // datetime-local input은 사용자 로컬 시각(Kind=Unspecified). DB는 UTC 저장 규약 → 명시 변환. var periodStartUtc = Input.PeriodStartAt.HasValue ? DateTime.SpecifyKind(Input.PeriodStartAt.Value, DateTimeKind.Local).ToUniversalTime() : (DateTime?)null; var periodEndUtc = Input.PeriodEndAt.HasValue ? DateTime.SpecifyKind(Input.PeriodEndAt.Value, DateTimeKind.Local).ToUniversalTime() : (DateTime?)null; var result = await mediator.Send(new UpdateProductLimitConfig.Command( Input.ProductID, Input.UseQuantityLimit, Input.MaxQuantity, Input.UsePeriodLimit, Input.PeriodType, periodStartUtc, periodEndUtc ), ct); if (result.IsFailure) { TempData["ErrorMessages"] = result.Error.Description; } else { TempData["SuccessMessage"] = "판매 정책이 저장되었습니다."; } return Redirect($"/Store/Product/LimitConfig/{Input.ProductID}"); } }