using Application.Abstractions.Data; using Application.Abstractions.Forum; using Application.Abstractions.Messaging; using Domain.Entities.Forum.ValueObject; using Microsoft.EntityFrameworkCore; namespace Application.Features.Api.Forum.Post.Search; public sealed class Handler(IAppDbContext db, IBoardPermissionService permissionService) : IQueryHandler { public async Task Handle(Query request, CancellationToken ct) { // 공통 기본 쿼리 var baseQuery = db.Post.AsNoTracking().Include(c => c.Board).Include(c => c.BoardPrefix).Where(c => !c.IsDeleted).AsQueryable(); if (request.BoardID.HasValue) { baseQuery = baseQuery.Where(c => c.BoardID == request.BoardID.Value); } if (!string.IsNullOrWhiteSpace(request.Keyword)) { var kw = request.Keyword.Trim(); baseQuery = request.Search switch { 0 => baseQuery.Where(c => c.Subject.Contains(kw)), 1 => baseQuery.Where(c => c.Content.Contains(kw)), 2 => baseQuery.Where(c => (c.Name != null && c.Name.Contains(kw)) || (c.SID != null && c.SID.Contains(kw))), _ => baseQuery.Where(c => c.Subject.Contains(kw) || c.Content.Contains(kw)) }; } if (!string.IsNullOrWhiteSpace(request.StartAt) && DateTime.TryParse(request.StartAt, out var startDate)) { baseQuery = baseQuery.Where(c => c.CreatedAt >= startDate); } if (!string.IsNullOrWhiteSpace(request.EndAt) && DateTime.TryParse(request.EndAt, out var endDate)) { baseQuery = baseQuery.Where(c => c.CreatedAt <= endDate.AddDays(1)); } if (request.IsSecret == true) { baseQuery = baseQuery.Where(c => c.IsSecret); } if (request.IsReply == true) { baseQuery = baseQuery.Where(c => c.IsReply); } if (request.IsDeleted == true) { baseQuery = baseQuery.Where(c => c.IsDeleted); } var exceptSpeaker = false; var exceptNotice = false; if (request.BoardID.HasValue) { var boardMeta = await db.BoardMeta.AsNoTracking().FirstOrDefaultAsync(x => x.BoardID == request.BoardID.Value, ct); if (boardMeta is not null) { exceptSpeaker = boardMeta.List.ExceptSpeaker; exceptNotice = boardMeta.List.ExceptNotice; // QnA(1:1 문의) 게시판: 본인 글만 표시 (관리자·매니저 제외) if (boardMeta.List.Layout == BoardLayout.QnA && request.MemberID.HasValue) { var member = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.ID == request.MemberID.Value, ct); if (member is not null && !member.IsAdmin) { var mgr = await permissionService.GetBoardManagerAsync(request.BoardID.Value, member.ID, ct); if (mgr is null) { baseQuery = baseQuery.Where(c => c.MemberID == request.MemberID.Value); } } } } } // ── 1) Speaker (전체공지) 조회: 1페이지 && ExceptSpeaker가 아닐 때 ── var speakerList = new List(); if (request.Page == 1 && !exceptSpeaker) { var speakers = await baseQuery .Where(c => c.IsSpeaker) .OrderByDescending(c => c.ID) .Select(c => new { c.ID, c.BoardID, BoardName = c.Board.Name, c.BoardPrefixID, BoardPrefix = c.BoardPrefix != null ? new { c.BoardPrefix.ID, c.BoardPrefix.BoardID, c.BoardPrefix.Name, c.BoardPrefix.Color, c.BoardPrefix.Posts } : null, c.Subject, c.Thumbnail, c.Name, c.SID, c.IsNotice, c.IsSecret, c.IsReply, c.IsSpeaker, c.IsAnonymous, c.IsDeleted, c.Views, c.Likes, c.Dislikes, c.Comments, c.Images, c.Medias, c.Files, c.Tags, c.UpdatedAt, c.CreatedAt }) .ToListAsync(ct); speakerList = [..speakers.Select(c => new Response.Row( Num: 0, c.ID, c.BoardID, c.BoardName, c.BoardPrefixID, c.BoardPrefix != null ? new Response.BoardPrefixData(c.BoardPrefix.ID, c.BoardPrefix.BoardID, c.BoardPrefix.Name, c.BoardPrefix.Color, c.BoardPrefix.Posts) : null, c.Subject, c.Thumbnail, c.Name, c.SID, c.IsNotice, c.IsSecret, c.IsReply, c.IsSpeaker, c.IsAnonymous, c.IsDeleted, c.Views, c.Likes, c.Dislikes, c.Comments, c.Images, c.Medias, c.Files, c.Tags, c.UpdatedAt, c.CreatedAt ))]; } // ── 2) Notice (게시판 공지) 조회: 1페이지 && ExceptNotice가 아닐 때 ── var noticeList = new List(); if (request.Page == 1 && !exceptNotice) { var notices = await baseQuery .Where(c => c.IsNotice && !c.IsSpeaker) .OrderByDescending(c => c.ID) .Select(c => new { c.ID, c.BoardID, BoardName = c.Board.Name, c.BoardPrefixID, BoardPrefix = c.BoardPrefix != null ? new { c.BoardPrefix.ID, c.BoardPrefix.BoardID, c.BoardPrefix.Name, c.BoardPrefix.Color, c.BoardPrefix.Posts } : null, c.Subject, c.Thumbnail, c.Name, c.SID, c.IsNotice, c.IsSecret, c.IsReply, c.IsSpeaker, c.IsAnonymous, c.IsDeleted, c.Views, c.Likes, c.Dislikes, c.Comments, c.Images, c.Medias, c.Files, c.Tags, c.UpdatedAt, c.CreatedAt }) .ToListAsync(ct); noticeList = [..notices.Select(c => new Response.Row( Num: 0, c.ID, c.BoardID, c.BoardName, c.BoardPrefixID, c.BoardPrefix != null ? new Response.BoardPrefixData(c.BoardPrefix.ID, c.BoardPrefix.BoardID, c.BoardPrefix.Name, c.BoardPrefix.Color, c.BoardPrefix.Posts) : null, c.Subject, c.Thumbnail, c.Name, c.SID, c.IsNotice, c.IsSecret, c.IsReply, c.IsSpeaker, c.IsAnonymous, c.IsDeleted, c.Views, c.Likes, c.Dislikes, c.Comments, c.Images, c.Medias, c.Files, c.Tags, c.UpdatedAt, c.CreatedAt ))]; } // ── 3) 일반 글 조회: 공지 제외 + 페이지네이션 ── var listQuery = baseQuery.Where(c => !c.IsNotice && !c.IsSpeaker); // IsNotice 필터가 명시적으로 true인 경우 (관리 목적 등) 공지만 조회 if (request.IsNotice == true) { listQuery = baseQuery.Where(c => c.IsNotice); } if (request.BoardPrefixID.HasValue) { listQuery = listQuery.Where(c => c.BoardPrefixID == request.BoardPrefixID.Value); } listQuery = request.Sort switch { 1 => listQuery.OrderByDescending(c => c.Views), 2 => listQuery.OrderByDescending(c => c.Comments), 3 => listQuery.OrderByDescending(c => c.Likes), _ => listQuery.OrderByDescending(c => c.ID) }; var total = await listQuery.CountAsync(ct); var list = await listQuery .Skip((request.Page - 1) * request.PerPage) .Take(request.PerPage) .Select(c => new { c.ID, c.BoardID, BoardName = c.Board.Name, c.BoardPrefixID, BoardPrefix = c.BoardPrefix != null ? new { c.BoardPrefix.ID, c.BoardPrefix.BoardID, c.BoardPrefix.Name, c.BoardPrefix.Color, c.BoardPrefix.Posts } : null, c.Subject, c.Thumbnail, c.Name, c.SID, c.IsNotice, c.IsSecret, c.IsReply, c.IsSpeaker, c.IsAnonymous, c.IsDeleted, c.Views, c.Likes, c.Dislikes, c.Comments, c.Images, c.Medias, c.Files, c.Tags, c.UpdatedAt, c.CreatedAt }) .ToListAsync(ct); var startNum = total - ((request.Page - 1) * request.PerPage); return new Response( total, speakerList, noticeList, [..list.Select((c, i) => new Response.Row( Num: startNum - i, c.ID, c.BoardID, c.BoardName, c.BoardPrefixID, c.BoardPrefix != null ? new Response.BoardPrefixData(c.BoardPrefix.ID, c.BoardPrefix.BoardID, c.BoardPrefix.Name, c.BoardPrefix.Color, c.BoardPrefix.Posts) : null, c.Subject, c.Thumbnail, c.Name, c.SID, c.IsNotice, c.IsSecret, c.IsReply, c.IsSpeaker, c.IsAnonymous, c.IsDeleted, c.Views, c.Likes, c.Dislikes, c.Comments, c.Images, c.Medias, c.Files, c.Tags, c.UpdatedAt, c.CreatedAt ))] ); } }