KIM-JINO5 2 mēneši atpakaļ
vecāks
revīzija
fd438547a2
55 mainītis faili ar 8710 papildinājumiem un 4 dzēšanām
  1. 1 1
      Admin/Pages/Crypto/List/Index.cshtml
  2. 148 0
      Admin/Pages/Crypto/News/Articles.cshtml
  3. 100 0
      Admin/Pages/Crypto/News/Articles.cshtml.cs
  4. 74 0
      Admin/Pages/Crypto/News/Edit.cshtml
  5. 91 0
      Admin/Pages/Crypto/News/Edit.cshtml.cs
  6. 134 0
      Admin/Pages/Crypto/News/Index.cshtml
  7. 106 0
      Admin/Pages/Crypto/News/Index.cshtml.cs
  8. 64 0
      Admin/Pages/Crypto/News/Write.cshtml
  9. 64 0
      Admin/Pages/Crypto/News/Write.cshtml.cs
  10. 16 1
      Admin/using.cs
  11. 5 0
      Application/Abstractions/Data/IAppDbContext.cs
  12. 6 0
      Application/Abstractions/News/IRssCollector.cs
  13. 6 0
      Application/Features/Admin/Crypto/News/Article/Delete/Command.cs
  14. 19 0
      Application/Features/Admin/Crypto/News/Article/Delete/Handler.cs
  15. 59 0
      Application/Features/Admin/Crypto/News/Article/Search/Handler.cs
  16. 11 0
      Application/Features/Admin/Crypto/News/Article/Search/Query.cs
  17. 16 0
      Application/Features/Admin/Crypto/News/Article/Search/Response.cs
  18. 11 0
      Application/Features/Admin/Crypto/News/Source/Create/Command.cs
  19. 17 0
      Application/Features/Admin/Crypto/News/Source/Create/Handler.cs
  20. 6 0
      Application/Features/Admin/Crypto/News/Source/Delete/Command.cs
  21. 19 0
      Application/Features/Admin/Crypto/News/Source/Delete/Handler.cs
  22. 6 0
      Application/Features/Admin/Crypto/News/Source/ForceFetch/Command.cs
  23. 13 0
      Application/Features/Admin/Crypto/News/Source/ForceFetch/Handler.cs
  24. 31 0
      Application/Features/Admin/Crypto/News/Source/Get/Handler.cs
  25. 6 0
      Application/Features/Admin/Crypto/News/Source/Get/Query.cs
  26. 14 0
      Application/Features/Admin/Crypto/News/Source/Get/Response.cs
  27. 50 0
      Application/Features/Admin/Crypto/News/Source/Search/Handler.cs
  28. 9 0
      Application/Features/Admin/Crypto/News/Source/Search/Query.cs
  29. 17 0
      Application/Features/Admin/Crypto/News/Source/Search/Response.cs
  30. 13 0
      Application/Features/Admin/Crypto/News/Source/Update/Command.cs
  31. 22 0
      Application/Features/Admin/Crypto/News/Source/Update/Handler.cs
  32. 37 0
      Application/Features/Api/News/GetArticle/Handler.cs
  33. 6 0
      Application/Features/Api/News/GetArticle/Query.cs
  34. 19 0
      Application/Features/Api/News/GetArticle/Response.cs
  35. 50 0
      Application/Features/Api/News/GetArticles/Handler.cs
  36. 10 0
      Application/Features/Api/News/GetArticles/Query.cs
  37. 20 0
      Application/Features/Api/News/GetArticles/Response.cs
  38. 15 0
      Application/Features/Api/News/GetSources/Handler.cs
  39. 5 0
      Application/Features/Api/News/GetSources/Query.cs
  40. 13 0
      Application/Features/Api/News/GetSources/Response.cs
  41. 106 0
      Domain/Entities/News/RssFeedSource.cs
  42. 95 0
      Domain/Entities/News/RssNewsArticle.cs
  43. 18 2
      Infrastructure/DependencyInjection.cs
  44. 220 0
      Infrastructure/News/RssCollectorService.cs
  45. 5 0
      Infrastructure/Persistence/AppDbContext.cs
  46. 28 0
      Infrastructure/Persistence/Configurations/News/RssFeedSourceConfiguration.cs
  47. 33 0
      Infrastructure/Persistence/Configurations/News/RssNewsArticleConfiguration.cs
  48. 6392 0
      Infrastructure/Persistence/Migrations/20260313180453_AddRssNewsTables.Designer.cs
  49. 111 0
      Infrastructure/Persistence/Migrations/20260313180453_AddRssNewsTables.cs
  50. 160 0
      Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs
  51. 7 0
      SharedKernel/Constants/Menus.cs
  52. 27 0
      Web.Api/Endpoints/News/Article.cs
  53. 26 0
      Web.Api/Endpoints/News/Articles.cs
  54. 20 0
      Web.Api/Endpoints/News/Sources.cs
  55. 133 0
      Web.Api/postman/News API.postman_collection.json

+ 1 - 1
Admin/Pages/Crypto/List/Index.cshtml

@@ -182,7 +182,7 @@
                             <td class="text-center @(row.IsDelisted == 'Y' ? "text-warning" : "text-muted")">@row.IsDelisted</td>
                             <td>@row.CreatedAt</td>
                             <td>
-                                <div class="d-grid gap-2 d-block d-xxl-inline">
+                                <div class="d-xl-flex gap-2 justify-content-center d-grid">
                                     <a class="btn btn-sm btn-outline-info" href="@row.EditURL">편집</a>
                                     <button type="button" class="btn btn-sm btn-outline-danger btn-row-delete" data-id="@row.ID">삭제</button>
                                 </div>

+ 148 - 0
Admin/Pages/Crypto/News/Articles.cshtml

@@ -0,0 +1,148 @@
+@page
+@model Admin.Pages.Crypto.News.ArticlesModel
+@{
+    ViewData["Title"] = "뉴스 관리";
+}
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <ul class="nav nav-tabs mt-2">
+        <li class="nav-item">
+            <a class="nav-link" href="/Crypto/News">RSS 소스</a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link active" href="/Crypto/News/Articles">수집된 기사</a>
+        </li>
+    </ul>
+
+    <div class="row g-2 mb-2 mb-sm-0 mt-3">
+        <div class="col-6 col-sm-4 col-lg-auto">
+            <select name="rssFeedSourceID" id="rssFeedSourceID" class="form-select" form="fAdminSearch">
+                <option value="">소스 전체</option>
+                @foreach (var item in Model.Sources)
+                {
+                    <option value="@item.Value" selected="@(item.Value == Model.Query.RssFeedSourceID?.ToString())">@item.Text</option>
+                }
+            </select>
+        </div>
+        <div class="col col-sm col-lg-auto">
+            <input type="search" name="keyword" class="form-control" value="@Model.Query.Keyword" placeholder="제목, 설명" form="fAdminSearch" />
+        </div>
+        <div class="col-12 col-md-auto text-center">
+            <button type="submit" id="btnSearch" class="btn btn-primary" form="fAdminSearch">검색</button>
+        </div>
+    </div>
+
+    <hr />
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @Model?.Total.ToString("N0")
+        </div>
+        <div class="col-auto">
+            <select name="perPage" id="perPage" class="form-select w-auto d-inline-block" form="fAdminSearch">
+                <option value="10" selected="@(Model.Query.PerPage == 10)">10</option>
+                <option value="20" selected="@(Model.Query.PerPage == 20)">20</option>
+                <option value="50" selected="@(Model.Query.PerPage == 50)">50</option>
+                <option value="100" selected="@(Model.Query.PerPage == 100)">100</option>
+            </select>
+        </div>
+        <div class="col-auto">
+            <button type="button" id="btnListDelete" class="btn btn-danger" form="fAdminList" disabled>삭제</button>
+        </div>
+    </div>
+
+    <div class="table-responsive">
+        <table class="table table-striped table-bordered table-hover mt-3">
+            <colgroup>
+                <col />
+                <col />
+                <col />
+                <col />
+                <col />
+                <col />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th>
+                        <div class="form-check form-check-inline">
+                            <input type="checkbox" id="checkedAll" class="form-check-input" value="1" form="fAdminList" />
+                            <label for="checkedAll">ID</label>
+                        </div>
+                    </th>
+                    <th>소스</th>
+                    <th>제목</th>
+                    <th>작성자</th>
+                    <th>발행일</th>
+                    <th>수집일</th>
+                </tr>
+            </thead>
+            <tbody>
+                @if (Model.List == null || Model.List.Count <= 0)
+                {
+                    <tr>
+                        <td colspan="6">No Data.</td>
+                    </tr>
+                }
+                else
+                {
+                    @foreach (var row in Model.List)
+                    {
+                        <tr>
+                            <td>
+                                <div class="form-check form-check-inline">
+                                    <input type="checkbox" name="ids[]" id="ids_@row.ID" class="form-check-input list-check-box" value="@row.ID" form="fAdminList" />
+                                    <label for="ids_@row.ID">@row.Num</label>
+                                </div>
+                            </td>
+                            <td>@row.FeedSourceName</td>
+                            <td>
+                                @if (!string.IsNullOrEmpty(row.Link))
+                                {
+                                    <a href="@row.Link" target="_blank" rel="noopener">@row.Title</a>
+                                }
+                                else
+                                {
+                                    @row.Title
+                                }
+                            </td>
+                            <td>@(row.Author ?? "-")</td>
+                            <td>@row.PublishedAt</td>
+                            <td>@row.CreatedAt</td>
+                        </tr>
+                    }
+                }
+            </tbody>
+        </table>
+
+        <partial name="_Pagination" model="@Model.Pagination" />
+    </div>
+</div>
+
+<!-- 검색용 폼 -->
+<form id="fAdminSearch" method="get" accept-charset="utf-8">
+    <input type="hidden" name="pageNum" value="@Model.Query.PageNum" />
+</form>
+
+<!-- 목록용 폼 -->
+<form id="fAdminList" method="post" accept-charset="utf-8" asp-page-handler="Delete">
+    @Html.AntiForgeryToken()
+    <input type="hidden" name="pageNum" value="@Model.Query.PageNum" />
+    <input type="hidden" name="perPage" value="@Model.Query.PerPage" />
+    <input type="hidden" name="rssFeedSourceID" value="@Model.Query.RssFeedSourceID" />
+    <input type="hidden" name="keyword" value="@Model.Query.Keyword" />
+</form>
+
+@section Scripts {
+    <script>
+        let searchForm = document.getElementById("fAdminSearch");
+
+        $(document).on("change", "#rssFeedSourceID, #perPage", function () {
+            searchForm.submit();
+        });
+    </script>
+}

+ 100 - 0
Admin/Pages/Crypto/News/Articles.cshtml.cs

@@ -0,0 +1,100 @@
+using SharedKernel.Extensions;
+using SharedKernel.Helpers;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+
+namespace Admin.Pages.Crypto.News
+{
+    public class ArticlesModel(IMediator mediator) : PageModel
+    {
+        [BindProperty(SupportsGet = true)]
+        public QueryParams Query { get; set; } = new();
+
+        public List<SelectListItem> Sources { get; set; } = [];
+
+        public sealed class QueryParams
+        {
+            [Range(1, int.MaxValue)]
+            [DisplayName("페이지 번호")]
+            public int PageNum { get; set; } = 1;
+
+            [Range(1, 100)]
+            [DisplayName("페이지 당 수")]
+            public ushort PerPage { get; set; } = 20;
+
+            [DisplayName("소스")]
+            public int? RssFeedSourceID { get; set; }
+
+            [DisplayName("검색어")]
+            public string? Keyword { get; set; }
+        }
+
+        public int Total { get; set; }
+
+        public List<(
+            int Num,
+            int ID,
+            string FeedSourceName,
+            string Title,
+            string? Author,
+            string? Link,
+            string? PublishedAt,
+            string CreatedAt
+        )> List { get; set; } = [];
+
+        public Pagination? Pagination { get; set; }
+
+        public async Task OnGetAsync(CancellationToken ct)
+        {
+            if (!ModelState.IsValid)
+            {
+                return;
+            }
+
+            var sources = await mediator.Send(new GetAllNewsSources.Query(), ct);
+            Sources = [..sources.Sources.Select(s => new SelectListItem(s.Name, s.ID.ToString()))];
+
+            var result = await mediator.Send(new SearchNewsArticles.Query(
+                Query.RssFeedSourceID,
+                Query.Keyword,
+                Query.PageNum,
+                Query.PerPage
+            ), ct);
+
+            Total = result.Total;
+
+            List = [..result.List.Select(c => (
+                c.Num,
+                c.ID,
+                c.FeedSourceName,
+                c.Title,
+                c.Author,
+                c.Link,
+                c.PublishedAt.GetDateAt() ?? "-",
+                c.CreatedAt.GetDateAt()
+            ))];
+
+            Pagination = new Pagination(result.Total, Query.PageNum, Query.PerPage);
+        }
+
+        public async Task<IActionResult> OnPostDeleteAsync(int[] ids, CancellationToken ct)
+        {
+            try
+            {
+                await mediator.Send(new DeleteNewsArticles.Command(ids), ct);
+
+                TempData["SuccessMessage"] = $"{ids.Length}개 기사가 삭제되었습니다.";
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+            }
+
+            return RedirectToPage("/Crypto/News/Articles", Query);
+        }
+    }
+}

+ 74 - 0
Admin/Pages/Crypto/News/Edit.cshtml

@@ -0,0 +1,74 @@
+@page "{id:int}"
+@model Admin.Pages.Crypto.News.EditModel
+@{
+    ViewData["Title"] = "RSS 소스 수정";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <form id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" class="mt-3">
+        @Html.AntiForgeryToken()
+
+        <div class="row mb-2">
+            <label asp-for="Input.Name" class="col-sm-2 col-form-label">
+                <span class="text-danger">*</span> 이름
+            </label>
+            <div class="col-sm-10">
+                <input asp-for="Input.Name" class="form-control" maxlength="200" required placeholder="CoinSpeaker KR" />
+                <span asp-validation-for="Input.Name" class="text-danger"></span>
+            </div>
+        </div>
+
+        <div class="row mb-2">
+            <label asp-for="Input.Url" class="col-sm-2 col-form-label">
+                <span class="text-danger">*</span> URL
+            </label>
+            <div class="col-sm-10">
+                <input asp-for="Input.Url" class="form-control" maxlength="2048" required placeholder="https://example.com/rss" />
+                <span asp-validation-for="Input.Url" class="text-danger"></span>
+            </div>
+        </div>
+
+        <div class="row mb-2">
+            <label asp-for="Input.Description" class="col-sm-2 col-form-label">설명</label>
+            <div class="col-sm-10">
+                <textarea asp-for="Input.Description" class="form-control" rows="3" maxlength="500" placeholder="소스에 대한 설명"></textarea>
+                <span asp-validation-for="Input.Description" class="text-danger"></span>
+            </div>
+        </div>
+
+        <div class="row mb-2">
+            <label asp-for="Input.IntervalMinutes" class="col-sm-2 col-form-label">
+                <span class="text-danger">*</span> 수집주기(분)
+            </label>
+            <div class="col-sm-10">
+                <input asp-for="Input.IntervalMinutes" type="number" class="form-control" min="1" max="1440" required />
+                <div class="form-text">1~1440분 (기본 10분)</div>
+                <span asp-validation-for="Input.IntervalMinutes" class="text-danger"></span>
+            </div>
+        </div>
+
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">상태</label>
+            <div class="col-sm-10 align-self-center">
+                <div class="form-check">
+                    <input asp-for="Input.IsActive" class="form-check-input" />
+                    <label asp-for="Input.IsActive" 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-success">저장</button>
+            <a href="/Crypto/News" class="btn btn-secondary">취소</a>
+        </div>
+
+        <br />
+    </form>
+</div>

+ 91 - 0
Admin/Pages/Crypto/News/Edit.cshtml.cs

@@ -0,0 +1,91 @@
+using SharedKernel.Extensions;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using System.ComponentModel.DataAnnotations;
+
+namespace Admin.Pages.Crypto.News
+{
+    public class EditModel(IMediator mediator) : PageModel
+    {
+        public int SourceID { get; private set; }
+
+        [BindProperty]
+        public InputModel Input { get; set; } = new();
+
+        public sealed class InputModel
+        {
+            [Required(ErrorMessage = "이름은 필수입니다.")]
+            [StringLength(200, ErrorMessage = "이름은 {1}자 이하로 입력하세요.")]
+            public string Name { get; set; } = null!;
+
+            [Required(ErrorMessage = "URL은 필수입니다.")]
+            [StringLength(2048, ErrorMessage = "URL은 {1}자 이하로 입력하세요.")]
+            [DataType(DataType.Url)]
+            public string Url { get; set; } = null!;
+
+            [StringLength(500, ErrorMessage = "설명은 {1}자 이하로 입력하세요.")]
+            public string? Description { get; set; }
+
+            [Range(1, 1440, ErrorMessage = "수집주기는 1~1440분 사이로 입력하세요.")]
+            public int IntervalMinutes { get; set; } = 10;
+
+            public bool IsActive { get; set; } = true;
+        }
+
+        public async Task<IActionResult> OnGetAsync(int id, CancellationToken ct)
+        {
+            try
+            {
+                var source = await mediator.Send(new GetNewsSource.Query(id), ct);
+
+                SourceID = source.ID;
+
+                Input = new InputModel
+                {
+                    Name = source.Name,
+                    Url = source.Url,
+                    Description = source.Description,
+                    IntervalMinutes = source.IntervalMinutes,
+                    IsActive = source.IsActive
+                };
+
+                return Page();
+            }
+            catch (KeyNotFoundException)
+            {
+                return NotFound();
+            }
+        }
+
+        public async Task<IActionResult> OnPostAsync(int id, CancellationToken ct)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception(ModelState.GetErrorMessages());
+                }
+
+                await mediator.Send(new UpdateNewsSource.Command(
+                    id,
+                    Input.Name,
+                    Input.Url,
+                    Input.Description,
+                    Input.IntervalMinutes,
+                    Input.IsActive
+                ), ct);
+
+                TempData["SuccessMessage"] = $"{Input.Name} 소스가 수정되었습니다.";
+
+                return RedirectToPage("/Crypto/News/Edit", new { id });
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+
+                return RedirectToPage("/Crypto/News/Edit", new { id });
+            }
+        }
+    }
+}

+ 134 - 0
Admin/Pages/Crypto/News/Index.cshtml

@@ -0,0 +1,134 @@
+@page
+@model Admin.Pages.Crypto.News.IndexModel
+@{
+    ViewData["Title"] = "뉴스 관리";
+}
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <ul class="nav nav-tabs mt-2">
+        <li class="nav-item">
+            <a class="nav-link active" href="/Crypto/News">RSS 소스</a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link" href="/Crypto/News/Articles">수집된 기사</a>
+        </li>
+    </ul>
+
+    <div class="row g-2 align-items-end mt-3">
+        <div class="col">
+            Total : @Model?.Total.ToString("N0")
+        </div>
+        <div class="col-auto">
+            <select name="perPage" id="perPage" class="form-select w-auto d-inline-block" form="fAdminSearch">
+                <option value="10" selected="@(Model.Query.PerPage == 10)">10</option>
+                <option value="20" selected="@(Model.Query.PerPage == 20)">20</option>
+                <option value="50" selected="@(Model.Query.PerPage == 50)">50</option>
+                <option value="100" selected="@(Model.Query.PerPage == 100)">100</option>
+            </select>
+        </div>
+        <div class="col-auto">
+            <button type="button" id="btnListDelete" class="btn btn-danger" form="fAdminList" disabled>삭제</button>
+            <a class="btn btn-success" asp-page="/Crypto/News/Write">추가</a>
+        </div>
+    </div>
+
+    <div class="table-responsive">
+        <table class="table table-striped table-bordered table-hover mt-3">
+            <colgroup>
+                <col />
+                <col />
+                <col />
+                <col />
+                <col />
+                <col />
+                <col />
+                <col />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th>
+                        <div class="form-check form-check-inline">
+                            <input type="checkbox" id="checkedAll" class="form-check-input" value="1" form="fAdminList" />
+                            <label for="checkedAll">ID</label>
+                        </div>
+                    </th>
+                    <th>이름</th>
+                    <th>URL</th>
+                    <th>수집주기(분)</th>
+                    <th>활성</th>
+                    <th>마지막 수집</th>
+                    <th>등록일시</th>
+                    <th>관리</th>
+                </tr>
+            </thead>
+            <tbody>
+                @if (Model.List == null || Model.List.Count <= 0)
+                {
+                    <tr>
+                        <td colspan="8">No Data.</td>
+                    </tr>
+                }
+                else
+                {
+                    @foreach (var row in Model.List)
+                    {
+                        <tr>
+                            <td>
+                                <div class="form-check form-check-inline">
+                                    <input type="checkbox" name="ids[]" id="ids_@row.ID" class="form-check-input list-check-box" value="@row.ID" form="fAdminList" />
+                                    <label for="ids_@row.ID">@row.Num</label>
+                                </div>
+                            </td>
+                            <td>@row.Name</td>
+                            <td><a href="@row.Url" target="_blank" rel="noopener">@row.Url</a></td>
+                            <td class="text-center">@row.IntervalMinutes</td>
+                            <td class="text-center @(row.IsActive == 'Y' ? "text-success" : "text-muted")">@row.IsActive</td>
+                            <td>@row.LastFetchedAt</td>
+                            <td>@row.CreatedAt</td>
+                            <td>
+                                <div class="d-xl-flex gap-2 justify-content-center d-grid">
+                                    <a class="btn btn-sm btn-outline-info" href="@row.EditURL">편집</a>
+                                    <form method="post" asp-page-handler="ForceFetch" class="d-inline">
+                                        @Html.AntiForgeryToken()
+                                        <input type="hidden" name="id" value="@row.ID" />
+                                        <button type="submit" class="btn btn-sm btn-outline-warning" onclick="return confirm('이 소스를 강제 수집하시겠습니까?')">수집</button>
+                                    </form>
+                                    <button type="button" class="btn btn-sm btn-outline-danger btn-row-delete" data-id="@row.ID">삭제</button>
+                                </div>
+                            </td>
+                        </tr>
+                    }
+                }
+            </tbody>
+        </table>
+
+        <partial name="_Pagination" model="@Model.Pagination" />
+    </div>
+</div>
+
+<!-- 검색용 폼 -->
+<form id="fAdminSearch" method="get" accept-charset="utf-8">
+    <input type="hidden" name="pageNum" value="@Model.Query.PageNum" />
+</form>
+
+<!-- 목록용 폼 -->
+<form id="fAdminList" method="post" accept-charset="utf-8" asp-page-handler="Delete">
+    @Html.AntiForgeryToken()
+    <input type="hidden" name="pageNum" value="@Model.Query.PageNum" />
+    <input type="hidden" name="perPage" value="@Model.Query.PerPage" />
+</form>
+
+@section Scripts {
+    <script>
+        let searchForm = document.getElementById("fAdminSearch");
+
+        $(document).on("change", "#perPage", function () {
+            searchForm.submit();
+        });
+    </script>
+}

+ 106 - 0
Admin/Pages/Crypto/News/Index.cshtml.cs

@@ -0,0 +1,106 @@
+using SharedKernel.Extensions;
+using SharedKernel.Helpers;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+
+namespace Admin.Pages.Crypto.News
+{
+    public class IndexModel(IMediator mediator) : PageModel
+    {
+        [BindProperty(SupportsGet = true)]
+        public QueryParams Query { get; set; } = new();
+
+        public sealed class QueryParams
+        {
+            [Range(1, int.MaxValue)]
+            [DisplayName("페이지 번호")]
+            public int PageNum { get; set; } = 1;
+
+            [Range(1, 100)]
+            [DisplayName("페이지 당 수")]
+            public ushort PerPage { get; set; } = 20;
+        }
+
+        public int Total { get; set; }
+
+        public List<(
+            int Num,
+            int ID,
+            string Name,
+            string Url,
+            string? Description,
+            int IntervalMinutes,
+            char IsActive,
+            string? LastFetchedAt,
+            string CreatedAt,
+            string EditURL
+        )> List { get; set; } = [];
+
+        public Pagination? Pagination { get; set; }
+
+        public async Task OnGetAsync(CancellationToken ct)
+        {
+            if (!ModelState.IsValid)
+            {
+                return;
+            }
+
+            var result = await mediator.Send(new SearchNewsSources.Query(
+                Query.PageNum,
+                Query.PerPage
+            ), ct);
+
+            Total = result.Total;
+
+            List = [..result.List.Select(c => (
+                c.Num,
+                c.ID,
+                c.Name,
+                c.Url,
+                c.Description,
+                c.IntervalMinutes,
+                c.IsActive ? 'Y' : 'N',
+                c.LastFetchedAt.GetDateAt() ?? "-",
+                c.CreatedAt.GetDateAt(),
+                EditURL: $"/Crypto/News/Edit/{c.ID}{Request.QueryString}"
+            ))];
+
+            Pagination = new Pagination(result.Total, Query.PageNum, Query.PerPage);
+        }
+
+        public async Task<IActionResult> OnPostDeleteAsync(int[] ids, CancellationToken ct)
+        {
+            try
+            {
+                await mediator.Send(new DeleteNewsSource.Command(ids), ct);
+
+                TempData["SuccessMessage"] = $"{ids.Length}개 소스가 삭제되었습니다.";
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+            }
+
+            return RedirectToPage("/Crypto/News/Index", Query);
+        }
+
+        public async Task<IActionResult> OnPostForceFetchAsync(int id, CancellationToken ct)
+        {
+            try
+            {
+                var count = await mediator.Send(new ForceFetchNewsSource.Command(id), ct);
+
+                TempData["SuccessMessage"] = $"강제 수집 완료: {count}건 수집되었습니다.";
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+            }
+
+            return RedirectToPage("/Crypto/News/Index", Query);
+        }
+    }
+}

+ 64 - 0
Admin/Pages/Crypto/News/Write.cshtml

@@ -0,0 +1,64 @@
+@page
+@model Admin.Pages.Crypto.News.WriteModel
+@{
+    ViewData["Title"] = "RSS 소스 등록";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <form id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" class="mt-3">
+        @Html.AntiForgeryToken()
+
+        <div class="row mb-2">
+            <label asp-for="Input.Name" class="col-sm-2 col-form-label">
+                <span class="text-danger">*</span> 이름
+            </label>
+            <div class="col-sm-10">
+                <input asp-for="Input.Name" class="form-control" maxlength="200" required placeholder="CoinSpeaker KR" />
+                <span asp-validation-for="Input.Name" class="text-danger"></span>
+            </div>
+        </div>
+
+        <div class="row mb-2">
+            <label asp-for="Input.Url" class="col-sm-2 col-form-label">
+                <span class="text-danger">*</span> URL
+            </label>
+            <div class="col-sm-10">
+                <input asp-for="Input.Url" class="form-control" maxlength="2048" required placeholder="https://example.com/rss" />
+                <span asp-validation-for="Input.Url" class="text-danger"></span>
+            </div>
+        </div>
+
+        <div class="row mb-2">
+            <label asp-for="Input.Description" class="col-sm-2 col-form-label">설명</label>
+            <div class="col-sm-10">
+                <textarea asp-for="Input.Description" class="form-control" rows="3" maxlength="500" placeholder="소스에 대한 설명"></textarea>
+                <span asp-validation-for="Input.Description" class="text-danger"></span>
+            </div>
+        </div>
+
+        <div class="row mb-2">
+            <label asp-for="Input.IntervalMinutes" class="col-sm-2 col-form-label">
+                <span class="text-danger">*</span> 수집주기(분)
+            </label>
+            <div class="col-sm-10">
+                <input asp-for="Input.IntervalMinutes" type="number" class="form-control" min="1" max="1440" required />
+                <div class="form-text">1~1440분 (기본 10분)</div>
+                <span asp-validation-for="Input.IntervalMinutes" class="text-danger"></span>
+            </div>
+        </div>
+
+        <hr />
+
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-success">저장</button>
+            <a href="/Crypto/News" class="btn btn-secondary">취소</a>
+        </div>
+
+        <br />
+    </form>
+</div>

+ 64 - 0
Admin/Pages/Crypto/News/Write.cshtml.cs

@@ -0,0 +1,64 @@
+using SharedKernel.Extensions;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using System.ComponentModel.DataAnnotations;
+
+namespace Admin.Pages.Crypto.News
+{
+    public class WriteModel(IMediator mediator) : PageModel
+    {
+        [BindProperty]
+        public InputModel Input { get; set; } = new();
+
+        public sealed class InputModel
+        {
+            [Required(ErrorMessage = "이름은 필수입니다.")]
+            [StringLength(200, ErrorMessage = "이름은 {1}자 이하로 입력하세요.")]
+            public string Name { get; set; } = null!;
+
+            [Required(ErrorMessage = "URL은 필수입니다.")]
+            [StringLength(2048, ErrorMessage = "URL은 {1}자 이하로 입력하세요.")]
+            [DataType(DataType.Url)]
+            public string Url { get; set; } = null!;
+
+            [StringLength(500, ErrorMessage = "설명은 {1}자 이하로 입력하세요.")]
+            public string? Description { get; set; }
+
+            [Range(1, 1440, ErrorMessage = "수집주기는 1~1440분 사이로 입력하세요.")]
+            public int IntervalMinutes { get; set; } = 10;
+        }
+
+        public void OnGet()
+        {
+        }
+
+        public async Task<IActionResult> OnPostAsync(CancellationToken ct)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception(ModelState.GetErrorMessages());
+                }
+
+                await mediator.Send(new CreateNewsSource.Command(
+                    Input.Name,
+                    Input.Url,
+                    Input.Description,
+                    Input.IntervalMinutes
+                ), ct);
+
+                TempData["SuccessMessage"] = $"{Input.Name} 소스가 등록되었습니다.";
+
+                return RedirectToPage("/Crypto/News/Index");
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+
+                return RedirectToPage("/Crypto/News/Write");
+            }
+        }
+    }
+}

+ 16 - 1
Admin/using.cs

@@ -219,4 +219,19 @@ global using SaveCoinCuration = Application.Features.Admin.Crypto.Curation.Save;
 
 // 코인 시세 설정
 global using GetTickerConfig = Application.Features.Admin.Crypto.TickerConfig.Get;
-global using SaveTickerConfig = Application.Features.Admin.Crypto.TickerConfig.Save;
+global using SaveTickerConfig = Application.Features.Admin.Crypto.TickerConfig.Save;
+
+// 뉴스 소스
+global using SearchNewsSources = Application.Features.Admin.Crypto.News.Source.Search;
+global using GetNewsSource = Application.Features.Admin.Crypto.News.Source.Get;
+global using CreateNewsSource = Application.Features.Admin.Crypto.News.Source.Create;
+global using UpdateNewsSource = Application.Features.Admin.Crypto.News.Source.Update;
+global using DeleteNewsSource = Application.Features.Admin.Crypto.News.Source.Delete;
+global using ForceFetchNewsSource = Application.Features.Admin.Crypto.News.Source.ForceFetch;
+
+// 뉴스 기사
+global using SearchNewsArticles = Application.Features.Admin.Crypto.News.Article.Search;
+global using DeleteNewsArticles = Application.Features.Admin.Crypto.News.Article.Delete;
+
+// 뉴스 소스 (API)
+global using GetAllNewsSources = Application.Features.Api.News.GetSources;

+ 5 - 0
Application/Abstractions/Data/IAppDbContext.cs

@@ -13,6 +13,7 @@ using Domain.Entities.Forum.Comments;
 using Domain.Entities.Director;
 using Domain.Entities.EmailVerification;
 using Domain.Entities.Forum.Logs;
+using Domain.Entities.News;
 using Domain.Entities.Page.Popup;
 
 namespace Application.Abstractions.Data
@@ -90,6 +91,10 @@ namespace Application.Abstractions.Data
         DbSet<CommentReport> CommentReport { get; set; }
         DbSet<CommentMention> CommentMention { get; set; }
 
+        // 뉴스
+        DbSet<RssFeedSource> RssFeedSource { get; set; }
+        DbSet<RssNewsArticle> RssNewsArticle { get; set; }
+
         // 게시글/댓글 로그
         DbSet<PostUpdateLog> PostUpdateLog { get; set; }
         DbSet<PostFileDownLog> PostFileDownLog { get; set; }

+ 6 - 0
Application/Abstractions/News/IRssCollector.cs

@@ -0,0 +1,6 @@
+namespace Application.Abstractions.News;
+
+public interface IRssCollector
+{
+    Task<int> FetchSourceAsync(int sourceID, CancellationToken ct);
+}

+ 6 - 0
Application/Features/Admin/Crypto/News/Article/Delete/Command.cs

@@ -0,0 +1,6 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Admin.Crypto.News.Article.Delete
+{
+    public sealed record Command(int[] IDs) : ICommand;
+}

+ 19 - 0
Application/Features/Admin/Crypto/News/Article/Delete/Handler.cs

@@ -0,0 +1,19 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Admin.Crypto.News.Article.Delete
+{
+    public sealed class Handler(IAppDbContext db) : ICommandHandler<Command>
+    {
+        public async Task Handle(Command request, CancellationToken ct)
+        {
+            if (request.IDs is null || request.IDs.Length == 0)
+            {
+                return;
+            }
+
+            await db.RssNewsArticle.Where(c => request.IDs.Contains(c.ID)).ExecuteDeleteAsync(ct);
+        }
+    }
+}

+ 59 - 0
Application/Features/Admin/Crypto/News/Article/Search/Handler.cs

@@ -0,0 +1,59 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Admin.Crypto.News.Article.Search
+{
+    public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
+    {
+        public async Task<Response> Handle(Query request, CancellationToken ct)
+        {
+            var query = db.RssNewsArticle.AsNoTracking().Include(x => x.RssFeedSource).AsQueryable();
+
+            if (request.RssFeedSourceID.HasValue)
+            {
+                query = query.Where(x => x.RssFeedSourceID == request.RssFeedSourceID.Value);
+            }
+
+            if (!string.IsNullOrWhiteSpace(request.Keyword))
+            {
+                var kw = request.Keyword.Trim();
+                query = query.Where(x => x.Title.Contains(kw) || (x.Description != null && x.Description.Contains(kw)));
+            }
+
+            var total = await query.CountAsync(ct);
+
+            var list = await query
+                .OrderByDescending(x => x.PublishedAt ?? x.CreatedAt)
+                .Skip((request.PageNum - 1) * request.PerPage)
+                .Take(request.PerPage)
+                .Select(x => new
+                {
+                    x.ID,
+                    FeedSourceName = x.RssFeedSource.Name,
+                    x.Title,
+                    x.Author,
+                    x.Link,
+                    x.PublishedAt,
+                    x.CreatedAt
+                })
+                .ToListAsync(ct);
+
+            var startNum = total - ((request.PageNum - 1) * request.PerPage);
+
+            return new Response(
+                total,
+                [..list.Select((c, i) => new Response.Row(
+                    Num: startNum - i,
+                    c.ID,
+                    c.FeedSourceName,
+                    c.Title,
+                    c.Author,
+                    c.Link,
+                    c.PublishedAt,
+                    c.CreatedAt
+                ))]
+            );
+        }
+    }
+}

+ 11 - 0
Application/Features/Admin/Crypto/News/Article/Search/Query.cs

@@ -0,0 +1,11 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Admin.Crypto.News.Article.Search
+{
+    public sealed record Query(
+        int? RssFeedSourceID,
+        string? Keyword,
+        int PageNum,
+        ushort PerPage
+    ) : IQuery<Response>;
+}

+ 16 - 0
Application/Features/Admin/Crypto/News/Article/Search/Response.cs

@@ -0,0 +1,16 @@
+namespace Application.Features.Admin.Crypto.News.Article.Search
+{
+    public sealed record Response(int Total, List<Response.Row> List)
+    {
+        public sealed record Row(
+            int Num,
+            int ID,
+            string FeedSourceName,
+            string Title,
+            string? Author,
+            string? Link,
+            DateTime? PublishedAt,
+            DateTime CreatedAt
+        );
+    }
+}

+ 11 - 0
Application/Features/Admin/Crypto/News/Source/Create/Command.cs

@@ -0,0 +1,11 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Admin.Crypto.News.Source.Create
+{
+    public sealed record Command(
+        string Name,
+        string Url,
+        string? Description,
+        int IntervalMinutes
+    ) : ICommand;
+}

+ 17 - 0
Application/Features/Admin/Crypto/News/Source/Create/Handler.cs

@@ -0,0 +1,17 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Domain.Entities.News;
+
+namespace Application.Features.Admin.Crypto.News.Source.Create
+{
+    public sealed class Handler(IAppDbContext db) : ICommandHandler<Command>
+    {
+        public async Task Handle(Command request, CancellationToken ct)
+        {
+            var source = RssFeedSource.Create(request.Name, request.Url, request.Description, request.IntervalMinutes);
+
+            db.RssFeedSource.Add(source);
+            await db.SaveChangesAsync(ct);
+        }
+    }
+}

+ 6 - 0
Application/Features/Admin/Crypto/News/Source/Delete/Command.cs

@@ -0,0 +1,6 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Admin.Crypto.News.Source.Delete
+{
+    public sealed record Command(int[] IDs) : ICommand;
+}

+ 19 - 0
Application/Features/Admin/Crypto/News/Source/Delete/Handler.cs

@@ -0,0 +1,19 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Admin.Crypto.News.Source.Delete
+{
+    public sealed class Handler(IAppDbContext db) : ICommandHandler<Command>
+    {
+        public async Task Handle(Command request, CancellationToken ct)
+        {
+            if (request.IDs is null || request.IDs.Length == 0)
+            {
+                return;
+            }
+
+            await db.RssFeedSource.Where(c => request.IDs.Contains(c.ID)).ExecuteDeleteAsync(ct);
+        }
+    }
+}

+ 6 - 0
Application/Features/Admin/Crypto/News/Source/ForceFetch/Command.cs

@@ -0,0 +1,6 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Admin.Crypto.News.Source.ForceFetch
+{
+    public sealed record Command(int ID) : ICommand<int>;
+}

+ 13 - 0
Application/Features/Admin/Crypto/News/Source/ForceFetch/Handler.cs

@@ -0,0 +1,13 @@
+using Application.Abstractions.Messaging;
+using Application.Abstractions.News;
+
+namespace Application.Features.Admin.Crypto.News.Source.ForceFetch
+{
+    public sealed class Handler(IRssCollector rssCollector) : ICommandHandler<Command, int>
+    {
+        public async Task<int> Handle(Command request, CancellationToken ct)
+        {
+            return await rssCollector.FetchSourceAsync(request.ID, ct);
+        }
+    }
+}

+ 31 - 0
Application/Features/Admin/Crypto/News/Source/Get/Handler.cs

@@ -0,0 +1,31 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Admin.Crypto.News.Source.Get
+{
+    public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
+    {
+        public async Task<Response> Handle(Query request, CancellationToken ct)
+        {
+            var item = await db.RssFeedSource.AsNoTracking().FirstOrDefaultAsync(x => x.ID == request.ID, ct);
+
+            if (item is null)
+            {
+                throw new KeyNotFoundException($"RSS 피드 소스를 찾을 수 없습니다. (ID: {request.ID})");
+            }
+
+            return new Response(
+                item.ID,
+                item.Name,
+                item.Url,
+                item.Description,
+                item.IntervalMinutes,
+                item.IsActive,
+                item.LastFetchedAt,
+                item.UpdatedAt,
+                item.CreatedAt
+            );
+        }
+    }
+}

+ 6 - 0
Application/Features/Admin/Crypto/News/Source/Get/Query.cs

@@ -0,0 +1,6 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Admin.Crypto.News.Source.Get
+{
+    public sealed record Query(int ID) : IQuery<Response>;
+}

+ 14 - 0
Application/Features/Admin/Crypto/News/Source/Get/Response.cs

@@ -0,0 +1,14 @@
+namespace Application.Features.Admin.Crypto.News.Source.Get
+{
+    public sealed record Response(
+        int ID,
+        string Name,
+        string Url,
+        string? Description,
+        int IntervalMinutes,
+        bool IsActive,
+        DateTime? LastFetchedAt,
+        DateTime? UpdatedAt,
+        DateTime CreatedAt
+    );
+}

+ 50 - 0
Application/Features/Admin/Crypto/News/Source/Search/Handler.cs

@@ -0,0 +1,50 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Admin.Crypto.News.Source.Search
+{
+    public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
+    {
+        public async Task<Response> Handle(Query request, CancellationToken ct)
+        {
+            var query = db.RssFeedSource.AsNoTracking().AsQueryable();
+
+            var total = await query.CountAsync(ct);
+
+            var list = await query
+                .OrderByDescending(c => c.ID)
+                .Skip((request.PageNum - 1) * request.PerPage)
+                .Take(request.PerPage)
+                .Select(c => new
+                {
+                    c.ID,
+                    c.Name,
+                    c.Url,
+                    c.Description,
+                    c.IntervalMinutes,
+                    c.IsActive,
+                    c.LastFetchedAt,
+                    c.CreatedAt
+                })
+                .ToListAsync(ct);
+
+            var startNum = total - ((request.PageNum - 1) * request.PerPage);
+
+            return new Response(
+                total,
+                [..list.Select((c, i) => new Response.Row(
+                    Num: startNum - i,
+                    c.ID,
+                    c.Name,
+                    c.Url,
+                    c.Description,
+                    c.IntervalMinutes,
+                    c.IsActive,
+                    c.LastFetchedAt,
+                    c.CreatedAt
+                ))]
+            );
+        }
+    }
+}

+ 9 - 0
Application/Features/Admin/Crypto/News/Source/Search/Query.cs

@@ -0,0 +1,9 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Admin.Crypto.News.Source.Search
+{
+    public sealed record Query(
+        int PageNum,
+        ushort PerPage
+    ) : IQuery<Response>;
+}

+ 17 - 0
Application/Features/Admin/Crypto/News/Source/Search/Response.cs

@@ -0,0 +1,17 @@
+namespace Application.Features.Admin.Crypto.News.Source.Search
+{
+    public sealed record Response(int Total, List<Response.Row> List)
+    {
+        public sealed record Row(
+            int Num,
+            int ID,
+            string Name,
+            string Url,
+            string? Description,
+            int IntervalMinutes,
+            bool IsActive,
+            DateTime? LastFetchedAt,
+            DateTime CreatedAt
+        );
+    }
+}

+ 13 - 0
Application/Features/Admin/Crypto/News/Source/Update/Command.cs

@@ -0,0 +1,13 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Admin.Crypto.News.Source.Update
+{
+    public sealed record Command(
+        int ID,
+        string Name,
+        string Url,
+        string? Description,
+        int IntervalMinutes,
+        bool IsActive
+    ) : ICommand;
+}

+ 22 - 0
Application/Features/Admin/Crypto/News/Source/Update/Handler.cs

@@ -0,0 +1,22 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Admin.Crypto.News.Source.Update
+{
+    public sealed class Handler(IAppDbContext db) : ICommandHandler<Command>
+    {
+        public async Task Handle(Command request, CancellationToken ct)
+        {
+            var source = await db.RssFeedSource.FirstOrDefaultAsync(x => x.ID == request.ID, ct);
+
+            if (source is null)
+            {
+                throw new KeyNotFoundException($"RSS 피드 소스를 찾을 수 없습니다. (ID: {request.ID})");
+            }
+
+            source.Update(request.Name, request.Url, request.Description, request.IntervalMinutes, request.IsActive);
+            await db.SaveChangesAsync(ct);
+        }
+    }
+}

+ 37 - 0
Application/Features/Api/News/GetArticle/Handler.cs

@@ -0,0 +1,37 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+using SharedKernel.Results;
+
+namespace Application.Features.Api.News.GetArticle;
+
+public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Result<Response>>
+{
+    public async Task<Result<Response>> Handle(Query request, CancellationToken ct)
+    {
+        var item = await db.RssNewsArticle.AsNoTracking().Include(x => x.RssFeedSource).FirstOrDefaultAsync(x => x.ID == request.ID, ct);
+
+        if (item is null)
+        {
+            return Result.Failure<Response>(Error.NotFound("News.NotFound", "뉴스 기사를 찾을 수 없습니다."));
+        }
+
+        return new Response(
+            item.ID,
+            item.RssFeedSourceID,
+            item.RssFeedSource.Name,
+            item.Title,
+            item.Link,
+            item.Guid,
+            item.Author,
+            item.Description,
+            item.Content,
+            item.ImageUrl,
+            item.SourceName,
+            item.Categories,
+            item.CommentCount,
+            item.PublishedAt,
+            item.CreatedAt
+        );
+    }
+}

+ 6 - 0
Application/Features/Api/News/GetArticle/Query.cs

@@ -0,0 +1,6 @@
+using Application.Abstractions.Messaging;
+using SharedKernel.Results;
+
+namespace Application.Features.Api.News.GetArticle;
+
+public sealed record Query(int ID) : IQuery<Result<Response>>;

+ 19 - 0
Application/Features/Api/News/GetArticle/Response.cs

@@ -0,0 +1,19 @@
+namespace Application.Features.Api.News.GetArticle;
+
+public sealed record Response(
+    int ID,
+    int RssFeedSourceID,
+    string FeedSourceName,
+    string Title,
+    string? Link,
+    string? Guid,
+    string? Author,
+    string? Description,
+    string? Content,
+    string? ImageUrl,
+    string? SourceName,
+    string? Categories,
+    int CommentCount,
+    DateTime? PublishedAt,
+    DateTime CreatedAt
+);

+ 50 - 0
Application/Features/Api/News/GetArticles/Handler.cs

@@ -0,0 +1,50 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Api.News.GetArticles;
+
+public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
+{
+    public async Task<Response> Handle(Query request, CancellationToken ct)
+    {
+        var query = db.RssNewsArticle.AsNoTracking().Include(x => x.RssFeedSource).AsQueryable();
+
+        if (request.RssFeedSourceID.HasValue)
+        {
+            query = query.Where(x => x.RssFeedSourceID == request.RssFeedSourceID.Value);
+        }
+
+        if (!string.IsNullOrWhiteSpace(request.Keyword))
+        {
+            var kw = request.Keyword.Trim();
+            query = query.Where(x => x.Title.Contains(kw) || (x.Description != null && x.Description.Contains(kw)));
+        }
+
+        query = query.OrderByDescending(x => x.PublishedAt ?? x.CreatedAt);
+
+        var total = await query.CountAsync(ct);
+
+        var list = await query
+            .Skip((request.Page - 1) * request.PerPage)
+            .Take(request.PerPage)
+            .Select(x => new Response.Row(
+                x.ID,
+                x.RssFeedSourceID,
+                x.RssFeedSource.Name,
+                x.Title,
+                x.Link,
+                x.Author,
+                x.Description,
+                x.ImageUrl,
+                x.SourceName,
+                x.Categories,
+                x.CommentCount,
+                x.PublishedAt,
+                x.CreatedAt
+            ))
+            .ToListAsync(ct);
+
+        return new Response(total, list);
+    }
+}

+ 10 - 0
Application/Features/Api/News/GetArticles/Query.cs

@@ -0,0 +1,10 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Api.News.GetArticles;
+
+public sealed record Query(
+    int? RssFeedSourceID,
+    string? Keyword,
+    int Page,
+    ushort PerPage
+) : IQuery<Response>;

+ 20 - 0
Application/Features/Api/News/GetArticles/Response.cs

@@ -0,0 +1,20 @@
+namespace Application.Features.Api.News.GetArticles;
+
+public sealed record Response(int Total, List<Response.Row> List)
+{
+    public sealed record Row(
+        int ID,
+        int RssFeedSourceID,
+        string FeedSourceName,
+        string Title,
+        string? Link,
+        string? Author,
+        string? Description,
+        string? ImageUrl,
+        string? SourceName,
+        string? Categories,
+        int CommentCount,
+        DateTime? PublishedAt,
+        DateTime CreatedAt
+    );
+}

+ 15 - 0
Application/Features/Api/News/GetSources/Handler.cs

@@ -0,0 +1,15 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.Messaging;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Api.News.GetSources;
+
+public sealed class Handler(IAppDbContext db) : IQueryHandler<Query, Response>
+{
+    public async Task<Response> Handle(Query request, CancellationToken ct)
+    {
+        var sources = await db.RssFeedSource.AsNoTracking().OrderBy(x => x.ID).Select(x => new Response.Row(x.ID, x.Name, x.Url, x.Description, x.IsActive, x.LastFetchedAt)).ToListAsync(ct);
+
+        return new Response(sources);
+    }
+}

+ 5 - 0
Application/Features/Api/News/GetSources/Query.cs

@@ -0,0 +1,5 @@
+using Application.Abstractions.Messaging;
+
+namespace Application.Features.Api.News.GetSources;
+
+public sealed record Query : IQuery<Response>;

+ 13 - 0
Application/Features/Api/News/GetSources/Response.cs

@@ -0,0 +1,13 @@
+namespace Application.Features.Api.News.GetSources;
+
+public sealed record Response(List<Response.Row> Sources)
+{
+    public sealed record Row(
+        int ID,
+        string Name,
+        string Url,
+        string? Description,
+        bool IsActive,
+        DateTime? LastFetchedAt
+    );
+}

+ 106 - 0
Domain/Entities/News/RssFeedSource.cs

@@ -0,0 +1,106 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Domain.Entities.News
+{
+    public class RssFeedSource
+    {
+        public virtual List<RssNewsArticle> RssNewsArticle { get; private set; } = [];
+
+        [Key]
+        public int ID { get; private set; }
+
+        public string Name { get; private set; } = default!;
+
+        public string Url { get; private set; } = default!;
+
+        public string? Description { get; private set; }
+
+        public int IntervalMinutes { get; private set; } = 10;
+
+        public bool IsActive { get; private set; } = true;
+
+        public DateTime? LastFetchedAt { get; private set; }
+
+        public DateTime? UpdatedAt { get; private set; }
+
+        public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
+
+        private RssFeedSource() { }
+
+        private RssFeedSource(string name, string url, string? description, int intervalMinutes)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentException("Name is required.", nameof(name));
+            }
+
+            if (name.Length > 200)
+            {
+                throw new ArgumentOutOfRangeException(nameof(name));
+            }
+
+            if (string.IsNullOrWhiteSpace(url))
+            {
+                throw new ArgumentException("Url is required.", nameof(url));
+            }
+
+            if (url.Length > 1000)
+            {
+                throw new ArgumentOutOfRangeException(nameof(url));
+            }
+
+            if (intervalMinutes < 1)
+            {
+                throw new ArgumentOutOfRangeException(nameof(intervalMinutes));
+            }
+
+            Name = name;
+            Url = url;
+            Description = description;
+            IntervalMinutes = intervalMinutes;
+        }
+
+        public static RssFeedSource Create(string name, string url, string? description = null, int intervalMinutes = 10)
+            => new(name, url, description, intervalMinutes);
+
+        public void Update(string name, string url, string? description, int intervalMinutes, bool isActive)
+        {
+            if (string.IsNullOrWhiteSpace(name))
+            {
+                throw new ArgumentException("Name is required.", nameof(name));
+            }
+
+            if (name.Length > 200)
+            {
+                throw new ArgumentOutOfRangeException(nameof(name));
+            }
+
+            if (string.IsNullOrWhiteSpace(url))
+            {
+                throw new ArgumentException("Url is required.", nameof(url));
+            }
+
+            if (url.Length > 1000)
+            {
+                throw new ArgumentOutOfRangeException(nameof(url));
+            }
+
+            if (intervalMinutes < 1)
+            {
+                throw new ArgumentOutOfRangeException(nameof(intervalMinutes));
+            }
+
+            Name = name;
+            Url = url;
+            Description = description;
+            IntervalMinutes = intervalMinutes;
+            IsActive = isActive;
+            UpdatedAt = DateTime.UtcNow;
+        }
+
+        public void MarkFetched()
+        {
+            LastFetchedAt = DateTime.UtcNow;
+        }
+    }
+}

+ 95 - 0
Domain/Entities/News/RssNewsArticle.cs

@@ -0,0 +1,95 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Domain.Entities.News
+{
+    public class RssNewsArticle
+    {
+        [ForeignKey(nameof(RssFeedSourceID))]
+        public virtual RssFeedSource RssFeedSource { get; set; } = null!;
+
+        [Key]
+        public int ID { get; private set; }
+
+        public int RssFeedSourceID { get; private set; }
+
+        public string Title { get; private set; } = default!;
+
+        public string? Link { get; private set; }
+
+        public string? Guid { get; private set; }
+
+        public string? Author { get; private set; }
+
+        public string? Description { get; private set; }
+
+        public string? Content { get; private set; }
+
+        public string? ImageUrl { get; private set; }
+
+        public string? SourceName { get; private set; }
+
+        public string? Categories { get; private set; }
+
+        public int CommentCount { get; private set; } = 0;
+
+        public DateTime? PublishedAt { get; private set; }
+
+        public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
+
+        private RssNewsArticle() { }
+
+        private RssNewsArticle(
+            int rssFeedSourceID,
+            string title,
+            string? link,
+            string? guid,
+            string? author,
+            string? description,
+            string? content,
+            string? imageUrl,
+            string? sourceName,
+            string? categories,
+            int commentCount,
+            DateTime? publishedAt
+        ) {
+            if (string.IsNullOrWhiteSpace(title))
+            {
+                throw new ArgumentException("Title is required.", nameof(title));
+            }
+
+            if (title.Length > 500)
+            {
+                title = title[..500];
+            }
+
+            RssFeedSourceID = rssFeedSourceID;
+            Title = title;
+            Link = link;
+            Guid = guid;
+            Author = author;
+            Description = description;
+            Content = content;
+            ImageUrl = imageUrl;
+            SourceName = sourceName;
+            Categories = categories;
+            CommentCount = commentCount;
+            PublishedAt = publishedAt;
+        }
+
+        public static RssNewsArticle Create(
+            int rssFeedSourceID,
+            string title,
+            string? link = null,
+            string? guid = null,
+            string? author = null,
+            string? description = null,
+            string? content = null,
+            string? imageUrl = null,
+            string? sourceName = null,
+            string? categories = null,
+            int commentCount = 0,
+            DateTime? publishedAt = null
+        ) => new(rssFeedSourceID, title, link, guid, author, description, content, imageUrl, sourceName, categories, commentCount, publishedAt);
+    }
+}

+ 18 - 2
Infrastructure/DependencyInjection.cs

@@ -12,6 +12,7 @@ using Infrastructure.Cache;
 using Infrastructure.Chat;
 using Infrastructure.Crypto;
 using Infrastructure.Messaging.Email;
+using Infrastructure.News;
 using Infrastructure.Persistence;
 using Infrastructure.Persistence.Identity;
 using Infrastructure.Storage;
@@ -98,6 +99,12 @@ namespace Infrastructure
             services.AddSingleton<IChatMessageStore, RedisChatMessageStore>();
             services.AddSingleton<IChatConnectionTracker, RedisChatConnectionTracker>();
             services.AddScoped<IBoardPermissionService, BoardPermissionService>();
+            services.AddHttpClient("RssFeed", client =>
+            {
+                client.Timeout = TimeSpan.FromSeconds(30);
+                client.DefaultRequestHeaders.Add("User-Agent", "BitForum/1.0");
+                client.DefaultRequestHeaders.Add("Accept", "application/rss+xml, application/xml, text/xml");
+            });
 
             return services;
         }
@@ -110,7 +117,7 @@ namespace Infrastructure
         // Admin 전용
         public static IServiceCollection AddAdminInfrastructure(this IServiceCollection services, IConfiguration configuration)
         {
-            return services.AddDatabase(configuration).AddRedis(configuration).AddServices().AddHealthChecks(configuration);
+            return services.AddDatabase(configuration).AddRedis(configuration).AddServices().AddNews().AddHealthChecks(configuration);
         }
 
         /**
@@ -121,7 +128,7 @@ namespace Infrastructure
         // API 전용
         public static IServiceCollection AddApiInfrastructure(this IServiceCollection services, IConfiguration configuration)
         {
-            return services.AddDatabase(configuration).AddRedis(configuration).AddApiAuthentication(configuration).AddServices().AddUpbit().AddHealthChecks(configuration);
+            return services.AddDatabase(configuration).AddRedis(configuration).AddApiAuthentication(configuration).AddServices().AddUpbit().AddNews().AddHealthChecks(configuration);
         }
 
         private static IServiceCollection AddUpbit(this IServiceCollection services)
@@ -131,6 +138,15 @@ namespace Infrastructure
             return services;
         }
 
+        private static IServiceCollection AddNews(this IServiceCollection services)
+        {
+            services.AddSingleton<RssCollectorService>();
+            services.AddHostedService(sp => sp.GetRequiredService<RssCollectorService>());
+            services.AddSingleton<Application.Abstractions.News.IRssCollector>(sp => sp.GetRequiredService<RssCollectorService>());
+
+            return services;
+        }
+
         private static IServiceCollection AddApiAuthentication(this IServiceCollection services, IConfiguration configuration)
         {
             var settings = configuration.Get<AppSettings>()!;

+ 220 - 0
Infrastructure/News/RssCollectorService.cs

@@ -0,0 +1,220 @@
+using Application.Abstractions.Data;
+using Application.Abstractions.News;
+using Domain.Entities.News;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using System.Text.Json;
+using System.Xml.Linq;
+using System.Text.RegularExpressions;
+
+namespace Infrastructure.News;
+
+public sealed class RssCollectorService(
+    IServiceScopeFactory scopeFactory,
+    IHttpClientFactory httpClientFactory,
+    ILogger<RssCollectorService> logger
+) : BackgroundService, IRssCollector
+{
+    private static readonly XNamespace _dc = "http://purl.org/dc/elements/1.1/";
+    private static readonly XNamespace _content = "http://purl.org/rss/1.0/modules/content/";
+    private static readonly XNamespace _media = "http://search.yahoo.com/mrss/";
+    private static readonly XNamespace _slash = "http://purl.org/rss/1.0/modules/slash/";
+
+    protected override async Task ExecuteAsync(CancellationToken ct)
+    {
+        // 서비스 시작 직후 잠시 대기 (DB 준비)
+        await Task.Delay(5000, ct);
+
+        while (!ct.IsCancellationRequested)
+        {
+            try
+            {
+                await CollectAllFeedsAsync(ct);
+            }
+            catch (OperationCanceledException) when (ct.IsCancellationRequested)
+            {
+                break;
+            }
+            catch (Exception ex)
+            {
+                logger.LogError(ex, "RSS 수집 루프에서 예외 발생");
+            }
+
+            await Task.Delay(TimeSpan.FromMinutes(1), ct);
+        }
+    }
+
+    public async Task<int> FetchSourceAsync(int sourceID, CancellationToken ct)
+    {
+        using var scope = scopeFactory.CreateScope();
+        var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
+
+        var source = await db.RssFeedSource.FindAsync([sourceID], ct) ?? throw new KeyNotFoundException($"소스 ID {sourceID}를 찾을 수 없습니다.");
+
+        var count = await FetchAndStoreAsync(source, db, ct);
+        source.MarkFetched();
+        await db.SaveChangesAsync(ct);
+
+        return count;
+    }
+
+    private async Task CollectAllFeedsAsync(CancellationToken ct)
+    {
+        using var scope = scopeFactory.CreateScope();
+        var db = scope.ServiceProvider.GetRequiredService<IAppDbContext>();
+
+        // 초기 시드: 테이블이 비어있으면 기본 소스 삽입
+        if (!await db.RssFeedSource.AnyAsync(ct))
+        {
+            await SeedDefaultSourcesAsync(db, ct);
+        }
+
+        var sources = await db.RssFeedSource.Where(x => x.IsActive).ToListAsync(ct);
+        var now = DateTime.UtcNow;
+
+        foreach (var source in sources)
+        {
+            // 수집 주기 체크
+            if (source.LastFetchedAt.HasValue && (now - source.LastFetchedAt.Value).TotalMinutes < source.IntervalMinutes)
+            {
+                continue;
+            }
+
+            try
+            {
+                var count = await FetchAndStoreAsync(source, db, ct);
+                source.MarkFetched();
+                await db.SaveChangesAsync(ct);
+
+                if (count > 0)
+                {
+                    logger.LogInformation("[RSS] {SourceName}: {Count}건 수집 완료", source.Name, count);
+                }
+            }
+            catch (Exception ex)
+            {
+                logger.LogWarning(ex, "[RSS] {SourceName} ({Url}) 수집 실패", source.Name, source.Url);
+            }
+        }
+    }
+
+    private async Task<int> FetchAndStoreAsync(RssFeedSource source, IAppDbContext db, CancellationToken ct)
+    {
+        var client = httpClientFactory.CreateClient("RssFeed");
+        var xml = await client.GetStringAsync(source.Url, ct);
+        var doc = XDocument.Parse(xml);
+
+        var items = doc.Descendants("item").ToList();
+        if (items.Count == 0)
+        {
+            return 0;
+        }
+
+        // 파싱
+        var articles = items.Select(item => ParseItem(source.ID, item)).ToList();
+
+        // 중복 체크: 기존 Guid 조회
+        var guids = articles.Where(a => a.Guid != null).Select(a => a.Guid!).ToList();
+        var existingGuids = await db.RssNewsArticle
+            .Where(x => x.RssFeedSourceID == source.ID && x.Guid != null && guids.Contains(x.Guid!))
+            .Select(x => x.Guid!)
+            .ToListAsync(ct);
+
+        var existingSet = existingGuids.ToHashSet();
+
+        var newArticles = articles.Where(a => a.Guid == null || !existingSet.Contains(a.Guid)).ToList();
+
+        if (newArticles.Count == 0)
+        {
+            return 0;
+        }
+
+        db.RssNewsArticle.AddRange(newArticles);
+        await db.SaveChangesAsync(ct);
+
+        return newArticles.Count;
+    }
+
+    private static RssNewsArticle ParseItem(int sourceID, XElement item)
+    {
+        var title = item.Element("title")?.Value?.Trim() ?? "";
+        var link = item.Element("link")?.Value?.Trim();
+        var guid = item.Element("guid")?.Value?.Trim() ?? link;
+        var author = item.Element(_dc + "creator")?.Value?.Trim();
+        var description = item.Element("description")?.Value?.Trim();
+        var content = item.Element(_content + "encoded")?.Value?.Trim();
+        var sourceName = item.Element("source")?.Value?.Trim();
+
+        // Author fallback: Google News의 source 요소
+        if (string.IsNullOrEmpty(author) && !string.IsNullOrEmpty(sourceName))
+        {
+            author = sourceName;
+        }
+
+        // 이미지: media:content > enclosure > description/content 내 첫 번째 <img>
+        var imageUrl = item.Element(_media + "content")?.Attribute("url")?.Value?.Trim()
+            ?? item.Element("enclosure")?.Attribute("url")?.Value?.Trim();
+
+        if (string.IsNullOrEmpty(imageUrl))
+        {
+            imageUrl = ExtractFirstImageUrl(description) ?? ExtractFirstImageUrl(content);
+        }
+
+        // 카테고리: JSON 배열로 저장
+        var categories = item.Elements("category").Select(c => c.Value.Trim()).Where(c => !string.IsNullOrEmpty(c)).ToList();
+        string? categoriesJson = categories.Count > 0 ? JsonSerializer.Serialize(categories) : null;
+
+        // 댓글 수
+        var commentCountStr = item.Element(_slash + "comments")?.Value;
+        int.TryParse(commentCountStr, out var commentCount);
+
+        // 발행일
+        DateTime? publishedAt = null;
+        var pubDateStr = item.Element("pubDate")?.Value;
+        if (!string.IsNullOrEmpty(pubDateStr) && DateTimeOffset.TryParse(pubDateStr, out var dto))
+        {
+            publishedAt = dto.UtcDateTime;
+        }
+
+        return RssNewsArticle.Create(
+            rssFeedSourceID: sourceID,
+            title: title,
+            link: link,
+            guid: guid,
+            author: author,
+            description: description,
+            content: content,
+            imageUrl: imageUrl,
+            sourceName: sourceName,
+            categories: categoriesJson,
+            commentCount: commentCount,
+            publishedAt: publishedAt
+        );
+    }
+
+    private static readonly Regex _imgSrcRegex = new(@"<img[^>]+src\s*=\s*[""']([^""']+)[""']", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+    private static string? ExtractFirstImageUrl(string? html)
+    {
+        if (string.IsNullOrEmpty(html))
+        {
+            return null;
+        }
+
+        var match = _imgSrcRegex.Match(html);
+        return match.Success ? match.Groups[1].Value.Trim() : null;
+    }
+
+    private static async Task SeedDefaultSourcesAsync(IAppDbContext db, CancellationToken ct)
+    {
+        db.RssFeedSource.AddRange(
+            RssFeedSource.Create("CoinSpeaker KR", "https://www.coinspeaker.com/kr/news/feed/", "CoinSpeaker 한국어 뉴스", 10),
+            RssFeedSource.Create("Google News 비트코인", "https://news.google.com/rss/search?q=%EB%B9%84%ED%8A%B8%EC%BD%94%EC%9D%B8&hl=ko&gl=KR&ceid=KR:ko", "구글 뉴스 비트코인 검색", 15),
+            RssFeedSource.Create("CoinTelegraph KR", "https://cointelegraph-kr.com/rss", "CoinTelegraph 한국어 뉴스", 10)
+        );
+
+        await db.SaveChangesAsync(ct);
+    }
+}

+ 5 - 0
Infrastructure/Persistence/AppDbContext.cs

@@ -13,6 +13,7 @@ using Domain.Entities.Forum.Comments;
 using Domain.Entities.Director;
 using Domain.Entities.EmailVerification;
 using Domain.Entities.Forum.Logs;
+using Domain.Entities.News;
 using Domain.Entities.Page.Popup;
 using Microsoft.EntityFrameworkCore;
 
@@ -95,6 +96,10 @@ namespace Infrastructure.Persistence
         public DbSet<CommentReport> CommentReport { get; set; }
         public DbSet<CommentMention> CommentMention { get; set; }
 
+        // 뉴스
+        public DbSet<RssFeedSource> RssFeedSource { get; set; }
+        public DbSet<RssNewsArticle> RssNewsArticle { get; set; }
+
         // 게시글/댓글 로그
         public DbSet<PostUpdateLog> PostUpdateLog { get; set; }
         public DbSet<PostFileDownLog> PostFileDownLog { get; set; }

+ 28 - 0
Infrastructure/Persistence/Configurations/News/RssFeedSourceConfiguration.cs

@@ -0,0 +1,28 @@
+using Domain.Entities.News;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Infrastructure.Persistence.Configurations.News;
+
+public sealed class RssFeedSourceConfiguration : IEntityTypeConfiguration<RssFeedSource>
+{
+    public void Configure(EntityTypeBuilder<RssFeedSource> builder)
+    {
+        builder.HasIndex(x => x.Url).IsUnique();
+        builder.HasIndex(x => x.IsActive);
+
+        builder.HasMany(x => x.RssNewsArticle).WithOne(x => x.RssFeedSource).HasForeignKey(x => x.RssFeedSourceID).OnDelete(DeleteBehavior.Cascade);
+
+        builder.ToTable(nameof(RssFeedSource), t => t.HasComment("RSS 피드 소스"));
+        builder.HasKey(x => x.ID);
+        builder.Property(x => x.ID).ValueGeneratedOnAdd().HasComment("PK");
+        builder.Property(x => x.Name).HasMaxLength(200).IsRequired().HasComment("소스 이름");
+        builder.Property(x => x.Url).HasMaxLength(1000).IsRequired().HasComment("RSS 피드 URL");
+        builder.Property(x => x.Description).HasColumnType("nvarchar(max)").HasComment("소스 설명");
+        builder.Property(x => x.IntervalMinutes).IsRequired().HasComment("수집 주기 (분)");
+        builder.Property(x => x.IsActive).IsRequired().HasComment("활성화 여부");
+        builder.Property(x => x.LastFetchedAt).HasComment("마지막 수집 일시");
+        builder.Property(x => x.UpdatedAt).HasComment("수정 일시");
+        builder.Property(x => x.CreatedAt).IsRequired().HasComment("등록 일시");
+    }
+}

+ 33 - 0
Infrastructure/Persistence/Configurations/News/RssNewsArticleConfiguration.cs

@@ -0,0 +1,33 @@
+using Domain.Entities.News;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Infrastructure.Persistence.Configurations.News;
+
+public sealed class RssNewsArticleConfiguration : IEntityTypeConfiguration<RssNewsArticle>
+{
+    public void Configure(EntityTypeBuilder<RssNewsArticle> builder)
+    {
+        builder.HasIndex(x => new { x.Guid, x.RssFeedSourceID }).IsUnique().HasFilter("[Guid] IS NOT NULL");
+        builder.HasIndex(x => x.PublishedAt);
+        builder.HasIndex(x => x.RssFeedSourceID);
+        builder.HasIndex(x => x.CreatedAt);
+
+        builder.ToTable(nameof(RssNewsArticle), t => t.HasComment("RSS 뉴스 기사"));
+        builder.HasKey(x => x.ID);
+        builder.Property(x => x.ID).ValueGeneratedOnAdd().HasComment("PK");
+        builder.Property(x => x.RssFeedSourceID).IsRequired().HasComment("RSS 피드 소스 FK");
+        builder.Property(x => x.Title).HasMaxLength(500).IsRequired().HasComment("기사 제목");
+        builder.Property(x => x.Link).HasMaxLength(2000).HasComment("원본 링크");
+        builder.Property(x => x.Guid).HasMaxLength(1000).HasComment("RSS GUID (중복 방지)");
+        builder.Property(x => x.Author).HasMaxLength(200).HasComment("작성자");
+        builder.Property(x => x.Description).HasColumnType("nvarchar(max)").HasComment("요약");
+        builder.Property(x => x.Content).HasColumnType("nvarchar(max)").HasComment("본문 (content:encoded)");
+        builder.Property(x => x.ImageUrl).HasMaxLength(2000).HasComment("썸네일 이미지 URL");
+        builder.Property(x => x.SourceName).HasMaxLength(200).HasComment("출처명");
+        builder.Property(x => x.Categories).HasColumnType("nvarchar(max)").HasComment("카테고리 (JSON 배열)");
+        builder.Property(x => x.CommentCount).IsRequired().HasComment("댓글 수");
+        builder.Property(x => x.PublishedAt).HasComment("발행 일시");
+        builder.Property(x => x.CreatedAt).IsRequired().HasComment("수집 일시");
+    }
+}

+ 6392 - 0
Infrastructure/Persistence/Migrations/20260313180453_AddRssNewsTables.Designer.cs

@@ -0,0 +1,6392 @@
+// <auto-generated />
+using System;
+using Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Infrastructure.Migrations.AppDb
+{
+    [DbContext(typeof(AppDbContext))]
+    [Migration("20260313180453_AddRssNewsTables")]
+    partial class AddRssNewsTables
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "10.0.2")
+                .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+            modelBuilder.Entity("Domain.Entities.Common.Config", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("LastUpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 수정일시");
+
+                    b.Property<byte[]>("RowVersion")
+                        .IsConcurrencyToken()
+                        .IsRequired()
+                        .ValueGeneratedOnAddOrUpdate()
+                        .HasColumnType("rowversion")
+                        .HasComment("동시성 제어용");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("Config", null, t =>
+                        {
+                            t.HasComment("운영 정보 설정 값");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Crypto.Coin", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("ContractAddress")
+                        .HasMaxLength(100)
+                        .HasColumnType("nvarchar(100)")
+                        .HasComment("컨트랙트 주소");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("설명");
+
+                    b.Property<short>("DisplayOrder")
+                        .HasColumnType("smallint")
+                        .HasComment("메인 노출 순서");
+
+                    b.Property<string>("EngName")
+                        .IsRequired()
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("영문 이름");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<bool>("IsDelisted")
+                        .HasColumnType("bit")
+                        .HasComment("상장 폐지");
+
+                    b.Property<bool>("IsFeatured")
+                        .HasColumnType("bit")
+                        .HasComment("주요 코인 (메인 노출)");
+
+                    b.Property<bool>("IsNew")
+                        .HasColumnType("bit")
+                        .HasComment("신규 상장");
+
+                    b.Property<bool>("IsWarning")
+                        .HasColumnType("bit")
+                        .HasComment("위험 경고");
+
+                    b.Property<string>("KorName")
+                        .IsRequired()
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("한글 이름");
+
+                    b.Property<string>("LogoImage")
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("로고 이미지");
+
+                    b.Property<string>("Symbol")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("심볼 (BTC, ETH 등)");
+
+                    b.Property<string>("TelegramUrl")
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("텔레그램 URL");
+
+                    b.Property<string>("TwitterUrl")
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("트위터 URL");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("WebsiteUrl")
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("홈페이지 URL");
+
+                    b.Property<string>("WhitepaperUrl")
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("백서 URL");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("DisplayOrder")
+                        .HasDatabaseName("IX_Coin_DisplayOrder");
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("IsDelisted");
+
+                    b.HasIndex("IsFeatured")
+                        .HasDatabaseName("IX_Coin_IsFeatured");
+
+                    b.HasIndex("IsNew");
+
+                    b.HasIndex("IsWarning");
+
+                    b.HasIndex("Symbol")
+                        .IsUnique();
+
+                    b.HasIndex("Symbol", "IsActive");
+
+                    b.ToTable("Coin", null, t =>
+                        {
+                            t.HasComment("코인/토큰");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Crypto.CoinCategory", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("카테고리 코드");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(100)
+                        .HasColumnType("nvarchar(100)")
+                        .HasComment("카테고리 이름");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Code")
+                        .IsUnique();
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("Order");
+
+                    b.ToTable("CoinCategory", null, t =>
+                        {
+                            t.HasComment("코인 카테고리");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Crypto.CoinCategoryMap", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("CategoryID")
+                        .HasColumnType("int")
+                        .HasComment("카테고리 ID");
+
+                    b.Property<int>("CoinID")
+                        .HasColumnType("int")
+                        .HasComment("코인 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CategoryID");
+
+                    b.HasIndex("CoinID", "CategoryID")
+                        .IsUnique();
+
+                    b.ToTable("CoinCategoryMap", null, t =>
+                        {
+                            t.HasComment("코인-카테고리 연결");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Crypto.CoinMarket", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("CoinID")
+                        .HasColumnType("int")
+                        .HasComment("코인 ID");
+
+                    b.Property<string>("Market")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("거래쌍 (KRW-BTC 등)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Market");
+
+                    b.HasIndex("CoinID", "Market")
+                        .IsUnique();
+
+                    b.ToTable("CoinMarket", null, t =>
+                        {
+                            t.HasComment("코인-거래쌍 연결");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Director.AdminAccessLog", b =>
+                {
+                    b.Property<long>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("bigint")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("생성 일시");
+
+                    b.Property<long>("ElapsedMs")
+                        .HasColumnType("bigint")
+                        .HasComment("처리 시간 (밀리초)");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(45)
+                        .HasColumnType("nvarchar(45)")
+                        .HasComment("IP 주소");
+
+                    b.Property<string>("MenuName")
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("메뉴 이름");
+
+                    b.Property<string>("Method")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("nvarchar(10)")
+                        .HasComment("HTTP Method");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(2048)
+                        .HasColumnType("nvarchar(2048)")
+                        .HasComment("요청 경로");
+
+                    b.Property<string>("QueryString")
+                        .HasMaxLength(2048)
+                        .HasColumnType("nvarchar(2048)")
+                        .HasComment("쿼리 스트링");
+
+                    b.Property<int>("StatusCode")
+                        .HasColumnType("int")
+                        .HasComment("응답 상태 코드");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(512)
+                        .HasColumnType("nvarchar(512)")
+                        .HasComment("User Agent");
+
+                    b.Property<string>("UserID")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("nvarchar(64)")
+                        .HasComment("관리자 사용자 ID");
+
+                    b.Property<string>("UserName")
+                        .HasMaxLength(100)
+                        .HasColumnType("nvarchar(100)")
+                        .HasComment("관리자 사용자 이름");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.HasIndex("UserID");
+
+                    b.ToTable("AdminAccessLog", null, t =>
+                        {
+                            t.HasComment("관리자 접근 기록");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Director.AdminLoginLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Account")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("로그인 시도 계정");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("생성 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(45)
+                        .HasColumnType("nvarchar(45)")
+                        .HasComment("IP 주소");
+
+                    b.Property<string>("Reason")
+                        .HasMaxLength(225)
+                        .HasColumnType("nvarchar(225)")
+                        .HasComment("실패 사유");
+
+                    b.Property<bool>("Success")
+                        .HasColumnType("bit")
+                        .HasComment("로그인 성공 여부");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(512)
+                        .HasColumnType("nvarchar(512)")
+                        .HasComment("User Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Account");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.ToTable("AdminLoginLog", null, t =>
+                        {
+                            t.HasComment("관리자 로그인 기록");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.EmailVerification.EmailVerifyNumber", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(10)
+                        .HasColumnType("nvarchar(10)")
+                        .HasComment("Code");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Email")
+                        .IsRequired()
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("이메일");
+
+                    b.Property<DateTime>("Expiration")
+                        .HasColumnType("datetime2")
+                        .HasComment("만료 일시");
+
+                    b.Property<bool>("IsVerified")
+                        .HasColumnType("bit")
+                        .HasComment("인증 여부");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int")
+                        .HasComment("인증 유형 (이메일 인증 / 비밀번호 재설정)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Code");
+
+                    b.HasIndex("Email");
+
+                    b.HasIndex("Expiration");
+
+                    b.HasIndex("IsVerified");
+
+                    b.HasIndex("Type");
+
+                    b.HasIndex("Type", "Code");
+
+                    b.HasIndex("Type", "Code", "IsVerified");
+
+                    b.ToTable("EmailVerifyNumber", null, t =>
+                        {
+                            t.HasComment("이메일 인증 번호들");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.EmailVerification.EmailVerifyToken", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Additional")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("추가 정보(JSON)");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Email")
+                        .IsRequired()
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("이메일");
+
+                    b.Property<DateTime>("Expiration")
+                        .HasColumnType("datetime2")
+                        .HasComment("만료 일시");
+
+                    b.Property<bool>("IsVerified")
+                        .HasColumnType("bit")
+                        .HasComment("인증 여부");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("nvarchar(256)")
+                        .HasComment("Token");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int")
+                        .HasComment("인증 유형 (이메일 인증 / 비밀번호 재설정)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Email");
+
+                    b.HasIndex("Expiration");
+
+                    b.HasIndex("IsVerified");
+
+                    b.HasIndex("Token");
+
+                    b.HasIndex("Type");
+
+                    b.HasIndex("Type", "Email", "Token");
+
+                    b.HasIndex("Type", "Email", "Token", "IsVerified");
+
+                    b.ToTable("EmailVerifyToken", null, t =>
+                        {
+                            t.HasComment("이메일 인증 토큰들");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.Board", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardGroupID")
+                        .HasColumnType("int")
+                        .HasComment("분류 ID");
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("게시판 주소");
+
+                    b.Property<int?>("CoinID")
+                        .HasColumnType("int");
+
+                    b.Property<int>("Comments")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<bool>("IsSearch")
+                        .HasColumnType("bit")
+                        .HasComment("검색 여부");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 이름");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<int>("Posts")
+                        .HasColumnType("int")
+                        .HasComment("게시글 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardGroupID");
+
+                    b.HasIndex("Code")
+                        .IsUnique();
+
+                    b.HasIndex("CoinID");
+
+                    b.HasIndex("Comments");
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("IsSearch");
+
+                    b.HasIndex("Name");
+
+                    b.HasIndex("Order");
+
+                    b.HasIndex("Posts");
+
+                    b.HasIndex("Code", "IsActive");
+
+                    b.HasIndex("Code", "IsSearch");
+
+                    b.HasIndex("IsSearch", "IsActive");
+
+                    b.ToTable("Board", null, t =>
+                        {
+                            t.HasComment("게시판");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.BoardGroup", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<short>("Boards")
+                        .HasColumnType("smallint")
+                        .HasComment("게시판 수");
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("게시판 분류 주소");
+
+                    b.Property<int>("Comments")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 분류 명");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<int>("Posts")
+                        .HasColumnType("int")
+                        .HasComment("게시글 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Code")
+                        .IsUnique();
+
+                    b.HasIndex("Order");
+
+                    b.HasIndex("Order", "CreatedAt");
+
+                    b.HasIndex("Code", "Order", "CreatedAt");
+
+                    b.ToTable("BoardGroup", null, t =>
+                        {
+                            t.HasComment("게시판 분류");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.BoardManager", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<bool>("CanDelete")
+                        .HasColumnType("bit")
+                        .HasComment("삭제 권한");
+
+                    b.Property<bool>("CanEdit")
+                        .HasColumnType("bit")
+                        .HasComment("수정 권한");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("관리자 ID");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("BoardID", "MemberID");
+
+                    b.ToTable("BoardManager", null, t =>
+                        {
+                            t.HasComment("게시판 관리자");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.BoardMeta", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID")
+                        .IsUnique();
+
+                    b.ToTable("BoardMeta", null, t =>
+                        {
+                            t.HasComment("게시판 설정");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.BoardPrefix", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<string>("Color")
+                        .HasMaxLength(10)
+                        .HasColumnType("nvarchar(10)")
+                        .HasComment("색상");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("말머리");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("정렬 순서");
+
+                    b.Property<int>("Posts")
+                        .HasColumnType("int")
+                        .HasComment("사용 게시글 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID", "IsActive", "Order", "CreatedAt");
+
+                    b.ToTable("BoardPrefix", null, t =>
+                        {
+                            t.HasComment("게시판 말머리");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.Comment", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<string>("Content")
+                        .IsRequired()
+                        .HasMaxLength(4000)
+                        .HasColumnType("nvarchar(4000)")
+                        .HasComment("댓글 내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<short>("Depth")
+                        .HasColumnType("smallint")
+                        .HasComment("댓글 깊이");
+
+                    b.Property<int>("Dislikes")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Email")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<byte>("Files")
+                        .HasColumnType("tinyint");
+
+                    b.Property<int>("Images")
+                        .HasColumnType("int");
+
+                    b.Property<string>("IpAddress")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<bool>("IsDeleted")
+                        .HasColumnType("bit")
+                        .HasComment("삭제 여부");
+
+                    b.Property<bool>("IsReply")
+                        .HasColumnType("bit");
+
+                    b.Property<bool>("IsSecret")
+                        .HasColumnType("bit");
+
+                    b.Property<int>("Likes")
+                        .HasColumnType("int");
+
+                    b.Property<byte>("Medias")
+                        .HasColumnType("tinyint");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<int?>("MentionMemberID")
+                        .HasColumnType("int")
+                        .HasComment("언급 대상 회원 ID");
+
+                    b.Property<string>("Name")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<int?>("ParentID")
+                        .HasColumnType("int")
+                        .HasComment("부모 댓글 ID");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<int>("Replies")
+                        .HasColumnType("int");
+
+                    b.Property<int>("Reports")
+                        .HasColumnType("int");
+
+                    b.Property<string>("SID")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<int>("Score")
+                        .HasColumnType("int")
+                        .HasComment("점수");
+
+                    b.Property<byte>("Status")
+                        .HasColumnType("tinyint")
+                        .HasComment("댓글 상태");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("UserAgent")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("MentionMemberID");
+
+                    b.HasIndex("ParentID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("PostID", "IsDeleted", "ParentID", "CreatedAt")
+                        .IsDescending(false, false, false, true);
+
+                    b.HasIndex("PostID", "IsDeleted", "ParentID", "Score", "ID")
+                        .IsDescending(false, false, false, true, true);
+
+                    b.HasIndex("PostID", "MemberID", "IsDeleted", "ParentID", "CreatedAt")
+                        .IsDescending(false, false, false, false, true);
+
+                    b.HasIndex("PostID", "MemberID", "IsDeleted", "ParentID", "Score", "ID")
+                        .IsDescending(false, false, false, false, true, true);
+
+                    b.ToTable("Comment", null, t =>
+                        {
+                            t.HasComment("댓글");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentFile", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int");
+
+                    b.Property<string>("ContentType")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<DateTime?>("DisabledAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<int>("Downloads")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Extension")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("HashedName")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<bool>("IsDisabled")
+                        .HasColumnType("bit");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("bigint");
+
+                    b.Property<Guid>("UUID")
+                        .HasColumnType("uniqueidentifier");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("UUID")
+                        .IsUnique();
+
+                    b.ToTable("CommentFile", null, t =>
+                        {
+                            t.HasComment("댓글 파일");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentImage", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int");
+
+                    b.Property<string>("ContentType")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<DateTime?>("DisabledAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("Extension")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("HashedName")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<short?>("Height")
+                        .HasColumnType("smallint");
+
+                    b.Property<bool>("IsDisabled")
+                        .HasColumnType("bit");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("bigint");
+
+                    b.Property<Guid>("UUID")
+                        .HasColumnType("uniqueidentifier");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<short?>("Width")
+                        .HasColumnType("smallint");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("UUID")
+                        .IsUnique();
+
+                    b.ToTable("CommentImage", null, t =>
+                        {
+                            t.HasComment("댓글 이미지");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentLink", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<int>("Clicks")
+                        .HasColumnType("int");
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<DateTime?>("DisabledAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<bool>("IsDisabled")
+                        .HasColumnType("bit");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<Guid>("UUID")
+                        .HasColumnType("uniqueidentifier");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("PostID");
+
+                    b.ToTable("CommentLink", null, t =>
+                        {
+                            t.HasComment("댓글 링크");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentMedia", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<DateTime?>("DisabledAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<bool>("IsDisabled")
+                        .HasColumnType("bit");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("PostID");
+
+                    b.ToTable("CommentMedia", null, t =>
+                        {
+                            t.HasComment("댓글 미디어");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentMention", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int")
+                        .HasComment("댓글 ID");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<int>("Length")
+                        .HasColumnType("int")
+                        .HasComment("길이");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("언급된 회원 ID");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<string>("RawHandle")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("nvarchar(64)")
+                        .HasComment("원문 회원 언급값");
+
+                    b.Property<int>("Start")
+                        .HasColumnType("int")
+                        .HasComment("본문 내 시작 인덱스");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("CommentID")
+                        .IsUnique();
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("CommentID", "ID");
+
+                    b.HasIndex("CommentID", "MemberID", "ID");
+
+                    b.HasIndex("CommentID", "MemberID", "Start");
+
+                    b.ToTable("CommentMention", null, t =>
+                        {
+                            t.HasComment("댓글 멘션");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentReaction", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int")
+                        .HasComment("댓글 ID");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<byte>("Reaction")
+                        .HasColumnType("tinyint")
+                        .HasComment("반응 구분");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("Reaction");
+
+                    b.HasIndex("CommentID", "ID");
+
+                    b.HasIndex("CommentID", "MemberID")
+                        .IsUnique();
+
+                    b.ToTable("CommentReaction", null, t =>
+                        {
+                            t.HasComment("댓글 반응");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentReport", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int")
+                        .HasComment("댓글 ID");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Memo")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("처리 내용");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<string>("Reason")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("신고 내용");
+
+                    b.Property<byte>("Status")
+                        .HasColumnType("tinyint")
+                        .HasComment("처리 상태");
+
+                    b.Property<byte>("Type")
+                        .HasColumnType("tinyint")
+                        .HasComment("신고 사유");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("Status");
+
+                    b.HasIndex("Type");
+
+                    b.HasIndex("CommentID", "ID");
+
+                    b.HasIndex("CommentID", "MemberID")
+                        .IsUnique();
+
+                    b.ToTable("CommentReport", null, t =>
+                        {
+                            t.HasComment("댓글 신고");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.CommentFileDownLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("CommentFileID")
+                        .HasColumnType("int")
+                        .HasComment("댓글 파일 ID");
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int")
+                        .HasComment("댓글 ID");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CommentFileID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("CommentID", "CommentFileID");
+
+                    b.HasIndex("CommentID", "CommentFileID", "MemberID");
+
+                    b.ToTable("CommentFileDownLog", null, t =>
+                        {
+                            t.HasComment("댓글 파일 다운로드 로그");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.CommentLinkClickLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int")
+                        .HasComment("댓글 ID");
+
+                    b.Property<int>("CommentLinkID")
+                        .HasColumnType("int")
+                        .HasComment("댓글 링크 ID");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("CommentLinkID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("CommentID", "CommentLinkID");
+
+                    b.HasIndex("CommentID", "CommentLinkID", "MemberID");
+
+                    b.ToTable("CommentLinkClickLog", null, t =>
+                        {
+                            t.HasComment("댓글 링크 클릭 로그");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.CommentUpdateLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("CommentID")
+                        .HasColumnType("int")
+                        .HasComment("댓글 ID");
+
+                    b.Property<string>("ContentDiff")
+                        .HasMaxLength(4000)
+                        .HasColumnType("nvarchar(4000)")
+                        .HasComment("변경 내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Note")
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("비고");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CommentID");
+
+                    b.HasIndex("ContentDiff");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("CommentID", "ID");
+
+                    b.HasIndex("CommentID", "MemberID");
+
+                    b.HasIndex("CommentID", "MemberID", "ID");
+
+                    b.ToTable("CommentUpdateLog", null, t =>
+                        {
+                            t.HasComment("댓글 수정 로그");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.PostFileDownLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<int>("PostFileID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 파일 ID");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostFileID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("PostID", "PostFileID");
+
+                    b.HasIndex("PostID", "PostFileID", "MemberID");
+
+                    b.ToTable("PostFileDownLog", null, t =>
+                        {
+                            t.HasComment("게시글 파일 다운로드 로그");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.PostLinkClickLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<int>("PostLinkID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 파일 ID");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("PostLinkID");
+
+                    b.HasIndex("PostID", "PostLinkID");
+
+                    b.HasIndex("PostID", "PostLinkID", "MemberID");
+
+                    b.ToTable("PostLinkClickLog", null, t =>
+                        {
+                            t.HasComment("게시글 링크 클릭 로그");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.PostUpdateLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("ContentDiff")
+                        .HasMaxLength(4000)
+                        .HasColumnType("nvarchar(4000)")
+                        .HasComment("변경 내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Note")
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("비고");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<string>("SubjectDiff")
+                        .HasMaxLength(4000)
+                        .HasColumnType("nvarchar(4000)")
+                        .HasComment("변경 제목");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("ContentDiff");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("SubjectDiff");
+
+                    b.HasIndex("PostID", "ID");
+
+                    b.HasIndex("PostID", "MemberID");
+
+                    b.HasIndex("PostID", "MemberID", "ID");
+
+                    b.ToTable("PostUpdateLog", null, t =>
+                        {
+                            t.HasComment("게시글 수정 로그");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.Post", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<int?>("BoardPrefixID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 말머리 ID");
+
+                    b.Property<int>("Bookmarks")
+                        .HasColumnType("int")
+                        .HasComment("즐겨찾기 수");
+
+                    b.Property<int>("Comments")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수");
+
+                    b.Property<string>("Content")
+                        .IsRequired()
+                        .HasMaxLength(8000)
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("삭제 일시");
+
+                    b.Property<int>("Dislikes")
+                        .HasColumnType("int")
+                        .HasComment("싫어요");
+
+                    b.Property<string>("Email")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("회원 이메일");
+
+                    b.Property<byte>("Files")
+                        .HasColumnType("tinyint")
+                        .HasComment("파일 수");
+
+                    b.Property<byte>("Images")
+                        .HasColumnType("tinyint")
+                        .HasComment("이미지 수");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP");
+
+                    b.Property<bool>("IsAnonymous")
+                        .HasColumnType("bit")
+                        .HasComment("익명 글 여부");
+
+                    b.Property<bool>("IsDeleted")
+                        .HasColumnType("bit")
+                        .HasComment("삭제 여부");
+
+                    b.Property<bool>("IsNotice")
+                        .HasColumnType("bit")
+                        .HasComment("일반 공지 여부");
+
+                    b.Property<bool>("IsReply")
+                        .HasColumnType("bit")
+                        .HasComment("답변 여부");
+
+                    b.Property<bool>("IsSecret")
+                        .HasColumnType("bit")
+                        .HasComment("비밀글 여부");
+
+                    b.Property<bool>("IsSpeaker")
+                        .HasColumnType("bit")
+                        .HasComment("전체 공지 여부");
+
+                    b.Property<DateTime?>("LastCommentUpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 댓글 일시");
+
+                    b.Property<DateTime?>("LastReplyUpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 답변 일시");
+
+                    b.Property<int>("Likes")
+                        .HasColumnType("int")
+                        .HasComment("좋아요");
+
+                    b.Property<byte>("Medias")
+                        .HasColumnType("tinyint")
+                        .HasComment("미디어 수");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Name")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 이름");
+
+                    b.Property<int>("Reports")
+                        .HasColumnType("int")
+                        .HasComment("신고 수");
+
+                    b.Property<string>("SID")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 SID");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("제목");
+
+                    b.Property<byte>("Tags")
+                        .HasColumnType("tinyint")
+                        .HasComment("Tag 수");
+
+                    b.Property<string>("Thumbnail")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("대표 이미지");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("BoardPrefixID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("ID", "BoardID");
+
+                    b.HasIndex("ID", "BoardID", "IsDeleted");
+
+                    b.HasIndex("ID", "BoardID", "BoardPrefixID", "IsDeleted", "Comments");
+
+                    b.HasIndex("ID", "BoardID", "BoardPrefixID", "IsDeleted", "CreatedAt");
+
+                    b.HasIndex("ID", "BoardID", "BoardPrefixID", "IsDeleted", "Likes");
+
+                    b.HasIndex("ID", "BoardID", "BoardPrefixID", "IsDeleted", "Views");
+
+                    b.ToTable("Post", null, t =>
+                        {
+                            t.HasComment("게시글");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostBookmark", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("IpAddress")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<string>("UserAgent")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("PostID", "MemberID")
+                        .IsUnique();
+
+                    b.ToTable("PostBookmark", null, t =>
+                        {
+                            t.HasComment("게시글 즐겨찾기");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostFile", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<string>("ContentType")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<DateTime?>("DisabledAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<int>("Downloads")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Extension")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("HashedName")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<bool>("IsDisabled")
+                        .HasColumnType("bit");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("bigint");
+
+                    b.Property<Guid>("UUID")
+                        .HasColumnType("uniqueidentifier");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("UUID")
+                        .IsUnique();
+
+                    b.ToTable("PostFile", null, t =>
+                        {
+                            t.HasComment("게시글 파일");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostImage", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<string>("ContentType")
+                        .HasMaxLength(100)
+                        .HasColumnType("nvarchar(100)")
+                        .HasComment("MIME 타입");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<DateTime?>("DisabledAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("비활성 일시");
+
+                    b.Property<string>("Extension")
+                        .HasMaxLength(10)
+                        .HasColumnType("nvarchar(10)")
+                        .HasComment("확장자");
+
+                    b.Property<string>("FileName")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("원본 파일명");
+
+                    b.Property<string>("HashedName")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("저장 파일명");
+
+                    b.Property<short?>("Height")
+                        .HasColumnType("smallint")
+                        .HasComment("세로 해상도(px)");
+
+                    b.Property<bool>("IsDisabled")
+                        .HasColumnType("bit")
+                        .HasComment("비활성 여부");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("저장 경로");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<long?>("Size")
+                        .HasColumnType("bigint")
+                        .HasComment("용량(byte)");
+
+                    b.Property<Guid>("UUID")
+                        .HasColumnType("uniqueidentifier")
+                        .HasComment("이미지 ID");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("URL");
+
+                    b.Property<short?>("Width")
+                        .HasColumnType("smallint")
+                        .HasComment("가로 해상도(px)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("UUID")
+                        .IsUnique();
+
+                    b.HasIndex("PostID", "HashedName");
+
+                    b.HasIndex("PostID", "HashedName", "IsDisabled");
+
+                    b.ToTable("PostImage", null, t =>
+                        {
+                            t.HasComment("게시글 이미지");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostLink", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<int>("Clicks")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<DateTime?>("DisabledAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<bool>("IsDisabled")
+                        .HasColumnType("bit");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<Guid>("UUID")
+                        .HasColumnType("uniqueidentifier");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("PostID");
+
+                    b.ToTable("PostLink", null, t =>
+                        {
+                            t.HasComment("게시글 링크");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostMedia", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<DateTime?>("DisabledAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<bool>("IsDisabled")
+                        .HasColumnType("bit");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("PostID");
+
+                    b.ToTable("PostMedia", null, t =>
+                        {
+                            t.HasComment("게시글 미디어");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostReaction", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<byte>("Reaction")
+                        .HasColumnType("tinyint")
+                        .HasComment("반응 구분");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("Reaction");
+
+                    b.HasIndex("PostID", "MemberID")
+                        .IsUnique();
+
+                    b.ToTable("PostReaction", null, t =>
+                        {
+                            t.HasComment("게시글 반응");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostReport", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Memo")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("처리 내용");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<string>("Reason")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("신고 내용");
+
+                    b.Property<byte>("Status")
+                        .HasColumnType("tinyint")
+                        .HasComment("처리 상태");
+
+                    b.Property<byte>("Type")
+                        .HasColumnType("tinyint")
+                        .HasComment("신고 사유");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("Status");
+
+                    b.HasIndex("Type");
+
+                    b.HasIndex("PostID", "MemberID")
+                        .IsUnique();
+
+                    b.ToTable("PostReport", null, t =>
+                        {
+                            t.HasComment("게시글 신고");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostTag", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int");
+
+                    b.Property<int>("TagID")
+                        .HasColumnType("int");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex("TagID");
+
+                    b.ToTable("PostTag", null, t =>
+                        {
+                            t.HasComment("게시글 태그 연결");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.Tag", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(450)");
+
+                    b.Property<string>("Slug")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(450)");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<long>("UsageCount")
+                        .HasColumnType("bigint");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Name")
+                        .IsUnique();
+
+                    b.HasIndex("Slug")
+                        .IsUnique();
+
+                    b.HasIndex("UsageCount");
+
+                    b.ToTable("Tag", null, t =>
+                        {
+                            t.HasComment("태그");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Channel", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Handle")
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("핸들");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("활성 여부");
+
+                    b.Property<bool>("IsVerified")
+                        .HasColumnType("bit")
+                        .HasComment("인증 여부");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("채널 이름");
+
+                    b.Property<decimal>("PlatformFeeRate")
+                        .HasPrecision(5, 2)
+                        .HasColumnType("decimal(5,2)")
+                        .HasComment("수수료(%)");
+
+                    b.Property<string>("SID")
+                        .IsRequired()
+                        .HasMaxLength(24)
+                        .HasColumnType("nvarchar(24)")
+                        .HasComment("채널 ID");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("YouTubeUrl")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("YouTube 채널 URL");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Handle")
+                        .IsUnique()
+                        .HasFilter("[Handle] IS NOT NULL");
+
+                    b.HasIndex("MemberID")
+                        .IsUnique();
+
+                    b.HasIndex("Name")
+                        .IsUnique();
+
+                    b.HasIndex("SID")
+                        .IsUnique();
+
+                    b.HasIndex("YouTubeUrl")
+                        .IsUnique();
+
+                    b.HasIndex("MemberID", "IsActive");
+
+                    b.HasIndex("MemberID", "IsVerified");
+
+                    b.HasIndex("MemberID", "IsVerified", "IsActive");
+
+                    b.ToTable("Channel", null, t =>
+                        {
+                            t.HasComment("채널 정보");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberEmailChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterEmail")
+                        .IsRequired()
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("바뀐 이메일");
+
+                    b.Property<string>("BeforeEmail")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("이전 이메일");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(45)
+                        .HasColumnType("nvarchar(45)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Referer")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이전 페이지 주소");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(512)
+                        .HasColumnType("nvarchar(512)")
+                        .HasComment("User Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.HasIndex("MemberID");
+
+                    b.ToTable("MemberEmailChangeLog", null, t =>
+                        {
+                            t.HasComment("사용자 이메일 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberExpLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("Amount")
+                        .HasColumnType("int")
+                        .HasComment("변동량");
+
+                    b.Property<long>("Balance")
+                        .HasColumnType("bigint")
+                        .HasComment("잔액");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("기록 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("사유");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("MemberID");
+
+                    b.ToTable("MemberExpLog", null, t =>
+                        {
+                            t.HasComment("경험치 변동 내역");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberIntroChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterIntro")
+                        .HasMaxLength(3000)
+                        .HasColumnType("nvarchar(3000)")
+                        .HasComment("바꾼 자기소개");
+
+                    b.Property<string>("BeforeIntro")
+                        .HasMaxLength(3000)
+                        .HasColumnType("nvarchar(3000)")
+                        .HasComment("이전 자기소개");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Referer")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이전 페이지 주소");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(512)
+                        .HasColumnType("nvarchar(512)")
+                        .HasComment("User Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("MemberID");
+
+                    b.ToTable("MemberIntroChangeLog", null, t =>
+                        {
+                            t.HasComment("자기소개 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberLoginLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Account")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("로그인 시도한 계정");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(45)
+                        .HasColumnType("nvarchar(45)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Reason")
+                        .HasMaxLength(225)
+                        .HasColumnType("nvarchar(225)")
+                        .HasComment("실패 이유");
+
+                    b.Property<string>("Referer")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이전 페이지 주소");
+
+                    b.Property<bool>("Success")
+                        .HasColumnType("bit")
+                        .HasComment("로그인 성공 여부 (0: 실패, 1: 성공)");
+
+                    b.Property<string>("Url")
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("요청 주소");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(512)
+                        .HasColumnType("nvarchar(512)")
+                        .HasComment("User Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Account");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("MemberID", "Success");
+
+                    b.ToTable("MemberLoginLog", null, t =>
+                        {
+                            t.HasComment("로그인 기록");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberNameChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterName")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("바꾼 별명");
+
+                    b.Property<string>("BeforeName")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("이전 별명");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Referer")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이전 페이지 주소");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(512)
+                        .HasColumnType("nvarchar(512)")
+                        .HasComment("User Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("MemberID");
+
+                    b.ToTable("MemberNameChangeLog", null, t =>
+                        {
+                            t.HasComment("별명 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberSummaryChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterSummary")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("바꾼 한마디");
+
+                    b.Property<string>("BeforeSummary")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("이전 한마디");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("IP Address");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Referer")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이전 페이지 주소");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(512)
+                        .HasColumnType("nvarchar(512)")
+                        .HasComment("User Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("MemberID");
+
+                    b.ToTable("MemberSummaryChangeLog", null, t =>
+                        {
+                            t.HasComment("한마디 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Member", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime?>("AuthCertifiedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("본인인증 일시");
+
+                    b.Property<DateOnly?>("Birthday")
+                        .HasColumnType("date")
+                        .HasComment("생년월일");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("가입 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("탈퇴 일시");
+
+                    b.Property<DateTime?>("DeniedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("차단 일시");
+
+                    b.Property<string>("DeviceInfo")
+                        .HasMaxLength(400)
+                        .HasColumnType("nvarchar(400)")
+                        .HasComment("로그인 단말기 정보");
+
+                    b.Property<string>("Email")
+                        .IsRequired()
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("이메일");
+
+                    b.Property<DateTime?>("EmailVerifiedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("이메일 인증 일시");
+
+                    b.Property<string>("FirstName")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("본명(성)");
+
+                    b.Property<string>("FullName")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("본명");
+
+                    b.Property<int?>("Gender")
+                        .HasColumnType("int")
+                        .HasComment("성별");
+
+                    b.Property<string>("Icon")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("아이콘");
+
+                    b.Property<string>("Intro")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("자기소개");
+
+                    b.Property<string>("IpAddress")
+                        .HasMaxLength(45)
+                        .HasColumnType("nvarchar(45)")
+                        .HasComment("IP Address");
+
+                    b.Property<bool>("IsAdmin")
+                        .HasColumnType("bit")
+                        .HasComment("운영진 여부");
+
+                    b.Property<bool>("IsAuthCertified")
+                        .HasColumnType("bit")
+                        .HasComment("본인 인증 여부");
+
+                    b.Property<bool>("IsCreator")
+                        .HasColumnType("bit")
+                        .HasComment("크리에이터 여부");
+
+                    b.Property<bool>("IsDenied")
+                        .HasColumnType("bit")
+                        .HasComment("차단 여부");
+
+                    b.Property<bool>("IsEmailVerified")
+                        .HasColumnType("bit")
+                        .HasComment("이메일 인증 여부");
+
+                    b.Property<bool>("IsWithdraw")
+                        .HasColumnType("bit")
+                        .HasComment("탈퇴 여부");
+
+                    b.Property<DateTime?>("LastEmailChangedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 이메일 변경 일시");
+
+                    b.Property<DateTime?>("LastIntroChangedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 자기소개 변경 일시");
+
+                    b.Property<DateTime?>("LastLoginAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 로그인 일시");
+
+                    b.Property<string>("LastLoginIp")
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("마지막 로그인 IP");
+
+                    b.Property<string>("LastName")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("본명(이름)");
+
+                    b.Property<DateTime?>("LastNameChangedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 별명 변경 일시");
+
+                    b.Property<DateTime?>("LastSummaryChangedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 한마디 변경 일시");
+
+                    b.Property<int?>("MemberGradeID")
+                        .HasColumnType("int")
+                        .HasComment("회원등급 PK");
+
+                    b.Property<string>("Name")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("별명");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("비밀번호");
+
+                    b.Property<string>("PasswordHash")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<DateTime>("PasswordUpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("비밀번호 변경 일시");
+
+                    b.Property<string>("Phone")
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("연락처");
+
+                    b.Property<string>("SID")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("SID");
+
+                    b.Property<string>("SignupIP")
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("회원가입 시 IP");
+
+                    b.Property<string>("Summary")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("한마디");
+
+                    b.Property<string>("Thumb")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("썸네일");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.HasIndex("DeletedAt");
+
+                    b.HasIndex("Email")
+                        .IsUnique();
+
+                    b.HasIndex("FullName");
+
+                    b.HasIndex("Gender");
+
+                    b.HasIndex("IsAdmin");
+
+                    b.HasIndex("IsAuthCertified");
+
+                    b.HasIndex("IsCreator");
+
+                    b.HasIndex("IsDenied");
+
+                    b.HasIndex("IsEmailVerified");
+
+                    b.HasIndex("IsWithdraw");
+
+                    b.HasIndex("MemberGradeID");
+
+                    b.HasIndex("Name")
+                        .IsUnique()
+                        .HasFilter("[Name] IS NOT NULL");
+
+                    b.HasIndex("Phone");
+
+                    b.HasIndex("SID")
+                        .IsUnique();
+
+                    b.ToTable("Member", null, t =>
+                        {
+                            t.HasComment("회원 정보");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.MemberApprove", b =>
+                {
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<DateTime?>("DisclosureInvestConsentAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("투자 현황 공개 동의 일시");
+
+                    b.Property<bool>("IsDisclosureInvest")
+                        .HasColumnType("bit")
+                        .HasComment("투자 현황 공개 여부");
+
+                    b.Property<bool>("IsReceiveEmail")
+                        .HasColumnType("bit")
+                        .HasComment("E-MAIL 수신 여부");
+
+                    b.Property<bool>("IsReceiveNote")
+                        .HasColumnType("bit")
+                        .HasComment("쪽지 수신 여부");
+
+                    b.Property<bool>("IsReceiveSMS")
+                        .HasColumnType("bit")
+                        .HasComment("SMS 수신 여부");
+
+                    b.Property<DateTime?>("ReceiveEmailConsentAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("E-MAIL 수신 동의 일시");
+
+                    b.Property<DateTime?>("ReceiveNoteConsentAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("쪽지 수신 동의 일시");
+
+                    b.Property<DateTime?>("ReceiveSMSConsentAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("SMS 수신 동의 일시");
+
+                    b.HasKey("MemberID");
+
+                    b.ToTable("MemberApprove", null, t =>
+                        {
+                            t.HasComment("회원 동의 및 수신 여부");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.MemberGrade", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Description")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("설명");
+
+                    b.Property<string>("EngName")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("영문 명");
+
+                    b.Property<string>("Image")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("KorName")
+                        .IsRequired()
+                        .HasMaxLength(240)
+                        .HasColumnType("nvarchar(240)")
+                        .HasComment("한글 명");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<long>("RequiredAttendance")
+                        .HasColumnType("bigint")
+                        .HasComment("누적 출석 수");
+
+                    b.Property<int>("RequiredExp")
+                        .HasColumnType("int")
+                        .HasComment("누적 경험치");
+
+                    b.Property<string>("TextColor")
+                        .HasMaxLength(7)
+                        .HasColumnType("nvarchar(7)")
+                        .HasComment("표시 색상");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("EngName")
+                        .IsUnique();
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("KorName")
+                        .IsUnique();
+
+                    b.HasIndex("Order");
+
+                    b.HasIndex("Order", "IsActive");
+
+                    b.ToTable("MemberGrade", null, t =>
+                        {
+                            t.HasComment("회원 등급");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.MemberStats", b =>
+                {
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<long>("AttendanceCount")
+                        .HasColumnType("bigint")
+                        .HasComment("출석");
+
+                    b.Property<long>("BookmarkGivenCount")
+                        .HasColumnType("bigint")
+                        .HasComment("즐겨찾기 글 수");
+
+                    b.Property<long>("CommentCount")
+                        .HasColumnType("bigint")
+                        .HasComment("작성 댓글");
+
+                    b.Property<long>("Exp")
+                        .HasColumnType("bigint")
+                        .HasComment("경험치");
+
+                    b.Property<long>("FollowerCount")
+                        .HasColumnType("bigint")
+                        .HasComment("구독자");
+
+                    b.Property<long>("FollowingCount")
+                        .HasColumnType("bigint")
+                        .HasComment("구독 중");
+
+                    b.Property<long>("LikeGivenCount")
+                        .HasColumnType("bigint")
+                        .HasComment("누른 좋아요 수");
+
+                    b.Property<long>("LikeReceivedCount")
+                        .HasColumnType("bigint")
+                        .HasComment("받은 좋아요 수");
+
+                    b.Property<long>("LoginCount")
+                        .HasColumnType("bigint")
+                        .HasComment("로그인");
+
+                    b.Property<long>("PaymentCount")
+                        .HasColumnType("bigint")
+                        .HasComment("결제 횟수");
+
+                    b.Property<long>("PostCount")
+                        .HasColumnType("bigint")
+                        .HasComment("작성 게시글");
+
+                    b.Property<long>("ReportedCount")
+                        .HasColumnType("bigint")
+                        .HasComment("신고 당한 횟수");
+
+                    b.Property<byte[]>("RowVersion")
+                        .IsConcurrencyToken()
+                        .IsRequired()
+                        .ValueGeneratedOnAddOrUpdate()
+                        .HasColumnType("rowversion")
+                        .HasComment("동시성");
+
+                    b.Property<int>("SuspensionCount")
+                        .HasColumnType("int")
+                        .HasComment("정지 횟수");
+
+                    b.Property<long>("TotalCanceledAmount")
+                        .HasColumnType("bigint")
+                        .HasComment("누적 취소/환불 금액");
+
+                    b.Property<long>("TotalPaidAmount")
+                        .HasColumnType("bigint")
+                        .HasComment("누적 결제 금액");
+
+                    b.Property<int>("WarningCount")
+                        .HasColumnType("int")
+                        .HasComment("경고 횟수");
+
+                    b.HasKey("MemberID");
+
+                    b.ToTable("MemberStats", null, t =>
+                        {
+                            t.HasComment("회원 활동 집계");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.RefreshToken", b =>
+                {
+                    b.Property<long>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("bigint");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<DateTime>("ExpiresAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<bool>("IsRevoked")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("bit")
+                        .HasDefaultValue(false);
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime?>("RevokedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("nvarchar(256)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("Token")
+                        .IsUnique();
+
+                    b.ToTable("RefreshToken", null, t =>
+                        {
+                            t.HasComment("리프레시 토큰");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.News.RssFeedSource", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("소스 설명");
+
+                    b.Property<int>("IntervalMinutes")
+                        .HasColumnType("int")
+                        .HasComment("수집 주기 (분)");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("활성화 여부");
+
+                    b.Property<DateTime?>("LastFetchedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 수집 일시");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("소스 이름");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("RSS 피드 URL");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("Url")
+                        .IsUnique();
+
+                    b.ToTable("RssFeedSource", null, t =>
+                        {
+                            t.HasComment("RSS 피드 소스");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.News.RssNewsArticle", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Author")
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("작성자");
+
+                    b.Property<string>("Categories")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("카테고리 (JSON 배열)");
+
+                    b.Property<int>("CommentCount")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수");
+
+                    b.Property<string>("Content")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("본문 (content:encoded)");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수집 일시");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("요약");
+
+                    b.Property<string>("Guid")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("RSS GUID (중복 방지)");
+
+                    b.Property<string>("ImageUrl")
+                        .HasMaxLength(2000)
+                        .HasColumnType("nvarchar(2000)")
+                        .HasComment("썸네일 이미지 URL");
+
+                    b.Property<string>("Link")
+                        .HasMaxLength(2000)
+                        .HasColumnType("nvarchar(2000)")
+                        .HasComment("원본 링크");
+
+                    b.Property<DateTime?>("PublishedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("발행 일시");
+
+                    b.Property<int>("RssFeedSourceID")
+                        .HasColumnType("int")
+                        .HasComment("RSS 피드 소스 FK");
+
+                    b.Property<string>("SourceName")
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("출처명");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("기사 제목");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.HasIndex("PublishedAt");
+
+                    b.HasIndex("RssFeedSourceID");
+
+                    b.HasIndex("Guid", "RssFeedSourceID")
+                        .IsUnique()
+                        .HasFilter("[Guid] IS NOT NULL");
+
+                    b.ToTable("RssNewsArticle", null, t =>
+                        {
+                            t.HasComment("RSS 뉴스 기사");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Banner.BannerItem", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("DesktopImage")
+                        .HasMaxLength(1024)
+                        .HasColumnType("nvarchar(1024)")
+                        .HasComment("이미지(Desktop)");
+
+                    b.Property<DateTime?>("EndAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("사용 기간 - 종료");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Link")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("주소");
+
+                    b.Property<string>("MobileImage")
+                        .HasMaxLength(1024)
+                        .HasColumnType("nvarchar(1024)")
+                        .HasComment("이미지(Mobile)");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<int>("PositionID")
+                        .HasColumnType("int")
+                        .HasComment("배너 위치 ID");
+
+                    b.Property<DateTime?>("StartAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("사용 기간 - 시작");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("배너 명");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("Order");
+
+                    b.HasIndex("PositionID");
+
+                    b.HasIndex("PositionID", "Order", "IsActive");
+
+                    b.ToTable("BannerItem", null, t =>
+                        {
+                            t.HasComment("배너 아이템");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Banner.BannerPosition", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("위치 구분");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("위치 명");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Code")
+                        .IsUnique();
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("Code", "IsActive");
+
+                    b.ToTable("BannerPosition", null, t =>
+                        {
+                            t.HasComment("배너 위치");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Document", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("주소");
+
+                    b.Property<string>("Content")
+                        .HasMaxLength(5000)
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("제목");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Code")
+                        .IsUnique();
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("Subject");
+
+                    b.HasIndex("Code", "IsActive");
+
+                    b.ToTable("Document", null, t =>
+                        {
+                            t.HasComment("문서");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Faq.FaqCategory", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("주소");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("분류 명");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Code")
+                        .IsUnique();
+
+                    b.HasIndex("Order", "IsActive");
+
+                    b.HasIndex("Code", "Order", "IsActive");
+
+                    b.ToTable("FaqCategory", null, t =>
+                        {
+                            t.HasComment("FAQ 분류");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Faq.FaqItem", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Answer")
+                        .HasMaxLength(4000)
+                        .HasColumnType("nvarchar(4000)")
+                        .HasComment("답변");
+
+                    b.Property<int>("CategoryID")
+                        .HasColumnType("int")
+                        .HasComment("분류 ID");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<string>("Question")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("질문");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CategoryID");
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("Order");
+
+                    b.HasIndex("Order", "IsActive");
+
+                    b.HasIndex("CategoryID", "Order", "IsActive");
+
+                    b.ToTable("FaqItem", null, t =>
+                        {
+                            t.HasComment("FAQ 목록");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Popup.Popup", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Content")
+                        .HasMaxLength(4000)
+                        .HasColumnType("nvarchar(4000)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<DateTime?>("EndAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("사용 기간 - 종료");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Link")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("주소");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<int>("PositionID")
+                        .HasColumnType("int")
+                        .HasComment("팝업 위치 ID");
+
+                    b.Property<DateTime?>("StartAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("사용 기간 - 시작");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("제목");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Order");
+
+                    b.HasIndex("PositionID");
+
+                    b.HasIndex("Order", "IsActive");
+
+                    b.HasIndex("StartAt", "EndAt", "Order", "IsActive");
+
+                    b.ToTable("Popup", null, t =>
+                        {
+                            t.HasComment("팝업");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Popup.PopupPosition", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(30)
+                        .HasColumnType("nvarchar(30)")
+                        .HasComment("위치 구분");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("위치 명");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Code")
+                        .IsUnique();
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("Code", "IsActive");
+
+                    b.ToTable("PopupPosition", null, t =>
+                        {
+                            t.HasComment("팝업 위치");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Wallets.Wallet", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<Guid>("WalletKey")
+                        .HasColumnType("uniqueidentifier");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("MemberID")
+                        .IsUnique();
+
+                    b.HasIndex("WalletKey")
+                        .IsUnique();
+
+                    b.ToTable("Wallet", null, t =>
+                        {
+                            t.HasComment("회원 지갑");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Wallets.WalletBalance", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("Type")
+                        .HasColumnType("int");
+
+                    b.Property<Guid>("WalletKey")
+                        .HasColumnType("uniqueidentifier");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("WalletKey", "Type")
+                        .IsUnique();
+
+                    b.ToTable("WalletBalance", null, t =>
+                        {
+                            t.HasComment("회원 지갑 잔액");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Wallets.WalletTransaction", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BalanceType")
+                        .HasColumnType("int");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("Memo")
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)");
+
+                    b.Property<string>("RefID")
+                        .HasMaxLength(100)
+                        .HasColumnType("nvarchar(100)");
+
+                    b.Property<int>("TxType")
+                        .HasColumnType("int");
+
+                    b.Property<string>("UserID")
+                        .HasMaxLength(100)
+                        .HasColumnType("nvarchar(100)");
+
+                    b.Property<Guid>("WalletKey")
+                        .HasColumnType("uniqueidentifier");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.HasIndex("WalletKey");
+
+                    b.HasIndex("WalletKey", "CreatedAt");
+
+                    b.ToTable("WalletTransaction", null, t =>
+                        {
+                            t.HasComment("회원 거래 장부");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.Common.Config", b =>
+                {
+                    b.OwnsOne("Domain.Entities.Common.AccountConfig", "Account", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.Property<int?>("ChangeEmailDay")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_ChangeEmailDay")
+                                .HasComment("이메일 갱신 주기(일)");
+
+                            b1.Property<int?>("ChangeIntroDay")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_ChangeIntroDay")
+                                .HasComment("자기소개 갱신 주기(일)");
+
+                            b1.Property<int?>("ChangeNameDay")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_ChangeNameDay")
+                                .HasComment("별명 갱신 주기(일)");
+
+                            b1.Property<int?>("ChangePasswordDay")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_ChangePasswordDay")
+                                .HasComment("비밀번호 갱신 주기(일)");
+
+                            b1.Property<int?>("ChangeSummaryDay")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_ChangeSummaryDay")
+                                .HasComment("한마디 갱신 주기(일)");
+
+                            b1.Property<string>("DeniedEmailList")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Account_DeniedEmailList")
+                                .HasComment("금지 이메일");
+
+                            b1.Property<string>("DeniedNameList")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Account_DeniedNameList")
+                                .HasComment("금지 별명");
+
+                            b1.Property<bool>("IsLoginEmailVerifiedOnly")
+                                .HasColumnType("bit");
+
+                            b1.Property<bool>("IsRegisterBlock")
+                                .HasColumnType("bit")
+                                .HasColumnName("Account_IsRegisterBlock")
+                                .HasComment("회원가입 차단");
+
+                            b1.Property<bool>("IsRegisterEmailAuth")
+                                .HasColumnType("bit")
+                                .HasColumnName("Account_IsRegisterEmailAuth")
+                                .HasComment("회원가입 시 이메일 인증");
+
+                            b1.Property<int?>("MaxLoginTryCount")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_MaxLoginTryCount")
+                                .HasComment("로그인 시도 제한 횟수");
+
+                            b1.Property<int?>("MaxLoginTryLimitSecond")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_MaxLoginTryLimitSecond")
+                                .HasComment("로그인 시도 제한 시간(초)");
+
+                            b1.Property<int?>("PasswordMinLength")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_PasswordMinLength")
+                                .HasComment("비밀번호 최소 길이");
+
+                            b1.Property<int?>("PasswordNumbersLength")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_PasswordNumbersLength")
+                                .HasComment("비밀번호 최소 숫자 수");
+
+                            b1.Property<int?>("PasswordSpecialcharsLength")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_PasswordSpecialcharsLength")
+                                .HasComment("비밀번호 최소 특수문자 수");
+
+                            b1.Property<int?>("PasswordUppercaseLength")
+                                .HasColumnType("int")
+                                .HasColumnName("Account_PasswordUppercaseLength")
+                                .HasComment("비밀번호 최소 대문자 수");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.BasicConfig", "Basic", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("AdminWhiteIPList")
+                                .HasMaxLength(1000)
+                                .HasColumnType("nvarchar(1000)")
+                                .HasColumnName("Basic_AdminWhiteIPList")
+                                .HasComment("관리자단 접근 가능 IP");
+
+                            b1.Property<string>("BlockAlertContent")
+                                .HasMaxLength(5000)
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Basic_BlockAlertContent")
+                                .HasComment("차단 시 안내문 내용");
+
+                            b1.Property<string>("BlockAlertTitle")
+                                .HasMaxLength(200)
+                                .HasColumnType("nvarchar(200)")
+                                .HasColumnName("Basic_BlockAlertTitle")
+                                .HasComment("차단 시 안내문 제목");
+
+                            b1.Property<string>("FromEmail")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Basic_FromEmail")
+                                .HasComment("송수신 이메일");
+
+                            b1.Property<string>("FromName")
+                                .HasMaxLength(30)
+                                .HasColumnType("nvarchar(30)")
+                                .HasColumnName("Basic_FromName")
+                                .HasComment("송수신자 이름");
+
+                            b1.Property<string>("FrontWhiteIPList")
+                                .HasMaxLength(1000)
+                                .HasColumnType("nvarchar(1000)")
+                                .HasColumnName("Basic_FrontWhiteIPList")
+                                .HasComment("사용자단 접근 가능 IP");
+
+                            b1.Property<bool>("IsMaintenance")
+                                .HasColumnType("bit")
+                                .HasColumnName("Basic_IsMaintenance")
+                                .HasComment("점검 여부");
+
+                            b1.Property<string>("MaintenanceContent")
+                                .HasMaxLength(5000)
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Basic_MaintenanceContent")
+                                .HasComment("점검 내용");
+
+                            b1.Property<string>("RootID")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Basic_RootID")
+                                .HasComment("최고 관리자 ID");
+
+                            b1.Property<string>("SiteName")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Basic_SiteName")
+                                .HasComment("사이트 이름");
+
+                            b1.Property<string>("SiteURL")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Basic_SiteURL")
+                                .HasComment("사이트 주소");
+
+                            b1.Property<bool>("SmtpEnableSSL")
+                                .HasColumnType("bit")
+                                .HasColumnName("Basic_SmtpEnableSSL")
+                                .HasComment("SMTP Enable SSL");
+
+                            b1.Property<string>("SmtpPassword")
+                                .HasMaxLength(200)
+                                .HasColumnType("nvarchar(200)")
+                                .HasColumnName("Basic_SmtpPassword")
+                                .HasComment("SMTP Password");
+
+                            b1.Property<int?>("SmtpPort")
+                                .HasColumnType("int")
+                                .HasColumnName("Basic_SmtpPort")
+                                .HasComment("SMTP Port");
+
+                            b1.Property<string>("SmtpServer")
+                                .HasMaxLength(200)
+                                .HasColumnType("nvarchar(200)")
+                                .HasColumnName("Basic_SmtpServer")
+                                .HasComment("SMTP Server");
+
+                            b1.Property<string>("SmtpUsername")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Basic_SmtpUsername")
+                                .HasComment("SMTP Username");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.CompanyConfig", "Company", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("AddedSaleNo")
+                                .HasMaxLength(20)
+                                .HasColumnType("nvarchar(20)")
+                                .HasColumnName("Company_AddedSaleNo")
+                                .HasComment("부가통신 사업자번호");
+
+                            b1.Property<string>("Address")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Company_Address")
+                                .HasComment("사업장 소재지");
+
+                            b1.Property<string>("AdminEmail")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Company_AdminEmail")
+                                .HasComment("정보관리책임자 이메일");
+
+                            b1.Property<string>("AdminName")
+                                .HasMaxLength(70)
+                                .HasColumnType("nvarchar(70)")
+                                .HasColumnName("Company_AdminName")
+                                .HasComment("정보관리책임자");
+
+                            b1.Property<string>("BankCode")
+                                .HasMaxLength(10)
+                                .HasColumnType("nvarchar(10)")
+                                .HasColumnName("Company_BankCode")
+                                .HasComment("입금계좌 - 은행");
+
+                            b1.Property<string>("BankNumber")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Company_BankNumber")
+                                .HasComment("입금계좌 - 계좌번호");
+
+                            b1.Property<string>("BankOwner")
+                                .HasMaxLength(70)
+                                .HasColumnType("nvarchar(70)")
+                                .HasColumnName("Company_BankOwner")
+                                .HasComment("입금계좌 - 예금주");
+
+                            b1.Property<string>("Fax")
+                                .HasMaxLength(20)
+                                .HasColumnType("nvarchar(20)")
+                                .HasColumnName("Company_Fax")
+                                .HasComment("FAX");
+
+                            b1.Property<string>("Hosting")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Company_Hosting")
+                                .HasComment("호스팅 서비스");
+
+                            b1.Property<string>("Name")
+                                .HasMaxLength(70)
+                                .HasColumnType("nvarchar(70)")
+                                .HasColumnName("Company_Name")
+                                .HasComment("상호 명");
+
+                            b1.Property<string>("Owner")
+                                .HasMaxLength(50)
+                                .HasColumnType("nvarchar(50)")
+                                .HasColumnName("Company_Owner")
+                                .HasComment("대표자 명");
+
+                            b1.Property<string>("RegNo")
+                                .HasMaxLength(100)
+                                .HasColumnType("nvarchar(100)")
+                                .HasColumnName("Company_RegNo")
+                                .HasComment("사업자 등록 번호");
+
+                            b1.Property<string>("RetailSaleNo")
+                                .HasMaxLength(20)
+                                .HasColumnType("nvarchar(20)")
+                                .HasColumnName("Company_RetailSaleNo")
+                                .HasComment("통신판매업 신고번호");
+
+                            b1.Property<string>("SiteUrl")
+                                .HasMaxLength(200)
+                                .HasColumnType("nvarchar(200)")
+                                .HasColumnName("Company_SiteUrl")
+                                .HasComment("사이트 주소");
+
+                            b1.Property<string>("Tel")
+                                .HasMaxLength(20)
+                                .HasColumnType("nvarchar(20)")
+                                .HasColumnName("Company_Tel")
+                                .HasComment("대표 전화번호");
+
+                            b1.Property<string>("ZipCode")
+                                .HasMaxLength(8)
+                                .HasColumnType("nvarchar(8)")
+                                .HasColumnName("Company_ZipCode")
+                                .HasComment("사업장 주소(우편번호)");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.CryptoConfig", "Crypto", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.Property<int>("MainPageCoinCount")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("int")
+                                .HasDefaultValue(10)
+                                .HasColumnName("Crypto_MainPageCoinCount")
+                                .HasComment("메인 페이지 기본 표시 코인 수");
+
+                            b1.Property<decimal>("PlungeThreshold")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("decimal(5,2)")
+                                .HasDefaultValue(-5.0m)
+                                .HasColumnName("Crypto_PlungeThreshold")
+                                .HasComment("급락 임계값 (%)");
+
+                            b1.Property<decimal>("SurgeThreshold")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("decimal(5,2)")
+                                .HasDefaultValue(5.0m)
+                                .HasColumnName("Crypto_SurgeThreshold")
+                                .HasComment("급등 임계값 (%)");
+
+                            b1.Property<int>("TickerRefreshSeconds")
+                                .ValueGeneratedOnAdd()
+                                .HasColumnType("int")
+                                .HasDefaultValue(5)
+                                .HasColumnName("Crypto_TickerRefreshSeconds")
+                                .HasComment("시세 업데이트 주기 (초)");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.EmailTemplateConfig", "EmailTemplate", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("ChangedEmailFormContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_ChangedEmailFormContent")
+                                .HasComment("이메일 변경 완료 - 내용");
+
+                            b1.Property<string>("ChangedEmailFormTitle")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_ChangedEmailFormTitle")
+                                .HasComment("이메일 변경 완료 - 제목");
+
+                            b1.Property<string>("ChangedPasswordEmailFormContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_ChangedPasswordEmailFormContent")
+                                .HasComment("비밀번호 변경 완료 - 내용");
+
+                            b1.Property<string>("ChangedPasswordEmailFormTitle")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_ChangedPasswordEmailFormTitle")
+                                .HasComment("비밀번호 변경 완료 - 제목");
+
+                            b1.Property<string>("EmailVerifyFormContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_EmailVerifyFormContent")
+                                .HasComment("이메일 변경 시 - 내용");
+
+                            b1.Property<string>("EmailVerifyFormTitle")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_EmailVerifyFormTitle")
+                                .HasComment("이메일 변경 시 - 제목");
+
+                            b1.Property<string>("RegisterEmailFormContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_RegisterEmailFormContent")
+                                .HasComment("회원가입 시 - 내용");
+
+                            b1.Property<string>("RegisterEmailFormTitle")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_RegisterEmailFormTitle")
+                                .HasComment("회원가입 시 - 제목");
+
+                            b1.Property<string>("RegistrationEmailFormContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_RegistrationEmailFormContent")
+                                .HasComment("회원가입 완료 - 내용");
+
+                            b1.Property<string>("RegistrationEmailFormTitle")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_RegistrationEmailFormTitle")
+                                .HasComment("회원가입 완료 - 제목");
+
+                            b1.Property<string>("ResetPasswordEmailFormContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_ResetPasswordEmailFormContent")
+                                .HasComment("비밀번호 재설정 - 내용");
+
+                            b1.Property<string>("ResetPasswordEmailFormTitle")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_ResetPasswordEmailFormTitle")
+                                .HasComment("비밀번호 재설정 - 제목");
+
+                            b1.Property<string>("WithdrawEmailFormContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_WithdrawEmailFormContent")
+                                .HasComment("회원탈퇴 시 - 내용");
+
+                            b1.Property<string>("WithdrawEmailFormTitle")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("EmailTemplate_WithdrawEmailFormTitle")
+                                .HasComment("회원탈퇴 시 - 제목");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.ExternalApiConfig", "External", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("GoogleAppId")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("External_GoogleAppId")
+                                .HasComment("Google APP ID");
+
+                            b1.Property<string>("GoogleClientId")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("External_GoogleClientId")
+                                .HasComment("Google Client ID");
+
+                            b1.Property<string>("GoogleClientSecretEnc")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("External_GoogleClientSecretEnc")
+                                .HasComment("Google Client Secret (암호화 저장 권장)");
+
+                            b1.Property<string>("YouTubeApiKeyEnc")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("External_YouTubeApiKeyEnc")
+                                .HasComment("YouTube API Key (암호화 저장 권장)");
+
+                            b1.Property<string>("YouTubeApiName")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("External_YouTubeApiName")
+                                .HasComment("YouTube API Name");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.ImagesConfig", "Images", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("AppIcon_192")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Images_AppIcon_192")
+                                .HasComment("App-icon-192");
+
+                            b1.Property<string>("AppIcon_512")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Images_AppIcon_512")
+                                .HasComment("App-icon-512");
+
+                            b1.Property<string>("AppleTouchIcon")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Images_AppleTouchIcon")
+                                .HasComment("Apple-touch-icon");
+
+                            b1.Property<string>("Favicon")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Images_Favicon")
+                                .HasComment("Favicon");
+
+                            b1.Property<string>("LogoHorizontal")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Images_LogoHorizontal")
+                                .HasComment("Logo-horizontal");
+
+                            b1.Property<string>("LogoSquare")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Images_LogoSquare")
+                                .HasComment("Logo-square");
+
+                            b1.Property<string>("OgDefault")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Images_OgDefault")
+                                .HasComment("og-default");
+
+                            b1.Property<string>("TwitterImage")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Images_TwitterImage")
+                                .HasComment("Twitter-image");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.MetaConfig", "Meta", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("Adds")
+                                .HasColumnType("nvarchar(max)");
+
+                            b1.Property<string>("ApplicationName")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Meta_ApplicationName")
+                                .HasComment("Meta Application Name");
+
+                            b1.Property<string>("Author")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Meta_Author")
+                                .HasComment("Meta Author");
+
+                            b1.Property<string>("Description")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Meta_Description")
+                                .HasComment("Meta Description");
+
+                            b1.Property<string>("Generator")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Meta_Generator")
+                                .HasComment("Meta Generator");
+
+                            b1.Property<string>("Keywords")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Meta_Keywords")
+                                .HasComment("Meta Keywords");
+
+                            b1.Property<string>("Robots")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Meta_Robots")
+                                .HasComment("Meta Robots");
+
+                            b1.Property<string>("Viewport")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasColumnName("Meta_Viewport")
+                                .HasComment("Meta Viewport");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.PaymentConfig", "Payment", b1 =>
+                        {
+                            b1.Property<int>("ConfigID")
+                                .HasColumnType("int");
+
+                            b1.HasKey("ConfigID");
+
+                            b1.ToTable("Config");
+
+                            b1.WithOwner()
+                                .HasForeignKey("ConfigID");
+                        });
+
+                    b.Navigation("Account")
+                        .IsRequired();
+
+                    b.Navigation("Basic")
+                        .IsRequired();
+
+                    b.Navigation("Company")
+                        .IsRequired();
+
+                    b.Navigation("Crypto")
+                        .IsRequired();
+
+                    b.Navigation("EmailTemplate")
+                        .IsRequired();
+
+                    b.Navigation("External")
+                        .IsRequired();
+
+                    b.Navigation("Images")
+                        .IsRequired();
+
+                    b.Navigation("Meta")
+                        .IsRequired();
+
+                    b.Navigation("Payment")
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Domain.Entities.Crypto.CoinCategoryMap", b =>
+                {
+                    b.HasOne("Domain.Entities.Crypto.CoinCategory", "CoinCategory")
+                        .WithMany("CoinCategoryMap")
+                        .HasForeignKey("CategoryID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Crypto.Coin", "Coin")
+                        .WithMany("CoinCategoryMap")
+                        .HasForeignKey("CoinID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Coin");
+
+                    b.Navigation("CoinCategory");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Crypto.CoinMarket", b =>
+                {
+                    b.HasOne("Domain.Entities.Crypto.Coin", "Coin")
+                        .WithMany("CoinMarket")
+                        .HasForeignKey("CoinID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Coin");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.Board", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.BoardGroup", "BoardGroup")
+                        .WithMany("Board")
+                        .HasForeignKey("BoardGroupID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Crypto.Coin", "Coin")
+                        .WithMany()
+                        .HasForeignKey("CoinID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("BoardGroup");
+
+                    b.Navigation("Coin");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.BoardManager", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany("BoardManager")
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.BoardMeta", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", null)
+                        .WithOne("BoardMeta")
+                        .HasForeignKey("Domain.Entities.Forum.Boards.BoardMeta", "BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaComment", "Comment", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AllowDeleteProtection")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_AllowDeleteProtection");
+
+                            b1.Property<bool>("AllowDisLike")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_AllowDisLike");
+
+                            b1.Property<bool>("AllowLike")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_AllowLike");
+
+                            b1.Property<bool>("AllowSecret")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_AllowSecret");
+
+                            b1.Property<bool>("AllowUpdateProtection")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_AllowUpdateProtection");
+
+                            b1.Property<int>("BlameHideCount")
+                                .HasColumnType("int")
+                                .HasColumnName("Comment_BlameHideCount");
+
+                            b1.Property<string>("ContentPlaceholder")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Comment_ContentPlaceholder");
+
+                            b1.Property<int>("DeleteProtectionDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Comment_DeleteProtectionDays");
+
+                            b1.Property<bool>("EnableComment")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_EnableComment");
+
+                            b1.Property<bool>("EnableCommentUpdateLog")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_EnableCommentUpdateLog");
+
+                            b1.Property<bool>("EnableEditor")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_EnableEditor");
+
+                            b1.Property<int>("MaxContentLength")
+                                .HasColumnType("int")
+                                .HasColumnName("Comment_MaxContentLength");
+
+                            b1.Property<int>("MinContentLength")
+                                .HasColumnType("int")
+                                .HasColumnName("Comment_MinContentLength");
+
+                            b1.Property<int>("PerPage")
+                                .HasColumnType("int")
+                                .HasColumnName("Comment_PerPage");
+
+                            b1.Property<bool>("ShowMemberIcon")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_ShowMemberIcon");
+
+                            b1.Property<bool>("ShowMemberThumb")
+                                .HasColumnType("bit")
+                                .HasColumnName("Comment_ShowMemberThumb");
+
+                            b1.Property<int>("UpdateProtectionDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Comment_UpdateProtectionDays");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaExp", "Exp", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<int>("CommentWriteExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_CommentWriteExp");
+
+                            b1.Property<int>("CommentWriteExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_CommentWriteExpWithinDays");
+
+                            b1.Property<int>("CommentWriteUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_CommentWriteUndoExp");
+
+                            b1.Property<bool>("EnableExp")
+                                .HasColumnType("bit")
+                                .HasColumnName("Exp_EnableExp");
+
+                            b1.Property<short>("FileDownloadExp")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Exp_FileDownloadExp");
+
+                            b1.Property<int>("FileUploadExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_FileUploadExp");
+
+                            b1.Property<int>("FileUploadExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_FileUploadExpWithinDays");
+
+                            b1.Property<int>("FileUploadUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_FileUploadUndoExp");
+
+                            b1.Property<int>("OtherCommentDisLikeExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherCommentDisLikeExp");
+
+                            b1.Property<int>("OtherCommentDisLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherCommentDisLikeExpWithinDays");
+
+                            b1.Property<int>("OtherCommentDisLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherCommentDisLikeUndoExp");
+
+                            b1.Property<int>("OtherCommentLikeExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherCommentLikeExp");
+
+                            b1.Property<int>("OtherCommentLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherCommentLikeExpWithinDays");
+
+                            b1.Property<int>("OtherCommentLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherCommentLikeUndoExp");
+
+                            b1.Property<int>("OtherPostDisLikeExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherPostDisLikeExp");
+
+                            b1.Property<int>("OtherPostDisLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherPostDisLikeExpWithinDays");
+
+                            b1.Property<int>("OtherPostDisLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherPostDisLikeUndoExp");
+
+                            b1.Property<int>("OtherPostLikeExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherPostLikeExp");
+
+                            b1.Property<int>("OtherPostLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherPostLikeExpWithinDays");
+
+                            b1.Property<int>("OtherPostLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherPostLikeUndoExp");
+
+                            b1.Property<short>("OtherPostReadExp")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Exp_OtherPostReadExp");
+
+                            b1.Property<int>("OtherPostReadExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherPostReadExpWithinDays");
+
+                            b1.Property<int>("OtherPostReadUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OtherPostReadUndoExp");
+
+                            b1.Property<short>("OwnCommentDisLikeExp")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Exp_OwnCommentDisLikeExp");
+
+                            b1.Property<int>("OwnCommentDisLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnCommentDisLikeExpWithinDays");
+
+                            b1.Property<int>("OwnCommentDisLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnCommentDisLikeUndoExp");
+
+                            b1.Property<int>("OwnCommentLikeExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnCommentLikeExp");
+
+                            b1.Property<int>("OwnCommentLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnCommentLikeExpWithinDays");
+
+                            b1.Property<int>("OwnCommentLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnCommentLikeUndoExp");
+
+                            b1.Property<short>("OwnPostDisLikeExp")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Exp_OwnPostDisLikeExp");
+
+                            b1.Property<int>("OwnPostDisLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnPostDisLikeExpWithinDays");
+
+                            b1.Property<int>("OwnPostDisLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnPostDisLikeUndoExp");
+
+                            b1.Property<int>("OwnPostLikeExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnPostLikeExp");
+
+                            b1.Property<int>("OwnPostLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnPostLikeExpWithinDays");
+
+                            b1.Property<int>("OwnPostLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnPostLikeUndoExp");
+
+                            b1.Property<int>("OwnPostReadExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnPostReadExp");
+
+                            b1.Property<int>("OwnPostReadExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnPostReadExpWithinDays");
+
+                            b1.Property<int>("OwnPostReadUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_OwnPostReadUndoExp");
+
+                            b1.Property<int>("PostWriteExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_PostWriteExp");
+
+                            b1.Property<int>("PostWriteExpWithinDays")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_PostWriteExpWithinDays");
+
+                            b1.Property<int>("PostWriteUndoExp")
+                                .HasColumnType("int")
+                                .HasColumnName("Exp_PostWriteUndoExp");
+
+                            b1.Property<bool>("ShowExpGuide")
+                                .HasColumnType("bit")
+                                .HasColumnName("Exp_ShowExpGuide");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaGeneral", "General", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AllowDeleteProtection")
+                                .HasColumnType("bit")
+                                .HasColumnName("General_AllowDeleteProtection");
+
+                            b1.Property<bool>("AllowUpdateProtection")
+                                .HasColumnType("bit")
+                                .HasColumnName("General_AllowUpdateProtection");
+
+                            b1.Property<int>("DeleteProtectionDays")
+                                .HasColumnType("int")
+                                .HasColumnName("General_DeleteProtectionDays");
+
+                            b1.Property<bool>("EnableFileDownLog")
+                                .HasColumnType("bit")
+                                .HasColumnName("General_EnableFileDownLog");
+
+                            b1.Property<bool>("EnableLinkClickLog")
+                                .HasColumnType("bit")
+                                .HasColumnName("General_EnableLinkClickLog");
+
+                            b1.Property<bool>("EnablePostUpdateLog")
+                                .HasColumnType("bit")
+                                .HasColumnName("General_EnablePostUpdateLog");
+
+                            b1.Property<int>("UpdateProtectionDays")
+                                .HasColumnType("int")
+                                .HasColumnName("General_UpdateProtectionDays");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaList", "List", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AlwaysShowWriteButton")
+                                .HasColumnType("bit")
+                                .HasColumnName("List_AlwaysShowWriteButton");
+
+                            b1.Property<bool>("ExceptNotice")
+                                .HasColumnType("bit")
+                                .HasColumnName("List_ExceptNotice");
+
+                            b1.Property<bool>("ExceptSpeaker")
+                                .HasColumnType("bit")
+                                .HasColumnName("List_ExceptSpeaker");
+
+                            b1.Property<string>("FooterContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("List_FooterContent");
+
+                            b1.Property<string>("HeaderContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("List_HeaderContent");
+
+                            b1.Property<bool>("IsHotIcon")
+                                .HasColumnType("bit")
+                                .HasColumnName("List_IsHotIcon");
+
+                            b1.Property<bool>("IsNewIcon")
+                                .HasColumnType("bit")
+                                .HasColumnName("List_IsNewIcon");
+
+                            b1.Property<byte?>("Layout")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("List_Layout");
+
+                            b1.Property<byte>("PerPage")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("List_PerPage");
+
+                            b1.Property<bool>("ShowFooter")
+                                .HasColumnType("bit")
+                                .HasColumnName("List_ShowFooter");
+
+                            b1.Property<bool>("ShowFooterListView")
+                                .HasColumnType("bit")
+                                .HasColumnName("List_ShowFooterListView");
+
+                            b1.Property<bool>("ShowHeader")
+                                .HasColumnType("bit")
+                                .HasColumnName("List_ShowHeader");
+
+                            b1.Property<byte?>("Sort")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("List_Sort");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaNotify", "Notify", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<byte?>("CommentWriteNotify")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("Notify_CommentWriteNotify");
+
+                            b1.Property<byte?>("PostWriteNotify")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("Notify_PostWriteNotify");
+
+                            b1.Property<byte?>("ReplyWriteNotify")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("Notify_ReplyWriteNotify");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaNotifyTemplate", "NotifyTemplate", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("CommentWriteEmailNotifyContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("NotifyTemplate_CommentWriteEmailNotifyContent");
+
+                            b1.Property<string>("CommentWriteEmailNotifySubject")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("NotifyTemplate_CommentWriteEmailNotifySubject");
+
+                            b1.Property<string>("PostWriteEmailNotifyContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("NotifyTemplate_PostWriteEmailNotifyContent");
+
+                            b1.Property<string>("PostWriteEmailNotifySubject")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("NotifyTemplate_PostWriteEmailNotifySubject");
+
+                            b1.Property<string>("ReplyWriteEmailNotifyContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("NotifyTemplate_ReplyWriteEmailNotifyContent");
+
+                            b1.Property<string>("ReplyWriteEmailNotifySubject")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("NotifyTemplate_ReplyWriteEmailNotifySubject");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaPermission", "Permission", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<short>("BoardAccess")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Permission_BoardAccess");
+
+                            b1.Property<short>("CommentView")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Permission_CommentView");
+
+                            b1.Property<short>("CommentWrite")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Permission_CommentWrite");
+
+                            b1.Property<short>("FileDownload")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Permission_FileDownload");
+
+                            b1.Property<short>("FileUpload")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Permission_FileUpload");
+
+                            b1.Property<short>("PostView")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Permission_PostView");
+
+                            b1.Property<short>("PostWrite")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Permission_PostWrite");
+
+                            b1.Property<short>("ReplyWrite")
+                                .HasColumnType("smallint")
+                                .HasColumnName("Permission_ReplyWrite");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaView", "View", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AllowBlame")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowBlame");
+
+                            b1.Property<bool>("AllowBookmark")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowBookmark");
+
+                            b1.Property<bool>("AllowContentLinkTargetBlank")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowContentLinkTargetBlank");
+
+                            b1.Property<bool>("AllowDislike")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowDislike");
+
+                            b1.Property<bool>("AllowLike")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowLike");
+
+                            b1.Property<bool>("AllowPostUrlCopy")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowPostUrlCopy");
+
+                            b1.Property<bool>("AllowPostUrlQrCode")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowPostUrlQrCode");
+
+                            b1.Property<bool>("AllowPrevNextBotton")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowPrevNextBotton");
+
+                            b1.Property<bool>("AllowPrint")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowPrint");
+
+                            b1.Property<bool>("AllowSnsShare")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_AllowSnsShare");
+
+                            b1.Property<int>("BlameHideCount")
+                                .HasColumnType("int")
+                                .HasColumnName("View_BlameHideCount");
+
+                            b1.Property<bool>("ShowMemberIcon")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_ShowMemberIcon");
+
+                            b1.Property<bool>("ShowMemberRegDate")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_ShowMemberRegDate");
+
+                            b1.Property<bool>("ShowMemberSummary")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_ShowMemberSummary");
+
+                            b1.Property<bool>("ShowMemberThumb")
+                                .HasColumnType("bit")
+                                .HasColumnName("View_ShowMemberThumb");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Forum.Boards.BoardMetaWrite", "Write", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AllowEditor")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_AllowEditor");
+
+                            b1.Property<bool>("AllowFile")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_AllowFile");
+
+                            b1.Property<bool>("AllowImage")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_AllowImage");
+
+                            b1.Property<bool>("AllowMedia")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_AllowMedia");
+
+                            b1.Property<bool>("AllowPrefix")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_AllowPrefix");
+
+                            b1.Property<bool>("AllowSecret")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_AllowSecret");
+
+                            b1.Property<bool>("AllowTag")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_AllowTag");
+
+                            b1.Property<string>("DefaultContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Write_DefaultContent");
+
+                            b1.Property<string>("DefaultSubject")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Write_DefaultSubject");
+
+                            b1.Property<string>("FileUploadExtension")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Write_FileUploadExtension");
+
+                            b1.Property<byte>("FileUploadLimit")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("Write_FileUploadLimit");
+
+                            b1.Property<int>("FileUploadMaxSize")
+                                .HasColumnType("int")
+                                .HasColumnName("Write_FileUploadMaxSize");
+
+                            b1.Property<string>("FooterContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Write_FooterContent");
+
+                            b1.Property<string>("HeaderContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasColumnName("Write_HeaderContent");
+
+                            b1.Property<byte>("ImageUploadLimit")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("Write_ImageUploadLimit");
+
+                            b1.Property<int>("ImageUploadMaxSize")
+                                .HasColumnType("int")
+                                .HasColumnName("Write_ImageUploadMaxSize");
+
+                            b1.Property<byte>("MediaUploadLimit")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("Write_MediaUploadLimit");
+
+                            b1.Property<bool>("RequiredPrefix")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_RequiredPrefix");
+
+                            b1.Property<bool>("ShowFooter")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_ShowFooter");
+
+                            b1.Property<bool>("ShowHeader")
+                                .HasColumnType("bit")
+                                .HasColumnName("Write_ShowHeader");
+
+                            b1.Property<byte>("TagLimit")
+                                .HasColumnType("tinyint")
+                                .HasColumnName("Write_TagLimit");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.Navigation("Comment")
+                        .IsRequired();
+
+                    b.Navigation("Exp")
+                        .IsRequired();
+
+                    b.Navigation("General")
+                        .IsRequired();
+
+                    b.Navigation("List")
+                        .IsRequired();
+
+                    b.Navigation("Notify")
+                        .IsRequired();
+
+                    b.Navigation("NotifyTemplate")
+                        .IsRequired();
+
+                    b.Navigation("Permission")
+                        .IsRequired();
+
+                    b.Navigation("View")
+                        .IsRequired();
+
+                    b.Navigation("Write")
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.BoardPrefix", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany("BoardPrefix")
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.Comment", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "MentionMember")
+                        .WithMany()
+                        .HasForeignKey("MentionMemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Parent")
+                        .WithMany("Children")
+                        .HasForeignKey("ParentID")
+                        .OnDelete(DeleteBehavior.Restrict);
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("Comment")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("MentionMember");
+
+                    b.Navigation("Parent");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentFile", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentFile")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany()
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentImage", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentImage")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany()
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentLink", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentLink")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany()
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentMedia", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentMedia")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany()
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentMention", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithOne("CommentMention")
+                        .HasForeignKey("Domain.Entities.Forum.Comments.CommentMention", "CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany()
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentReaction", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentReaction")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany()
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.CommentReport", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentReport")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany()
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.CommentFileDownLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Comments.CommentFile", "CommentFile")
+                        .WithMany()
+                        .HasForeignKey("CommentFileID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentFileDownLog")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Restrict);
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("CommentFile");
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.CommentLinkClickLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentLinkClickLog")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Comments.CommentLink", "CommentLink")
+                        .WithMany()
+                        .HasForeignKey("CommentLinkID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Restrict);
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("CommentLink");
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.CommentUpdateLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Comments.Comment", "Comment")
+                        .WithMany("CommentUpdateLog")
+                        .HasForeignKey("CommentID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Comment");
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.PostFileDownLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Restrict);
+
+                    b.HasOne("Domain.Entities.Forum.Posts.PostFile", "PostFile")
+                        .WithMany()
+                        .HasForeignKey("PostFileID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostFileDownLog")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+
+                    b.Navigation("PostFile");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.PostLinkClickLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Restrict);
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostLinkClickLog")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.PostLink", "PostLink")
+                        .WithMany()
+                        .HasForeignKey("PostLinkID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+
+                    b.Navigation("PostLink");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Logs.PostUpdateLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostUpdateLog")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.Post", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany("Post")
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Boards.BoardPrefix", "BoardPrefix")
+                        .WithMany()
+                        .HasForeignKey("BoardPrefixID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Board");
+
+                    b.Navigation("BoardPrefix");
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostBookmark", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostBookmark")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostFile", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostFile")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostImage", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostImage")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostLink", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostLink")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostMedia", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostMedia")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostReaction", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostReaction")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostReport", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostReport")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.PostTag", b =>
+                {
+                    b.HasOne("Domain.Entities.Forum.Boards.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Restrict)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Post", "Post")
+                        .WithMany("PostTag")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("Domain.Entities.Forum.Posts.Tag", "Tag")
+                        .WithMany("PostTag")
+                        .HasForeignKey("TagID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Post");
+
+                    b.Navigation("Tag");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Channel", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithOne("Channel")
+                        .HasForeignKey("Domain.Entities.Members.Channel", "MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberEmailChangeLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberExpLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberIntroChangeLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberLoginLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberNameChangeLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Logs.MemberSummaryChangeLog", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Member", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.MemberGrade", "MemberGrade")
+                        .WithMany()
+                        .HasForeignKey("MemberGradeID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("MemberGrade");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.MemberApprove", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithOne("MemberApprove")
+                        .HasForeignKey("Domain.Entities.Members.MemberApprove", "MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.MemberStats", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithOne("MemberStats")
+                        .HasForeignKey("Domain.Entities.Members.MemberStats", "MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.RefreshToken", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.News.RssNewsArticle", b =>
+                {
+                    b.HasOne("Domain.Entities.News.RssFeedSource", "RssFeedSource")
+                        .WithMany("RssNewsArticle")
+                        .HasForeignKey("RssFeedSourceID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("RssFeedSource");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Banner.BannerItem", b =>
+                {
+                    b.HasOne("Domain.Entities.Page.Banner.BannerPosition", "BannerPosition")
+                        .WithMany("BannerItems")
+                        .HasForeignKey("PositionID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("BannerPosition");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Faq.FaqItem", b =>
+                {
+                    b.HasOne("Domain.Entities.Page.Faq.FaqCategory", "FaqCategory")
+                        .WithMany("FaqItems")
+                        .HasForeignKey("CategoryID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("FaqCategory");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Popup.Popup", b =>
+                {
+                    b.HasOne("Domain.Entities.Page.Popup.PopupPosition", "PopupPosition")
+                        .WithMany("Popups")
+                        .HasForeignKey("PositionID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("PopupPosition");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Wallets.Wallet", b =>
+                {
+                    b.HasOne("Domain.Entities.Members.Member", "Member")
+                        .WithOne("Wallet")
+                        .HasForeignKey("Domain.Entities.Wallets.Wallet", "MemberID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Wallets.WalletBalance", b =>
+                {
+                    b.HasOne("Domain.Entities.Wallets.Wallet", null)
+                        .WithMany("Balances")
+                        .HasForeignKey("WalletKey")
+                        .HasPrincipalKey("WalletKey")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsOne("Domain.Entities.Common.ValueObject.Money", "Amount", b1 =>
+                        {
+                            b1.Property<int>("WalletBalanceID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("Currency")
+                                .IsRequired()
+                                .HasMaxLength(10)
+                                .HasColumnType("nvarchar(10)")
+                                .HasColumnName("Currency");
+
+                            b1.Property<decimal>("Value")
+                                .HasPrecision(18)
+                                .HasColumnType("decimal(18,0)")
+                                .HasColumnName("Amount");
+
+                            b1.HasKey("WalletBalanceID");
+
+                            b1.ToTable("WalletBalance");
+
+                            b1.WithOwner()
+                                .HasForeignKey("WalletBalanceID");
+                        });
+
+                    b.Navigation("Amount")
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Domain.Entities.Wallets.WalletTransaction", b =>
+                {
+                    b.HasOne("Domain.Entities.Wallets.Wallet", "Wallet")
+                        .WithMany("Transactions")
+                        .HasForeignKey("WalletKey")
+                        .HasPrincipalKey("WalletKey")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsOne("Domain.Entities.Common.ValueObject.Money", "Amount", b1 =>
+                        {
+                            b1.Property<int>("WalletTransactionID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("Currency")
+                                .IsRequired()
+                                .HasMaxLength(10)
+                                .HasColumnType("nvarchar(10)")
+                                .HasColumnName("Currency");
+
+                            b1.Property<decimal>("Value")
+                                .HasPrecision(18)
+                                .HasColumnType("decimal(18,0)")
+                                .HasColumnName("Amount");
+
+                            b1.HasKey("WalletTransactionID");
+
+                            b1.ToTable("WalletTransaction");
+
+                            b1.WithOwner()
+                                .HasForeignKey("WalletTransactionID");
+                        });
+
+                    b.OwnsOne("Domain.Entities.Common.ValueObject.Money", "BalanceAfter", b1 =>
+                        {
+                            b1.Property<int>("WalletTransactionID")
+                                .HasColumnType("int");
+
+                            b1.Property<string>("Currency")
+                                .IsRequired()
+                                .HasMaxLength(10)
+                                .HasColumnType("nvarchar(10)")
+                                .HasColumnName("BalanceAfterCurrency");
+
+                            b1.Property<decimal>("Value")
+                                .HasPrecision(18)
+                                .HasColumnType("decimal(18,0)")
+                                .HasColumnName("BalanceAfter");
+
+                            b1.HasKey("WalletTransactionID");
+
+                            b1.ToTable("WalletTransaction");
+
+                            b1.WithOwner()
+                                .HasForeignKey("WalletTransactionID");
+                        });
+
+                    b.Navigation("Amount")
+                        .IsRequired();
+
+                    b.Navigation("BalanceAfter")
+                        .IsRequired();
+
+                    b.Navigation("Wallet");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Crypto.Coin", b =>
+                {
+                    b.Navigation("CoinCategoryMap");
+
+                    b.Navigation("CoinMarket");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Crypto.CoinCategory", b =>
+                {
+                    b.Navigation("CoinCategoryMap");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.Board", b =>
+                {
+                    b.Navigation("BoardManager");
+
+                    b.Navigation("BoardMeta")
+                        .IsRequired();
+
+                    b.Navigation("BoardPrefix");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Boards.BoardGroup", b =>
+                {
+                    b.Navigation("Board");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Comments.Comment", b =>
+                {
+                    b.Navigation("Children");
+
+                    b.Navigation("CommentFile");
+
+                    b.Navigation("CommentFileDownLog");
+
+                    b.Navigation("CommentImage");
+
+                    b.Navigation("CommentLink");
+
+                    b.Navigation("CommentLinkClickLog");
+
+                    b.Navigation("CommentMedia");
+
+                    b.Navigation("CommentMention");
+
+                    b.Navigation("CommentReaction");
+
+                    b.Navigation("CommentReport");
+
+                    b.Navigation("CommentUpdateLog");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.Post", b =>
+                {
+                    b.Navigation("Comment");
+
+                    b.Navigation("PostBookmark");
+
+                    b.Navigation("PostFile");
+
+                    b.Navigation("PostFileDownLog");
+
+                    b.Navigation("PostImage");
+
+                    b.Navigation("PostLink");
+
+                    b.Navigation("PostLinkClickLog");
+
+                    b.Navigation("PostMedia");
+
+                    b.Navigation("PostReaction");
+
+                    b.Navigation("PostReport");
+
+                    b.Navigation("PostTag");
+
+                    b.Navigation("PostUpdateLog");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Forum.Posts.Tag", b =>
+                {
+                    b.Navigation("PostTag");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Members.Member", b =>
+                {
+                    b.Navigation("Channel");
+
+                    b.Navigation("MemberApprove")
+                        .IsRequired();
+
+                    b.Navigation("MemberStats")
+                        .IsRequired();
+
+                    b.Navigation("Wallet");
+                });
+
+            modelBuilder.Entity("Domain.Entities.News.RssFeedSource", b =>
+                {
+                    b.Navigation("RssNewsArticle");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Banner.BannerPosition", b =>
+                {
+                    b.Navigation("BannerItems");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Faq.FaqCategory", b =>
+                {
+                    b.Navigation("FaqItems");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Page.Popup.PopupPosition", b =>
+                {
+                    b.Navigation("Popups");
+                });
+
+            modelBuilder.Entity("Domain.Entities.Wallets.Wallet", b =>
+                {
+                    b.Navigation("Balances");
+
+                    b.Navigation("Transactions");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 111 - 0
Infrastructure/Persistence/Migrations/20260313180453_AddRssNewsTables.cs

@@ -0,0 +1,111 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Migrations.AppDb
+{
+    /// <inheritdoc />
+    public partial class AddRssNewsTables : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "RssFeedSource",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false, comment: "소스 이름"),
+                    Url = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false, comment: "RSS 피드 URL"),
+                    Description = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "소스 설명"),
+                    IntervalMinutes = table.Column<int>(type: "int", nullable: false, comment: "수집 주기 (분)"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "활성화 여부"),
+                    LastFetchedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "마지막 수집 일시"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_RssFeedSource", x => x.ID);
+                },
+                comment: "RSS 피드 소스");
+
+            migrationBuilder.CreateTable(
+                name: "RssNewsArticle",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    RssFeedSourceID = table.Column<int>(type: "int", nullable: false, comment: "RSS 피드 소스 FK"),
+                    Title = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false, comment: "기사 제목"),
+                    Link = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true, comment: "원본 링크"),
+                    Guid = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true, comment: "RSS GUID (중복 방지)"),
+                    Author = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true, comment: "작성자"),
+                    Description = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "요약"),
+                    Content = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "본문 (content:encoded)"),
+                    ImageUrl = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true, comment: "썸네일 이미지 URL"),
+                    SourceName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true, comment: "출처명"),
+                    Categories = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "카테고리 (JSON 배열)"),
+                    CommentCount = table.Column<int>(type: "int", nullable: false, comment: "댓글 수"),
+                    PublishedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "발행 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "수집 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_RssNewsArticle", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_RssNewsArticle_RssFeedSource_RssFeedSourceID",
+                        column: x => x.RssFeedSourceID,
+                        principalTable: "RssFeedSource",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.Cascade);
+                },
+                comment: "RSS 뉴스 기사");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_RssFeedSource_IsActive",
+                table: "RssFeedSource",
+                column: "IsActive");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_RssFeedSource_Url",
+                table: "RssFeedSource",
+                column: "Url",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_RssNewsArticle_CreatedAt",
+                table: "RssNewsArticle",
+                column: "CreatedAt");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_RssNewsArticle_Guid_RssFeedSourceID",
+                table: "RssNewsArticle",
+                columns: new[] { "Guid", "RssFeedSourceID" },
+                unique: true,
+                filter: "[Guid] IS NOT NULL");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_RssNewsArticle_PublishedAt",
+                table: "RssNewsArticle",
+                column: "PublishedAt");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_RssNewsArticle_RssFeedSourceID",
+                table: "RssNewsArticle",
+                column: "RssFeedSourceID");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "RssNewsArticle");
+
+            migrationBuilder.DropTable(
+                name: "RssFeedSource");
+        }
+    }
+}

+ 160 - 0
Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs

@@ -3325,6 +3325,150 @@ namespace Infrastructure.Persistence.Migrations
                         });
                 });
 
+            modelBuilder.Entity("Domain.Entities.News.RssFeedSource", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("소스 설명");
+
+                    b.Property<int>("IntervalMinutes")
+                        .HasColumnType("int")
+                        .HasComment("수집 주기 (분)");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("활성화 여부");
+
+                    b.Property<DateTime?>("LastFetchedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 수집 일시");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("소스 이름");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("Url")
+                        .IsRequired()
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("RSS 피드 URL");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("IsActive");
+
+                    b.HasIndex("Url")
+                        .IsUnique();
+
+                    b.ToTable("RssFeedSource", null, t =>
+                        {
+                            t.HasComment("RSS 피드 소스");
+                        });
+                });
+
+            modelBuilder.Entity("Domain.Entities.News.RssNewsArticle", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Author")
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("작성자");
+
+                    b.Property<string>("Categories")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("카테고리 (JSON 배열)");
+
+                    b.Property<int>("CommentCount")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수");
+
+                    b.Property<string>("Content")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("본문 (content:encoded)");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수집 일시");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("요약");
+
+                    b.Property<string>("Guid")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("RSS GUID (중복 방지)");
+
+                    b.Property<string>("ImageUrl")
+                        .HasMaxLength(2000)
+                        .HasColumnType("nvarchar(2000)")
+                        .HasComment("썸네일 이미지 URL");
+
+                    b.Property<string>("Link")
+                        .HasMaxLength(2000)
+                        .HasColumnType("nvarchar(2000)")
+                        .HasComment("원본 링크");
+
+                    b.Property<DateTime?>("PublishedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("발행 일시");
+
+                    b.Property<int>("RssFeedSourceID")
+                        .HasColumnType("int")
+                        .HasComment("RSS 피드 소스 FK");
+
+                    b.Property<string>("SourceName")
+                        .HasMaxLength(200)
+                        .HasColumnType("nvarchar(200)")
+                        .HasComment("출처명");
+
+                    b.Property<string>("Title")
+                        .IsRequired()
+                        .HasMaxLength(500)
+                        .HasColumnType("nvarchar(500)")
+                        .HasComment("기사 제목");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CreatedAt");
+
+                    b.HasIndex("PublishedAt");
+
+                    b.HasIndex("RssFeedSourceID");
+
+                    b.HasIndex("Guid", "RssFeedSourceID")
+                        .IsUnique()
+                        .HasFilter("[Guid] IS NOT NULL");
+
+                    b.ToTable("RssNewsArticle", null, t =>
+                        {
+                            t.HasComment("RSS 뉴스 기사");
+                        });
+                });
+
             modelBuilder.Entity("Domain.Entities.Page.Banner.BannerItem", b =>
                 {
                     b.Property<int>("ID")
@@ -5956,6 +6100,17 @@ namespace Infrastructure.Persistence.Migrations
                     b.Navigation("Member");
                 });
 
+            modelBuilder.Entity("Domain.Entities.News.RssNewsArticle", b =>
+                {
+                    b.HasOne("Domain.Entities.News.RssFeedSource", "RssFeedSource")
+                        .WithMany("RssNewsArticle")
+                        .HasForeignKey("RssFeedSourceID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("RssFeedSource");
+                });
+
             modelBuilder.Entity("Domain.Entities.Page.Banner.BannerItem", b =>
                 {
                     b.HasOne("Domain.Entities.Page.Banner.BannerPosition", "BannerPosition")
@@ -6202,6 +6357,11 @@ namespace Infrastructure.Persistence.Migrations
                     b.Navigation("Wallet");
                 });
 
+            modelBuilder.Entity("Domain.Entities.News.RssFeedSource", b =>
+                {
+                    b.Navigation("RssNewsArticle");
+                });
+
             modelBuilder.Entity("Domain.Entities.Page.Banner.BannerPosition", b =>
                 {
                     b.Navigation("BannerItems");

+ 7 - 0
SharedKernel/Constants/Menus.cs

@@ -228,6 +228,13 @@ namespace SharedKernel.Constants
                             Name = "큐레이션",
                             Path = "/Crypto/Curation",
                             Roles = [ "Admin", "코인 - 큐레이션" ]
+                        },
+                        new Menu
+                        {
+                            Id = 355,
+                            Name = "뉴스 관리",
+                            Path = "/Crypto/News",
+                            Roles = [ "Admin", "코인 - 뉴스 관리" ]
                         }
                     }
                 },

+ 27 - 0
Web.Api/Endpoints/News/Article.cs

@@ -0,0 +1,27 @@
+using MediatR;
+using Web.Api.Common;
+using Web.Api.Extensions;
+
+namespace Web.Api.Endpoints.News;
+
+internal sealed class Article : IEndpoint
+{
+    public void MapEndpoint(IEndpointRouteBuilder app)
+    {
+        app.MapGet("api/news/articles/{id}", async (
+            int id,
+            ISender sender,
+            CancellationToken ct
+        ) =>
+        {
+            var result = await sender.Send(new Application.Features.Api.News.GetArticle.Query(id), ct);
+
+            return result.Match(
+                data => ApiResponse.Ok(data),
+                CustomResults.Problem
+            );
+        })
+        .WithTags("News")
+        .AllowAnonymous();
+    }
+}

+ 26 - 0
Web.Api/Endpoints/News/Articles.cs

@@ -0,0 +1,26 @@
+using MediatR;
+using Web.Api.Common;
+
+namespace Web.Api.Endpoints.News;
+
+internal sealed class Articles : IEndpoint
+{
+    public void MapEndpoint(IEndpointRouteBuilder app)
+    {
+        app.MapGet("api/news/articles", async (
+            ISender sender,
+            CancellationToken ct,
+            int? rssFeedSourceID = null,
+            string? keyword = null,
+            int page = 1,
+            ushort perPage = 20
+        ) =>
+        {
+            var query = new Application.Features.Api.News.GetArticles.Query(rssFeedSourceID, keyword, page, perPage);
+
+            return ApiResponse.Ok(await sender.Send(query, ct));
+        })
+        .WithTags("News")
+        .AllowAnonymous();
+    }
+}

+ 20 - 0
Web.Api/Endpoints/News/Sources.cs

@@ -0,0 +1,20 @@
+using MediatR;
+using Web.Api.Common;
+
+namespace Web.Api.Endpoints.News;
+
+internal sealed class Sources : IEndpoint
+{
+    public void MapEndpoint(IEndpointRouteBuilder app)
+    {
+        app.MapGet("api/news/sources", async (
+            ISender sender,
+            CancellationToken ct
+        ) =>
+        {
+            return ApiResponse.Ok(await sender.Send(new Application.Features.Api.News.GetSources.Query(), ct));
+        })
+        .WithTags("News")
+        .AllowAnonymous();
+    }
+}

+ 133 - 0
Web.Api/postman/News API.postman_collection.json

@@ -0,0 +1,133 @@
+{
+	"info": {
+		"name": "bitForum - News API",
+		"description": "bitForum 뉴스 API 컬렉션\n\nBase URL: https://api.bitforum.io",
+		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+	},
+	"variable": [
+		{
+			"key": "baseUrl",
+			"value": "https://localhost:4000",
+			"type": "string"
+		}
+	],
+	"item": [
+		{
+			"name": "기사 목록 조회",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "{{baseUrl}}/api/news/articles?page=1&perPage=20",
+					"host": ["{{baseUrl}}"],
+					"path": ["api", "news", "articles"],
+					"query": [
+						{
+							"key": "page",
+							"value": "1",
+							"description": "페이지 번호 (기본값: 1)"
+						},
+						{
+							"key": "perPage",
+							"value": "20",
+							"description": "페이지당 건수 (기본값: 20)"
+						},
+						{
+							"key": "rssFeedSourceID",
+							"value": "",
+							"description": "RSS 소스 ID 필터 (선택)",
+							"disabled": true
+						},
+						{
+							"key": "keyword",
+							"value": "",
+							"description": "제목/내용 검색어 (선택)",
+							"disabled": true
+						}
+					]
+				},
+				"description": "수집된 뉴스 기사 목록을 페이징하여 조회합니다.\n\n선택적으로 RSS 소스 ID나 키워드로 필터링할 수 있습니다."
+			},
+			"response": [
+				{
+					"name": "200 OK",
+					"status": "OK",
+					"code": 200,
+					"header": [
+						{
+							"key": "Content-Type",
+							"value": "application/json"
+						}
+					],
+					"body": "{\n  \"success\": true,\n  \"status\": 200,\n  \"message\": null,\n  \"data\": {\n    \"total\": 42,\n    \"list\": [\n      {\n        \"id\": 1,\n        \"rssFeedSourceID\": 1,\n        \"feedSourceName\": \"CoinDesk RSS\",\n        \"title\": \"Bitcoin Surges Past $100K\",\n        \"link\": \"https://example.com/article/1\",\n        \"author\": \"John Doe\",\n        \"description\": \"Bitcoin has surged past the $100,000 mark...\",\n        \"imageUrl\": \"https://example.com/images/btc.jpg\",\n        \"sourceName\": \"CoinDesk\",\n        \"categories\": \"Bitcoin,Markets\",\n        \"commentCount\": 5,\n        \"publishedAt\": \"2026-03-14T09:00:00Z\",\n        \"createdAt\": \"2026-03-14T09:05:00Z\"\n      }\n    ]\n  },\n  \"errors\": null\n}"
+				}
+			]
+		},
+		{
+			"name": "기사 상세 조회",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "{{baseUrl}}/api/news/articles/1",
+					"host": ["{{baseUrl}}"],
+					"path": ["api", "news", "articles", "1"]
+				},
+				"description": "특정 뉴스 기사의 상세 정보를 조회합니다.\n\n목록 조회 대비 guid, content 필드가 추가로 포함됩니다."
+			},
+			"response": [
+				{
+					"name": "200 OK",
+					"status": "OK",
+					"code": 200,
+					"header": [
+						{
+							"key": "Content-Type",
+							"value": "application/json"
+						}
+					],
+					"body": "{\n  \"success\": true,\n  \"status\": 200,\n  \"message\": null,\n  \"data\": {\n    \"id\": 1,\n    \"rssFeedSourceID\": 1,\n    \"feedSourceName\": \"CoinDesk RSS\",\n    \"title\": \"Bitcoin Surges Past $100K\",\n    \"link\": \"https://example.com/article/1\",\n    \"guid\": \"https://example.com/article/1\",\n    \"author\": \"John Doe\",\n    \"description\": \"Bitcoin has surged past the $100,000 mark...\",\n    \"content\": \"<p>Bitcoin has surged past the $100,000 mark for the first time...</p>\",\n    \"imageUrl\": \"https://example.com/images/btc.jpg\",\n    \"sourceName\": \"CoinDesk\",\n    \"categories\": \"Bitcoin,Markets\",\n    \"commentCount\": 5,\n    \"publishedAt\": \"2026-03-14T09:00:00Z\",\n    \"createdAt\": \"2026-03-14T09:05:00Z\"\n  },\n  \"errors\": null\n}"
+				},
+				{
+					"name": "404 Not Found",
+					"status": "Not Found",
+					"code": 404,
+					"header": [
+						{
+							"key": "Content-Type",
+							"value": "application/json"
+						}
+					],
+					"body": "{\n  \"success\": false,\n  \"status\": 404,\n  \"message\": \"뉴스 기사를 찾을 수 없습니다.\",\n  \"data\": null,\n  \"errors\": null\n}"
+				}
+			]
+		},
+		{
+			"name": "RSS 소스 목록 조회",
+			"request": {
+				"method": "GET",
+				"header": [],
+				"url": {
+					"raw": "{{baseUrl}}/api/news/sources",
+					"host": ["{{baseUrl}}"],
+					"path": ["api", "news", "sources"]
+				},
+				"description": "활성화된 RSS 피드 소스 목록을 조회합니다."
+			},
+			"response": [
+				{
+					"name": "200 OK",
+					"status": "OK",
+					"code": 200,
+					"header": [
+						{
+							"key": "Content-Type",
+							"value": "application/json"
+						}
+					],
+					"body": "{\n  \"success\": true,\n  \"status\": 200,\n  \"message\": null,\n  \"data\": {\n    \"sources\": [\n      {\n        \"id\": 1,\n        \"name\": \"CoinDesk RSS\",\n        \"url\": \"https://www.coindesk.com/arc/outboundfeeds/rss/\",\n        \"description\": \"CoinDesk 암호화폐 뉴스\",\n        \"isActive\": true,\n        \"lastFetchedAt\": \"2026-03-14T09:00:00Z\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"CoinTelegraph RSS\",\n        \"url\": \"https://cointelegraph.com/rss\",\n        \"description\": \"CoinTelegraph 뉴스 피드\",\n        \"isActive\": true,\n        \"lastFetchedAt\": \"2026-03-14T08:45:00Z\"\n      }\n    ]\n  },\n  \"errors\": null\n}"
+				}
+			]
+		}
+	]
+}