ソースを参照

회원 관리 기능

KIM-JINO5 4 ヶ月 前
コミット
4ec9d16f5a
52 ファイル変更5006 行追加68 行削除
  1. 5 1
      .claude/settings.local.json
  2. 43 32
      Admin/Admin.csproj
  3. 1 1
      Admin/Pages/Director/Role/Index.cshtml
  4. 4 4
      Admin/Pages/Director/Role/Permission.cshtml
  5. 2 2
      Admin/Pages/Director/User/Edit.cshtml
  6. 1 1
      Admin/Pages/Director/User/Index.cshtml
  7. 3 1
      Admin/Pages/Document/Index.cshtml
  8. 1 1
      Admin/Pages/Member/Grade/Index.cshtml
  9. 80 0
      Admin/Pages/Member/List/Approve.cshtml
  10. 86 0
      Admin/Pages/Member/List/Approve.cshtml.cs
  11. 267 0
      Admin/Pages/Member/List/Edit.cshtml
  12. 216 0
      Admin/Pages/Member/List/Edit.cshtml.cs
  13. 273 0
      Admin/Pages/Member/List/Index.cshtml
  14. 155 0
      Admin/Pages/Member/List/Index.cshtml.cs
  15. 268 0
      Admin/Pages/Member/List/View.cshtml
  16. 108 0
      Admin/Pages/Member/List/View.cshtml.cs
  17. 241 0
      Admin/Pages/Member/List/Write.cshtml
  18. 192 0
      Admin/Pages/Member/List/Write.cshtml.cs
  19. 2 0
      Admin/Pages/Member/Log/Email.cshtml
  20. 8 4
      Admin/Pages/Member/Log/Intro.cshtml
  21. 7 3
      Admin/Pages/Member/Log/Login/Index.cshtml
  22. 8 4
      Admin/Pages/Member/Log/Name.cshtml
  23. 8 4
      Admin/Pages/Member/Log/Summary.cshtml
  24. 3 1
      Admin/Pages/Popup/Index.cshtml
  25. 8 0
      Admin/using.cs
  26. 3 2
      Application/Abstractions/Data/IAppDbContext.cs
  27. 1 0
      Application/Application.csproj
  28. 10 3
      Application/Features/Banner/Item/Delete/Handler.cs
  29. 11 0
      Application/Features/Member/List/Approve/Command.cs
  30. 51 0
      Application/Features/Member/List/Approve/CommandHandler.cs
  31. 33 0
      Application/Features/Member/List/Approve/GetHandler.cs
  32. 5 0
      Application/Features/Member/List/Approve/Query.cs
  33. 14 0
      Application/Features/Member/List/Approve/Response.cs
  34. 29 0
      Application/Features/Member/List/Create/Command.cs
  35. 72 0
      Application/Features/Member/List/Create/Handler.cs
  36. 5 0
      Application/Features/Member/List/Delete/Command.cs
  37. 34 0
      Application/Features/Member/List/Delete/Handler.cs
  38. 68 0
      Application/Features/Member/List/Get/Handler.cs
  39. 5 0
      Application/Features/Member/List/Get/Query.cs
  40. 63 0
      Application/Features/Member/List/Get/Response.cs
  41. 131 0
      Application/Features/Member/List/Search/Handler.cs
  42. 17 0
      Application/Features/Member/List/Search/Query.cs
  43. 32 0
      Application/Features/Member/List/Search/Response.cs
  44. 28 0
      Application/Features/Member/List/Update/Command.cs
  45. 86 0
      Application/Features/Member/List/Update/Handler.cs
  46. 4 1
      Application/Features/MemberGrade/Delete/Handler.cs
  47. 1 1
      Domain/Entities/Members/Member.cs
  48. 2281 0
      Infrastructure/Infrastructure/Persistence/Migrations/20260206143102_a4.Designer.cs
  49. 28 0
      Infrastructure/Infrastructure/Persistence/Migrations/20260206143102_a4.cs
  50. 2 0
      Infrastructure/Persistence/AppDbContext.cs
  51. 1 1
      Infrastructure/Persistence/Configurations/Members/MemberConfiguration.cs
  52. 1 1
      Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs

+ 5 - 1
.claude/settings.local.json

@@ -3,7 +3,11 @@
     "allow": [
       "Bash(dotnet build:*)",
       "Bash(dotnet ef migrations add:*)",
-      "mcp__Desktop_Commander__start_process"
+      "mcp__Desktop_Commander__start_process",
+	  "FileEdit(E:\\workspace\\bitforum.io:*)",
+      "FileRead(E:\\workspace\\bitforum.io:*)",
+      "FileCreate(E:\\workspace\\bitforum.io:*)",
+      "FileDelete(E:\\workspace\\bitforum.io:*)"
     ]
   },
   "additionalDirectories": [

+ 43 - 32
Admin/Admin.csproj

@@ -1,40 +1,51 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
-  <PropertyGroup>
-    <TargetFramework>net10.0</TargetFramework>
-    <Nullable>enable</Nullable>
-    <ImplicitUsings>enable</ImplicitUsings>
-  </PropertyGroup>
+	<PropertyGroup>
+		<TargetFramework>net10.0</TargetFramework>
+		<Nullable>enable</Nullable>
+		<ImplicitUsings>enable</ImplicitUsings>
+	</PropertyGroup>
 
-  <ItemGroup>
-    <None Include=".github\copilot-instructions.md" />
-  </ItemGroup>
+	<ItemGroup>
+	  <Content Remove="Pages\Member\Wallet\List\Index.cshtml" />
+	  <Content Remove="Pages\Member\Wallet\List\View.cshtml" />
+	  <Content Remove="Pages\Member\Wallet\Transactions\List.cshtml" />
+	  <Content Remove="Pages\Member\Wallet\Transactions\View.cshtml" />
+	</ItemGroup>
 
-  <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
-    <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.2" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-      <PrivateAssets>all</PrivateAssets>
-    </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.2" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-      <PrivateAssets>all</PrivateAssets>
-    </PackageReference>
-    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
-    <PackageReference Include="System.Management" Version="10.0.2" />
-  </ItemGroup>
+	<ItemGroup>
+		<None Include=".github\copilot-instructions.md" />
+		<None Include="Pages\Member\Wallet\List\Index.cshtml" />
+		<None Include="Pages\Member\Wallet\List\View.cshtml" />
+		<None Include="Pages\Member\Wallet\Transactions\List.cshtml" />
+		<None Include="Pages\Member\Wallet\Transactions\View.cshtml" />
+	</ItemGroup>
 
-  <ItemGroup>
-    <ProjectReference Include="..\Application\Application.csproj" />
-    <ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
-  </ItemGroup>
+	<ItemGroup>
+		<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.2" />
+		<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="10.0.2" />
+		<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
+			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+			<PrivateAssets>all</PrivateAssets>
+		</PackageReference>
+		<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.2" />
+		<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.2">
+			<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+			<PrivateAssets>all</PrivateAssets>
+		</PackageReference>
+		<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.2" />
+		<PackageReference Include="System.Management" Version="10.0.2" />
+	</ItemGroup>
 
-  <ItemGroup>
-    <Folder Include="Middleware\" />
-    <Folder Include="wwwroot\uploads\basic\" />
-    <Folder Include="wwwroot\uploads\banner\" />
-  </ItemGroup>
+	<ItemGroup>
+		<ProjectReference Include="..\Application\Application.csproj" />
+		<ProjectReference Include="..\Infrastructure\Infrastructure.csproj" />
+	</ItemGroup>
+
+	<ItemGroup>
+		<Folder Include="Middleware\" />
+		<Folder Include="wwwroot\uploads\basic\" />
+		<Folder Include="wwwroot\uploads\banner\" />
+	</ItemGroup>
 
 </Project>

+ 1 - 1
Admin/Pages/Director/Role/Index.cshtml

@@ -10,7 +10,7 @@
             <h3>@ViewData["Title"]</h3>
         </div>
         <div class="col text-end align-self-center">
-            <a asp-page="/Director/User/Index" class="btn btn-sm btn-secondary">취소</a>
+            <a asp-page="/Director/User/Index" class="btn btn-secondary">취소</a>
         </div>
     </div>
     <hr />

+ 4 - 4
Admin/Pages/Director/Role/Permission.cshtml

@@ -18,8 +18,8 @@
                 권한을 추가하거나 회수할 수 있습니다. <ins>Create: 읽기, View: 열람, Edit: 수정, Delete: 삭제</ins>
             </div>
             <div class="col-auto align-self-end">
-                <button type="submit" class="btn btn-sm btn-success">저장</button>
-                <a asp-page="/Director/Role/Index" class="btn btn-sm btn-secondary">취소</a>
+                <button type="submit" class="btn btn-success">저장</button>
+                <a asp-page="/Director/Role/Index" class="btn btn-secondary">취소</a>
             </div>
         </div>
 
@@ -80,8 +80,8 @@
             </div>
 
         <div class="card-footer text-center p-4">
-            <button type="submit" class="btn btn-sm btn-success">저장</button>
-            <a asp-page="/Director/Role/Index" class="btn btn-sm btn-secondary">취소</a>
+            <button type="submit" class="btn btn-success">저장</button>
+            <a asp-page="/Director/Role/Index" class="btn btn-secondary">취소</a>
         </div>
 
         <br />

+ 2 - 2
Admin/Pages/Director/User/Edit.cshtml

@@ -84,8 +84,8 @@
         </div>
         <hr />
         <div class="d-grid gap-2 text-center d-md-block">
-            <button type="submit" class="btn btn-sm btn-success">저장</button>
-            <a asp-page="/Director/User/Index" class="btn btn-sm btn-secondary">취소</a>
+            <button type="submit" class="btn btn-success">저장</button>
+            <a asp-page="/Director/User/Index" class="btn btn-secondary">취소</a>
         </div>
     </form>
 </div>

+ 1 - 1
Admin/Pages/Director/User/Index.cshtml

@@ -16,7 +16,7 @@
             Total : @Model.Total.ToString("N0")
         </div>
         <div class="col text-end">
-            <a class="btn btn-sm btn-primary" asp-page="/Director/Role/Index">역할 관리</a>
+            <a class="btn btn-primary" asp-page="/Director/Role/Index">역할 관리</a>
         </div>
     </div>
 

+ 3 - 1
Admin/Pages/Document/Index.cshtml

@@ -14,13 +14,15 @@
         <div class="col">
             Total : @Model.Total
         </div>
-        <div class="col text-end">
+        <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="/Document/Write">추가</a>
         </div>

+ 1 - 1
Admin/Pages/Member/Grade/Index.cshtml

@@ -15,7 +15,7 @@
             Total : @Model.Total
         </div>
         <div class="col text-end">
-            <a class="btn btn-sm btn-success" href="/Member/Grade/Write">추가</a>
+            <a class="btn btn-success" href="/Member/Grade/Write">추가</a>
         </div>
     </div>
 

+ 80 - 0
Admin/Pages/Member/List/Approve.cshtml

@@ -0,0 +1,80 @@
+@page "{id:int}"
+@model Admin.Pages.Member.List.ApproveModel
+@{
+    ViewData["Title"] = "회원 알림 및 동의";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off">
+        <input type="hidden" asp-for="Input.MemberID" />
+        <input type="hidden" asp-for="QueryString" />
+
+        <div class="row mb-2">
+            <label asp-for="Input.IsReceiveSMS" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsReceiveSMS" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsReceiveSMS">수신합니다.</label>
+                    <span asp-validation-for="Input.IsReceiveSMS" class="text-danger"></span>
+                    @if (Model.ReceiveSMSConsentAt is not null)
+                    {
+                        <span class="form-text text-muted">@Model.ReceiveSMSConsentAt</span>
+                    }
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsReceiveEmail" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsReceiveEmail" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsReceiveEmail">수신합니다.</label>
+                    <span asp-validation-for="Input.IsReceiveEmail" class="text-danger"></span>
+                    @if (Model.ReceiveEmailConsentAt is not null)
+                    {
+                        <span class="form-text text-muted">@Model.ReceiveEmailConsentAt</span>
+                    }
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsReceiveNote" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsReceiveNote" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsReceiveNote">수신합니다.</label>
+                    <span asp-validation-for="Input.IsReceiveNote" class="text-danger"></span>
+                    @if (Model.ReceiveNoteConsentAt is not null)
+                    {
+                        <span class="form-text text-muted">@Model.ReceiveNoteConsentAt</span>
+                    }
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsDisclosureInvest" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsDisclosureInvest" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsDisclosureInvest">수신합니다.</label>
+                    <span asp-validation-for="Input.IsDisclosureInvest" class="text-danger"></span>
+                    @if (Model.DisclosureInvestConsentAt is not null)
+                    {
+                        <span class="form-text text-muted">@Model.DisclosureInvestConsentAt</span>
+                    }
+                </div>
+            </div>
+        </div>
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-success">저장</button>
+            <a href="/Member/List?@Model.QueryString" class="btn btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>

+ 86 - 0
Admin/Pages/Member/List/Approve.cshtml.cs

@@ -0,0 +1,86 @@
+using SharedKernel.Extensions;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using System.ComponentModel;
+
+namespace Admin.Pages.Member.List;
+
+public class ApproveModel(IMediator mediator) : PageModel
+{
+    [BindProperty]
+    public string? QueryString { get; set; }
+
+    public string? ReceiveSMSConsentAt { get; set; }
+    public string? ReceiveEmailConsentAt { get; set; }
+    public string? ReceiveNoteConsentAt { get; set; }
+    public string? DisclosureInvestConsentAt { get; set; }
+
+    [BindProperty]
+    public InputModel Input { get; set; } = new();
+
+    public sealed class InputModel
+    {
+        public int MemberID { get; set; }
+
+        [DisplayName("SMS 수신")]
+        public bool IsReceiveSMS { get; set; }
+
+        [DisplayName("이메일 수신")]
+        public bool IsReceiveEmail { get; set; }
+
+        [DisplayName("쪽지 수신")]
+        public bool IsReceiveNote { get; set; }
+
+        [DisplayName("투자 현황 공개")]
+        public bool IsDisclosureInvest { get; set; }
+    }
+
+    public async Task OnGetAsync(int id, CancellationToken ct)
+    {
+        var result = await mediator.Send(new ApproveMember.Query(id), ct);
+
+        ReceiveSMSConsentAt = result.ReceiveSMSConsentAt.GetDateAt();
+        ReceiveEmailConsentAt = result.ReceiveEmailConsentAt.GetDateAt();
+        ReceiveNoteConsentAt = result.ReceiveNoteConsentAt.GetDateAt();
+        DisclosureInvestConsentAt = result.DisclosureInvestConsentAt.GetDateAt();
+
+        Input = new InputModel
+        {
+            MemberID = result.MemberID,
+            IsReceiveSMS = result.IsReceiveSMS,
+            IsReceiveEmail = result.IsReceiveEmail,
+            IsReceiveNote = result.IsReceiveNote,
+            IsDisclosureInvest = result.IsDisclosureInvest
+        };
+
+        QueryString = Request.QueryString.ToString();
+    }
+
+    public async Task<IActionResult> OnPostAsync(CancellationToken ct)
+    {
+        try
+        {
+            if (!ModelState.IsValid)
+            {
+                throw new Exception(ModelState.GetErrorMessages());
+            }
+
+            await mediator.Send(new ApproveMember.Command(
+                Input.MemberID,
+                Input.IsReceiveSMS,
+                Input.IsReceiveEmail,
+                Input.IsReceiveNote,
+                Input.IsDisclosureInvest
+            ), ct);
+
+            TempData["SuccessMessage"] = "알림 및 동의 정보가 수정되었습니다.";
+        }
+        catch (Exception e)
+        {
+            TempData["ErrorMessages"] = e.Message;
+        }
+
+        return Redirect($"/Member/List/Approve/{Input.MemberID}{QueryString}");
+    }
+}

+ 267 - 0
Admin/Pages/Member/List/Edit.cshtml

@@ -0,0 +1,267 @@
+@page "{id:int}"
+@model Admin.Pages.Member.List.EditModel
+@using Domain.Entities.Members.ValueObject
+@{
+    ViewData["Title"] = "회원 수정";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" enctype="multipart/form-data">
+        <input type="hidden" asp-for="Input.ID" />
+        <input type="hidden" asp-for="QueryString" />
+
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label"><span>*</span> PK</label>
+            <div class="col-sm-10">
+                <input type="text" readonly class="form-control-plaintext" value="@Model.Input.ID" />
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.MemberGradeID" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <select asp-for="Input.MemberGradeID" class="form-select w-auto" asp-items="Model.MemberGradeList">
+                    <option value="">등급 선택</option>
+                </select>
+                <span asp-validation-for="Input.MemberGradeID" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Input_Email" class="col-sm-2 col-form-label"><span>*</span> 이메일</label>
+            <div class="col-sm-10">
+                <input type="email" asp-for="Input.Email" class="form-control" required maxlength="60" placeholder="중복 시 등록이 불가합니다. 60자 이내" />
+                <span asp-validation-for="Input.Email" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Input_Name" class="col-sm-2 col-form-label"><span>*</span> 별명</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.Name" class="form-control" required maxlength="20" placeholder="중복 시 등록이 불가합니다. 20자 이내" />
+                <span asp-validation-for="Input.Name" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Password" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <input type="password" asp-for="Input.Password" class="form-control" minlength="4" />
+                <span asp-validation-for="Input.Password" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="RePassword" class="col-sm-2 col-form-label">비밀번호 확인</label>
+            <div class="col-sm-10">
+                <input type="password" name="RePassword" id="RePassword" class="form-control" minlength="4" />
+                <span class="form-text text-muted">비밀번호는 변경할 경우 입력합니다.</span>
+            </div>
+        </div>
+        <hr/>
+        <div class="row mb-2">
+            <label asp-for="Input.FirstName" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.FirstName" class="form-control" maxlength="20" placeholder="최대 20자" />
+                <span asp-validation-for="Input.FirstName" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.LastName" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.LastName" class="form-control" maxlength="40" placeholder="최대 40자" />
+                <span asp-validation-for="Input.LastName" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Intro" class="col-sm-2"></label>
+            <div class="col-sm-10">
+                <textarea asp-for="Input.Intro" class="form-control" placeholder="최대 1000자" rows="2" maxlength="1000"></textarea>
+                <span asp-validation-for="Input.Intro" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Summary" class="col-sm-2"></label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.Summary" class="form-control" maxlength="50" placeholder="50자 이내"/>
+                <span asp-validation-for="Input.Summary" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Phone" class="col-sm-2"></label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.Phone" class="form-control" maxlength="15" placeholder="010-0000-0000 형식으로 입력하세요." pattern="010-\d{4}-\d{4}" />
+                <span asp-validation-for="Input.Phone" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Gender" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="radio" asp-for="Input.Gender" id="male" class="form-check-input" value="@Gender.Male" />
+                    <label class="form-check-label" for="male">남자</label>
+                </div>
+                <div class="form-check-inline">
+                    <input type="radio" asp-for="Input.Gender" id="female" class="form-check-input" value="@Gender.Female" />
+                    <label class="form-check-label" for="female">여자</label>
+                </div>
+                <span asp-validation-for="Input.Gender" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Birthday" class="col-sm-2"></label>
+            <div class="col-sm-10">
+                <input type="date" asp-for="Input.Birthday" class="form-control w-auto" />
+                <span asp-validation-for="Input.Birthday" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Thumb" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                @if (!string.IsNullOrEmpty(Model.Thumb))
+                {
+                    <img src="@Url.Content(Model.Thumb)" class="img-fluid img-thumbnail" alt="사진" /><br />
+                    <div class="form-check-inline">
+                        <input type="checkbox" name="isThumbRemove" id="IsThumbRemove" class="form-check-input" value="true" />
+                        <label for="IsThumbRemove" class="form-check-label">삭제</label>
+                    </div>
+                }
+                else
+                {
+                    <div id="thumbPrev" hidden><img class="img-fluid img-thumbnail" alt="사진 미리보기" /></div>
+                    <input type="file" asp-for="Input.Thumb" class="form-control" accept="image/*" />
+                    <span asp-validation-for="Input.Thumb" class="text-danger"></span>
+                }
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Icon" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                @if (!string.IsNullOrEmpty(Model.Icon))
+                {
+                    <img src="@Url.Content(Model.Icon)" class="img-fluid img-thumbnail" alt="아이콘" />
+                    <br />
+                    <div class="form-check-inline">
+                        <input type="checkbox" name="isIconRemove" id="IsIconRemove" class="form-check-input" value="true" />
+                        <label for="IsIconRemove" class="form-check-label">삭제</label>
+                    </div>
+                }
+                else
+                {
+                    <div id="iconPrev" hidden><img class="img-fluid img-thumbnail" alt="아이콘 미리보기" /></div>
+                    <input type="file" asp-for="Input.Icon" class="form-control" accept="image/*" />
+                    <span asp-validation-for="Input.Icon" class="text-danger"></span>
+                }
+            </div>
+        </div>
+        <hr />
+        <div class="row mb-2">
+            <label asp-for="Input.IsEmailVerified" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsEmailVerified" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsEmailVerified">인증 했습니다.</label>
+                    <span asp-validation-for="Input.IsEmailVerified" class="text-danger"></span>
+                    @if (Model.EmailVerifiedAt is not null)
+                    {
+                        <span class="form-text text-muted">@Model.EmailVerifiedAt</span>
+                    }
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsAuthCertified" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsAuthCertified" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsAuthCertified">인증 했습니다.</label>
+                    <span asp-validation-for="Input.IsAuthCertified" class="text-danger"></span>
+                    @if (Model.AuthCertifiedAt is not null)
+                    {
+                        <span class="form-text text-muted">@Model.AuthCertifiedAt</span>
+                    }
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsDenied" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsDenied" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsDenied">차단합니다.</label>
+                    <span asp-validation-for="Input.IsDenied" class="text-danger"></span>
+                    @if (Model.DeniedAt is not null)
+                    {
+                        <span class="form-text text-muted">@Model.DeniedAt</span>
+                    }
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsAdmin" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsAdmin" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsAdmin">관리자입니다.</label>
+                    <span asp-validation-for="Input.IsAdmin" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsWithdraw" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsWithdraw" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsWithdraw">탈퇴했습니다.</label>
+                    <span asp-validation-for="Input.IsWithdraw" class="text-danger"></span>
+                    @if (Model.DeletedAt is not null)
+                    {
+                        <span class="form-text text-muted">@Model.DeletedAt</span>
+                    }
+                </div>
+            </div>
+        </div>
+        @if (Model.UpdatedAt is not null)
+        {
+            <div class="row mb-2">
+                <label class="col-sm-2 col-form-label">수정일시</label>
+                <div class="col-sm-10">
+                    <input class="form-control-plaintext" type="text" readonly value="@Model.UpdatedAt" />
+                </div>
+            </div>
+        }
+        @if (Model.CreatedAt is not null)
+        {
+            <div class="row mb-2">
+                <label class="col-sm-2 col-form-label">등록일시</label>
+                <div class="col-sm-10">
+                    <input class="form-control-plaintext" type="text" readonly value="@Model.CreatedAt" />
+                </div>
+            </div>
+        }
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-success">저장</button>
+            <a href="/Member/List?@Model.QueryString" class="btn btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>
+
+@section Scripts {
+    <script>
+        setupImagePreview("Input_Thumb", "thumbPrev");
+        setupImagePreview("Input_Icon", "iconPrev");
+
+        $(document).on("submit", "#fAdminWrite", function(e) {
+            e.preventDefault();
+            const password = e.target.elements["Input.Password"].value;
+            const rePassword = e.target.elements["RePassword"].value;
+            if (password !== rePassword) {
+                alert("비밀번호가 일치하지 않습니다.");
+                return false;
+            }
+            this.submit();
+        });
+    </script>
+}

+ 216 - 0
Admin/Pages/Member/List/Edit.cshtml.cs

@@ -0,0 +1,216 @@
+using Domain.Entities.Members.ValueObject;
+using SharedKernel.Attributes;
+using SharedKernel.Extensions;
+using SharedKernel.Storage;
+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.Member.List;
+
+public class EditModel(IMediator mediator, IFileStorage fileStorage) : PageModel
+{
+    private static readonly string[] AllowedFileExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
+
+    [BindProperty]
+    public string? QueryString { get; set; }
+
+    public List<SelectListItem> MemberGradeList { get; set; } = [];
+
+    public string? Thumb { get; set; }
+    public string? Icon { get; set; }
+    public string? EmailVerifiedAt { get; set; }
+    public string? AuthCertifiedAt { get; set; }
+    public string? DeniedAt { get; set; }
+    public string? DeletedAt { get; set; }
+    public string? UpdatedAt { get; set; }
+    public string? CreatedAt { get; set; }
+
+    [BindProperty]
+    public InputModel Input { get; set; } = new();
+
+    public sealed class InputModel
+    {
+        [Required]
+        public int ID { get; set; }
+
+        [DisplayName("회원등급")]
+        public int? MemberGradeID { get; set; }
+
+        [DisplayName("이메일")]
+        [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
+        [EmailAddress(ErrorMessage = "올바른 이메일 형식이 아닙니다.")]
+        [StringLength(60, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string Email { get; set; } = default!;
+
+        [DisplayName("별명")]
+        [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
+        [StringLength(20, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? Name { get; set; }
+
+        [DisplayName("비밀번호")]
+        [MinLength(4, ErrorMessage = "{0}은(는) {1}자 이상 입력하세요.")]
+        public string? Password { get; set; }
+
+        [DisplayName("성")]
+        [StringLength(20, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? FirstName { get; set; }
+
+        [DisplayName("이름")]
+        [StringLength(40, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? LastName { get; set; }
+
+        [DisplayName("자기소개")]
+        [StringLength(1000, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? Intro { get; set; }
+
+        [DisplayName("한마디")]
+        [StringLength(50, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? Summary { get; set; }
+
+        [DisplayName("연락처")]
+        [StringLength(15)]
+        public string? Phone { get; set; }
+
+        [DisplayName("성별")]
+        public Gender? Gender { get; set; }
+
+        [DisplayName("생년월일")]
+        public DateOnly? Birthday { get; set; }
+
+        [DisplayName("사진")]
+        [AllowedExtensions("jpg,jpeg,png,gif,webp", ErrorMessage = "이미지 형식은 jpg, jpeg, png, gif, webp 파일이어야 합니다.")]
+        public IFormFile? Thumb { get; set; }
+
+        [DisplayName("아이콘")]
+        [AllowedExtensions("jpg,jpeg,png,gif,webp", ErrorMessage = "이미지 형식은 jpg, jpeg, png, gif, webp 파일이어야 합니다.")]
+        public IFormFile? Icon { get; set; }
+
+        [DisplayName("이메일 인증")]
+        public bool IsEmailVerified { get; set; }
+
+        [DisplayName("본인 인증")]
+        public bool IsAuthCertified { get; set; }
+
+        [DisplayName("차단 여부")]
+        public bool IsDenied { get; set; }
+
+        [DisplayName("관리자 여부")]
+        public bool IsAdmin { get; set; }
+
+        [DisplayName("탈퇴 여부")]
+        public bool IsWithdraw { get; set; }
+    }
+
+    public async Task OnGetAsync(int id, CancellationToken ct)
+    {
+        await LoadMemberGradeList(ct);
+
+        var result = await mediator.Send(new GetMember.Query(id), ct);
+        if (result is null) return;
+
+        Thumb = result.Thumb;
+        Icon = result.Icon;
+        EmailVerifiedAt = result.EmailVerifiedAt.GetDateAt();
+        AuthCertifiedAt = result.AuthCertifiedAt.GetDateAt();
+        DeniedAt = result.DeniedAt.GetDateAt();
+        DeletedAt = result.DeletedAt.GetDateAt();
+        UpdatedAt = result.UpdatedAt.GetDateAt();
+        CreatedAt = result.CreatedAt.GetDateAt();
+
+        Input = new InputModel
+        {
+            ID = result.ID,
+            MemberGradeID = result.MemberGradeID,
+            Email = result.Email,
+            Name = result.Name,
+            FirstName = result.FirstName,
+            LastName = result.LastName,
+            Intro = result.Intro,
+            Summary = result.Summary,
+            Phone = result.Phone,
+            Gender = result.Gender,
+            Birthday = result.Birthday,
+            IsEmailVerified = result.IsEmailVerified,
+            IsAuthCertified = result.IsAuthCertified,
+            IsDenied = result.IsDenied,
+            IsAdmin = result.IsAdmin,
+            IsWithdraw = result.IsWithdraw
+        };
+
+        QueryString = Request.QueryString.ToString();
+    }
+
+    public async Task<IActionResult> OnPostAsync(bool isThumbRemove, bool isIconRemove, CancellationToken ct)
+    {
+        try
+        {
+            if (!ModelState.IsValid)
+            {
+                throw new Exception(ModelState.GetErrorMessages());
+            }
+
+            // 파일 저장 (PageModel에서 처리)
+            string? thumbUrl = null;
+            string? iconUrl = null;
+
+            if (Input.Thumb is not null)
+            {
+                var thumbPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.MemberThumb, Input.ID);
+                thumbUrl = (await fileStorage.SaveFileAsync(Input.Thumb, thumbPath, AllowedFileExtensions, ct))?.Url;
+            }
+
+            if (Input.Icon is not null)
+            {
+                var iconPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.MemberIcon, Input.ID);
+                iconUrl = (await fileStorage.SaveFileAsync(Input.Icon, iconPath, AllowedFileExtensions, ct))?.Url;
+            }
+
+            await mediator.Send(new UpdateMember.Command(
+                Input.ID,
+                Input.MemberGradeID,
+                Input.Email,
+                Input.Name,
+                Input.Password,
+                Input.FirstName,
+                Input.LastName,
+                Input.Intro,
+                Input.Summary,
+                Input.Phone,
+                Input.Birthday,
+                Input.Gender,
+                thumbUrl,
+                isThumbRemove,
+                iconUrl,
+                isIconRemove,
+                Input.IsEmailVerified,
+                Input.IsAuthCertified,
+                Input.IsDenied,
+                Input.IsAdmin,
+                Input.IsWithdraw
+            ), ct);
+
+            TempData["SuccessMessage"] = "회원 정보가 수정되었습니다.";
+        }
+        catch (Exception e)
+        {
+            TempData["ErrorMessages"] = e.Message;
+        }
+
+        return Redirect($"/Member/List/Edit/{Input.ID}{QueryString}");
+    }
+
+    private async Task LoadMemberGradeList(CancellationToken ct)
+    {
+        var grades = await mediator.Send(new GetMemberGrades.Query(), ct);
+
+        MemberGradeList = [.. grades.List.Select(g => new SelectListItem
+        {
+            Value = g.ID.ToString(),
+            Text = g.KorName
+        })];
+    }
+}

+ 273 - 0
Admin/Pages/Member/List/Index.cshtml

@@ -0,0 +1,273 @@
+@page
+@model Admin.Pages.Member.List.IndexModel
+@using Domain.Entities.Members.ValueObject
+@{
+    ViewData["Title"] = "회원 목록";
+}
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <div class="row g-2 mb-2">
+        <div class="col-12 col-lg-auto">
+            <div class="row g-2">
+                <div class="col-auto col-md-auto">
+                    <select id="search" class="form-select">
+                        <option value="1" selected="@(Model.Query.Search == 1)">회원ID</option>
+                        <option value="2" selected="@(Model.Query.Search == 2)">회원 이메일</option>
+                        <option value="3" selected="@(Model.Query.Search == 3)">회원 별명</option>
+                        <option value="4" selected="@(Model.Query.Search == 4)">회원 이름</option>
+                        <option value="5" selected="@(Model.Query.Search == 5)">회원 연락처</option>
+                    </select>
+                </div>
+                <div class="col col-md-auto">
+                    <input type="search" id="keyword" class="form-control" maxlength="100" value="@Model.Query.Keyword" />
+                </div>
+            </div>
+        </div>
+        <div class="col-12 col-lg-auto">
+            <div class="row row-cols-2 g-2">
+                <div class="col">
+                    <input type="datetime-local" id="startAt" class="form-control" value="@Model.Query.StartAt" />
+                </div>
+                <div class="col d-none">~</div>
+                <div class="col">
+                    <input type="datetime-local" id="endAt" class="form-control" value="@Model.Query.EndAt" />
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="row g-2 mb-2">
+        <div class="col-12 col-sm">
+            <div class="row g-2">
+                <div class="col-12 col-md-auto">
+                    <div class="row g-3 align-items-center">
+                        <div class="col-auto">
+                            <label class="col-form-label">성별</label>
+                        </div>
+                        <div class="col">
+                            <div class="form-check form-check-inline">
+                                <input type="checkbox" name="gender" id="male" class="form-check-input" value="@Gender.Male" checked="@(Model.Query.Gender == Gender.Male)" />
+                                <label for="male" class="form-check-label">남자</label>
+                            </div>
+                            <div class="form-check form-check-inline">
+                                <input type="checkbox" name="gender" id="female" class="form-check-input" value="@Gender.Female" checked="@(Model.Query.Gender == Gender.Female)" />
+                                <label for="female" class="form-check-label">여자</label>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-12 col-md-auto align-self-center">
+                    <div class="row g-2">
+                        <div class="col-auto">
+                            <div class="form-check form-check-inline">
+                                <input type="checkbox" id="isEmailVerified" class="form-check-input" value="1" checked="@(Model.Query.IsEmailVerified == 1)" />
+                                <label for="isEmailVerified" class="form-check-label">이메일 인증</label>
+                            </div>
+                        </div>
+                        <div class="col-auto">
+                            <div class="form-check form-check-inline">
+                                <input type="checkbox" id="isAuthCertified" class="form-check-input" value="1" checked="@(Model.Query.IsAuthCertified == 1)" />
+                                <label for="isAuthCertified" class="form-check-label">본인 인증</label>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div class="row justify-content-center mt-3">
+        <div class="col col-sm-auto">
+            <button type="submit" id="btnSearch" class="btn btn-primary w-100">검색</button>
+        </div>
+    </div>
+
+    <hr />
+
+    <div class="row g-2">
+        <div class="col">
+            Total : @Model.Total
+        </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="Write">추가</a>
+        </div>
+    </div>
+
+    <ul class="nav nav-tabs">
+        <li class="nav-item">
+            <a class="nav-link @(Model.Query.Tab == 0 ? "active" : null)" href="/Member/List?tab=0">전체</a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link @(Model.Query.Tab == 1 ? "active" : null)" href="/Member/List?tab=1">차단</a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link @(Model.Query.Tab == 2 ? "active" : null)" href="/Member/List?tab=2">탈퇴</a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link @(Model.Query.Tab == 3 ? "active" : null)" href="/Member/List?tab=3">GM</a>
+        </li>
+    </ul>
+
+    <form name="f_admin_list" id="fAdminList" method="post" accept-charset="utf-8" autocomplete="off" asp-page-handler="Delete">
+        @Html.AntiForgeryToken()
+    </form>
+
+    <div class="table-responsive">
+        <table class="table table-bordered mt-3">
+            <colgroup>
+                <col style="width: 5%;" />
+                <col style="width: 10%;" />
+                <col style="width: 10%;" />
+                <col style="width: 7%;" />
+                <col style="width: 10%;" />
+                <col style="width: 10%;" />
+                <col style="width: 17%;" />
+                <col style="width: 8%;" />
+                <col style="width: 5%;" />
+                <col style="width: 11%;" />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th rowspan="2">ID</th>
+                    <th colspan="2">
+                        <div class="form-check-inline">
+                            <input type="checkbox" id="checkedAll" class="form-check-input" value="1" form="fAdminList" />
+                            <label for="checkedAll">이메일</label>
+                        </div>
+                    </th>
+                    <th>성별</th>
+                    <th>구독자</th>
+                    <th>연락처</th>
+                    <th rowspan="2">인증/본인/차단/탈퇴</th>
+                    <th>최종 접속</th>
+                    <th>가입 일시</th>
+                    <th rowspan="2">비고</th>
+                </tr>
+                <tr>
+                    <th>이름</th>
+                    <th>별명</th>
+                    <th>생년월일</th>
+                    <th>구독 중</th>
+                    <th>등급</th>
+                    <th>로그인 IP</th>
+                    <th>수정 일시</th>
+                </tr>
+            </thead>
+            @if (Model.List == null || Model.List.Count <= 0)
+            {
+                <tbody>
+                    <tr>
+                        <td colspan="11">No Data.</td>
+                    </tr>
+                </tbody>
+            }
+            else
+            {
+                @foreach (var row in Model.List)
+                {
+                    <tbody class="striped">
+                        <tr>
+                            <td rowspan="2">@row.ID</td>
+                            <td colspan="2">
+                                <div class="form-check-inline">
+                                    <input type="checkbox" name="ids[]" id="chk_@row.ID" class="form-check-input list-check-box" value="@row.ID" form="fAdminList" />
+                                    <label for="chk_@row.ID" class="form-check-inline">@row.Email</label>
+                                </div>
+                            </td>
+                            <td>@row.Gender</td>
+                            <td>@row.Followed</td>
+                            <td>@row.Phone</td>
+                            <td rowspan="2">
+                                <input type="checkbox" readonly checked="@(row.IsEmailVerified == 'Y')" disabled />
+                                <input type="checkbox" readonly checked="@(row.IsAuthCertified == 'Y')" disabled />
+                                <input type="checkbox" readonly checked="@(row.IsDenied == 'Y')" disabled />
+                                <input type="checkbox" readonly checked="@(row.IsWithdraw == 'Y')" disabled />
+                            </td>
+                            <td>@row.LastLoginAt</td>
+                            <td>@row.CreatedAt</td>
+                            <td rowspan="2">
+                                <div class="d-xl-flex gap-2 justify-content-center d-grid">
+                                    <a class="btn btn-sm btn-outline-dark" href="@row.ViewURL">상세</a>
+                                    <a class="btn btn-sm btn-outline-secondary" href="@row.ApproveURL">승인</a>
+                                    <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>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>@row.FullName</td>
+                            <td>
+                                @if (!string.IsNullOrEmpty(row.Icon))
+                                {
+                                    <img src="@row.Icon" class="img-thumbnail me-2" alt="@row.Name" />
+                                }
+                                @row.Name
+                            </td>
+                            <td>@row.Birthday</td>
+                            <td>@row.Following</td>
+                            <td>@row.GradeName</td>
+                            <td>@row.LastLoginIp</td>
+                            <td>@row.UpdatedAt</td>
+                        </tr>
+                    </tbody>
+                }
+            }
+        </table>
+
+        <partial name="_Pagination" model="Model.Pagination" />
+    </div>
+</div>
+
+@section Scripts {
+    <script>
+        function updateQueryString() {
+            let queryParams = new URLSearchParams();
+            const gender = $("[name='gender']:checked").val();
+
+            queryParams.set("search", document.getElementById("search").value);
+            queryParams.set("keyword", document.getElementById("keyword").value);
+            queryParams.set("startAt", document.getElementById("startAt").value);
+            queryParams.set("endAt", document.getElementById("endAt").value);
+            queryParams.set("perPage", document.getElementById("perPage").value);
+
+            if (gender) {
+                queryParams.set("gender", $("[name='gender']:checked").val());
+            }
+
+            queryParams.set("isEmailVerified", Number(document.getElementById("isEmailVerified").checked));
+            queryParams.set("isAuthCertified", Number(document.getElementById("isAuthCertified").checked));
+            queryParams.set("tab", @Model.Query.Tab);
+
+            window.location.href = window.location.pathname + "?" + queryParams.toString();
+        }
+
+        $(document).on("change", "#perPage", function () {
+            updateQueryString();
+        });
+
+        $(document).on("click", "#btnSearch", function(e) {
+            e.preventDefault();
+            updateQueryString();
+        });
+
+        $(document).on("keyup", "#keyword, #startAt, #endAt", function(e) {
+            if (e.which === 13 || e.key === "Enter") {
+                e.preventDefault();
+                updateQueryString();
+            }
+        });
+    </script>
+}

+ 155 - 0
Admin/Pages/Member/List/Index.cshtml.cs

@@ -0,0 +1,155 @@
+using SharedKernel.Helpers;
+using SharedKernel.Extensions;
+using Domain.Entities.Members.ValueObject;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+
+namespace Admin.Pages.Member.List;
+
+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; } = 10;
+
+        [DisplayName("검색 조건")]
+        [Range(1, 5, ErrorMessage = "{0}이(가) 올바르지 않습니다.")]
+        public int? Search { get; set; }
+
+        [DisplayName("검색어")]
+        [MaxLength(255, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? Keyword { get; set; }
+
+        [DisplayName("시작일")]
+        public string? StartAt { get; set; }
+
+        [DisplayName("종료일")]
+        public string? EndAt { get; set; }
+
+        [DisplayName("성별")]
+        public Gender? Gender { get; set; }
+
+        [DisplayName("이메일 인증")]
+        public int? IsEmailVerified { get; set; }
+
+        [DisplayName("본인 인증")]
+        public int? IsAuthCertified { get; set; }
+
+        [DisplayName("탭")]
+        public int Tab { get; set; } = 0;
+    }
+
+    public int Total { get; set; } = 0;
+
+    public List<(
+        int ID,
+        string Email,
+        string? Name,
+        string? FullName,
+        string? Icon,
+        string? Phone,
+        string? Birthday,
+        string? Gender,
+        string? GradeName,
+        char IsEmailVerified,
+        char IsAuthCertified,
+        char IsDenied,
+        char IsWithdraw,
+        long Following,
+        long Followed,
+        string? LastLoginAt,
+        string? LastLoginIp,
+        string? UpdatedAt,
+        string CreatedAt,
+        string ViewURL,
+        string ApproveURL,
+        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 SearchMembers.Query(
+            Query.PageNum,
+            Query.PerPage,
+            Query.Search,
+            Query.Keyword,
+            Query.StartAt,
+            Query.EndAt,
+            Query.Gender,
+            Query.IsEmailVerified,
+            Query.IsAuthCertified,
+            Query.Tab
+        ), ct);
+
+        Total = result.Total;
+
+        var qs = Request.QueryString.ToString();
+
+        List = [..result.List.Select(c => (
+            c.ID,
+            c.Email,
+            Name: c.Name ?? "-",
+            FullName: c.FullName ?? "-",
+            Icon: c.Icon ?? "-",
+            Phone: c.Phone ?? "-",
+            Birthday: c.Birthday ?? "-",
+            Gender: c.Gender ?? "-",
+            GradeName: c.GradeName ?? "-",
+            IsEmailVerified: c.IsEmailVerified ? 'Y' : 'N',
+            IsAuthCertified: c.IsAuthCertified ? 'Y' : 'N',
+            IsDenied: c.IsDenied ? 'Y' : 'N',
+            IsWithdraw: c.IsWithdraw ? 'Y' : 'N',
+            c.Following,
+            c.Followed,
+            LastLoginAt: c.LastLoginAt.GetDateAt() ?? "-",
+            c.LastLoginIp,
+            UpdatedAt: c.UpdatedAt.GetDateAt() ?? "-",
+            CreatedAt: c.CreatedAt.GetDateAt(),
+            ViewURL: $"/Member/List/View/{c.ID}{qs}",
+            ApproveURL: $"/Member/List/Approve/{c.ID}{qs}",
+            EditURL: $"/Member/List/Edit/{c.ID}{qs}"
+        ))];
+
+        Pagination = new Pagination(result.Total, Query.PageNum, Query.PerPage);
+    }
+
+    public async Task<IActionResult> OnPostDeleteAsync(int[] ids, CancellationToken ct)
+    {
+        try
+        {
+            if (ids.Length == 0)
+            {
+                throw new Exception("삭제할 항목을 선택해주세요.");
+            }
+
+            await mediator.Send(new DeleteMember.Command(ids), ct);
+
+            TempData["SuccessMessage"] = $"{ids.Length}건이 삭제되었습니다.";
+        }
+        catch (Exception e)
+        {
+            TempData["ErrorMessages"] = e.Message;
+        }
+
+        return RedirectToPage(Query);
+    }
+}

+ 268 - 0
Admin/Pages/Member/List/View.cshtml

@@ -0,0 +1,268 @@
+@page "{id:int}"
+@model Admin.Pages.Member.List.ViewModel
+@{
+    ViewData["Title"] = "회원 상세 관리";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <!-- 기본 정보 -->
+    <div class="row mb-3">
+        <label class="col-sm-2">회원 ID</label>
+        <div class="col-sm-10">
+            @Model.ID
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">SID</label>
+        <div class="col-sm-10">
+            @Model.SID
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">회원 이메일</label>
+        <div class="col-sm-10">
+            @Model.Email
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">별명</label>
+        <div class="col-sm-10">
+            @(Model.Name ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">본명</label>
+        <div class="col-sm-10">
+            @(Model.FullName ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">성 / 이름</label>
+        <div class="col-sm-10">
+            @(Model.FirstName ?? "-") @(Model.LastName ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">회원 등급</label>
+        <div class="col-sm-10">
+            @(Model.GradeName ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">성별</label>
+        <div class="col-sm-10">
+            @(Model.Gender ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">생년월일</label>
+        <div class="col-sm-10">
+            @(Model.Birthday ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">연락처</label>
+        <div class="col-sm-10">
+            @(Model.Phone ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">한마디</label>
+        <div class="col-sm-10">
+            @(string.IsNullOrWhiteSpace(Model.Summary) ? "-" : Model.Summary)
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">자기소개</label>
+        <div class="col-sm-10">
+            @(string.IsNullOrWhiteSpace(Model.Intro) ? "-" : Model.Intro)
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">구독 중 / 구독자</label>
+        <div class="col-sm-10">
+            @Model.Following.ToString("N0") / @Model.Followed.ToString("N0")
+        </div>
+    </div>
+
+    <!-- 상태 -->
+    <div class="row mb-3">
+        <label class="col-sm-2">이메일 인증</label>
+        <div class="col-sm-10">
+            @Model.IsEmailVerified
+            <span class="text-muted ms-2">@Model.EmailVerifiedAt</span>
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">본인 인증</label>
+        <div class="col-sm-10">
+            @Model.IsAuthCertified
+            <span class="text-muted ms-2">@Model.AuthCertifiedAt</span>
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">차단</label>
+        <div class="col-sm-10">
+            @Model.IsDenied
+            <span class="text-muted ms-2">@Model.DeniedAt</span>
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">운영진</label>
+        <div class="col-sm-10">
+            @Model.IsAdmin
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">탈퇴</label>
+        <div class="col-sm-10">
+            @Model.IsWithdraw
+            <span class="text-muted ms-2">@Model.DeletedAt</span>
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">크리에이터</label>
+        <div class="col-sm-10">
+            @Model.IsCreator
+        </div>
+    </div>
+
+    <!-- 접속/로그 정보 -->
+    <div class="row mb-3">
+        <label class="col-sm-2">마지막 로그인</label>
+        <div class="col-sm-10">
+            @(Model.LastLoginAt ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">가입 IP</label>
+        <div class="col-sm-10">
+            @(Model.SignupIP ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">마지막 로그인 IP</label>
+        <div class="col-sm-10">
+            @(Model.LastLoginIp ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">IP</label>
+        <div class="col-sm-10">
+            @(Model.IpAddress ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">UserAgent</label>
+        <div class="col-sm-10">
+            <small class="text-muted">@(Model.UserAgent ?? "-")</small>
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">Device</label>
+        <div class="col-sm-10">
+            <small class="text-muted">@(Model.DeviceInfo ?? "-")</small>
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">수정 일시</label>
+        <div class="col-sm-10">
+            @(Model.UpdatedAt ?? "-")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">가입 일시</label>
+        <div class="col-sm-10">
+            @Model.CreatedAt
+        </div>
+    </div>
+
+    <hr />
+
+    <h4>지갑 정보</h4>
+    <div class="row mb-3">
+        <label class="col-sm-2">보유 잔액(P)</label>
+        <div class="col-sm-10">
+            @Model.Wallet.Balance.ToString("N0")
+        </div>
+    </div>
+
+    <div class="row mb-3">
+        <label class="col-sm-2">출금 가능 금액(P)</label>
+        <div class="col-sm-10">
+            @Model.Wallet.CreditBalance.ToString("N0")
+        </div>
+    </div>
+
+    <hr />
+    <h4>채널 정보</h4>
+
+    @if (Model.Channel != null) {
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">채널 SID</label>
+            <div class="col-sm-10">
+                @Model.Channel.Value.SID
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">채널명</label>
+            <div class="col-sm-10">
+                @Model.Channel.Value.Name
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">@@핸들</label>
+            <div class="col-sm-10">
+                @Model.Channel.Value.Handle
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">채널 주소</label>
+            <div class="col-sm-10">
+                <a href="@Model.Channel.Value.YouTubeUrl" target="_blank" rel="external">@Model.Channel.Value.YouTubeUrl</a>
+            </div>
+        </div>
+    }
+    else
+    {
+        <text>채널을 소유하지 않았습니다.</text>
+    }
+
+    <hr />
+    <div class="d-grid gap-2 text-center d-md-block">
+        <a href="/Member/List?@ViewData["QueryString"]" class="btn btn-secondary">확인</a>
+    </div>
+</div>
+
+@section Scripts {
+    <script></script>
+}

+ 108 - 0
Admin/Pages/Member/List/View.cshtml.cs

@@ -0,0 +1,108 @@
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace Admin.Pages.Member.List;
+
+public class ViewModel(IMediator mediator) : PageModel
+{
+    // 기본 정보
+    public int ID { get; set; }
+    public string SID { get; set; } = "";
+    public string Email { get; set; } = "";
+    public string? Name { get; set; }
+    public string? FullName { get; set; }
+    public string? FirstName { get; set; }
+    public string? LastName { get; set; }
+    public string? Phone { get; set; }
+    public string? Birthday { get; set; }
+    public string? Gender { get; set; }
+    public string? Summary { get; set; }
+    public string? Intro { get; set; }
+    public string? GradeName { get; set; }
+    public long Following { get; set; }
+    public long Followed { get; set; }
+
+    // 상태
+    public char IsEmailVerified { get; set; }
+    public char IsAuthCertified { get; set; }
+    public char IsDenied { get; set; }
+    public char IsAdmin { get; set; }
+    public char IsWithdraw { get; set; }
+    public char IsCreator { get; set; }
+
+    // 상태 일시
+    public string? EmailVerifiedAt { get; set; }
+    public string? AuthCertifiedAt { get; set; }
+    public string? DeniedAt { get; set; }
+    public string? DeletedAt { get; set; }
+
+    // 접속/로그 정보
+    public string? DeviceInfo { get; set; }
+    public string? SignupIP { get; set; }
+    public string? LastLoginIp { get; set; }
+    public string? IpAddress { get; set; }
+    public string? UserAgent { get; set; }
+    public string? LastLoginAt { get; set; }
+    public string? UpdatedAt { get; set; }
+    public string CreatedAt { get; set; } = "";
+
+    // 지갑 정보
+    public (long Balance, long CreditBalance) Wallet { get; set; }
+
+    // 채널 정보
+    public (string SID, string Name, string? Handle, string YouTubeUrl)? Channel { get; set; }
+
+    public async Task<IActionResult> OnGetAsync(int id, CancellationToken ct)
+    {
+        var result = await mediator.Send(new GetMember.Query(id), ct);
+
+        ID = result.ID;
+        SID = result.SID;
+        Email = result.Email;
+        Name = result.Name;
+        FullName = result.FullName;
+        FirstName = result.FirstName;
+        LastName = result.LastName;
+        Phone = result.Phone;
+        Birthday = result.Birthday?.ToString("yyyy-MM-dd");
+        Gender = result.Gender?.ToString();
+        Summary = result.Summary;
+        Intro = result.Intro;
+        GradeName = result.GradeName;
+        Following = result.Following;
+        Followed = result.Followed;
+
+        IsEmailVerified = result.IsEmailVerified ? 'Y' : 'N';
+        IsAuthCertified = result.IsAuthCertified ? 'Y' : 'N';
+        IsDenied = result.IsDenied ? 'Y' : 'N';
+        IsAdmin = result.IsAdmin ? 'Y' : 'N';
+        IsWithdraw = result.IsWithdraw ? 'Y' : 'N';
+        IsCreator = result.IsCreator ? 'Y' : 'N';
+
+        EmailVerifiedAt = result.EmailVerifiedAt?.ToString("yyyy-MM-dd HH:mm:ss");
+        AuthCertifiedAt = result.AuthCertifiedAt?.ToString("yyyy-MM-dd HH:mm:ss");
+        DeniedAt = result.DeniedAt?.ToString("yyyy-MM-dd HH:mm:ss");
+        DeletedAt = result.DeletedAt?.ToString("yyyy-MM-dd HH:mm:ss");
+
+        DeviceInfo = result.DeviceInfo;
+        SignupIP = result.SignupIP;
+        LastLoginIp = result.LastLoginIp;
+        IpAddress = result.IpAddress;
+        UserAgent = result.UserAgent;
+        LastLoginAt = result.LastLoginAt?.ToString("yyyy-MM-dd HH:mm:ss");
+        UpdatedAt = result.UpdatedAt?.ToString("yyyy-MM-dd HH:mm:ss");
+        CreatedAt = result.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss");
+
+        Wallet = (result.Wallet.Balance, result.Wallet.CreditBalance);
+
+        if (result.Channel is not null)
+        {
+            Channel = (result.Channel.SID, result.Channel.Name, result.Channel.Handle, result.Channel.YouTubeUrl);
+        }
+
+        ViewData["QueryString"] = Request.QueryString.ToString().TrimStart('?');
+
+        return Page();
+    }
+}

+ 241 - 0
Admin/Pages/Member/List/Write.cshtml

@@ -0,0 +1,241 @@
+@page
+@model Admin.Pages.Member.List.WriteModel
+@using Domain.Entities.Members.ValueObject
+@{
+    ViewData["Title"] = "회원 등록";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" enctype="multipart/form-data">
+        <input type="hidden" asp-for="QueryString" />
+
+        <div class="row mb-2">
+            <label asp-for="Input.MemberGradeID" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <select asp-for="Input.MemberGradeID" class="form-select w-auto" asp-items="Model.MemberGradeList">
+                    <option value="">등급 선택</option>
+                </select>
+                <span asp-validation-for="Input.MemberGradeID" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Input_Email" class="col-sm-2 col-form-label"><span>*</span> 이메일</label>
+            <div class="col-sm-10">
+                <input type="email" asp-for="Input.Email" class="form-control" required maxlength="60" placeholder="중복 시 등록이 불가합니다. 60자 이내" />
+                <span asp-validation-for="Input.Email" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Input_Name" class="col-sm-2 col-form-label"><span>*</span> 별명</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.Name" class="form-control" required maxlength="20" placeholder="중복 시 등록이 불가합니다. 20자 이내" />
+                <span asp-validation-for="Input.Name" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Input_Password" class="col-sm-2 col-form-label"><span>*</span> 비밀번호</label>
+            <div class="col-sm-10">
+                <input type="password" asp-for="Input.Password" class="form-control" required minlength="4" />
+                <span asp-validation-for="Input.Password" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="RePassword" class="col-sm-2 col-form-label"><span>*</span> 비밀번호 확인</label>
+            <div class="col-sm-10">
+                <input type="password" name="RePassword" id="RePassword" class="form-control" required minlength="4" />
+            </div>
+        </div>
+        <hr/>
+        <div class="row mb-2">
+            <label asp-for="Input.FirstName" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.FirstName" class="form-control" maxlength="20" placeholder="최대 20자" />
+                <span asp-validation-for="Input.FirstName" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.LastName" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.LastName" class="form-control" maxlength="40" placeholder="최대 40자" />
+                <span asp-validation-for="Input.LastName" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Intro" class="col-sm-2"></label>
+            <div class="col-sm-10">
+                <textarea asp-for="Input.Intro" class="form-control" placeholder="최대 1000자" rows="2" maxlength="1000"></textarea>
+                <span asp-validation-for="Input.Intro" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Summary" class="col-sm-2"></label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.Summary" class="form-control" maxlength="50" placeholder="50자 이내"/>
+                <span asp-validation-for="Input.Summary" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Phone" class="col-sm-2"></label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.Phone" class="form-control" maxlength="15" placeholder="010-0000-0000 형식으로 입력하세요." pattern="010-\d{4}-\d{4}" />
+                <span asp-validation-for="Input.Phone" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Gender" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="radio" asp-for="Input.Gender" id="male" class="form-check-input" value="@Gender.Male" />
+                    <label class="form-check-label" for="male">남자</label>
+                </div>
+                <div class="form-check-inline">
+                    <input type="radio" asp-for="Input.Gender" id="female" class="form-check-input" value="@Gender.Female" />
+                    <label class="form-check-label" for="female">여자</label>
+                </div>
+                <span asp-validation-for="Input.Gender" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Birthday" class="col-sm-2"></label>
+            <div class="col-sm-10">
+                <input type="date" asp-for="Input.Birthday" class="form-control w-auto" />
+                <span asp-validation-for="Input.Birthday" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Thumb" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <div id="thumbPrev" hidden><img class="img-fluid img-thumbnail" alt="사진 미리보기" /></div>
+                <input type="file" asp-for="Input.Thumb" class="form-control" accept="image/*" />
+                <span asp-validation-for="Input.Thumb" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.Icon" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <div id="iconPrev" hidden><img class="img-fluid img-thumbnail" alt="아이콘 미리보기" /></div>
+                <input type="file" asp-for="Input.Icon" class="form-control" accept="image/*" />
+                <span asp-validation-for="Input.Icon" class="text-danger"></span>
+            </div>
+        </div>
+        <hr/>
+        <div class="row mb-2">
+            <label asp-for="Input.IsEmailVerified" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsEmailVerified" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsEmailVerified">인증 했습니다.</label>
+                    <span asp-validation-for="Input.IsEmailVerified" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsAuthCertified" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsAuthCertified" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsAuthCertified">인증 했습니다.</label>
+                    <span asp-validation-for="Input.IsAuthCertified" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsDenied" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsDenied" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsDenied">차단합니다.</label>
+                    <span asp-validation-for="Input.IsDenied" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsAdmin" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsAdmin" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsAdmin">관리자입니다.</label>
+                    <span asp-validation-for="Input.IsAdmin" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.IsWithdraw" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="Input.IsWithdraw" class="form-check-input" />
+                    <label class="form-check-label" for="Input_IsWithdraw">탈퇴했습니다.</label>
+                    <span asp-validation-for="Input.IsWithdraw" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <br/>
+        <h4>YouTube 채널 정보</h4>
+        <hr/>
+        <div class="row mb-2">
+            <label for="Input_YouTubeSID" class="col-sm-2 col-form-label"><span>*</span> SID</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.YouTubeSID" class="form-control" minlength="24" maxlength="24" placeholder="중복 시 등록이 불가합니다. 24자" />
+                <span asp-validation-for="Input.YouTubeSID" class="text-danger"></span>
+                <div class="text-muted form-text">
+                    YouTube 채널 고유 ID (예: UCxxxxxxxxxxxxxxxxxxxxxx)
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Input_YouTubeName" class="col-sm-2 col-form-label"><span>*</span> 이름</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Input.YouTubeName" class="form-control" maxlength="200" />
+                <span asp-validation-for="Input.YouTubeName" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label asp-for="Input.YouTubeHandle" class="col-sm-2 col-form-label"></label>
+            <div class="col-sm-10">
+                <div class="input-group mb-3">
+                    <span class="input-group-text">@@</span>
+                    <input type="text" asp-for="Input.YouTubeHandle" class="form-control" maxlength="30" />
+                </div>
+                <span asp-validation-for="Input.YouTubeHandle" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Input_YouTubeUrl" class="col-sm-2 col-form-label"><span>*</span> YouTube 주소</label>
+            <div class="col-sm-10">
+                <input type="url" asp-for="Input.YouTubeUrl" class="form-control" maxlength="255" />
+                <div class="text-muted form-text">
+                    YouTube 채널 주소 (예: https://www.youtube.com/channel/UCxxxxxxxxxxxxxxxxxxxxxx)
+                </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="/Member/List?@Model.QueryString" class="btn btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>
+
+@section Scripts {
+    <script>
+        setupImagePreview("Input_Thumb", "thumbPrev");
+        setupImagePreview("Input_Icon", "iconPrev");
+
+        $(document).on("submit", "#fAdminWrite", function(e) {
+            e.preventDefault();
+            const password = e.target.elements["Input.Password"].value;
+            const rePassword = e.target.elements["RePassword"].value;
+            if (password !== rePassword) {
+                alert("비밀번호가 일치하지 않습니다.");
+                return false;
+            }
+            this.submit();
+        });
+    </script>
+}

+ 192 - 0
Admin/Pages/Member/List/Write.cshtml.cs

@@ -0,0 +1,192 @@
+using Domain.Entities.Members.ValueObject;
+using SharedKernel.Extensions;
+using SharedKernel.Storage;
+using SharedKernel.Attributes;
+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.Member.List;
+
+public class WriteModel(IMediator mediator, IFileStorage fileStorage) : PageModel
+{
+    private static readonly string[] AllowedFileExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
+
+    [BindProperty]
+    public string? QueryString { get; set; }
+
+    public List<SelectListItem> MemberGradeList { get; set; } = [];
+
+    [BindProperty]
+    public InputModel Input { get; set; } = new();
+
+    public sealed class InputModel
+    {
+        [DisplayName("회원등급")]
+        public int? MemberGradeID { get; set; }
+
+        [DisplayName("이메일")]
+        [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
+        [EmailAddress(ErrorMessage = "올바른 이메일 형식이 아닙니다.")]
+        [StringLength(60, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string Email { get; set; } = default!;
+
+        [DisplayName("별명")]
+        [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
+        [StringLength(20, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? Name { get; set; }
+
+        [DisplayName("비밀번호")]
+        [Required(ErrorMessage = "{0}은(는) 필수입니다.")]
+        [MinLength(4, ErrorMessage = "{0}은(는) {1}자 이상 입력하세요.")]
+        public string Password { get; set; } = default!;
+
+        [DisplayName("성")]
+        [StringLength(20, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? FirstName { get; set; }
+
+        [DisplayName("이름")]
+        [StringLength(40, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? LastName { get; set; }
+
+        [DisplayName("자기소개")]
+        [StringLength(1000, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? Intro { get; set; }
+
+        [DisplayName("한마디")]
+        [StringLength(50, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? Summary { get; set; }
+
+        [DisplayName("연락처")]
+        [StringLength(15)]
+        public string? Phone { get; set; }
+
+        [DisplayName("성별")]
+        public Gender? Gender { get; set; }
+
+        [DisplayName("생년월일")]
+        public DateOnly? Birthday { get; set; }
+
+        [DisplayName("사진")]
+        [AllowedExtensions("jpg,jpeg,png,gif,webp", ErrorMessage = "이미지 형식은 jpg, jpeg, png, gif, webp 파일이어야 합니다.")]
+        public IFormFile? Thumb { get; set; }
+
+        [DisplayName("아이콘")]
+        [AllowedExtensions("jpg,jpeg,png,gif,webp", ErrorMessage = "이미지 형식은 jpg, jpeg, png, gif, webp 파일이어야 합니다.")]
+        public IFormFile? Icon { get; set; }
+
+        [DisplayName("이메일 인증")]
+        public bool IsEmailVerified { get; set; }
+
+        [DisplayName("본인 인증")]
+        public bool IsAuthCertified { get; set; }
+
+        [DisplayName("차단 여부")]
+        public bool IsDenied { get; set; }
+
+        [DisplayName("관리자 여부")]
+        public bool IsAdmin { get; set; }
+
+        [DisplayName("탈퇴 여부")]
+        public bool IsWithdraw { get; set; }
+
+        // YouTube 채널 정보
+        [DisplayName("YouTube SID")]
+        [StringLength(24, MinimumLength = 24, ErrorMessage = "{0}은(는) {1}자여야 합니다.")]
+        public string? YouTubeSID { get; set; }
+
+        [DisplayName("YouTube 이름")]
+        [StringLength(200, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? YouTubeName { get; set; }
+
+        [DisplayName("YouTube 핸들")]
+        [StringLength(30, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? YouTubeHandle { get; set; }
+
+        [DisplayName("YouTube 주소")]
+        [StringLength(255, ErrorMessage = "{0}은(는) {1}자 이하로 입력하세요.")]
+        public string? YouTubeUrl { get; set; }
+    }
+
+    public async Task OnGetAsync(CancellationToken ct)
+    {
+        await LoadMemberGradeList(ct);
+        QueryString = Request.QueryString.ToString();
+    }
+
+    public async Task<IActionResult> OnPostAsync(CancellationToken ct)
+    {
+        try
+        {
+            if (!ModelState.IsValid)
+            {
+                throw new Exception(ModelState.GetErrorMessages());
+            }
+
+            // 파일 저장 (PageModel에서 처리)
+            string? thumbUrl = null;
+            string? iconUrl = null;
+
+            if (Input.Thumb is not null)
+            {
+                var thumbPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.MemberThumb, 0);
+                thumbUrl = (await fileStorage.SaveFileAsync(Input.Thumb, thumbPath, AllowedFileExtensions, ct))?.Url;
+            }
+
+            if (Input.Icon is not null)
+            {
+                var iconPath = new FileStoragePath(UploadTarget.Upload, UploadFolder.MemberIcon, 0);
+                iconUrl = (await fileStorage.SaveFileAsync(Input.Icon, iconPath, AllowedFileExtensions, ct))?.Url;
+            }
+
+            await mediator.Send(new CreateMember.Command(
+                Input.MemberGradeID,
+                Input.Email,
+                Input.Name,
+                Input.Password,
+                Input.FirstName,
+                Input.LastName,
+                Input.Intro,
+                Input.Summary,
+                Input.Phone,
+                Input.Birthday,
+                Input.Gender,
+                thumbUrl,
+                iconUrl,
+                Input.IsEmailVerified,
+                Input.IsAuthCertified,
+                Input.IsDenied,
+                Input.IsAdmin,
+                Input.IsWithdraw,
+                Input.YouTubeSID,
+                Input.YouTubeName,
+                Input.YouTubeHandle,
+                Input.YouTubeUrl
+            ), ct);
+
+            TempData["SuccessMessage"] = "회원이 등록되었습니다.";
+            return RedirectToPage("Index");
+        }
+        catch (Exception e)
+        {
+            TempData["ErrorMessages"] = e.Message;
+        }
+
+        await LoadMemberGradeList(ct);
+        return Page();
+    }
+
+    private async Task LoadMemberGradeList(CancellationToken ct)
+    {
+        var grades = await mediator.Send(new GetMemberGrades.Query(), ct);
+
+        MemberGradeList = [.. grades.List.Select(g => new SelectListItem
+        {
+            Value = g.ID.ToString(),
+            Text = g.KorName
+        })];
+    }
+}

+ 2 - 0
Admin/Pages/Member/Log/Email.cshtml

@@ -53,6 +53,8 @@
         </div>
     </form>
 
+    <hr/>
+
     <div class="row g-2 align-items-end">
         <div class="col">
             Total : @Model.Total

+ 8 - 4
Admin/Pages/Member/Log/Intro.cshtml

@@ -53,18 +53,22 @@
         </div>
     </form>
 
-    <div class="row g-2 align-items-end">
-        <div class="col">
+    <hr/>
+
+    <div class="row g-2">
+        <div class="col align-self-end">
             Total : @Model.Total
         </div>
-        <div class="col text-end">
+        <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>
-            <button type="submit" id="btnListDelete" class="btn btn-danger" form="fAdminList" disabled>삭제</button>
+        </div>
+        <div class="col-auto">
+            <button type="button" id="btnListDelete" class="btn btn-danger" form="fAdminList" disabled>삭제</button>
         </div>
     </div>
 

+ 7 - 3
Admin/Pages/Member/Log/Login/Index.cshtml

@@ -51,17 +51,21 @@
         </div>
     </form>
 
-    <div class="row g-2 align-items-end">
-        <div class="col">
+    <hr/>
+
+    <div class="row g-2">
+        <div class="col align-self-end">
             Total : @Model.Total
         </div>
-        <div class="col text-end">
+        <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>

+ 8 - 4
Admin/Pages/Member/Log/Name.cshtml

@@ -53,18 +53,22 @@
         </div>
     </form>
 
-    <div class="row g-2 align-items-end">
-        <div class="col">
+    <hr/>
+
+    <div class="row g-2">
+        <div class="col align-self-end">
             Total : @Model.Total
         </div>
-        <div class="col text-end">
+        <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>
-            <button type="submit" id="btnListDelete" class="btn btn-danger" form="fAdminList" disabled>삭제</button>
+        </div>
+        <div class="col-auto">
+            <button type="button" id="btnListDelete" class="btn btn-danger" form="fAdminList" disabled>삭제</button>
         </div>
     </div>
 

+ 8 - 4
Admin/Pages/Member/Log/Summary.cshtml

@@ -53,18 +53,22 @@
         </div>
     </form>
 
-    <div class="row g-2 align-items-end">
-        <div class="col">
+    <hr/>
+
+    <div class="row g-2">
+        <div class="col align-self-end">
             Total : @Model.Total
         </div>
-        <div class="col text-end">
+        <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>
-            <button type="submit" id="btnListDelete" class="btn btn-danger" form="fAdminList" disabled>삭제</button>
+        </div>
+        <div class="col-auto">
+            <button type="button" id="btnListDelete" class="btn btn-danger" form="fAdminList" disabled>삭제</button>
         </div>
     </div>
 

+ 3 - 1
Admin/Pages/Popup/Index.cshtml

@@ -14,13 +14,15 @@
         <div class="col">
             Total : @Model.Total
         </div>
-        <div class="col text-end">
+        <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="/Popup/Write">Ãß°¡</a>
         </div>

+ 8 - 0
Admin/using.cs

@@ -48,6 +48,14 @@ global using CreateBannerItem = Application.Features.Banner.Item.Create;
 global using UpdateBannerItem = Application.Features.Banner.Item.Update;
 global using DeleteBannerItem = Application.Features.Banner.Item.Delete;
 
+// 회원 목록
+global using SearchMembers = Application.Features.Member.List.Search;
+global using GetMember = Application.Features.Member.List.Get;
+global using CreateMember = Application.Features.Member.List.Create;
+global using UpdateMember = Application.Features.Member.List.Update;
+global using DeleteMember = Application.Features.Member.List.Delete;
+global using ApproveMember = Application.Features.Member.List.Approve;
+
 // 회원 등급
 global using GetMemberGrades = Application.Features.MemberGrade.GetAll;
 global using GetMemberGrade = Application.Features.MemberGrade.Get;

+ 3 - 2
Application/Abstractions/Data/IAppDbContext.cs

@@ -17,15 +17,16 @@ namespace Application.Abstractions.Data
         DbSet<FaqItem> FaqItem { get; set;  }
         DbSet<BannerPosition> BannerPosition { get; set;  }
         DbSet<BannerItem> BannerItem { get; set;  }
-        DbSet<MemberGrade> MemberGrade { get; set;  }
 
-        // Member Logs
         DbSet<Member> Member { get; set; }
+        DbSet<MemberApprove> MemberApprove { get; set; }
+        DbSet<MemberGrade> MemberGrade { get; set; }
         DbSet<MemberLoginLog> MemberLoginLog { get; set; }
         DbSet<MemberEmailChangeLog> MemberEmailChangeLog { get; set; }
         DbSet<MemberNameChangeLog> MemberNameChangeLog { get; set; }
         DbSet<MemberSummaryChangeLog> MemberSummaryChangeLog { get; set; }
         DbSet<MemberIntroChangeLog> MemberIntroChangeLog { get; set; }
+        DbSet<Channel> Channel { get; set; }
 
         Task<int> SaveChangesAsync(CancellationToken ct = default);
     }

+ 1 - 0
Application/Application.csproj

@@ -9,6 +9,7 @@
   <ItemGroup>
     <Folder Include="Authentication\" />
     <Folder Include="Behaviors\" />
+    <Folder Include="Features\Member\Logs\" />
     <Folder Include="Features\ReferenceData\Dtos\" />
   </ItemGroup>
 

+ 10 - 3
Application/Features/Banner/Item/Delete/Handler.cs

@@ -1,7 +1,7 @@
 using Application.Abstractions.Data;
+using SharedKernel.Storage;
 using MediatR;
 using Microsoft.EntityFrameworkCore;
-using SharedKernel.Storage;
 
 namespace Application.Features.Banner.Item.Delete;
 
@@ -18,8 +18,15 @@ public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IReque
 
         foreach (var img in images)
         {
-            fileStorage.DeleteByUrl(img.DesktopImage);
-            fileStorage.DeleteByUrl(img.MobileImage);
+            if (img.DesktopImage != null)
+            {
+                fileStorage.DeleteByUrl(img.DesktopImage);
+            }
+
+            if (img.MobileImage != null)
+            {
+                fileStorage.DeleteByUrl(img.MobileImage);
+            }
         }
 
         await db.BannerItem.Where(c => request.IDs.Contains(c.ID)).ExecuteDeleteAsync(ct);

+ 11 - 0
Application/Features/Member/List/Approve/Command.cs

@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace Application.Features.Member.List.Approve;
+
+public sealed record Command(
+    int MemberID,
+    bool IsReceiveSMS,
+    bool IsReceiveEmail,
+    bool IsReceiveNote,
+    bool IsDisclosureInvest
+) : IRequest;

+ 51 - 0
Application/Features/Member/List/Approve/CommandHandler.cs

@@ -0,0 +1,51 @@
+using Application.Abstractions.Data;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Member.List.Approve;
+
+public sealed class CommandHandler(IAppDbContext db) : IRequestHandler<Command>
+{
+    public async Task Handle(Command request, CancellationToken ct)
+    {
+        var member = await db.Member.Include(x => x.MemberApprove).FirstOrDefaultAsync(x => x.ID == request.MemberID, ct);
+
+        if (member is null)
+        {
+            throw new KeyNotFoundException("회원을 찾을 수 없습니다.");
+        }
+
+        var approve = member.MemberApprove;
+        var now = DateTime.UtcNow;
+
+        // SMS 수신
+        if (request.IsReceiveSMS != approve.IsReceiveSMS)
+        {
+            approve.IsReceiveSMS = request.IsReceiveSMS;
+            approve.ReceiveSMSConsentAt = request.IsReceiveSMS ? now : null;
+        }
+
+        // 이메일 수신
+        if (request.IsReceiveEmail != approve.IsReceiveEmail)
+        {
+            approve.IsReceiveEmail = request.IsReceiveEmail;
+            approve.ReceiveEmailConsentAt = request.IsReceiveEmail ? now : null;
+        }
+
+        // 쪽지 수신
+        if (request.IsReceiveNote != approve.IsReceiveNote)
+        {
+            approve.IsReceiveNote = request.IsReceiveNote;
+            approve.ReceiveNoteConsentAt = request.IsReceiveNote ? now : null;
+        }
+
+        // 투자 현황 공개
+        if (request.IsDisclosureInvest != approve.IsDisclosureInvest)
+        {
+            approve.IsDisclosureInvest = request.IsDisclosureInvest;
+            approve.DisclosureInvestConsentAt = request.IsDisclosureInvest ? now : null;
+        }
+
+        await db.SaveChangesAsync(ct);
+    }
+}

+ 33 - 0
Application/Features/Member/List/Approve/GetHandler.cs

@@ -0,0 +1,33 @@
+using Application.Abstractions.Data;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Member.List.Approve;
+
+public sealed class GetHandler(IAppDbContext db) : IRequestHandler<Query, Response>
+{
+    public async Task<Response> Handle(Query request, CancellationToken ct)
+    {
+        var member = await db.Member.AsNoTracking().Include(x => x.MemberApprove).FirstOrDefaultAsync(x => x.ID == request.MemberID, ct);
+
+        if (member is null)
+        {
+            throw new KeyNotFoundException("회원을 찾을 수 없습니다.");
+        }
+
+        var approve = member.MemberApprove;
+
+        return new Response
+        {
+            MemberID = member.ID,
+            IsReceiveSMS = approve.IsReceiveSMS,
+            ReceiveSMSConsentAt = approve.ReceiveSMSConsentAt,
+            IsReceiveEmail = approve.IsReceiveEmail,
+            ReceiveEmailConsentAt = approve.ReceiveEmailConsentAt,
+            IsReceiveNote = approve.IsReceiveNote,
+            ReceiveNoteConsentAt = approve.ReceiveNoteConsentAt,
+            IsDisclosureInvest = approve.IsDisclosureInvest,
+            DisclosureInvestConsentAt = approve.DisclosureInvestConsentAt
+        };
+    }
+}

+ 5 - 0
Application/Features/Member/List/Approve/Query.cs

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

+ 14 - 0
Application/Features/Member/List/Approve/Response.cs

@@ -0,0 +1,14 @@
+namespace Application.Features.Member.List.Approve;
+
+public sealed class Response
+{
+    public int MemberID { get; init; }
+    public bool IsReceiveSMS { get; init; }
+    public DateTime? ReceiveSMSConsentAt { get; init; }
+    public bool IsReceiveEmail { get; init; }
+    public DateTime? ReceiveEmailConsentAt { get; init; }
+    public bool IsReceiveNote { get; init; }
+    public DateTime? ReceiveNoteConsentAt { get; init; }
+    public bool IsDisclosureInvest { get; init; }
+    public DateTime? DisclosureInvestConsentAt { get; init; }
+}

+ 29 - 0
Application/Features/Member/List/Create/Command.cs

@@ -0,0 +1,29 @@
+using Domain.Entities.Members.ValueObject;
+using MediatR;
+
+namespace Application.Features.Member.List.Create;
+
+public sealed record Command(
+    int? MemberGradeID,
+    string Email,
+    string? Name,
+    string Password,
+    string? FirstName,
+    string? LastName,
+    string? Intro,
+    string? Summary,
+    string? Phone,
+    DateOnly? Birthday,
+    Gender? Gender,
+    string? ThumbUrl,
+    string? IconUrl,
+    bool IsEmailVerified,
+    bool IsAuthCertified,
+    bool IsDenied,
+    bool IsAdmin,
+    bool IsWithdraw,
+    string? YouTubeSID,
+    string? YouTubeName,
+    string? YouTubeHandle,
+    string? YouTubeUrl
+) : IRequest;

+ 72 - 0
Application/Features/Member/List/Create/Handler.cs

@@ -0,0 +1,72 @@
+using Application.Abstractions.Data;
+using Domain.Entities.Members;
+using MemberEntity = Domain.Entities.Members.Member;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Member.List.Create;
+
+public sealed class Handler(IAppDbContext db) : IRequestHandler<Command>
+{
+    public async Task Handle(Command request, CancellationToken ct)
+    {
+        // 이메일 중복 체크
+        if (await db.Member.AnyAsync(x => x.Email == request.Email, ct))
+        {
+            throw new InvalidOperationException("이미 등록된 이메일입니다.");
+        }
+
+        // 별명 중복 체크
+        if (request.Name is not null && await db.Member.AnyAsync(x => x.Name == request.Name, ct))
+        {
+            throw new InvalidOperationException("이미 등록된 별명입니다.");
+        }
+
+        // 회원 생성
+        var member = MemberEntity.Create(request.Email);
+
+        await db.Member.AddAsync(member, ct);
+        await db.SaveChangesAsync(ct);
+
+        // 기본 정보 업데이트
+        await db.Member.Where(x => x.ID == member.ID).ExecuteUpdateAsync(s => s
+            .SetProperty(x => x.MemberGradeID, request.MemberGradeID)
+            .SetProperty(x => x.Name, request.Name)
+            .SetProperty(x => x.Password, request.Password)
+            .SetProperty(x => x.FirstName, request.FirstName)
+            .SetProperty(x => x.LastName, request.LastName)
+            .SetProperty(x => x.Intro, request.Intro)
+            .SetProperty(x => x.Summary, request.Summary)
+            .SetProperty(x => x.Phone, request.Phone)
+            .SetProperty(x => x.Birthday, request.Birthday)
+            .SetProperty(x => x.Gender, request.Gender)
+            .SetProperty(x => x.Thumb, request.ThumbUrl)
+            .SetProperty(x => x.Icon, request.IconUrl)
+            .SetProperty(x => x.IsEmailVerified, request.IsEmailVerified)
+            .SetProperty(x => x.IsAuthCertified, request.IsAuthCertified)
+            .SetProperty(x => x.IsDenied, request.IsDenied)
+            .SetProperty(x => x.IsAdmin, request.IsAdmin)
+            .SetProperty(x => x.IsWithdraw, request.IsWithdraw),
+        ct);
+
+        // MemberApprove 생성
+        await db.MemberApprove.AddAsync(
+            MemberApprove.Create(member.ID)
+        , ct);
+
+        // YouTube 채널 정보가 있는 경우
+        if (!string.IsNullOrWhiteSpace(request.YouTubeSID) &&
+            !string.IsNullOrWhiteSpace(request.YouTubeName) &&
+            !string.IsNullOrWhiteSpace(request.YouTubeUrl))
+        {
+            await db.Channel.AddAsync(Channel.Create(
+                member.ID,
+                request.YouTubeSID,
+                request.YouTubeName,
+                request.YouTubeUrl
+            ), ct);
+        }
+
+        await db.SaveChangesAsync(ct);
+    }
+}

+ 5 - 0
Application/Features/Member/List/Delete/Command.cs

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

+ 34 - 0
Application/Features/Member/List/Delete/Handler.cs

@@ -0,0 +1,34 @@
+using Application.Abstractions.Data;
+using SharedKernel.Storage;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Member.List.Delete;
+
+public sealed class Handler(IAppDbContext db, IFileStorage fileStorage) : IRequestHandler<Command>
+{
+    public async Task Handle(Command request, CancellationToken ct)
+    {
+        if (request.IDs is null || request.IDs.Length == 0)
+        {
+            return;
+        }
+
+        var images = await db.Member.Where(c => request.IDs.Contains(c.ID)).Select(c => new { c.Thumb, c.Icon }).ToListAsync(ct);
+
+        foreach (var img in images)
+        {
+            if (!string.IsNullOrEmpty(img.Thumb))
+            {
+                fileStorage.DeleteByUrl(img.Thumb);
+            }
+
+            if (!string.IsNullOrEmpty(img.Icon))
+            {
+                fileStorage.DeleteByUrl(img.Icon);
+            }
+        }
+
+        await db.Member.Where(x => request.IDs.Contains(x.ID)).ExecuteDeleteAsync(ct);
+    }
+}

+ 68 - 0
Application/Features/Member/List/Get/Handler.cs

@@ -0,0 +1,68 @@
+using Application.Abstractions.Data;
+using Domain.Entities.Wallets.ValueObject;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Member.List.Get;
+
+public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response?>
+{
+    public async Task<Response?> Handle(Query request, CancellationToken ct)
+    {
+        var member = await db.Member.AsNoTracking().Include(x => x.MemberGrade).Include(x => x.MemberStats).Include(x => x.Channel).Include(x => x.Wallet).ThenInclude(w => w!.Balances).FirstOrDefaultAsync(x => x.ID == request.Id, ct);
+
+        if (member is null) return null;
+
+        return new Response
+        {
+            ID = member.ID,
+            SID = member.SID,
+            Email = member.Email,
+            Name = member.Name,
+            FullName = member.FullName,
+            FirstName = member.FirstName,
+            LastName = member.LastName,
+            Phone = member.Phone,
+            Birthday = member.Birthday,
+            Gender = member.Gender,
+            Summary = member.Summary,
+            Intro = member.Intro,
+            Thumb = member.Thumb,
+            Icon = member.Icon,
+            MemberGradeID = member.MemberGradeID,
+            GradeName = member.MemberGrade?.KorName,
+            IsEmailVerified = member.IsEmailVerified,
+            IsAuthCertified = member.IsAuthCertified,
+            IsDenied = member.IsDenied,
+            IsAdmin = member.IsAdmin,
+            IsWithdraw = member.IsWithdraw,
+            IsCreator = member.IsCreator,
+            DeviceInfo = member.DeviceInfo,
+            SignupIP = member.SignupIP,
+            LastLoginIp = member.LastLoginIp,
+            IpAddress = member.IpAddress,
+            UserAgent = member.UserAgent,
+            LastLoginAt = member.LastLoginAt,
+            EmailVerifiedAt = member.EmailVerifiedAt,
+            AuthCertifiedAt = member.AuthCertifiedAt,
+            DeniedAt = member.DeniedAt,
+            DeletedAt = member.DeletedAt,
+            UpdatedAt = member.UpdatedAt,
+            CreatedAt = member.CreatedAt,
+            Following = member.MemberStats?.FollowingCount ?? 0,
+            Followed = member.MemberStats?.FollowerCount ?? 0,
+            Channel = member.Channel != null ? new Response.ChannelInfo
+            {
+                SID = member.Channel.SID,
+                Name = member.Channel.Name,
+                Handle = member.Channel.Handle,
+                YouTubeUrl = member.Channel.YouTubeUrl
+            } : null,
+            Wallet = new Response.WalletInfo
+            {
+                Balance = (long)(member.Wallet?.GetTotalAvailable().Value ?? 0),
+                CreditBalance = (long)(member.Wallet?.GetBalance(WalletBalanceType.Locked).Value ?? 0)
+            }
+        };
+    }
+}

+ 5 - 0
Application/Features/Member/List/Get/Query.cs

@@ -0,0 +1,5 @@
+using MediatR;
+
+namespace Application.Features.Member.List.Get;
+
+public sealed record Query(int Id) : IRequest<Response>;

+ 63 - 0
Application/Features/Member/List/Get/Response.cs

@@ -0,0 +1,63 @@
+using Domain.Entities.Members.ValueObject;
+
+namespace Application.Features.Member.List.Get;
+
+public sealed class Response
+{
+    public int ID { get; init; }
+    public string SID { get; init; } = "";
+    public required string Email { get; init; }
+    public string? Name { get; init; }
+    public string? FullName { get; init; }
+    public string? FirstName { get; init; }
+    public string? LastName { get; init; }
+    public string? Phone { get; init; }
+    public DateOnly? Birthday { get; init; }
+    public Gender? Gender { get; init; }
+    public string? Summary { get; init; }
+    public string? Intro { get; init; }
+    public string? Thumb { get; init; }
+    public string? Icon { get; init; }
+    public int? MemberGradeID { get; init; }
+    public string? GradeName { get; init; }
+    public bool IsEmailVerified { get; init; }
+    public bool IsAuthCertified { get; init; }
+    public bool IsDenied { get; init; }
+    public bool IsAdmin { get; init; }
+    public bool IsWithdraw { get; init; }
+    public bool IsCreator { get; init; }
+    public string? DeviceInfo { get; init; }
+    public string? SignupIP { get; init; }
+    public string? LastLoginIp { get; init; }
+    public string? IpAddress { get; init; }
+    public string? UserAgent { get; init; }
+    public DateTime? LastLoginAt { get; init; }
+    public DateTime? EmailVerifiedAt { get; init; }
+    public DateTime? AuthCertifiedAt { get; init; }
+    public DateTime? DeniedAt { get; init; }
+    public DateTime? DeletedAt { get; init; }
+    public DateTime? UpdatedAt { get; init; }
+    public DateTime CreatedAt { get; init; }
+    public long Following { get; init; }
+    public long Followed { get; init; }
+
+    // Channel
+    public ChannelInfo? Channel { get; init; }
+
+    // Wallet
+    public WalletInfo Wallet { get; init; } = new();
+
+    public sealed class ChannelInfo
+    {
+        public string SID { get; init; } = "";
+        public string Name { get; init; } = "";
+        public string? Handle { get; init; }
+        public string YouTubeUrl { get; init; } = "";
+    }
+
+    public sealed class WalletInfo
+    {
+        public long Balance { get; init; }
+        public long CreditBalance { get; init; }
+    }
+}

+ 131 - 0
Application/Features/Member/List/Search/Handler.cs

@@ -0,0 +1,131 @@
+using Application.Abstractions.Data;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Member.List.Search;
+
+public sealed class Handler(IAppDbContext db) : IRequestHandler<Query, Response>
+{
+    public async Task<Response> Handle(Query request, CancellationToken ct)
+    {
+        var query = db.Member.AsNoTracking().Include(x => x.MemberGrade).Include(x => x.MemberStats).AsQueryable();
+
+        // 탭 필터
+        query = request.Tab switch
+        {
+            1 => query.Where(x => x.IsDenied),
+            2 => query.Where(x => x.IsWithdraw),
+            3 => query.Where(x => x.IsAdmin),
+            _ => query
+        };
+
+        // 키워드 검색
+        if (!string.IsNullOrWhiteSpace(request.Keyword))
+        {
+            query = request.Search switch
+            {
+                1 => query.Where(x => x.ID.ToString().Contains(request.Keyword)),
+                2 => query.Where(x => x.Email.Contains(request.Keyword)),
+                3 => query.Where(x => x.Name != null && x.Name.Contains(request.Keyword)),
+                4 => query.Where(x => x.FullName != null && x.FullName.Contains(request.Keyword)),
+                5 => query.Where(x => x.Phone != null && x.Phone.Contains(request.Keyword)),
+                _ => query
+            };
+        }
+
+        // 날짜 필터
+        if (!string.IsNullOrWhiteSpace(request.StartAt) && DateTime.TryParse(request.StartAt, out var startAt))
+        {
+            query = query.Where(x => x.CreatedAt >= startAt);
+        }
+
+        if (!string.IsNullOrWhiteSpace(request.EndAt) && DateTime.TryParse(request.EndAt, out var endAt))
+        {
+            query = query.Where(x => x.CreatedAt <= endAt);
+        }
+
+        // 성별 필터
+        if (request.Gender.HasValue)
+        {
+            query = query.Where(x => x.Gender == request.Gender.Value);
+        }
+
+        // 이메일 인증 필터
+        if (request.IsEmailVerified == 1)
+        {
+            query = query.Where(x => x.IsEmailVerified);
+        }
+
+        // 본인 인증 필터
+        if (request.IsAuthCertified == 1)
+        {
+            query = query.Where(x => x.IsAuthCertified);
+        }
+
+        var total = await query.CountAsync(ct);
+        var skip = (request.PageNum - 1) * request.PerPage;
+
+        var list = await query
+            .OrderByDescending(x => x.ID)
+            .Skip(skip)
+            .Take(request.PerPage)
+            .Select(x => new
+            {
+                x.ID,
+                x.Email,
+                x.Name,
+                x.FullName,
+                x.Icon,
+                x.Phone,
+                x.Birthday,
+                x.Gender,
+                GradeName = x.MemberGrade != null ? x.MemberGrade.KorName : null,
+                x.IsEmailVerified,
+                x.IsAuthCertified,
+                x.IsDenied,
+                x.IsWithdraw,
+                Following = x.MemberStats != null ? x.MemberStats.FollowingCount : 0,
+                Followed = x.MemberStats != null ? x.MemberStats.FollowerCount : 0,
+                x.LastLoginAt,
+                x.LastLoginIp,
+                x.CreatedAt,
+                x.UpdatedAt
+            })
+            .ToListAsync(ct);
+
+        var rows = list.Select((x, idx) => new Response.Row
+        {
+            Num = total - skip - idx,
+            ID = x.ID,
+            Email = x.Email,
+            Name = x.Name,
+            FullName = x.FullName,
+            Icon = x.Icon,
+            Phone = x.Phone,
+            Birthday = x.Birthday?.ToString("yyyy-MM-dd"),
+            Gender = x.Gender switch
+            {
+                Domain.Entities.Members.ValueObject.Gender.Male => "남자",
+                Domain.Entities.Members.ValueObject.Gender.Female => "여자",
+                _ => null
+            },
+            GradeName = x.GradeName,
+            IsEmailVerified = x.IsEmailVerified,
+            IsAuthCertified = x.IsAuthCertified,
+            IsDenied = x.IsDenied,
+            IsWithdraw = x.IsWithdraw,
+            Following = x.Following,
+            Followed = x.Followed,
+            LastLoginAt = x.LastLoginAt,
+            LastLoginIp = x.LastLoginIp,
+            CreatedAt = x.CreatedAt,
+            UpdatedAt = x.UpdatedAt
+        }).ToList();
+
+        return new Response
+        {
+            Total = total,
+            List = rows
+        };
+    }
+}

+ 17 - 0
Application/Features/Member/List/Search/Query.cs

@@ -0,0 +1,17 @@
+using Domain.Entities.Members.ValueObject;
+using MediatR;
+
+namespace Application.Features.Member.List.Search;
+
+public sealed record Query(
+    int PageNum,
+    ushort PerPage,
+    int? Search = null,
+    string? Keyword = null,
+    string? StartAt = null,
+    string? EndAt = null,
+    Gender? Gender = null,
+    int? IsEmailVerified = null,
+    int? IsAuthCertified = null,
+    int Tab = 0
+) : IRequest<Response>;

+ 32 - 0
Application/Features/Member/List/Search/Response.cs

@@ -0,0 +1,32 @@
+namespace Application.Features.Member.List.Search;
+
+public sealed class Response
+{
+    public int Total { get; init; }
+
+    public required IReadOnlyList<Row> List { get; init; }
+
+    public sealed class Row
+    {
+        public int Num { get; init; }
+        public int ID { get; init; }
+        public required string Email { get; init; }
+        public string? Name { get; init; }
+        public string? FullName { get; init; }
+        public string? Icon { get; init; }
+        public string? Phone { get; init; }
+        public string? Birthday { get; init; }
+        public string? Gender { get; init; }
+        public string? GradeName { get; init; }
+        public bool IsEmailVerified { get; init; }
+        public bool IsAuthCertified { get; init; }
+        public bool IsDenied { get; init; }
+        public bool IsWithdraw { get; init; }
+        public long Following { get; init; }
+        public long Followed { get; init; }
+        public DateTime? LastLoginAt { get; init; }
+        public string? LastLoginIp { get; init; }
+        public DateTime? UpdatedAt { get; init; }
+        public required DateTime CreatedAt { get; init; }
+    }
+}

+ 28 - 0
Application/Features/Member/List/Update/Command.cs

@@ -0,0 +1,28 @@
+using Domain.Entities.Members.ValueObject;
+using MediatR;
+
+namespace Application.Features.Member.List.Update;
+
+public sealed record Command(
+    int ID,
+    int? MemberGradeID,
+    string Email,
+    string? Name,
+    string? Password,
+    string? FirstName,
+    string? LastName,
+    string? Intro,
+    string? Summary,
+    string? Phone,
+    DateOnly? Birthday,
+    Gender? Gender,
+    string? ThumbUrl,
+    bool IsThumbRemove,
+    string? IconUrl,
+    bool IsIconRemove,
+    bool IsEmailVerified,
+    bool IsAuthCertified,
+    bool IsDenied,
+    bool IsAdmin,
+    bool IsWithdraw
+) : IRequest;

+ 86 - 0
Application/Features/Member/List/Update/Handler.cs

@@ -0,0 +1,86 @@
+using Application.Abstractions.Data;
+using MediatR;
+using Microsoft.EntityFrameworkCore;
+
+namespace Application.Features.Member.List.Update;
+
+public sealed class Handler(IAppDbContext db) : IRequestHandler<Command>
+{
+    public async Task Handle(Command request, CancellationToken ct)
+    {
+        var member = await db.Member.AsNoTracking().FirstOrDefaultAsync(x => x.ID == request.ID, ct);
+
+        if (member is null)
+        {
+            throw new KeyNotFoundException("회원을 찾을 수 없습니다.");
+        }
+
+        // 이메일 중복 체크
+        if (member.Email != request.Email && await db.Member.AnyAsync(x => x.Email == request.Email && x.ID != request.ID, ct))
+        {
+            throw new InvalidOperationException("이미 등록된 이메일입니다.");
+        }
+
+        // 별명 중복 체크
+        if (request.Name is not null && member.Name != request.Name && await db.Member.AnyAsync(x => x.Name == request.Name && x.ID != request.ID, ct))
+        {
+            throw new InvalidOperationException("이미 등록된 별명입니다.");
+        }
+
+        // Thumb 처리
+        string? thumbPath = request.IsThumbRemove ? null : (request.ThumbUrl ?? member.Thumb);
+
+        // Icon 처리
+        string? iconPath = request.IsIconRemove ? null : (request.IconUrl ?? member.Icon);
+
+        // 기본 정보 + 비밀번호 업데이트 (단일 쿼리)
+        if (!string.IsNullOrWhiteSpace(request.Password))
+        {
+            await db.Member.Where(x => x.ID == request.ID).ExecuteUpdateAsync(s => s
+                .SetProperty(x => x.MemberGradeID, request.MemberGradeID)
+                .SetProperty(x => x.Email, request.Email)
+                .SetProperty(x => x.Name, request.Name)
+                .SetProperty(x => x.FirstName, request.FirstName)
+                .SetProperty(x => x.LastName, request.LastName)
+                .SetProperty(x => x.Intro, request.Intro)
+                .SetProperty(x => x.Summary, request.Summary)
+                .SetProperty(x => x.Phone, request.Phone)
+                .SetProperty(x => x.Birthday, request.Birthday)
+                .SetProperty(x => x.Gender, request.Gender)
+                .SetProperty(x => x.Thumb, thumbPath)
+                .SetProperty(x => x.Icon, iconPath)
+                .SetProperty(x => x.IsEmailVerified, request.IsEmailVerified)
+                .SetProperty(x => x.IsAuthCertified, request.IsAuthCertified)
+                .SetProperty(x => x.IsDenied, request.IsDenied)
+                .SetProperty(x => x.IsAdmin, request.IsAdmin)
+                .SetProperty(x => x.IsWithdraw, request.IsWithdraw)
+                .SetProperty(x => x.Password, request.Password)
+                .SetProperty(x => x.PasswordUpdatedAt, DateTime.UtcNow)
+                .SetProperty(x => x.UpdatedAt, DateTime.UtcNow),
+            ct);
+        }
+        else
+        {
+            await db.Member.Where(x => x.ID == request.ID).ExecuteUpdateAsync(s => s
+                .SetProperty(x => x.MemberGradeID, request.MemberGradeID)
+                .SetProperty(x => x.Email, request.Email)
+                .SetProperty(x => x.Name, request.Name)
+                .SetProperty(x => x.FirstName, request.FirstName)
+                .SetProperty(x => x.LastName, request.LastName)
+                .SetProperty(x => x.Intro, request.Intro)
+                .SetProperty(x => x.Summary, request.Summary)
+                .SetProperty(x => x.Phone, request.Phone)
+                .SetProperty(x => x.Birthday, request.Birthday)
+                .SetProperty(x => x.Gender, request.Gender)
+                .SetProperty(x => x.Thumb, thumbPath)
+                .SetProperty(x => x.Icon, iconPath)
+                .SetProperty(x => x.IsEmailVerified, request.IsEmailVerified)
+                .SetProperty(x => x.IsAuthCertified, request.IsAuthCertified)
+                .SetProperty(x => x.IsDenied, request.IsDenied)
+                .SetProperty(x => x.IsAdmin, request.IsAdmin)
+                .SetProperty(x => x.IsWithdraw, request.IsWithdraw)
+                .SetProperty(x => x.UpdatedAt, DateTime.UtcNow),
+            ct);
+        }
+    }
+}

+ 4 - 1
Application/Features/MemberGrade/Delete/Handler.cs

@@ -18,7 +18,10 @@ namespace Application.Features.MemberGrade.Delete
 
             foreach (var img in images)
             {
-                fileStorage.DeleteByUrl(img);
+                if (img != null)
+                {
+                    fileStorage.DeleteByUrl(img);
+                }
             }
 
             await db.MemberGrade.Where(c => request.IDs.Contains(c.ID)).ExecuteDeleteAsync(ct);

+ 1 - 1
Domain/Entities/Members/Member.cs

@@ -52,7 +52,7 @@ namespace Domain.Entities.Members
 
         public Gender? Gender { get; private set; }
 
-        public string? Thunmbnail { get; private set; }
+        public string? Thumb { get; private set; }
 
         public string? Icon { get; private set; }
 

+ 2281 - 0
Infrastructure/Infrastructure/Persistence/Migrations/20260206143102_a4.Designer.cs

@@ -0,0 +1,2281 @@
+// <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.Infrastructure.Persistence.Migrations
+{
+    [DbContext(typeof(AppDbContext))]
+    [Migration("20260206143102_a4")]
+    partial class a4
+    {
+        /// <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.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.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.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<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.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", 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<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("Order", "IsActive");
+
+                    b.HasIndex("StartAt", "EndAt", "Order", "IsActive");
+
+                    b.ToTable("Popup", 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>("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.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("EmailTemplate")
+                        .IsRequired();
+
+                    b.Navigation("External")
+                        .IsRequired();
+
+                    b.Navigation("Images")
+                        .IsRequired();
+
+                    b.Navigation("Meta")
+                        .IsRequired();
+
+                    b.Navigation("Payment")
+                        .IsRequired();
+                });
+
+            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.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.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.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.Members.Member", b =>
+                {
+                    b.Navigation("Channel");
+
+                    b.Navigation("MemberApprove")
+                        .IsRequired();
+
+                    b.Navigation("MemberStats")
+                        .IsRequired();
+
+                    b.Navigation("Wallet");
+                });
+
+            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.Wallets.Wallet", b =>
+                {
+                    b.Navigation("Balances");
+
+                    b.Navigation("Transactions");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 28 - 0
Infrastructure/Infrastructure/Persistence/Migrations/20260206143102_a4.cs

@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Infrastructure.Infrastructure.Persistence.Migrations
+{
+    /// <inheritdoc />
+    public partial class a4 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.RenameColumn(
+                name: "Thunmbnail",
+                table: "Member",
+                newName: "Thumb");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.RenameColumn(
+                name: "Thumb",
+                table: "Member",
+                newName: "Thunmbnail");
+        }
+    }
+}

+ 2 - 0
Infrastructure/Persistence/AppDbContext.cs

@@ -21,6 +21,8 @@ namespace Infrastructure.Persistence
         public DbSet<BannerPosition> BannerPosition { get; set; }
         public DbSet<BannerItem> BannerItem { get; set; }
         public DbSet<MemberGrade> MemberGrade { get; set; }
+        public DbSet<MemberApprove> MemberApprove { get; set; }
+        public DbSet<Channel> Channel { get; set; }
 
         // Member Logs
         public DbSet<Member> Member { get; set; }

+ 1 - 1
Infrastructure/Persistence/Configurations/Members/MemberConfiguration.cs

@@ -55,7 +55,7 @@ public sealed class MemberConfiguration : IEntityTypeConfiguration<Member>
         builder.Property(x => x.Phone).HasMaxLength(15).HasComment("연락처");
         builder.Property(x => x.Birthday).HasComment("생년월일");
         builder.Property(x => x.Gender).HasConversion<int?>().HasComment("성별");
-        builder.Property(x => x.Thunmbnail).HasMaxLength(255).HasComment("썸네일");
+        builder.Property(x => x.Thumb).HasMaxLength(255).HasComment("썸네일");
         builder.Property(x => x.Icon).HasMaxLength(255).HasComment("아이콘");
         builder.Property(x => x.IsEmailVerified).IsRequired().HasComment("이메일 인증 여부");
         builder.Property(x => x.IsAuthCertified).IsRequired().HasComment("본인 인증 여부");

+ 1 - 1
Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs

@@ -696,7 +696,7 @@ namespace Infrastructure.Persistence.Migrations
                         .HasColumnType("nvarchar(50)")
                         .HasComment("한마디");
 
-                    b.Property<string>("Thunmbnail")
+                    b.Property<string>("Thumb")
                         .HasMaxLength(255)
                         .HasColumnType("nvarchar(255)")
                         .HasComment("썸네일");