Handler.cs 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Features.Api.Feed.Shared;
  4. using Domain.Entities.Feed;
  5. using Microsoft.EntityFrameworkCore;
  6. using SharedKernel.Results;
  7. namespace Application.Features.Api.Feed.Update;
  8. public sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Result>
  9. {
  10. private const int MaxContentLength = 5000;
  11. public async Task<Result> Handle(Command request, CancellationToken ct)
  12. {
  13. if (string.IsNullOrWhiteSpace(request.Content))
  14. {
  15. return Result.Failure(Error.Problem("Feed.ContentRequired", "내용을 입력해주세요."));
  16. }
  17. var content = request.Content.Trim();
  18. if (content.Length > MaxContentLength)
  19. {
  20. return Result.Failure(Error.Problem("Feed.ContentTooLong", $"내용은 최대 {MaxContentLength}자까지 입력 가능합니다."));
  21. }
  22. var post = await db.FeedPost.FirstOrDefaultAsync(p => p.ID == request.PostID && !p.IsDeleted, ct);
  23. if (post is null)
  24. {
  25. return Result.Failure(Error.NotFound("Feed.PostNotFound", "게시글을 찾을 수 없습니다."));
  26. }
  27. if (post.MemberID != request.MemberID)
  28. {
  29. return Result.Failure(Error.Forbidden("Feed.NotOwner", "수정 권한이 없습니다."));
  30. }
  31. post.Content = content;
  32. post.UpdatedAt = DateTime.UtcNow;
  33. // ── 태그 재동기화: 기존 FeedPostTag 전부 제거 후 다시 붙임 ──
  34. var existingTagLinks = await db.FeedPostTag.Where(pt => pt.FeedPostID == post.ID).ToListAsync(ct);
  35. var existingTagIDs = existingTagLinks.Select(l => l.FeedTagID).ToList();
  36. db.FeedPostTag.RemoveRange(existingTagLinks);
  37. // 기존 태그 UsageCount 차감
  38. if (existingTagIDs.Count > 0)
  39. {
  40. var oldTags = await db.FeedTag.Where(t => existingTagIDs.Contains(t.ID)).ToListAsync(ct);
  41. foreach (var tag in oldTags)
  42. {
  43. tag.UsageCount = tag.UsageCount > 0 ? tag.UsageCount - 1 : 0;
  44. tag.UpdatedAt = DateTime.UtcNow;
  45. }
  46. }
  47. var hashtagsFromContent = FeedContent.ExtractHashtags(content);
  48. var mergedTags = new List<string>(hashtagsFromContent);
  49. if (request.Tags is { Count: > 0 })
  50. {
  51. foreach (var t in request.Tags)
  52. {
  53. if (!string.IsNullOrWhiteSpace(t) && !mergedTags.Any(x => string.Equals(x, t, StringComparison.OrdinalIgnoreCase)))
  54. {
  55. mergedTags.Add(t.Trim());
  56. }
  57. }
  58. }
  59. byte tagCount = 0;
  60. foreach (var tagName in mergedTags)
  61. {
  62. if (string.IsNullOrWhiteSpace(tagName))
  63. {
  64. continue;
  65. }
  66. var name = tagName.Trim();
  67. var slug = name.ToLowerInvariant().Replace(' ', '-');
  68. var tag = await db.FeedTag.FirstOrDefaultAsync(t => t.Name == name, ct);
  69. if (tag is null)
  70. {
  71. tag = new FeedTag { Name = name, Slug = slug };
  72. await db.FeedTag.AddAsync(tag, ct);
  73. await db.SaveChangesAsync(ct);
  74. }
  75. tag.UsageCount++;
  76. tag.UpdatedAt = DateTime.UtcNow;
  77. await db.FeedPostTag.AddAsync(new FeedPostTag
  78. {
  79. FeedPostID = post.ID,
  80. FeedTagID = tag.ID
  81. }, ct);
  82. tagCount++;
  83. }
  84. post.Tags = tagCount;
  85. await db.SaveChangesAsync(ct);
  86. return Result.Success();
  87. }
  88. }