using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Features.Api.Feed.Shared; using Domain.Entities.Feed; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Feed.Update; public sealed class Handler(IAppDbContext db) : ICommandHandler { private const int MaxContentLength = 5000; public async Task Handle(Command request, CancellationToken ct) { if (string.IsNullOrWhiteSpace(request.Content)) { return Result.Failure(Error.Problem("Feed.ContentRequired", "내용을 입력해주세요.")); } var content = request.Content.Trim(); if (content.Length > MaxContentLength) { return Result.Failure(Error.Problem("Feed.ContentTooLong", $"내용은 최대 {MaxContentLength}자까지 입력 가능합니다.")); } var post = await db.FeedPost.FirstOrDefaultAsync(p => p.ID == request.PostID && !p.IsDeleted, ct); if (post is null) { return Result.Failure(Error.NotFound("Feed.PostNotFound", "게시글을 찾을 수 없습니다.")); } if (post.MemberID != request.MemberID) { return Result.Failure(Error.Forbidden("Feed.NotOwner", "수정 권한이 없습니다.")); } post.Content = content; post.UpdatedAt = DateTime.UtcNow; // ── 태그 재동기화: 기존 FeedPostTag 전부 제거 후 다시 붙임 ── var existingTagLinks = await db.FeedPostTag.Where(pt => pt.FeedPostID == post.ID).ToListAsync(ct); var existingTagIDs = existingTagLinks.Select(l => l.FeedTagID).ToList(); db.FeedPostTag.RemoveRange(existingTagLinks); // 기존 태그 UsageCount 차감 if (existingTagIDs.Count > 0) { var oldTags = await db.FeedTag.Where(t => existingTagIDs.Contains(t.ID)).ToListAsync(ct); foreach (var tag in oldTags) { tag.UsageCount = tag.UsageCount > 0 ? tag.UsageCount - 1 : 0; tag.UpdatedAt = DateTime.UtcNow; } } var hashtagsFromContent = FeedContent.ExtractHashtags(content); var mergedTags = new List(hashtagsFromContent); if (request.Tags is { Count: > 0 }) { foreach (var t in request.Tags) { if (!string.IsNullOrWhiteSpace(t) && !mergedTags.Any(x => string.Equals(x, t, StringComparison.OrdinalIgnoreCase))) { mergedTags.Add(t.Trim()); } } } byte tagCount = 0; foreach (var tagName in mergedTags) { if (string.IsNullOrWhiteSpace(tagName)) { continue; } var name = tagName.Trim(); var slug = name.ToLowerInvariant().Replace(' ', '-'); var tag = await db.FeedTag.FirstOrDefaultAsync(t => t.Name == name, ct); if (tag is null) { tag = new FeedTag { Name = name, Slug = slug }; await db.FeedTag.AddAsync(tag, ct); await db.SaveChangesAsync(ct); } tag.UsageCount++; tag.UpdatedAt = DateTime.UtcNow; await db.FeedPostTag.AddAsync(new FeedPostTag { FeedPostID = post.ID, FeedTagID = tag.ID }, ct); tagCount++; } post.Tags = tagCount; await db.SaveChangesAsync(ct); return Result.Success(); } }