using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; using SharedKernel.Storage; using Domain.Entities.Store; using GameEntity = Domain.Entities.Store.Game; namespace Application.Features.Admin.Store.Game.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) { // 커미션 검증 (Domain 검증 보강 — 사전 차단) if (request.CommissionRate < 0 || request.CommissionRate > 100) { return Result.Failure(Error.Problem("Game.InvalidCommissionRate", "게임사 수익률은 0~100% 사이여야 합니다.")); } var code = NormalizeOptional(request.Code); var engName = NormalizeOptional(request.EngName); var link = NormalizeOptional(request.Link); // 한글명 중복 차단 if (await db.Game.AnyAsync(c => c.KorName == request.KorName, ct)) { return Result.Failure(Error.Conflict("Game.DuplicateKorName", "이미 같은 한글명의 게임이 등록되어 있습니다.")); } // 게임 코드 중복 차단 (입력 시에만) if (code is not null && await db.Game.AnyAsync(c => c.Code == code, ct)) { return Result.Failure(Error.Conflict("Game.DuplicateCode", "이미 같은 코드의 게임이 등록되어 있습니다.")); } var game = GameEntity.Create( korName: request.KorName, publisher: request.Publisher, commissionRate: request.CommissionRate, code: code, engName: engName, description: request.Description, link: link, order: request.Order ); if (!request.IsActive) { game.Deactivate(); } game.SetDetail(request.TrailerUrl, request.MinSpec, request.RecommendedSpec, request.ReleaseDate); db.Game.Add(game); await db.SaveChangesAsync(ct); // 이미지 업로드 (ID 확보 후) — 작은 이미지 + 큰 이미지 string? thumbnailUrl = null; string? bigImageUrl = null; if (request.ThumbnailFile is not null) { var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.StoreGame, game.ID); var saved = await fileStorage.SaveFileAsync(request.ThumbnailFile, uploadPath, AllowedImageExt, ct); thumbnailUrl = saved?.Url; } if (request.BigImageFile is not null) { var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.StoreGame, game.ID); var saved = await fileStorage.SaveFileAsync(request.BigImageFile, uploadPath, AllowedImageExt, ct); bigImageUrl = saved?.Url; } if (thumbnailUrl is not null || bigImageUrl is not null) { game.Update( korName: game.KorName, publisher: game.Publisher, commissionRate: game.CommissionRate, code: game.Code, engName: game.EngName, thumbnail: thumbnailUrl ?? game.Thumbnail, bigImage: bigImageUrl ?? game.BigImage, description: game.Description, link: game.Link, order: game.Order, isActive: game.IsActive ); await db.SaveChangesAsync(ct); } await SyncDetailAsync(game.ID, request, ct); return Result.Success(game.ID); } private async Task SyncDetailAsync(int gameID, Command request, CancellationToken ct) { var changed = false; if (request.GenreIDs is { Count: > 0 } genreIds) { var validIds = await db.GameGenre.Where(g => genreIds.Contains(g.ID)).Select(g => g.ID).ToListAsync(ct); foreach (var id in validIds) { db.GameGenreMap.Add(GameGenreMap.Create(gameID, id)); changed = true; } } if (request.CategoryIDs is { Count: > 0 } categoryIds) { var validIds = await db.GameCategory.Where(c => categoryIds.Contains(c.ID)).Select(c => c.ID).ToListAsync(ct); foreach (var id in validIds) { db.GameCategoryMap.Add(GameCategoryMap.Create(gameID, id)); changed = true; } } if (request.DetailImages is { Count: > 0 }) { var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.StoreGameDetail, gameID); var order = 0; foreach (var image in request.DetailImages) { var saved = await fileStorage.SaveFileAsync(image, uploadPath, AllowedImageExt, ct); if (saved is null) { continue; } db.GameImage.Add(new GameImage { GameID = gameID, FileName = image.FileName, HashedName = saved.FileName, Path = uploadPath.ToRelativePath(), Url = saved.Url, Extension = saved.Extension, ContentType = image.ContentType, Size = saved.Size, Width = saved.Width, Height = saved.Height, Order = order++ }); changed = true; } } if (changed) { await db.SaveChangesAsync(ct); } } private static string? NormalizeOptional(string? value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } }