Handler.cs 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Microsoft.EntityFrameworkCore;
  4. using SharedKernel.Results;
  5. using GameLanguageEntity = Domain.Entities.Store.GameLanguage;
  6. namespace Application.Features.Admin.Store.GameLanguage.Save;
  7. public sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result<Response>>
  8. {
  9. public async Task<Result<Response>> Handle(Command request, CancellationToken ct)
  10. {
  11. var items = request.Items;
  12. var dbRows = await db.GameLanguage.ToListAsync(ct);
  13. var dbByID = dbRows.ToDictionary(c => c.ID);
  14. var requestIDs = items.Where(c => c.ID.HasValue && c.ID.Value > 0).Select(c => c.ID!.Value).ToHashSet();
  15. var deleteTargets = dbRows.Where(x => !requestIDs.Contains(x.ID)).ToList();
  16. if (deleteTargets.Count > 0)
  17. {
  18. var deleteIDs = deleteTargets.Select(c => c.ID).ToList();
  19. var inUse = await db.GameLanguageSupport.AsNoTracking().AnyAsync(c => deleteIDs.Contains(c.GameLanguageID), ct);
  20. if (inUse)
  21. {
  22. return Result.Failure<Response>(Error.Conflict("GameLanguage.InUse", "게임에 연결된 언어는 삭제할 수 없습니다. 먼저 해당 게임에서 언어 선택을 해제하세요."));
  23. }
  24. db.GameLanguage.RemoveRange(deleteTargets);
  25. }
  26. ushort inserted = 0;
  27. ushort updated = 0;
  28. // 이미 사용 중인 한글명(트림 기준) → 소유 ID. 삭제 대상 제외. 배치 내 중복까지 인메모리로 차단 (per-row AnyAsync 제거)
  29. var deleteSet = deleteTargets.ToHashSet();
  30. var takenNames = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
  31. foreach (var x in dbRows)
  32. {
  33. if (!deleteSet.Contains(x))
  34. {
  35. takenNames[x.KorName.Trim()] = x.ID;
  36. }
  37. }
  38. foreach (var row in items)
  39. {
  40. if (string.IsNullOrWhiteSpace(row.KorName))
  41. {
  42. return Result.Failure<Response>(Error.Problem("GameLanguage.KorNameRequired", "언어 한글명은 필수입니다."));
  43. }
  44. var name = row.KorName.Trim();
  45. // 신규 추가
  46. if (!row.ID.HasValue || row.ID.Value <= 0)
  47. {
  48. if (takenNames.ContainsKey(name))
  49. {
  50. return Result.Failure<Response>(Error.Conflict("GameLanguage.DuplicateKorName", $"`{name}`는 이미 등록된 언어입니다."));
  51. }
  52. db.GameLanguage.Add(GameLanguageEntity.Create(row.KorName, row.EngName, row.Order, row.IsActive));
  53. takenNames[name] = 0; // 배치 내 신규(ID 미정) — 후속 행의 중복 차단용
  54. inserted++;
  55. continue;
  56. }
  57. // 수정
  58. if (!dbByID.TryGetValue(row.ID.Value, out var existing))
  59. {
  60. return Result.Failure<Response>(Error.NotFound("GameLanguage.NotFound", $"존재하지 않는 ID: {row.ID.Value}"));
  61. }
  62. if (takenNames.TryGetValue(name, out var owner) && owner != row.ID.Value)
  63. {
  64. return Result.Failure<Response>(Error.Conflict("GameLanguage.DuplicateKorName", $"`{name}`는 이미 등록된 언어입니다."));
  65. }
  66. var oldName = existing.KorName.Trim();
  67. if (!string.Equals(oldName, name, StringComparison.OrdinalIgnoreCase))
  68. {
  69. takenNames.Remove(oldName);
  70. takenNames[name] = row.ID.Value;
  71. }
  72. existing.Update(row.KorName, row.EngName, row.Order, row.IsActive);
  73. updated++;
  74. }
  75. var deleted = (ushort)deleteTargets.Count;
  76. await db.SaveChangesAsync(ct);
  77. return Result.Success(new Response(inserted, updated, deleted));
  78. }
  79. }