Handler.cs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. using Application.Abstractions.Cache;
  2. using Application.Abstractions.Messaging;
  3. using Application.Abstractions.Data;
  4. using Application.Helpers;
  5. using Domain.Entities.Forum.ValueObject;
  6. using SharedKernel.Results;
  7. using Microsoft.EntityFrameworkCore;
  8. namespace Application.Features.Api.Forum.PostReaction.Toggle;
  9. public sealed class Handler(IAppDbContext db, ICacheService cache) : ICommandHandler<Command, Result>
  10. {
  11. public async Task<Result> Handle(Command request, CancellationToken ct)
  12. {
  13. var post = await db.Post.FirstOrDefaultAsync(x => x.ID == request.PostID, ct);
  14. if (post is null)
  15. {
  16. return Result.Failure(Error.NotFound("PostReaction.PostNotFound", "게시글을 찾을 수 없습니다."));
  17. }
  18. var existing = await db.PostReaction.FirstOrDefaultAsync(x => x.PostID == request.PostID && x.MemberID == request.MemberID, ct);
  19. // MemberStats 조회 (Like만 카운트) — 1회 쿼리로 giver/receiver 동시 로드
  20. var memberIDs = post.MemberID.HasValue && post.MemberID.Value != request.MemberID
  21. ? new[] { request.MemberID, post.MemberID.Value }
  22. : new[] { request.MemberID };
  23. var statsList = await db.MemberStats
  24. .Where(x => memberIDs.Contains(x.MemberID))
  25. .ToListAsync(ct);
  26. var giverStats = statsList.FirstOrDefault(x => x.MemberID == request.MemberID);
  27. var receiverStats = post.MemberID.HasValue && post.MemberID.Value != request.MemberID
  28. ? statsList.FirstOrDefault(x => x.MemberID == post.MemberID.Value)
  29. : null;
  30. // 신규 추천(Like) 성립 여부 — 보상 지급 판단용 (취소/재추천은 refID 멱등으로 이중 지급 차단).
  31. var likedNow = false;
  32. if (existing is not null)
  33. {
  34. // 같은 반응이면 취소
  35. if (existing.Reaction == request.Reaction)
  36. {
  37. if (existing.Reaction == Reaction.Like)
  38. {
  39. post.Likes--;
  40. if (giverStats is not null) { giverStats.LikeGivenCount--; }
  41. if (receiverStats is not null) { receiverStats.LikeReceivedCount--; }
  42. }
  43. else
  44. {
  45. post.Dislikes--;
  46. }
  47. db.PostReaction.Remove(existing);
  48. }
  49. else
  50. {
  51. // 다른 반응이면 전환
  52. if (existing.Reaction == Reaction.Like)
  53. {
  54. post.Likes--;
  55. post.Dislikes++;
  56. // Like → Dislike: Like 카운트 감소
  57. if (giverStats is not null) { giverStats.LikeGivenCount--; }
  58. if (receiverStats is not null) { receiverStats.LikeReceivedCount--; }
  59. }
  60. else
  61. {
  62. post.Dislikes--;
  63. post.Likes++;
  64. // Dislike → Like: Like 카운트 증가
  65. if (giverStats is not null) { giverStats.LikeGivenCount++; }
  66. if (receiverStats is not null) { receiverStats.LikeReceivedCount++; }
  67. likedNow = true;
  68. }
  69. existing.Reaction = request.Reaction;
  70. existing.UpdatedAt = DateTime.UtcNow;
  71. }
  72. }
  73. else
  74. {
  75. // 새 반응
  76. var reaction = new Domain.Entities.Forum.Posts.PostReaction
  77. {
  78. BoardID = post.BoardID,
  79. PostID = request.PostID,
  80. MemberID = request.MemberID,
  81. Reaction = request.Reaction
  82. };
  83. await db.PostReaction.AddAsync(reaction, ct);
  84. if (request.Reaction == Reaction.Like)
  85. {
  86. post.Likes++;
  87. if (giverStats is not null) { giverStats.LikeGivenCount++; }
  88. if (receiverStats is not null) { receiverStats.LikeReceivedCount++; }
  89. likedNow = true;
  90. }
  91. else
  92. {
  93. post.Dislikes++;
  94. }
  95. }
  96. // 추천 보상 (누른 쪽 XP + 받은 작성자 토큰, 캡·자동 승급 — d3 M2).
  97. // refID 가 (post, giver) 단위라 취소 후 재추천해도 이중 지급되지 않는다. 지급 실패해도 추천은 유지.
  98. if (likedNow)
  99. {
  100. await RewardService.GrantForLikeAsync(db, cache, request.MemberID, post.MemberID, request.PostID, ct);
  101. }
  102. await db.SaveChangesAsync(ct);
  103. return Result.Success();
  104. }
  105. }