using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Domain.Entities.Store.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Store.Games.Detail; internal sealed class Handler(IAppDbContext db) : IQueryHandler> { public async Task> Handle(Query request, CancellationToken ct) { var now = DateTime.UtcNow; var game = await db.Game.AsNoTracking() .Where(g => g.ID == request.ID && g.IsActive) .Select(g => new { g.ID, g.KorName, g.EngName, g.Publisher, g.Thumbnail, g.BigImage, g.Description, g.Link, g.TrailerUrl, g.MinSpec, g.RecommendedSpec, g.CreatedAt, g.ReleaseDate }) .FirstOrDefaultAsync(ct); if (game is null) { return Result.Failure(Error.NotFound("Game.NotFound", "게임을 찾을 수 없습니다.")); } var genres = await db.GameGenreMap.AsNoTracking() .Where(m => m.GameID == game.ID && m.GameGenre.IsActive) .OrderBy(m => m.GameGenre.Order) .Select(m => m.GameGenre.KorName) .ToListAsync(ct); var categories = await db.GameCategoryMap.AsNoTracking() .Where(m => m.GameID == game.ID && m.GameCategory.IsActive) .OrderBy(m => m.GameCategory.Order) .Select(m => m.GameCategory.KorName) .ToListAsync(ct); var images = await db.GameImage.AsNoTracking() .Where(i => i.GameID == game.ID && !i.IsDisabled) .OrderBy(i => i.Order).ThenBy(i => i.ID) .Select(i => i.Url) .ToListAsync(ct); var languages = await db.GameLanguageSupport.AsNoTracking() .Where(s => s.GameID == game.ID && s.GameLanguage.IsActive) .OrderBy(s => s.GameLanguage.Order).ThenBy(s => s.GameLanguage.KorName) .Select(s => new Response.LanguageRow(s.GameLanguage.KorName, s.GameLanguage.EngName, s.Interface, s.FullAudio, s.Subtitles)) .ToListAsync(ct); // 같은 게임 활성 상품 중 무작위 8개 (NEWID 셔플) var productsRaw = await db.Product.AsNoTracking() .Where(p => p.GameID == game.ID && p.IsActive && (p.SaleStartAt == null || p.SaleStartAt <= now) && (p.SaleEndAt == null || p.SaleEndAt > now)) .OrderBy(p => Guid.NewGuid()) .Take(8) .Select(p => new { p.ID, p.Name, p.Thumbnail, p.Price, p.DiscountType, p.DiscountValue }) .ToListAsync(ct); var products = productsRaw.Select(p => new Response.ProductRow( p.ID, p.Name, p.Thumbnail, p.Price, CalcSalePrice(p.Price, p.DiscountType, p.DiscountValue) )).ToList(); return Result.Success(new Response( game.ID, game.KorName, game.EngName, game.Publisher, game.Thumbnail, game.BigImage, game.Description, game.Link, game.TrailerUrl, game.MinSpec, game.RecommendedSpec, game.CreatedAt, game.ReleaseDate, genres, categories, images, languages, products )); } private static int CalcSalePrice(int price, DiscountType type, int value) { return type switch { DiscountType.Percent => Math.Max(0, price - (price * value / 100)), DiscountType.Amount => Math.Max(0, price - value), _ => price }; } }