using Domain.Entities.Store.ValueObject; using SharedKernel.Attributes; using SharedKernel.Extensions; using SharedKernel.Helpers; using Application.Abstractions.Messaging; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Admin.Pages.Store.Coupon; public class CodesModel(IMediator mediator) : PageModel { [BindProperty(SupportsGet = true)] public int ID { get; set; } [BindProperty(SupportsGet = true)] public QueryParams Query { get; set; } = new(); public string CouponName { get; private set; } = ""; public string GameName { get; private set; } = ""; public string ProductName { get; private set; } = ""; public int Total { get; set; } public int AvailableCount { get; set; } public int ReservedCount { get; set; } public int IssuedCount { get; set; } public int UsedCount { get; set; } public int ExpiredCount { get; set; } public sealed class QueryParams { [Range(1, int.MaxValue)] public int PageNum { get; set; } = 1; [Range(1, 200)] public ushort PerPage { get; set; } = 50; [DisplayName("코드 상태")] public CouponCodeStatus? Status { get; set; } } public List<( int Num, int ID, string Code, string StatusText, int? IssuedToMemberID, string? IssuedToMemberName, string? IssuedToMemberEmail, string? IssuedAt, string? UsedAt, string CreatedAt )> List { get; set; } = []; public Pagination? Pagination { get; set; } [BindProperty] public UploadInput Upload { get; set; } = new(); public sealed class UploadInput { [DisplayName("CSV 파일")] [Required(ErrorMessage = "{0}을(를) 선택하세요.")] [AllowedExtensions("csv,txt", ErrorMessage = "CSV 또는 TXT 파일만 허용됩니다.")] public IFormFile? CsvFile { get; set; } } public async Task OnGetAsync(CancellationToken ct) { if (!ModelState.IsValid) { return; } await LoadHeaderAsync(ct); await LoadListAsync(ct); } public async Task OnPostUploadAsync(CancellationToken ct) { if (!ModelState.IsValid) { TempData["ErrorMessages"] = ModelState.GetErrorMessages(); return RedirectToPage("/Store/Coupon/Codes", new { id = ID }); } var result = await mediator.Send(new ImportCouponCodes.Command(ID, Upload.CsvFile!), ct); if (result.IsFailure) { TempData["ErrorMessages"] = result.Error.Description; } else { var r = result.Value; TempData["SuccessMessage"] = $"CSV 업로드 완료. 총 {r.TotalLines}줄 / 추가 {r.Added}개 (미사용 {r.AddedAvailable} + 사용됨 {r.AddedUsed}) / 중복 skip {r.SkippedDuplicate}개 / 회원미존재 skip {r.SkippedMissingMember}개 / 무효 skip {r.SkippedInvalid}개."; } return RedirectToPage("/Store/Coupon/Codes", new { id = ID }); } public async Task OnGetExportAsync(int id, CancellationToken ct) { var result = await mediator.Send(new ExportCouponCodes.Query(id), ct); if (result.IsFailure) { TempData["ErrorMessages"] = result.Error.Description; return RedirectToPage("/Store/Coupon/Codes", new { id }); } var data = result.Value; return File(data.CsvBytes, "text/csv; charset=utf-8", data.FileName); } public async Task OnPostDeleteCodesAsync(int id, int[] codeIDs, CancellationToken ct) { if (codeIDs is null || codeIDs.Length == 0) { TempData["ErrorMessages"] = "삭제할 코드를 선택해 주세요."; return RedirectToPage("/Store/Coupon/Codes", new { id }); } var result = await mediator.Send(new DeleteCouponCodes.Command(id, codeIDs), ct); if (result.IsFailure) { TempData["ErrorMessages"] = result.Error.Description; } else { TempData["SuccessMessage"] = $"{result.Value.Deleted}개 코드가 삭제되었습니다."; } return RedirectToPage("/Store/Coupon/Codes", new { id }); } public async Task OnPostExpireCodesAsync(int id, int[] codeIDs, CancellationToken ct) { if (codeIDs is null || codeIDs.Length == 0) { TempData["ErrorMessages"] = "만료 처리할 코드를 선택해 주세요."; return RedirectToPage("/Store/Coupon/Codes", new { id }); } var result = await mediator.Send(new ExpireCouponCodes.Command(id, codeIDs), ct); if (result.IsFailure) { TempData["ErrorMessages"] = result.Error.Description; } else { TempData["SuccessMessage"] = $"{result.Value.Expired}개 코드가 만료 처리되었습니다."; } return RedirectToPage("/Store/Coupon/Codes", new { id }); } public async Task OnPostRestoreCodesAsync(int id, int[] codeIDs, CancellationToken ct) { if (codeIDs is null || codeIDs.Length == 0) { TempData["ErrorMessages"] = "복귀 처리할 코드를 선택해 주세요."; return RedirectToPage("/Store/Coupon/Codes", new { id }); } var result = await mediator.Send(new RestoreCouponCodes.Command(id, codeIDs), ct); if (result.IsFailure) { TempData["ErrorMessages"] = result.Error.Description; } else { TempData["SuccessMessage"] = $"{result.Value.Restored}개 코드가 복귀 처리되었습니다."; } return RedirectToPage("/Store/Coupon/Codes", new { id }); } public IActionResult OnGetTemplate() { // 헤더만 담긴 빈 양식 var header = new[] { "ID", "Code", "Status", "IssuedToMemberID", "IssuedToMemberName", "IssuedToMemberEmail", "IssuedAt", "UsedAt", "CreatedAt" }; var bytes = SharedKernel.Helpers.CsvHelper.WriteUtf8Bom([header]); return File(bytes, "text/csv; charset=utf-8", "coupon-codes-template.csv"); } private async Task LoadHeaderAsync(CancellationToken ct) { var head = await mediator.Send(new GetCoupon.Query(ID), ct); if (head.IsFailure) { TempData["ErrorMessages"] = head.Error.Description; return; } CouponName = head.Value.Name; GameName = head.Value.GameName; ProductName = head.Value.ProductName; } private async Task LoadListAsync(CancellationToken ct) { var result = await mediator.Send(new ListCouponCodes.Query(ID, Query.Status, Query.PageNum, Query.PerPage), ct); Total = result.Total; AvailableCount = result.AvailableCount; ReservedCount = result.ReservedCount; IssuedCount = result.IssuedCount; UsedCount = result.UsedCount; ExpiredCount = result.ExpiredCount; List = [.. result.List.Select(c => ( c.Num, c.ID, c.Code, StatusText: c.Status switch { CouponCodeStatus.Available => "사용 가능", CouponCodeStatus.Reserved => "결제 진행 중", CouponCodeStatus.Issued => "발급됨", CouponCodeStatus.Used => "사용됨", CouponCodeStatus.Expired => "만료", _ => "?" }, c.IssuedToMemberID, c.IssuedToMemberName, c.IssuedToMemberEmail, c.IssuedAt?.ToString("yyyy-MM-dd HH:mm"), c.UsedAt?.ToString("yyyy-MM-dd HH:mm"), c.CreatedAt.GetDateAt() ))]; Pagination = new Pagination(result.Total, Query.PageNum, Query.PerPage); } }