Handler.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. using Application.Abstractions.Messaging;
  2. using Application.Abstractions.Data;
  3. using Domain.Entities.Forum.Logs;
  4. using Domain.Entities.Forum.ValueObject;
  5. using SharedKernel.Results;
  6. using Microsoft.EntityFrameworkCore;
  7. namespace Application.Features.Api.Forum.Post.Get;
  8. public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Result<Response>>
  9. {
  10. public async Task<Result<Response>> Handle(Query request, CancellationToken ct)
  11. {
  12. // 게시글 조회
  13. var item = await db.Post
  14. .Include(c => c.Board)
  15. .Include(c => c.BoardPrefix)
  16. .Include(c => c.Member).ThenInclude(m => m.MemberGrade)
  17. .Include(c => c.PostTag).ThenInclude(t => t.Tag)
  18. .FirstOrDefaultAsync(x => x.ID == request.ID && !x.IsDeleted, ct);
  19. if (item is null)
  20. {
  21. return Result.Failure<Response>(
  22. Error.NotFound("Post.NotFound", "게시글을 찾을 수 없습니다.")
  23. );
  24. }
  25. // 조회수 증가 (24시간 내 중복 방지)
  26. var cutoff = DateTime.UtcNow.AddHours(-24);
  27. bool isDuplicate;
  28. if (request.MemberID is int viewerID)
  29. {
  30. isDuplicate = await db.PostViewLog.AsNoTracking().AnyAsync(x => x.PostID == item.ID && x.MemberID == viewerID && x.CreatedAt > cutoff, ct);
  31. }
  32. else
  33. {
  34. isDuplicate = await db.PostViewLog.AsNoTracking().AnyAsync(x => x.PostID == item.ID && x.MemberID == null && x.IpAddress == request.IpAddress && x.CreatedAt > cutoff, ct);
  35. }
  36. if (!isDuplicate)
  37. {
  38. item.Views++;
  39. db.PostViewLog.Add(new PostViewLog
  40. {
  41. PostID = item.ID,
  42. MemberID = request.MemberID,
  43. IpAddress = request.IpAddress,
  44. });
  45. await db.SaveChangesAsync(ct);
  46. }
  47. // 이전/다음 게시글
  48. var prevID = await db.Post.AsNoTracking()
  49. .Where(x => x.BoardID == item.BoardID && x.ID < item.ID && !x.IsDeleted)
  50. .OrderByDescending(x => x.ID)
  51. .Select(x => (int?)x.ID)
  52. .FirstOrDefaultAsync(ct);
  53. var nextID = await db.Post.AsNoTracking()
  54. .Where(x => x.BoardID == item.BoardID && x.ID > item.ID && !x.IsDeleted)
  55. .OrderBy(x => x.ID)
  56. .Select(x => (int?)x.ID)
  57. .FirstOrDefaultAsync(ct);
  58. // 사용자별 상태
  59. bool hasLike = false, hasDislike = false, hasBookmark = false, hasReport = false;
  60. if (request.MemberID is int memberID)
  61. {
  62. hasLike = await db.PostReaction.AsNoTracking().AnyAsync(x => x.PostID == item.ID && x.MemberID == memberID && x.Reaction == Reaction.Like, ct);
  63. hasDislike = await db.PostReaction.AsNoTracking().AnyAsync(x => x.PostID == item.ID && x.MemberID == memberID && x.Reaction == Reaction.Dislike, ct);
  64. hasBookmark = await db.PostBookmark.AsNoTracking().AnyAsync(x => x.PostID == item.ID && x.MemberID == memberID, ct);
  65. hasReport = await db.PostReport.AsNoTracking().AnyAsync(x => x.PostID == item.ID && x.MemberID == memberID, ct);
  66. }
  67. // 태그 목록
  68. var tagList = item.PostTag.Select(t => new Response.TagDto(t.Tag.ID, t.Tag.Slug)).ToList();
  69. // 말머리
  70. Response.BoardPrefixDto? boardPrefix = item.BoardPrefix is not null
  71. ? new Response.BoardPrefixDto(
  72. item.BoardPrefix.ID,
  73. item.BoardPrefix.BoardID,
  74. item.BoardPrefix.Name,
  75. item.BoardPrefix.Color,
  76. item.BoardPrefix.Posts
  77. ) : null;
  78. // 작성자
  79. Response.WriterDto? writer = item.Member is not null
  80. ? new Response.WriterDto(
  81. item.Member.ID,
  82. item.Member.SID,
  83. item.Member.Name,
  84. item.Member.Thumb,
  85. item.Member.Icon,
  86. item.Member.MemberGrade?.Image,
  87. item.Member.Summary,
  88. item.Member.CreatedAt
  89. ) : null;
  90. return new Response(
  91. item.ID,
  92. item.BoardID,
  93. item.Board.Code,
  94. item.BoardPrefixID,
  95. item.MemberID,
  96. item.Subject,
  97. item.Content,
  98. item.SID,
  99. item.Email,
  100. item.Name,
  101. boardPrefix,
  102. writer,
  103. item.IsReply,
  104. item.IsAnonymous,
  105. item.IsSecret,
  106. item.IsNotice,
  107. item.IsSpeaker,
  108. item.Views,
  109. item.Likes,
  110. item.Dislikes,
  111. item.Comments,
  112. item.Reports,
  113. item.Images,
  114. item.Medias,
  115. item.Files,
  116. item.Tags,
  117. item.IpAddress,
  118. item.CreatedAt,
  119. tagList,
  120. prevID,
  121. nextID,
  122. hasLike,
  123. hasDislike,
  124. hasBookmark,
  125. hasReport
  126. );
  127. }
  128. }