瀏覽代碼

no message

KIM-JINO5 2 月之前
父節點
當前提交
a47ff1d2ba

+ 37 - 3
Admin/Pages/Forum/Trash/Comment/Index.cshtml

@@ -60,8 +60,8 @@
             </select>
         </div>
         <div class="col-auto">
-            <button type="button" id="btnListRestore" class="btn btn-success" disabled>복원</button>
-            <button type="button" id="btnListDelete" class="btn btn-danger" disabled>삭제</button>
+            <button type="button" id="btnListRestore" class="btn btn-success" disabled form="fAdminList">복원</button>
+            <button type="button" id="btnListDelete" class="btn btn-danger" disabled form="fAdminList">삭제</button>
         </div>
     </div>
 
@@ -111,7 +111,41 @@
                                 </div>
                             </td>
                             <td class="text-start">
-                                @(row.Content.Length > 80 ? row.Content[..80] + "..." : row.Content)
+                                @{
+                                    var plainText = System.Text.RegularExpressions.Regex.Replace(row.Content ?? "", "<[^>]+>", string.Empty).Trim();
+                                    var preview = plainText.Length > 80 ? plainText[..80] + "..." : plainText;
+                                    var hasHtml = System.Text.RegularExpressions.Regex.IsMatch(row.Content ?? "", "<[^>]+>");
+                                }
+                                @if (row.IsReply)
+                                {
+                                    <span class="badge bg-info me-1">답글</span>
+                                }
+                                @if (hasHtml)
+                                {
+                                    <button type="button" class="btn btn-sm btn-link p-0 ms-1" data-bs-toggle="modal" data-bs-target="#@("detail_" + row.ID)">
+                                        [상세보기]
+                                    </button>
+                                    <div class="modal fade" id="@("detail_" + row.ID)" tabindex="-1" aria-labelledby="@("detail_" + row.ID + "Label")" aria-hidden="true">
+                                        <div class="modal-dialog modal-lg">
+                                            <div class="modal-content">
+                                                <div class="modal-header">
+                                                    <h1 class="modal-title fs-5" id="@("detail_" + row.ID + "Label")">댓글 내용</h1>
+                                                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                                                </div>
+                                                <div class="modal-body">
+                                                    @Html.Raw(row.Content)
+                                                </div>
+                                                <div class="modal-footer justify-content-center">
+                                                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">닫기</button>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                } 
+                                else
+                                {
+                                    @(preview)
+                                }
                             </td>
                             <td class="text-start">
                                 <a href="/Forum/Posts/List/Edit/@row.PostID">

+ 2 - 0
Admin/Pages/Forum/Trash/Comment/Index.cshtml.cs

@@ -42,6 +42,7 @@ namespace Admin.Pages.Forum.Trash.Comment
             int PostID,
             string PostSubject,
             string Content,
+            bool IsReply,
             string? Name,
             string? SID,
             string? DeletedAt,
@@ -82,6 +83,7 @@ namespace Admin.Pages.Forum.Trash.Comment
                 c.PostID,
                 c.PostSubject,
                 c.Content,
+                c.IsReply,
                 c.Name,
                 c.SID,
                 c.DeletedAt.GetDateAt() ?? "-",

+ 2 - 2
Admin/Pages/Forum/Trash/Post/Index.cshtml

@@ -59,8 +59,8 @@
             </select>
         </div>
         <div class="col-auto">
-            <button type="button" id="btnListRestore" class="btn btn-success" disabled>복원</button>
-            <button type="button" id="btnListDelete" class="btn btn-danger" disabled>삭제</button>
+            <button type="button" id="btnListRestore" class="btn btn-success" disabled form="fAdminList">복원</button>
+            <button type="button" id="btnListDelete" class="btn btn-danger" disabled form="fAdminList">삭제</button>
         </div>
     </div>
 

二進制
Admin/wwwroot/uploads/post/38/62386546b4b346a185ce41ee05fcf257.jpg


二進制
Admin/wwwroot/uploads/post/39/a8b1d1256b004204b6c630f24a737536.png


二進制
Admin/wwwroot/uploads/post/40/21eca82bc422402fb216643f9ee13865.jpg


二進制
Admin/wwwroot/uploads/post/5/0af0d89a44af4bd794dffa7ccb0f65a3.jpg


+ 16 - 10
Application/Features/Admin/Forum/Comment/Delete/Handler.cs

@@ -9,38 +9,44 @@ public sealed class Handler(IAppDbContext db) : ICommandHandler<Command>
     public async Task Handle(Command request, CancellationToken ct)
     {
         if (request.IDs is null || request.IDs.Length == 0)
+        {
             return;
+        }
 
-        var comments = await db.Comment
-            .Where(c => request.IDs.Contains(c.ID))
-            .ToListAsync(ct);
+        var comments = await db.Comment.Where(c => request.IDs.Contains(c.ID) && !c.IsDeleted).ToListAsync(ct);
+        if (comments.Count == 0)
+        {
+            return;
+        }
 
-        // 미삭제 댓글만 카운트 감소 (소프트 삭제된 댓글은 이미 카운트가 감소됨)
-        var activeComments = comments.Where(c => !c.IsDeleted).ToList();
+        foreach (var comment in comments)
+        {
+            comment.IsDeleted = true;
+            comment.DeletedAt = DateTime.UtcNow;
+        }
 
         // Post 댓글 카운트 감소
-        var postIDs = activeComments.Select(c => c.PostID).Distinct().ToList();
+        var postIDs = comments.Select(c => c.PostID).Distinct().ToList();
         var posts = await db.Post.Where(c => postIDs.Contains(c.ID)).ToListAsync(ct);
 
         foreach (var post in posts)
         {
-            var count = activeComments.Count(c => c.PostID == post.ID);
+            var count = comments.Count(c => c.PostID == post.ID);
             post.Comments -= count;
             post.UpdatedAt = DateTime.UtcNow;
         }
 
         // Board 댓글 카운트 감소
-        var boardIDs = activeComments.Select(c => c.BoardID).Distinct().ToList();
+        var boardIDs = comments.Select(c => c.BoardID).Distinct().ToList();
         var boards = await db.Board.Where(c => boardIDs.Contains(c.ID)).ToListAsync(ct);
 
         foreach (var board in boards)
         {
-            var count = activeComments.Count(c => c.BoardID == board.ID);
+            var count = comments.Count(c => c.BoardID == board.ID);
             board.Comments -= count;
             board.UpdatedAt = DateTime.UtcNow;
         }
 
-        db.Comment.RemoveRange(comments);
         await db.SaveChangesAsync(ct);
     }
 }

+ 9 - 10
Application/Features/Admin/Forum/Post/Delete/Handler.cs

@@ -1,11 +1,10 @@
 using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using SharedKernel.Storage;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Admin.Forum.Post.Delete;
 
-public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : ICommandHandler<Command>
+public sealed class Handler(IAppDbContext db) : ICommandHandler<Command>
 {
     public async Task Handle(Command request, CancellationToken ct)
     {
@@ -14,18 +13,19 @@ public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IComma
             return;
         }
 
-        var posts = await db.Post.Where(c => request.IDs.Contains(c.ID)).ToListAsync(ct);
+        var posts = await db.Post.Where(c => request.IDs.Contains(c.ID) && !c.IsDeleted).ToListAsync(ct);
+        if (posts.Count == 0)
+        {
+            return;
+        }
 
-        // 썸네일 파일 삭제
         foreach (var post in posts)
         {
-            if (!string.IsNullOrEmpty(post.Thumbnail))
-            {
-                fileStorage.DeleteByUrl(post.Thumbnail);
-            }
+            post.IsDeleted = true;
+            post.DeletedAt = DateTime.UtcNow;
         }
 
-        // Board/BoardGroup 카운트 감소
+        // Board 카운트 감소
         var boardIDs = posts.Select(c => c.BoardID).Distinct().ToList();
         var boards = await db.Board.Where(c => boardIDs.Contains(c.ID)).ToListAsync(ct);
         var groupIDs = boards.Select(c => c.BoardGroupID).Distinct().ToList();
@@ -46,7 +46,6 @@ public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IComma
             group.UpdatedAt = DateTime.UtcNow;
         }
 
-        db.Post.RemoveRange(posts);
         await db.SaveChangesAsync(ct);
     }
 }

+ 39 - 2
Application/Features/Admin/Forum/Trash/Comment/PermanentDelete/Handler.cs

@@ -14,9 +14,46 @@ public sealed class Handler(IAppDbContext db) : ICommandHandler<Command>
         }
 
         var comments = await db.Comment.Where(c => request.IDs.Contains(c.ID) && c.IsDeleted).ToListAsync(ct);
+        if (comments.Count == 0)
+        {
+            return;
+        }
+
+        // 자식 댓글(대댓글) 재귀 조회 → 삭제 대상에 포함
+        var allIDs = comments.Select(c => c.ID).ToHashSet();
+        var parentIDs = allIDs.ToList();
+
+        while (parentIDs.Count > 0)
+        {
+            var childComments = await db.Comment.Where(c => c.ParentID != null && parentIDs.Contains(c.ParentID.Value) && !allIDs.Contains(c.ID)).ToListAsync(ct);
+            if (childComments.Count == 0)
+            {
+                break;
+            }
+
+            parentIDs = [.. childComments.Select(c => c.ID)];
+            allIDs.UnionWith(parentIDs);
+            comments.AddRange(childComments);
+        }
+
+        var allIDList = allIDs.ToList();
+
+        // 로그 테이블 선삭제 (Restrict FK 충돌 방지)
+        var fileDownLogs = await db.CommentFileDownLog.Where(c => allIDList.Contains(c.CommentID)).ToListAsync(ct);
+        if (fileDownLogs.Count > 0)
+        {
+            db.CommentFileDownLog.RemoveRange(fileDownLogs);
+        }
+
+        var linkClickLogs = await db.CommentLinkClickLog.Where(c => allIDList.Contains(c.CommentID)).ToListAsync(ct);
+        if (linkClickLogs.Count > 0)
+        {
+            db.CommentLinkClickLog.RemoveRange(linkClickLogs);
+        }
 
-        // 소프트 삭제 시 이미 카운트가 감소되었으므로 영구 삭제에서는 카운트를 건드리지 않음
-        db.Comment.RemoveRange(comments);
+        // 자식 먼저 삭제되도록 Depth 역순 정렬
+        var orderedComments = comments.OrderByDescending(c => c.Depth).ToList();
+        db.Comment.RemoveRange(orderedComments);
 
         await db.SaveChangesAsync(ct);
     }

+ 2 - 0
Application/Features/Admin/Forum/Trash/Comment/Search/Handler.cs

@@ -53,6 +53,7 @@ public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
                 c.PostID,
                 PostSubject = c.Post.Subject,
                 c.Content,
+                c.IsReply,
                 c.Name,
                 c.SID,
                 c.DeletedAt,
@@ -72,6 +73,7 @@ public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
                 c.PostID,
                 c.PostSubject,
                 c.Content,
+                c.IsReply,
                 c.Name,
                 c.SID,
                 c.DeletedAt,

+ 1 - 0
Application/Features/Admin/Forum/Trash/Comment/Search/Response.cs

@@ -10,6 +10,7 @@ public sealed record Response(int Total, List<Response.Row> List)
         int PostID,
         string PostSubject,
         string Content,
+        bool IsReply,
         string? Name,
         string? SID,
         DateTime? DeletedAt,

+ 28 - 14
Application/Features/Admin/Forum/Trash/Post/PermanentDelete/Handler.cs

@@ -15,6 +15,10 @@ public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IComma
         }
 
         var posts = await db.Post.Where(c => request.IDs.Contains(c.ID) && c.IsDeleted).ToListAsync(ct);
+        if (posts.Count == 0)
+        {
+            return;
+        }
 
         foreach (var post in posts)
         {
@@ -24,26 +28,36 @@ public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IComma
             }
         }
 
-        var boardPostCounts = posts.GroupBy(p => p.BoardID).ToDictionary(g => g.Key, g => g.Count());
-        var boards = await db.Board.Where(b => boardPostCounts.Keys.Contains(b.ID)).ToListAsync(ct);
-        foreach (var board in boards)
+        var postIDs = posts.Select(c => c.ID).ToList();
+
+        // 로그 테이블 선삭제 (Restrict FK 충돌 방지)
+        // Post 로그
+        var postFileDownLogs = await db.PostFileDownLog.Where(c => postIDs.Contains(c.PostID)).ToListAsync(ct);
+        if (postFileDownLogs.Count > 0)
         {
-            board.Posts -= boardPostCounts[board.ID];
-            board.UpdatedAt = DateTime.UtcNow;
+            db.PostFileDownLog.RemoveRange(postFileDownLogs);
         }
 
-        var boardGroupIDs = boards.Select(c => c.BoardGroupID).Distinct().ToList();
-        var boardGroups = await db.BoardGroup.Where(g => boardGroupIDs.Contains(g.ID)).ToListAsync(ct);
-
-        var boardToGroup = boards.ToDictionary(b => b.ID, b => b.BoardGroupID);
-        var groupPostCounts = posts.GroupBy(p => boardToGroup[p.BoardID]).ToDictionary(g => g.Key, g => g.Count());
+        var postLinkClickLogs = await db.PostLinkClickLog.Where(c => postIDs.Contains(c.PostID)).ToListAsync(ct);
+        if (postLinkClickLogs.Count > 0)
+        {
+            db.PostLinkClickLog.RemoveRange(postLinkClickLogs);
+        }
 
-        foreach (var group in boardGroups)
+        // Comment 로그 (Post → Comment Cascade 시 충돌 방지)
+        var commentIDs = await db.Comment.Where(c => postIDs.Contains(c.PostID)).Select(c => c.ID).ToListAsync(ct);
+        if (commentIDs.Count > 0)
         {
-            if (groupPostCounts.TryGetValue(group.ID, out var count))
+            var commentFileDownLogs = await db.CommentFileDownLog.Where(c => commentIDs.Contains(c.CommentID)).ToListAsync(ct);
+            if (commentFileDownLogs.Count > 0)
+            {
+                db.CommentFileDownLog.RemoveRange(commentFileDownLogs);
+            }
+
+            var commentLinkClickLogs = await db.CommentLinkClickLog.Where(c => commentIDs.Contains(c.CommentID)).ToListAsync(ct);
+            if (commentLinkClickLogs.Count > 0)
             {
-                group.Posts -= count;
-                group.UpdatedAt = DateTime.UtcNow;
+                db.CommentLinkClickLog.RemoveRange(commentLinkClickLogs);
             }
         }
 

+ 10 - 10
Infrastructure/Infrastructure.csproj

@@ -9,19 +9,19 @@
   <ItemGroup>
     <PackageReference Include="AspNetCore.HealthChecks.Redis" Version="9.0.0" />
     <PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
-    <PackageReference Include="MailKit" Version="4.14.1" />
-    <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.2" />
-    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
-    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
-    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.2" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
+    <PackageReference Include="MailKit" Version="4.15.1" />
+    <PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.5" />
+    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
+    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
+    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.5" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.2" />
-    <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.2" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.5" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.5" />
+    <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.5" />
     <PackageReference Include="MimeKit" Version="4.15.1" />
   </ItemGroup>
 

+ 2 - 3
Web.Api/appsettings.Development.json

@@ -1,9 +1,8 @@
 {
     "Kestrel": {
         "Endpoints": {
-            "Http": {
-                "Url": "http://localhost:4000"
-            }
+            "Http": { "Url": "http://localhost:4000" },
+            "Https": { "Url": "https://localhost:4000" }
         }
     },