Handler.cs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Domain.Entities.Store.ValueObject;
  4. using Microsoft.EntityFrameworkCore;
  5. using SharedKernel.Results;
  6. using SharedKernel.Storage;
  7. using ProductEntity = Domain.Entities.Store.Product;
  8. namespace Application.Features.Admin.Store.Product.Create;
  9. public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : ICommandHandler<Command, Result<int>>
  10. {
  11. private static readonly string[] AllowedImageExt = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
  12. public async Task<Result<int>> Handle(Command request, CancellationToken ct)
  13. {
  14. // Game 존재 검증 (사용자 정책: GameID 필수)
  15. if (!await db.Game.AnyAsync(c => c.ID == request.GameID && c.IsActive, ct))
  16. {
  17. return Result.Failure<int>(Error.NotFound("Product.GameNotFound", "선택한 게임이 없거나 비활성 상태입니다."));
  18. }
  19. if (request.Price < 0)
  20. {
  21. return Result.Failure<int>(Error.Problem("Product.InvalidPrice", "가격은 0 이상이어야 합니다."));
  22. }
  23. if (request.DiscountType == DiscountType.Percent && (request.DiscountValue < 0 || request.DiscountValue > 99))
  24. {
  25. return Result.Failure<int>(Error.Problem("Product.InvalidDiscountPercent", "할인율은 0 ~ 99% 범위로 입력하세요."));
  26. }
  27. if (request.DiscountType == DiscountType.Amount && (request.DiscountValue < 0 || request.DiscountValue > request.Price))
  28. {
  29. return Result.Failure<int>(Error.Problem("Product.InvalidDiscountAmount", "할인 금액은 0 이상이며 정가 이하로 입력하세요."));
  30. }
  31. if (request.Stock < -1)
  32. {
  33. return Result.Failure<int>(Error.Problem("Product.InvalidStock", "재고는 -1(무제한) 또는 0 이상이어야 합니다."));
  34. }
  35. if (request.MinPurchase < 1 || request.MaxPurchase < request.MinPurchase || request.MaxPurchase > 999)
  36. {
  37. return Result.Failure<int>(Error.Problem("Product.InvalidPurchaseRange", "최소/최대 구매 수량 범위가 올바르지 않습니다 (1 ≤ 최소 ≤ 최대 ≤ 999)."));
  38. }
  39. if (request.SaleStartAt.HasValue && request.SaleEndAt.HasValue && request.SaleStartAt.Value >= request.SaleEndAt.Value)
  40. {
  41. return Result.Failure<int>(Error.Problem("Product.InvalidSalePeriod", "판매 시작일은 종료일보다 이전이어야 합니다."));
  42. }
  43. var product = ProductEntity.Create(
  44. gameID: request.GameID,
  45. name: request.Name,
  46. type: request.Type,
  47. price: request.Price,
  48. discountType: request.DiscountType,
  49. discountValue: request.DiscountValue,
  50. stock: request.Stock,
  51. description: request.Description,
  52. order: request.Order,
  53. minPurchase: request.MinPurchase,
  54. maxPurchase: request.MaxPurchase,
  55. saleStartAt: request.SaleStartAt,
  56. saleEndAt: request.SaleEndAt,
  57. requireDonationChannel: request.RequireDonationChannel
  58. );
  59. if (!request.IsActive)
  60. {
  61. product.Deactivate();
  62. }
  63. db.Product.Add(product);
  64. await db.SaveChangesAsync(ct);
  65. if (request.ThumbnailFile is not null)
  66. {
  67. var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.StoreProduct, product.ID);
  68. var saved = await fileStorage.SaveFileAsync(request.ThumbnailFile, uploadPath, AllowedImageExt, ct);
  69. if (saved is not null)
  70. {
  71. product.Update(
  72. gameID: product.GameID,
  73. name: product.Name,
  74. type: product.Type,
  75. price: product.Price,
  76. discountType: product.DiscountType,
  77. discountValue: product.DiscountValue,
  78. stock: product.Stock,
  79. description: product.Description,
  80. thumbnail: saved.Url,
  81. order: product.Order,
  82. minPurchase: product.MinPurchase,
  83. maxPurchase: product.MaxPurchase,
  84. saleStartAt: product.SaleStartAt,
  85. saleEndAt: product.SaleEndAt,
  86. isActive: product.IsActive,
  87. requireDonationChannel: product.RequireDonationChannel
  88. );
  89. await db.SaveChangesAsync(ct);
  90. }
  91. }
  92. return Result.Success(product.ID);
  93. }
  94. }