KIM-JINO5 4 ماه پیش
والد
کامیت
23cd24143c
100فایلهای تغییر یافته به همراه3277 افزوده شده و 142 حذف شده
  1. 202 0
      Admin/Pages/Forum/Board/Group.cshtml
  2. 74 0
      Admin/Pages/Forum/Board/List/Edit.cshtml
  3. 125 0
      Admin/Pages/Forum/Board/List/Index.cshtml
  4. 70 0
      Admin/Pages/Forum/Board/List/Write.cshtml
  5. 160 0
      Admin/Pages/Forum/Board/Manager.cshtml
  6. 225 0
      Admin/Pages/Forum/Board/Meta/Comment.cshtml
  7. 293 0
      Admin/Pages/Forum/Board/Meta/Exp.cshtml
  8. 97 0
      Admin/Pages/Forum/Board/Meta/General.cshtml
  9. 153 0
      Admin/Pages/Forum/Board/Meta/List.cshtml
  10. 109 0
      Admin/Pages/Forum/Board/Meta/Notify.cshtml
  11. 58 0
      Admin/Pages/Forum/Board/Meta/NotifyTemplate.cshtml
  12. 82 0
      Admin/Pages/Forum/Board/Meta/Permission.cshtml
  13. 175 0
      Admin/Pages/Forum/Board/Meta/View.cshtml
  14. 267 0
      Admin/Pages/Forum/Board/Meta/Write.cshtml
  15. 44 0
      Admin/Pages/Forum/Board/Meta/_Header.cshtml
  16. 43 0
      Admin/Pages/Forum/Board/Meta/_Navbar.cshtml
  17. 148 0
      Admin/Pages/Forum/Board/Prefix.cshtml
  18. 44 0
      Admin/Pages/Forum/Board/_Header.cshtml
  19. 42 0
      Admin/Pages/Forum/Board/_Navbar.cshtml
  20. 96 0
      Admin/Pages/Forum/Posts/List/Edit.cshtml
  21. 268 0
      Admin/Pages/Forum/Posts/List/List.cshtml
  22. 136 0
      Admin/Pages/Forum/Posts/List/Write.cshtml
  23. 10 0
      Application/Abstractions/Messaging/ICommand.cs
  24. 11 0
      Application/Abstractions/Messaging/ICommandHandler.cs
  25. 12 0
      Application/Abstractions/Messaging/ICommandHandlerWithResponse.cs
  26. 11 0
      Application/Abstractions/Messaging/ICommandWithResponse.cs
  27. 11 0
      Application/Abstractions/Messaging/IQuery.cs
  28. 12 0
      Application/Abstractions/Messaging/IQueryHandler.cs
  29. 157 0
      Application/Abstractions/Messaging/README.md
  30. 2 2
      Application/Features/Banner/Item/Create/Command.cs
  31. 2 2
      Application/Features/Banner/Item/Create/Handler.cs
  32. 2 2
      Application/Features/Banner/Item/Delete/Command.cs
  33. 2 2
      Application/Features/Banner/Item/Delete/Handler.cs
  34. 2 2
      Application/Features/Banner/Item/Get/Handler.cs
  35. 2 2
      Application/Features/Banner/Item/Get/Query.cs
  36. 2 2
      Application/Features/Banner/Item/Search/Handler.cs
  37. 2 2
      Application/Features/Banner/Item/Search/Query.cs
  38. 2 2
      Application/Features/Banner/Item/Update/Command.cs
  39. 2 2
      Application/Features/Banner/Item/Update/Handler.cs
  40. 2 2
      Application/Features/Banner/Position/GetAll/Handler.cs
  41. 2 2
      Application/Features/Banner/Position/GetAll/Query.cs
  42. 2 2
      Application/Features/Banner/Position/Save/Command.cs
  43. 2 2
      Application/Features/Banner/Position/Save/Handler.cs
  44. 2 2
      Application/Features/Config/Get/Handler.cs
  45. 2 2
      Application/Features/Config/Get/Query.cs
  46. 2 2
      Application/Features/Config/Update/Command.cs
  47. 2 2
      Application/Features/Config/Update/Handler.cs
  48. 2 2
      Application/Features/Director/Role/Create/Command.cs
  49. 2 2
      Application/Features/Director/Role/Create/Handler.cs
  50. 2 2
      Application/Features/Director/Role/Delete/Command.cs
  51. 2 2
      Application/Features/Director/Role/Delete/Handler.cs
  52. 2 2
      Application/Features/Director/Role/Get/Handler.cs
  53. 2 2
      Application/Features/Director/Role/Get/Query.cs
  54. 2 2
      Application/Features/Director/Role/Permissions/Get/Handler.cs
  55. 2 2
      Application/Features/Director/Role/Permissions/Get/Query.cs
  56. 2 2
      Application/Features/Director/Role/Permissions/Update/Command.cs
  57. 2 2
      Application/Features/Director/Role/Permissions/Update/Handler.cs
  58. 2 2
      Application/Features/Director/Roles/Get/Handler.cs
  59. 2 2
      Application/Features/Director/Roles/Get/Query.cs
  60. 2 2
      Application/Features/Director/User/Get/Handler.cs
  61. 2 2
      Application/Features/Director/User/Get/Query.cs
  62. 2 2
      Application/Features/Director/User/GetRoles/Handler.cs
  63. 2 2
      Application/Features/Director/User/GetRoles/Query.cs
  64. 2 2
      Application/Features/Director/User/Update/Command.cs
  65. 2 2
      Application/Features/Director/User/Update/Handler.cs
  66. 2 2
      Application/Features/Director/User/UpdateRoles/Command.cs
  67. 2 2
      Application/Features/Director/User/UpdateRoles/Handler.cs
  68. 2 2
      Application/Features/Director/Users/Get/Handler.cs
  69. 2 2
      Application/Features/Director/Users/Get/Query.cs
  70. 2 2
      Application/Features/Document/Create/Command.cs
  71. 2 2
      Application/Features/Document/Create/Handler.cs
  72. 2 2
      Application/Features/Document/Delete/Command.cs
  73. 2 2
      Application/Features/Document/Delete/Handler.cs
  74. 2 2
      Application/Features/Document/Get/Handler.cs
  75. 2 2
      Application/Features/Document/Get/Query.cs
  76. 2 2
      Application/Features/Document/Search/Handler.cs
  77. 2 2
      Application/Features/Document/Search/Query.cs
  78. 2 2
      Application/Features/Document/Update/Command.cs
  79. 2 2
      Application/Features/Document/Update/Handler.cs
  80. 2 2
      Application/Features/Faq/Category/GetAll/Handler.cs
  81. 2 2
      Application/Features/Faq/Category/GetAll/Query.cs
  82. 2 2
      Application/Features/Faq/Category/Save/Command.cs
  83. 2 2
      Application/Features/Faq/Category/Save/Handler.cs
  84. 2 2
      Application/Features/Faq/Item/Create/Command.cs
  85. 2 2
      Application/Features/Faq/Item/Create/Handler.cs
  86. 2 2
      Application/Features/Faq/Item/Delete/Command.cs
  87. 2 2
      Application/Features/Faq/Item/Delete/Handler.cs
  88. 2 2
      Application/Features/Faq/Item/Get/Handler.cs
  89. 2 2
      Application/Features/Faq/Item/Get/Query.cs
  90. 2 2
      Application/Features/Faq/Item/Search/Handler.cs
  91. 2 2
      Application/Features/Faq/Item/Search/Query.cs
  92. 2 2
      Application/Features/Faq/Item/Update/Command.cs
  93. 2 2
      Application/Features/Faq/Item/Update/Handler.cs
  94. 2 2
      Application/Features/Member/List/Approve/Command.cs
  95. 2 2
      Application/Features/Member/List/Approve/CommandHandler.cs
  96. 2 2
      Application/Features/Member/List/Approve/GetHandler.cs
  97. 2 2
      Application/Features/Member/List/Approve/Query.cs
  98. 2 2
      Application/Features/Member/List/Create/Command.cs
  99. 2 2
      Application/Features/Member/List/Create/Handler.cs
  100. 2 2
      Application/Features/Member/List/Delete/Command.cs

+ 202 - 0
Admin/Pages/Forum/Board/Group.cshtml

@@ -0,0 +1,202 @@
+@model Admin.ViewModels.Forum.Board.Group.IndexViewModel
+@using Library.Extensions
+@{
+    ViewData["Title"] = "게시판 분류";
+}
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @Model.Total
+        </div>
+        <div class="col text-end">
+            <button type="button" id="btnAdd" class="btn btn-sm btn-primary" form="fAdminWrite">추가</button>
+            <button type="submit" id="btnSave" class="btn btn-sm btn-success" form="fAdminWrite">저장</button>
+        </div>
+    </div>
+
+    <div class="table-responsive">
+        <form id="fAdminWrite" asp-action="Save" method="post" accept-charset="utf-8" autocomplete="off">
+            <table class="table table-striped table-bordered table-hover mt-3">
+                <caption>
+                    게시판 분류에 등록된 게시판이 있다면 삭제가 불가합니다.<br/>
+                    게시판 분류를 삭제하려면 등록된 게시판을 먼저 삭제해주세요.
+                </caption>
+                <colgroup>
+                    <col width="5%" />
+                    <col width="*" />
+                    <col width="20%" />
+                    <col width="*" />
+                    <col width="*" />
+                    <col width="*" />
+                    <col width="*" />
+                    <col width="*" />
+                    <col width="*" />
+                    <col width="*" />
+                </colgroup>
+                <thead>
+                    <tr>
+                        <th>ID</th>
+                        <th>Code</th>
+                        <th>분류 명</th>
+                        <th>순서</th>
+                        <th>게시판 수</th>
+                        <th>게시글 수</th>
+                        <th>댓글 수</th>
+                        <th>등록일시</th>
+                        <th>수정일시</th>
+                        <th>비고</th>
+                    </tr>
+                </thead>
+                <tbody id="boardGroupList">
+                    @if (Model.Data == null || !Model.Data.Any())
+                    {
+                        <tr>
+                            <td colspan="10">No Data.</td>
+                        </tr>
+                    }
+                    else
+                    {
+                        @foreach (var row in Model.Data)
+                        {
+                            var index = Model.Data.IndexOf(row);
+
+                            <tr>
+                                <td>
+                                    <input type="text" name="request[@index].ID" readonly class="form-control-plaintext text-center" required value="@row.ID" />
+                                    @if(row.Boards > 0) {
+                                        <span class="badge text-bg-danger">삭제 X</span>
+                                    }
+                                </td>
+                                <td>
+                                    <input type="text" name="request[@index].Code" class="form-control" maxlength="30" required value="@row.Code" />
+                                </td>
+                                <td>
+                                    <input type="text" name="request[@index].Name" class="form-control" maxlength="255" required value="@row.Name" />
+                                </td>
+                                <td>
+                                    <input type="number" name="request[@index].Order" class="form-control" min="-999" max="999" required value="@row.Order" />
+                                </td>
+                                <td>@row.Boards.ToString("N0")</td>
+                                <td>@row.Posts.ToString("N0")</td>
+                                <td>@row.Comments.ToString("N0")</td>
+                                <td>@row.CreatedAt.GetDateAt()</td>
+                                <td>@(row.UpdatedAt.GetDateAt() ?? "-")</td>
+                                <td>
+                                    <button type="button" class="btn btn-sm btn-danger btn-delete">삭제</button>
+                                </td>
+                            </tr>
+                        }
+                    }
+                </tbody>
+            </table>
+        </form>
+    </div>
+</div>
+
+@section Scripts {
+<script>
+    $(function() {
+        let $boardGroupList = $("#boardGroupList");
+        let total = Number(@Model.Total);
+
+        // 추가
+        $(document).on("click", "#btnAdd", function() {
+            if (total <= 0) {
+                $boardGroupList.empty();
+            }
+
+            let now = new Date().toLocaleString();
+            let tableRow = `
+                <tr>
+                    <td>-</td>
+                    <td>
+                        <input type="text" name="request[${total}].Code" class="form-control" maxlength="30" required form="fAdminWrite" />
+                    </td>
+                    <td>
+                        <input type="text" name="request[${total}].Name" class="form-control" maxlength="255" required form="fAdminWrite" />
+                    </td>
+                        <td>
+                        <input type="number" name="request[${total}].Order" class="form-control" min="-999" max="999" required form="fAdminWrite" />
+                    </td>
+                    <td>-</td>
+                    <td>-</td>
+                    <td>-</td>
+                    <td>${now}</td>
+                    <td>-</td>
+                    <td>
+                        <button type="button" class="btn btn-danger btn-sm btn-delete">삭제</button>
+                    </td>
+                </tr>
+            `;
+
+            $boardGroupList.append(tableRow);
+            total++;
+            recalculateIndices();
+        });
+
+        // 삭제
+        $(document).on("click", "button.btn-delete", function(e) {
+            e.target.closest("tr").remove();
+            total--;
+
+            if (total <= 0) {
+                $boardGroupList.append(
+                    `<tr><td colspan="10">No Data.</td></tr>`
+                );
+                total = 0;
+                } else {
+                recalculateIndices();
+                }
+        });
+
+        // 저장
+        $(document).on("click", "#btnSave", function() {
+            if (confirm("저장 하시겠습니까?")) {
+                let form = document.getElementById("fAdminWrite");
+                    if (form.checkValidity()) { // HTML5 폼 검증 수행
+                    form.submit();
+                } else {
+                    form.reportValidity();
+                }
+            }
+            return false;
+        });
+
+        // 인덱스 재계산 함수
+        function recalculateIndices() {
+            $boardGroupList.find("tr").each(function(index, tr) {
+                $(tr)
+                    .find("input, label")
+                    .each(function() {
+                        let name = $(this).attr("name");
+                        let id = $(this).attr("id");
+
+                        if (name) {
+                            $(this).attr("name", name.replace(/\[\d+\]/, `[${index}]`));
+                        }
+
+                        if (id) {
+                            $(this).attr("id", id.replace(/_\d+_/, `_${index}_`));
+                        }
+                    });
+
+                // 인덱스 기반으로 라벨의 `for` 속성도 수정
+                $(tr)
+                    .find("label")
+                    .each(function() {
+                        let labelFor = $(this).attr("for");
+                        if (labelFor) {
+                            $(this).attr("for", labelFor.replace(/_\d+_/, `_${index}_`));
+                        }
+                    });
+            });
+        }
+    });
+</script>
+}

+ 74 - 0
Admin/Pages/Forum/Board/List/Edit.cshtml

@@ -0,0 +1,74 @@
+@page
+@model Admin.Pages.Forum.Board.List.EditModel
+@{
+    ViewData["Title"] = "게시판 관리 - 기본";
+    ViewData["BoardID"] = Model.BoardID;
+    ViewData["BoardList"] = Model.BoardList;
+    ViewData["QueryString"] = Model.QueryString;
+}
+
+<div class="container">
+    <partial name="_StatusMessage" />
+    <partial name="_navTabs" />
+
+    <form id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off">
+        <input type="hidden" asp-for="ID" />
+
+        <div class="row mb-2">
+            <label for="BoardGroupID" class="col-sm-2 col-form-label"><span>*</span> 게시판 분류</label>
+            <div class="col-sm-10">
+                <select id="BoardGroupID" name="BoardGroupID" class="form-select w-auto" required asp-items="@Model.BoardGroupList">
+                    <option value="">게시판 분류 선택</option>
+                </select>
+                <span asp-validation-for="BoardGroupID" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Code" class="col-sm-2 col-form-label"><span>*</span> 주소(Code)</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Code" class="form-control" required maxlength="70" placeholder="중복 시 등록이 불가합니다. 70자 이내" />
+                <span asp-validation-for="Code" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Name" class="col-sm-2 col-form-label"><span>*</span> 게시판 명</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Name" class="form-control" required maxlength="70" />
+                <span asp-validation-for="Name" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Order" class="col-sm-2 col-form-label"><span>*</span> 순서</label>
+            <div class="col-sm-10">
+                <input type="number" asp-for="Order" class="form-control" min="-999" max="999" required />
+                <span asp-validation-for="Order" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsSearch" class="col-sm-2 col-form-label">검색 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsSearch" class="form-check-input" />
+                    <label class="form-check-label" for="IsSearch">검색에 포함합니다.</label>
+                    <span asp-validation-for="IsSearch" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">사용합니다.</label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-success">저장</button>
+            <a href="/Forum/Board/List?@Model.QueryString" class="btn btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>

+ 125 - 0
Admin/Pages/Forum/Board/List/Index.cshtml

@@ -0,0 +1,125 @@
+@page
+@model Admin.Pages.Forum.Board.List.IndexModel
+@{
+    ViewData["Title"] = "게시판 관리";
+}
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @Model.Total
+        </div>
+        <div class="col text-end">
+            <button type="button" id="btnListDelete" class="btn btn-sm btn-danger" disabled>삭제</button>
+            <button type="button" id="btnListUpdate" class="btn btn-sm btn-primary" disabled>수정</button>
+            <a class="btn btn-sm btn-success" asp-page="/Forum/Board/List/Write">추가</a>
+        </div>
+    </div>
+
+    <div class="table-responsive">
+        <form name="f_admin_list" id="fAdminList" method="post" accept-charset="utf-8" autocomplete="off">
+
+        <table class="table table-bordered mt-3">
+            <colgroup>
+                <col width="8%"/>
+                <col width="15%"/>
+                <col width="15%"/>
+                <col width="*"/>
+                <col width="*"/>
+                <col width="*"/>
+                <col width="*"/>
+                <col width="*"/>
+                <col width="11%"/>
+            </colgroup>
+            <thead>
+                <tr>
+                    <th rowspan="2">
+                        <div class="form-check-inline me-0">
+                            <input type="checkbox" id="checkedAll" class="form-check-input" value="1" form="fAdminList" />
+                            <label for="checkedAll" class="form-check-inline">ID</label>
+                        </div>
+                    </th>
+                    <th rowspan="2">분류</th>
+                    <th rowspan="2">Code</th>
+                    <th rowspan="2">제목</th>
+                    <th rowspan="2">순서</th>
+                    <th>검색 여부</th>
+                    <th>게시글 수</th>
+                    <th>등록일시</th>
+                    <th rowspan="2">비고</th>
+                </tr>
+                <tr>
+                    <th>사용 여부</th>
+                    <th>댓글 수</th>
+                    <th>수정일시</th>
+                </tr>
+            </thead>
+
+                @if (Model.Data == null || !Model.Data.Any())
+                {
+                    <tbody>
+                        <tr>
+                            <td colspan="9">No Data.</td>
+                        </tr>
+                    </tbody>
+                }
+                else
+                {
+                    @foreach (var row in Model.Data)
+                    {
+                        var index = row.ID;
+
+                        <tbody class="striped">
+                            <tr>
+                                <td rowspan="2">
+                                    <input type="hidden" name="request.Index" value="@index" />
+
+                                    <div class="form-check-inline me-0">
+                                        <input type="checkbox" name="request[@index].ID" id="chk_@row.ID" class="form-check-input list-check-box" value="@row.ID" />
+                                        <label for="chk_@row.ID" class="form-check-inline">@row.ID</label>
+                                    </div>
+                                </td>
+                                <td rowspan="2">
+                                    <select name="request[@index].BoardGroupID" class="form-select" required asp-items="@row.SelectBoardGroup"></select>
+                                </td>
+                                <td rowspan="2">
+                                    <input type="text" name="request[@index].Code" class="form-control" required maxlength="70" value="@row.Code" />
+                                </td>
+                                <td rowspan="2">
+                                    <input type="text" name="request[@index].Name" class="form-control" required maxlength="70" value="@row.Name" />
+                                </td>
+                                <td rowspan="2">
+                                    <input type="number" name="request[@index].Order" class="form-control" required min="-999" max="999" value="@row.Order" />
+                                </td>
+                                <td>
+                                    <input type="checkbox" name="request[@index].IsSearch" class="form-check-input" checked="@row.IsSearch" value="true" />
+                                </td>
+                                <td>@row.Posts</td>
+                                <td>@row.CreatedAt</td>
+                                <td rowspan="2">
+                                    <div class="d-xl-flex gap-2 justify-content-center d-grid">
+                                        <a class="btn btn-sm btn-outline-info" href="@row.EditURL">수정</a>
+                                        <a class="btn btn-sm btn-outline-danger btn-row-delete" data-id="@row.ID">삭제</a>
+                                    </div>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>
+                                    <input type="checkbox" name="request[@index].IsActive" class="form-check-input" checked="@row.IsActive" value="true" />
+                                </td>
+                                <td>@row.Comments</td>
+                                <td>@row.UpdatedAt</td>
+                            </tr>
+                        </tbody>
+                    }
+                }
+        </table>
+
+        </form>
+    </div>
+</div>

+ 70 - 0
Admin/Pages/Forum/Board/List/Write.cshtml

@@ -0,0 +1,70 @@
+@model Admin.ViewModels.Forum.Board.List.WriteViewModel
+@{
+    ViewData["Title"] = "게시판 등록";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/List/Create" enctype="multipart/form-data">
+        <div class="row mb-2">
+            <label for="BoardGroupID" class="col-sm-2 col-form-label"><span>*</span> 게시판 분류</label>
+            <div class="col-sm-10">
+                <select id="BoardGroupID" name="BoardGroupID" class="form-select w-auto" required asp-items="@Model.BoardGroupList">
+                    <option value="">게시판 분류 선택</option>
+                </select>
+                <span asp-validation-for="BoardGroupID" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Code" class="col-sm-2 col-form-label"><span>*</span> 주소(Code)</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Code" class="form-control" required maxlength="70" placeholder="중복 시 등록이 불가합니다. 70자 이내" />
+                <span asp-validation-for="Code" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Name" class="col-sm-2 col-form-label"><span>*</span> 게시판 명</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Name" class="form-control" required maxlength="70" />
+                <span asp-validation-for="Name" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Order" class="col-sm-2 col-form-label"><span>*</span> 순서</label>
+            <div class="col-sm-10">
+                <input type="number" asp-for="Order" class="form-control" min="-9999" max="9999" required />
+                <span asp-validation-for="Order" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsSearch" class="col-sm-2 col-form-label">검색 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsSearch" class="form-check-input" />
+                    <label class="form-check-label" for="IsSearch">검색에 포함합니다.</label>
+                    <span asp-validation-for="IsSearch" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">사용합니다.</label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>

+ 160 - 0
Admin/Pages/Forum/Board/Manager.cshtml

@@ -0,0 +1,160 @@
+@model Admin.ViewModels.Forum.Board.Manager.IndexViewModel
+@using Library.Extensions
+@{
+    ViewData["Title"] = "게시판 관리 - 관리자";
+    ViewData["BoardID"] = Model.BoardID;
+    ViewData["BoardList"] = Model.BoardList;
+    ViewData["QueryString"] = Model.QueryString;
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="~/Views/Forum/Board/_Navbar.cshtml" />
+
+    <form id="fAdminWrite" asp-action="Create" method="post" accept-charset="utf-8" autocomplete="off">
+        <input type="hidden" name="boardID" value="@Model.BoardID" />
+
+        <div class="row g-2">
+            <label class="col-lg-1 col-form-label">관리자</label>
+            <div class="col-12 col-sm-auto">
+                @if (Model.AdminList != null)
+                {
+                    <select name="UserId" class="form-select" required>
+                        <option value="">선택하세요.</option>
+                        @if (Model.AdminList != null)
+                        {
+                            @foreach (var row in Model.AdminList)
+                            {
+                                <option value="@row.Id">
+                                    @row.Email - @row.FullName
+                                </option>
+                            }
+                        }
+                    </select>
+                }
+            </div>
+            <div class="col-auto col-sm-auto col-form-label">
+                <div class="form-check form-check-inline">
+                    <input type="checkbox" name="CanEdit" id="CanEdit" class="form-check-input" checked value="true" />
+                    <label for="CanEdit" class="form-check-label">수정</label>
+                </div>
+            </div>
+            <div class="col-auto col-sm-auto col-form-label">
+                <div class="form-check form-check-inline">
+                    <input type="checkbox" name="CanDelete" id="CanDelete" class="form-check-input" checked value="true" />
+                    <label for="CanDelete" class="form-check-label">삭제</label>
+                </div>
+            </div>
+            <div class="col-12 col-sm-auto text-center">
+                <button type="submit" class="btn btn-primary w-100">등록</button>
+            </div>
+        </div>
+    </form>
+    
+    <hr/>
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @Model.Total.ToString("N0")
+        </div>
+        <div class="col text-end">
+            <button type="button" id="btnListDelete" class="btn btn-sm btn-danger" form="fAdminList" data-action="/Forum/Board/Manager/@Model.BoardID/Delete" disabled>삭제</button>
+            <button type="submit" id="btnListSave" class="btn btn-sm btn-success" form="fAdminList" @(Model.Total <= 0 ? "disabled" : "")> 저장</button>
+        </div>
+    </div>
+
+    <div class="table-responsive">
+        <form id="fAdminList" asp-action="Update" method="post" accept-charset="utf-8" autocomplete="off">
+            <input type="hidden" name="BoardID" value="@Model.BoardID" />
+
+            <table class="table table-striped table-bordered table-hover mt-3">
+                <caption>
+                    게시판을 운영할 수 있는 관리자를 등록합니다.
+                </caption>
+                <colgroup>
+                    <col width="10%"/>
+                    <col width="*"/>
+                    <col width="12%"/>
+                    <col width="12%"/>
+                    <col width="15%"/>
+                    <col width="15%"/>
+                </colgroup>
+                <thead>
+                    <tr>
+                        <th>
+                            <div class="form-check form-check-inline">
+                                <input type="checkbox" id="checkedAll" class="form-check-input" value="1" />
+                                <label for="checkedAll" class="form-check-label">ID</label>
+                            </div>
+                        </th>
+                        <th>관리자</th>
+                        <th>수정 권한</th>
+                        <th>삭제 권한</th>
+                        <th>등록일시</th>
+                        <th>수정일시</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    @if (Model.Data == null || !Model.Data.Any())
+                    {
+                        <tr>
+                            <td colspan="6">No Data.</td>
+                        </tr>
+                    }
+                    else
+                    {
+                        @foreach (var row in Model.Data)
+                        {
+                            var index = Model.Data.IndexOf(row);
+
+                            <tr>
+                                <td>
+                                    <div class="form-check form-check-inline">
+                                        <input type="checkbox" name="CheckList[]" id="CheckList_@index" class="form-check-input list-check-box" value="@row.ID" />
+                                        <label for="CheckList_@index" class="form-check-label">@row.ID</label>
+                                    </div>
+
+                                    <input type="hidden" name="Items[@index].ID" value="@row.ID" />
+                                    <input type="hidden" name="Items[@index].UserId" value="@row.UserId" />
+                                </td>
+                                <td>@row.User.Email</td>
+                                <td>
+                                    <div class="form-check form-check-inline">
+                                        <input type="checkbox" name="Items[@index].CanEdit" id="Items_@(index)_CanEdit" class="form-check-input" checked="@row.CanEdit" value="true" />
+                                        <label for="Items_@(index)_CanEdit" class="form-check-label">수정</label>
+                                    </div>
+                                </td>
+                                <td>
+                                    <div class="form-check form-check-inline">
+                                        <input type="checkbox" name="Items[@index].CanDelete" id="Items_@(index)_CanDelete" class="form-check-input" checked="@row.CanDelete" value="true" />
+                                        <label for="Items_@(index)_CanDelete" class="form-check-label">삭제</label>
+                                    </div>
+                                </td>
+                                <td>@row.CreatedAt.GetDateAt()</td>
+                                <td>@(row.UpdatedAt.GetDateAt() ?? "-")</td>
+                            </tr>
+                        }
+                    }
+                </tbody>
+            </table>
+        </form>
+    </div>
+</div>
+
+@section Scripts {
+<script>
+    // 저장
+    $(document).on("click", "#btnListSave", function() {
+        if (confirm("저장 하시겠습니까?")) {
+            let form = document.getElementById("fAdminList");
+            if (form.checkValidity()) { // HTML5 폼 검증 수행
+                form.submit();
+            } else {
+                form.reportValidity();
+            }
+        }
+        return false;
+    });
+</script>
+}

+ 225 - 0
Admin/Pages/Forum/Board/Meta/Comment.cshtml

@@ -0,0 +1,225 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@{
+    ViewData["Title"] = "게시판 관리 - 댓글";
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/Comment" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_EnableComment" class="col-md-3 col-form-label">댓글 사용</label>
+            <div class="col-md-9">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.EnableComment" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.EnableComment" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">댓글 목록, 사용을 활성화합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_PerPage" class="col-12 col-md-3 col-form-label">목록 표시</label>
+            <div class="col-lg-9">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Comment.PerPage" class="form-control" min="10" max="100" required />
+                        <span asp-validation-for="BoardMeta.Comment.PerPage" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">한 페이지에 보이는 댓글 수, (최대 100개)</small>
+            </div>
+        </div>
+
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_AllowLike" class="col-md-3">댓글 좋아요</label>
+            <div class="col-md-9 pt-2 pt-md-0">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.AllowLike" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.AllowLike" class="form-check-label">사용합니다.</label>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_AllowDisLike" class="col-md-3">댓글 싫어요</label>
+            <div class="col-md-9 pt-2 pt-md-0">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.AllowDisLike" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.AllowDisLike" class="form-check-label">사용합니다.</label>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_ShowMemberPhoto" class="col-md-3">회원 사진 공개</label>
+            <div class="col-md-9 pt-2 pt-md-0">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.ShowMemberPhoto" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.ShowMemberPhoto" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">회원이 등록한 사진을 좌측에 보일지를 결정합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_ShowMemberIcon" class="col-md-3">회원 아이콘 공개</label>
+            <div class="col-md-9 pt-2 pt-md-0">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.ShowMemberIcon" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.ShowMemberIcon" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">회원의 등급/첨부 아이콘을 글쓴이명 좌측에 보일지를 결정합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_ContentPlaceholder" class="col-md-3 col-form-label">안내 문구</label>
+            <div class="col-md-9">
+                <textarea asp-for="BoardMeta.Comment.ContentPlaceholder" class="form-control" rows="3"></textarea>
+                <span asp-validation-for="BoardMeta.Comment.ContentPlaceholder" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_MinContentLength" class="col-md-3 col-form-label">최소 입력 글자</label>
+            <div class="col-lg-9">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Comment.MinContentLength" class="form-control" min="0" required />
+                        <span asp-validation-for="BoardMeta.Comment.MinContentLength" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">댓글 최소 입력 길이를 지정합니다. 0 입력시 제한 없음</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_MaxContentLength" class="col-md-3 col-form-label">최대 입력 글자</label>
+            <div class="col-lg-9">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Comment.MaxContentLength" class="form-control" min="0" required />
+                        <span asp-validation-for="BoardMeta.Comment.MaxContentLength" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">댓글 최대 입력 길이를 지정합니다. 0 입력시 제한 없음</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_EnableEditor" class="col-md-3 col-form-label">웹 에디터 사용</label>
+            <div class="col-md-9">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.EnableEditor" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.EnableEditor" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">댓글을 웹 기반 에디터로 수정할 수 있도록합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_AllowSecret" class="col-md-3 col-form-label">비밀글 사용</label>
+            <div class="col-md-9">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.AllowSecret" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.AllowSecret" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">비밀글 작성 기능을 활성화합니다. 비밀글은 작성자 본인과 게시판 관리자 이상만 열람 가능합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_BlameHideCount" class="col-md-3 col-form-label">댓글 신고 시 숨김</label>
+            <div class="col-lg-9">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Comment.BlameHideCount" class="form-control" min="0" required />
+                        <span asp-validation-for="BoardMeta.Comment.BlameHideCount" class="text-danger"></span>
+                    </div>
+                    <small class="text-muted form-text">댓글을 신고할 수 있도록 합니다. 숨김 횟수가 0이면 작동하지 않습니다.</small>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_AllowUpdateProtection" class="col-md-3 col-form-label">댓글 보호 기능 (수정 시)</label>
+            <div class="col-md-9">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.AllowUpdateProtection" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.AllowUpdateProtection" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">수정 시 댓글을 보호하는 기능을 활성화합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_UpdateProtectionDays" class="col-md-3 col-form-label">댓글 수정 금지 기간</label>
+            <div class="col-lg-9">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Comment.UpdateProtectionDays" class="form-control" min="0" max="365" />
+                        <span asp-validation-for="BoardMeta.Comment.UpdateProtectionDays" class="text-danger"></span>
+                    </div>
+                    <small class="text-muted form-text">댓글이 수정되지 않도록 보호하는 기간을 일 단위로 설정합니다.</small>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_AllowDeleteProtection" class="col-md-3 col-form-label">댓글 보호 기능 (삭제 시)</label>
+            <div class="col-md-9">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.AllowDeleteProtection" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.AllowDeleteProtection" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">삭제 시 댓글을 보호하는 기능을 활성화합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_DeleteProtectionDays" class="col-md-3 col-form-label">댓글 삭제 금지 기간</label>
+            <div class="col-lg-9">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Comment.DeleteProtectionDays" class="form-control" min="0" max="365" />
+                        <span asp-validation-for="BoardMeta.Comment.DeleteProtectionDays" class="text-danger"></span>
+                    </div>
+                    <small class="text-muted form-text">댓글이 삭제되지 않도록 보호하는 기간을 일 단위로 설정합니다.</small>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Comment_EnableCommentUpdateLog" class="col-md-3 col-form-label">댓글 변경 기록</label>
+            <div class="col-md-9">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Comment.EnableCommentUpdateLog" class="form-check-input" />
+                    <label asp-for="BoardMeta.Comment.EnableCommentUpdateLog" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">댓글 변경 시 기록을 남깁니다.</small>
+            </div>
+        </div>
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@ViewBag.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>
+
+@section Scripts {
+    <script>
+        $("#fAdminWrite").validate({
+            rules: {
+                "BoardMeta.Comment.UpdateProtectionDays": {
+                    required: "#BoardMeta_Comment_AllowUpdateProtection:checked",
+                    min: function () {
+                        return $("#BoardMeta_Comment_AllowUpdateProtection").is(":checked") ? 1 : null;
+                    }
+                },
+                "BoardMeta.Comment.DeleteProtectionDays": {
+                    required: "#BoardMeta_Comment_AllowDeleteProtection:checked",
+                    min: function () {
+                        return $("#BoardMeta_Comment_AllowDeleteProtection").is(":checked") ? 1 : null;
+                    }
+                }
+            },
+            submitHandler: function(form) {
+                form.submit();
+            }
+        });
+    </script>
+}

+ 293 - 0
Admin/Pages/Forum/Board/Meta/Exp.cshtml

@@ -0,0 +1,293 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@{
+    ViewData["Title"] = "게시판 관리 - 경험치";
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/Exp" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <div class="row mb-3">
+            <label for="BoardMeta_Exp_EnableExp" class="col-auto col-md-3">경험치 기능</label>
+            <div class="col col-md-9">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Exp.EnableExp" class="form-check-input" />
+                    <label asp-for="BoardMeta.Exp.EnableExp" class="form-check-label">사용합니다.</label>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Exp_ShowExpGuide" class="col-auto col-md-3">경험치 안내</label>
+            <div class="col col-md-9">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Exp.ShowExpGuide" class="form-check-input" />
+                    <label asp-for="BoardMeta.Exp.ShowExpGuide" class="form-check-label">사용합니다.</label>
+                </div>
+            </div>
+        </div>
+
+        <hr/>
+
+        <div class="row align-items-top text-center">
+            <label class="col-md-3 col-form-label">-</label>
+            <div class="col">
+                지급
+            </div>
+            <div class="col">
+                차감
+            </div>
+            <div class="col">
+                기한
+            </div>
+        </div>
+
+        <!-- 게시글 작성 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">게시글 작성</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.PostWriteExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.PostWriteExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.PostWriteUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.PostWriteUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.PostWriteExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.PostWriteExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 댓글 작성 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">댓글 작성</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.CommentWriteExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.CommentWriteExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.CommentWriteUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.CommentWriteUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.CommentWriteExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.CommentWriteExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 파일 업로드 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">파일 업로드</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.FileUploadExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.FileUploadExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.FileUploadUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.FileUploadUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.FileUploadExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.FileUploadExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 파일 다운로드 (단, 지급량만) -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">파일 다운로드</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.FileDownloadExp" class="form-control" min="-10000" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.FileDownloadExp" class="text-danger"></span>
+            </div>
+            <div class="col text-center">
+                -
+            </div>
+            <div class="col text-center">
+                -
+            </div>
+        </div>
+
+        <!-- 게시글 읽기 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">게시글 읽기</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostReadExp" class="form-control" min="-10000" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostReadExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostReadUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostReadUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostReadExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostReadExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 게시글 좋아요 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">게시글 좋아요</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostLikeExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostLikeExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostLikeUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostLikeUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostLikeExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostLikeExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 게시글 싫어요 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">게시글 싫어요</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostDisLikeExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostDisLikeExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostDisLikeUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostDisLikeUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherPostDisLikeExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherPostDisLikeExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 댓글 좋아요 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">댓글 좋아요</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherCommentLikeExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherCommentLikeExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherCommentLikeUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherCommentLikeUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherCommentLikeExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherCommentLikeExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 댓글 싫어요 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">댓글 싫어요</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherCommentDisLikeExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherCommentDisLikeExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherCommentDisLikeUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherCommentDisLikeUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OtherCommentDisLikeExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OtherCommentDisLikeExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 내 게시글 읽힘 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">내 게시글 읽힘</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostReadExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostReadExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostReadUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostReadUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostReadExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostReadExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 내 게시글 좋아요 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">내 게시글 좋아요</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostLikeExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostLikeExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostLikeUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostLikeUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostLikeExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostLikeExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 내 게시글 싫어요 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">내 게시글 싫어요</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostDisLikeExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostDisLikeExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostDisLikeUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostDisLikeUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnPostDisLikeExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnPostDisLikeExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 내 댓글 좋아요 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">내 댓글 좋아요</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnCommentLikeExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnCommentLikeExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnCommentLikeUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnCommentLikeUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnCommentLikeExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnCommentLikeExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <!-- 내 댓글 싫어요 -->
+        <div class="row mb-3">
+            <label class="col-md-3 col-form-label">내 댓글 싫어요</label>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnCommentDisLikeExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnCommentDisLikeExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnCommentDisLikeUndoExp" class="form-control" min="0" max="10000" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnCommentDisLikeUndoExp" class="text-danger"></span>
+            </div>
+            <div class="col">
+                <input type="number" asp-for="BoardMeta.Exp.OwnCommentDisLikeExpWithinDays" class="form-control" min="0" max="365" required />
+                <span asp-validation-for="BoardMeta.Exp.OwnCommentDisLikeExpWithinDays" class="text-danger"></span>
+            </div>
+        </div>
+
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@ViewBag.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>

+ 97 - 0
Admin/Pages/Forum/Board/Meta/General.cshtml

@@ -0,0 +1,97 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@{
+    ViewData["Title"] = "게시판 관리 - 일반";
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/General" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <div class="row mb-3">
+            <label for="BoardMeta_General_AllowUpdateProtection" class="col-md-3 col-form-label">게시글 보호 기능 (수정 시)</label>
+            <div class="col-md-9 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.General.AllowUpdateProtection" class="form-check-input" />
+                    <label asp-for="BoardMeta.General.AllowUpdateProtection" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">수정 시 게시글을 보호하는 기능을 활성화합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_General_UpdateProtectionDays" class="col-12 col-md-3 col-form-label">게시글 수정 금지 기간</label>
+            <div class="col-lg-9">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.General.UpdateProtectionDays" class="form-control" min="0" max="365" required />
+                        <span asp-validation-for="BoardMeta.General.UpdateProtectionDays" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">게시글이 수정되지 않도록 보호하는 기간을 일 단위로 설정합니다.<br />(몇 일이 지난 게시글)</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_General_AllowDeleteProtection" class="col-md-3 col-form-label">게시글 보호 기능 (삭제 시)</label>
+            <div class="col-md-9 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.General.AllowDeleteProtection" class="form-check-input" />
+                    <label asp-for="BoardMeta.General.AllowDeleteProtection" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">삭제 시 게시글을 보호하는 기능을 활성화합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_General_DeleteProtectionDays" class="col-12 col-md-3 col-form-label">게시글 삭제 금지 기간</label>
+            <div class="col-lg-9">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.General.DeleteProtectionDays" class="form-control" min="0" max="365" required />
+                        <span asp-validation-for="BoardMeta.General.DeleteProtectionDays" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">게시글이 삭제되지 않도록 보호하는 기간을 일 단위로 설정합니다.<br />(몇 일이 지난 게시글)</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_General_EnableFileDownLog" class="col-md-3 col-form-label">다운로드 기록</label>
+            <div class="col-md-9 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.General.EnableFileDownLog" class="form-check-input" />
+                    <label asp-for="BoardMeta.General.EnableFileDownLog" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">파일 다운로드 시 기록을 남깁니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_General_EnableLinkClickLog" class="col-md-3 col-form-label">링크 클릭 기록</label>
+            <div class="col-md-9 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.General.EnableLinkClickLog" class="form-check-input" />
+                    <label asp-for="BoardMeta.General.EnableLinkClickLog" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">링크 클릭 시 기록을 남깁니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_General_EnablePostUpdateLog" class="col-md-3 col-form-label">게시글 변경 기록</label>
+            <div class="col-md-9 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.General.EnablePostUpdateLog" class="form-check-input" />
+                    <label asp-for="BoardMeta.General.EnablePostUpdateLog" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 변경 시 기록을 남깁니다.</small>
+            </div>
+        </div>
+        <hr />
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@ViewBag.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>

+ 153 - 0
Admin/Pages/Forum/Board/Meta/List.cshtml

@@ -0,0 +1,153 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@using Library.Constants
+@{
+    ViewData["Title"] = "게시판 관리 - 목록";
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/List" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <div class="row mb-3">
+            <label for="BoardMeta_List_ShowHeader" class="col-md-2 col-form-label">상단 내용</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.List.ShowHeader" class="form-check-input" />
+                    <label for="BoardMeta_List_ShowHeader" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시판 상단에 내용을 출력합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_HeaderContent" class="col-md-2 col-form-label"></label>
+            <div class="col-md-10">
+                <textarea asp-for="BoardMeta.List.HeaderContent" class="ck-editor"></textarea>
+                <span asp-validation-for="BoardMeta.List.HeaderContent" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_ShowFooter" class="col-sm-2 col-form-label">하단 내용</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.List.ShowFooter" class="form-check-input" />
+                    <label for="BoardMeta_List_ShowFooter" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시판 하단에 내용을 출력합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="BoardMeta_List_FooterContent" class="col-md-2 col-form-label"></label>
+            <div class="col-md-10">
+                <textarea asp-for="BoardMeta.List.FooterContent" class="ck-editor"></textarea>
+                <span asp-validation-for="BoardMeta.List.FooterContent" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_Layout" class="col-md-2 col-form-label">게시판 종류</label>
+            <div class="col col-md-auto">
+                <select asp-for="BoardMeta.List.Layout" class="form-select" required asp-items="@Html.GetEnumSelectList<BoardConst.Layout>()"></select>
+                <span asp-validation-for="BoardMeta.List.Layout" class="text-danger"></span>
+                <small class="text-muted form-text">지원하는 게시판 종류를 지정합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_Sort" class="col-md-2 col-form-label">기본 정렬</label>
+            <div class="col col-md-auto">
+                <select asp-for="BoardMeta.List.Sort" class="form-select" required asp-items="@Html.GetEnumSelectList<BoardConst.Sort>()"></select>
+                <span asp-validation-for="BoardMeta.List.Sort" class="text-danger"></span>
+                <small class="text-muted form-text">게시판 기본 정렬을 지정합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_PerPage" class="col-md-2 col-form-label">목록 표시 개수</label>
+            <div class="col-lg-10">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.List.PerPage" class="form-control" min="0" max="100" required />
+                        <span asp-validation-for="BoardMeta.List.PerPage" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">한 페이지에 보이는 게시물 수, 0이면 페이징 기능 사용하지 않음, (최대 100개)</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_AlwaysShowWriteButton" class="col-sm-2 col-form-label">글쓰기 버튼 보이기</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.List.AlwaysShowWriteButton" class="form-check-input" />
+                    <label for="BoardMeta_List_AlwaysShowWriteButton" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">권한이 없는 사용자라도 글쓰기 버튼은 항상 보입니다</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_ShowFooterListView" class="col-sm-2 col-form-label">하단 목록 보이기</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.List.ShowFooterListView" class="form-check-input" />
+                    <label for="BoardMeta_List_ShowFooterListView" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">글읽기 화면 하단에도 목록을 표시합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_IsNewIcon" class="col-sm-2 col-form-label">NEW 아이콘 사용</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.List.IsNewIcon" class="form-check-input" />
+                    <label for="BoardMeta_List_IsNewIcon" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">글 작성 후 24시간 동안 NEW 아이콘이 노출됩니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_IsHotIcon" class="col-sm-2 col-form-label">HOT 아이콘 사용</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.List.IsHotIcon" class="form-check-input" />
+                    <label for="BoardMeta_List_IsHotIcon" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">글 조회 수가 1000건 이상이면 7일간 아이콘이 노출됩니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_ExceptNotice" class="col-sm-2 col-form-label">공지사항 제외</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.List.ExceptNotice" class="form-check-input" />
+                    <label for="BoardMeta_List_ExceptNotice" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">
+                    목록 상단에 늘 나타나는 공지사항을 일반 목록에서 나타나지 않도록 합니다.
+                    공지사항 게시판은 해당되지 않습니다.
+                </small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_List_ExceptSpeaker" class="col-sm-2 col-form-label">전체공지 제외</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.List.ExceptSpeaker" class="form-check-input" />
+                    <label for="BoardMeta_List_ExceptSpeaker" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">
+                    설정하시면 다른 게시판에서 입력한 전체 공지가 이 게시판에서는 보이지 않습니다.
+                    공지사항 게시판은 해당되지 않습니다.
+                </small>
+            </div>
+        </div>
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@ViewBag.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>

+ 109 - 0
Admin/Pages/Forum/Board/Meta/Notify.cshtml

@@ -0,0 +1,109 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@using Library.Constants
+@{
+    ViewData["Title"] = "게시판 관리 - 알림";
+
+    var notifyList = Model.NotifyList;
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/Notify" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <h5>이메일 알림</h5>
+        <p class="text-muted form-text">이메일 알림이 양식에 맞추어 아래 대상자에게 발송됩니다.</p>
+
+        <div class="row mb-3">
+            <label class="col-md-2 col-form-label">게시글 작성 시</label>
+            <div class="col-md-10 align-self-center">
+                @if (notifyList is not null) {
+                    @foreach (var item in notifyList)
+                    {
+                        var notifyValue = (BoardConst.Notify)Enum.Parse(typeof(BoardConst.Notify), item.Value);
+                        <div class="form-check form-check-inline">
+                            <input type="checkbox" name="PostWriteNotify[]" value="@item.Value" id="postWriteNotify_@item.Value" class="form-check-input" checked="@Model.BoardMeta.Notify.PostWriteNotifyEnum.HasFlag(notifyValue)" />
+                            <label for="postWriteNotify_@item.Value" class="form-check-label">@item.Text</label>
+                        </div>
+                    }
+                }
+            </div>
+        </div>
+         <div class="row mb-3">
+            <label class="col-md-2 col-form-label">댓글 작성 시</label>
+            <div class="col-md-10 align-self-center">
+                @if (notifyList is not null) {
+                    @foreach (var item in notifyList)
+                    {
+                        var notifyValue = (BoardConst.Notify)Enum.Parse(typeof(BoardConst.Notify), item.Value);
+                        <div class="form-check form-check-inline">
+                            <input type="checkbox" name="CommentWriteNotify[]" value="@item.Value" id="commentWriteNotify_@item.Value" class="form-check-input" checked="@Model.BoardMeta.Notify.CommentWriteNotifyEnum.HasFlag(notifyValue)" />
+                            <label for="commentWriteNotify_@item.Value" class="form-check-label">@item.Text</label>
+                        </div>
+                    }
+                }
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="Notify_ReplyWriteNotify" class="col-md-2 col-form-label">답글 작성 시</label>
+            <div class="col-md-10 align-self-center">
+                @if (notifyList is not null) {
+                    @foreach (var item in notifyList)
+                    {
+                        var notifyValue = (BoardConst.Notify)Enum.Parse(typeof(BoardConst.Notify), item.Value);
+                        <div class="form-check form-check-inline">
+                            <input type="checkbox" name="ReplyWriteNotify[]" value="@item.Value" id="replyWriteNotify_@item.Value" class="form-check-input" checked="@Model.BoardMeta.Notify.ReplyWriteNotifyEnum.HasFlag(notifyValue)" />
+                            <label for="replyWriteNotify_@item.Value" class="form-check-label">@item.Text</label>
+                        </div>
+                    }
+                }
+            </div>
+        </div>
+
+        <input type="hidden" name="BoardMeta.Notify.PostWriteNotify" id="PostWriteNotifyValue" />
+        <input type="hidden" name="BoardMeta.Notify.CommentWriteNotify" id="CommentWriteNotifyValue" />
+        <input type="hidden" name="BoardMeta.Notify.ReplyWriteNotify" id="ReplyWriteNotifyValue" />
+
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@ViewBag.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+    </form>
+</div>
+
+@section Scripts {
+    <script>
+        document.addEventListener("DOMContentLoaded", function () {
+            function calculateFlagValue(groupName) {
+                let checkboxes = document.querySelectorAll(`input[name="${groupName}[]"]:checked`);
+                let flagValue = 0;
+
+                checkboxes.forEach(checkbox => {
+                    flagValue |= parseInt(checkbox.value, 10);
+                });
+
+                document.getElementById(`${groupName}Value`).value = flagValue;
+            }
+
+            function updateAllFlags() {
+                calculateFlagValue("PostWriteNotify");
+                calculateFlagValue("CommentWriteNotify");
+                calculateFlagValue("ReplyWriteNotify");
+            }
+
+            document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
+                checkbox.addEventListener("change", updateAllFlags);
+            });
+
+            document.getElementById("fAdminWrite").addEventListener("submit", (e) => {
+                updateAllFlags();
+            });
+        });
+    </script>
+}

+ 58 - 0
Admin/Pages/Forum/Board/Meta/NotifyTemplate.cshtml

@@ -0,0 +1,58 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@using Library.Constants
+@{
+    ViewData["Title"] = "게시판 관리 - 양식";
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml" /> 
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/NotifyTemplate" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <div class="text-muted form-text mb-3">
+            이메일 전송에 사용되는 알림 양식을 설정합니다. 양식 별로 치환 가능한 변수가 다릅니다. 변수를 확인해주세요.
+        </div>
+
+        <div class="row mb-3">
+            <label for="BoardMeta_NotifyTemplate_PostWriteEmailNotifySubject" class="col-md-2 col-form-label">게시글 작성 시</label>
+            <div class="col-md-10">
+                <input asp-for="BoardMeta.NotifyTemplate.PostWriteEmailNotifySubject" class="form-control mb-2" maxlength="255" placeholder="제목" />
+                <span asp-validation-for="BoardMeta.NotifyTemplate.PostWriteEmailNotifySubject" class="text-danger"></span>
+                <textarea asp-for="BoardMeta.NotifyTemplate.PostWriteEmailNotifyContent" class="ck-editor"></textarea>
+                <span asp-validation-for="BoardMeta.NotifyTemplate.PostWriteEmailNotifyContent" class="text-danger"></span>
+            </div>
+        </div>
+        <hr/>
+        <div class="row mb-3">
+            <label for="BoardMeta_NotifyTemplate_CommentWriteEmailNotifySubject" class="col-md-2 col-form-label">댓글 작성 시</label>
+            <div class="col-md-10">
+                <input asp-for="BoardMeta.NotifyTemplate.CommentWriteEmailNotifySubject" class="form-control mb-2" maxlength="255" placeholder="제목" />
+                <span asp-validation-for="BoardMeta.NotifyTemplate.CommentWriteEmailNotifySubject" class="text-danger"></span>
+                <textarea asp-for="BoardMeta.NotifyTemplate.CommentWriteEmailNotifyContent" class="ck-editor"></textarea>
+                <span asp-validation-for="BoardMeta.NotifyTemplate.CommentWriteEmailNotifyContent" class="text-danger"></span>
+            </div>
+        </div>
+        <hr/>
+        <div class="row mb-3">
+            <label for="BoardMeta_NotifyTemplate_ReplyWriteEmailNotifySubject" class="col-md-2 col-form-label">답글 작성 시</label>
+            <div class="col-md-10">
+                <input asp-for="BoardMeta.NotifyTemplate.ReplyWriteEmailNotifySubject" class="form-control mb-2" maxlength="255" placeholder="제목" />
+                <span asp-validation-for="BoardMeta.NotifyTemplate.ReplyWriteEmailNotifySubject" class="text-danger"></span>
+                <textarea asp-for="BoardMeta.NotifyTemplate.ReplyWriteEmailNotifyContent" class="ck-editor"></textarea>
+                <span asp-validation-for="BoardMeta.NotifyTemplate.ReplyWriteEmailNotifyContent" class="text-danger"></span>
+            </div>
+        </div>
+        <hr />
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@ViewBag.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>

+ 82 - 0
Admin/Pages/Forum/Board/Meta/Permission.cshtml

@@ -0,0 +1,82 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@using Library.Constants
+@{
+    ViewData["Title"] = "게시판 관리 - 권한";
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/Permission" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <p class="text-muted form-text">권한은 최고관리자가 가장 높습니다. 권한이 높은 순으로 정책이 적용됩니다.</p>
+
+        <div class="row mb-3">
+            <label for="BoardMeta_Permission_BoardAccess" class="col-md-2 col-form-label">게시판 접근</label>
+            <div class="col-12 col-lg-auto">
+                <select asp-for="BoardMeta.Permission.BoardAccess" class="form-select" asp-items="@Model.Permissions"></select>
+                <span asp-validation-for="BoardMeta.Permission.BoardAccess" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Permission_PostView" class="col-md-2 col-form-label">글 열람</label>
+            <div class="col-12 col-lg-auto">
+                <select asp-for="BoardMeta.Permission.PostView" class="form-select" asp-items="@Model.Permissions"></select>
+                <span asp-validation-for="BoardMeta.Permission.PostView" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Permission_PostWrite" class="col-md-2 col-form-label">글 작성</label>
+            <div class="col-12 col-lg-auto">
+                <select asp-for="BoardMeta.Permission.PostWrite" class="form-select" asp-items="@Model.Permissions"></select>
+                <span asp-validation-for="BoardMeta.Permission.PostWrite" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Permission_CommentView" class="col-md-2 col-form-label">댓글 목록</label>
+            <div class="col-12 col-lg-auto">
+                <select asp-for="BoardMeta.Permission.CommentView" class="form-select" asp-items="@Model.Permissions"></select>
+                <span asp-validation-for="BoardMeta.Permission.CommentView" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Permission_CommentWrite" class="col-md-2 col-form-label">댓글 작성</label>
+            <div class="col-12 col-lg-auto">
+                <select asp-for="BoardMeta.Permission.CommentWrite" class="form-select" asp-items="@Model.Permissions"></select>
+                <span asp-validation-for="BoardMeta.Permission.CommentWrite" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Permission_ReplyWrite" class="col-md-2 col-form-label">답글 작성</label>
+            <div class="col-12 col-lg-auto">
+                <select asp-for="BoardMeta.Permission.ReplyWrite" class="form-select" asp-items="@Model.Permissions"></select>
+                <span asp-validation-for="BoardMeta.Permission.ReplyWrite" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Permission_FileUpload" class="col-md-2 col-form-label">파일 업로드</label>
+            <div class="col-12 col-lg-auto">
+                <select asp-for="BoardMeta.Permission.FileUpload" class="form-select" asp-items="@Model.Permissions"></select>
+                <span asp-validation-for="BoardMeta.Permission.FileUpload" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Permission_FileDownload" class="col-md-2 col-form-label">파일 다운로드</label>
+            <div class="col-12 col-lg-auto">
+                <select asp-for="BoardMeta.Permission.FileDownload" class="form-select" asp-items="@Model.Permissions"></select>
+                <span asp-validation-for="BoardMeta.Permission.FileDownload" class="text-danger"></span>
+            </div>
+        </div>
+        <hr />
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@@Model.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>

+ 175 - 0
Admin/Pages/Forum/Board/Meta/View.cshtml

@@ -0,0 +1,175 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@{
+    ViewData["Title"] = "게시판 관리 - 열람";
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/View" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowBookmark" class="col-md-2 col-form-label">즐겨찾기 기능</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowBookmark" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowBookmark" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글을 즐겨찾기 시 글 보관함에 등록할 수있습니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowLike" class="col-md-2 col-form-label">좋아요 기능</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowLike" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowLike" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 반응 기능(좋아요)를 사용할 수 있도록 합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowDislike" class="col-md-2 col-form-label">싫어요 기능</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowDislike" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowDislike" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 반응 기능(싫어요)를 사용할 수 있도록 합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowPrint" class="col-md-2 col-form-label">본문 인쇄 기능</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowPrint" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowPrint" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 내용을 인쇄할 수 있도록 합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowSnsShare" class="col-md-2 col-form-label">SNS 보내기 기능</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowSnsShare" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowSnsShare" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">Facebook, Twitter, Reddit, Band 공유 기능을 활성화 합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowPrevNextBotton" class="col-md-2 col-form-label">이전글, 다음글 버튼</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowPrevNextBotton" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowPrevNextBotton" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 이전, 다음 글 버튼을 활성화 합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowBlame" class="col-md-2 col-form-label">신고 기능</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowBlame" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowBlame" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글을 신고할 수 있도록 합니다. 숨김 횟수가 0이면 작동하지 않습니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_BlameHideCount" class="col-12 col-lg-2">신고 시 숨김 횟수</label>
+            <div class="col-lg-10">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.View.BlameHideCount" class="form-control" min="0" max="100" required />
+                        <span asp-validation-for="BoardMeta.View.BlameHideCount" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">지정한 횟수 이상 신고가 발생하면 게시물을 숨김 처리합니다. 숨김된 게시물은 관리자와 본인만 열람이 가능합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowContentLinkTargetBlank" class="col-md-2 col-form-label">URL 새창 열림</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowContentLinkTargetBlank" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowContentLinkTargetBlank" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">본문 안의 URL 주소는 무조건 새창으로 열립니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowPostUrlCopy" class="col-md-2 col-form-label">주소 복사 버튼</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowPostUrlCopy" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowPostUrlCopy" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">글 주소를 복사 할 수 있는 버튼이 나타납니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_AllowPostUrlQrCode" class="col-md-2 col-form-label">글 주소 QR 코드</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.AllowPostUrlQrCode" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.AllowPostUrlQrCode" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">글 주소를 복사하는 버튼 우측에 현재 주소를 담은 QR 코드를 볼 수 있는 버튼이 생겨납니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_ShowMemberPhoto" class="col-md-2 col-form-label">회원 사진 공개</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.ShowMemberPhoto" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.ShowMemberPhoto" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 상단에 회원 사진이 노출됩니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_ShowMemberIcon" class="col-md-2 col-form-label">회원 아이콘 공개</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.ShowMemberIcon" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.ShowMemberIcon" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 상단에 회원 아이콘이 노출됩니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_ShowMemberRegDate" class="col-md-2 col-form-label">회원 가입일 공개</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.ShowMemberRegDate" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.ShowMemberRegDate" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 작성자의 가입일을 보여줍니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_View_ShowMemberSummary" class="col-md-2 col-form-label">오늘의 한마디 공개</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.View.ShowMemberSummary" class="form-check-input" />
+                    <label asp-for="BoardMeta.View.ShowMemberSummary" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시글 작성자의 오늘의 한마디를 보여줍니다.</small>
+            </div>
+        </div>
+        <hr />
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@ViewBag.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>

+ 267 - 0
Admin/Pages/Forum/Board/Meta/Write.cshtml

@@ -0,0 +1,267 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@using Library.Constants
+@{
+    ViewData["Title"] = "게시판 관리 - 작성";
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/Meta/_Header.cshtml"/>
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+    <partial name="~/Views/Forum/Board/Meta/_Navbar.cshtml" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Forum/Board/Meta/Update/Write" enctype="multipart/form-data">
+        <input type="hidden" name="BoardMeta.Board.Code" value="@Model.Board.Code" />
+        <input type="hidden" asp-for="BoardMeta.ID" />
+        <input type="hidden" asp-for="BoardMeta.BoardID" />
+
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_ShowHeader" class="col-sm-2 col-form-label">상단 내용</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.ShowHeader" class="form-check-input" />
+                    <label for="BoardMeta_Write_ShowHeader" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시판 상단에 내용을 출력합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label class="col-md-2 col-form-label"></label>
+            <div class="col-md-10">
+                <textarea asp-for="BoardMeta.Write.HeaderContent" class="form-control ck-editor"></textarea>
+                <span asp-validation-for="BoardMeta.Write.HeaderContent" class="text-danger"></span>
+                <small class="text-muted form-text">작성란 상단에 표시될 내용을 입력합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_ShowFooter" class="col-sm-2 col-form-label">하단 내용</label>
+            <div class="col-md-10">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.ShowFooter" class="form-check-input" />
+                    <label for="BoardMeta_Write_ShowFooter" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">게시판 하단에 내용을 출력합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label class="col-md-2 col-form-label"></label>
+            <div class="col-md-10">
+                <textarea asp-for="BoardMeta.Write.FooterContent" class="form-control ck-editor"></textarea>
+                <span asp-validation-for="BoardMeta.Write.FooterContent" class="text-danger"></span>
+                <small class="text-muted form-text">작성란 하단에 표시될 내용을 입력합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_DefaultSubject" class="col-md-2 col-form-label">기본 제목</label>
+            <div class="col-md-10">
+                <input type="text" asp-for="BoardMeta.Write.DefaultSubject" class="form-control" maxlength="@PostConst.MaxAllowedSubjectLength" />
+                <span asp-validation-for="BoardMeta.Write.DefaultSubject" class="text-danger"></span>
+                <small class="text-muted form-text">글 작성 시 기본으로 표시될 제목입니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_DefaultContent" class="col-md-2 col-form-label">기본 내용</label>
+            <div class="col-md-10">
+                <textarea asp-for="BoardMeta.Write.DefaultContent" class="form-control @(Model.BoardMeta.Write.AllowEditor ? "ck-editor" : "")" maxlength="@PostConst.MaxAllowedContentLength"></textarea>
+                <span asp-validation-for="BoardMeta.Write.DefaultContent" class="text-danger"></span>
+                <small class="text-muted form-text">글 작성 시 기본으로 표시될 내용입니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_AllowEditor" class="col-md-2 col-form-label">웹 에디터 사용</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.AllowEditor" class="form-check-input" />
+                    <label asp-for="BoardMeta.Write.AllowEditor" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">본문을 웹 기반 에디터로 수정할 수 있도록합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_AllowPrefix" class="col-md-2 col-form-label">말머리 사용</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.AllowPrefix" class="form-check-input" />
+                    <label asp-for="BoardMeta.Write.AllowPrefix" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">말머리를 사용할 수 있도록 합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_RequiredPrefix" class="col-md-2 col-form-label">말머리 필수 선택</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.RequiredPrefix" class="form-check-input" />
+                    <label asp-for="BoardMeta.Write.RequiredPrefix" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">말머리를 필수 선택하도록 합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_AllowSecret" class="col-md-2 col-form-label">비밀글 사용</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.AllowSecret" class="form-check-input" />
+                    <label asp-for="BoardMeta.Write.AllowSecret" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">비밀글 작성 기능을 활성화합니다. 비밀글은 작성자 본인과 게시판 관리자 이상만 열람 가능합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_AllowTag" class="col-md-2 col-form-label">태그 사용</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.AllowTag" class="form-check-input" />
+                    <label asp-for="BoardMeta.Write.AllowTag" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">태그 기능을 활성화합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_TagLimit" class="col-12 col-lg-2 col-form-label">태그 개수 제한</label>
+            <div class="col-lg-10">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Write.TagLimit" class="form-control" min="1" max="{@PostConst.MaxAllowedTags}" required />
+                        <span asp-validation-for="BoardMeta.Write.TagLimit" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">태그의 최대 개수를 설정합니다. 최대 {@PostConst.MaxAllowedTags}개</small>
+            </div>
+        </div>
+
+        <br />
+        <h3>웹 에디터 기능 설정</h3>
+        <hr />
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_AllowImage" class="col-md-2 col-form-label">이미지 사용</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.AllowImage" class="form-check-input" />
+                    <label asp-for="BoardMeta.Write.AllowImage" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">이미지 업로드 기능을 활성화합니다. </small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_ImageUploadLimit" class="col-12 col-lg-2 col-form-label">이미지 개수 제한</label>
+            <div class="col-lg-10">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Write.ImageUploadLimit" class="form-control" min="1" max="{@PostConst.MaxAllowedImages}" required />
+                        <span asp-validation-for="BoardMeta.Write.ImageUploadLimit" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">첨부 가능한 이미지의 최대 개수를 설정합니다. 최대 @(PostConst.MaxAllowedImages)개</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_ImageUploadMaxSize" class="col-12 col-lg-2 col-form-label">이미지 용량 제한</label>
+            <div class="col-lg-10">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <div class="input-group">
+                            <input type="number" asp-for="BoardMeta.Write.ImageUploadMaxSize" class="form-control" min="0" required />
+                            <span class="input-group-text">KB</span>
+                        </div>
+                        <span asp-validation-for="BoardMeta.Write.ImageUploadMaxSize" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">이미지 하나당 최대 용량을 설정합니다. 최대 @(PostConst.MaxAllowedImageSize)KB</small>
+            </div>
+        </div>
+        <hr />
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_AllowMedia" class="col-md-2 col-form-label">미디어 사용</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.AllowMedia" class="form-check-input" />
+                    <label asp-for="BoardMeta.Write.AllowMedia" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">미디어 추가 기능을 활성화합니다. (웹 에디터를 사용해야 가능)</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_MediaUploadLimit" class="col-12 col-lg-2 col-form-label">동영상 개수 제한</label>
+            <div class="col-lg-10">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Write.MediaUploadLimit" class="form-control" min="1" max="@PostConst.MaxAllowedMedias" required />
+                        <span asp-validation-for="BoardMeta.Write.MediaUploadLimit" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">첨부 가능한 동영상의 최대 개수를 설정합니다. 최대 @(PostConst.MaxAllowedMedias)개</small>
+            </div>
+        </div>
+        <hr />
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_AllowFile" class="col-md-2 col-form-label">파일 사용</label>
+            <div class="col-md-10 align-self-center">
+                <div class="form-check">
+                    <input type="checkbox" asp-for="BoardMeta.Write.AllowFile" class="form-check-input" />
+                    <label asp-for="BoardMeta.Write.AllowFile" class="form-check-label">사용합니다.</label>
+                </div>
+                <small class="text-muted form-text">파일 추가 기능을 활성화합니다.</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_FileUploadLimit" class="col-12 col-lg-2 col-form-label">파일 개수 제한</label>
+            <div class="col-lg-10">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <input type="number" asp-for="BoardMeta.Write.FileUploadLimit" class="form-control" min="1" max="@PostConst.MaxAllowedFiles" required />
+                        <span asp-validation-for="BoardMeta.Write.FileUploadLimit" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">첨부 가능한 파일의 최대 개수를 설정합니다. 최대 @(PostConst.MaxAllowedFiles)개</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_FileUploadMaxSize" class="col-12 col-lg-2 col-form-label">파일 용량 제한</label>
+            <div class="col-lg-10">
+                <div class="row">
+                    <div class="col-12 col-lg-auto">
+                        <div class="input-group">
+                            <input type="number" asp-for="BoardMeta.Write.FileUploadMaxSize" class="form-control" min="1" max="@PostConst.MaxAllowedFileSize" required />
+                            <span class="input-group-text">KB</span>
+                        </div>
+                        <span asp-validation-for="BoardMeta.Write.FileUploadMaxSize" class="text-danger"></span>
+                    </div>
+                </div>
+                <small class="text-muted form-text">파일 하나당 최대 용량을 설정합니다. 최대 @(PostConst.MaxAllowedFileSize)KB</small>
+            </div>
+        </div>
+        <div class="row mb-3">
+            <label for="BoardMeta_Write_FileUploadExtension" class="col-md-2 col-form-label">파일 허용 확장자</label>
+            <div class="col-md-10">
+                <input type="text" asp-for="BoardMeta.Write.FileUploadExtension" class="form-control" />
+                <span asp-validation-for="BoardMeta.Write.FileUploadExtension" class="text-danger"></span>
+                <small class="form-text text-muted">
+                    허용할 파일 확장자를 입력합니다. (예: jpg,png,gif)<br/>
+                    HTML5 File 속성 사용, `|` 로 구분하여 입력, 입력하지 않으면 확장자 제한없이 첨부 가능
+                    <br/><a href="//www.w3schools.com/tags/att_input_accept.asp" target="_blank">[FILE 속성 참고]</a><br/>
+                    <pre>&lt;input accept="<em>file_extension</em>|audio/*|video/*|image/*|<i>media_type</i>"&gt;</pre>
+                </small>
+            </div>
+        </div>
+        <hr />
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Forum/Board/List?@ViewBag.QueryString" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>
+
+@section Scripts {
+    <script>
+        $(document).on("change", "#BoardMeta_Write_AllowEditor", function(e) {
+            const textareaID = "BoardMeta_Write_DefaultContent";
+            if (e.target.checked) { // CKEditor 표시
+                initEditor(textareaID);
+            } else { // Textarea로 변경
+                destroyEditor(textareaID);
+            }
+        });
+    </script>
+}

+ 44 - 0
Admin/Pages/Forum/Board/Meta/_Header.cshtml

@@ -0,0 +1,44 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@{
+    var boardID = Model.Board.ID;
+    var boardList = Model.BoardList;
+}
+
+<div class="row">
+    <div class="col">
+        <h3>@ViewData["Title"]</h3>
+    </div>
+    <div class="col-auto">
+        <select id="boardID" class="form-select" required>
+            <option value="">게시판 선택</option>
+            @if (boardList != null)
+            {
+                @foreach (var row in boardList)
+                {
+                    <option value="@row.ID" selected="@(row.ID == boardID)">@row.Name</option>
+                }
+            }
+        </select>
+    </div>
+</div>
+<hr />
+
+<script type="module">
+    $(document).on("change", "#boardID", function () {
+        var boardID = $(this).val();
+        if (boardID) {
+            const url = new URL(window.location.href);
+
+            const pathSegments = url.pathname.split('/');
+
+            if (pathSegments.length >= 6) {
+                pathSegments[5] = boardID;
+            }
+
+            const newPath = pathSegments.join('/');
+            const newURL = `${newPath}${url.search}`;
+
+            window.location.href = newURL;
+        }
+    });
+</script>

+ 43 - 0
Admin/Pages/Forum/Board/Meta/_Navbar.cshtml

@@ -0,0 +1,43 @@
+@model Admin.ViewModels.Forum.Board.Meta.IndexViewModel
+@{
+    string? currentAction = ViewContext.RouteData.Values["action"] as string;
+    string? currentController = ViewContext.RouteData.Values["controller"] as string;
+
+    var sector = Model.Sector;
+    var boardID = Model.Board.ID;
+    var queryString = Model.QueryString;
+
+    var tabs = new List<(string controller, string Action, string Name)>
+    {
+        ("Meta", "List", "목록"),
+        ("Meta", "View", "열람"),
+        ("Meta", "Write", "작성"),
+        ("Prefix", "", "말머리"),
+        ("Meta", "Comment", "댓글"),
+        ("Meta", "General", "일반"),
+        ("Meta", "Notify", "알림"),
+        ("Meta", "NotifyTemplate", "양식"),
+        ("Meta", "Permission", "권한"),
+        ("Meta", "Exp", "경험치"),
+        ("Manager", "", "관리자")
+    };
+}
+
+<ul class="nav nav-tabs">
+    <li class="nav-item">
+        <a class="nav-link @(currentController == "List" && currentAction == "Edit" ? "active" : "")" href="@Url.Content($"~/Forum/Board/List/{boardID}/Edit?{queryString}")">기본</a>
+    </li>
+
+    @foreach (var (controller, action, name) in tabs)
+    {
+        var isActive = (currentController == controller && sector == action) ? "active" : "";
+        var href = string.IsNullOrEmpty(action)
+                    ? Url.Content($"~/Forum/Board/{controller}/{boardID}?{queryString}")
+                    : Url.Content($"~/Forum/Board/{controller}/{action}/{boardID}?{queryString}");
+
+        <li class="nav-item">
+            <a class="nav-link @isActive" href="@href">@name</a>
+        </li>
+    }
+</ul>
+<br/>

+ 148 - 0
Admin/Pages/Forum/Board/Prefix.cshtml

@@ -0,0 +1,148 @@
+@model Admin.ViewModels.Forum.Board.Prefix.IndexViewModel
+@using Library.Extensions
+@{
+    ViewData["Title"] = "게시판 관리 - 말머리";
+    ViewData["BoardID"] = Model.BoardID;
+    ViewData["BoardList"] = Model.BoardList;
+    ViewData["QueryString"] = Model.QueryString;
+}
+
+<div class="container">
+    <partial name="~/Views/Forum/Board/_Header.cshtml" />
+    <partial name="_StatusMessage" />
+    <partial name="~/Views/Forum/Board/_Navbar.cshtml" />
+
+    <form id="fAdminWrite" asp-action="Create" method="post" accept-charset="utf-8" autocomplete="off">
+        <input type="hidden" name="boardID" value="@Model.BoardID" />
+
+        <div class="row g-2">
+            <label class="col-lg-1 col-form-label">말머리</label>
+            <div class="col-7 col-sm-auto">
+                <div class="input-group">
+                    <div class="input-group-text">
+                        <input type="color" name="color" class="h-100" maxlength="10" required />
+                    </div>
+                    <input type="text" name="name" class="form-control" maxlength="20" required placeholder="이름" />
+                </div>
+            </div>
+            <div class="col col-lg-auto">
+                <input type="number" name="order" class="form-control" min="-999" max="999" required placeholder="순서" />
+            </div>
+            <div class="col-12 col-sm-auto text-center">
+                <button type="submit" class="btn btn-primary w-100">등록</button>
+            </div>
+        </div>
+    </form>
+    
+    <hr/>
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @Model.Total.ToString("N0")
+        </div>
+        <div class="col text-end">
+            <button type="button" id="btnListDelete" class="btn btn-sm btn-danger" form="fAdminList" data-action="/Forum/Board/Prefix/@Model.BoardID/Delete" disabled>삭제</button>
+            <button type="submit" id="btnListSave" class="btn btn-sm btn-success" form="fAdminList" @(Model.Total <= 0 ? "disabled" : "")> 저장</button>
+        </div>
+    </div>
+
+    <div class="table-responsive">
+        <form id="fAdminList" asp-action="Update" method="post" accept-charset="utf-8" autocomplete="off">
+            <input type="hidden" name="boardID" value="@Model.BoardID" />
+
+            <table class="table table-striped table-bordered table-hover mt-3">
+                <caption>
+                    게시글 제목에 특정 단어를 넣는 기능입니다. 최대 10개를 추가할 수 있습니다.
+                </caption>
+                <colgroup>
+                    <col width="5%"/>
+                    <col width="*"/>
+                    <col width="*"/>
+                    <col width="*"/>
+                    <col width="*"/>
+                    <col width="12%" />
+                    <col width="12%"/>
+                </colgroup>
+                <thead>
+                    <tr>
+                        <th>
+                            <div class="form-check form-check-inline">
+                                <input type="checkbox" id="checkedAll" class="form-check-input" value="1" />
+                                <label for="checkedAll" class="form-check-label">ID</label>
+                            </div>
+                        </th>
+                        <th>말머리</th>
+                        <th>순서</th>
+                        <th>사용 횟수</th>
+                        <th>사용 여부</th>
+                        <th>등록일시</th>
+                        <th>수정일시</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    @if (Model.Data == null || !Model.Data.Any())
+                    {
+                        <tr>
+                            <td colspan="7">No Data.</td>
+                        </tr>
+                    }
+                    else
+                    {
+                        @foreach (var row in Model.Data)
+                        {
+                            var index = Model.Data.IndexOf(row);
+
+                            <tr>
+                                <td>
+                                    <div class="form-check form-check-inline">
+                                        <input type="checkbox" name="CheckList[]" id="CheckList_@index" class="form-check-input list-check-box" value="@row.ID" />
+                                        <label for="CheckList_@index" class="form-check-label">@row.ID</label>
+                                    </div>
+
+                                    <input type="hidden" name="Items[@index].ID" class="form-control-plaintext text-center" value="@row.ID" />
+                                </td>
+                                <td>
+                                    <div class="input-group">
+                                        <div class="input-group-text">
+                                            <input type="color" name="Items[@index].Color" class="h-100" maxlength="10" value="@row.Color" required />
+                                        </div>
+                                        <input type="text" name="Items[@index].Name" class="form-control" maxlength="20" value="@row.Name" required />
+                                    </div>
+                                </td>
+                                <td>
+                                    <input type="number" name="Items[@index].Order" class="form-control" min="-999" max="999" value="@row.Order" required />
+                                </td>
+                                <td>@row.Posts</td>
+                                <td>
+                                    <div class="form-check form-check-inline">
+                                        <input type="checkbox" name="Items[@index].IsActive" id="Items_@(index)_IsActive" class="form-check-input" checked="@row.IsActive" value="true" />
+                                        <label for="Items_@(index)_IsActive" class="form-check-label">사용</label>
+                                    </div>
+                                </td>
+                                <td>@row.CreatedAt.GetDateAt()</td>
+                                <td>@(row.UpdatedAt.GetDateAt() ?? "-")</td>
+                            </tr>
+                        }
+                    }
+                </tbody>
+            </table>
+        </form>
+    </div>
+</div>
+
+@section Scripts {
+    <script>
+        // 저장
+        $(document).on("click", "#btnListSave", function() {
+            if (confirm("저장 하시겠습니까?")) {
+                let form = document.getElementById("fAdminList");
+                if (form.checkValidity()) { // HTML5 폼 검증 수행
+                    form.submit();
+                } else {
+                    form.reportValidity();
+                }
+            }
+            return false;
+        });
+    </script>
+}

+ 44 - 0
Admin/Pages/Forum/Board/_Header.cshtml

@@ -0,0 +1,44 @@
+@using Library.Models.Forum
+@{
+    var boardID = ViewData["BoardID"] as int?;
+    var boardList = ViewData["BoardList"] as List<Board>;
+}
+
+<div class="row">
+    <div class="col">
+        <h3>@ViewData["Title"]</h3>
+    </div>
+    <div class="col-auto">
+        <select id="boardID" class="form-select" required>
+            <option value="">게시판 선택</option>
+            @if (boardList != null)
+            {
+                @foreach (var row in boardList)
+                {
+                    <option value="@row.ID" selected="@(row.ID == boardID)">@row.Name</option>
+                }
+            }
+        </select>
+    </div>
+</div>
+<hr />
+
+<script type="module">
+    $(document).on("change", "#boardID", function () {
+        var boardID = $(this).val();
+        if (boardID) {
+            const url = new URL(window.location.href);
+
+            const pathSegments = url.pathname.split('/');
+
+            if (pathSegments.length >= 6) {
+                pathSegments[5] = boardID;
+            }
+
+            const newPath = pathSegments.join('/');
+            const newURL = `${newPath}${url.search}`;
+
+            window.location.href = newURL;
+        }
+    });
+</script>

+ 42 - 0
Admin/Pages/Forum/Board/_Navbar.cshtml

@@ -0,0 +1,42 @@
+@{
+    string? currentAction = ViewContext.RouteData.Values["action"] as string;
+    string? currentController = ViewContext.RouteData.Values["controller"] as string;
+
+    var sector = ViewData["Sector"] as string ?? string.Empty;
+    var boardID = ViewData["BoardID"] as int?;
+    var queryString = ViewData["QueryString"] as string ?? string.Empty;
+
+    var tabs = new List<(string controller, string Action, string Name)>
+    {
+        ("Meta", "List", "목록"),
+        ("Meta", "View", "열람"),
+        ("Meta", "Write", "작성"),
+        ("Prefix", "", "말머리"),
+        ("Meta", "Comment", "댓글"),
+        ("Meta", "General", "일반"),
+        ("Meta", "Notify", "알림"),
+        ("Meta", "NotifyTemplate", "양식"),
+        ("Meta", "Permission", "권한"),
+        ("Meta", "Exp", "경험치"),
+        ("Manager", "", "관리자")
+    };
+}
+
+<ul class="nav nav-tabs">
+    <li class="nav-item">
+        <a class="nav-link @(currentController == "List" && currentAction == "Edit" ? "active" : "")" href="@Url.Content($"~/Forum/Board/List/{boardID}/Edit?{queryString}")">기본</a>
+    </li>
+
+    @foreach (var (controller, action, name) in tabs)
+    {
+        var isActive = (currentController == controller && sector == action) ? "active" : "";
+        var href = string.IsNullOrEmpty(action)
+                    ? Url.Content($"~/Forum/Board/{controller}/{boardID}?{queryString}")
+                    : Url.Content($"~/Forum/Board/{controller}/{action}/{boardID}?{queryString}");
+
+        <li class="nav-item">
+            <a class="nav-link @isActive" href="@href">@name</a>
+        </li>
+    }
+</ul>
+<br/>

+ 96 - 0
Admin/Pages/Forum/Posts/List/Edit.cshtml

@@ -0,0 +1,96 @@
+@model Admin.ViewModels.Forum.Posts.List.EditViewModel
+@{
+    ViewData["Title"] = "게시글 수정";
+}
+
+<div class="container">
+    <h3 class="mb-3">@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <form method="post" action="/Forum/Post/Edit/@Model.ID" accept-charset="utf-8" autocomplete="off" enctype="multipart/form-data">
+        <input type="hidden" name="ID" value="@Model.ID" />
+        <input type="hidden" name="BoardID" value="@Model.BoardID" />
+        <input type="hidden" name="ReturnUrl" value="@(Model.ReturnUrl ?? "/Forum/Post/List")" />
+
+        <!-- 제목 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label"><span class="text-danger">*</span> 제목</label>
+            <div class="col-sm-10">
+                <input type="text" name="Subject" class="form-control" value="@Model.Subject" maxlength="255" required />
+            </div>
+        </div>
+
+        <!-- 내용 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">내용</label>
+            <div class="col-sm-10">
+                <textarea name="Content" class="form-control" rows="10" maxlength="8000">@Model.Content</textarea>
+            </div>
+        </div>
+
+        <!-- 대표 이미지 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">대표 이미지</label>
+            <div class="col-sm-10">
+                <div id="thumbPrev" @(string.IsNullOrWhiteSpace(Model.Thumbnail) ? "hidden" : "")>
+                    <img class="img-fluid img-thumbnail" alt="대표 이미지 미리보기" src="@(Model.Thumbnail ?? "")" />
+                </div>
+                <input type="file" id="ThumbnailFile" name="ThumbnailFile" class="form-control" accept=".jpg,.jpeg,.png,.gif,.webp,.bmp" />
+                <span class="form-text text-muted">
+                    지원 확장자: <code>.jpg</code>, <code>.jpeg</code>, <code>.png</code>, <code>.gif</code>, <code>.webp</code>, <code>.bmp</code><br />
+                    권장 크기: 가로 300px 이상 (최대 10MB)
+                </span>
+            </div>
+        </div>
+
+
+        <!-- 상태 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">상태</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check form-check-inline">
+                    <input type="checkbox" class="form-check-input" name="IsNotice" value="true" @(Model.IsNotice ? "checked" : "") />
+                    <input type="hidden" name="IsNotice" value="false" />
+                    <label class="form-check-label">공지</label>
+                </div>
+
+                <div class="form-check form-check-inline">
+                    <input type="checkbox" class="form-check-input" name="IsSecret" value="true" @(Model.IsSecret ? "checked" : "") />
+                    <input type="hidden" name="IsSecret" value="false" />
+                    <label class="form-check-label">비밀</label>
+                </div>
+
+                <div class="form-check form-check-inline">
+                    <input type="checkbox" class="form-check-input" name="IsAnonymous" value="true" @(Model.IsAnonymous ? "checked" : "") />
+                    <input type="hidden" name="IsAnonymous" value="false" />
+                    <label class="form-check-label">익명</label>
+                </div>
+            </div>
+        </div>
+
+        <hr />
+
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="@(Model.ReturnUrl ?? "/Forum/Post/List")" class="btn btn-sm btn-secondary">취소</a>
+            <button type="submit"
+                    class="btn btn-sm btn-danger"
+                    formaction="/Forum/Post/Delete/@Model.ID"
+                    formmethod="post"
+                    formnovalidate
+                    onclick="return confirm('삭제 하시겠습니까? 삭제된 게시물은 복구가 불가능합니다.');">
+                삭제
+            </button>
+        </div>
+
+        <br />
+    </form>
+</div>
+
+@section Scripts {
+    <script>
+        setupImagePreview("ThumbnailFile", "thumbPrev");
+    </script>
+}

+ 268 - 0
Admin/Pages/Forum/Posts/List/List.cshtml

@@ -0,0 +1,268 @@
+@model Admin.ViewModels.Forum.Posts.List.ListViewModel
+@using Library.Helpers
+@{
+    ViewData["Title"] = "게시글 목록";
+    var pageIndex = (Model.Parameter.Page <= 0 ? 1 : Model.Parameter.Page);
+    var perPage = (Model.Parameter.PerPage <= 0 ? 20 : Model.Parameter.PerPage);
+}
+
+<div class="container-fluid">
+    <h3 class="mb-3">@ViewData["Title"]</h3>
+
+    <partial name="_StatusMessage" />
+
+    <div class="card mb-3">
+        <div class="card-body">
+            <form class="row gx-2 gy-2 align-items-end filter-bar">
+                <div class="col-12 col-sm-6 col-md-auto">
+                    <label class="form-label mb-1 small">게시판 그룹</label>
+                    <select id="boardId" class="form-select form-select-sm w-100">
+                        <option value="">- 전체 -</option>
+                        @foreach (var g in (Model?.BoardList ?? Enumerable.Empty<SelectListItem>()))
+                        {
+                            var selected = ((Model?.Parameter?.BoardID?.ToString() ?? "") == (g.Value ?? ""));
+                            <option value="@g.Value" selected="@(selected)">@g.Text</option>
+                        }
+                    </select>
+                </div>
+
+                <div class="col-12 col-sm-6 col-md-auto">
+                    <label class="form-label mb-1 small">검색</label>
+                    <select id="search" class="form-select form-select-sm w-100">
+                        <option value="">- 전체 -</option>
+                        <option value="0" selected="@(Model?.Parameter.Search == 0)">제목</option>
+                        <option value="1" selected="@(Model?.Parameter.Search == 1)">내용</option>
+                        <option value="2" selected="@(Model?.Parameter.Search == 2)">작성자</option>
+                        <option value="3" selected="@(Model?.Parameter.Search == 3)">댓글</option>
+                    </select>
+                </div>
+
+                <div class="col-12 col-sm-6 col-md-4 col-lg-3">
+                    <label class="form-label mb-1 small">키워드</label>
+                    <input type="text" id="keyword" class="form-control form-control-sm w-100" value="@(Model?.Parameter.Keyword ?? "")" placeholder="검색어" />
+                </div>
+
+                <div class="col-12 col-sm-6 col-md-auto">
+                    <label class="form-label mb-1 small">기간</label>
+                    <div class="input-group input-group-sm">
+                        <input type="date" id="startAt" class="form-control" value="@(Model?.Parameter.StartAt ?? "")" />
+                        <span class="input-group-text">~</span>
+                        <input type="date" id="endAt" class="form-control" value="@(Model?.Parameter.EndAt ?? "")" />
+                    </div>
+                </div>
+
+                <div class="col-12 col-sm-6 col-md-auto">
+                    <label class="form-label mb-1 small">정렬</label>
+                    <select id="sort" class="form-select form-select-sm w-100">
+                        <option value="0" selected="@(Model?.Parameter.Sort == 0)">최신순</option>
+                        <option value="1" selected="@(Model?.Parameter.Sort == 1)">조회순</option>
+                        <option value="2" selected="@(Model?.Parameter.Sort == 2)">댓글순</option>
+                        <option value="3" selected="@(Model?.Parameter.Sort == 3)">공감순</option>
+                    </select>
+                </div>
+
+                <div class="col-12 col-md-auto">
+                    <label class="form-label mb-1 small d-block">상태</label>
+                    <div class="d-flex flex-wrap gap-2">
+                        <div class="form-check form-check-inline m-0">
+                            <input class="form-check-input" type="checkbox" id="isNotice" checked="@(Model?.Parameter.IsNotice == true)" />
+                            <label class="form-check-label small" for="isNotice">공지</label>
+                        </div>
+                        <div class="form-check form-check-inline m-0">
+                            <input class="form-check-input" type="checkbox" id="isSecret" checked="@(Model?.Parameter.IsSecret == true)" />
+                            <label class="form-check-label small" for="isSecret">비밀</label>
+                        </div>
+                        <div class="form-check form-check-inline m-0">
+                            <input class="form-check-input" type="checkbox" id="isReply" checked="@(Model?.Parameter.IsReply == true)" />
+                            <label class="form-check-label small" for="isReply">답글</label>
+                        </div>
+                        <div class="form-check form-check-inline m-0">
+                            <input class="form-check-input" type="checkbox" id="isDeleted" checked="@(Model?.Parameter.IsDeleted == true)" />
+                            <label class="form-check-label small" for="isDeleted">삭제</label>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="col-6 col-sm-4 col-md-auto">
+                    <label class="form-label mb-1 small d-block">&nbsp;</label>
+                    <button id="btnSearch" class="btn btn-sm btn-primary w-100">검색</button>
+                </div>
+                 
+                <div class="col-12 col-sm-auto ms-auto text-sm-end">
+                    <label class="form-label mb-1 small d-block">&nbsp;</label>
+                    <div class="d-flex align-items-center justify-content-end gap-2">
+                        <select id="perPage" class="form-select form-select-sm w-auto">
+                            <option value="10" selected="@(Model?.Parameter.PerPage == 10)">10</option>
+                            <option value="20" selected="@(Model?.Parameter.PerPage == 20)">20</option>
+                            <option value="50" selected="@(Model?.Parameter.PerPage == 50)">50</option>
+                            <option value="100" selected="@(Model?.Parameter.PerPage == 100)">100</option>
+                        </select>
+
+                        <a id="btnCreate" class="btn btn-sm btn-success" href="/Forum/Post/Create">추가</a>
+                    </div>
+                </div>
+
+            </form>
+        </div>
+    </div>
+
+    <div class="card">
+        <div class="table-responsive">
+            <table class="table table-sm table-hover align-middle mb-0 table-fix">
+                <thead class="table-light">
+                    <tr>
+                        <th class="text-center" style="width:70px">No</th>
+                        <th class="col-subject" style="min-width:320px">제목</th>
+                        <th class="col-author" style="width:120px">작성자</th>
+                        <th class="text-end">조회</th>
+                        <th class="text-end">공감</th>
+                        <th class="text-end">비공감</th>
+                        <th class="text-end">댓글</th>
+                        <th class="text-center">첨부</th>
+                        <th class="col-date" style="width:150px">작성일</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    @if (Model?.Data == null || Model.Data.Count == 0)
+                    {
+                        <tr>
+                            <td colspan="10" class="text-center text-muted py-4">데이터가 없습니다.</td>
+                        </tr>
+                    }
+                    else
+                    {
+                        foreach (var item in Model.Data)
+                        {
+                            <tr class="@(item.IsActive ? "table-primary" : (item.IsNotice ? "table-warning" : ""))">
+                                <td class="text-center">@item.No</td>
+                                <td>
+                                    <div class="d-flex align-items-center gap-2">
+                                        @if (!string.IsNullOrEmpty(item.Thumbnail))
+                                        {
+                                            <img src="@item.Thumbnail" alt="" style="width:36px;height:36px;object-fit:cover;border-radius:.5rem;" />
+                                        }
+                                        <div>
+                                            <div>
+                                                @if (item.IsNotice)
+                                                {
+                                                    <span class="badge text-bg-warning me-1">공지</span>
+                                                }
+                                                @if (item.IsSecret)
+                                                {
+                                                    <span class="badge text-bg-secondary me-1">비밀</span>
+                                                }
+                                                @if (item.IsReply)
+                                                {
+                                                    <span class="badge text-bg-info me-1">답글</span>
+                                                }
+                                                @if (item.IsSpeaker)
+                                                {
+                                                    <span class="badge text-bg-primary me-1">작성자</span>
+                                                }
+                                                @if (item.IsAnonymous)
+                                                {
+                                                    <span class="badge text-bg-dark me-1">익명</span>
+                                                }
+                                                <a class="text-decoration-none" href="@item.EditURL">@item.Subject</a>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </td>
+                                <td>@(item.Name ?? item.SID ?? "-")</td>
+                                <td class="text-end">@item.Views</td>
+                                <td class="text-end">@item.Likes</td>
+                                <td class="text-end">@item.Dislikes</td>
+                                <td class="text-end">@item.Comments</td>
+                                <td class="text-center">
+                                    <span title="이미지">@item.Images</span> /
+                                    <span title="미디어">@item.Medias</span> /
+                                    <span title="파일">@item.Files</span> /
+                                    <span title="태그">@item.Tags</span>
+                                </td>
+                                <td>@item.CreatedAt</td>
+                            </tr>
+                        }
+                    }
+
+                </tbody>
+            </table>
+        </div>
+        </<br />
+        <partial name="_Pagination" model="Model!.Pagination" />
+    </div>
+</div>
+
+<style>
+    .table-responsive {
+        overflow-x: auto;
+    }
+
+    .table-fix th, .table-fix td {
+        white-space: nowrap;
+        vertical-align: middle;
+    }
+
+    .table-fix .col-subject {
+        white-space: normal;
+        min-width: 280px;
+    }
+
+    .table-fix .col-author {
+        min-width: 140px;
+    }
+
+    .table-fix .col-date {
+        min-width: 160px;
+    }
+
+    .table-fix .col-actions {
+        min-width: 120px;
+    }
+
+    .table-fix td .attach-compact > span {
+        margin-right: 6px;
+    }
+</style>
+
+@section Scripts {
+    <script>
+        function updateQueryString(resetPage = true) {
+            const qp = new URLSearchParams();
+
+            qp.set("BoardGroupID", $("#boardGroupId").val() || "");
+            qp.set("BoardID",      $("#boardId").val()      || "");
+
+            const searchVal = $("#search").val();
+            if (searchVal !== null && searchVal !== "") qp.set("Search", searchVal);
+
+            qp.set("Keyword", $("#keyword").val() || "");
+            qp.set("StartAt", $("#startAt").val() || "");
+            qp.set("EndAt",   $("#endAt").val()   || "");
+
+            const sortVal = $("#sort").val();
+            if (sortVal !== null && sortVal !== "") qp.set("Sort", sortVal);
+
+            if ($("#isNotice").is(":checked"))  qp.set("IsNotice", "true");
+            if ($("#isSecret").is(":checked"))  qp.set("IsSecret", "true");
+            if ($("#isReply").is(":checked"))   qp.set("IsReply",  "true");
+            if ($("#isDeleted").is(":checked")) qp.set("IsDeleted","true");
+
+            qp.set("PerPage", $("#perPage").val() || "10");
+            qp.set("Page", resetPage ? "1" : "@pageIndex");
+
+            window.location.href = window.location.pathname + "?" + qp.toString();
+        }
+
+        $("#btnSearch").on("click", function (e)
+        {
+            e.preventDefault(); updateQueryString(true);
+        });
+
+        $("#keyword,#startAt,#endAt").on("keyup", function (e)
+        {
+            if (e.key === "Enter") updateQueryString(true);
+        });
+
+
+    </script>
+}

+ 136 - 0
Admin/Pages/Forum/Posts/List/Write.cshtml

@@ -0,0 +1,136 @@
+@model Admin.ViewModels.Forum.Posts.List.WriteViewModel
+@using Microsoft.AspNetCore.Mvc.Rendering
+@{
+    ViewData["Title"] = "게시글 등록";
+}
+
+<div class="container">
+    <h3 class="mb-3">@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <form id="fPostWrite" method="post" accept-charset="utf-8" autocomplete="off"
+          action="/Forum/Post/Create" enctype="multipart/form-data">
+        @Html.AntiForgeryToken()
+        <input type="hidden" name="ReturnUrl" value="@(Model.ReturnUrl ?? "/Forum/Post/List")" />
+
+        <!-- 게시판 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label"><span class="text-danger">*</span> 게시판</label>
+            <div class="col-sm-10">
+                @if ((Model.BoardList?.Count ?? 0) > 0)
+                {
+                    <select name="BoardID" class="form-select w-auto" required
+                            asp-items="@(new SelectList(Model.BoardList!, "Value", "Text",
+                                                            Model.BoardID == 0 ? null : Model.BoardID.ToString()))">
+                    <option value="">- 선택 -</option>
+                </select>
+                 }
+                else
+                {
+                    <input type="number" name="BoardID" class="form-control w-auto"
+                           value="@(Model.BoardID == 0 ? "" : Model.BoardID.ToString())" required />
+                }
+            </div>
+        </div>
+
+        <!-- 제목 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label"><span class="text-danger">*</span> 제목</label>
+            <div class="col-sm-10">
+                <input type="text" name="Subject" class="form-control"
+                       required maxlength="255" placeholder="제목을 입력하세요 (최대 255자)" value="@Model.Subject" />
+            </div>
+        </div>
+
+        <!-- 내용 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">내용</label>
+            <div class="col-sm-10">
+                <textarea name="Content" class="form-control" rows="12" maxlength="8000"
+                          placeholder="내용을 입력하세요 (최대 8000자)">@Model.Content</textarea>
+            </div>
+        </div>
+
+        <!-- 썸네일 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">대표 이미지(썸네일)</label>
+            <div class="col-sm-10">
+                <input type="file" name="ThumbnailFile" class="form-control" placeholder="" accept="image/*" />
+            </div>
+        </div>
+
+        <!-- 이미지 여러 장 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">이미지</label>
+            <div class="col-sm-10">
+                <input type="file" name="ImageFiles" class="form-control" placeholder="" accept="image/*" multiple />
+            </div>
+        </div>
+
+        <!-- 미디어 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">미디어</label>
+            <div class="col-sm-10">
+                <input type="file" name="MediaFiles" class="form-control" placeholder="" accept="video/*,audio/*" multiple />
+            </div>
+        </div>
+
+        <!-- 첨부 파일 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">첨부 파일</label>
+            <div class="col-sm-10">
+                <input type="file" name="AttachFiles" class="form-control" placeholder=""  multiple />
+            </div>
+        </div>
+
+        <!-- 상태 -->
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">상태</label>
+            <div class="col-sm-10 align-content-center">
+                <input type="hidden" name="IsNotice" value="false" />
+                <div class="form-check form-check-inline">
+                    <input class="form-check-input" type="checkbox" name="IsNotice" value="true" @(Model.IsNotice ? "checked" : "") />
+                    <label class="form-check-label">공지</label>
+                </div>
+
+                <input type="hidden" name="IsSecret" value="false" />
+                <div class="form-check form-check-inline">
+                    <input class="form-check-input" type="checkbox" name="IsSecret" value="true" @(Model.IsSecret ? "checked" : "") />
+                    <label class="form-check-label">비밀</label>
+                </div>
+
+                <input type="hidden" name="IsAnonymous" value="false" />
+                <div class="form-check form-check-inline">
+                    <input class="form-check-input" type="checkbox" name="IsAnonymous" value="true" @(Model.IsAnonymous ? "checked" : "") />
+                    <label class="form-check-label">익명</label>
+                </div>
+            </div>
+        </div>
+
+        <hr />
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a class="btn btn-sm btn-secondary btn-cancel" href="@(Model.ReturnUrl ?? "/Forum/Post/List")">취소</a>
+        </div>
+        <br />
+    </form>
+</div>
+
+@section Scripts {
+    <script>
+        $(function () {
+            $(".btn-cancel").on("click", function (e) {
+                const s = $("input[name='Subject']").val()?.trim();
+                const c = $("textarea[name='Content']").val()?.trim();
+                if (s || c) {
+                    e.preventDefault();
+                    if (confirm("내용이나 제목이 남아 있습니다. 글 작성을 취소하시겠습니까?")) {
+                        location.href = $(this).attr("href");
+                    }
+                }
+            });
+        });
+    </script>
+}

+ 10 - 0
Application/Abstractions/Messaging/ICommand.cs

@@ -0,0 +1,10 @@
+using MediatR;
+
+namespace Application.Abstractions.Messaging;
+
+/// <summary>
+/// 반환값이 없는 Command를 나타냅니다.
+/// </summary>
+public interface ICommand : IRequest
+{
+}

+ 11 - 0
Application/Abstractions/Messaging/ICommandHandler.cs

@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace Application.Abstractions.Messaging;
+
+/// <summary>
+/// 반환값이 없는 Command Handler를 나타냅니다.
+/// </summary>
+/// <typeparam name="TCommand">Command 타입</typeparam>
+public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand> where TCommand : ICommand
+{
+}

+ 12 - 0
Application/Abstractions/Messaging/ICommandHandlerWithResponse.cs

@@ -0,0 +1,12 @@
+using MediatR;
+
+namespace Application.Abstractions.Messaging;
+
+/// <summary>
+/// 반환값이 있는 Command Handler를 나타냅니다.
+/// </summary>
+/// <typeparam name="TCommand">Command 타입</typeparam>
+/// <typeparam name="TResponse">반환 타입</typeparam>
+public interface ICommandHandler<in TCommand, TResponse> : IRequestHandler<TCommand, TResponse> where TCommand : ICommand<TResponse>
+{
+}

+ 11 - 0
Application/Abstractions/Messaging/ICommandWithResponse.cs

@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace Application.Abstractions.Messaging;
+
+/// <summary>
+/// 반환값이 있는 Command를 나타냅니다.
+/// </summary>
+/// <typeparam name="TResponse">반환 타입</typeparam>
+public interface ICommand<out TResponse> : IRequest<TResponse>
+{
+}

+ 11 - 0
Application/Abstractions/Messaging/IQuery.cs

@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace Application.Abstractions.Messaging;
+
+/// <summary>
+/// Query를 나타냅니다. Query는 항상 반환값이 있습니다.
+/// </summary>
+/// <typeparam name="TResponse">반환 타입</typeparam>
+public interface IQuery<out TResponse> : IRequest<TResponse>
+{
+}

+ 12 - 0
Application/Abstractions/Messaging/IQueryHandler.cs

@@ -0,0 +1,12 @@
+using MediatR;
+
+namespace Application.Abstractions.Messaging;
+
+/// <summary>
+/// Query Handler를 나타냅니다.
+/// </summary>
+/// <typeparam name="TQuery">Query 타입</typeparam>
+/// <typeparam name="TResponse">반환 타입</typeparam>
+public interface IQueryHandler<in TQuery, TResponse> : IRequestHandler<TQuery, TResponse> where TQuery : IQuery<TResponse>
+{
+}

+ 157 - 0
Application/Abstractions/Messaging/README.md

@@ -0,0 +1,157 @@
+# MediatR 추상 인터페이스
+
+MediatR의 `IRequest`와 `IRequestHandler`를 래핑한 도메인 의도를 명확히 하는 인터페이스입니다.
+
+## 📚 제공 인터페이스
+
+### Command (쓰기 작업)
+```csharp
+ICommand                                    // 반환값 없음
+ICommand<TResponse>                         // 반환값 있음
+ICommandHandler<TCommand>                   // 반환값 없는 Handler
+ICommandHandler<TCommand, TResponse>        // 반환값 있는 Handler
+```
+
+### Query (읽기 작업)
+```csharp
+IQuery<TResponse>                           // 항상 반환값 있음
+IQueryHandler<TQuery, TResponse>            // Query Handler
+```
+
+## 🎯 사용 방법
+
+### 1. 반환값이 없는 Command
+
+```csharp
+// Command
+public sealed record DeleteMemberCommand(int MemberID) : ICommand;
+
+// Handler
+public sealed class Handler(IAppDbContext db)
+    : ICommandHandler<DeleteMemberCommand>
+{
+    public async Task Handle(DeleteMemberCommand request, CancellationToken ct)
+    {
+        await db.Member
+            .Where(x => x.ID == request.MemberID)
+            .ExecuteDeleteAsync(ct);
+    }
+}
+```
+
+### 2. 반환값이 있는 Command
+
+```csharp
+// Command
+public sealed record CreateMemberCommand(
+    string Email,
+    string Name
+) : ICommand<int>; // 생성된 ID 반환
+
+// Handler
+public sealed class Handler(IAppDbContext db)
+    : ICommandHandler<CreateMemberCommand, int>
+{
+    public async Task<int> Handle(CreateMemberCommand request, CancellationToken ct)
+    {
+        var member = Member.Create(request.Email);
+        await db.Member.AddAsync(member, ct);
+        await db.SaveChangesAsync(ct);
+
+        return member.ID;
+    }
+}
+```
+
+### 3. Query
+
+```csharp
+// Query
+public sealed record GetMemberQuery(int MemberID) : IQuery<MemberResponse>;
+
+// Handler
+public sealed class Handler(IAppDbContext db)
+    : IQueryHandler<GetMemberQuery, MemberResponse>
+{
+    public async Task<MemberResponse> Handle(GetMemberQuery request, CancellationToken ct)
+    {
+        var member = await db.Member
+            .AsNoTracking()
+            .FirstOrDefaultAsync(x => x.ID == request.MemberID, ct);
+
+        if (member is null)
+            throw new KeyNotFoundException();
+
+        return new MemberResponse { /* ... */ };
+    }
+}
+```
+
+## ✅ 장점
+
+### 1. 의도 명확화
+- `ICommand` = 데이터 변경 작업
+- `IQuery` = 데이터 조회 작업
+- 코드를 읽는 사람이 즉시 이해 가능
+
+### 2. CQRS 원칙 강제
+- Command와 Query가 명확히 구분됨
+- Query는 반드시 반환값 필요
+
+### 3. Pipeline Behavior 구분 가능
+```csharp
+// Query에만 캐싱 적용
+public class QueryCachingBehavior<TQuery, TResponse>
+    : IPipelineBehavior<TQuery, TResponse>
+    where TQuery : IQuery<TResponse> // 👈 타입 제약
+{
+    // ...
+}
+
+// Command에만 트랜잭션 적용
+if (request is ICommand or ICommand<TResponse>)
+{
+    // 트랜잭션 시작
+}
+```
+
+### 4. 타입 안정성
+- 컴파일 타임에 타입 체크
+- `where TCommand : ICommand` 제약으로 안전성 보장
+
+## 🔄 마이그레이션 가이드
+
+### 기존 코드
+```csharp
+// ❌ Before
+public sealed record Command(...) : IRequest;
+public sealed class Handler : IRequestHandler<Command> { }
+```
+
+### 새 코드
+```csharp
+// ✅ After
+public sealed record Command(...) : ICommand;
+public sealed class Handler : ICommandHandler<Command> { }
+```
+
+### 호환성
+- 기존 `IRequest` 코드도 계속 동작함
+- 새로운 Feature부터 `ICommand`/`IQuery` 사용
+- 점진적 마이그레이션 가능
+
+## 📋 적용 규칙
+
+| 작업 유형 | 사용 인터페이스 | 예시 |
+|----------|----------------|------|
+| 생성 (Create) | `ICommand<TResponse>` | CreateMemberCommand |
+| 수정 (Update) | `ICommand` | UpdateMemberCommand |
+| 삭제 (Delete) | `ICommand` | DeleteMemberCommand |
+| 조회 단건 (Get) | `IQuery<TResponse>` | GetMemberQuery |
+| 조회 목록 (Search) | `IQuery<TResponse>` | SearchMembersQuery |
+
+## 🎓 참고 자료
+
+- [MediatR 공식 문서](https://github.com/jbogard/MediatR)
+- [CQRS 패턴](https://martinfowler.com/bliki/CQRS.html)
+- Clean Architecture - Robert C. Martin

+ 2 - 2
Application/Features/Banner/Item/Create/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 using Microsoft.AspNetCore.Http;
 
 namespace Application.Features.Banner.Item.Create
@@ -13,5 +13,5 @@ namespace Application.Features.Banner.Item.Create
         bool IsActive,
         DateTime? StartAt,
         DateTime? EndAt
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Banner/Item/Create/Handler.cs

@@ -1,12 +1,12 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using Domain.Entities.Page.Banner;
 using SharedKernel.Storage;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Banner.Item.Create;
 
-public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IRequestHandler<Command>
+public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : ICommandHandler<Command>
 {
     public async Task Handle(Command request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Banner/Item/Delete/Command.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Banner.Item.Delete
 {
-    public sealed record Command(int[] IDs) : IRequest;
+    public sealed record Command(int[] IDs) : ICommand;
 }

+ 2 - 2
Application/Features/Banner/Item/Delete/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using SharedKernel.Storage;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Banner.Item.Delete;
 
-public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IRequestHandler<Command>
+public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : ICommandHandler<Command>
 {
     public async Task Handle(Command request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Banner/Item/Get/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Banner.Item.Get;
 
-public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response>
+public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
 {
     public async Task<Response> Handle(Query request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Banner/Item/Get/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Banner.Item.Get
 {
-    public sealed record Query(int ID) : IRequest<Response>;
+    public sealed record Query(int ID) : IQuery<Response>;
 }

+ 2 - 2
Application/Features/Banner/Item/Search/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Banner.Item.Search;
 
-public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response>
+public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
 {
     public async Task<Response> Handle(Query request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Banner/Item/Search/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Banner.Item.Search
 {
-    public sealed record Query(int? PositionID, string? Keyword, int PageNum, ushort PerPage) : IRequest<Response>;
+    public sealed record Query(int? PositionID, string? Keyword, int PageNum, ushort PerPage) : IQuery<Response>;
 }

+ 2 - 2
Application/Features/Banner/Item/Update/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 using Microsoft.AspNetCore.Http;
 
 namespace Application.Features.Banner.Item.Update
@@ -14,5 +14,5 @@ namespace Application.Features.Banner.Item.Update
         bool IsActive,
         DateTime? StartAt,
         DateTime? EndAt
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Banner/Item/Update/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using SharedKernel.Storage;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Banner.Item.Update;
 
-public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IRequestHandler<Command>
+public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : ICommandHandler<Command>
 {
     public async Task Handle(Command request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Banner/Position/GetAll/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Banner.Position.GetAll;
 
-public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response>
+public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
 {
     public async Task<Response> Handle(Query request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Banner/Position/GetAll/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Banner.Position.GetAll
 {
-    public sealed record Query : IRequest<Response>;
+    public sealed record Query : IQuery<Response>;
 }

+ 2 - 2
Application/Features/Banner/Position/Save/Command.cs

@@ -1,8 +1,8 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Banner.Position.Save
 {
-    public sealed record Command(List<Command.Row> Items) : IRequest<Response>
+    public sealed record Command(List<Command.Row> Items) : ICommand<Response>
     {
         public sealed record Row(int? ID, string Code, string Subject, bool IsActive);
     }

+ 2 - 2
Application/Features/Banner/Position/Save/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using Domain.Entities.Page.Banner;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Banner.Position.Save;
 
-public sealed class Handler(IAppDbContext db) : IRequestHandler<Command, Response>
+public sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Response>
 {
     public async Task<Response> Handle(Command request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Config/Get/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Config.Get;
 
-public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response?>
+public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response?>
 {
     public async Task<Response?> Handle(Query request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Config/Get/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Config.Get
 {
-    public sealed record Query : IRequest<Response?>;
+    public sealed record Query : IQuery<Response?>;
 }

+ 2 - 2
Application/Features/Config/Update/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Config.Update
 {
@@ -12,7 +12,7 @@ namespace Application.Features.Config.Update
         Request.ExternalApiConfigDto? External = null,
         Request.PaymentConfigDto? Payment = null,
         Command.ImagesDeleteFlags? ImagesDelete = null
-    ) : IRequest {
+    ) : ICommand {
         public sealed record ImagesDeleteFlags(
             bool Favicon = false,
             bool LogoSquare = false,

+ 2 - 2
Application/Features/Config/Update/Handler.cs

@@ -1,14 +1,14 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using Domain.Entities.Common;
 using ConfigEntity = Domain.Entities.Common.Config;
-using MediatR;
 using Microsoft.AspNetCore.Http;
 using Microsoft.EntityFrameworkCore;
 using SharedKernel.Storage;
 
 namespace Application.Features.Config.Update;
 
-public sealed class Handler(IAppDbContext db, IFileStorage storage, IEditorImageService editorImage) : IRequestHandler<Command>
+public sealed class Handler(IAppDbContext db, IFileStorage storage, IEditorImageService editorImage) : ICommandHandler<Command>
 {
     public async Task Handle(Command request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Director/Role/Create/Command.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.Role.Create
 {
-    public sealed record Command(string? RoleName) : IRequest;
+    public sealed record Command(string? RoleName) : ICommand;
 }

+ 2 - 2
Application/Features/Director/Role/Create/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
-using MediatR;
 
 namespace Application.Features.Director.Role.Create
 {
-    public sealed class Handler(IIdentityRoleWriter roleWriter) : IRequestHandler<Command>
+    public sealed class Handler(IIdentityRoleWriter roleWriter) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/Role/Delete/Command.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.Role.Delete
 {
-    public sealed record Command(string? RoleID) : IRequest;
+    public sealed record Command(string? RoleID) : ICommand;
 }

+ 2 - 2
Application/Features/Director/Role/Delete/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
-using MediatR;
 
 namespace Application.Features.Director.Role.Delete
 {
-    public sealed class Handler(IIdentityRoleWriter roleWriter) : IRequestHandler<Command>
+    public sealed class Handler(IIdentityRoleWriter roleWriter) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/Role/Get/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
-using MediatR;
 
 namespace Application.Features.Director.Role.Get
 {
-    public sealed class Handler(IIdentityRoleReader roleReader) : IRequestHandler<Query, Response>
+    public sealed class Handler(IIdentityRoleReader roleReader) : IQueryHandler<Query, Response>
     {
         public async Task<Response> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/Role/Get/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.Role.Get
 {
-    public sealed record Query(string ID) : IRequest<Response>;
+    public sealed record Query(string ID) : IQuery<Response>;
 }

+ 2 - 2
Application/Features/Director/Role/Permissions/Get/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
 using SharedKernel.Constants;
-using MediatR;
 using System.Data;
 
 namespace Application.Features.Director.Role.Permissions.Get
 {
-    public sealed class Handler(IIdentityRoleReader roleReader) : IRequestHandler<Query, Response>
+    public sealed class Handler(IIdentityRoleReader roleReader) : IQueryHandler<Query, Response>
     {
         public async Task<Response> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/Role/Permissions/Get/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.Role.Permissions.Get
 {
-    public sealed record Query(string RoleID) : IRequest<Response>;
+    public sealed record Query(string RoleID) : IQuery<Response>;
 }

+ 2 - 2
Application/Features/Director/Role/Permissions/Update/Command.cs

@@ -1,10 +1,10 @@
 using Application.Abstractions.Identity.Models;
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.Role.Permissions.Update
 {
     public sealed record Command(
         string RoleID,
         PermissionDto Permissions
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Director/Role/Permissions/Update/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
-using MediatR;
 
 namespace Application.Features.Director.Role.Permissions.Update
 {
-    public sealed class Handler(IIdentityRoleWriter roleWriter) : IRequestHandler<Command>
+    public sealed class Handler(IIdentityRoleWriter roleWriter) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/Roles/Get/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
-using MediatR;
 
 namespace Application.Features.Director.Roles.Get
 {
-    public sealed class Handler(IIdentityRoleReader roleReader) : IRequestHandler<Query, List<Response>>
+    public sealed class Handler(IIdentityRoleReader roleReader) : IQueryHandler<Query, List<Response>>
     {
         public async Task<List<Response>> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/Roles/Get/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.Roles.Get
 {
-    public sealed record Query() : IRequest<List<Response>>;
+    public sealed record Query() : IQuery<List<Response>>;
 }

+ 2 - 2
Application/Features/Director/User/Get/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
 using Application.Abstractions.Identity.Models;
-using MediatR;
 
 namespace Application.Features.Director.User.Get
 {
-    public sealed class Handler(IIdentityUserReader userReader) : IRequestHandler<Query, AspNetUserDto?>
+    public sealed class Handler(IIdentityUserReader userReader) : IQueryHandler<Query, AspNetUserDto?>
     {
         public async Task<AspNetUserDto?> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/User/Get/Query.cs

@@ -1,7 +1,7 @@
 using Application.Abstractions.Identity.Models;
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.User.Get
 {
-    public sealed record Query(string ID) : IRequest<AspNetUserDto?>;
+    public sealed record Query(string ID) : IQuery<AspNetUserDto?>;
 }

+ 2 - 2
Application/Features/Director/User/GetRoles/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
-using MediatR;
 
 namespace Application.Features.Director.User.GetRoles
 {
-    public sealed class Handler(IIdentityRoleReader roleReader, IIdentityUserReader userReader) : IRequestHandler<Query, Response>
+    public sealed class Handler(IIdentityRoleReader roleReader, IIdentityUserReader userReader) : IQueryHandler<Query, Response>
     {
         public async Task<Response> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/User/GetRoles/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.User.GetRoles
 {
-    public sealed record Query(string UserID) : IRequest<Response>;
+    public sealed record Query(string UserID) : IQuery<Response>;
 }

+ 2 - 2
Application/Features/Director/User/Update/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.User.Update
 {
@@ -12,5 +12,5 @@ namespace Application.Features.Director.User.Update
         bool IsDeleted,
         bool EmailConfirmed,
         bool LockoutEnd
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Director/User/Update/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
 using Application.Abstractions.Identity.Models;
-using MediatR;
 
 namespace Application.Features.Director.User.Update
 {
-    public sealed class Handler(IIdentityUserWriter userWriter) : IRequestHandler<Command>
+    public sealed class Handler(IIdentityUserWriter userWriter) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/User/UpdateRoles/Command.cs

@@ -1,5 +1,5 @@
 using Application.Abstractions.Identity.Models;
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.User.UpdateRoles
 {
@@ -7,5 +7,5 @@ namespace Application.Features.Director.User.UpdateRoles
     (
         string UserID,
         UserRolesDto? Roles
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Director/User/UpdateRoles/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
-using MediatR;
 
 namespace Application.Features.Director.User.UpdateRoles
 {
-    public sealed class Handler(IIdentityUserWriter userWriter) : IRequestHandler<Command>
+    public sealed class Handler(IIdentityUserWriter userWriter) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/Users/Get/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Identity;
-using MediatR;
 
 namespace Application.Features.Director.Users.Get
 {
-    public sealed class Handler(IIdentityUserReader userReader) : IRequestHandler<Query, List<Response>>
+    public sealed class Handler(IIdentityUserReader userReader) : IQueryHandler<Query, List<Response>>
     {
         public async Task<List<Response>> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Director/Users/Get/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Director.Users.Get
 {
-    public sealed record Query() : IRequest<List<Response>>;
+    public sealed record Query() : IQuery<List<Response>>;
 }

+ 2 - 2
Application/Features/Document/Create/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Document.Create
 {
@@ -7,5 +7,5 @@ namespace Application.Features.Document.Create
         string Subject,
         string? Content,
         bool IsActive
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Document/Create/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using SharedKernel.Storage;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Document.Create
 {
-    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : IRequestHandler<Command>
+    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Document/Delete/Command.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Document.Delete
 {
-    public sealed record Command(int[] IDs) : IRequest;
+    public sealed record Command(int[] IDs) : ICommand;
 }

+ 2 - 2
Application/Features/Document/Delete/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using SharedKernel.Storage;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Document.Delete
 {
-    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : IRequestHandler<Command>
+    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Document/Get/Handler.cs

@@ -1,9 +1,9 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 
 namespace Application.Features.Document.Get
 {
-    public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response?>
+    public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response?>
     {
         public async Task<Response?> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Document/Get/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Document.Get
 {
-    public sealed record Query(int ID) : IRequest<Response?>;
+    public sealed record Query(int ID) : IQuery<Response?>;
 }

+ 2 - 2
Application/Features/Document/Search/Handler.cs

@@ -1,13 +1,13 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using SharedKernel;
 using SharedKernel.Extensions;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Options;
 
 namespace Application.Features.Document.Search
 {
-    public sealed class Handler(IAppDbContext db, IOptions<AppSettings> settings) : IRequestHandler<Query, Response>
+    public sealed class Handler(IAppDbContext db, IOptions<AppSettings> settings) : IQueryHandler<Query, Response>
     {
         public async Task<Response> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Document/Search/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Document.Search
 {
-    public sealed record Query(int Page, ushort PerPage) : IRequest<Response>;
+    public sealed record Query(int Page, ushort PerPage) : IQuery<Response>;
 }

+ 2 - 2
Application/Features/Document/Update/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Document.Update
 {
@@ -8,5 +8,5 @@ namespace Application.Features.Document.Update
         string Subject,
         string? Content,
         bool IsActive
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Document/Update/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 using SharedKernel.Storage;
 
 namespace Application.Features.Document.Update
 {
-    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : IRequestHandler<Command>
+    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Faq/Category/GetAll/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Faq.Category.GetAll
 {
-    public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response>
+    public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
     {
         public async Task<Response> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Faq/Category/GetAll/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Faq.Category.GetAll
 {
-    public sealed record Query: IRequest<Response>;
+    public sealed record Query: IQuery<Response>;
 }

+ 2 - 2
Application/Features/Faq/Category/Save/Command.cs

@@ -1,8 +1,8 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Faq.Category.Save
 {
-    public sealed record Command(IReadOnlyList<Command.Row> Items) : IRequest<Response>
+    public sealed record Command(IReadOnlyList<Command.Row> Items) : ICommand<Response>
     {
         public sealed record Row(
             int? ID,

+ 2 - 2
Application/Features/Faq/Category/Save/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 using Domain.Entities.Page.Faq;
 
 namespace Application.Features.Faq.Category.Save
 {
-    public sealed class Handler(IAppDbContext db) : IRequestHandler<Command, Response>
+    public sealed class Handler(IAppDbContext db) : ICommandHandler<Command, Response>
     {
         public async Task<Response> Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Faq/Item/Create/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Faq.Item.Create
 {
@@ -8,5 +8,5 @@ namespace Application.Features.Faq.Item.Create
         string? Answer,
         short Order,
         bool IsActive
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Faq/Item/Create/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using SharedKernel.Storage;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Faq.Item.Create
 {
-    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : IRequestHandler<Command>
+    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Faq/Item/Delete/Command.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Faq.Item.Delete
 {
-    public sealed record Command(int[] IDs) : IRequest;
+    public sealed record Command(int[] IDs) : ICommand;
 }

+ 2 - 2
Application/Features/Faq/Item/Delete/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 using SharedKernel.Storage;
 
 namespace Application.Features.Faq.Item.Delete
 {
-    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : IRequestHandler<Command>
+    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Faq/Item/Get/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Faq.Item.Get
 {
-    public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response?>
+    public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response?>
     {
         public async Task<Response?> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Faq/Item/Get/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Faq.Item.Get
 {
-    public sealed record Query(int ID) : IRequest<Response?>;
+    public sealed record Query(int ID) : IQuery<Response?>;
 }

+ 2 - 2
Application/Features/Faq/Item/Search/Handler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Faq.Item.Search
 {
-    public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response>
+    public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
     {
         public async Task<Response> Handle(Query request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Faq/Item/Search/Query.cs

@@ -1,6 +1,6 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Faq.Item.Search
 {
-    public sealed record Query(int? CategoryID, string? Keyword, int Page, ushort PerPage) : IRequest<Response>;
+    public sealed record Query(int? CategoryID, string? Keyword, int Page, ushort PerPage) : IQuery<Response>;
 }

+ 2 - 2
Application/Features/Faq/Item/Update/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Faq.Item.Update
 {
@@ -9,5 +9,5 @@ namespace Application.Features.Faq.Item.Update
         string? Answer,
         short Order,
         bool IsActive
-    ) : IRequest;
+    ) : ICommand;
 }

+ 2 - 2
Application/Features/Faq/Item/Update/Handler.cs

@@ -1,11 +1,11 @@
+using Application.Abstractions.Messaging;
 using SharedKernel.Storage;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Faq.Item.Update
 {
-    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : IRequestHandler<Command>
+    public sealed class Handler(IAppDbContext db, IEditorImageService editorImage) : ICommandHandler<Command>
     {
         public async Task Handle(Command request, CancellationToken ct)
         {

+ 2 - 2
Application/Features/Member/List/Approve/Command.cs

@@ -1,4 +1,4 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Member.List.Approve;
 
@@ -8,4 +8,4 @@ public sealed record Command(
     bool IsReceiveEmail,
     bool IsReceiveNote,
     bool IsDisclosureInvest
-) : IRequest;
+) : ICommand;

+ 2 - 2
Application/Features/Member/List/Approve/CommandHandler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Member.List.Approve;
 
-public sealed class CommandHandler(IAppDbContext db) : IRequestHandler<Command>
+public sealed class CommandHandler(IAppDbContext db) : ICommandHandler<Command>
 {
     public async Task Handle(Command request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Member/List/Approve/GetHandler.cs

@@ -1,10 +1,10 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Member.List.Approve;
 
-public sealed class GetHandler(IAppDbContext db) : IRequestHandler<Query, Response>
+public sealed class GetHandler(IAppDbContext db) : IQueryHandler<Query, Response>
 {
     public async Task<Response> Handle(Query request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Member/List/Approve/Query.cs

@@ -1,5 +1,5 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Member.List.Approve;
 
-public sealed record Query(int MemberID) : IRequest<Response>;
+public sealed record Query(int MemberID) : IQuery<Response>;

+ 2 - 2
Application/Features/Member/List/Create/Command.cs

@@ -1,5 +1,5 @@
 using Domain.Entities.Members.ValueObject;
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Member.List.Create;
 
@@ -26,4 +26,4 @@ public sealed record Command(
     string? YouTubeName,
     string? YouTubeHandle,
     string? YouTubeUrl
-) : IRequest;
+) : ICommand;

+ 2 - 2
Application/Features/Member/List/Create/Handler.cs

@@ -1,12 +1,12 @@
+using Application.Abstractions.Messaging;
 using Application.Abstractions.Data;
 using Domain.Entities.Members;
 using MemberEntity = Domain.Entities.Members.Member;
-using MediatR;
 using Microsoft.EntityFrameworkCore;
 
 namespace Application.Features.Member.List.Create;
 
-public sealed class Handler(IAppDbContext db) : IRequestHandler<Command>
+public sealed class Handler(IAppDbContext db) : ICommandHandler<Command>
 {
     public async Task Handle(Command request, CancellationToken ct)
     {

+ 2 - 2
Application/Features/Member/List/Delete/Command.cs

@@ -1,5 +1,5 @@
-using MediatR;
+using Application.Abstractions.Messaging;
 
 namespace Application.Features.Member.List.Delete;
 
-public sealed record Command(int[] IDs) : IRequest;
+public sealed record Command(int[] IDs) : ICommand;

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است