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 존재 검증 — 소모품(ItemKind)은 게임 없이 등록 가능 var gameID = request.GameID is > 0 ? request.GameID : null; if (gameID.HasValue && !await db.Game.AnyAsync(c => c.ID == gameID.Value && 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: 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, itemKind: request.ItemKind, durationDays: request.DurationDays, effectPayload: request.EffectPayload, isGiftable: request.IsGiftable ); 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); } }