| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- using Application.Abstractions.Cache;
- using Application.Abstractions.Messaging;
- using Application.Abstractions.Data;
- using Application.Helpers;
- using Domain.Entities.Forum.ValueObject;
- using SharedKernel.Results;
- using Microsoft.EntityFrameworkCore;
- namespace Application.Features.Api.Forum.PostReaction.Toggle;
- public sealed class Handler(IAppDbContext db, ICacheService cache) : ICommandHandler<Command, Result>
- {
- public async Task<Result> Handle(Command request, CancellationToken ct)
- {
- var post = await db.Post.FirstOrDefaultAsync(x => x.ID == request.PostID, ct);
- if (post is null)
- {
- return Result.Failure(Error.NotFound("PostReaction.PostNotFound", "게시글을 찾을 수 없습니다."));
- }
- var existing = await db.PostReaction.FirstOrDefaultAsync(x => x.PostID == request.PostID && x.MemberID == request.MemberID, ct);
- // MemberStats 조회 (Like만 카운트) — 1회 쿼리로 giver/receiver 동시 로드
- var memberIDs = post.MemberID.HasValue && post.MemberID.Value != request.MemberID
- ? new[] { request.MemberID, post.MemberID.Value }
- : new[] { request.MemberID };
- var statsList = await db.MemberStats
- .Where(x => memberIDs.Contains(x.MemberID))
- .ToListAsync(ct);
- var giverStats = statsList.FirstOrDefault(x => x.MemberID == request.MemberID);
- var receiverStats = post.MemberID.HasValue && post.MemberID.Value != request.MemberID
- ? statsList.FirstOrDefault(x => x.MemberID == post.MemberID.Value)
- : null;
- // 신규 추천(Like) 성립 여부 — 보상 지급 판단용 (취소/재추천은 refID 멱등으로 이중 지급 차단).
- var likedNow = false;
- if (existing is not null)
- {
- // 같은 반응이면 취소
- if (existing.Reaction == request.Reaction)
- {
- if (existing.Reaction == Reaction.Like)
- {
- post.Likes--;
- if (giverStats is not null) { giverStats.LikeGivenCount--; }
- if (receiverStats is not null) { receiverStats.LikeReceivedCount--; }
- }
- else
- {
- post.Dislikes--;
- }
- db.PostReaction.Remove(existing);
- }
- else
- {
- // 다른 반응이면 전환
- if (existing.Reaction == Reaction.Like)
- {
- post.Likes--;
- post.Dislikes++;
- // Like → Dislike: Like 카운트 감소
- if (giverStats is not null) { giverStats.LikeGivenCount--; }
- if (receiverStats is not null) { receiverStats.LikeReceivedCount--; }
- }
- else
- {
- post.Dislikes--;
- post.Likes++;
- // Dislike → Like: Like 카운트 증가
- if (giverStats is not null) { giverStats.LikeGivenCount++; }
- if (receiverStats is not null) { receiverStats.LikeReceivedCount++; }
- likedNow = true;
- }
- existing.Reaction = request.Reaction;
- existing.UpdatedAt = DateTime.UtcNow;
- }
- }
- else
- {
- // 새 반응
- var reaction = new Domain.Entities.Forum.Posts.PostReaction
- {
- BoardID = post.BoardID,
- PostID = request.PostID,
- MemberID = request.MemberID,
- Reaction = request.Reaction
- };
- await db.PostReaction.AddAsync(reaction, ct);
- if (request.Reaction == Reaction.Like)
- {
- post.Likes++;
- if (giverStats is not null) { giverStats.LikeGivenCount++; }
- if (receiverStats is not null) { receiverStats.LikeReceivedCount++; }
- likedNow = true;
- }
- else
- {
- post.Dislikes++;
- }
- }
- // 추천 보상 (누른 쪽 XP + 받은 작성자 토큰, 캡·자동 승급 — d3 M2).
- // refID 가 (post, giver) 단위라 취소 후 재추천해도 이중 지급되지 않는다. 지급 실패해도 추천은 유지.
- if (likedNow)
- {
- await RewardService.GrantForLikeAsync(db, cache, request.MemberID, post.MemberID, request.PostID, ct);
- }
- await db.SaveChangesAsync(ct);
- return Result.Success();
- }
- }
|