Handler.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Microsoft.EntityFrameworkCore;
  4. using SharedKernel.Results;
  5. using SharedKernel.Storage;
  6. using Domain.Entities.Store;
  7. using GameEntity = Domain.Entities.Store.Game;
  8. namespace Application.Features.Admin.Store.Game.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. // 커미션 검증 (Domain 검증 보강 — 사전 차단)
  15. if (request.CommissionRate < 0 || request.CommissionRate > 100)
  16. {
  17. return Result.Failure<int>(Error.Problem("Game.InvalidCommissionRate", "게임사 수익률은 0~100% 사이여야 합니다."));
  18. }
  19. var code = NormalizeOptional(request.Code);
  20. var engName = NormalizeOptional(request.EngName);
  21. var link = NormalizeOptional(request.Link);
  22. // 한글명 중복 차단
  23. if (await db.Game.AnyAsync(c => c.KorName == request.KorName, ct))
  24. {
  25. return Result.Failure<int>(Error.Conflict("Game.DuplicateKorName", "이미 같은 한글명의 게임이 등록되어 있습니다."));
  26. }
  27. // 게임 코드 중복 차단 (입력 시에만)
  28. if (code is not null && await db.Game.AnyAsync(c => c.Code == code, ct))
  29. {
  30. return Result.Failure<int>(Error.Conflict("Game.DuplicateCode", "이미 같은 코드의 게임이 등록되어 있습니다."));
  31. }
  32. var game = GameEntity.Create(
  33. korName: request.KorName,
  34. publisher: request.Publisher,
  35. commissionRate: request.CommissionRate,
  36. code: code,
  37. engName: engName,
  38. description: request.Description,
  39. link: link,
  40. order: request.Order
  41. );
  42. if (!request.IsActive)
  43. {
  44. game.Deactivate();
  45. }
  46. if (request.ApiCommissionRate < 0 || request.ApiCommissionRate > 100)
  47. {
  48. return Result.Failure<int>(Error.Problem("Game.InvalidApiCommissionRate", "API 수수료율은 0~100% 사이여야 합니다."));
  49. }
  50. game.SetApiCommissionRate(request.ApiCommissionRate);
  51. game.SetDetail(request.TrailerUrl, request.MinSpec, request.RecommendedSpec, request.ReleaseDate);
  52. db.Game.Add(game);
  53. await db.SaveChangesAsync(ct);
  54. // 이미지 업로드 (ID 확보 후) — 작은 이미지 + 큰 이미지
  55. string? thumbnailUrl = null;
  56. string? bigImageUrl = null;
  57. if (request.ThumbnailFile is not null)
  58. {
  59. var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.StoreGame, game.ID);
  60. var saved = await fileStorage.SaveFileAsync(request.ThumbnailFile, uploadPath, AllowedImageExt, ct);
  61. thumbnailUrl = saved?.Url;
  62. }
  63. if (request.BigImageFile is not null)
  64. {
  65. var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.StoreGame, game.ID);
  66. var saved = await fileStorage.SaveFileAsync(request.BigImageFile, uploadPath, AllowedImageExt, ct);
  67. bigImageUrl = saved?.Url;
  68. }
  69. if (thumbnailUrl is not null || bigImageUrl is not null)
  70. {
  71. game.Update(
  72. korName: game.KorName,
  73. publisher: game.Publisher,
  74. commissionRate: game.CommissionRate,
  75. code: game.Code,
  76. engName: game.EngName,
  77. thumbnail: thumbnailUrl ?? game.Thumbnail,
  78. bigImage: bigImageUrl ?? game.BigImage,
  79. description: game.Description,
  80. link: game.Link,
  81. order: game.Order,
  82. isActive: game.IsActive
  83. );
  84. await db.SaveChangesAsync(ct);
  85. }
  86. await SyncDetailAsync(game.ID, request, ct);
  87. return Result.Success(game.ID);
  88. }
  89. private async Task SyncDetailAsync(int gameID, Command request, CancellationToken ct)
  90. {
  91. var changed = false;
  92. if (request.GenreIDs is { Count: > 0 } genreIds)
  93. {
  94. var validIds = await db.GameGenre.Where(g => genreIds.Contains(g.ID)).Select(g => g.ID).ToListAsync(ct);
  95. foreach (var id in validIds)
  96. {
  97. db.GameGenreMap.Add(GameGenreMap.Create(gameID, id));
  98. changed = true;
  99. }
  100. }
  101. if (request.CategoryIDs is { Count: > 0 } categoryIds)
  102. {
  103. var validIds = await db.GameCategory.Where(c => categoryIds.Contains(c.ID)).Select(c => c.ID).ToListAsync(ct);
  104. foreach (var id in validIds)
  105. {
  106. db.GameCategoryMap.Add(GameCategoryMap.Create(gameID, id));
  107. changed = true;
  108. }
  109. }
  110. if (request.DetailImages is { Count: > 0 })
  111. {
  112. var uploadPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.StoreGameDetail, gameID);
  113. var order = 0;
  114. foreach (var image in request.DetailImages)
  115. {
  116. var saved = await fileStorage.SaveFileAsync(image, uploadPath, AllowedImageExt, ct);
  117. if (saved is null)
  118. {
  119. continue;
  120. }
  121. db.GameImage.Add(new GameImage
  122. {
  123. GameID = gameID,
  124. FileName = image.FileName,
  125. HashedName = saved.FileName,
  126. Path = uploadPath.ToRelativePath(),
  127. Url = saved.Url,
  128. Extension = saved.Extension,
  129. ContentType = image.ContentType,
  130. Size = saved.Size,
  131. Width = saved.Width,
  132. Height = saved.Height,
  133. Order = order++
  134. });
  135. changed = true;
  136. }
  137. }
  138. if (changed)
  139. {
  140. await db.SaveChangesAsync(ct);
  141. }
  142. }
  143. private static string? NormalizeOptional(string? value)
  144. {
  145. return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
  146. }
  147. }