using Domain.Entities.Store.ValueObject; using SharedKernel.Attributes; using SharedKernel.Extensions; using Application.Abstractions.Messaging; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Admin.Pages.Store.Product; public class WriteModel(IMediator mediator) : PageModel { public List Games { get; set; } = []; [BindProperty] public InputModel Input { get; set; } = new(); public sealed class InputModel { [DisplayName("게임")] public int? GameID { get; set; } [DisplayName("상품명")] [Required(ErrorMessage = "{0}은(는) 필수입니다.")] [StringLength(200, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")] public string Name { get; set; } = null!; [DisplayName("썸네일")] [AllowedExtensions("jpg,jpeg,png,gif,webp", ErrorMessage = "이미지 형식은 jpg, jpeg, png, gif, webp 만 허용합니다.")] public IFormFile? ThumbnailFile { get; set; } [DisplayName("설명")] public string? Description { get; set; } [DisplayName("상품 유형")] [Required(ErrorMessage = "{0}은(는) 필수입니다.")] public ProductType Type { get; set; } = ProductType.Physical; [DisplayName("가격(P)")] [Required(ErrorMessage = "{0}은(는) 필수입니다.")] [Range(0, int.MaxValue, ErrorMessage = "{0}은(는) 0 이상이어야 합니다.")] public int Price { get; set; } [DisplayName("할인 모드")] public DiscountType DiscountType { get; set; } = DiscountType.None; [DisplayName("할인 값")] [Range(0, int.MaxValue)] public int DiscountValue { get; set; } [DisplayName("재고 (-1=무제한)")] [Range(-1, int.MaxValue, ErrorMessage = "{0}은(는) -1(무제한) 또는 0 이상이어야 합니다.")] public int Stock { get; set; } = -1; [DisplayName("최소 구매 수량")] [Range(1, 999, ErrorMessage = "{0}은(는) {1} 이상 {2} 이하로 입력하세요.")] public int MinPurchase { get; set; } = 1; [DisplayName("최대 구매 수량")] [Range(1, 999, ErrorMessage = "{0}은(는) {1} 이상 {2} 이하로 입력하세요.")] public int MaxPurchase { get; set; } = 999; [DisplayName("순서")] [Range(0, 9999)] public int Order { get; set; } = 0; [DisplayName("사용 여부")] public bool IsActive { get; set; } = true; [DisplayName("판매 시작")] [DataType(DataType.DateTime)] public DateTime? SaleStartAt { get; set; } [DisplayName("판매 종료")] [DataType(DataType.DateTime)] public DateTime? SaleEndAt { get; set; } [DisplayName("후원 채널 필수")] public bool RequireDonationChannel { get; set; } [DisplayName("소모품 종류")] public ItemKind? ItemKind { get; set; } [DisplayName("소모품 효과 지속일")] [Range(0, 3650)] public int DurationDays { get; set; } } public async Task OnGetAsync(CancellationToken ct) { await LoadGamesAsync(ct); } public async Task OnPostAsync(CancellationToken ct) { if (!ModelState.IsValid) { TempData["ErrorMessages"] = ModelState.GetErrorMessages(); await LoadGamesAsync(ct); return Page(); } // datetime-local input은 사용자 로컬 시각(Kind=Unspecified). DB는 UTC 저장 규약 → 명시 변환. var saleStartUtc = Input.SaleStartAt.HasValue ? DateTime.SpecifyKind(Input.SaleStartAt.Value, DateTimeKind.Local).ToUniversalTime() : (DateTime?)null; var saleEndUtc = Input.SaleEndAt.HasValue ? DateTime.SpecifyKind(Input.SaleEndAt.Value, DateTimeKind.Local).ToUniversalTime() : (DateTime?)null; var result = await mediator.Send(new CreateProduct.Command( Input.GameID, Input.Name, Input.ThumbnailFile, Input.Description, Input.Type, Input.Price, Input.DiscountType, Input.DiscountValue, Input.Stock, Input.MinPurchase, Input.MaxPurchase, Input.Order, Input.IsActive, saleStartUtc, saleEndUtc, Input.RequireDonationChannel, Input.ItemKind, Input.DurationDays ), ct); if (result.IsFailure) { TempData["ErrorMessages"] = result.Error.Description; await LoadGamesAsync(ct); return Page(); } TempData["SuccessMessage"] = $"{Input.Name} 상품이 등록되었습니다."; return RedirectToPage("/Store/Product/Index"); } private async Task LoadGamesAsync(CancellationToken ct) { var result = await mediator.Send(new GetGames.Query(true), ct); Games = [.. result.List.Select(g => new SelectListItem { Value = g.ID.ToString(), Text = $"{g.KorName} ({g.Publisher})" })]; } }