using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Store.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; using SharedKernel.Storage; using ProductEntity = Domain.Entities.Store.Product; namespace Application.Features.Admin.Store.Product.Create; public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : ICommandHandler> { private static readonly string[] AllowedImageExt = [".jpg", ".jpeg", ".png", ".gif", ".webp"]; public async Task> Handle(Command request, CancellationToken ct) { // Game 존재 검증 (사용자 정책: GameID 필수) if (!await db.Game.AnyAsync(c => c.ID == request.GameID && c.IsActive, ct)) { return Result.Failure(Error.NotFound("Product.GameNotFound", "선택한 게임이 없거나 비활성 상태입니다.")); } if (request.Price < 0) { return Result.Failure(Error.Problem("Product.InvalidPrice", "가격은 0 이상이어야 합니다.")); } if (request.DiscountType == DiscountType.Percent && (request.DiscountValue < 0 || request.DiscountValue > 99)) { return Result.Failure(Error.Problem("Product.InvalidDiscountPercent", "할인율은 0 ~ 99% 범위로 입력하세요.")); } if (request.DiscountType == DiscountType.Amount && (request.DiscountValue < 0 || request.DiscountValue > request.Price)) { return Result.Failure(Error.Problem("Product.InvalidDiscountAmount", "할인 금액은 0 이상이며 정가 이하로 입력하세요.")); } if (request.Stock < -1) { return Result.Failure(Error.Problem("Product.InvalidStock", "재고는 -1(무제한) 또는 0 이상이어야 합니다.")); } if (request.MinPurchase < 1 || request.MaxPurchase < request.MinPurchase || request.MaxPurchase > 999) { return Result.Failure(Error.Problem("Product.InvalidPurchaseRange", "최소/최대 구매 수량 범위가 올바르지 않습니다 (1 ≤ 최소 ≤ 최대 ≤ 999).")); } if (request.SaleStartAt.HasValue && request.SaleEndAt.HasValue && request.SaleStartAt.Value >= request.SaleEndAt.Value) { return Result.Failure(Error.Problem("Product.InvalidSalePeriod", "판매 시작일은 종료일보다 이전이어야 합니다.")); } var product = ProductEntity.Create( gameID: request.GameID, name: request.Name, type: request.Type, price: request.Price, discountType: request.DiscountType, discountValue: request.DiscountValue, stock: request.Stock, description: request.Description, order: request.Order, minPurchase: request.MinPurchase, maxPurchase: request.MaxPurchase, saleStartAt: request.SaleStartAt, saleEndAt: request.SaleEndAt, requireDonationChannel: request.RequireDonationChannel ); if (!request.IsActive) { product.Deactivate(); } db.Product.Add(product); await db.SaveChangesAsync(ct); if (request.ThumbnailFile is not null) { var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.StoreProduct, product.ID); var saved = await fileStorage.SaveFileAsync(request.ThumbnailFile, uploadPath, AllowedImageExt, ct); if (saved is not null) { product.Update( gameID: product.GameID, name: product.Name, type: product.Type, price: product.Price, discountType: product.DiscountType, discountValue: product.DiscountValue, stock: product.Stock, description: product.Description, thumbnail: saved.Url, order: product.Order, minPurchase: product.MinPurchase, maxPurchase: product.MaxPurchase, saleStartAt: product.SaleStartAt, saleEndAt: product.SaleEndAt, isActive: product.IsActive, requireDonationChannel: product.RequireDonationChannel ); await db.SaveChangesAsync(ct); } } return Result.Success(product.ID); } }