浏览代码

no message

X\choro 9 月之前
父节点
当前提交
0362ecc565
共有 100 个文件被更改,包括 13993 次插入4478 次删除
  1. 二进制
      backend/.vs/ProjectEvaluation/bitforum.metadata.v9.bin
  2. 二进制
      backend/.vs/ProjectEvaluation/bitforum.projects.v9.bin
  3. 二进制
      backend/.vs/ProjectEvaluation/bitforum.strings.v9.bin
  4. 二进制
      backend/.vs/bitforum/CopilotIndices/17.12.38.29086/CodeChunks.db
  5. 二进制
      backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db
  6. 二进制
      backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db-shm
  7. 二进制
      backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db-wal
  8. 二进制
      backend/.vs/bitforum/DesignTimeBuild/.dtbcache.v2
  9. 二进制
      backend/.vs/bitforum/v17/.futdcache.v2
  10. 二进制
      backend/.vs/bitforum/v17/.suo
  11. 116 10
      backend/.vs/bitforum/v17/DocumentLayout.backup.json
  12. 116 10
      backend/.vs/bitforum/v17/DocumentLayout.json
  13. 2 2
      backend/Areas/Identity/Pages/Account/Login.cshtml
  14. 1 1
      backend/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml
  15. 17 0
      backend/Attributes/MustBeTrueAttribute.cs
  16. 66 0
      backend/Constants/Common.cs
  17. 37 0
      backend/Constants/Member.cs
  18. 37 31
      backend/Constants/Menus.cs
  19. 2 6
      backend/Constants/Permissions.cs
  20. 62 0
      backend/Constants/Template.cs
  21. 0 11
      backend/Constants/UserData.cs
  22. 400 0
      backend/Controllers/API/AccountController.cs
  23. 100 0
      backend/Controllers/API/Auth/LoginController.cs
  24. 161 0
      backend/Controllers/API/Auth/PasswordController.cs
  25. 175 0
      backend/Controllers/API/Auth/RegisterController.cs
  26. 274 0
      backend/Controllers/API/AuthController.cs
  27. 286 0
      backend/Controllers/API/SystemController.cs
  28. 133 0
      backend/Controllers/BBS/Board/GroupController.cs
  29. 3 2
      backend/Controllers/Director/RoleController.cs
  30. 1 1
      backend/Controllers/Director/UserController.cs
  31. 241 0
      backend/Controllers/Member/GradeController.cs
  32. 596 0
      backend/Controllers/Member/ListController.cs
  33. 219 0
      backend/Controllers/Member/Log/EmailController.cs
  34. 286 0
      backend/Controllers/Member/Log/LoginController.cs
  35. 219 0
      backend/Controllers/Member/Log/NameController.cs
  36. 83 32
      backend/Controllers/Page/Banner/ItemController.cs
  37. 35 27
      backend/Controllers/Page/Banner/PositionController.cs
  38. 67 31
      backend/Controllers/Page/DocumentController.cs
  39. 33 22
      backend/Controllers/Page/Faq/CategoryController.cs
  40. 73 29
      backend/Controllers/Page/Faq/ItemController.cs
  41. 79 30
      backend/Controllers/Page/PopupController.cs
  42. 0 66
      backend/Controllers/Setting/BasicController.cs
  43. 0 68
      backend/Controllers/Setting/CompanyController.cs
  44. 0 59
      backend/Controllers/Setting/MetaController.cs
  45. 0 59
      backend/Controllers/Setting/RegisterController.cs
  46. 0 92
      backend/Controllers/Setting/TestController.cs
  47. 81 0
      backend/Controllers/System/BasicController.cs
  48. 77 0
      backend/Controllers/System/CompanyController.cs
  49. 9 9
      backend/Controllers/System/EnvsController.cs
  50. 67 0
      backend/Controllers/System/MetaController.cs
  51. 67 0
      backend/Controllers/System/RegisterController.cs
  52. 12 12
      backend/Controllers/System/ServerController.cs
  53. 77 0
      backend/Controllers/System/TemplateController.cs
  54. 74 0
      backend/Controllers/System/TestController.cs
  55. 17 0
      backend/DTOs/Request/ChangeEmailDto.cs
  56. 17 0
      backend/DTOs/Request/ChangeNicknameDto.cs
  57. 24 0
      backend/DTOs/Request/ChangePasswordDto.cs
  58. 18 0
      backend/DTOs/Request/LoginDto.cs
  59. 29 0
      backend/DTOs/Request/RegisterDto.cs
  60. 19 0
      backend/DTOs/Request/ResendVerifyNumberDto.cs
  61. 24 0
      backend/DTOs/Request/ResetPasswordDto.cs
  62. 23 0
      backend/DTOs/Request/VerificationNumberDto.cs
  63. 24 0
      backend/DTOs/Response/ResultDto.cs
  64. 39 1
      backend/Database/DefaultDbContext.cs
  65. 94 0
      backend/Extensions/ServiceExtensions.cs
  66. 22 0
      backend/Extensions/UserExtension.cs
  67. 0 52
      backend/Helpers/Func.cs
  68. 125 0
      backend/Helpers/Functions.cs
  69. 9 9
      backend/Helpers/Pagination.cs
  70. 2 9
      backend/Middleware/Common.cs
  71. 100 0
      backend/Middleware/IPFilter.cs
  72. 0 58
      backend/Migrations/DefaultDb/20250111031413_AddConfigTable.Designer.cs
  73. 0 44
      backend/Migrations/DefaultDb/20250111031413_AddConfigTable.cs
  74. 0 58
      backend/Migrations/DefaultDb/20250115122254_initial.Designer.cs
  75. 0 203
      backend/Migrations/DefaultDb/20250117130258_AddFaqs.Designer.cs
  76. 0 124
      backend/Migrations/DefaultDb/20250117130258_AddFaqs.cs
  77. 0 203
      backend/Migrations/DefaultDb/20250117140508_SyncWithExistingDb.Designer.cs
  78. 0 22
      backend/Migrations/DefaultDb/20250117140508_SyncWithExistingDb.cs
  79. 0 200
      backend/Migrations/DefaultDb/20250117142625_DeleteContentColumnByFaqCategory.Designer.cs
  80. 0 28
      backend/Migrations/DefaultDb/20250117142625_DeleteContentColumnByFaqCategory.cs
  81. 0 203
      backend/Migrations/DefaultDb/20250117153800_UpdateFields.Designer.cs
  82. 0 38
      backend/Migrations/DefaultDb/20250117153800_UpdateFields.cs
  83. 0 251
      backend/Migrations/DefaultDb/20250119234313_AddPopupTable.Designer.cs
  84. 0 68
      backend/Migrations/DefaultDb/20250119234313_AddPopupTable.cs
  85. 0 250
      backend/Migrations/DefaultDb/20250120034312_AddBannerTables.Designer.cs
  86. 0 40
      backend/Migrations/DefaultDb/20250120034312_AddBannerTables.cs
  87. 0 364
      backend/Migrations/DefaultDb/20250120035304_UpdateFieldInBannerItem.Designer.cs
  88. 0 94
      backend/Migrations/DefaultDb/20250120035304_UpdateFieldInBannerItem.cs
  89. 0 668
      backend/Migrations/DefaultDb/20250124015524_AddMember.Designer.cs
  90. 0 191
      backend/Migrations/DefaultDb/20250124015524_AddMember.cs
  91. 0 668
      backend/Migrations/DefaultDb/20250124020320_UpdateMember.Designer.cs
  92. 0 22
      backend/Migrations/DefaultDb/20250124020320_UpdateMember.cs
  93. 1146 0
      backend/Migrations/DefaultDb/20250220162340_a1.Designer.cs
  94. 687 0
      backend/Migrations/DefaultDb/20250220162340_a1.cs
  95. 1160 0
      backend/Migrations/DefaultDb/20250222130451_a2.Designer.cs
  96. 105 0
      backend/Migrations/DefaultDb/20250222130451_a2.cs
  97. 1862 0
      backend/Migrations/DefaultDb/20250222131829_a3.Designer.cs
  98. 537 0
      backend/Migrations/DefaultDb/20250222131829_a3.cs
  99. 2363 0
      backend/Migrations/DefaultDb/20250222132132_a4.Designer.cs
  100. 1163 0
      backend/Migrations/DefaultDb/20250222132132_a4.cs

二进制
backend/.vs/ProjectEvaluation/bitforum.metadata.v9.bin


二进制
backend/.vs/ProjectEvaluation/bitforum.projects.v9.bin


二进制
backend/.vs/ProjectEvaluation/bitforum.strings.v9.bin


二进制
backend/.vs/bitforum/CopilotIndices/17.12.38.29086/CodeChunks.db


二进制
backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db


二进制
backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db-shm


二进制
backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db-wal


二进制
backend/.vs/bitforum/DesignTimeBuild/.dtbcache.v2


二进制
backend/.vs/bitforum/v17/.futdcache.v2


二进制
backend/.vs/bitforum/v17/.suo


+ 116 - 10
backend/.vs/bitforum/v17/DocumentLayout.backup.json

@@ -3,8 +3,32 @@
   "WorkspaceRootPath": "E:\\workspace\\bitforum\\backend\\",
   "Documents": [
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\user\\member.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\user\\member.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\controllers\\page\\banner\\positioncontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:controllers\\page\\banner\\positioncontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\faq\\category\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\faq\\category\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\bbs\\board\\group.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\bbs\\board\\group.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\banner\\position\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\banner\\position\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\controllers\\page\\faq\\categorycontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:controllers\\page\\faq\\categorycontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\controllers\\bbs\\board\\groupcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:controllers\\bbs\\board\\groupcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\constants\\menus.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:constants\\menus.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
     }
   ],
   "DocumentGroupContainers": [
@@ -14,7 +38,7 @@
       "DocumentGroups": [
         {
           "DockedWidth": 200,
-          "SelectedChildIndex": 5,
+          "SelectedChildIndex": 8,
           "Children": [
             {
               "$type": "Bookmark",
@@ -36,17 +60,99 @@
               "$type": "Bookmark",
               "Name": "ST:130:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
             },
+            {
+              "$type": "Bookmark",
+              "Name": "ST:0:0:{57d563b6-44a5-47df-85be-f4199ad6b651}"
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 1,
+              "Title": "Index.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Category\\Index.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Faq\\Category\\Index.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Category\\Index.cshtml",
+              "RelativeToolTip": "Views\\Page\\Faq\\Category\\Index.cshtml",
+              "ViewState": "AgIAAIcAAAAAAAAAAAAAAJsAAAAdAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-02-22T15:34:58.757Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 4,
+              "Title": "CategoryController.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Faq\\CategoryController.cs",
+              "RelativeDocumentMoniker": "Controllers\\Page\\Faq\\CategoryController.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Faq\\CategoryController.cs",
+              "RelativeToolTip": "Controllers\\Page\\Faq\\CategoryController.cs",
+              "ViewState": "AgIAAAAAAAAAAAAAAAAAAA0AAAAwAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
+              "WhenOpened": "2025-02-22T15:31:41.311Z",
+              "EditorCaption": ""
+            },
             {
               "$type": "Document",
               "DocumentIndex": 0,
-              "Title": "Member.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\User\\Member.cs",
-              "RelativeDocumentMoniker": "Models\\User\\Member.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\User\\Member.cs",
-              "RelativeToolTip": "Models\\User\\Member.cs",
-              "ViewState": "AgIAAAEAAAAAAAAAAAAAABIAAAAAAAAAAAAAAA==",
+              "Title": "PositionController.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Banner\\PositionController.cs",
+              "RelativeDocumentMoniker": "Controllers\\Page\\Banner\\PositionController.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Banner\\PositionController.cs",
+              "RelativeToolTip": "Controllers\\Page\\Banner\\PositionController.cs",
+              "ViewState": "AgIAACoAAAAAAAAAAAAAAE0AAAAUAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
+              "WhenOpened": "2025-02-22T15:28:27.389Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 3,
+              "Title": "Index.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Position\\Index.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Banner\\Position\\Index.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Position\\Index.cshtml",
+              "RelativeToolTip": "Views\\Page\\Banner\\Position\\Index.cshtml",
+              "ViewState": "AgIAAGMAAAAAAAAAAAAAAIkAAAAcAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-02-22T15:26:28.148Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 2,
+              "Title": "Group.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\BBS\\Board\\Group.cshtml",
+              "RelativeDocumentMoniker": "Views\\BBS\\Board\\Group.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\BBS\\Board\\Group.cshtml",
+              "RelativeToolTip": "Views\\BBS\\Board\\Group.cshtml",
+              "ViewState": "AgIAAH4AAAAAAAAAAAAAAKMAAAANAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-02-22T14:23:36.472Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 5,
+              "Title": "GroupController.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Controllers\\BBS\\Board\\GroupController.cs",
+              "RelativeDocumentMoniker": "Controllers\\BBS\\Board\\GroupController.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Controllers\\BBS\\Board\\GroupController.cs",
+              "RelativeToolTip": "Controllers\\BBS\\Board\\GroupController.cs",
+              "ViewState": "AgIAAEsAAAAAAAAAAAAAAF8AAABQAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
+              "WhenOpened": "2025-02-22T14:05:06.792Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 6,
+              "Title": "Menus.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Constants\\Menus.cs",
+              "RelativeDocumentMoniker": "Constants\\Menus.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Constants\\Menus.cs",
+              "RelativeToolTip": "Constants\\Menus.cs",
+              "ViewState": "AgIAAGYAAAAAAAAAAAAAAKUAAAAZAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-22T19:33:26.617Z",
+              "WhenOpened": "2025-02-22T13:59:15.831Z",
               "EditorCaption": ""
             }
           ]

+ 116 - 10
backend/.vs/bitforum/v17/DocumentLayout.json

@@ -3,8 +3,32 @@
   "WorkspaceRootPath": "E:\\workspace\\bitforum\\backend\\",
   "Documents": [
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\user\\member.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\user\\member.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\bbs\\board\\group.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\bbs\\board\\group.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\faq\\category\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\faq\\category\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\banner\\position\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\banner\\position\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\controllers\\page\\banner\\positioncontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:controllers\\page\\banner\\positioncontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\controllers\\page\\faq\\categorycontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:controllers\\page\\faq\\categorycontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\controllers\\bbs\\board\\groupcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:controllers\\bbs\\board\\groupcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+    },
+    {
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\constants\\menus.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:constants\\menus.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
     }
   ],
   "DocumentGroupContainers": [
@@ -14,7 +38,7 @@
       "DocumentGroups": [
         {
           "DockedWidth": 200,
-          "SelectedChildIndex": 5,
+          "SelectedChildIndex": 10,
           "Children": [
             {
               "$type": "Bookmark",
@@ -36,17 +60,99 @@
               "$type": "Bookmark",
               "Name": "ST:130:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
             },
+            {
+              "$type": "Bookmark",
+              "Name": "ST:0:0:{57d563b6-44a5-47df-85be-f4199ad6b651}"
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 1,
+              "Title": "Index.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Category\\Index.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Faq\\Category\\Index.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Category\\Index.cshtml",
+              "RelativeToolTip": "Views\\Page\\Faq\\Category\\Index.cshtml",
+              "ViewState": "AgIAAGwAAAAAAAAAAAAAAFAAAAA3AAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-02-22T15:34:58.757Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 4,
+              "Title": "CategoryController.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Faq\\CategoryController.cs",
+              "RelativeDocumentMoniker": "Controllers\\Page\\Faq\\CategoryController.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Faq\\CategoryController.cs",
+              "RelativeToolTip": "Controllers\\Page\\Faq\\CategoryController.cs",
+              "ViewState": "AgIAAAAAAAAAAAAAAAAAAA0AAAAwAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
+              "WhenOpened": "2025-02-22T15:31:41.311Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 3,
+              "Title": "PositionController.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Banner\\PositionController.cs",
+              "RelativeDocumentMoniker": "Controllers\\Page\\Banner\\PositionController.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Banner\\PositionController.cs",
+              "RelativeToolTip": "Controllers\\Page\\Banner\\PositionController.cs",
+              "ViewState": "AgIAAEgAAAAAAAAAAAAAAGAAAABvAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
+              "WhenOpened": "2025-02-22T15:28:27.389Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 2,
+              "Title": "Index.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Position\\Index.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Banner\\Position\\Index.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Position\\Index.cshtml",
+              "RelativeToolTip": "Views\\Page\\Banner\\Position\\Index.cshtml",
+              "ViewState": "AgIAAGwAAAAAAAAAAAAAAHAAAAAUAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-02-22T15:26:28.148Z",
+              "EditorCaption": ""
+            },
             {
               "$type": "Document",
               "DocumentIndex": 0,
-              "Title": "Member.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\User\\Member.cs",
-              "RelativeDocumentMoniker": "Models\\User\\Member.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\User\\Member.cs*",
-              "RelativeToolTip": "Models\\User\\Member.cs*",
-              "ViewState": "AgIAAF8AAAAAAAAAAAAvwJAAAAAfAAAAAAAAAA==",
+              "Title": "Group.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\BBS\\Board\\Group.cshtml",
+              "RelativeDocumentMoniker": "Views\\BBS\\Board\\Group.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\BBS\\Board\\Group.cshtml",
+              "RelativeToolTip": "Views\\BBS\\Board\\Group.cshtml",
+              "ViewState": "AgIAAAAAAAAAAAAAAAAAACEAAAAhAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-02-22T14:23:36.472Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 5,
+              "Title": "GroupController.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Controllers\\BBS\\Board\\GroupController.cs",
+              "RelativeDocumentMoniker": "Controllers\\BBS\\Board\\GroupController.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Controllers\\BBS\\Board\\GroupController.cs",
+              "RelativeToolTip": "Controllers\\BBS\\Board\\GroupController.cs",
+              "ViewState": "AgIAAEsAAAAAAAAAAAAAAF8AAABQAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
+              "WhenOpened": "2025-02-22T14:05:06.792Z",
+              "EditorCaption": ""
+            },
+            {
+              "$type": "Document",
+              "DocumentIndex": 6,
+              "Title": "Menus.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Constants\\Menus.cs",
+              "RelativeDocumentMoniker": "Constants\\Menus.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Constants\\Menus.cs",
+              "RelativeToolTip": "Constants\\Menus.cs",
+              "ViewState": "AgIAAGYAAAAAAAAAAAAAAKUAAAAZAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-22T19:33:26.617Z",
+              "WhenOpened": "2025-02-22T13:59:15.831Z",
               "EditorCaption": ""
             }
           ]

+ 2 - 2
backend/Areas/Identity/Pages/Account/Login.cshtml

@@ -14,12 +14,12 @@
                 <hr />
                 <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
                 <div class="form-floating mb-3">
-                    <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
+                    <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" value="chorongski@gmail.com" />
                     <label asp-for="Input.Email" class="form-label">Email</label>
                     <span asp-validation-for="Input.Email" class="text-danger"></span>
                 </div>
                 <div class="form-floating mb-3">
-                    <input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" />
+                    <input asp-for="Input.Password" class="form-control" autocomplete="current-password" aria-required="true" placeholder="password" value="@@120726KKh" />
                     <label asp-for="Input.Password" class="form-label">Password</label>
                     <span asp-validation-for="Input.Password" class="text-danger"></span>
                 </div>

+ 1 - 1
backend/Areas/Identity/Pages/Account/Manage/_ManageNav.cshtml

@@ -2,7 +2,7 @@
 @{
     var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any();
 }
-<ul class="nav nav-underline flex-column">
+<ul class="nav nav-underline flex-column mb-3">
     <li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">기본 정보</a></li>
     <li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">이메일 변경</a></li>
     <li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">비밀번호 변경</a></li>

+ 17 - 0
backend/Attributes/MustBeTrueAttribute.cs

@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.Attributes
+{
+    public class MustBeTrueAttribute : ValidationAttribute
+    {
+        protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
+        {
+            if (value is bool boolValue && boolValue)
+            {
+                return ValidationResult.Success;
+            }
+
+            return new ValidationResult(ErrorMessage ?? "값은 `true` 여야 합니다.");
+        }
+    }
+}

+ 66 - 0
backend/Constants/Common.cs

@@ -0,0 +1,66 @@
+namespace bitforum.Constants
+{
+    public enum MailStatus
+    {
+        Pending = 0,       // 대기 중
+        Processing = 1,    // 처리 중
+        Sent = 2,          // 발송 완료
+        Failed = 3         // 발송 실패
+    }
+
+    // Redis 키 값들
+    public static class RedisConst
+    {
+        public const int CacheExpiration = 60;
+        public const string ConfigKey = "Config:All";
+        public const string DocumentKey = "Document";
+        public const string FaqKey = "FaqList";
+        public const string PopupKey = "PopupList";
+        public const string BannerKey = "BannerList";
+    }
+
+    // 추가 저장 값들
+    public record AdditionalData
+    {
+        public string? Email { get; init; } // 이메일 변경 시 
+    }
+
+    // 게시판 설정 값들
+    public static class BoardConst
+    {
+        // 종류
+        public enum Layout
+        {
+            Default = 0, // 일반
+            Media = 1, // 사진/동영상
+            QnA = 2 // 질문과 답변 (1:1 게시판)
+        }
+
+        // 기본 정렬
+        public enum OrderBy
+        {
+            CreatedAt = 0, // 날짜순
+            Views = 1, // 조회순
+            Comments = 2, // 댓글순
+            Likes = 3 // 공감순
+        }
+
+        // 알림
+        public enum Notify
+        {
+            Admin = 0, // 최고관리자
+            Manager = 1, // 게시판 관리자
+            PostAuthor = 2, // 게시글 작성자
+            CommentAuthor = 3, // 댓글 작성자
+            ReplyAuthor = 4 // 답글 작성자
+        }
+
+        // 권한
+        public enum Permission
+        {
+            Guest = -1, // 비회원
+            Member = 0, // 정회원
+            Admin = 1000 // 최고관리자
+        }
+    }
+}

+ 37 - 0
backend/Constants/Member.cs

@@ -0,0 +1,37 @@
+namespace bitforum.Constants
+{
+    /// <summary>
+    /// 성별을 나타내는 열거형
+    /// </summary>
+    public enum Gender
+    {
+        Male = 1,
+        Female = 2
+    }
+
+    /// <summary>
+    /// 인증 구별
+    /// </summary>
+    public enum VerificationType
+    {
+        Registration,    // 회원가입
+        ForgotPassword,  // 비밀번호 재설정
+        ChangedEmail     // 이메일 변경
+    }
+
+    /// <summary>
+    /// 로그인 기록 검색기간 구분
+    /// </summary>
+    public static class LoginLogType
+    {
+        public const string Today = "today";
+        public const string Week = "week";
+        public const string Month = "month";
+        public const string HalfYear = "halfyear";
+
+        public static readonly HashSet<string> ValidTypes = new()
+        {
+            Today, Week, Month, HalfYear
+        };
+    }
+}

+ 37 - 31
backend/Constants/MenuData.cs → backend/Constants/Menus.cs

@@ -3,13 +3,13 @@ namespace bitforum.Constants
     public class Menu
     {
         public int Id { get; set; }
-        public string Name { get; set; }
+        public string Name { get; set; } = null!;
         public string? Path { get; set; }
         public string? Icon { get; set; }
         public List<Menu>? Children { get; set; } = new List<Menu>();
     }
 
-    public static class MenuData
+    public static class Menus
     {
         public static List<Menu> GetMenus()
         {
@@ -36,47 +36,53 @@ namespace bitforum.Constants
                         {
                             Id = 201,
                             Name = "서버 정보",
-                            Path = "/Setting/Server"
+                            Path = "/System/Server"
                         },
                         new Menu
                         {
                             Id = 202,
                             Name = "환경변수",
-                            Path = "/Setting/Envs"
+                            Path = "/System/Envs"
                         },
                         new Menu
                         {
                             Id = 203,
                             Name = "기본 설정",
-                            Path = "/Setting/Basic"
+                            Path = "/System/Basic"
                         },
                         new Menu
                         {
                             Id = 204,
                             Name = "메타 태그",
-                            Path = "/Setting/Meta"
+                            Path = "/System/Meta"
                         },
                         new Menu
                         {
                             Id = 205,
                             Name = "회사 정보",
-                            Path = "/Setting/Company"
+                            Path = "/System/Company"
                         },
                         new Menu
                         {
                             Id = 207,
                             Name = "회원가입 설정",
-                            Path = "/Setting/Register"
+                            Path = "/System/Register"
                         },
                         new Menu
                         {
                             Id = 208,
                             Name = "알림 발송 확인",
-                            Path = "/Setting/Test"
+                            Path = "/System/Test"
                         },
                         new Menu
                         {
                             Id = 209,
+                            Name = "알림 발송 양식",
+                            Path = "/System/Template/Email"
+                        },
+                        new Menu
+                        {
+                            Id = 210,
                             Name = "권한 관리",
                             Path = "/Director/User"
                         },
@@ -130,37 +136,37 @@ namespace bitforum.Constants
                         {
                             Id = 401,
                             Name = "회원 목록",
-                            Path = "/User/List"
+                            Path = "/Member/List"
                         },
                         new Menu
                         {
                             Id = 402,
                             Name = "회원 등급",
-                            Path = "/User/Grade"
+                            Path = "/Member/Grade"
                         },
                         new Menu
                         {
                             Id = 403,
                             Name = "현재 접속자",
-                            Path = "/User/Current"
+                            Path = "/Member/Logined"
                         },
                         new Menu
                         {
                             Id = 404,
-                            Name = "로그인 기록",
-                            Path = "/User/Logs/Login"
+                            Name = "로그인 내역",
+                            Path = "/Member/Log/Login"
                         },
                         new Menu
                         {
                             Id = 405,
                             Name = "별명 변경 내역",
-                            Path = "/User/Logs/Name"
+                            Path = "/Member/Log/Name"
                         },
                         new Menu
                         {
                             Id = 406,
                             Name = "이메일 변경 내역",
-                            Path = "/User/Logs/Email"
+                            Path = "/Member/Log/Email"
                         }
                     }
                 },
@@ -177,49 +183,49 @@ namespace bitforum.Constants
                         {
                             Id = 501,
                             Name = "분류 관리",
-                            Path = "/Board/Group"
+                            Path = "/BBS/Board/Group"
                         },
                         new Menu
                         {
                             Id = 502,
                             Name = "게시판 관리",
-                            Path = "/Board/List"
+                            Path = "/BBS/Board/List"
                         },
                         new Menu
                         {
                             Id = 503,
                             Name = "게시물 관리",
-                            Path = "/Board/Post"
+                            Path = "/BBS/Post/List"
                         },
                         new Menu
                         {
                             Id = 504,
                             Name = "휴지통",
-                            Path = "/Board/Post/Trash"
+                            Path = "/BBS/Post/Trash"
                         },
                         new Menu
                         {
                             Id = 505,
                             Name = "첨부파일",
-                            Path = "/Board/Post/File"
+                            Path = "/BBS/Post/File"
                         },
                         new Menu
                         {
                             Id = 506,
                             Name = "이미지",
-                            Path = "/Board/Post/Image"
+                            Path = "/BBS/Post/Image"
                         },
                         new Menu
                         {
                             Id = 507,
-                            Name = "공감 관리",
-                            Path = "/Board/Post/Like"
+                            Name = "반응 관리",
+                            Path = "/BBS/Post/Feedback"
                         },
                         new Menu
                         {
                             Id = 508,
                             Name = "신고 관리",
-                            Path = "/Board/Post/Blame"
+                            Path = "/BBS/Post/Blame"
                         }
                     }
                 },
@@ -236,37 +242,37 @@ namespace bitforum.Constants
                         {
                             Id = 601,
                             Name = "댓글 관리",
-                            Path = "/Board/Comment"
+                            Path = "/BBS/Comment/List"
                         },
                         new Menu
                         {
                             Id = 602,
                             Name = "휴지통",
-                            Path = "/Board/Comment/Trash"
+                            Path = "/BBS/Comment/Trash"
                         },
                         new Menu
                         {
                             Id = 603,
                             Name = "첨부파일",
-                            Path = "/Board/Comment/File"
+                            Path = "/BBS/Comment/File"
                         },
                         new Menu
                         {
                             Id = 604,
                             Name = "이미지",
-                            Path = "/Board/Comment/Image"
+                            Path = "/BBS/Comment/Image"
                         },
                         new Menu
                         {
                             Id = 605,
                             Name = "공감 관리",
-                            Path = "/Board/Comment/Like"
+                            Path = "/BBS/Comment/Feedback"
                         },
                         new Menu
                         {
                             Id = 606,
                             Name = "신고 관리",
-                            Path = "/Board/Comment/Blame"
+                            Path = "/BBS/Comment/Blame"
                         }
                     }
                 }

+ 2 - 6
backend/Constants/Permissions.cs

@@ -1,8 +1,4 @@
-using bitforum.Constants;
-using System.IO;
-using System.Security;
-
-namespace bitforum.Models
+namespace bitforum.Constants
 {
     public static class Permissions
     {
@@ -46,7 +42,7 @@ namespace bitforum.Models
         public static List<string> GeneratePermissions()
         {
             var allPermissions = new List<string>();
-            var menus = MenuData.GetMenus();
+            var menus = Menus.GetMenus();
 
             foreach (var menu in menus)
             {

+ 62 - 0
backend/Constants/Template.cs

@@ -0,0 +1,62 @@
+namespace bitforum.Constants
+{
+    public class TemplateKey
+    {
+        public string Name { get; }
+        public string Subject { get; }
+        public string Content { get; }
+
+        public TemplateKey(string name, string subject, string content)
+        {
+            Name = name;
+            Subject = subject;
+            Content = content;
+        }
+    }
+
+    public static class Template
+    {
+        public static readonly TemplateKey RegisterEmailForm = new("회원가입 시", "register_email_form_title", "register_email_form_content");
+        public static readonly TemplateKey RegistrationEmailForm = new("회원가입 완료", "registration_email_form_title", "registration_email_form_content");
+        public static readonly TemplateKey ForgotPasswordEmailForm = new("비밀번호 재설정", "forgot_password_email_form_title", "forgot_password_email_form_content");
+        public static readonly TemplateKey ChangedPasswordEmailForm = new("비밀번호 변경 완료", "changed_password_email_form_title", "changed_password_email_form_content");
+        public static readonly TemplateKey WithdrawEmailForm = new("회원탈퇴 시", "withdraw_email_form_title", "withdraw_email_form_content");
+        public static readonly TemplateKey EmailVerifyForm = new("이메일 변경 시", "email_verify_form_title", "email_verify_form_content");
+        public static readonly TemplateKey ChangedEmailForm = new("이메일 변경 완료", "changed_email_form_title", "changed_email_form_content");
+    }
+
+    public interface IPlaceholder
+    {
+        Dictionary<string, string> ToDictionary();
+    }
+
+    public class Placeholders : IPlaceholder
+    {
+        private readonly Dictionary<string, string> _data = new();
+
+        private Placeholders() { } // 생성자 직접 호출 방지
+
+        // 회원가입 시
+        public static Placeholders RegisterEmailForm(string email, string code) => new Placeholders { _data = { ["email"] = email, ["code"] = code } };
+
+        // 회원가입 수락
+        public static Placeholders RegistrationEmailForm(string email, string createdAt) => new Placeholders { _data = { ["email"] = email, ["createdAt"] = createdAt } };
+
+        // 비밀번호 재설정
+        public static Placeholders ForgotPasswordEmailForm(string email, string code) => new Placeholders { _data = { ["email"] = email, ["code"] = code } };
+
+        // 비밀번호 변경 완료
+        public static Placeholders ChangedPasswordEmailForm(string email, string createdAt) => new Placeholders { _data = { ["email"] = email, ["createdAt"] = createdAt } };
+
+        // 회원탈퇴 시
+        public static Placeholders WithdrawEmailForm(string email, string reason, string createdAt) => new Placeholders { _data = { ["email"] = email, ["reason"] = reason, ["createdAt"] = createdAt } };
+
+        // 이메일 변경 시
+        public static Placeholders EmailVerifyForm(string email, string link) => new Placeholders { _data = { ["email"] = email, ["link"] = link } };
+
+        // 이메일 변경 완료
+        public static Placeholders ChangedEmailForm(string email, string createdAt) => new Placeholders { _data = { ["email"] = email, ["createdAt"] = createdAt } };
+
+        public Dictionary<string, string> ToDictionary() => _data;
+    }
+}

+ 0 - 11
backend/Constants/UserData.cs

@@ -1,11 +0,0 @@
-namespace bitforum.Constants
-{
-    /// <summary>
-    /// 성별을 나타내는 열거형
-    /// </summary>
-    public enum Gender
-    {
-        Male = 1,
-        Female = 2
-    }
-}

+ 400 - 0
backend/Controllers/API/AccountController.cs

@@ -0,0 +1,400 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.EntityFrameworkCore;
+using bitforum.DTOs.Request;
+using bitforum.DTOs.Response;
+using bitforum.Repository;
+using bitforum.Services;
+using bitforum.Helpers;
+using bitforum.Constants;
+using bitforum.Models.Log;
+using bitforum.Extensions;
+
+namespace bitforum.Controllers.API
+{
+    [ApiController]
+    [Route("api/[controller]")]
+    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] // JWT 인증 적용
+    public class AccountController: ControllerBase
+    {
+        private readonly ILogger<AccountController> _logger;
+        private readonly DefaultDbContext _db;
+        private readonly IMemberRepository _memberRepository;
+        private readonly IMailService _mailService;
+        private readonly IConfigService _configService;
+        private ResultDto _result = new ResultDto();
+
+        public AccountController(ILogger<AccountController> logger, DefaultDbContext db, IMemberRepository memberRepository, IMailService mailService, IConfigService configService)
+        {
+            _logger = logger;
+            _db = db;
+            _memberRepository = memberRepository;
+            _mailService = mailService;
+            _configService = configService;
+        }
+
+        // 회원 조회
+        [HttpGet("Info/{email}")]
+        public async Task<ActionResult<ResultDto>> Info([FromRoute] string email)
+        {
+            try
+            {
+                if (string.IsNullOrEmpty(email))
+                {
+                    throw new Exception("이메일을 입력해주세요.");
+                }
+
+                var member = await _memberRepository.FindMemberByEmail(email);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                _result.Data = member;
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 별명 수정
+        [HttpPost("change-nickname")]
+        public async Task<ActionResult<ResultDto>> ChangeNickname([FromBody] ChangeNicknameDto request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("유효성 검사에 실패했습니다.");
+                }
+
+                // 중복 여부 확인
+                var isExist = await _memberRepository.IsExistNickname(request.Name);
+                if (isExist)
+                {
+                    throw new Exception("이미 사용중인 별명입니다.");
+                }
+
+                // 회원 조회
+                var member = await _memberRepository.FindMemberByID(request.ID);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                // 별명 변경 가능 기간 계산 (현재 날짜 - 설정된 변경 기간)
+                var changeNicknameDay = Config.Register.ChangeNicknameDay ?? 0;
+                if (changeNicknameDay > 0)
+                {
+                    // 사용자의 마지막 별명 변경 날짜가 설정된 제한일 이내인지 확인
+                    if (member.LastNameChangedAt.HasValue)
+                    {
+                        int daysSinceLastChange = (int)(DateTime.UtcNow - member.LastNameChangedAt.Value).TotalDays; // 남은 일수 계산
+                        if (daysSinceLastChange < changeNicknameDay)
+                        {
+                            throw new Exception($"별명 변경은 {changeNicknameDay - daysSinceLastChange}일 후에 가능합니다.");
+                        }
+                    }
+                }
+
+                await _db.NameChangeLog.AddAsync(new NameChangeLog
+                {
+                    MemberID = member.ID,
+                    BeforeName = member.Name,
+                    AfterName = request.Name,
+                    CreatedAt = DateTime.UtcNow
+                });
+
+                member.Name = request.Name;
+                member.LastNameChangedAt = DateTime.UtcNow;
+
+                await _db.SaveChangesAsync();
+
+                _result.Message = "별명이 변경되었습니다.";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 이메일 변경
+        [HttpPost("change-email")]
+        public async Task<ActionResult<ResultDto>> ChangeEmail([FromBody] ChangeEmailDto request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("유효성 검사에 실패했습니다.");
+                }
+
+                // 중복 여부 확인
+                var isExist = await _memberRepository.IsExistEmail(request.Email);
+                if (isExist)
+                {
+                    throw new Exception("이미 사용중인 이메일입니다.");
+                }
+
+                // 회원 조회
+                var member = await _memberRepository.FindMemberByID(request.ID);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                // 이메일 변경 가능 기간 계산 (현재 날짜 - 설정된 변경 기간)
+                var changeEmailDay = Config.Register.ChangeEmailDay ?? 0;
+                if (changeEmailDay > 0)
+                {
+                    // 사용자의 마지막 이메일 변경 날짜가 설정된 제한일 이내인지 확인
+                    if (member.LastEmailChangedAt.HasValue)
+                    {
+                        int daysSinceLastChange = (int)(DateTime.UtcNow - member.LastEmailChangedAt.Value).TotalDays; // 남은 일수 계산
+                        if (daysSinceLastChange < changeEmailDay)
+                        {
+                            throw new Exception($"별명 변경은 {changeEmailDay - daysSinceLastChange}일 후에 가능합니다.");
+                        }
+                    }
+                }
+
+                // 이메일 인증 메일 발송
+                await _mailService.SendEmailVerifyAsync(request.Email, member, new AdditionalData
+                {
+                    Email = member.Email
+                });
+
+                _result.Message = "이메일 확인을 위해 인증 메일이 전송되었습니다.";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 비밀번호 변경
+        [HttpPost("change-password")]
+        public async Task<ActionResult<ResultDto>> ChangePassword([FromBody] ChangePasswordDto request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                var memberID = User.GetID();
+
+                // 회원 확인
+                var isExist = await _memberRepository.IsExistID(memberID);
+                if (!isExist)
+                {
+                    throw new Exception("존재하지 않는 회원입니다.");
+                }
+
+                // 회원 조회
+                var member = await _memberRepository.FindMemberByID(memberID);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                // 현재 비밀번호가 맞는지 확인
+                var isValidPassword = BCrypt.Net.BCrypt.Verify(request.CurrentPassword, member.Password);
+                if (!isValidPassword)
+                {
+                    throw new Exception("현재 비밀번호가 일치하지 않습니다.");
+                }
+
+                var newPassword = BCrypt.Net.BCrypt.HashPassword(request.NewPassword);
+                if (member.Password == newPassword)
+                {
+                    throw new Exception("기존 비밀번호와 동일합니다.");
+                }
+
+                var isValidPolicy = _configService.IsPasswordPolicyValid(request.ConfirmPassword);
+                if (!isValidPolicy)
+                {
+                    string message = "";
+                    int minLengthConfig = Config.Register.PasswordMinLength ?? 6;
+                    int uppercaseLengthConfig = Config.Register.PasswordUppercaseLength ?? 0;
+                    int numbersLengthConfig = Config.Register.PasswordNumbersLength ?? 0;
+                    int specialCharsLengthConfig = Config.Register.PasswordSpecialcharsLength ?? 0;
+
+                    // 위에 데이터로 안내 문구 만들어줘
+                    if (minLengthConfig > 0)
+                    {
+                        message += $"최소 {minLengthConfig}자 이상, ";
+                    }
+                    if (uppercaseLengthConfig > 0)
+                    {
+                        message += $"대문자 {uppercaseLengthConfig}자 이상, ";
+                    }
+                    if (numbersLengthConfig > 0)
+                    {
+                        message += $"숫자 {numbersLengthConfig}자 이상, ";
+                    }
+                    if (specialCharsLengthConfig > 0)
+                    {
+                        message += $"특수문자 {specialCharsLengthConfig}자 이상, ";
+                    }
+
+                    message = message.TrimEnd(',', ' ');
+
+                    throw new Exception($"입력하신 비밀번호는 사용하실 수 없습니다. 다음 조건을 충족해주세요.\n\n{message}");
+                }
+
+                member.Password = newPassword;
+                member.PasswordUpdatedAt = DateTime.UtcNow;
+
+                await _db.SaveChangesAsync();
+                await _mailService.SendChangedPasswordEmailAsync(member);
+
+                _result.Message = "비밀번호가 변경되었습니다.";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 회원탈퇴
+        [HttpPost("withdraw")]
+        public async Task<ActionResult<ResultDto>> Withdraw()
+        {
+            try
+            {
+                var memberID = User.GetID();
+
+                // 회원 확인
+                var isExist = await _memberRepository.IsExistID(memberID);
+                if (!isExist)
+                {
+                    throw new Exception("존재하지 않는 회원입니다.");
+                }
+
+                // 회원 조회
+                var member = await _memberRepository.FindMemberByID(memberID);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                if (member.IsWithdraw)
+                {
+                    throw new Exception("이미 탈퇴한 회원입니다.");
+                }
+
+                member.IsWithdraw = true;
+                member.DeletedAt = DateTime.UtcNow;
+
+                // 탈퇴 완료 메일 발송
+                await _mailService.SendWithdrawEmailAsync(member);
+
+                // 로그인 강제 로그아웃(쿠키 삭제)
+                Response.Cookies.Delete("accessToken");
+                Response.Cookies.Delete("refreshToken");
+
+                _result.Message = "정상적으로 탈퇴되었습니다. 이용해 주셔서 감사합니다.";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 로그인 기록
+        [HttpGet("login-log")]
+        public async Task<ActionResult<ResultDto>> LoginLog([FromQuery] int page = 1, [FromQuery] string type = LoginLogType.Today)
+        {
+            try
+            {
+                var memberID = User.GetID();
+
+                // 회원 확인
+                var isExist = await _memberRepository.IsExistID(memberID);
+                if (!isExist)
+                {
+                    throw new Exception("존재하지 않는 회원입니다.");
+                }
+              
+                if (!LoginLogType.ValidTypes.Contains(type))
+                {
+                    throw new ArgumentException("유효하지 않은 타입입니다.");
+                }
+
+                DateTime now = DateTime.UtcNow;
+                DateTime startDate = now.Date;
+                DateTime endOfDate = type switch
+                {
+                    LoginLogType.Today => now.Date.AddDays(1).AddTicks(-1),
+                    LoginLogType.Week => now.AddDays(-7),
+                    LoginLogType.Month => now.AddMonths(-1),
+                    LoginLogType.HalfYear => now.AddMonths(-6),
+                    _ => DateTime.MinValue // 전체 조회
+                };
+
+                var query = _db.LoginLog.Where(c => c.MemberID == memberID && c.CreatedAt >= startDate && c.CreatedAt <= endOfDate)
+                    .Select(c => new
+                    {
+                        c.ID,
+                        c.MemberID,
+                        c.Success,
+                        c.IpAddress,
+                        c.UserAgent,
+                        CreatedAt = c.CreatedAt.ToLocalTime()
+                    })
+                    .OrderByDescending(c => c.ID)
+                    .Skip(page * 10)
+                    .Take(10);
+
+                var logs = await query.ToListAsync();
+                if (logs.Count == 0)
+                {
+                    throw new Exception("로그인 기록이 없습니다.");
+                }
+
+                _result.Data = logs;
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+    }
+}

+ 100 - 0
backend/Controllers/API/Auth/LoginController.cs

@@ -0,0 +1,100 @@
+using Microsoft.AspNetCore.Mvc;
+using bitforum.Models.Log;
+using bitforum.DTOs.Request;
+using bitforum.DTOs.Response;
+using bitforum.Helpers;
+using bitforum.Services;
+using bitforum.Repository;
+
+namespace bitforum.Controllers.API.Auth
+{
+    [ApiController]
+    [Route("api/auth")]
+    public class LoginController : ControllerBase
+    {
+        private readonly ILogger<LoginController> _logger;
+        private readonly DefaultDbContext _db;
+        private readonly IJwtAuthService _jwtAuthService;
+        private readonly IMemberRepository _memberRepository;
+        private ResultDto _result = new ResultDto();
+
+        public LoginController(ILogger<LoginController> logger, DefaultDbContext db, IJwtAuthService jwtAuthService, IMemberRepository memberRepository)
+        {
+            _logger = logger;
+            _db = db;
+            _jwtAuthService = jwtAuthService;
+            _memberRepository = memberRepository;
+        }
+
+        // 로그인
+        [HttpPost("login")]
+        public async Task<ActionResult<ResultDto>> Login([FromBody] LoginDto request)
+        {
+            var loginLog = new LoginLog();
+
+            try
+            {
+                // 유효성 검사
+                if (!ModelState.IsValid)
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                loginLog.Account = request.Email;
+                loginLog.Referer = HttpContext.Request.Headers["Referer"].ToString();
+                loginLog.Url = HttpContext.Request.Path.ToString();
+                loginLog.IpAddress = HttpContext.GetClientIP();
+                loginLog.UserAgent = Request.Headers["User-Agent"].ToString();
+
+                // 계정 확인
+                var member = await _memberRepository.FindMemberByEmail(request.Email);
+                if (member == null)
+                {
+                    loginLog.Reason = "계정을 찾을 수 없음";
+                    throw new Exception("해당 정보로 귀하의 계정을 확인할 수 없습니다.");
+                }
+
+                if (member.IsDenied)
+                {
+                    loginLog.Reason = "접속 차단되었음";
+                    throw new Exception("접속이 차단되어 로그인이 불가합니다.");
+                }
+
+                if (!BCrypt.Net.BCrypt.Verify(request.Password, member.Password))
+                {
+                    loginLog.Reason = "비밀번호 틀림";
+                    throw new Exception("해당 정보로 귀하의 계정을 확인할 수 없습니다.");
+                }
+
+                // 로그인 기록
+                loginLog.MemberID = member.ID;
+                loginLog.Success = true;
+                loginLog.Reason = "로그인 성공";
+                loginLog.CreatedAt = DateTime.UtcNow;
+
+                // 로그인 시간, IP 갱신
+                member.LastLoginIp = HttpContext.GetClientIP();
+                member.LastLoginAt = DateTime.UtcNow;
+
+                // JWT 토큰 발급
+                _jwtAuthService.SetAccessTokenAndCookieAsync(member);
+                await _jwtAuthService.SetRefreshTokenAndCookieAsync(member);
+
+                _result.Message = "로그인 성공";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            await _db.LoginLog.AddAsync(loginLog);
+            await _db.SaveChangesAsync();
+
+            return _result;
+        }
+    }
+}

+ 161 - 0
backend/Controllers/API/Auth/PasswordController.cs

@@ -0,0 +1,161 @@
+using Microsoft.AspNetCore.Mvc;
+using bitforum.DTOs.Request;
+using bitforum.DTOs.Response;
+using bitforum.Constants;
+using bitforum.Helpers;
+using bitforum.Services;
+using bitforum.Repository;
+
+namespace bitforum.Controllers.API.Auth
+{
+    [ApiController]
+    [Route("api/auth")]
+    public class PasswordController : ControllerBase
+    {
+        private readonly ILogger<PasswordController> _logger;
+        private readonly DefaultDbContext _db;
+        private readonly IConfigService _configService;
+        private readonly IMailService _mailService;
+        private readonly IMemberRepository _memberRepository;
+        private ResultDto _result = new ResultDto();
+
+        public PasswordController(ILogger<PasswordController> logger, DefaultDbContext db, IConfigService configService, IMailService mailService, IEmailVerifyNumberRepository emailVerifyNumberRepository, IMemberRepository memberRepository)
+        {
+            _logger = logger;
+            _db = db;
+            _configService = configService;
+            _mailService = mailService;
+            _memberRepository = memberRepository;
+        }
+
+        // 비밀번호 재설정 요청
+        [HttpGet("forgot-password/{email}")]
+        public async Task<ActionResult<ResultDto>> ForgotPassword([FromRoute] string email)
+        {
+            try
+            {
+                if (string.IsNullOrEmpty(email))
+                {
+                    throw new Exception("잘못된 접근입니다.");
+                }
+
+                // 이메일 거부 목록 확인
+                if (_configService.IsDeniedEmail(email))
+                {
+                    throw new Exception($"`{email}`은 조회할 수 없습니다.");
+                }
+
+                // 회원 확인
+                var member = await _memberRepository.FindMemberByEmail(email);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                // 차단 회원은 거부
+                if (member.IsDenied)
+                {
+                    throw new Exception("권한이 없습니다.");
+                }
+
+                await _mailService.SendForgotPasswordEmailAsync(member);
+
+                _result.Message = "이메일 인증 확인 후 비밀번호를 변경할 수 있습니다.";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 비밀번호 변경 처리
+        [HttpPost("reset-password")]
+        public async Task<ActionResult<ResultDto>> ResetPassword([FromBody] ResetPasswordDto request)
+        {
+            try
+            {
+                var cookieName = $"isVerified-{VerificationType.ForgotPassword}";
+                if (!Request.Cookies.ContainsKey(cookieName) || Request.Cookies[cookieName] != "true")
+                {
+                    throw new Exception("사전 인증을 먼저 수행하세요.");
+                }
+
+                // 유효성 검사
+                if (!ModelState.IsValid)
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                // 회원 확인
+                var member = await _memberRepository.FindMemberByEmail(request.Email);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                var newPassword = BCrypt.Net.BCrypt.HashPassword(request.Password);
+                if (member.Password == newPassword)
+                {
+                    throw new Exception("기존 비밀번호와 동일합니다.");
+                }
+
+                var isValid = _configService.IsPasswordPolicyValid(request.Password);
+                if (!isValid)
+                {
+                    string message = "";
+                    int minLengthConfig = Config.Register.PasswordMinLength ?? 6;
+                    int uppercaseLengthConfig = Config.Register.PasswordUppercaseLength ?? 0;
+                    int numbersLengthConfig = Config.Register.PasswordNumbersLength ?? 0;
+                    int specialCharsLengthConfig = Config.Register.PasswordSpecialcharsLength ?? 0;
+
+                    // 위에 데이터로 안내 문구 만들어줘
+                    if (minLengthConfig > 0)
+                    {
+                        message += $"최소 {minLengthConfig}자 이상, ";
+                    }
+                    if (uppercaseLengthConfig > 0)
+                    {
+                        message += $"대문자 {uppercaseLengthConfig}자 이상, ";
+                    }
+                    if (numbersLengthConfig > 0)
+                    {
+                        message += $"숫자 {numbersLengthConfig}자 이상, ";
+                    }
+                    if (specialCharsLengthConfig > 0)
+                    {
+                        message += $"특수문자 {specialCharsLengthConfig}자 이상, ";
+                    }
+
+                    message = message.TrimEnd(',', ' ');
+
+                    throw new Exception($"입력하신 비밀번호는 사용하실 수 없습니다. 다음 조건을 충족해주세요.\n\n{message}");
+                }
+
+                member.Password = newPassword;
+                member.PasswordUpdatedAt = DateTime.UtcNow;
+
+                await _db.SaveChangesAsync();
+                await _mailService.SendChangedPasswordEmailAsync(member);
+
+                // 쿠키 삭제
+                Response.Cookies.Delete(cookieName);
+
+                _result.Message = "비밀번호가 변경되었습니다.";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+            return _result;
+        }
+    }
+}

+ 175 - 0
backend/Controllers/API/Auth/RegisterController.cs

@@ -0,0 +1,175 @@
+using Microsoft.AspNetCore.Mvc;
+using bitforum.DTOs.Request;
+using bitforum.DTOs.Response;
+using bitforum.Helpers;
+using bitforum.Services;
+using bitforum.Constants;
+using bitforum.Repository;
+using bitforum.Models.Account;
+
+namespace bitforum.Controllers.API.Auth
+{
+    [ApiController]
+    [Route("api/auth")]
+    public class RegisterController : ControllerBase
+    {
+        private readonly ILogger<RegisterController> _logger;
+        private readonly DefaultDbContext _db;
+        private readonly IConfigService _configService;
+        private readonly IMailService _mailService;
+        private readonly IMemberRepository _memberRepository;
+        private ResultDto _result = new ResultDto();
+
+        public RegisterController(ILogger<RegisterController> logger, DefaultDbContext db, IConfigService configService, IMailService mailService, IMemberRepository memberRepository)
+        {
+            _logger = logger;
+            _db = db;
+            _configService = configService;
+            _mailService = mailService;
+            _memberRepository = memberRepository;
+        }
+
+        // 회원가입 요청
+        [HttpPost("register")]
+        public async Task<ActionResult<ResultDto>> Register([FromBody] RegisterDto request)
+        {
+            try
+            {
+                // 회원 가입 차단
+                if (_configService.IsRegisterBlock())
+                {
+                    throw new Exception("현재 회원가입이 일시 중단되었습니다.");
+                }
+
+                // 유효성 검사
+                if (!ModelState.IsValid)
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                // 이메일 거부 목록 확인
+                if (_configService.IsDeniedEmail(request.Email))
+                {
+                    throw new Exception($"`{request.Email}`은 사용할 수 없는 이메일입니다.");
+                }
+
+                // 이메일 중복 확인
+                if (await _memberRepository.IsExistEmail(request.Email))
+                {
+                    throw new Exception("이미 가입된 이메일입니다.");
+                }
+
+                // 비밀번호 정책 확인
+                if (!_configService.IsPasswordPolicyValid(request.Password))
+                {
+                    throw new Exception("비밀번호 구성이 적절하지 않습니다.");
+                }
+
+                bool isNew = false;
+                var member = await _memberRepository.FindNonMemberByEmail(request.Email);
+                if (member == null)
+                {
+                    member = new Models.Account.Member
+                    {
+                        Email = request.Email,
+                        CreatedAt = DateTime.UtcNow
+                    };
+
+                    isNew = true;
+                }
+
+                member.Password = BCrypt.Net.BCrypt.HashPassword(request.Password);
+                member.SignupIP = HttpContext.GetClientIP();
+                member.PasswordUpdatedAt = DateTime.UtcNow;
+
+                if (isNew)
+                {
+                    await _db.Member.AddAsync(member);
+                }
+
+                await _db.SaveChangesAsync();
+
+                if (member.ID > 0)
+                {
+                    await _db.MemberApprove.AddAsync(new MemberApprove
+                    {
+                        MemberID = member.ID
+                    });
+                }
+
+                bool isRegisterEmailAuth = _configService.IsRegisterEmailAuth();
+                if (isRegisterEmailAuth)
+                {
+                    // 회원가입 이메일 인증 메일 발송
+                    await _mailService.SendRegisterEmailAsync(member);
+                    _result.Message = "이메일 인증 후 회원가입이 완료됩니다.";
+                } 
+                else
+                {
+                    // 회원가입 완료 메일 발송
+                    await _mailService.SendRegistrationEmailAsync(member);
+                    _result.Message = "회원가입을 환영합니다.";
+                }
+
+                _result.Data = new { isRegisterEmailAuth };
+        
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 회원가입 수락
+        [HttpGet("registration/{email}")]
+        public async Task<ActionResult<ResultDto>> Registration([FromRoute] string email)
+        {
+            try
+            {
+                var cookieName = $"isVerified-{VerificationType.Registration}";
+                if (!Request.Cookies.ContainsKey(cookieName) || Request.Cookies[cookieName] != "true")
+                {
+                    throw new Exception("사전 인증을 먼저 수행하세요.");
+                }
+
+                if (string.IsNullOrEmpty(email))
+                {
+                    throw new Exception("이메일을 입력해주세요.");
+                }
+
+                // 회원 확인
+                var member = await _memberRepository.FindNonMemberByEmail(email);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                member.IsEmailVerified = true;
+                member.EmailVerifiedAt = DateTime.UtcNow;
+
+                await _db.SaveChangesAsync();
+                await _mailService.SendRegistrationEmailAsync(member);
+
+                _result.Message = "회원가입을 환영합니다."; 
+
+                // 쿠키 삭제
+                Response.Cookies.Delete(cookieName);
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+    }
+}

+ 274 - 0
backend/Controllers/API/AuthController.cs

@@ -0,0 +1,274 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using bitforum.DTOs.Request;
+using bitforum.DTOs.Response;
+using bitforum.Helpers;
+using bitforum.Services;
+using bitforum.Constants;
+using bitforum.Models.Log;
+using System.Web;
+
+namespace bitforum.Controllers.API
+{
+    [ApiController]
+    [Route("api/[controller]")]
+    public class AuthController : ControllerBase
+    {
+        private readonly ILogger<AuthController> _logger;
+        private readonly DefaultDbContext _db;
+        private readonly IJwtAuthService _jwtAuthService;
+        private readonly IMailService _mailService;
+        private ResultDto _result = new ResultDto();
+
+        public AuthController(ILogger<AuthController> logger, DefaultDbContext db, IJwtAuthService jwtAuthService, IMailService mailService)
+        {
+            _logger = logger;
+            _db = db;
+            _jwtAuthService = jwtAuthService;
+            _mailService = mailService;
+        }
+
+        // 인증번호 검증
+        [HttpPost("verification")]
+        public async Task<ActionResult<ResultDto>> Verification([FromBody] VerificationNumberDto request)
+        {
+            try
+            {
+                // 유효성 검사
+                if (!ModelState.IsValid)
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("인증 거부");
+                }
+
+                // 회원 확인
+                var member = await _db.Member.FirstOrDefaultAsync(c => c.Email == request.Email);
+                if (member is null)
+                {
+                    throw new Exception("인증 불가");
+                }
+
+                // 인증번호 확인
+                var isValid = await _mailService.VerifyCodeAsync(request.Email, request.Code, request.Type);
+                if (!isValid)
+                {
+                    throw new Exception("인증 실패");
+                }
+
+                var cookie = new CookieOptions
+                {
+                    HttpOnly = true,
+                    Secure = true,
+                    SameSite = SameSiteMode.None
+                };
+
+                switch (request.Type)
+                {
+                    case VerificationType.Registration:
+                        cookie.Expires = DateTime.UtcNow.AddMinutes(5);
+                        break;
+
+                    case VerificationType.ForgotPassword:
+                        cookie.Expires = DateTime.UtcNow.AddMinutes(10);
+                        break;
+                }
+
+                Response.Cookies.Append($"isVerified-{request.Type}", "true", cookie);
+                Console.WriteLine("Set-Cookie Header:", Response.Headers["Set-Cookie"]);
+
+                _result.Message = "인증 성공";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 인증번호 다시 보내기
+        [HttpPost("resend-verify")]
+        public async Task<ActionResult<ResultDto>> ResendVerify([FromBody] ResendVerifyNumberDto request)
+        {
+            try
+            {
+                // 유효성 검사
+                if (!ModelState.IsValid)
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("잘못된 접근입니다.");
+                }
+
+                // 회원 확인
+                var member = await _db.Member.FirstOrDefaultAsync(c => c.Email == request.Email);
+                if (member is null)
+                {
+                    throw new Exception("회원을 조회할 수 없습니다.");
+                }
+
+                switch (request.Type)
+                {
+                    // 회원가입 이메일 인증 메일 발송
+                    case VerificationType.Registration:
+                        await _mailService.SendRegisterEmailAsync(member);
+                        break;
+
+                    // 비밀번호 재설정 이메일 발송
+                    case VerificationType.ForgotPassword:
+                        await _mailService.SendForgotPasswordEmailAsync(member);
+                        break;
+                }
+             
+                _result.Message = "인증 재전송 완료";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        [HttpGet("verify-token")]
+        public ActionResult<ResultDto> VerifyAccessToken()
+        {
+            try
+            {
+                if (!Request.Cookies.TryGetValue("accessToken", out var accessToken))
+                {
+                    throw new Exception("잘못된 접근입니다.");
+                }
+
+                var isValid = _jwtAuthService.ValidateAccessToken(accessToken);
+                if (!isValid)
+                {
+                    throw new Exception("`AccessToken`이 유효하지 않습니다.");
+                }
+
+                var member = _jwtAuthService.GetMemberFromAccessToken(accessToken);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                _logger.LogInformation("AccessToken 유효!");
+                _result.Message = "OK";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 재로그인 토큰 재발급
+        [HttpGet("refresh-token")]
+        public async Task<ActionResult<ResultDto>> RefreshToken()
+        {
+            try
+            {
+                if (!Request.Cookies.TryGetValue("refreshToken", out var refreshToken))
+                {
+                    throw new Exception("잘못된 접근입니다.");
+                }
+
+                var isValid = await _jwtAuthService.ValidateRefreshToken(refreshToken);
+                if (!isValid)
+                {
+                    throw new Exception("확인할 수 없는 요청입니다.");
+                }
+
+                var member = await _jwtAuthService.GetMemberFromRefreshToken(refreshToken);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수  없습니다.");
+                }
+
+                // JWT 토큰 발급
+                await _jwtAuthService.SetRefreshTokenAndCookieAsync(member);
+
+                _logger.LogInformation("RefreshToken 유효!");
+                _result.Message = "OK";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        [HttpGet("verify-email/{token}")]
+        public async Task<ActionResult<ResultDto>> VerifyEmail([FromRoute] string token)
+        {
+            try
+            {
+                if (string.IsNullOrEmpty(token))
+                {
+                    _result.Errors = ModelState.GetErrors();
+                    throw new Exception("유효성 검사에 실패했습니다.");
+                }
+
+                token = HttpUtility.UrlDecode(token);
+
+                // 인증주소 확인
+                var tokenData = await _mailService.VerifyTokenAsync(token, VerificationType.ChangedEmail);
+                if (tokenData is null)
+                {
+                    throw new Exception("인증 실패");
+                }
+
+                if (string.IsNullOrEmpty(tokenData?.AdditionalData?.Email))
+                {
+                    throw new Exception("최소 필수 정보가 누락되었습니다.");
+                }
+
+                // 회원 조회
+                var member = await _db.Member.FirstOrDefaultAsync(c => c.Email == tokenData.AdditionalData.Email);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                await _db.EmailChangeLog.AddAsync(new EmailChangeLog
+                {
+                    MemberID = member.ID,
+                    BeforeEmail = member.Email,
+                    AfterEmail = tokenData.Email,
+                    CreatedAt = DateTime.UtcNow
+                });
+
+                member.Email = tokenData.Email;
+                member.LastEmailChangedAt = DateTime.UtcNow;
+
+                await _db.SaveChangesAsync();
+
+                await _mailService.SendChangedEmailAsync(member); // 이메일 변경 완료 메일 발송
+
+                _result.Message = "이메일이 변경되었습니다.";
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+    }
+}

+ 286 - 0
backend/Controllers/API/SystemController.cs

@@ -0,0 +1,286 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using bitforum.DTOs.Response;
+using bitforum.Repository;
+using bitforum.Constants;
+
+namespace bitforum.Controllers.API
+{
+    [ApiController]
+    [Route("api/[controller]")]
+    public class SystemController : ControllerBase
+    {
+        private readonly ILogger<SystemController> _logger;
+        private readonly IRedisRepository _redisRepository;
+        private readonly IConfigRepository _configRepository;
+        private readonly DefaultDbContext _db;
+        private ResultDto _result = new ResultDto();
+
+        public SystemController(ILogger<SystemController> logger, IRedisRepository redisRepository, IConfigRepository configRepository, DefaultDbContext db)
+        {
+            _logger = logger;
+            _redisRepository = redisRepository;
+            _configRepository = configRepository;
+            _db = db;
+        }
+
+        // Config 값 JSON으로 반환
+        [HttpGet("configs")]
+        public async Task<ResultDto> Configs()
+        {
+            try
+            {
+                var config = await _redisRepository.GetObjectAsync<Dictionary<string, string>>(RedisConst.ConfigKey);
+                if (config is null)
+                {
+                    config = _configRepository.GetAll();
+                    await _redisRepository.SetObjectAsync(RedisConst.ConfigKey, config, RedisConst.CacheExpiration);
+                }
+
+                _result.Data = config;
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 문서 조회
+        [HttpGet("docs/{code}")]
+        public async Task<ResultDto> Docs([FromRoute] string code)
+        {
+            try
+            {
+                string cacheKey = $"{RedisConst.DocumentKey}-{code}";
+
+                var document = await _redisRepository.GetObjectAsync<object>(cacheKey);
+                if (document is null)
+                {
+                    document = await _db.Document.Where(c => c.Code == code && c.IsActive)
+                        .Select(c => new
+                        {
+                            c.ID,
+                            c.Code,
+                            c.Subject,
+                            c.Content
+                        })
+                        .FirstOrDefaultAsync();
+
+                    await _redisRepository.SetObjectAsync(cacheKey, document, RedisConst.CacheExpiration);
+                }
+
+                if (document is null)
+                {
+                    throw new Exception("문서 정보가 존재하지 않습니다.");
+                }
+
+                _result.Data = document;
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // FAQ 분류 목록
+        [HttpGet("faq/categories")]
+        public async Task<ResultDto> FaqCategories()
+        {
+            try
+            {
+                string cacheKey = "FaqCategories";
+
+                var faqCategories = await _redisRepository.GetObjectAsync<object>(cacheKey);
+                if (faqCategories is null)
+                {
+                    faqCategories = await _db.FaqCategory.Where(c => c.IsActive)
+                        .OrderBy(c => c.Order)
+                        .Select(c => new
+                        {
+                            c.ID,
+                            c.Code,
+                            c.Subject
+                        }).ToListAsync();
+
+                    await _redisRepository.SetObjectAsync(cacheKey, faqCategories, RedisConst.CacheExpiration);
+                }
+
+                if (faqCategories is null)
+                {
+                    throw new Exception("FAQ 분류 목록이 존재하지 않습니다.");
+                }
+
+                _result.Data = faqCategories;
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // FAQ 분류별 자주 묻는 질문 목록
+        [HttpGet("faq/{code}")]
+        public async Task<ResultDto> Faq([FromRoute] string code, string? search = null)
+        {
+            try
+            {
+                string cacheKey = $"{RedisConst.FaqKey}-{code}";
+
+                await _redisRepository.DeleteAsync(cacheKey);
+                var faqItems = await _redisRepository.GetObjectAsync<List<object>>(cacheKey);
+                if (faqItems == null)
+                {
+                    var query = _db.FaqItem
+                        .Include(f => f.FaqCategory)
+                        .Where(f => f.FaqCategory.Code == code && f.FaqCategory.IsActive && f.IsActive);
+
+                    if (!string.IsNullOrEmpty(search))
+                    {
+                        query = query.Where(f => f.Question.Contains(search) || f.Answer.Contains(search));
+                    }
+
+                    faqItems = await query
+                        .OrderBy(f => f.Order)
+                        .Select(f => (object)new
+                        {
+                            f.ID,
+                            f.Question,
+                            f.Answer,
+                            f.Order,
+                            f.IsActive,
+                            CategoryCode = f.FaqCategory.Code,
+                            CategorySubject = f.FaqCategory.Subject
+                        })
+                        .ToListAsync();
+
+                    await _redisRepository.SetObjectAsync(cacheKey, faqItems, RedisConst.CacheExpiration);
+                }
+
+                if (faqItems == null || !faqItems.Any())
+                {
+                    throw new Exception("해당하는 FAQ 목록이 존재하지 않습니다.");
+                }
+
+                _result.Data = faqItems;
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        // 팝업 조회
+        [HttpGet("popup")]
+        public async Task<ResultDto> Popup()
+        {
+            try
+            {
+                string cacheKey = RedisConst.PopupKey;
+
+                var popups = await _redisRepository.GetObjectAsync<List<object>>(cacheKey);
+                if (popups is null)
+                {
+                    var now = DateTime.UtcNow;
+                    popups = await _db.Popup.Where(c => c.IsActive && (c.StartAt == null || c.StartAt <= now) && (c.EndAt == null || c.EndAt.Value.Date >= now.Date))
+                        .OrderBy(c => c.Order)
+                        .Select(c => (object)new
+                        {
+                            c.ID,
+                            c.Subject,
+                            c.Content,
+                            c.Link
+                        })
+                        .ToListAsync();
+
+                    await _redisRepository.SetObjectAsync(cacheKey, popups, RedisConst.CacheExpiration);
+                }
+
+                if (popups is null)
+                {
+                    throw new Exception("팝업이 존재하지 않습니다.");
+                }
+
+                _result.Data = popups;
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+
+        [HttpGet("banner/{code}")]
+        public async Task<ResultDto> Banner([FromRoute] string code)
+        {
+            try
+            {
+                string cacheKey = RedisConst.BannerKey;
+
+                var banners = await _redisRepository.GetObjectAsync<List<object>>(cacheKey);
+                if (banners == null)
+                {
+                    var now = DateTime.UtcNow;
+                    banners = await _db.BannerItem
+                        .Include(c => c.BannerPosition)
+                        .Where(c => c.BannerPosition.Code == code && c.BannerPosition.IsActive && (c.StartAt == null || c.StartAt <= now) && (c.EndAt == null || c.EndAt.Value.Date >= now.Date))
+                        .Where(c => c.IsActive)
+                        .OrderBy(c => c.Order)
+                        .Select(c => (object)new
+                        {
+                            c.ID,
+                            c.Subject,
+                            c.Image,
+                            c.Width,
+                            c.Height,
+                            c.Link,
+                            PositionID = c.BannerPosition.Code,
+                            PositionName = c.BannerPosition.Subject
+                        })
+                        .ToListAsync();
+
+                    await _redisRepository.SetObjectAsync(cacheKey, banners, RedisConst.CacheExpiration);
+                }
+
+                if (banners == null || !banners.Any())
+                {
+                    throw new Exception("해당하는 배너가 존재하지 않습니다.");
+                }
+
+                _result.Data = banners;
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                _result.Ok = false;
+                _result.Status = StatusCodes.Status400BadRequest;
+                _result.Message = e.Message;
+            }
+
+            return _result;
+        }
+    }
+}

+ 133 - 0
backend/Controllers/BBS/Board/GroupController.cs

@@ -0,0 +1,133 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using bitforum.Models;
+using bitforum.Models.BBS;
+
+namespace bitforum.Controllers.BBS.Board
+{
+    [Authorize]
+    [Route("BBS/Board")]
+    public class GroupController : Controller
+    {
+        private readonly ILogger<GroupController> _logger;
+        private readonly DefaultDbContext _db;
+        private readonly string _ViewPath = "~/Views/BBS/Board/Group.cshtml";
+
+        public GroupController(ILogger<GroupController> logger, DefaultDbContext db)
+        {
+            _logger = logger;
+            _db = db;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Group")]
+        public IActionResult Index()
+        {
+            ViewBag.BoardGroups = _db.BoardGroup.OrderBy(c => c.Order).ToList();
+            ViewBag.Total = ViewBag.BoardGroups?.Count ?? 0;
+
+            return View(_ViewPath);
+        }
+
+        [HttpPost("Group")]
+        public async Task<IActionResult> Save([FromForm] List<BoardGroup> request)
+        {
+            using var transaction = await _db.Database.BeginTransactionAsync();
+
+            try
+            {
+                if (request == null || !request.Any())
+                {
+                    // 전체 삭제
+                    var boardGroups = await _db.BoardGroup.ToListAsync();
+                    if (boardGroups.Any())
+                    {
+                        _db.BoardGroup.RemoveRange(boardGroups);
+                        await _db.SaveChangesAsync();
+                    }
+
+                    await transaction.CommitAsync();
+                    return Index();
+                }
+
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+                
+                var requestIDs = request.Select(x => x.ID).ToList(); // 요청 데이터의 ID 목록
+                var existingIDs = await _db.BoardGroup.Select(c => c.ID).ToListAsync(); // 데이터베이스에 존재하는 ID 목록
+                var IDsToDelete = existingIDs.Except(requestIDs).ToList(); // 삭제 대상 ID: 요청 데이터에 없는 항목
+
+                // 삭제 대상 항목 제거
+                if (IDsToDelete.Any())
+                {
+                    _db.BoardGroup.RemoveRange(
+                        await _db.BoardGroup.Where(c => IDsToDelete.Contains(c.ID) && !c.Board.Any()).ToListAsync()
+                    );
+                }
+
+                foreach (var row in request)
+                {
+                    // 중복 확인
+                    if (await _db.BoardGroup.AnyAsync(c => c.Code == row.Code && c.ID != row.ID))
+                    {
+                        throw new Exception($"{row.Code} `Code`는 이미 존재합니다.");
+                    }
+
+                    if (row.ID == 0)
+                    {
+                        row.CreatedAt = DateTime.UtcNow;
+
+                        await _db.BoardGroup.AddAsync(row);
+                    }
+                    else
+                    {
+                        var boardGroup = await _db.BoardGroup.FindAsync(row.ID);
+                        if (boardGroup == null)
+                        {
+                            throw new Exception($"ID {row.ID}에 해당하는 정보가 없습니다.");
+                        }
+
+                        boardGroup.Code = row.Code;
+                        boardGroup.Name = row.Name;
+                        boardGroup.Order = row.Order;
+                        boardGroup.UpdatedAt = DateTime.UtcNow;
+
+                        _db.BoardGroup.Update(boardGroup);
+                    }
+                }
+
+                await transaction.CommitAsync();
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("저장 중 오류가 발생했습니다.");
+                }
+
+                string message = "게시판 분류가 저장되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+
+                return RedirectToAction("Index");
+            }
+            catch (Exception e)
+            {
+                await transaction.RollbackAsync();
+
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+
+                return Index();
+            }
+        }
+    }
+}

+ 3 - 2
backend/Controllers/Director/RoleController.cs

@@ -1,11 +1,12 @@
 using System.Diagnostics;
 using System.Security.Claims;
-using bitforum.Models;
-using bitforum.Models.Views;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Identity;
 using Microsoft.EntityFrameworkCore;
+using bitforum.Models;
+using bitforum.Models.Views;
+using bitforum.Constants;
 
 namespace bitforum.Controllers.Director
 {

+ 1 - 1
backend/Controllers/Director/UserController.cs

@@ -82,7 +82,7 @@ namespace bitforum.Controllers.Director
 
             var viewModel = new UserViewModel{
                 ID = user.Id,
-                Name = user.UserName,
+                Name = user.FullName,
                 Email = user.Email,
                 Phone = user.PhoneNumber
             };

+ 241 - 0
backend/Controllers/Member/GradeController.cs

@@ -0,0 +1,241 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using bitforum.Helpers;
+using bitforum.Models;
+using bitforum.Models.Account;
+using bitforum.Services;
+
+namespace bitforum.Controllers.Member
+{
+    [Authorize]
+    [Route("Member")]
+    public class GradeController : Controller
+    {
+        private readonly ILogger<GradeController> _logger;
+        private readonly IFileUploadService _fileUploadService;
+        private readonly DefaultDbContext _db;
+        private readonly string _IndexViewPath = "~/Views/Member/Grade/Index.cshtml";
+        private readonly string _WriteViewPath = "~/Views/Member/Grade/Write.cshtml";
+        private readonly string _EditViewPath = "~/Views/Member/Grade/Edit.cshtml";
+
+        public GradeController(ILogger<GradeController> logger, IFileUploadService fileUploadService, DefaultDbContext db)
+        {
+            _logger = logger;
+            _fileUploadService = fileUploadService;
+            _db = db;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Grade")]
+        public IActionResult Index()
+        {
+            var memberGrades = _db.MemberGrade.OrderByDescending(c => c.Order).ToList();
+            var data = new List<object>();
+
+            if (memberGrades.Count > 0)
+            {
+                foreach (var row in memberGrades)
+                {
+                    data.Add(new
+                    {
+                        row.ID,
+                        row.Image,
+                        Name = $"{row.KorName}({row.EngName})",
+                        row.Order,
+                        RequiredExp = row.RequiredExp.ToString("N0"),
+                        RequiredCoin = row.RequiredCoin.ToString("N0"),
+                        IsActive = (row.IsActive ? 'Y' : 'N'),
+                        UpdatedAt = row.UpdatedAt.GetDateAt(),
+                        CreatedAt = row.CreatedAt.GetDateAt(),
+                        MemberRows = _db.Member.Count(c => c.GradeID == row.ID),
+                        EditURL = $"/Member/Grade/{row.ID}/Edit",
+                        DeleteURL = $"/Member/Grade/{row.ID}/Delte"
+                    });
+                }
+            }
+
+            ViewBag.Data = data;
+            ViewBag.Total = (data?.Count ?? 0);
+
+            return View(_IndexViewPath);
+        }
+
+        [HttpGet("Grade/Write")]
+        public IActionResult Write()
+        {
+            return View(_WriteViewPath);
+        }
+
+        [HttpPost("Grade/Create")]
+        public async Task<IActionResult> Create(MemberGrade request, IFormFile? Image)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                request.Image = await _fileUploadService.UploadImageAsync(Image, UploadFolder.Grade);
+                request.UpdatedAt = null;
+                request.CreatedAt = DateTime.UtcNow;
+
+                _db.MemberGrade.Add(request);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("회원등급 등록 중 오류가 발생했습니다.");
+                }
+
+                string message = "회원등급이 등록되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return Redirect("/Member/Grade");
+            }
+            catch (ArgumentException e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message); 
+                return Write();
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return Write();
+            }
+        }
+
+        [HttpGet("Grade/{id}/Edit")]
+        public async Task<IActionResult> Edit(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var memberGrade = await _db.MemberGrade.FirstAsync(c => c.ID == id);
+                if (memberGrade is null)
+                {
+                    throw new Exception("회원등급 정보를 찾을 수 없습니다.");
+                }
+
+                return View(_EditViewPath, memberGrade);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return Redirect("/Member/Grade");
+            }
+        }
+
+        [HttpPost("Grade/Update")]
+        public async Task<IActionResult> Update(MemberGrade request, IFormFile? Image, [FromForm] bool IsImageRemove = false)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                var memberGrade = await _db.MemberGrade.FirstAsync(c => c.ID == request.ID);
+                if (memberGrade is null)
+                {
+                    throw new Exception("회원등급 정보를 찾을 수 없습니다.");
+                }
+
+                // 이미지 저장
+                if (IsImageRemove)
+                {
+                    // 실제 파일 삭제
+                    _fileUploadService.RemoveFile(memberGrade.Image);
+                    memberGrade.Image = null;
+                }
+                else if (Image is not null)
+                {
+                    memberGrade.Image = await _fileUploadService.UploadImageAsync(Image, UploadFolder.Grade);
+                }
+
+                memberGrade.KorName = request.KorName;
+                memberGrade.EngName = request.EngName;
+                memberGrade.Description = request.Description;
+                memberGrade.Order = request.Order;
+                memberGrade.RequiredExp = request.RequiredExp;
+                memberGrade.RequiredCoin = request.RequiredCoin;
+                memberGrade.IsActive = request.IsActive;
+                memberGrade.UpdatedAt = DateTime.UtcNow;
+
+                _db.MemberGrade.Update(memberGrade);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("회원등급 수정 중 오류가 발생했습니다.");
+                }
+
+                string message = "회원등급이 수정되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return Redirect($"/Member/Grade/{request.ID}/Edit");
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return await Edit(request.ID);
+            }
+        }
+
+        [HttpGet("Grade/{id}/Delete")]
+        public async Task<IActionResult> Delete(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var memberGrade = await _db.MemberGrade.FindAsync(id);
+                if (memberGrade == null)
+                {
+                    throw new Exception("회원등급 정보를 찾을 수 없습니다.");
+                }
+
+                _fileUploadService.RemoveFile(memberGrade.Image);
+                _db.MemberGrade.Remove(memberGrade);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("회원등급 삭제 중 오류가 발생했습니다.");
+                }
+
+                _fileUploadService.RemoveFile(memberGrade.Image);
+
+                string message = "회원등급이 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect("/Member/Grade");
+        }
+    }
+}

+ 596 - 0
backend/Controllers/Member/ListController.cs

@@ -0,0 +1,596 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.EntityFrameworkCore;
+using bitforum.Helpers;
+using bitforum.Models;
+using bitforum.Models.Account;
+using bitforum.Services;
+using bitforum.Constants;
+
+namespace bitforum.Controllers.Member
+{
+    [Authorize]
+    [Route("Member")]
+    public class ListController : Controller
+    {
+        private readonly ILogger<ListController> _logger;
+        private readonly IFileUploadService _fileUploadService;
+        private readonly DefaultDbContext _db;
+        private readonly string _IndexViewPath = "~/Views/Member/List/Index.cshtml";
+        private readonly string _WriteViewPath = "~/Views/Member/List/Write.cshtml";
+        private readonly string _EditViewPath = "~/Views/Member/List/Edit.cshtml";
+        private string? _queryString = null;
+
+        public ListController(ILogger<ListController> logger, IFileUploadService fileUploadService, DefaultDbContext db)
+        {
+            _logger = logger;
+            _fileUploadService = fileUploadService;
+            _db = db;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        public override void OnActionExecuting(ActionExecutingContext context)
+        {
+            ViewBag.QueryString = _queryString = string.Join("&", QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).Select(r => $"{r.Key}={r.Value.FirstOrDefault()}"));
+
+            base.OnActionExecuting(context);
+        }
+
+        [HttpGet("List")]
+        public async Task<IActionResult> Index(
+            [FromQuery] 
+            int page = 1, int perPage = 10, byte? search = null, string? keyword = null, string startAt = null!, string endAt = null!, sbyte tab = 0, sbyte isEmailVerified = 0, sbyte isAuthCertified = 0, Gender? gender = null
+        ) {
+            var query = _db.Member.AsQueryable();
+
+            // 검색 조건 적용
+            if (!string.IsNullOrEmpty(keyword) && search.HasValue)
+            {
+                switch (search)
+                {
+                    case 1: // ID 검색
+                        if (int.TryParse(keyword, out int memberID))
+                        {
+                            query = query.Where(m => m.ID == memberID);
+                        }
+                        else
+                        {
+                            query = query.Where(m => m.ID == 0);
+                        }
+                        break;
+                    case 2: // 이메일 검색
+                        query = query.Where(m => m.Email.Contains(keyword));
+                        break;
+                    case 3: // 별명 검색
+                        query = query.Where(m => m.Name != null && m.Name.Contains(keyword));
+                        break;
+                    case 4: // 이름 검색
+                        query = query.Where(m => m.FullName != null && m.FullName.Contains(keyword));
+                        break;
+                    case 5: // 연락처 검색
+                        query = query.Where(m => m.Phone != null && m.Phone.Contains(keyword));
+                        break;
+                }
+            }
+
+            if (tab > 0)
+            {
+                switch (tab)
+                {
+                    case 1: // 차단
+                        query = query.Where(m => m.IsDenied);
+                        break;
+                    case 2: // 탈퇴
+                        query = query.Where(m => m.IsWithdraw);
+                        break;
+                    case 3: // 관리자
+                        query = query.Where(m => m.IsAdmin);
+                        break;
+                }
+            }
+
+            if (gender is not null) // 성별
+            {
+                query = query.Where(m => m.Gender == gender);
+            }
+
+            if (isEmailVerified > 0) // 이메일 인증 여부
+            {
+                query = query.Where(m => m.IsEmailVerified == true);
+            }
+
+            if (isAuthCertified > 0) // 본인 인증 여부
+            {
+                query = query.Where(m => m.IsAuthCertified == true);
+            }
+
+            // 가입 일시
+            var today = DateTime.UtcNow.Date;
+            if (startAt != null && DateTime.TryParse(startAt, out var st))
+            {
+                query = query.Where(log => log.CreatedAt >= st);
+            }
+            if (endAt != null && DateTime.TryParse(endAt, out var et))
+            {
+                query = query.Where(log => log.CreatedAt <= et);
+            }
+
+            query = query.OrderByDescending(c => c.CreatedAt);
+
+            var members = await query
+                .Skip((page - 1) * perPage)
+                .Take(perPage)
+                .Select(c => new
+                {
+                    GradeName = _db.MemberGrade.Where(m => m.ID == c.GradeID).Select(m => m.KorName).FirstOrDefault(), // 회원등급 이름
+                    c.ID,
+                    c.SID,
+                    c.Email,
+                    c.Name,
+                    c.FullName,
+                    c.Icon,
+                    c.Coin,
+                    c.Exp,
+                    c.Phone,
+                    c.Gender,
+                    c.Birthday,
+                    c.Following,
+                    c.Followed,
+                    c.IsEmailVerified,
+                    c.IsAuthCertified,
+                    c.IsDenied,
+                    c.IsAdmin,
+                    c.IsWithdraw,
+                    c.LastLoginIp,
+                    c.LastLoginAt,
+                    c.CreatedAt,
+                    c.UpdatedAt
+                })
+                .ToListAsync();
+
+
+            var data = new List<object>();
+
+            if (members.Count > 0)
+            {
+                foreach (var row in members)
+                {
+                    data.Add(new
+                    {
+                        row.GradeName,
+                        row.ID,
+                        row.Email,
+                        row.Name,
+                        row.FullName,
+                        row.Icon,
+                        Phone = row?.Phone ?? "-",
+                        Gender = row.Gender.HasValue ? row.Gender.ToString() : "-",
+                        Birthday = row.Birthday?.ToString("yyyy.MM.dd"),
+                        Coin = row.Coin.ToString("N0"),
+                        Exp = row.Exp.ToString("N0"),
+                        Following = row.Following.ToString("N0"),
+                        Followed = row.Followed.ToString("N0"),
+                        row.IsEmailVerified,
+                        row.IsAuthCertified,
+                        row.IsDenied,
+                        row.IsAdmin,
+                        row.IsWithdraw,
+                        row.LastLoginIp,
+                        LastLoginAt = row.LastLoginAt?.ToString("yyyy.MM.dd"),
+                        CreatedAt = row.CreatedAt.ToString("yyyy.MM.dd"),
+                        UpdatedAt = row.UpdatedAt?.ToString("yyyy.MM.dd"),
+                        ApproveURL = $"/Member/List/{row.ID}/Approve?{_queryString}",
+                        EditURL = $"/Member/List/{row.ID}/Edit?{_queryString}",
+                        DeleteURL = $"/Member/List/{row.ID}/Delete?{_queryString}"
+                    });
+                }
+            }
+
+            var parameter = new
+            {
+                Page = page,
+                PerPage = perPage,
+                Search = search,
+                Keyword = keyword,
+                StartAt = startAt,
+                EndAt = endAt,
+                Tab = tab,
+                IsEmailVerified = isEmailVerified,
+                IsAuthCertified = isAuthCertified,
+                Gender = gender,
+            };
+
+            ViewBag.Data = data;
+            ViewBag.Total = await query.CountAsync();
+            ViewBag.Parameter = parameter;
+            ViewBag.Pagination = new Pagination(ViewBag.Total, page, perPage, parameter);
+
+            return View(_IndexViewPath);
+        }
+
+        [HttpGet("List/Write")]
+        public IActionResult Write()
+        {
+            ViewBag.MemberGrades = new SelectList(
+                _db.MemberGrade.Where(c => c.IsActive).OrderByDescending(c => c.Order),
+                "ID",
+                "KorName",
+                null
+            );
+
+            return View(_WriteViewPath);
+        }
+
+        [HttpPost("List/Create")]
+        public async Task<IActionResult> Create([FromForm] Models.Account.Member request, IFormFile? photo, IFormFile? icon)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                if (request.FirstName != null || request.LastName != null)
+                {
+                    request.FullName = $"{request.FirstName}{request.LastName}";
+                }
+
+                // 이메일 중복 확인
+                if (await _db.Member.AnyAsync(c => c.Email == request.Email))
+                {
+                    throw new ArgumentException("이미 등록된 이메일 주소입니다.");
+                }
+
+                // 별명 중복 확인
+                if (await _db.Member.AnyAsync(c => c.Name == request.Name))
+                {
+                    throw new ArgumentException("이미 등록된 별명입니다.");
+                }
+          
+                request.Password = BCrypt.Net.BCrypt.HashPassword(request.Password);
+                request.PasswordUpdatedAt = DateTime.UtcNow;
+                request.SignupIP = HttpContext.GetClientIP();
+                request.CreatedAt = DateTime.UtcNow;
+
+                await _db.Member.AddAsync(request);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("회원 등록 중 오류가 발생했습니다.");
+                }
+
+                request.Photo = await _fileUploadService.UploadImageAsync(photo, UploadFolder.Member, request.ID);
+                request.Icon = await _fileUploadService.UploadImageAsync(icon, UploadFolder.Member, request.ID);
+
+                request.EmailVerifiedAt = (request.IsEmailVerified ? DateTime.UtcNow : null);
+                request.AuthCertifiedAt = (request.IsAuthCertified ? DateTime.UtcNow : null);
+                request.DeletedAt = (request.IsWithdraw ? DateTime.UtcNow : null);
+
+                // 회원의 약관 및 알림 동의 정보 생성
+                await _db.MemberApprove.AddAsync(new MemberApprove
+                {
+                    MemberID = request.ID,
+                    IsReceiveSMS = false,
+                    IsReceiveEmail = false,
+                    IsReceiveNote = false,
+                    IsDisclosureInvest = false
+                });
+
+                await _db.SaveChangesAsync();
+
+                string message = "회원이 등록되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return Redirect("/Member/List");
+            }
+            catch (ArgumentException e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return Write();
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return Write();
+            }
+        }
+
+        [HttpGet("List/{id}/Edit")]
+        public async Task<IActionResult> Edit(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var member = await _db.Member.Where(m => m.ID == id).FirstOrDefaultAsync();
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                ViewBag.MemberGrades = new SelectList(
+                    _db.MemberGrade.Where(c => c.IsActive).OrderByDescending(c => c.Order),
+                    "ID",
+                    "KorName",
+                    member.GradeID
+                );
+
+                return View(_EditViewPath, member);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return Redirect("/Member/List");
+            }
+        }
+
+        [HttpPost("List/Update")]
+        public async Task<IActionResult> Update([FromForm] Models.Account.Member request, IFormFile? photo, IFormFile? icon, [FromForm] bool IsPhotoRemove = false, bool IsIconRemove = false)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                var member = await _db.Member.FirstOrDefaultAsync(c => c.ID == request.ID);
+                if (member is null)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                if (request.FirstName != null || request.LastName != null)
+                {
+                    member.FullName = $"{request.FirstName}{request.LastName}";
+                }
+
+                // 이메일 중복 확인
+                if (await _db.Member.AnyAsync(c => c.Email == request.Email && c.ID != member.ID))
+                {
+                    throw new ArgumentException("이미 등록된 이메일 주소입니다.");
+                }
+
+                // 별명 중복 확인
+                if (await _db.Member.AnyAsync(c => c.Name == request.Name && c.ID != member.ID))
+                {
+                    throw new ArgumentException("이미 등록된 별명입니다.");
+                }
+
+                // 비밀번호 변경
+                if (!string.IsNullOrEmpty(request.Password))
+                {
+                    member.Password = BCrypt.Net.BCrypt.HashPassword(request.Password);
+                    member.PasswordUpdatedAt = DateTime.UtcNow;
+                }
+
+                // 사진 저장
+                if (IsPhotoRemove)
+                {
+                    _fileUploadService.RemoveFile(request.Photo);
+                    member.Photo = null;
+                }
+                else if (photo is not null)
+                {
+                    member.Photo = await _fileUploadService.UploadImageAsync(photo, UploadFolder.Grade, member.ID);
+                }
+
+                // 아이콘 저장
+                if (IsIconRemove)
+                {
+                    _fileUploadService.RemoveFile(request.Photo);
+                    member.Icon = null;
+                }
+                else if (icon is not null)
+                {
+                    member.Icon = await _fileUploadService.UploadImageAsync(icon, UploadFolder.Grade, member.ID);
+                }
+
+                member.GradeID = request.GradeID;
+                member.Email = request.Email;
+                member.LastEmailChangedAt = (member.Email != request.Email ? DateTime.UtcNow : null);
+                member.Name = request.Name;
+                member.LastNameChangedAt = (member.Name != request.Name ? DateTime.UtcNow : null);
+                member.FirstName = request.FirstName;
+                member.LastName = request.LastName;
+                member.Intro = request.Intro;
+                member.Summary = request.Summary;
+                member.Phone = request.Phone;
+                member.Birthday = request.Birthday;
+                member.Gender = request.Gender;
+                member.EmailVerifiedAt = (request.IsEmailVerified && !member.IsEmailVerified && member.IsEmailVerified != request.IsEmailVerified ? DateTime.UtcNow : (request.IsEmailVerified ? member.EmailVerifiedAt : null));
+                member.IsEmailVerified = request.IsEmailVerified;
+                member.AuthCertifiedAt = (request.IsAuthCertified && !member.IsAuthCertified && member.IsAuthCertified != request.IsAuthCertified ? DateTime.UtcNow : (request.IsAuthCertified ? member.AuthCertifiedAt : null));
+                member.IsAuthCertified = request.IsAuthCertified;
+                member.IsDenied = request.IsDenied;
+                member.IsAdmin = request.IsAdmin;
+                member.DeletedAt = (request.IsWithdraw && !member.IsWithdraw && member.IsWithdraw != request.IsWithdraw  ? DateTime.UtcNow : (request.IsWithdraw ? member.DeletedAt : null));
+                member.IsWithdraw = request.IsWithdraw;
+                member.UpdatedAt = DateTime.UtcNow;
+
+                _db.Member.Update(member);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("회원 수정 중 오류가 발생했습니다.");
+                }
+
+                string message = "회원 정보가 수정되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return Redirect($"/Member/List/{request.ID}/Edit?{_queryString}");
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return await Edit(request.ID);
+            }
+        }
+
+        [HttpGet("List/{id}/Delete")]
+        public async Task<IActionResult> Delete(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var isMember = await _db.Member.AnyAsync(c => c.ID == id);
+                if (!isMember)
+                {
+                    throw new Exception("회원 정보를 찾을 수 없습니다.");
+                }
+
+                var member = await _db.Member.FindAsync(id);
+
+                _db.Member.Remove(member);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("회원 삭제 중 오류가 발생했습니다.");
+                }
+
+                _fileUploadService.RemoveFile(member.Photo);
+                _fileUploadService.RemoveFile(member.Icon);
+
+                string message = "회원이 정상적으로 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect($"/Member/List?{_queryString}");
+        }
+
+        [HttpPost("List/Delete")]
+        public async Task<IActionResult> Delete([FromForm] int[] ids)
+        {
+            try
+            {
+                if (ids == null || ids.Length <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                foreach (var id in ids)
+                {
+                    var member = await _db.Member.FindAsync(id);
+                    if (member == null)
+                    {
+                        throw new Exception($"{id}번 회원을 찾을 수 없습니다.");
+                    }
+
+                    _db.Member.Remove(member);
+
+                    int affectedRows = await _db.SaveChangesAsync();
+                    if (affectedRows <= 0)
+                    {
+                        throw new Exception($"{id}번 회원 삭제 중 오류가 발생했습니다.");
+                    }
+
+                    _fileUploadService.RemoveFile(member.Photo);
+                    _fileUploadService.RemoveFile(member.Icon);
+                }
+
+                string message = $"{ids.Length}건의 회원이 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect($"/Member/List?{_queryString}");
+        }
+
+        [HttpGet("List/{id}/Approve")]
+        public async Task<IActionResult> Approve(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var memberApprove = await _db.MemberApprove.FindAsync(id);
+                if (memberApprove is null)
+                {
+                    throw new Exception("회원 알림 및 동의 정보를 찾을 수 없습니다.");
+                }
+
+                return View("~/Views/Member/List/Approve.cshtml", memberApprove);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return Redirect("/Member/List");
+            }
+        }
+
+        [HttpPost("List/Approve")]
+        public async Task<IActionResult> Approve([FromForm] MemberApprove request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                if (!await _db.MemberApprove.AnyAsync(c => c.MemberID == request.MemberID))
+                {
+                    throw new ArgumentException("회원 알림 및 동의 정보를 찾을 수 없습니다.");
+                }
+
+                _db.MemberApprove.Update(request);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("수정 중 오류가 발생했습니다.");
+                }
+
+                string message = "회원 알림 및 동의 정보가 수정되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return Redirect("/Member/List");
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+                return await Approve(request.MemberID);
+            }
+        }
+    }
+}

+ 219 - 0
backend/Controllers/Member/Log/EmailController.cs

@@ -0,0 +1,219 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.WebUtilities;
+using DeviceDetectorNET;
+using bitforum.Helpers;
+using bitforum.Models;
+
+namespace bitforum.Controllers.Member.Log
+{
+    [Authorize]
+    [Route("Member/Log")]
+    public class Emailontroller : Controller
+    {
+        private readonly ILogger<Emailontroller> _logger;
+        private readonly DefaultDbContext _db;
+        private string? _queryString = null;
+
+        public Emailontroller(ILogger<Emailontroller> logger, DefaultDbContext db)
+        {
+            _logger = logger;
+            _db = db;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        public override void OnActionExecuting(ActionExecutingContext context)
+        {
+            ViewBag.QueryString = _queryString = string.Join("&", QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).Select(r => $"{r.Key}={r.Value.FirstOrDefault()}"));
+
+            base.OnActionExecuting(context);
+        }
+
+        [HttpGet("Email")]
+        public async Task<IActionResult> Index([FromQuery] int page = 1, int perPage = 10, byte? search = null, string? keyword = null, string startAt = null!, string endAt = null!)
+        {
+            var query = _db.EmailChangeLog.AsQueryable();
+
+            // 검색 조건 적용
+            if (!string.IsNullOrEmpty(keyword) && search.HasValue)
+            {
+                if (search == 1) // ID 검색
+                {
+                    if (int.TryParse(keyword, out int memberID))
+                    {
+                        query = query.Where(log => log.MemberID == memberID);
+                    }
+                    else
+                    {
+                        query = query.Where(log => log.MemberID == 0);
+                    }
+                }
+                else if (search == 2)
+                {
+                    query = query.Where(log => _db.Member.Any(c => c.Email.Contains(keyword))); // 회원 이메일 검색
+                }
+                else if (search == 3)
+                {
+                    query = query.Where(log => _db.Member.Any(c => c.Name != null && c.Name.Contains(keyword))); // 회원 별명 검색
+                }
+                else if (search == 4)
+                {
+                    query = query.Where(log => log.BeforeEmail != null && log.BeforeEmail.Contains(keyword));
+                }
+                else if (search == 5)
+                {
+                    query = query.Where(log => log.AfterEmail.Contains(keyword));
+                }
+            }
+
+            var today = DateTime.UtcNow.Date;
+
+            if (startAt != null && DateTime.TryParse(startAt, out var st))
+            {
+                query = query.Where(log => log.CreatedAt >= st);
+            }
+            if (endAt != null && DateTime.TryParse(endAt, out var et))
+            {
+                query = query.Where(log => log.CreatedAt <= et);
+            }
+
+            query = query.OrderByDescending(c => c.CreatedAt);
+
+
+            var logs = await (from C in query
+                              join M in _db.Member on C.MemberID equals M.ID into MB
+                              from M in MB.DefaultIfEmpty()
+                              select new
+                              {
+                                  M.Name,
+                                  M.Email,
+                                  C.ID,
+                                  C.MemberID,
+                                  C.BeforeEmail,
+                                  C.AfterEmail,
+                                  C.CreatedAt
+                              }).Skip((page -1) * perPage).Take(perPage).ToListAsync();
+
+            var data = new List<object>();
+            if (logs.Count > 0)
+            {
+                foreach (var row in logs)
+                {
+                    data.Add(new
+                    {
+                        row.ID,
+                        row.MemberID,
+                        row.Name,
+                        row.Email,
+                        row.BeforeEmail,
+                        row.AfterEmail,
+                        DeleteURL = $"/Member/Log/Email/{row.ID}/Delete?{_queryString}",
+                        CreatedAt = row.CreatedAt.GetDateAt()
+                    });
+                }
+            }
+
+            var parameter = new
+            {
+                Page = page,
+                PerPage = perPage,
+                Search = search,
+                Keyword = keyword,
+                StartAt = startAt,
+                EndAt = endAt
+            };
+
+            ViewBag.Data = data;
+            ViewBag.Total = await query.CountAsync();
+            ViewBag.Parameter = parameter;
+            ViewBag.Pagination = new Pagination(ViewBag.Total, page, perPage, parameter);
+
+            return View("~/Views/Member/Log/Email.cshtml");
+        }
+
+        [HttpGet("Email/{id}/Delete")]
+        public async Task<IActionResult> Delete(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var data = await _db.NameChangeLog.FindAsync(id);
+                if (data == null)
+                {
+                    throw new Exception("별명 변경 정보를 찾을 수 없습니다.");
+                }
+
+                _db.NameChangeLog.Remove(data);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("별명 변경 내역 삭제 중 오류가 발생했습니다.");
+                }
+
+                string message = "별명 변경 내역이 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect($"/Member/Log/Email?{_queryString}");
+        }
+
+        [HttpPost("Email/Delete")]
+        public async Task<IActionResult> Delete([FromForm] int[] ids)
+        {
+            try
+            {
+                if (ids == null || ids.Length <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                foreach (var id in ids)
+                {
+                    var data = await _db.NameChangeLog.FindAsync(id);
+                    if (data == null)
+                    {
+                        throw new Exception($"{id}번 이메일 변경 정보를 찾을 수 없습니다.");
+                    }
+
+                    _db.NameChangeLog.Remove(data);
+
+                    int affectedRows = await _db.SaveChangesAsync();
+                    if (affectedRows <= 0)
+                    {
+                        throw new Exception($"{id}번 이메일 변경 내역 삭제 중 오류가 발생했습니다.");
+                    }
+                }
+
+                string message = $"{ids.Length}건의 이메일 변경 내역이 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect($"/Member/Log/Email?{_queryString}");
+        }
+    }
+}

+ 286 - 0
backend/Controllers/Member/Log/LoginController.cs

@@ -0,0 +1,286 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.WebUtilities;
+using DeviceDetectorNET;
+using bitforum.Helpers;
+using bitforum.Models;
+
+namespace bitforum.Controllers.Member.Log
+{
+    [Authorize]
+    [Route("Member/Log")]
+    public class LoginController : Controller
+    {
+        private readonly ILogger<LoginController> _logger;
+        private readonly DefaultDbContext _db;
+        private string? _queryString = null;
+
+        public LoginController(ILogger<LoginController> logger, DefaultDbContext db)
+        {
+            _logger = logger;
+            _db = db;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        public override void OnActionExecuting(ActionExecutingContext context)
+        {
+            ViewBag.QueryString = _queryString = string.Join("&", QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).Select(r => $"{r.Key}={r.Value.FirstOrDefault()}"));
+
+            base.OnActionExecuting(context);
+        }
+
+        [HttpGet("Login")]
+        public async Task<IActionResult> Index([FromQuery] int page = 1, int perPage = 10, byte? search = null, string? keyword = null, string startAt = null!, string endAt = null!)
+        {
+            var query = _db.LoginLog.AsQueryable(); // 기본 정렬 유지
+
+            // 검색 조건 적용
+            if (!string.IsNullOrEmpty(keyword) && search.HasValue)
+            {
+                if (search == 1) // ID 검색
+                {
+                    if (int.TryParse(keyword, out int memberID))
+                    {
+                        query = query.Where(log => log.MemberID == memberID);
+                    }
+                    else
+                    {
+                        query = query.Where(log => log.MemberID == 0);
+                    }
+                }
+                else if (search == 2)
+                {
+                    query = query.Where(log => _db.LoginLog.Any(c => c.Account.Contains(keyword))); // 이메일 검색
+                }
+                else if (search == 3)
+                {
+                    query = query.Where(log => _db.Member.Any(m => m.Name != null && m.Name.Contains(keyword))); // 사용자명 검색
+                }
+            }
+
+            var today = DateTime.UtcNow.Date;
+
+            if (startAt != null && DateTime.TryParse(startAt, out var st))
+            {
+                query = query.Where(log => log.CreatedAt >= st);
+            }
+            if (endAt != null && DateTime.TryParse(endAt, out var et))
+            {
+                query = query.Where(log => log.CreatedAt <= et);
+            }
+
+            query = query.OrderByDescending(c => c.CreatedAt);
+
+            // 페이징 처리
+            var logs = await query
+                .Skip((page - 1) * perPage)
+                .Take(perPage)
+                .Select(c => new
+                {
+                    Name = _db.Member.Where(m => m.ID == c.MemberID).Select(m => m.Name).FirstOrDefault(), // 사용자명 추가
+                    c.ID,
+                    c.MemberID,
+                    c.Account,
+                    c.Success,
+                    c.Reason,
+                    c.Referer,
+                    c.Url,
+                    c.IpAddress,
+                    c.UserAgent,
+                    c.CreatedAt
+                })
+                .ToListAsync();
+
+
+            var data = new List<object>();
+            if (logs.Count > 0)
+            {
+                foreach (var row in logs)
+                {
+                    var deviceDetector = new DeviceDetector(row.UserAgent);
+                    deviceDetector.Parse();
+                    var browser = deviceDetector.GetClient();
+                    var osInfo = deviceDetector.GetOs();
+                    var device = deviceDetector.GetModel();
+
+                    data.Add(new
+                    {
+                        row.ID,
+                        row.MemberID,
+                        row.Name,
+                        row.Account,
+                        Success = row.Success ? 'Y' : 'N',
+                        row.Reason,
+                        row.Referer,
+                        row.Url,
+                        row.IpAddress,
+                        Browser = browser,
+                        OS = osInfo,
+                        Device = device,
+                        ViewURL = $"/Member/Log/Login/{row.ID}?{_queryString}",
+                        DeleteURL = $"/Member/Log/Login/{row.ID}/Delete?{_queryString}",
+                        CreatedAt = row.CreatedAt.GetDateAt()
+                    });
+                }
+            }
+
+            var parameter = new
+            {
+                Page = page,
+                PerPage = perPage,
+                Search = search,
+                Keyword = keyword,
+                StartAt = startAt,
+                EndAt = endAt
+            };
+
+            ViewBag.Data = data;
+            ViewBag.Total = await query.CountAsync();
+            ViewBag.Parameter = parameter;
+            ViewBag.Pagination = new Pagination(ViewBag.Total, page, perPage, parameter);
+
+            return View("~/Views/Member/Log/Login/Index.cshtml");
+        }
+
+        [HttpGet("Login/{id}")]
+        public async Task<IActionResult> View([FromRoute] int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var loginData = await _db.LoginLog.FindAsync(id);
+                if (loginData == null)
+                {
+                    throw new Exception("로그인 정보를 찾을 수 없습니다.");
+                }
+
+                var deviceDetector = new DeviceDetector(loginData.UserAgent);
+                deviceDetector.Parse();
+
+                var botInfo = deviceDetector.GetBot();
+                var clientInfo = deviceDetector.GetClient();
+                var osInfo = deviceDetector.GetOs();
+                var device = deviceDetector.GetDeviceName();
+                var brand = deviceDetector.GetBrandName();
+                var model = deviceDetector.GetModel();
+
+                ViewBag.Data = new
+                {
+                    loginData.ID,
+                    loginData.MemberID,
+                    Name = _db.Member.Where(m => m.ID == loginData.MemberID).Select(m => m.Name).FirstOrDefault(), // 사용자명 추가
+                    Success = loginData.Success ? 'Y' : 'N',
+                    loginData.Account,
+                    loginData.Reason,
+                    loginData.Referer,
+                    loginData.Url,
+                    loginData.IpAddress,
+                    loginData.UserAgent,
+                    loginData.CreatedAt,
+                    Bot = botInfo,
+                    Browser = clientInfo,
+                    OS = osInfo,
+                    Device = device,
+                    Brand = brand,
+                    Model = model,
+                };
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return View("~/Views/Member/Log/Login/View.cshtml");
+        }
+
+        [HttpGet("Login/{id}/Delete")]
+        public async Task<IActionResult> Delete(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var data = await _db.LoginLog.FindAsync(id);
+                if (data == null)
+                {
+                    throw new Exception("로그인 정보를 찾을 수 없습니다.");
+                }
+
+                _db.LoginLog.Remove(data);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("로그인 내역 삭제 중 오류가 발생했습니다.");
+                }
+
+                string message = "로그인 내역이 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect($"/Member/Log/Login?{_queryString}");
+        }
+
+        [HttpPost("Login/Delete")]
+        public async Task<IActionResult> Delete([FromForm] int[] ids)
+        {
+            try
+            {
+                if (ids == null || ids.Length <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                foreach (var id in ids)
+                {
+                    var data = await _db.LoginLog.FindAsync(id);
+                    if (data == null)
+                    {
+                        throw new Exception($"{id}번 로그인 정보를 찾을 수 없습니다.");
+                    }
+
+                    _db.LoginLog.Remove(data);
+
+                    int affectedRows = await _db.SaveChangesAsync();
+                    if (affectedRows <= 0)
+                    {
+                        throw new Exception($"{id}번 로그인 내역 삭제 중 오류가 발생했습니다.");
+                    }
+                }
+
+                string message = $"{ids.Length}건의 로그인 내역이 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect($"/Member/Log/Login?{_queryString}");
+        }
+    }
+}

+ 219 - 0
backend/Controllers/Member/Log/NameController.cs

@@ -0,0 +1,219 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.WebUtilities;
+using DeviceDetectorNET;
+using bitforum.Helpers;
+using bitforum.Models;
+
+namespace bitforum.Controllers.Member.Log
+{
+    [Authorize]
+    [Route("Member/Log")]
+    public class NameController : Controller
+    {
+        private readonly ILogger<NameController> _logger;
+        private readonly DefaultDbContext _db;
+        private string? _queryString = null;
+
+        public NameController(ILogger<NameController> logger, DefaultDbContext db)
+        {
+            _logger = logger;
+            _db = db;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        public override void OnActionExecuting(ActionExecutingContext context)
+        {
+            ViewBag.QueryString = _queryString = string.Join("&", QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).Select(r => $"{r.Key}={r.Value.FirstOrDefault()}"));
+
+            base.OnActionExecuting(context);
+        }
+
+        [HttpGet("Name")]
+        public async Task<IActionResult> Index([FromQuery] int page = 1, int perPage = 10, byte? search = null, string? keyword = null, string startAt = null!, string endAt = null!)
+        {
+            var query = _db.NameChangeLog.AsQueryable();
+
+            // 검색 조건 적용
+            if (!string.IsNullOrEmpty(keyword) && search.HasValue)
+            {
+                if (search == 1) // ID 검색
+                {
+                    if (int.TryParse(keyword, out int memberID))
+                    {
+                        query = query.Where(log => log.MemberID == memberID);
+                    }
+                    else
+                    {
+                        query = query.Where(log => log.MemberID == 0);
+                    }
+                }
+                else if (search == 2)
+                {
+                    query = query.Where(log => _db.Member.Any(c => c.Email.Contains(keyword))); // 회원 이메일 검색
+                }
+                else if (search == 3)
+                {
+                    query = query.Where(log => _db.Member.Any(c => c.Name != null && c.Name.Contains(keyword))); // 회원 별명 검색
+                }
+                else if (search == 4)
+                {
+                    query = query.Where(log => log.BeforeName != null && log.BeforeName.Contains(keyword));
+                }
+                else if (search == 5)
+                {
+                    query = query.Where(log => log.AfterName.Contains(keyword));
+                }
+            }
+
+            var today = DateTime.UtcNow.Date;
+
+            if (startAt != null && DateTime.TryParse(startAt, out var st))
+            {
+                query = query.Where(log => log.CreatedAt >= st);
+            }
+            if (endAt != null && DateTime.TryParse(endAt, out var et))
+            {
+                query = query.Where(log => log.CreatedAt <= et);
+            }
+
+            query = query.OrderByDescending(c => c.CreatedAt);
+
+
+            var logs = await (from C in query
+                              join M in _db.Member on C.MemberID equals M.ID into MB
+                              from M in MB.DefaultIfEmpty()
+                              select new
+                              {
+                                  M.Name,
+                                  M.Email,
+                                  C.ID,
+                                  C.MemberID,
+                                  C.BeforeName,
+                                  C.AfterName,
+                                  C.CreatedAt
+                              }).Skip((page -1) * perPage).Take(perPage).ToListAsync();
+
+            var data = new List<object>();
+            if (logs.Count > 0)
+            {
+                foreach (var row in logs)
+                {
+                    data.Add(new
+                    {
+                        row.ID,
+                        row.MemberID,
+                        row.Name,
+                        row.Email,
+                        row.BeforeName,
+                        row.AfterName,
+                        DeleteURL = $"/Member/Log/Name/{row.ID}/Delete?{_queryString}",
+                        CreatedAt = row.CreatedAt.GetDateAt()
+                    });
+                }
+            }
+
+            var parameter = new
+            {
+                Page = page,
+                PerPage = perPage,
+                Search = search,
+                Keyword = keyword,
+                StartAt = startAt,
+                EndAt = endAt
+            };
+
+            ViewBag.Data = data;
+            ViewBag.Total = await query.CountAsync();
+            ViewBag.Parameter = parameter;
+            ViewBag.Pagination = new Pagination(ViewBag.Total, page, perPage, parameter);
+
+            return View("~/Views/Member/Log/Name.cshtml");
+        }
+
+        [HttpGet("Name/{id}/Delete")]
+        public async Task<IActionResult> Delete(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var data = await _db.NameChangeLog.FindAsync(id);
+                if (data == null)
+                {
+                    throw new Exception("별명 변경 정보를 찾을 수 없습니다.");
+                }
+
+                _db.NameChangeLog.Remove(data);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("별명 변경 내역 삭제 중 오류가 발생했습니다.");
+                }
+
+                string message = "별명 변경 내역이 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect($"/Member/Log/Name?{_queryString}");
+        }
+
+        [HttpPost("Name/Delete")]
+        public async Task<IActionResult> Delete([FromForm] int[] ids)
+        {
+            try
+            {
+                if (ids == null || ids.Length <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                foreach (var id in ids)
+                {
+                    var data = await _db.NameChangeLog.FindAsync(id);
+                    if (data == null)
+                    {
+                        throw new Exception($"{id}번 별명 변경 정보를 찾을 수 없습니다.");
+                    }
+
+                    _db.NameChangeLog.Remove(data);
+
+                    int affectedRows = await _db.SaveChangesAsync();
+                    if (affectedRows <= 0)
+                    {
+                        throw new Exception($"{id}번 별명 변경 내역 삭제 중 오류가 발생했습니다.");
+                    }
+                }
+
+                string message = $"{ids.Length}건의 별명 변경 내역이 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Redirect($"/Member/Log/Name?{_queryString}");
+        }
+    }
+}

+ 83 - 32
backend/Controllers/Page/Banner/ItemController.cs

@@ -8,6 +8,9 @@ using Microsoft.AspNetCore.WebUtilities;
 using bitforum.Models;
 using bitforum.Models.Page.Banner;
 using bitforum.Services;
+using bitforum.Helpers;
+using bitforum.Repository;
+using bitforum.Constants;
 
 namespace bitforum.Controllers.Page.Banner
 {
@@ -16,18 +19,20 @@ namespace bitforum.Controllers.Page.Banner
     public class ItemController : Controller
     {
         private readonly ILogger<ItemController> _logger;
+        private readonly IFileUploadService _fileUploadService;
+        private readonly IRedisRepository _redisRepository;
         private readonly DefaultDbContext _db;
-        private readonly FileUploadService _fileUploadService;
         private readonly string _IndexViewPath = "~/Views/Page/Banner/Item/Index.cshtml";
         private readonly string _WriteViewPath = "~/Views/Page/Banner/Item/Write.cshtml";
         private readonly string _EditViewPath = "~/Views/Page/Banner/Item/Edit.cshtml";
-        private Dictionary<string, string>? _queryString = null;
+        private string? _queryString = null;
 
-        public ItemController(ILogger<ItemController> logger, DefaultDbContext db, FileUploadService fileUploadService)
+        public ItemController(ILogger<ItemController> logger, IFileUploadService fileUploadService, IRedisRepository redisRepository, DefaultDbContext db)
         {
             _logger = logger;
-            _db = db;
             _fileUploadService = fileUploadService;
+            _redisRepository = redisRepository;
+            _db = db;
         }
 
         [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
@@ -38,7 +43,8 @@ namespace bitforum.Controllers.Page.Banner
 
         public override void OnActionExecuting(ActionExecutingContext context)
         {
-            ViewBag.QueryString = QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).ToDictionary(k => k.Key, v => string.Join(",", v.Value));
+            ViewBag.QueryString = _queryString = string.Join("&", QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).Select(r => $"{r.Key}={string.Join(",", r.Value)}"));
+
             base.OnActionExecuting(context);
         }
 
@@ -51,13 +57,52 @@ namespace bitforum.Controllers.Page.Banner
             ViewBag.BannerPositions = _db.BannerPosition.Include(c => c.BannerItem).Where(c => c.IsActive).ToList();
 
             // 배너 목록
-            var bannerItems = _db.BannerItem.Include(c => c.BannerPosition).OrderBy(c => c.Order).AsQueryable();
+            var bannerItemQuery = _db.BannerItem.Include(c => c.BannerPosition).OrderBy(c => c.Order).AsQueryable();
             if (positionID.HasValue)
             {
-                bannerItems = bannerItems.Where(c => c.PositionID == positionID);
+                bannerItemQuery = bannerItemQuery.Where(c => c.PositionID == positionID);
             }
-            ViewBag.BannerItems = bannerItems.ToList();
-            ViewBag.Total = bannerItems.Count();
+
+            var bannerItems = bannerItemQuery.ToList();
+            var data = new List<object>();
+
+            if (bannerItems.Count > 0)
+            {
+                foreach (var row in bannerItems)
+                {
+                    string Expired = row switch
+                    {
+                        { StartAt: not null, EndAt: not null } => $"{row.StartAt.GetDateAt()} ~ {row.EndAt.GetDateAt()}",
+                        { StartAt: not null, EndAt: null } => $"{row.StartAt.GetDateAt()}",
+                        { StartAt: null, EndAt: not null } => $"~ {row.EndAt.GetDateAt()}",
+                        _ => "[무기한]"
+                    };
+
+                    var entry = new
+                    {
+                        row.ID,
+                        BannerPositionName = row.BannerPosition?.Subject,
+                        Expired,
+                        row.Subject,
+                        row.Image,
+                        row.Width,
+                        row.Height,
+                        row.Link,
+                        row.Order,
+                        IsActive = (row.IsActive ? 'Y' : 'N'),
+                        Views = row.Views.ToString("N0"),
+                        UpdatedAt = row.UpdatedAt.GetDateAt(),
+                        CreatedAt = row.CreatedAt.GetDateAt(),
+                        EditURL = $"/Page/Banner/Item/{row.ID}/Edit?{_queryString}",
+                        DeleteURL = $"/Page/Banner/Item/{row.ID}/Delete?{_queryString}"
+                    };
+
+                    data.Add(entry);
+                }
+            }
+
+            ViewBag.Data = data;
+            ViewBag.Total = (data?.Count ?? 0);
             ViewBag.Pagination = new Pagination(ViewBag.Total, page, 20, null);
 
             return View(_IndexViewPath);
@@ -89,7 +134,7 @@ namespace bitforum.Controllers.Page.Banner
                 // 이미지 저장
                 request.Image = await _fileUploadService.UploadImageAsync(Image, UploadFolder.Banner);
                 request.UpdatedAt = null;
-                request.CreatedAt = DateTime.Now;
+                request.CreatedAt = DateTime.UtcNow;
 
                 _db.BannerItem.Add(request);
 
@@ -99,21 +144,23 @@ namespace bitforum.Controllers.Page.Banner
                     throw new Exception("배너 등록 중 오류가 발생했습니다.");
                 }
 
-                string message = "배너가 정상적으로 등록되었습니다.";
+                await _redisRepository.DeleteAsync(RedisConst.BannerKey);
+
+                string message = "배너가 등록되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return Redirect("/Page/Banner/Item");
+                return Redirect($"/Page/Banner/Item?{_queryString}");
             }
             catch (ArgumentException e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return Write();
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return Write();
             }
         }
@@ -141,9 +188,9 @@ namespace bitforum.Controllers.Page.Banner
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return Redirect("/Page/Banner/Item");
+                _logger.LogError(e, e.Message);
+                return Redirect($"/Page/Banner/Item?{_queryString}");
             }
         }
 
@@ -194,9 +241,7 @@ namespace bitforum.Controllers.Page.Banner
                 bannerItem.IsActive = request.IsActive;
                 bannerItem.StartAt = request.StartAt;
                 bannerItem.EndAt = request.EndAt;
-                bannerItem.UpdatedAt = DateTime.Now;
-
-                _db.BannerItem.Update(bannerItem);
+                bannerItem.UpdatedAt = DateTime.UtcNow;
 
                 int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
@@ -204,15 +249,17 @@ namespace bitforum.Controllers.Page.Banner
                     throw new Exception("배너 수정 중 오류가 발생했습니다.");
                 }
 
-                string message = "배너가 정상적으로 수정되었습니다.";
+                await _redisRepository.DeleteAsync(RedisConst.BannerKey);
+
+                string message = "배너가 수정되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return Redirect($"/Page/Banner/Item/{request.ID}/Edit");
+                return Redirect($"/Page/Banner/Item/{request.ID}/Edit?{_queryString}");
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return await Edit(request.ID);
             }
         }
@@ -233,7 +280,6 @@ namespace bitforum.Controllers.Page.Banner
                     throw new Exception("배너 정보를 찾을 수 없습니다.");
                 }
 
-                _fileUploadService.RemoveFile(bannerItem.Image);
                 _db.BannerItem.Remove(bannerItem);
 
                 int affectedRows = await _db.SaveChangesAsync();
@@ -242,17 +288,20 @@ namespace bitforum.Controllers.Page.Banner
                     throw new Exception("배너 삭제 중 오류가 발생했습니다.");
                 }
 
-                string message = "배너가 정상적으로 삭제되었습니다.";
+                _fileUploadService.RemoveFile(bannerItem.Image);
+                await _redisRepository.DeleteAsync(RedisConst.BannerKey);
+
+                string message = "배너가 삭제되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
             }
 
-            return Redirect("/Page/Banner/Item");
+            return Redirect($"/Page/Banner/Item?{_queryString}");
         }
 
         [HttpPost("Item/Delete")]
@@ -270,30 +319,32 @@ namespace bitforum.Controllers.Page.Banner
                     var bannerItem = await _db.BannerItem.FindAsync(id);
                     if (bannerItem == null)
                     {
-                        throw new Exception("배너 정보를 찾을 수 없습니다.");
+                        throw new Exception($"{id}번 배너 정보를 찾을 수 없습니다.");
                     }
 
-                    _fileUploadService.RemoveFile(bannerItem.Image);
                     _db.BannerItem.Remove(bannerItem);
 
                     int affectedRows = await _db.SaveChangesAsync();
                     if (affectedRows <= 0)
                     {
-                        throw new Exception($"{id}번호의 배너 삭제 중 오류가 발생했습니다.");
+                        throw new Exception($"{id}번 배너 삭제 중 오류가 발생했습니다.");
                     }
+
+                    _fileUploadService.RemoveFile(bannerItem.Image);
+                    await _redisRepository.DeleteAsync(RedisConst.BannerKey);
                 }
 
-                string message = "배너가 정상적으로 삭제되었습니다.";
+                string message = $"{ids.Length}건의 배너가 삭제되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
             }
 
-            return Redirect("/Page/Banner/Item");
+            return Redirect($"/Page/Banner/Item?{_queryString}");
         }
     }
 }

+ 35 - 27
backend/Controllers/Page/Banner/PositionController.cs

@@ -1,23 +1,27 @@
 using System.Diagnostics;
-using bitforum.Models;
-using bitforum.Models.Page.Banner;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
+using bitforum.Constants;
+using bitforum.Models;
+using bitforum.Models.Page.Banner;
+using bitforum.Repository;
 
-namespace bitforum.Controllers.Page
+namespace bitforum.Controllers.Page.Banner
 {
     [Authorize]
     [Route("Page")]
     public class PositionController : Controller
     {
         private readonly ILogger<PositionController> _logger;
+        private readonly IRedisRepository _redisRepository;
         private readonly DefaultDbContext _db;
         private readonly string _ViewPath = "~/Views/Page/Banner/Position/Index.cshtml";
 
-        public PositionController(ILogger<PositionController> logger, DefaultDbContext db)
+        public PositionController(ILogger<PositionController> logger, IRedisRepository redisRepository, DefaultDbContext db)
         {
             _logger = logger;
+            _redisRepository = redisRepository;
             _db = db;
         }
 
@@ -31,6 +35,7 @@ namespace bitforum.Controllers.Page
         public IActionResult Index()
         {
             ViewBag.BannerPositions = _db.BannerPosition.Include(c => c.BannerItem).ToList();
+            ViewBag.Total = ViewBag.BannerPositions?.Count ?? 0;
 
             return View(_ViewPath);
         }
@@ -61,15 +66,16 @@ namespace bitforum.Controllers.Page
                     throw new Exception("유효성 검사에 실패하였습니다.");
                 }
                 
-                var requestIds = request.Select(x => x.ID).ToList(); // 요청 데이터의 ID 목록
-                var existingIds = await _db.BannerPosition.Select(c => c.ID).ToListAsync(); // 데이터베이스에 존재하는 ID 목록
-                var idsToDelete = existingIds.Except(requestIds).ToList(); // 삭제 대상 ID: 요청 데이터에 없는 항목
+                var requestIDs = request.Select(x => x.ID).ToList(); // 요청 데이터의 ID 목록
+                var existingIDs = await _db.BannerPosition.Select(c => c.ID).ToListAsync(); // 데이터베이스에 존재하는 ID 목록
+                var IDsToDelete = existingIDs.Except(requestIDs).ToList(); // 삭제 대상 ID: 요청 데이터에 없는 항목
 
                 // 삭제 대상 항목 제거
-                if (idsToDelete.Any())
+                if (IDsToDelete.Any())
                 {
-                    var selectedRows = await _db.BannerPosition.Where(c => idsToDelete.Contains(c.ID) && !c.BannerItem.Any()).ToListAsync();
-                    _db.BannerPosition.RemoveRange(selectedRows);
+                    _db.BannerPosition.RemoveRange(
+                        await _db.BannerPosition.Where(c => IDsToDelete.Contains(c.ID) && !c.BannerItem.Any()).ToListAsync()
+                    );
                 }
 
                 foreach (var row in request)
@@ -77,29 +83,29 @@ namespace bitforum.Controllers.Page
                     // 중복 확인
                     if (await _db.BannerPosition.AnyAsync(c => c.Code == row.Code && c.ID != row.ID))
                     {
-                        throw new Exception($"이미 존재하는 배너 위치입니다: {row.Code}");
+                        throw new Exception($"{row.Code} `Code`는 이미 존재합니다.");
                     }
 
                     if (row.ID == 0)
                     {
-                        row.UpdatedAt = null;
-                        row.CreatedAt = DateTime.Now;
+                        row.CreatedAt = DateTime.UtcNow;
+
                         await _db.BannerPosition.AddAsync(row);
                     }
                     else
                     {
-                        var existing = await _db.BannerPosition.FirstOrDefaultAsync(c => c.ID == row.ID);
-                        if (existing == null)
+                        var bannerPosition = await _db.BannerPosition.FirstOrDefaultAsync(c => c.ID == row.ID);
+                        if (bannerPosition == null)
                         {
-                            throw new Exception($"ID {row.ID}에 해당하는 데이터가 없습니다.");
+                            throw new Exception($"ID {row.ID}에 해당하는 정보가 없습니다.");
                         }
 
-                        existing.Code = row.Code;
-                        existing.Subject = row.Subject;
-                        existing.IsActive = row.IsActive;
-                        existing.UpdatedAt = DateTime.Now;
+                        bannerPosition.Code = row.Code;
+                        bannerPosition.Subject = row.Subject;
+                        bannerPosition.IsActive = row.IsActive;
+                        bannerPosition.UpdatedAt = DateTime.UtcNow;
 
-                        _db.BannerPosition.Update(existing);
+                        _db.BannerPosition.Update(bannerPosition);
                     }
                 }
 
@@ -108,23 +114,25 @@ namespace bitforum.Controllers.Page
                 int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
                 {
-                    throw new Exception("배너 위치 저장 중 오류가 발생했습니다.");
+                    throw new Exception("저장 중 오류가 발생했습니다.");
                 }
 
-                string message = "배너 위치가 정상적으로 저장되었습니다.";
+                await _redisRepository.DeleteAsync(RedisConst.BannerKey);
+
+                string message = "배너 위치가 저장되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
+
                 return RedirectToAction("Index");
             }
             catch (Exception e)
             {
                 await transaction.RollbackAsync();
-                _logger.LogError(e, e.Message);
-                TempData["ErrorMessages"] = e.Message;
 
-                ViewBag.BannerPositions = _db.BannerPosition.Include(c => c.BannerItem).ToList();
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
 
-                return View(_ViewPath, request);
+                return Index();
             }
         }
     }

+ 67 - 31
backend/Controllers/Page/DocumentController.cs

@@ -1,9 +1,13 @@
 using System.Diagnostics;
-using bitforum.Models;
-using bitforum.Models.Page;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
+using bitforum.Models;
+using bitforum.Models.Page;
+using bitforum.Constants;
+using bitforum.Helpers;
+using bitforum.Services;
+using bitforum.Repository;
 
 namespace bitforum.Controllers.Page
 {
@@ -12,17 +16,19 @@ namespace bitforum.Controllers.Page
     public class DocumentController : Controller
     {
         private readonly ILogger<DocumentController> _logger;
+        private readonly IFileUploadService _fileUploadService;
+        private readonly IRedisRepository _redisRepository;
         private readonly DefaultDbContext _db;
-        private readonly IConfiguration _config;
         private readonly string _IndexViewPath = "~/Views/Page/Document/Index.cshtml";
         private readonly string _WriteViewPath = "~/Views/Page/Document/Write.cshtml";
         private readonly string _EditViewPath = "~/Views/Page/Document/Edit.cshtml";
 
-        public DocumentController(ILogger<DocumentController> logger, DefaultDbContext db, IConfiguration config)
+        public DocumentController(ILogger<DocumentController> logger, IFileUploadService fileUploadService, IRedisRepository redisRepository, DefaultDbContext db)
         {
             _logger = logger;
+            _fileUploadService = fileUploadService;
+            _redisRepository = redisRepository;
             _db = db;
-            _config = config;
         }
 
         [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
@@ -34,8 +40,35 @@ namespace bitforum.Controllers.Page
         [HttpGet("Document")]
         public IActionResult Index()
         {
-            ViewBag.siteURL = _config["AppConfig:AppName"];
-            ViewBag.Documents = _db.Document.OrderByDescending(c => c.ID).ToList();
+            var domain = Setting.AppConfig.AppName;
+            var documents = _db.Document.OrderByDescending(c => c.ID).ToList();
+            var data = new List<object>();
+
+            if (documents.Count > 0)
+            {
+                foreach (var row in documents)
+                {
+                    var entry = new
+                    {
+                        row.ID,
+                        Link = $"{domain }/docs/{row.Code}",
+                        row.Code,
+                        row.Subject,
+                        row.Content,
+                        Views = row.Views.ToString("N0"),
+                        IsActive = (row.IsActive ? 'Y' : 'N'),
+                        UpdatedAt = row.UpdatedAt.GetDateAt(),
+                        CreatedAt = row.CreatedAt.GetDateAt(),
+                        EditURL = $"/Page/Document/{row.ID}/Edit",
+                        DeleteURL = $"/Page/Document/{row.ID}/Delete"
+                    };
+
+                    data.Add(entry);
+                }
+            }
+
+            ViewBag.Data = data;
+            ViewBag.Total = (data?.Count ?? 0);
 
             return View(_IndexViewPath);
         }
@@ -43,7 +76,7 @@ namespace bitforum.Controllers.Page
         [HttpGet("Document/Write")]
         public IActionResult Write()
         {
-            ViewBag.siteURL = _config["AppConfig:AppName"];
+            ViewBag.siteURL = Setting.AppConfig.AppName;
             return View(_WriteViewPath);
         }
 
@@ -60,39 +93,40 @@ namespace bitforum.Controllers.Page
                 // 중복확인
                 if (await _db.Document.AnyAsync(c => c.Code == request.Code))
                 {
-                    throw new Exception("이미 존재하는 Code 주소입니다.");
+                    throw new Exception("이미 존재하는 주소입니다.");
                 }
 
 
+                request.Content = _fileUploadService.UploadEditorAsync(request.Content, null, UploadFolder.Document).Result;
                 request.UpdatedAt = null;
-                request.CreatedAt = DateTime.Now;
+                request.CreatedAt = DateTime.UtcNow;
 
                 _db.Document.Add(request);
-                int affectedRows = await _db.SaveChangesAsync();
 
+                int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
                 {
                     throw new Exception("문서 등록 중 오류가 발생했습니다.");
                 }
 
-                string message = "문서가 정상적으로 등록되었습니다.";
+                await _redisRepository.SetObjectAsync($"{RedisConst.DocumentKey}-{request.Code}", request, RedisConst.CacheExpiration);
+
+                string message = "문서가 등록되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
                 return RedirectToAction("Index");
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return View(_WriteViewPath, request);
             }
         }
 
-        [HttpGet("Document/Edit/{id}")]
+        [HttpGet("Document/{id}/Edit")]
         public async Task<IActionResult> Edit(int id)
         {
-            ViewBag.siteURL = _config["AppConfig:AppName"];
-
             try
             {
                 if (id <= 0)
@@ -101,18 +135,18 @@ namespace bitforum.Controllers.Page
                 }
 
                 var document = await _db.Document.FirstAsync(c => c.ID == id);
-
                 if (document is null)
                 {
                     throw new Exception("사용자 정보를 찾을 수 없습니다.");
                 }
 
+                ViewBag.siteURL = Setting.AppConfig.AppName;
                 return View(_EditViewPath, document);
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return Index();
             }
         }
@@ -135,7 +169,6 @@ namespace bitforum.Controllers.Page
 
                 // 기존 문서 조회
                 var document = await _db.Document.FindAsync(request.ID);
-
                 if (document is null)
                 {
                     throw new Exception("사용자 정보를 찾을 수 없습니다.");
@@ -144,10 +177,8 @@ namespace bitforum.Controllers.Page
                 document.IsActive = request.IsActive;
                 document.Code = request.Code;
                 document.Subject = request.Subject;
-                document.Content = request.Content;
-                document.UpdatedAt = DateTime.Now;
-
-                _db.Document.Update(document);
+                document.Content = _fileUploadService.UploadEditorAsync(request.Content, document.Content, UploadFolder.Document).Result;
+                document.UpdatedAt = DateTime.UtcNow;
 
                 int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
@@ -155,20 +186,22 @@ namespace bitforum.Controllers.Page
                     throw new Exception("문서 수정 중 오류가 발생했습니다.");
                 }
 
-                string message = "문서가 정상적으로 수정되었습니다.";
+                await _redisRepository.SetObjectAsync($"{RedisConst.DocumentKey}-{request.Code}", document, RedisConst.CacheExpiration);
+
+                string message = $"{document.ID}번 문서가 수정되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return RedirectToAction("Edit", new { request.ID });
+                return await Edit(request.ID);
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return View(_EditViewPath, request);
             }
         }
 
-        [HttpGet("Document/Delete/{id}")]
+        [HttpGet("Document/{id}/Delete")]
         public async Task<IActionResult> Delete(int id)
         {
             try
@@ -192,17 +225,20 @@ namespace bitforum.Controllers.Page
                     throw new Exception("문서 삭제 중 오류가 발생했습니다.");
                 }
 
-                string message = "문서가 정상적으로 삭제되었습니다.";
+                await _fileUploadService.CleanUpEditorImagesAsync(document.Content);
+                await _redisRepository.DeleteAsync($"{RedisConst.DocumentKey}-{document.Code}");
+
+                string message = "문서가 삭제되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return RedirectToAction("Index");
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return Index();
+                _logger.LogError(e, e.Message);
             }
+            
+            return Index();
         }
     }
 }

+ 33 - 22
backend/Controllers/Page/Faq/CategoryController.cs

@@ -4,6 +4,8 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using bitforum.Models;
 using bitforum.Models.Page.Faq;
+using bitforum.Repository;
+using bitforum.Constants;
 
 namespace bitforum.Controllers.Page.Faq
 {
@@ -12,12 +14,14 @@ namespace bitforum.Controllers.Page.Faq
     public class CategoryController : Controller
     {
         private readonly ILogger<CategoryController> _logger;
+        private readonly IRedisRepository _redisRepository;
         private readonly DefaultDbContext _db;
         private readonly string _ViewPath = "~/Views/Page/Faq/Category/Index.cshtml";
 
-        public CategoryController(ILogger<CategoryController> logger, DefaultDbContext db)
+        public CategoryController(ILogger<CategoryController> logger, IRedisRepository redisRepository, DefaultDbContext db)
         {
             _logger = logger;
+            _redisRepository = redisRepository;
             _db = db;
         }
 
@@ -31,6 +35,7 @@ namespace bitforum.Controllers.Page.Faq
         public IActionResult Index()
         {
             ViewBag.FaqCategories = _db.FaqCategory.Include(c => c.FaqItem).OrderBy(c => c.Order).ToList();
+            ViewBag.Total = ViewBag.FaqCategories?.Count ?? 0;
 
             return View(_ViewPath);
         }
@@ -61,14 +66,14 @@ namespace bitforum.Controllers.Page.Faq
                     throw new Exception("유효성 검사에 실패하였습니다.");
                 }
                 
-                var requestIds = request.Select(x => x.ID).ToList(); // 요청 데이터의 ID 목록
-                var existingIds = await _db.FaqCategory.Select(c => c.ID).ToListAsync(); // 데이터베이스에 존재하는 ID 목록
-                var idsToDelete = existingIds.Except(requestIds).ToList(); // 삭제 대상 ID: 요청 데이터에 없는 항목
+                var requestIDs = request.Select(x => x.ID).ToList(); // 요청 데이터의 ID 목록
+                var existingIDs = await _db.FaqCategory.Select(c => c.ID).ToListAsync(); // 데이터베이스에 존재하는 ID 목록
+                var IDsToDelete = existingIDs.Except(requestIDs).ToList(); // 삭제 대상 ID: 요청 데이터에 없는 항목
 
                 // 삭제 대상 항목 제거
-                if (idsToDelete.Any())
+                if (IDsToDelete.Any())
                 {
-                    var selectedRows = await _db.FaqCategory.Where(c => idsToDelete.Contains(c.ID) && !c.FaqItem.Any()).ToListAsync();
+                    var selectedRows = await _db.FaqCategory.Where(c => IDsToDelete.Contains(c.ID) && !c.FaqItem.Any()).ToListAsync();
                     _db.FaqCategory.RemoveRange(selectedRows);
                 }
 
@@ -77,52 +82,58 @@ namespace bitforum.Controllers.Page.Faq
                     // 중복 확인
                     if (await _db.FaqCategory.AnyAsync(c => c.Code == row.Code && c.ID != row.ID))
                     {
-                        throw new Exception($"이미 존재하는 분류 주소입니다: {row.Code}");
+                        throw new Exception($"{row.Code} `Code`는 이미 존재합니다.");
                     }
 
                     if (row.ID == 0)
                     {
-                        row.UpdatedAt = null;
-                        row.CreatedAt = DateTime.Now;
+                        row.CreatedAt = DateTime.UtcNow;
+
                         await _db.FaqCategory.AddAsync(row);
                     }
                     else
                     {
-                        var existing = await _db.FaqCategory.FirstOrDefaultAsync(c => c.ID == row.ID);
-                        if (existing == null)
+                        var faqCategory = await _db.FaqCategory.FirstOrDefaultAsync(c => c.ID == row.ID);
+                        if (faqCategory == null)
                         {
-                            throw new Exception($"ID {row.ID}에 해당하는 데이터가 없습니다.");
+                            throw new Exception($"ID {row.ID}에 해당하는 정보가 없습니다.");
                         }
 
                         // 기존 엔터티 업데이트
-                        existing.Code = row.Code;
-                        existing.Subject = row.Subject;
-                        existing.Order = row.Order;
-                        existing.IsActive = row.IsActive;
-                        existing.UpdatedAt = DateTime.Now;
+                        faqCategory.Code = row.Code;
+                        faqCategory.Subject = row.Subject;
+                        faqCategory.Order = row.Order;
+                        faqCategory.IsActive = row.IsActive;
+                        faqCategory.UpdatedAt = DateTime.UtcNow;
 
-                        _db.FaqCategory.Update(existing);
+                        _db.FaqCategory.Update(faqCategory);
                     }
                 }
 
                 await transaction.CommitAsync();
+
                 int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
                 {
-                    throw new Exception("FAQ 저장 중 오류가 발생했습니다.");
+                    throw new Exception("저장 중 오류가 발생했습니다.");
                 }
 
-                string message = "FAQ 분류가 정상적으로 저장되었습니다.";
+                await _redisRepository.DeleteAsync(RedisConst.BannerKey);
+
+                string message = "FAQ 분류가 저장되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
+
                 return RedirectToAction("Index");
             }
             catch (Exception e)
             {
                 await transaction.RollbackAsync();
-                _logger.LogError(e, e.Message);
+
                 TempData["ErrorMessages"] = e.Message;
-                return View(_ViewPath, request);
+                _logger.LogError(e, e.Message);
+
+                return Index();
             }
         }
     }

+ 73 - 29
backend/Controllers/Page/Faq/ItemController.cs

@@ -7,6 +7,10 @@ using Microsoft.AspNetCore.Mvc.Filters;
 using Microsoft.AspNetCore.WebUtilities;
 using bitforum.Models;
 using bitforum.Models.Page.Faq;
+using bitforum.Helpers;
+using bitforum.Services;
+using bitforum.Repository;
+using bitforum.Constants;
 
 namespace bitforum.Controllers.Page.Faq
 {
@@ -15,15 +19,19 @@ namespace bitforum.Controllers.Page.Faq
     public class ItemController : Controller
     {
         private readonly ILogger<ItemController> _logger;
+        private readonly IFileUploadService _fileUploadService;
+        private readonly IRedisRepository _redisRepository;
         private readonly DefaultDbContext _db;
         private readonly string _IndexViewPath = "~/Views/Page/Faq/Item/Index.cshtml";
         private readonly string _WriteViewPath = "~/Views/Page/Faq/Item/Write.cshtml";
         private readonly string _EditViewPath = "~/Views/Page/Faq/Item/Edit.cshtml";
-        private Dictionary<string, string> _queryString;
+        private string? _queryString = null;
 
-        public ItemController(ILogger<ItemController> logger, DefaultDbContext db)
+        public ItemController(ILogger<ItemController> logger, IFileUploadService fileUploadService, IRedisRepository redisRepository, DefaultDbContext db)
         {
             _logger = logger;
+            _fileUploadService = fileUploadService;
+            _redisRepository = redisRepository;
             _db = db;
         }
 
@@ -35,7 +43,8 @@ namespace bitforum.Controllers.Page.Faq
 
         public override void OnActionExecuting(ActionExecutingContext context)
         {
-            ViewBag.QueryString = QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).ToDictionary(k => k.Key, v => string.Join(",", v.Value));
+            ViewBag.QueryString = _queryString = string.Join("&", QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).Select(r => $"{r.Key}={r.Value.FirstOrDefault()}"));
+
             base.OnActionExecuting(context);
         }
 
@@ -48,14 +57,40 @@ namespace bitforum.Controllers.Page.Faq
             ViewBag.FaqCategories = _db.FaqCategory.Include(c => c.FaqItem).Where(c => c.IsActive).OrderBy(c => c.Order).ToList();
 
             // FAQ 목록
-            var faqItems = _db.FaqItem.Include(c => c.FaqCategory).OrderBy(c => c.Order).AsQueryable();
+            var faqItemQuery = _db.FaqItem.Include(c => c.FaqCategory).OrderBy(c => c.Order).AsQueryable();
             if (categoryID.HasValue)
             {
-                faqItems = faqItems.Where(c => c.CategoryID == categoryID);
+                faqItemQuery = faqItemQuery.Where(c => c.CategoryID == categoryID);
+            }
+
+            var faqItems = faqItemQuery.ToList();
+            var data = new List<object>();
+
+            if (faqItems.Count > 0)
+            {
+                foreach (var row in faqItems)
+                {
+                    var entry = new
+                    {
+                        row.ID,
+                        CategoryName = row.FaqCategory.Subject,
+                        row.Question,
+                        row.Answer,
+                        row.Order,
+                        IsActive = (row.IsActive ? 'Y' : 'N'),
+                        Views = row.Views.ToString("N0"),
+                        UpdatedAt = row.UpdatedAt.GetDateAt(),
+                        CreatedAt = row.CreatedAt.GetDateAt(),
+                        EditURL = $"/Page/Faq/Item/{row.ID}/Edit?{_queryString}",
+                        DeleteURL = $"/Page/Faq/Item/{row.ID}/Delete?{_queryString}"
+                    };
+
+                    data.Add(entry);
+                }
             }
 
-            ViewBag.FaqItems = faqItems.ToList();
-            ViewBag.Total = faqItems.Count();
+            ViewBag.Data = data;
+            ViewBag.Total = (data?.Count ?? 0);
             ViewBag.Pagination = new Pagination(ViewBag.Total, page, 20, null);
 
             return View(_IndexViewPath);
@@ -84,8 +119,9 @@ namespace bitforum.Controllers.Page.Faq
                     throw new Exception("유효한 분류를 선택하세요.");
                 }
 
+                request.Answer = _fileUploadService.UploadEditorAsync(request.Answer, null, UploadFolder.Faq).Result;
                 request.UpdatedAt = null;
-                request.CreatedAt = DateTime.Now;
+                request.CreatedAt = DateTime.UtcNow;
 
                 _db.FaqItem.Add(request);
 
@@ -95,15 +131,17 @@ namespace bitforum.Controllers.Page.Faq
                     throw new Exception("FAQ 등록 중 오류가 발생했습니다.");
                 }
 
-                string message = "FAQ가 정상적으로 등록되었습니다.";
+                await _redisRepository.DeleteAsync(RedisConst.FaqKey);
+
+                string message = "FAQ가 등록되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return Redirect("/Page/Faq/Item");
+                return Redirect($"/Page/Faq/Item?{_queryString}");
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return Write();
             }
         }
@@ -131,9 +169,9 @@ namespace bitforum.Controllers.Page.Faq
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return Redirect("/Page/Faq/Item");
+                _logger.LogError(e, e.Message);
+                return Redirect($"/Page/Faq/Item?{_queryString}");
             }
         }
 
@@ -160,11 +198,9 @@ namespace bitforum.Controllers.Page.Faq
 
                 faqItem.CategoryID = request.CategoryID;
                 faqItem.Question = request.Question;
-                faqItem.Answer = request.Answer;
+                faqItem.Answer = _fileUploadService.UploadEditorAsync(request.Answer, faqItem.Answer, UploadFolder.Faq).Result;
                 faqItem.IsActive = request.IsActive;
-                faqItem.UpdatedAt = DateTime.Now;
-
-                _db.FaqItem.Update(faqItem);
+                faqItem.UpdatedAt = DateTime.UtcNow;
 
                 int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
@@ -172,17 +208,19 @@ namespace bitforum.Controllers.Page.Faq
                     throw new Exception("FAQ 수정 중 오류가 발생했습니다.");
                 }
 
-                string message = "FAQ가 정상적으로 수정되었습니다.";
+                await _redisRepository.DeleteAsync(RedisConst.FaqKey);
+
+                string message = "FAQ가 수정되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return Redirect($"/Page/Faq/Item/{request.ID}/Edit");
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return await Edit(request.ID);
+                _logger.LogError(e, e.Message);
             }
+
+            return await Edit(request.ID);
         }
 
         [HttpGet("Item/{id}/Delete")]
@@ -209,17 +247,20 @@ namespace bitforum.Controllers.Page.Faq
                     throw new Exception("FAQ 삭제 중 오류가 발생했습니다.");
                 }
 
-                string message = "FAQ가 정상적으로 삭제되었습니다.";
+                await _fileUploadService.CleanUpEditorImagesAsync(faqItem.Answer);
+                await _redisRepository.DeleteAsync(RedisConst.FaqKey);
+
+                string message = "FAQ가 삭제되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
             }
 
-            return Redirect("/Page/Faq/Item");
+            return Redirect($"/Page/Faq/Item?{_queryString}");
         }
 
         [HttpPost("Item/Delete")]
@@ -237,7 +278,7 @@ namespace bitforum.Controllers.Page.Faq
                     var faqItem = await _db.FaqItem.FindAsync(id);
                     if (faqItem == null)
                     {
-                        throw new Exception("FAQ 정보를 찾을 수 없습니다.");
+                        throw new Exception($"{id}번 FAQ 정보를 찾을 수 없습니다.");
                     }
 
                     _db.FaqItem.Remove(faqItem);
@@ -245,21 +286,24 @@ namespace bitforum.Controllers.Page.Faq
                     int affectedRows = await _db.SaveChangesAsync();
                     if (affectedRows <= 0)
                     {
-                        throw new Exception($"{id}번호의 FAQ 삭제 중 오류가 발생했습니다.");
+                        throw new Exception($"{id}번 FAQ 삭제 중 오류가 발생했습니다.");
                     }
+
+                    await _fileUploadService.CleanUpEditorImagesAsync(faqItem.Answer);
+                    await _redisRepository.DeleteAsync(RedisConst.FaqKey);
                 }
 
-                string message = "FAQ가 정상적으로 삭제되었습니다.";
+                string message = $"{ids.Length}건의 FAQ가 삭제되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
             }
 
-            return Redirect("/Page/Faq/Item");
+            return Redirect($"/Page/Faq/Item?{_queryString}");
         }
     }
 }

+ 79 - 30
backend/Controllers/Page/PopupController.cs

@@ -4,7 +4,10 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using bitforum.Models;
 using bitforum.Models.Page;
-using Microsoft.AspNetCore.Mvc.Rendering;
+using bitforum.Helpers;
+using bitforum.Services;
+using bitforum.Repository;
+using bitforum.Constants;
 
 namespace bitforum.Controllers.Page
 {
@@ -13,17 +16,19 @@ namespace bitforum.Controllers.Page
     public class PopupController : Controller
     {
         private readonly ILogger<PopupController> _logger;
+        private readonly IFileUploadService _fileUploadService;
+        private readonly IRedisRepository _redisRepository;
         private readonly DefaultDbContext _db;
-        private readonly IConfiguration _config;
         private readonly string _IndexViewPath = "~/Views/Page/Popup/Index.cshtml";
         private readonly string _WriteViewPath = "~/Views/Page/Popup/Write.cshtml";
         private readonly string _EditViewPath = "~/Views/Page/Popup/Edit.cshtml";
 
-        public PopupController(ILogger<PopupController> logger, DefaultDbContext db, IConfiguration config)
+        public PopupController(ILogger<PopupController> logger, IFileUploadService fileUploadService, IRedisRepository redisRepository, DefaultDbContext db)
         {
             _logger = logger;
+            _fileUploadService = fileUploadService;
+            _redisRepository = redisRepository;
             _db = db;
-            _config = config;
         }
 
         [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
@@ -35,8 +40,43 @@ namespace bitforum.Controllers.Page
         [HttpGet("Popup")]
         public IActionResult Index([FromQuery] int page = 1)
         {
-            ViewBag.Popups = _db.Popup.OrderByDescending(c => c.ID).ToList();
-            ViewBag.Total = ViewBag.Popups.Count;
+            var popups = _db.Popup.OrderByDescending(c => c.ID).ToList();
+            var data = new List<object>();
+
+            if (popups.Count > 0)
+            {
+                foreach (var row in popups)
+                {
+                    string Expired = row switch
+                    {
+                        { StartAt: not null, EndAt: not null } => $"{row.StartAt.GetDateAt()} ~ {row.EndAt.GetDateAt()}",
+                        { StartAt: not null, EndAt: null } => $"{row.StartAt.GetDateAt()}",
+                        { StartAt: null, EndAt: not null } => $"~ {row.EndAt.GetDateAt()}",
+                        _ => "[무기한]"
+                    };
+
+                    var entry = new
+                    {
+                        row.ID,
+                        row.Subject,
+                        row.Content,
+                        row.Link,
+                        Expired,
+                        row.Order,
+                        IsActive = (row.IsActive ? 'Y' : 'N'),
+                        Views = row.Views.ToString("N0"),
+                        UpdatedAt = row.UpdatedAt.GetDateAt(),
+                        CreatedAt = row.CreatedAt.GetDateAt(),
+                        EditURL = $"/Page/Popup/{row.ID}/Edit",
+                        DeleteURL = $"/Page/Popup/{row.ID}/Delete"
+                    };
+
+                    data.Add(entry);
+                }
+            }
+
+            ViewBag.Data = data;
+            ViewBag.Total = (data?.Count ?? 0);
             ViewBag.Pagination = new Pagination(ViewBag.Total, page, 20, null);
 
             return View(_IndexViewPath);
@@ -63,8 +103,9 @@ namespace bitforum.Controllers.Page
                     throw new Exception("사용 기간을 확인해주세요.");
                 }
 
+                request.Content = _fileUploadService.UploadEditorAsync(request.Content, null, UploadFolder.Popup).Result;
                 request.UpdatedAt = null;
-                request.CreatedAt = DateTime.Now;
+                request.CreatedAt = DateTime.UtcNow;
 
                 _db.Popup.Add(request);
 
@@ -74,15 +115,17 @@ namespace bitforum.Controllers.Page
                     throw new Exception("팝업 등록 중 오류가 발생했습니다.");
                 }
 
-                string message = "팝업이 정상적으로 등록되었습니다.";
+                await _redisRepository.DeleteAsync(RedisConst.PopupKey);
+
+                string message = "팝업이 등록되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return RedirectToAction("Index");
+                return Index();
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return View(_WriteViewPath, request);
             }
         }
@@ -107,9 +150,9 @@ namespace bitforum.Controllers.Page
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return RedirectToAction("Index");
+                _logger.LogError(e, e.Message);
+                return Index();
             }
         }
 
@@ -135,15 +178,13 @@ namespace bitforum.Controllers.Page
                 }
 
                 popup.Subject = request.Subject;
-                popup.Content = request.Content;
+                popup.Content = _fileUploadService.UploadEditorAsync(request.Content, popup.Content, UploadFolder.Popup).Result;
                 popup.IsActive = request.IsActive;
                 popup.Link = request.Link;
                 popup.Order = request.Order;
                 popup.StartAt = request.StartAt;
                 popup.EndAt = request.EndAt;
-                popup.UpdatedAt = DateTime.Now;
-
-                _db.Popup.Update(popup);
+                popup.UpdatedAt = DateTime.UtcNow;
 
                 int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
@@ -151,20 +192,22 @@ namespace bitforum.Controllers.Page
                     throw new Exception("팝업 수정 중 오류가 발생했습니다.");
                 }
 
-                string message = "팝업이 정상적으로 수정되었습니다.";
+                await _redisRepository.DeleteAsync(RedisConst.PopupKey);
+
+                string message = $"{popup.ID}번 팝업이 수정되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return RedirectToAction("Edit", new { request.ID });
+                return await Edit(request.ID);
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
                 return View(_EditViewPath, request);
             }
         }
 
-        [HttpGet("Popup/Delete/{id}")]
+        [HttpGet("Popup/{id}/Delete")]
         public async Task<IActionResult> Delete(int id)
         {
             try
@@ -188,17 +231,20 @@ namespace bitforum.Controllers.Page
                     throw new Exception("팝업 삭제 중 오류가 발생했습니다.");
                 }
 
-                string message = "팝업이 정상적으로 삭제되었습니다.";
+                await _fileUploadService.CleanUpEditorImagesAsync(popup.Content);
+                await _redisRepository.DeleteAsync(RedisConst.PopupKey);
+
+                string message = "팝업이 삭제되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return RedirectToAction("Index");
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return Index();
+                _logger.LogError(e, e.Message);
             }
+
+            return Index();
         }
 
         [HttpPost("Popup/Delete")]
@@ -216,7 +262,7 @@ namespace bitforum.Controllers.Page
                     var popup = await _db.Popup.FindAsync(id);
                     if (popup == null)
                     {
-                        throw new Exception("팝업 정보를 찾을 수 없습니다.");
+                        throw new Exception($"{id}번 팝업 정보를 찾을 수 없습니다.");
                     }
 
                     _db.Popup.Remove(popup);
@@ -224,21 +270,24 @@ namespace bitforum.Controllers.Page
                     int affectedRows = await _db.SaveChangesAsync();
                     if (affectedRows <= 0)
                     {
-                        throw new Exception($"{id}번호의 팝업 삭제 중 오류가 발생했습니다.");
+                        throw new Exception($"{id}번 팝업 삭제 중 오류가 발생했습니다.");
                     }
+
+                    await _fileUploadService.CleanUpEditorImagesAsync(popup.Content);
+                    await _redisRepository.DeleteAsync(RedisConst.PopupKey);
                 }
 
-                string message = "팝업이 정상적으로 삭제되었습니다.";
+                string message = $"{ids.Length}건의 팝업이 삭제되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return RedirectToAction("Index");
             }
             catch (Exception e)
             {
-                _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return RedirectToAction("Index");
+                _logger.LogError(e, e.Message);
             }
+
+            return Index();
         }
     }
 }

+ 0 - 66
backend/Controllers/Setting/BasicController.cs

@@ -1,66 +0,0 @@
-using System.Diagnostics;
-using bitforum.Models;
-using bitforum.Repository;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using bitforum.Helpers;
-using bitforum.Models.User;
-using Microsoft.AspNetCore.Identity;
-
-namespace bitforum.Controllers.Setting
-{
-    [Authorize]
-    [Route("Setting")]
-    public class BasicController : Controller
-    {
-        private readonly ILogger<BasicController> _logger;
-        private readonly ConfigRepository _configRepository;
-        private readonly UserManager<ApplicationUser> _userManager;
-        private readonly string _ViewPath = "~/Views/Setting/Basic.cshtml";
-
-        public BasicController(ILogger<BasicController> logger, ConfigRepository configRepository, UserManager<ApplicationUser> userManager)
-        {
-            _logger = logger;
-            _configRepository = configRepository;
-            _userManager = userManager;
-        }
-
-        [HttpGet("Basic")]
-        public IActionResult Index()
-        {
-            ViewBag.config = _configRepository.GetAll();
-
-            // 최고 관리자
-            ViewBag.Admin = _userManager.GetUsersInRoleAsync("Admin").Result?.ToList();
-
-            return View(_ViewPath);
-        }
-
-        [HttpPost("Basic")]
-        public IActionResult Save(BasicForm request)
-        {
-            string message;
-
-            if (!ModelState.IsValid)
-            {
-                message = "기본 설정 값 저장에 실패";
-                _logger.LogWarning(message);
-                TempData["ErrorMessages"] = message;
-                return View(_ViewPath, request);
-            }
-
-            Func.SaveConfig(request, _configRepository.Replace);
-
-            message = "기본 설정 값이 정상적으로 저장되었습니다.";
-            _logger.LogInformation(message);
-            TempData["SuccessMessage"] = message;
-            return RedirectToAction("Index");
-        }
-
-        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
-        public IActionResult Error()
-        {
-            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
-        }
-    }
-}

+ 0 - 68
backend/Controllers/Setting/CompanyController.cs

@@ -1,68 +0,0 @@
-using System.Diagnostics;
-using bitforum.Models;
-using bitforum.Repository;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using bitforum.Helpers;
-using Microsoft.AspNetCore.Mvc.Rendering;
-using bitforum.Constants;
-
-namespace bitforum.Controllers.Setting
-{
-    [Authorize]
-    [Route("Setting")]
-    public class CompanyController : Controller
-    {
-        private readonly ILogger<CompanyController> _logger;
-        private readonly ConfigRepository _configRepository;
-        private readonly string _ViewPath = "~/Views/Setting/Company.cshtml";
-
-        public CompanyController(ILogger<CompanyController> logger, ConfigRepository configRepository)
-        {
-            _logger = logger;
-            _configRepository = configRepository;
-        }
-
-        [HttpGet("Company")]
-        public IActionResult Index()
-        {
-            var config = _configRepository.GetAll();
-            ViewBag.config = config;
-            ViewBag.bankCodes = BankCodeData.List.Select(item => new SelectListItem
-                {
-                    Value = item.Value,
-                    Text = item.Text,
-                    Selected = item.Value == config.GetConfig("company_bank_code")
-            }).ToList();
-
-            return View(_ViewPath);
-        }
-
-        [HttpPost("Company")]
-        public IActionResult Save(CompanyForm request)
-        {
-            string message;
-
-            if (!ModelState.IsValid)
-            {
-                message = "회사 정보 설정 값 저장에 실패";
-                _logger.LogWarning(message);
-                TempData["ErrorMessages"] = message;
-                return View(_ViewPath, request);
-            }
-
-            Func.SaveConfig(request, _configRepository.Replace);
-
-            message = "회사 정보 설정 값이 정상적으로 저장되었습니다.";
-            _logger.LogInformation(message);
-            TempData["SuccessMessage"] = message;
-            return RedirectToAction("Index");
-        }
-
-        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
-        public IActionResult Error()
-        {
-            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
-        }
-    }
-}

+ 0 - 59
backend/Controllers/Setting/MetaController.cs

@@ -1,59 +0,0 @@
-using System.Diagnostics;
-using bitforum.Models;
-using bitforum.Repository;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using bitforum.Helpers;
-
-namespace bitforum.Controllers.Setting
-{
-    [Authorize]
-    [Route("Setting")]
-    public class MetaController : Controller
-    {
-        private readonly ILogger<MetaController> _logger;
-        private readonly ConfigRepository _configRepository;
-        private readonly string _ViewPath = "~/Views/Setting/Meta.cshtml";
-
-        public MetaController(ILogger<MetaController> logger, ConfigRepository configRepository)
-        {
-            _logger = logger;
-            _configRepository = configRepository;
-        }
-
-        [HttpGet("Meta")]
-        public IActionResult Index()
-        {
-            ViewBag.config = _configRepository.GetAll();
-
-            return View(_ViewPath);
-        }
-
-        [HttpPost("Meta")]
-        public IActionResult Save(MetaForm request)
-        {
-            string message;
-
-            if (!ModelState.IsValid)
-            {
-                message = "메타 태그 설정 값 저장에 실패";
-                _logger.LogWarning(message);
-                TempData["ErrorMessages"] = message;
-                return View(_ViewPath, request);
-            }
-
-            Func.SaveConfig(request, _configRepository.Replace);
-
-            message = "메타 태그 설정 값이 정상적으로 저장되었습니다.";
-            _logger.LogInformation(message);
-            TempData["SuccessMessage"] = message;
-            return RedirectToAction("Index");
-        }
-
-        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
-        public IActionResult Error()
-        {
-            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
-        }
-    }
-}

+ 0 - 59
backend/Controllers/Setting/RegisterController.cs

@@ -1,59 +0,0 @@
-using System.Diagnostics;
-using bitforum.Models;
-using bitforum.Repository;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using bitforum.Helpers;
-
-namespace bitforum.Controllers.Setting
-{
-    [Authorize]
-    [Route("Setting")]
-    public class RegisterController : Controller
-    {
-        private readonly ILogger<RegisterController> _logger;
-        private readonly ConfigRepository _configRepository;
-        private readonly string _ViewPath = "~/Views/Setting/Register.cshtml";
-
-        public RegisterController(ILogger<RegisterController> logger, ConfigRepository configRepository)
-        {
-            _logger = logger;
-            _configRepository = configRepository;
-        }
-
-        [HttpGet("Register")]
-        public IActionResult Index()
-        {
-            ViewBag.config = _configRepository.GetAll();
-
-            return View(_ViewPath);
-        }
-
-        [HttpPost("Register")]
-        public IActionResult Save(RegisterForm request)
-        {
-            string message;
-
-            if (!ModelState.IsValid)
-            {
-                message = "회원가입 설정 값 저장에 실패";
-                _logger.LogWarning(message);
-                TempData["ErrorMessages"] = message;
-                return View(_ViewPath, request);
-            }
-
-            Func.SaveConfig(request, _configRepository.Replace);
-
-            message = "회원가입 설정 값이 정상적으로 저장되었습니다.";
-            _logger.LogInformation(message);
-            TempData["SuccessMessage"] = message;
-            return RedirectToAction("Index");
-        }
-
-        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
-        public IActionResult Error()
-        {
-            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
-        }
-    }
-}

+ 0 - 92
backend/Controllers/Setting/TestController.cs

@@ -1,92 +0,0 @@
-using System.Diagnostics;
-using bitforum.Models;
-using bitforum.Repository;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using bitforum.Helpers;
-using System.Net.Mail;
-using System.Net;
-
-namespace bitforum.Controllers.Setting
-{
-    [Authorize]
-    [Route("Setting")]
-    public class TestController : Controller
-    {
-        private readonly ILogger<TestController> _logger;
-        private readonly ConfigRepository _configRepository;
-        private readonly string _ViewPath = "~/Views/Setting/Test.cshtml";
-
-        public TestController(ILogger<TestController> logger, ConfigRepository configRepository)
-        {
-            _logger = logger;
-            _configRepository = configRepository;
-        }
-
-        [HttpGet("Test")]
-        public IActionResult Index()
-        {
-            ViewBag.config = _configRepository.GetAll();
-
-            return View(_ViewPath);
-        }
-
-        [HttpPost("Test")]
-        public async Task<IActionResult> Send(string? email)
-        {
-            try
-            {
-                if (string.IsNullOrEmpty(email))
-                {
-                    TempData["ErrorMessages"] = "이메일 주소를 입력해주세요.";
-                    return RedirectToAction("Index", "Test");
-                }
-
-                var config = _configRepository.GetAll();
-                var _smtpServer = config.GetConfig("smtp_server");
-                var _smtpPort = int.Parse(config.GetConfig("smtp_port"));
-                var _fromEmail = config.GetConfig("smtp_from_email");
-                var _emailPassword = config.GetConfig("smtp_email_password");
-
-                var client = new SmtpClient(_smtpServer, _smtpPort)
-                {
-                    Credentials = new NetworkCredential(_fromEmail, _emailPassword),
-                    EnableSsl = true
-                };
-
-                var mailMessage = new MailMessage
-                {
-                    From = new MailAddress(_fromEmail),
-                    Subject = "Test E-mail",
-                    Body = "현재 이메일을 보고 있거나 받았다면 이메일이 정상적으로 수신된 것입니다.",
-                    IsBodyHtml = false
-                };
-                mailMessage.To.Add(email);
-                await client.SendMailAsync(mailMessage);
-
-                TempData["SuccessMessage"] = "이메일이 전송되었습니다.";
-                return View(_ViewPath);
-            }
-            catch (SmtpException smtpEx)
-            {
-                TempData["ErrorMessages"] = smtpEx.Message;
-                Console.WriteLine($"SMTP error: {smtpEx.Message}");
-                _logger.LogError($"SMTP error: {smtpEx.StatusCode} - {smtpEx.Message}");
-                return RedirectToAction("Index", "Test");
-            }
-            catch (Exception ex)
-            {
-                TempData["ErrorMessages"] = ex.Message;
-                Console.WriteLine($"Email sending failed: {ex.Message}");
-                _logger.LogError($"Failed to send email to {email}: {ex.Message}");
-                return RedirectToAction("Index", "Test");
-            }
-        }
-
-        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
-        public IActionResult Error()
-        {
-            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
-        }
-    }
-}

+ 81 - 0
backend/Controllers/System/BasicController.cs

@@ -0,0 +1,81 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Authorization;
+using bitforum.Models;
+using bitforum.Models.User;
+using bitforum.Repository;
+using bitforum.Helpers;
+using bitforum.Services;
+
+namespace bitforum.Controllers.System
+{
+    [Authorize]
+    [Route("System")]
+    public class BasicController : Controller
+    {
+        private readonly ILogger<BasicController> _logger;
+        private readonly IConfigRepository _configRepository;
+        private readonly IFileUploadService _fileUploadService;
+        private readonly ISetupService _setupService;
+        private readonly UserManager<ApplicationUser> _userManager;
+        private readonly string _ViewPath = "~/Views/System/Basic.cshtml";
+
+        public BasicController(ILogger<BasicController> logger, IConfigRepository configRepository, IFileUploadService fileUploadService, ISetupService setupService, UserManager<ApplicationUser> userManager)
+        {
+            _logger = logger;
+            _configRepository = configRepository;
+            _fileUploadService = fileUploadService;
+            _setupService = setupService;
+            _userManager = userManager;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Basic")]
+        public IActionResult Index()
+        {
+            ViewBag.Config = _configRepository.GetAll();
+
+            // 최고 관리자
+            ViewBag.Admin = _userManager.GetUsersInRoleAsync("Admin").Result?.ToList();
+
+            return View(_ViewPath);
+        }
+
+        [HttpPost("Basic")]
+        public IActionResult Save(BasicForm request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("기본 설정 값 저장에 실패");
+                }
+
+                var config = _configRepository.GetAll();
+
+                request.BlockAlertContent = _fileUploadService.UploadEditorAsync(request.BlockAlertContent, config.GetConfig("block_alert_content"), UploadFolder.Basic, 1).Result;
+                request.MaintenanceContent = _fileUploadService.UploadEditorAsync(request.MaintenanceContent, config.GetConfig("maintenance_content"), UploadFolder.Basic, 2).Result;
+
+                Functions.SaveConfig(request, _configRepository.Replace);
+                _setupService.RefreshConfigAsync().Wait();
+
+                string message = "기본 설정 값이 정상적으로 저장되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Index();
+        }
+    }
+}

+ 77 - 0
backend/Controllers/System/CompanyController.cs

@@ -0,0 +1,77 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Authorization;
+using bitforum.Models;
+using bitforum.Repository;
+using bitforum.Helpers;
+using bitforum.Constants;
+using bitforum.Services;
+
+namespace bitforum.Controllers.System
+{
+    [Authorize]
+    [Route("System")]
+    public class CompanyController : Controller
+    {
+        private readonly ILogger<CompanyController> _logger;
+        private readonly IConfigRepository _configRepository;
+        private readonly ISetupService _setupService;
+        private readonly string _ViewPath = "~/Views/System/Company.cshtml";
+
+        public CompanyController(ILogger<CompanyController> logger, IConfigRepository configRepository, ISetupService setupService)
+        {
+            _logger = logger;
+            _configRepository = configRepository;
+            _setupService = setupService;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Company")]
+        public IActionResult Index()
+        {
+            var config = _configRepository.GetAll();
+
+            ViewBag.Config = config;
+            ViewBag.BankCodes = BankCodeData.List.Select(item => new SelectListItem
+            {
+                Value = item.Value,
+                Text = item.Text,
+                Selected = item.Value == config.GetConfig("company_bank_code")
+            }).ToList();
+
+            return View(_ViewPath);
+        }
+
+        [HttpPost("Company")]
+        public IActionResult Save(CompanyForm request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("회사 정보 설정 값 저장에 실패");
+                }
+
+                Functions.SaveConfig(request, _configRepository.Replace);
+                _setupService.RefreshConfigAsync().Wait();
+
+                string message = "회사 정보 설정 값이 정상적으로 저장되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Index();
+        }
+    }
+}

+ 9 - 9
backend/Controllers/Setting/EnvsController.cs → backend/Controllers/System/EnvsController.cs

@@ -3,10 +3,10 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using bitforum.Models;
 
-namespace bitforum.Controllers.Setting
+namespace bitforum.Controllers.System
 {
     [Authorize]
-    [Route("Setting")]
+    [Route("System")]
     public class EnvsController : Controller
     {
         private readonly ILogger<EnvsController> _logger;
@@ -18,18 +18,18 @@ namespace bitforum.Controllers.Setting
             _env = env;
         }
 
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
         [HttpGet("Envs")]
         public IActionResult Index()
         {
             ViewBag.EnvVars = Environment.GetEnvironmentVariables(); ;
 
-            return View("~/Views/Setting/Envs.cshtml");
-        }
-
-        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
-        public IActionResult Error()
-        {
-            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+            return View("~/Views/System/Envs.cshtml");
         }
     }
 }

+ 67 - 0
backend/Controllers/System/MetaController.cs

@@ -0,0 +1,67 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using bitforum.Models;
+using bitforum.Helpers;
+using bitforum.Repository;
+using bitforum.Services;
+
+namespace bitforum.Controllers.System
+{
+    [Authorize]
+    [Route("System")]
+    public class MetaController : Controller
+    {
+        private readonly ILogger<MetaController> _logger;
+        private readonly IConfigRepository _configRepository;
+        private readonly ISetupService _setupService;
+        private readonly string _ViewPath = "~/Views/System/Meta.cshtml";
+
+        public MetaController(ILogger<MetaController> logger, IConfigRepository configRepository, ISetupService setupService)
+        {
+            _logger = logger;
+            _configRepository = configRepository;
+            _setupService = setupService;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Meta")]
+        public IActionResult Index()
+        {
+            ViewBag.Config = _configRepository.GetAll();
+
+            return View(_ViewPath);
+        }
+
+        [HttpPost("Meta")]
+        public IActionResult Save(MetaForm request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("메타 태그 설정 값 저장에 실패");
+                }
+
+                Functions.SaveConfig(request, _configRepository.Replace);
+                _setupService.RefreshConfigAsync().Wait();
+
+                string message = "메타 태그 설정 값이 정상적으로 저장되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Index();
+        }
+    }
+}

+ 67 - 0
backend/Controllers/System/RegisterController.cs

@@ -0,0 +1,67 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using bitforum.Models;
+using bitforum.Repository;
+using bitforum.Helpers;
+using bitforum.Services;
+
+namespace bitforum.Controllers.System
+{
+    [Authorize]
+    [Route("System")]
+    public class RegisterController : Controller
+    {
+        private readonly ILogger<RegisterController> _logger;
+        private readonly IConfigRepository _configRepository;
+        private readonly ISetupService _setupService;
+        private readonly string _ViewPath = "~/Views/System/Register.cshtml";
+
+        public RegisterController(ILogger<RegisterController> logger, IConfigRepository configRepository, ISetupService setupService)
+        {
+            _logger = logger;
+            _configRepository = configRepository;
+            _setupService = setupService;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Register")]
+        public IActionResult Index()
+        {
+            ViewBag.Config = _configRepository.GetAll();
+
+            return View(_ViewPath);
+        }
+
+        [HttpPost("Register")]
+        public IActionResult Save(RegisterForm request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("회원가입 설정 값 저장에 실패");
+                }
+
+                Functions.SaveConfig(request, _configRepository.Replace);
+                _setupService.RefreshConfigAsync().Wait();
+
+                string message = "회원가입 설정 값이 정상적으로 저장되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Index();
+        }
+    }
+}

+ 12 - 12
backend/Controllers/Setting/ServerController.cs → backend/Controllers/System/ServerController.cs

@@ -1,14 +1,14 @@
 using System.Diagnostics;
-using bitforum.Models;
+using System.Management;
+using System.Runtime.InteropServices;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
-using System.Runtime.InteropServices;
-using System.Management;
+using bitforum.Models;
 
-namespace bitforum.Controllers.Setting
+namespace bitforum.Controllers.System
 {
     [Authorize]
-    [Route("Setting")]
+    [Route("System")]
     public class ServerController : Controller
     {
         private readonly ILogger<ServerController> _logger;
@@ -20,6 +20,12 @@ namespace bitforum.Controllers.Setting
             _env = env;
         }
 
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
         [HttpGet("Server")]
         public IActionResult Index()
         {
@@ -63,13 +69,7 @@ namespace bitforum.Controllers.Setting
 
             ViewBag.Info = info;
 
-            return View("~/Views/Setting/Server.cshtml");
-        }
-
-        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
-        public IActionResult Error()
-        {
-            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+            return View("~/Views/System/Server.cshtml");
         }
     }
 }

+ 77 - 0
backend/Controllers/System/TemplateController.cs

@@ -0,0 +1,77 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using bitforum.Models;
+using bitforum.Repository;
+using bitforum.Helpers;
+using bitforum.Services;
+
+namespace bitforum.Controllers.System
+{
+    [Authorize]
+    [Route("System")]
+    public class TemplateController : Controller
+    {
+        private readonly ILogger<TemplateController> _logger;
+        private readonly IConfigRepository _configRepository;
+        private readonly IFileUploadService _fileUploadService;
+        private readonly ISetupService _setupService;
+
+        public TemplateController(ILogger<TemplateController> logger, IConfigRepository configRepository, IFileUploadService fileUploadService, ISetupService setupService)
+        {
+            _logger = logger;
+            _configRepository = configRepository;
+            _fileUploadService = fileUploadService;
+            _setupService = setupService;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Template/Email")]
+        public IActionResult Index()
+        {
+            ViewBag.Config = _configRepository.GetAll();
+
+            return View("~/Views/System/Template.cshtml");
+        }
+
+        [HttpPost("Template")]
+        public IActionResult Save(EmailTemplate request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("알림 양식 설정 값 저장에 실패");
+                }
+
+                var config = _configRepository.GetAll();
+
+                request.RegisterEmailFormContent = _fileUploadService.UploadEditorAsync(request.RegisterEmailFormContent, config.GetConfig("register_email_form_content"), UploadFolder.Template).Result;
+                request.RegistrationEmailFormContent = _fileUploadService.UploadEditorAsync(request.RegistrationEmailFormContent, config.GetConfig("registration_email_form_content"), UploadFolder.Template).Result;
+                request.ResetPasswordEmailFormContent = _fileUploadService.UploadEditorAsync(request.ResetPasswordEmailFormContent, config.GetConfig("reset_password_email_form_content"), UploadFolder.Template).Result;
+                request.ChangedPasswordEmailFormContent = _fileUploadService.UploadEditorAsync(request.ChangedPasswordEmailFormContent, config.GetConfig("changed_password_email_form_content"), UploadFolder.Template).Result;
+                request.WithdrawEmailFormContent = _fileUploadService.UploadEditorAsync(request.WithdrawEmailFormContent, config.GetConfig("withdraw_email_form_content"), UploadFolder.Template).Result;
+                request.EmailVerifyFormContent = _fileUploadService.UploadEditorAsync(request.EmailVerifyFormContent, config.GetConfig("email_verify_form_content"), UploadFolder.Template).Result;
+                request.ChangedEmailFormContent = _fileUploadService.UploadEditorAsync(request.ChangedEmailFormContent, config.GetConfig("changed_email_form_content"), UploadFolder.Template).Result;
+
+                Functions.SaveConfig(request, _configRepository.Replace);
+                _setupService.RefreshConfigAsync().Wait();
+
+                string message = "알림 양식 설정 값이 정상적으로 저장되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e) {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e, e.Message);
+            }
+
+            return Index();
+        }
+    }
+}

+ 74 - 0
backend/Controllers/System/TestController.cs

@@ -0,0 +1,74 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Authorization;
+using bitforum.Models;
+using bitforum.Repository;
+using bitforum.Services;
+
+namespace bitforum.Controllers.System
+{
+    [Authorize]
+    [Route("System")]
+    public class TestController : Controller
+    {
+        private readonly ILogger<TestController> _logger;
+        private readonly IConfigRepository _configRepository;
+        private readonly IMailService _mailService;
+        private readonly string _ViewPath = "~/Views/System/Test.cshtml";
+
+        public TestController(ILogger<TestController> logger, IConfigRepository configRepository, IMailService mailService)
+        {
+            _logger = logger;
+            _configRepository = configRepository;
+            _mailService = mailService;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Test")]
+        public IActionResult Index()
+        {
+            ViewBag.Config = _configRepository.GetAll();
+
+            return View(_ViewPath);
+        }
+
+        [HttpPost("Test")]
+        public async Task<IActionResult> Send(string? toAddress)
+        {
+            try
+            {
+                if (string.IsNullOrEmpty(toAddress))
+                {
+                    TempData["ErrorMessages"] = "이메일 주소를 입력해주세요.";
+                    return Index();
+                }
+
+                string subject = "Test E-mail";
+                string content = "현재 이메일을 보고 있거나 받았다면 이메일이 정상적으로 수신된 것입니다.";
+
+                await _mailService.SendEmailAsync(new SendData
+                {
+                    ToAddress = toAddress,
+                    Subject = subject,
+                    Message = content
+                });
+
+                string message = "이메일이 전송되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                TempData["ErrorMessages"] = e.Message;
+                _logger.LogError(e.Message);
+            }
+
+            return Index();
+        }
+    }
+}

+ 17 - 0
backend/DTOs/Request/ChangeEmailDto.cs

@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.DTOs.Request
+{
+    /// <summary>
+    /// 이메일 변경 요청 DTO
+    /// </summary>
+    public class ChangeEmailDto
+    {
+        [Required(ErrorMessage = "회원 ID를 입력해주세요.")]
+        public required int ID { get; set; }
+
+        [Required(ErrorMessage = "이메일을 입력해주세요.")]
+        [StringLength(60, ErrorMessage = "이메일은 60자 이내로 입력해주세요.")]
+        public required string Email { get; set; }
+    }
+}

+ 17 - 0
backend/DTOs/Request/ChangeNicknameDto.cs

@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.DTOs.Request
+{
+    /// <summary>
+    /// 별명 변경 요청 DTO
+    /// </summary>
+    public class ChangeNicknameDto
+    {
+        [Required(ErrorMessage = "회원 ID를 입력해주세요.")]
+        public required int ID { get; set; }
+
+        [Required(ErrorMessage = "별명을 입력해주세요.")]
+        [StringLength(20, ErrorMessage = "별명은 20자 이내로 입력해주세요.")]
+        public required string Name { get; set; }
+    }
+}

+ 24 - 0
backend/DTOs/Request/ChangePasswordDto.cs

@@ -0,0 +1,24 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.DTOs.Request
+{
+    /// <summary>
+    /// 비밀번호 변경 DTO
+    /// </summary>
+    public class ChangePasswordDto
+    {
+        [Required(ErrorMessage = "현재 비밀번호를 입력해주세요.")]
+        [DataType(DataType.EmailAddress)]
+        public required string CurrentPassword { get; set; }
+
+        [Required(ErrorMessage = "새 비밀번호를 입력해주세요.")]
+        [MaxLength(255, ErrorMessage = "새 비밀번호를 확인해주세요.")]
+        [DataType(DataType.Password)]
+        public required string NewPassword { get; set; }
+
+        [Required(ErrorMessage = "새 비밀번호 확인을 입력해주세요.")]
+        [Compare("NewPassword", ErrorMessage = "새 비밀번호가 일치하지 않습니다.")]
+        [DataType(DataType.Password)]
+        public required string ConfirmPassword { get; set; }
+    }
+}

+ 18 - 0
backend/DTOs/Request/LoginDto.cs

@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.DTOs.Request
+{
+    /// <summary>
+    /// 로그인 요청 DTO
+    /// </summary>
+    public class LoginDto
+    {
+        [Required(ErrorMessage = "이메일을 입력해주세요.")]
+        [DataType(DataType.EmailAddress)]
+        public required string Email { get; set; }
+
+        [Required(ErrorMessage = "비밀번호를 입력해주세요.")]
+        [DataType(DataType.Password)]
+        public required string Password { get; set; }
+    }
+}

+ 29 - 0
backend/DTOs/Request/RegisterDto.cs

@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+using bitforum.Attributes;
+
+namespace bitforum.DTOs.Request
+{
+    /// <summary>
+    /// 회원가입 요청 DTO
+    /// </summary>
+    public class RegisterDto
+    {
+        [Required(ErrorMessage = "이메일을 입력해주세요.")]
+        [MaxLength(60, ErrorMessage = "이메일 주소는 60자 이하로 입력 가능합니다.")]
+        [DataType(DataType.EmailAddress)]
+        public required string Email { get; set; }
+
+        [Required(ErrorMessage = "비밀번호를 입력해주세요.")]
+        [MaxLength(255, ErrorMessage = "비밀번호를 확인해주세요.")]
+        [DataType(DataType.Password)]
+        public required string Password { get; set; }
+
+        [Required(ErrorMessage = "이용약관에 동의해주세요.")]
+        [MustBeTrue(ErrorMessage = "이용약관에 동의해야 합니다.")]
+        public required bool IsPolicyAgree { get; set; }
+
+        [Required(ErrorMessage = "개인정보처리방침에 동의해주세요.")]
+        [MustBeTrue(ErrorMessage = "개인정보처리방침에 동의해야 합니다.")]
+        public required bool IsPrivacyAgree { get; set; }
+    }
+}

+ 19 - 0
backend/DTOs/Request/ResendVerifyNumberDto.cs

@@ -0,0 +1,19 @@
+using bitforum.Constants;
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.DTOs.Request
+{
+    /// <summary>
+    /// 인증번호 다시보내기 DTO
+    /// </summary>
+    public class ResendVerifyNumberDto
+    {
+        [Required(ErrorMessage = "이메일을 입력해주세요.")]
+        [DataType(DataType.EmailAddress)]
+        public required string Email { get; set; }
+
+        [Required(ErrorMessage = "인증 구분을 입력해주세요.")]
+        [EnumDataType(typeof(VerificationType), ErrorMessage = "올바른 인증 구분을 입력해주세요.")]
+        public required VerificationType Type { get; set; }
+    }
+}

+ 24 - 0
backend/DTOs/Request/ResetPasswordDto.cs

@@ -0,0 +1,24 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.DTOs.Request
+{
+    /// <summary>
+    /// 비밀번호 재설정 DTO
+    /// </summary>
+    public class ResetPasswordDto
+    {
+        [Required(ErrorMessage = "이메일을 입력해주세요.")]
+        [DataType(DataType.EmailAddress)]
+        public required string Email { get; set; }
+
+        [Required(ErrorMessage = "비밀번호를 입력해주세요.")]
+        [MaxLength(255, ErrorMessage = "비밀번호를 확인해주세요.")]
+        [DataType(DataType.Password)]
+        public required string Password { get; set; }
+
+        [Required(ErrorMessage = "비밀번호 확인을 입력해주세요.")]
+        [Compare("Password", ErrorMessage = "비밀번호가 일치하지 않습니다.")]
+        [DataType(DataType.Password)]
+        public required string RePassword { get; set; }
+    }
+}

+ 23 - 0
backend/DTOs/Request/VerificationNumberDto.cs

@@ -0,0 +1,23 @@
+using bitforum.Constants;
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.DTOs.Request
+{
+    /// <summary>
+    /// 인증번호 확인 DTO
+    /// </summary>
+    public class VerificationNumberDto
+    {
+        [Required(ErrorMessage = "이메일을 입력해주세요.")]
+        [DataType(DataType.EmailAddress)]
+        public required string Email { get; set; }
+
+        [Required(ErrorMessage = "인증번호를 입력해주세요.")]
+        [StringLength(6, ErrorMessage = "인증번호는 6자리 입니다.")]
+        public required string Code { get; set; }
+
+        [Required(ErrorMessage = "인증 구분을 입력해주세요.")]
+        [EnumDataType(typeof(VerificationType), ErrorMessage = "올바른 인증 구분을 입력해주세요.")]
+        public required VerificationType Type { get; set; }
+    }
+}

+ 24 - 0
backend/DTOs/Response/ResultDto.cs

@@ -0,0 +1,24 @@
+/// <summary>
+/// 응답 DTO 모음
+/// </summary>
+namespace bitforum.DTOs.Response
+{
+    public record ResultDto
+    {
+        public bool Ok { get; set; } = true;
+        public int Status { get; set; } = StatusCodes.Status200OK;
+        public string? Message { get; set; } = null;
+        public object? Data { get; set; } = null;
+        public Dictionary<string, List<string>>? Errors { get; set; } = null;
+
+        public ResultDto() { } // 기본 생성자
+
+        public ResultDto(bool ok, int status, string? message = "", object? data = null, Dictionary<string, List<string>>? errors = null)
+        {
+            Ok = ok;
+            Status = status;
+            Message = message;
+            Data = data;
+        }
+    }
+}

+ 39 - 1
backend/Database/DefaultDbContext.cs

@@ -3,7 +3,9 @@ using bitforum.Models;
 using bitforum.Models.Page;
 using bitforum.Models.Page.Faq;
 using bitforum.Models.Page.Banner;
-using bitforum.Models.User;
+using bitforum.Models.Account;
+using bitforum.Models.Log;
+using bitforum.Models.BBS;
 
 public class DefaultDbContext : DbContext
 {
@@ -22,4 +24,40 @@ public class DefaultDbContext : DbContext
     public DbSet<Member> Member { get; set; }
     public DbSet<MemberApprove> MemberApprove { get; set; }
     public DbSet<MemberGrade> MemberGrade { get; set; }
+    public DbSet<RefreshToken> RefreshToken { get; set; }
+    public DbSet<EmailVerifyToken> EmailVerifyToken { get; set; }
+    public DbSet<EmailVerifyNumber> EmailVerifyNumber { get; set; }
+    public DbSet<LoginLog> LoginLog { get; set; }
+    public DbSet<EmailLog> EmailLog { get; set; }
+    public DbSet<EmailChangeLog> EmailChangeLog { get; set; }
+    public DbSet<NameChangeLog> NameChangeLog { get; set; }
+
+    // 게시판
+    public DbSet<BoardGroup> BoardGroup { get; set; }
+    public DbSet<Board> Board { get; set; }
+    public DbSet<BoardMeta> BoardMeta { get; set; }
+    public DbSet<Post> Post { get; set; }
+    public DbSet<PostMeta> PostMeta { get; set; }
+    public DbSet<Comment> Comment { get; set; }
+    public DbSet<CommentMeta> CommentMeta { get; set; }
+
+    protected override void OnModelCreating(ModelBuilder modelBuilder)
+    {
+        // 삭제 시 SET NULL 1:N 관계
+        modelBuilder.Entity<LoginLog>().HasOne(e => e.Member).WithMany(e => e.LoginLog).HasForeignKey(e => e.MemberID).OnDelete(DeleteBehavior.SetNull);
+        modelBuilder.Entity<EmailLog>().HasOne(e => e.Member).WithMany(e => e.EmailLog).HasForeignKey(e => e.MemberID).OnDelete(DeleteBehavior.SetNull);
+
+        // 회원 - 회원 기록들 1:N 관계
+        modelBuilder.Entity<EmailChangeLog>().HasOne(e => e.Member).WithMany(e => e.EmailChangeLog).HasForeignKey(e => e.MemberID).OnDelete(DeleteBehavior.NoAction);
+        modelBuilder.Entity<NameChangeLog>().HasOne(e => e.Member).WithMany(e => e.NameChangeLog).HasForeignKey(e => e.MemberID).OnDelete(DeleteBehavior.NoAction);
+
+        // 회원 - 회원등급 1:1 관계
+        modelBuilder.Entity<MemberApprove>().HasOne(e => e.Member).WithOne(e => e.MemberApprove).HasForeignKey<MemberApprove>(e => e.MemberID).OnDelete(DeleteBehavior.NoAction); // 1:1
+
+        // 게시판 관계 설정
+        modelBuilder.Entity<Board>().HasOne(e => e.BoardGroup) .WithMany(e => e.Board).HasForeignKey(e => e.BoardGroupID).OnDelete(DeleteBehavior.NoAction); // 게시판 분류-게시판 관계
+        modelBuilder.Entity<Post>().HasOne(e => e.Board).WithMany(e => e.Post).HasForeignKey(e => e.BoardID).OnDelete(DeleteBehavior.NoAction); // 게시판-게시글 관계
+        modelBuilder.Entity<Comment>().HasOne(e => e.Post).WithMany(e => e.Comment).HasForeignKey(e => e.PostID).OnDelete(DeleteBehavior.NoAction); // 게시판-댓글 관계
+        modelBuilder.Entity<Comment>().HasOne(e => e.Parent).WithMany(e => e.Reply).HasForeignKey(e => e.ParentID).OnDelete(DeleteBehavior.NoAction); // 댓글 부모-자식 관계
+    }
 }

+ 94 - 0
backend/Extensions/ServiceExtensions.cs

@@ -0,0 +1,94 @@
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.IdentityModel.Tokens;
+using System.Text;
+using Microsoft.AspNetCore.Identity;
+using bitforum.Models;
+using bitforum.Models.User;
+
+namespace bitforum.Extensions
+{
+    public static class ServiceExtensions
+    {
+        // CORS 구성
+        public static void ConfigureCors(this IServiceCollection services, IConfiguration configuration)
+        {
+            services.AddCors(options =>
+            {
+                options.AddPolicy("AllowFrontend", builder =>
+                {
+                    builder.WithOrigins("https://localhost:3000").AllowAnyHeader().AllowAnyMethod().AllowCredentials();
+                });
+            });
+        }
+
+        // 관리자단 비밀번호 정책 구성
+        public static void ConfigureIdentity(this IServiceCollection services, IConfiguration configuration)
+        {
+            services.AddIdentity<ApplicationUser, IdentityRole>(options =>
+            {
+                options.SignIn.RequireConfirmedAccount = true; // 이메일 확인 비활성화
+
+                // Password settings.
+                options.Password.RequireDigit = true;
+                options.Password.RequireLowercase = true;
+                options.Password.RequireNonAlphanumeric = true;
+                options.Password.RequireUppercase = true;
+                options.Password.RequiredLength = 6;
+                options.Password.RequiredUniqueChars = 1;
+
+                // Lockout settings.
+                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
+                options.Lockout.MaxFailedAccessAttempts = 5;
+                options.Lockout.AllowedForNewUsers = true;
+
+                // User settings.
+                options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
+                options.User.RequireUniqueEmail = false;
+            })
+            .AddEntityFrameworkStores<UserContext>() // 사용자 계정 저장소
+            .AddDefaultUI() // 기본 UI 사용
+            .AddDefaultTokenProviders(); // 기본 토큰 제공자 사용
+
+            services.ConfigureApplicationCookie(options =>
+            {
+                // Cookie settings
+                options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
+                options.LoginPath = "/Identity/Account/Login";
+                options.AccessDeniedPath = "/Identity/Account/AccessDenied";
+                options.SlidingExpiration = true;
+            });
+        }
+
+        // JWT 전달자 인증 구성
+        public static void ConfigureAuthentication(this IServiceCollection services, IConfiguration configuration)
+        {
+            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
+            {
+                options.TokenValidationParameters = new TokenValidationParameters
+                {
+                    ValidateIssuerSigningKey = true,
+                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Setting.Jwt.SecretKey)),
+                    ValidateIssuer = true,
+                    ValidateAudience = true,
+                    ValidIssuer = Setting.Jwt.Issuer,
+                    ValidAudience = Setting.Jwt.Audience,
+                    ValidateLifetime = true,
+                    ClockSkew = TimeSpan.Zero
+                };
+
+                options.Events = new JwtBearerEvents
+                {
+                    OnMessageReceived = context =>
+                    {
+                        if (context.Request.Cookies.ContainsKey("accessToken"))
+                        {
+                            context.Token = context.Request.Cookies["accessToken"];
+                        }
+                        return Task.CompletedTask;
+                    }
+                };
+            });
+        }
+    }
+}

+ 22 - 0
backend/Extensions/UserExtension.cs

@@ -0,0 +1,22 @@
+using System.Security.Claims;
+
+namespace bitforum.Extensions
+{
+    public static class UserExtension
+    {
+        public static int GetID(this ClaimsPrincipal user)
+        {
+            return int.TryParse(user.FindFirst(ClaimTypes.NameIdentifier)?.Value, out var id) ? id : 0;
+        }
+
+        public static string? GetEmail(this ClaimsPrincipal user)
+        {
+            return user.FindFirst(ClaimTypes.Email)?.Value;
+        }
+
+        public static string? GetName(this ClaimsPrincipal user)
+        {
+            return user.FindFirst(ClaimTypes.Name)?.Value;
+        }
+    }
+}

+ 0 - 52
backend/Helpers/Func.cs

@@ -1,52 +0,0 @@
-using bitforum.Models;
-using Microsoft.AspNetCore.Mvc;
-using System.ComponentModel.DataAnnotations;
-using System.Reflection;
-
-namespace bitforum.Helpers
-{
-    public static class Func
-    {
-        // SaveConfig 함수는 Config 모델에 대한 정보를 저장하는 함수
-        public static void SaveConfig<T>(T request, Action<Config> saveAction)
-        {
-            var properties = typeof(T).GetProperties();
-
-            foreach (var property in properties)
-            {
-                // 속성 Key 이름
-                var key = property.GetCustomAttribute<BindPropertyAttribute>()?.Name ?? property.Name;
-
-                // 속성 값
-                var value = property.GetValue(request)?.ToString();
-
-                // 속성 표시 이름
-                var displayName = property.GetCustomAttribute<DisplayAttribute>()?.Name ?? key;
-
-                saveAction(new Config
-                {
-                    Key = key,
-                    Value = value?.ToString(),
-                    Description = displayName
-                });
-            }
-        }
-
-        // Config 값 조회
-        public static string GetConfig(this Dictionary<string, string> dictionary, string key)
-        {
-            if (dictionary != null && dictionary.ContainsKey(key))
-            {
-                return dictionary[key];
-            }
-
-            return string.Empty;
-        }
-
-        // 날짜 포맷 조회
-        public static string GetDateAt(this DateTime? dateTime, string format = "yyyy.MM.dd HH:mm:ss")
-        {
-            return dateTime.HasValue ? dateTime.Value.ToString(format) : string.Empty;
-        }
-    }
-}

+ 125 - 0
backend/Helpers/Functions.cs

@@ -0,0 +1,125 @@
+using System.ComponentModel.DataAnnotations;
+using System.Reflection;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using bitforum.Models;
+
+namespace bitforum.Helpers
+{
+    public static class Functions
+    {
+        // SaveConfig 함수는 Config 모델에 대한 정보를 저장하는 함수
+        public static void SaveConfig<T>(T request, Action<Config> saveAction)
+        {
+            var properties = typeof(T).GetProperties();
+
+            foreach (var property in properties)
+            {
+                var key = (property.GetCustomAttribute<BindPropertyAttribute>()?.Name ?? property.Name); // 속성 Key 이름
+                var value = property.GetValue(request)?.ToString(); // 속성 값
+                var displayName = (property.GetCustomAttribute<DisplayAttribute>()?.Name ?? key); // 속성 표시 이름
+
+                saveAction(new Config
+                {
+                    Key = key,
+                    Value = value,
+                    Description = displayName
+                });
+            }
+        }
+
+        // Config 값 조회
+        public static string? GetConfig(this Dictionary<string, string> config, string k, string? d = null)
+        {
+            return config.TryGetValue(k, out var v) ? v : d;
+        }
+
+        // 날짜 포맷 조회
+        public static string GetDateAt(this DateTime? dateTime, string format = "yyyy.MM.dd HH:mm:ss") => dateTime?.ToString(format) ?? string.Empty;
+        public static string GetDateAt(this DateTime dateTime, string format = "yyyy.MM.dd HH:mm:ss") => dateTime.ToString(format);
+        
+        // ModelState 오류 조회
+        public static Dictionary<string, List<string>> GetErrors(this ModelStateDictionary modelState)
+        {
+            return modelState.Where(ms => ms.Value.Errors.Any()).ToDictionary(
+                ms => ms.Key,
+                ms => ms.Value.Errors.Select(e => e.ErrorMessage).ToList()
+            );
+        }
+
+        // Client IP 조회
+        public static string GetClientIP(this HttpContext context)
+        {
+            return context.Request.Headers["X-Forwarded-For"].FirstOrDefault() ?? context.Connection.RemoteIpAddress?.ToString() ?? "Unknown";
+        }
+
+        // Dictionary 데이터를 특정 객체에 자동 매핑
+        public static T Mapping<T>(Dictionary<string, string> dict) where T : new()
+        {
+            var obj = new T();
+            var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+            foreach (var prop in properties)
+            {
+                var key = prop.GetCustomAttribute<BindPropertyAttribute>()?.Name ?? prop.Name;
+
+                if (dict.TryGetValue(key, out var value) && !string.IsNullOrEmpty(value))
+                {
+                    try
+                    {
+                        prop.SetValue(obj, ConvertValue(value, prop.PropertyType));
+                    }
+                    catch (Exception ex)
+                    {
+                        Console.WriteLine($"`{prop.Name}` 객체 변환 오류: {ex.Message}");
+                    }
+                }
+            }
+
+            return obj;
+        }
+
+        // 문자열 값을 해당 프로퍼티 타입으로 자동 변환
+        public static object? ConvertValue(string value, Type t)
+        {
+            if (string.IsNullOrEmpty(value))
+            {
+                return null;
+            }else if (Nullable.GetUnderlyingType(t) is Type underlyingType) {
+                t = underlyingType; 
+            }
+            if (t == typeof(bool) && bool.TryParse(value, out bool boolValue)) {
+                return boolValue;
+            }else if (t == typeof(byte) && byte.TryParse(value, out byte byteValue)) {
+                return byteValue;
+            }else if (t == typeof(sbyte) && sbyte.TryParse(value, out sbyte sbyteValue)) {
+                return sbyteValue;
+            }else if (t == typeof(char) && value.Length > 0) {
+                return value[0];
+            }else if (t == typeof(short) && short.TryParse(value, out short shortValue)) {
+                return shortValue;
+            }else if (t == typeof(ushort) && ushort.TryParse(value, out ushort ushortValue)) {
+                return ushortValue;
+            }else if (t == typeof(int) && int.TryParse(value, out int intValue)) {
+                return intValue;
+            }else if (t == typeof(uint) && uint.TryParse(value, out uint uintValue)) {
+                return uintValue;
+            }else  if (t == typeof(long) && long.TryParse(value, out long longValue)) {
+                return longValue;
+            }else if (t == typeof(ulong) && ulong.TryParse(value, out ulong ulongValue)) {
+                return ulongValue;
+            }else if (t == typeof(float) && float.TryParse(value, out float floatValue)) {
+                return floatValue;
+            }else if (t == typeof(double) && double.TryParse(value, out double doubleValue)) {
+                return doubleValue;
+            }else if (t == typeof(decimal) && decimal.TryParse(value, out decimal decimalValue)) {
+                return decimalValue;
+            }else if (t == typeof(DateTime) && DateTime.TryParse(value, out DateTime dateTimeValue)) {
+                return dateTimeValue;
+            }else if (t == typeof(Guid) && Guid.TryParse(value, out Guid guidValue)) {
+                return guidValue;
+            }
+            return value;
+        }
+    }
+}

+ 9 - 9
backend/Models/Pagination.cs → backend/Helpers/Pagination.cs

@@ -1,7 +1,7 @@
 using Microsoft.AspNetCore.WebUtilities;
 using System.Reflection;
 
-namespace bitforum.Models
+namespace bitforum.Helpers
 {
     public class Pagination
     {
@@ -27,15 +27,15 @@ namespace bitforum.Models
         {
             Page = page;
             PerPage = perPage;
-            TotalRows = (totalRows ?? 0);
-            TotalPage = ((int)Math.Ceiling((double)TotalRows / perPage));
+            TotalRows = totalRows ?? 0;
+            TotalPage = (int)Math.Ceiling((double)TotalRows / perPage);
 
             CurrentPageGroup = (int)Math.Ceiling((double)Page / PageGroupSize);
-            StartPage = ((CurrentPageGroup - 1) * PageGroupSize + 1);
-            EndPage = Math.Min((StartPage + PageGroupSize - 1), TotalPage);
+            StartPage = (CurrentPageGroup - 1) * PageGroupSize + 1;
+            EndPage = Math.Min(StartPage + PageGroupSize - 1, TotalPage);
 
-            GroupStartPage = (GroupStartPage - PageGroupSize);
-            GroupEndPage = (((Page - 1) / PageGroupSize + 1) * PageGroupSize);
+            GroupStartPage = GroupStartPage - PageGroupSize;
+            GroupEndPage = ((Page - 1) / PageGroupSize + 1) * PageGroupSize;
 
             PrevGroupPage = Math.Max(StartPage - PageGroupSize, 1); // 이전 페이지 그룹의 첫 페이지 계산
             NextGroupPage = Math.Min(EndPage + 1, TotalPage); // 다음 페이지 그룹의 첫 페이지 계산
@@ -45,10 +45,10 @@ namespace bitforum.Models
         }
 
         // 이전 페이지 여부
-        public bool HasPreviousPage => (Page > 1);
+        public bool HasPreviousPage => Page > 1;
 
         // 다음 페이지 여부
-        public bool HasNextPage => (Page < TotalPage);
+        public bool HasNextPage => Page < TotalPage;
 
         // 객체를 Dictionary로 변환하는 메서드
         private Dictionary<string, string> ToDictionary(object? obj)

+ 2 - 9
backend/Middleware/Common.cs

@@ -1,16 +1,10 @@
 using System.Text.Json;
 using bitforum.Constants;
 using bitforum.Models;
+using bitforum.Models.Views;
 
 namespace bitforum.Middleware
 {
-    // 환경변수 구조
-    public class AppConfig
-    {
-        public string AppName { get; set; }
-        public string AppVersion { get; set; }
-    }
-
     public class Common
     {
         private readonly RequestDelegate _next;
@@ -24,7 +18,6 @@ namespace bitforum.Middleware
 
         public async Task InvokeAsync(HttpContext context)
         {
-         
             AppConfig? appConfig = null;
 
             try
@@ -42,7 +35,7 @@ namespace bitforum.Middleware
                 AppConfig = appConfig,
 
                 // 메뉴 데이터 가져오기
-                Menus = MenuData.GetMenus()
+                Menus = Menus.GetMenus()
             };
 
             await _next(context);

+ 100 - 0
backend/Middleware/IPFilter.cs

@@ -0,0 +1,100 @@
+using System.Net;
+using System.Text.RegularExpressions;
+using bitforum.Services;
+
+namespace bitforum.Middleware
+{
+    public class IPFilter
+    {
+        private readonly RequestDelegate _next;
+        private readonly ISetupService _setupService;
+
+        public IPFilter(RequestDelegate next, ISetupService setupService)
+        {
+            _next = next;
+            _setupService = setupService;
+        }
+
+        public async Task Invoke(HttpContext context)
+        {
+            var remoteIP = context.Connection.RemoteIpAddress;
+
+            // IP가 허용되지 않으면 403 Forbidden 반환
+            if (!IsIpAllowed(remoteIP))
+            {
+                context.Response.StatusCode = StatusCodes.Status403Forbidden;
+                await context.Response.WriteAsync("Access Denied: Your IP is not allowed to access this resource.");
+                return;
+            }
+
+            await _next(context);
+        }
+
+        private bool IsIpAllowed(IPAddress? remoteIP)
+        {
+            if (remoteIP == null)
+            {
+                return false;
+            }
+
+            var adminWhiteIpList = _setupService.GetConfig("admin_white_ip_list");
+            var allowedIPs = string.IsNullOrWhiteSpace(adminWhiteIpList) ? new List<string>() : adminWhiteIpList.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).Select(ip => ip.Trim()).ToList();
+
+            // 허용된 IP가 없으면 기본적으로 모든 요청 허용
+            if (!allowedIPs.Any())
+            {
+                return true;
+            }
+
+            var ipString = remoteIP.ToString();
+
+            foreach (var allowedIP in allowedIPs)
+            {
+                if (allowedIP.Contains("*"))
+                {
+                    var regex = "^" + Regex.Escape(allowedIP).Replace("\\*", ".*") + "$";
+                    if (Regex.IsMatch(ipString, regex))
+                    {
+                        return true;
+                    }
+                }
+                else if (allowedIP.Contains("-"))
+                {
+                    var parts = allowedIP.Split('-');
+                    if (parts.Length == 2 && IPAddress.TryParse(parts[0].Trim(), out var startIp) && IPAddress.TryParse(parts[1].Trim(), out var endIp) && IsInRange(remoteIP, startIp, endIp))
+                    {
+                        return true;
+                    }
+                }
+                else if (IPAddress.TryParse(allowedIP, out var allowedAddress) && remoteIP.Equals(allowedAddress))
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        private bool IsInRange(IPAddress IP, IPAddress startIP, IPAddress endIP)
+        {
+            var ipBytes = IP.GetAddressBytes();
+            var startBytes = startIP.GetAddressBytes();
+            var endBytes = endIP.GetAddressBytes();
+
+            if (ipBytes.Length != startBytes.Length || ipBytes.Length != endBytes.Length)
+            {
+                return false;
+            }
+
+            for (int i = 0; i < ipBytes.Length; i++)
+            {
+                if (ipBytes[i] < startBytes[i] || ipBytes[i] > endBytes[i])
+                {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+    }
+}

+ 0 - 58
backend/Migrations/DefaultDb/20250111031413_AddConfigTable.Designer.cs

@@ -1,58 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250111031413_AddConfigTable")]
-    partial class AddConfigTable
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 44
backend/Migrations/DefaultDb/20250111031413_AddConfigTable.cs

@@ -1,44 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class AddConfigTable : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.CreateTable(
-                name: "Config",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false)
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    Key = table.Column<string>(type: "nvarchar(450)", nullable: false),
-                    Value = table.Column<string>(type: "nvarchar(max)", nullable: true),
-                    Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Config", x => x.ID);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Config_Key",
-                table: "Config",
-                column: "Key",
-                unique: true);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "Config");
-        }
-    }
-}

+ 0 - 58
backend/Migrations/DefaultDb/20250115122254_initial.Designer.cs

@@ -1,58 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250115122254_initial")]
-    partial class initial
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 203
backend/Migrations/DefaultDb/20250117130258_AddFaqs.Designer.cs

@@ -1,203 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250117130258_AddFaqs")]
-    partial class AddFaqs
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsDisplay")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 124
backend/Migrations/DefaultDb/20250117130258_AddFaqs.cs

@@ -1,124 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class AddFaqs : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.CreateTable(
-                name: "Document",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false)
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    IsDisplay = table.Column<bool>(type: "bit", nullable: false),
-                    Code = table.Column<string>(type: "nvarchar(30)", maxLength: 30, nullable: false),
-                    Subject = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false),
-                    Content = table.Column<string>(type: "nvarchar(max)", nullable: true),
-                    Views = table.Column<int>(type: "int", nullable: false),
-                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Document", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "FaqCategory",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false)
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    Code = table.Column<string>(type: "nvarchar(30)", maxLength: 30, nullable: false),
-                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
-                    Content = table.Column<string>(type: "nvarchar(max)", nullable: true),
-                    Order = table.Column<int>(type: "int", nullable: false),
-                    IsActive = table.Column<bool>(type: "bit", nullable: false),
-                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_FaqCategory", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "FaqItem",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false)
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    CategoryID = table.Column<int>(type: "int", nullable: false),
-                    Question = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
-                    Answer = table.Column<string>(type: "nvarchar(max)", nullable: true),
-                    Order = table.Column<int>(type: "int", nullable: false),
-                    IsActive = table.Column<bool>(type: "bit", nullable: false),
-                    Views = table.Column<int>(type: "int", nullable: false),
-                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_FaqItem", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_FaqItem_FaqCategory_CategoryID",
-                        column: x => x.CategoryID,
-                        principalTable: "FaqCategory",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Document_Code",
-                table: "Document",
-                column: "Code",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_FaqCategory_Code",
-                table: "FaqCategory",
-                column: "Code",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_FaqCategory_Order",
-                table: "FaqCategory",
-                column: "Order");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_FaqItem_CategoryID",
-                table: "FaqItem",
-                column: "CategoryID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_FaqItem_IsActive",
-                table: "FaqItem",
-                column: "IsActive");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_FaqItem_Order",
-                table: "FaqItem",
-                column: "Order");
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "Document");
-
-            migrationBuilder.DropTable(
-                name: "FaqItem");
-
-            migrationBuilder.DropTable(
-                name: "FaqCategory");
-        }
-    }
-}

+ 0 - 203
backend/Migrations/DefaultDb/20250117140508_SyncWithExistingDb.Designer.cs

@@ -1,203 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250117140508_SyncWithExistingDb")]
-    partial class SyncWithExistingDb
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsDisplay")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 22
backend/Migrations/DefaultDb/20250117140508_SyncWithExistingDb.cs

@@ -1,22 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class SyncWithExistingDb : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-
-        }
-    }
-}

+ 0 - 200
backend/Migrations/DefaultDb/20250117142625_DeleteContentColumnByFaqCategory.Designer.cs

@@ -1,200 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250117142625_DeleteContentColumnByFaqCategory")]
-    partial class DeleteContentColumnByFaqCategory
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsDisplay")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 28
backend/Migrations/DefaultDb/20250117142625_DeleteContentColumnByFaqCategory.cs

@@ -1,28 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class DeleteContentColumnByFaqCategory : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropColumn(
-                name: "Content",
-                table: "FaqCategory");
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AddColumn<string>(
-                name: "Content",
-                table: "FaqCategory",
-                type: "nvarchar(max)",
-                nullable: true);
-        }
-    }
-}

+ 0 - 203
backend/Migrations/DefaultDb/20250117153800_UpdateFields.Designer.cs

@@ -1,203 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250117153800_UpdateFields")]
-    partial class UpdateFields
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive")
-                        .IsUnique();
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 38
backend/Migrations/DefaultDb/20250117153800_UpdateFields.cs

@@ -1,38 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class UpdateFields : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.RenameColumn(
-                name: "IsDisplay",
-                table: "Document",
-                newName: "IsActive");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Document_IsActive",
-                table: "Document",
-                column: "IsActive",
-                unique: true);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_Document_IsActive",
-                table: "Document");
-
-            migrationBuilder.RenameColumn(
-                name: "IsActive",
-                table: "Document",
-                newName: "IsDisplay");
-        }
-    }
-}

+ 0 - 251
backend/Migrations/DefaultDb/20250119234313_AddPopupTable.Designer.cs

@@ -1,251 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250119234313_AddPopupTable")]
-    partial class AddPopupTable
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<DateTime?>("EndAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Link")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("StartAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
-
-                    b.ToTable("Popup");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 68
backend/Migrations/DefaultDb/20250119234313_AddPopupTable.cs

@@ -1,68 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class AddPopupTable : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropIndex(
-                name: "IX_Document_IsActive",
-                table: "Document");
-
-            migrationBuilder.CreateTable(
-                name: "Popup",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false)
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
-                    Content = table.Column<string>(type: "nvarchar(max)", nullable: true),
-                    Link = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
-                    StartAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    EndAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    Order = table.Column<int>(type: "int", nullable: false),
-                    IsActive = table.Column<bool>(type: "bit", nullable: false),
-                    Views = table.Column<int>(type: "int", nullable: false),
-                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Popup", x => x.ID);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Document_IsActive",
-                table: "Document",
-                column: "IsActive");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Popup_IsActive",
-                table: "Popup",
-                column: "IsActive");
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "Popup");
-
-            migrationBuilder.DropIndex(
-                name: "IX_Document_IsActive",
-                table: "Document");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Document_IsActive",
-                table: "Document",
-                column: "IsActive",
-                unique: true);
-        }
-    }
-}

+ 0 - 250
backend/Migrations/DefaultDb/20250120034312_AddBannerTables.Designer.cs

@@ -1,250 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250120034312_AddBannerTables")]
-    partial class AddBannerTables
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<DateTime?>("EndAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Link")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("StartAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
-
-                    b.ToTable("Popup");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 40
backend/Migrations/DefaultDb/20250120034312_AddBannerTables.cs

@@ -1,40 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class AddBannerTables : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "Link",
-                table: "Popup",
-                type: "nvarchar(255)",
-                maxLength: 255,
-                nullable: true,
-                oldClrType: typeof(string),
-                oldType: "nvarchar(255)",
-                oldMaxLength: 255);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "Link",
-                table: "Popup",
-                type: "nvarchar(255)",
-                maxLength: 255,
-                nullable: false,
-                defaultValue: "",
-                oldClrType: typeof(string),
-                oldType: "nvarchar(255)",
-                oldMaxLength: 255,
-                oldNullable: true);
-        }
-    }
-}

+ 0 - 364
backend/Migrations/DefaultDb/20250120035304_UpdateFieldInBannerItem.Designer.cs

@@ -1,364 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250120035304_UpdateFieldInBannerItem")]
-    partial class UpdateFieldInBannerItem
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<DateTime?>("EndAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Height")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Image")
-                        .IsRequired()
-                        .HasMaxLength(1024)
-                        .HasColumnType("nvarchar(1024)");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Link")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<int>("PositionID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("StartAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.Property<int>("Width")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("PositionID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_BannerItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_BannerItem_Order");
-
-                    b.ToTable("BannerItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_BannerPosition_Code")
-                        .IsUnique();
-
-                    b.ToTable("BannerPosition");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<DateTime?>("EndAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Link")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("StartAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
-
-                    b.ToTable("Popup");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Banner.BannerPosition", "BannerPosition")
-                        .WithMany("BannerItem")
-                        .HasForeignKey("PositionID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("BannerPosition");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
-                {
-                    b.Navigation("BannerItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 94
backend/Migrations/DefaultDb/20250120035304_UpdateFieldInBannerItem.cs

@@ -1,94 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class UpdateFieldInBannerItem : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.CreateTable(
-                name: "BannerPosition",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false)
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    Code = table.Column<string>(type: "nvarchar(30)", maxLength: 30, nullable: false),
-                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
-                    IsActive = table.Column<bool>(type: "bit", nullable: false),
-                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_BannerPosition", x => x.ID);
-                });
-
-            migrationBuilder.CreateTable(
-                name: "BannerItem",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false)
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    PositionID = table.Column<int>(type: "int", nullable: false),
-                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
-                    Image = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: false),
-                    Width = table.Column<int>(type: "int", nullable: false),
-                    Height = table.Column<int>(type: "int", nullable: false),
-                    Link = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true),
-                    Order = table.Column<int>(type: "int", nullable: false),
-                    IsActive = table.Column<bool>(type: "bit", nullable: false),
-                    StartAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    EndAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    Views = table.Column<int>(type: "int", nullable: false),
-                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: true)
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_BannerItem", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_BannerItem_BannerPosition_PositionID",
-                        column: x => x.PositionID,
-                        principalTable: "BannerPosition",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                });
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BannerItem_IsActive",
-                table: "BannerItem",
-                column: "IsActive");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BannerItem_Order",
-                table: "BannerItem",
-                column: "Order");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BannerItem_PositionID",
-                table: "BannerItem",
-                column: "PositionID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BannerPosition_Code",
-                table: "BannerPosition",
-                column: "Code",
-                unique: true);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "BannerItem");
-
-            migrationBuilder.DropTable(
-                name: "BannerPosition");
-        }
-    }
-}

+ 0 - 668
backend/Migrations/DefaultDb/20250124015524_AddMember.Designer.cs

@@ -1,668 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250124015524_AddMember")]
-    partial class AddMember
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<DateTime?>("EndAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Height")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Image")
-                        .HasMaxLength(1024)
-                        .HasColumnType("nvarchar(1024)");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Link")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<int>("PositionID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("StartAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.Property<int>("Width")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("PositionID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_BannerItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_BannerItem_Order");
-
-                    b.ToTable("BannerItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_BannerPosition_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_BannerPosition_IsActive");
-
-                    b.ToTable("BannerPosition");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<DateTime?>("EndAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Link")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("StartAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
-
-                    b.ToTable("Popup");
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.Member", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int")
-                        .HasComment("PK");
-
-                    b.Property<DateTime?>("AuthCertifiedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("본인인증 일시");
-
-                    b.Property<DateOnly?>("Birthday")
-                        .HasMaxLength(10)
-                        .HasColumnType("date")
-                        .HasComment("생년월일");
-
-                    b.Property<long>("Coin")
-                        .HasColumnType("bigint")
-                        .HasComment("코인");
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("가입 일시");
-
-                    b.Property<DateTime?>("DeletedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("탈퇴 일시");
-
-                    b.Property<string>("DeviceInfo")
-                        .HasMaxLength(400)
-                        .HasColumnType("nvarchar(400)")
-                        .HasComment("로그인 단말기 정보");
-
-                    b.Property<string>("Email")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)")
-                        .HasComment("이메일");
-
-                    b.Property<DateTime?>("EmailVerifiedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("이메일 인증 일시");
-
-                    b.Property<int>("Exp")
-                        .HasColumnType("int")
-                        .HasComment("경험치");
-
-                    b.Property<string>("FirstName")
-                        .HasMaxLength(20)
-                        .HasColumnType("nvarchar(20)")
-                        .HasComment("본명(성)");
-
-                    b.Property<int>("Followed")
-                        .HasColumnType("int")
-                        .HasComment("구독자");
-
-                    b.Property<int>("Following")
-                        .HasColumnType("int")
-                        .HasComment("구독 중");
-
-                    b.Property<string>("FullName")
-                        .HasMaxLength(40)
-                        .HasColumnType("nvarchar(40)")
-                        .HasComment("본명");
-
-                    b.Property<int?>("Gender")
-                        .HasMaxLength(6)
-                        .HasColumnType("int")
-                        .HasComment("성별");
-
-                    b.Property<int?>("GradeID")
-                        .HasColumnType("int")
-                        .HasComment("회원등급 ID");
-
-                    b.Property<string>("Intro")
-                        .HasMaxLength(1000)
-                        .HasColumnType("nvarchar(1000)")
-                        .HasComment("자기소개");
-
-                    b.Property<bool>("IsAdmin")
-                        .HasColumnType("bit")
-                        .HasComment("운영진 여부");
-
-                    b.Property<bool>("IsAuthCertified")
-                        .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?>("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<string>("Name")
-                        .HasMaxLength(20)
-                        .HasColumnType("nvarchar(20)")
-                        .HasComment("별명");
-
-                    b.Property<string>("Password")
-                        .IsRequired()
-                        .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>("Photo")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)")
-                        .HasComment("사진");
-
-                    b.Property<string>("SID")
-                        .IsRequired()
-                        .HasMaxLength(20)
-                        .HasColumnType("nvarchar(20)")
-                        .HasComment("SID");
-
-                    b.Property<string>("SignupIP")
-                        .IsRequired()
-                        .HasMaxLength(15)
-                        .HasColumnType("nvarchar(15)")
-                        .HasComment("회원가입 시 IP");
-
-                    b.Property<string>("Summary")
-                        .HasMaxLength(50)
-                        .HasColumnType("nvarchar(50)")
-                        .HasComment("한마디");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("수정 일시");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("GradeID");
-
-                    b.HasIndex(new[] { "Email" }, "IX_Member_Email")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Name" }, "IX_Member_Name")
-                        .IsUnique()
-                        .HasFilter("[Name] IS NOT NULL");
-
-                    b.HasIndex(new[] { "SID" }, "IX_Member_SID")
-                        .IsUnique();
-
-                    b.ToTable("Member", t =>
-                        {
-                            t.HasComment("회원 정보");
-                        });
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.MemberApprove", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int")
-                        .HasComment("회원 ID");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    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.HasKey("ID");
-
-                    b.ToTable("MemberApprove", t =>
-                        {
-                            t.HasComment("회원 동의 및 수신 여부");
-                        });
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.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")
-                        .HasColumnType("nvarchar(max)")
-                        .HasComment("이미지");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit")
-                        .HasComment("사용 여부");
-
-                    b.Property<string>("KorName")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)")
-                        .HasComment("한글 명");
-
-                    b.Property<short>("Order")
-                        .HasColumnType("smallint")
-                        .HasComment("순서");
-
-                    b.Property<int>("RequiredCoin")
-                        .HasColumnType("int")
-                        .HasComment("최소 코인(Coin)");
-
-                    b.Property<int>("RequiredExp")
-                        .HasColumnType("int")
-                        .HasComment("최소 경험치(Exp)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("수정 일시");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "EngName" }, "IX_MemberGrade_EngName")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "KorName" }, "IX_MemberGrade_KorName")
-                        .IsUnique();
-
-                    b.ToTable("MemberGrade", t =>
-                        {
-                            t.HasComment("회원 등급");
-                        });
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Banner.BannerPosition", "BannerPosition")
-                        .WithMany("BannerItem")
-                        .HasForeignKey("PositionID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("BannerPosition");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.Member", b =>
-                {
-                    b.HasOne("bitforum.Models.User.MemberGrade", "MemberGrade")
-                        .WithMany()
-                        .HasForeignKey("GradeID");
-
-                    b.HasOne("bitforum.Models.User.MemberApprove", "MemberApproves")
-                        .WithOne("Member")
-                        .HasForeignKey("bitforum.Models.User.Member", "ID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("MemberApproves");
-
-                    b.Navigation("MemberGrade");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
-                {
-                    b.Navigation("BannerItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.MemberApprove", b =>
-                {
-                    b.Navigation("Member")
-                        .IsRequired();
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 191
backend/Migrations/DefaultDb/20250124015524_AddMember.cs

@@ -1,191 +0,0 @@
-using System;
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class AddMember : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.AlterColumn<string>(
-                name: "Image",
-                table: "BannerItem",
-                type: "nvarchar(1024)",
-                maxLength: 1024,
-                nullable: true,
-                oldClrType: typeof(string),
-                oldType: "nvarchar(1024)",
-                oldMaxLength: 1024);
-
-            migrationBuilder.CreateTable(
-                name: "MemberApprove",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false, comment: "회원 ID")
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    IsReceiveSMS = table.Column<bool>(type: "bit", nullable: false, comment: "SMS 수신 여부"),
-                    IsReceiveEmail = table.Column<bool>(type: "bit", nullable: false, comment: "E-MAIL 수신 여부"),
-                    IsReceiveNote = table.Column<bool>(type: "bit", nullable: false, comment: "쪽지 수신 여부"),
-                    IsDisclosureInvest = table.Column<bool>(type: "bit", nullable: false, comment: "투자 현황 공개 여부")
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_MemberApprove", x => x.ID);
-                },
-                comment: "회원 동의 및 수신 여부");
-
-            migrationBuilder.CreateTable(
-                name: "MemberGrade",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
-                        .Annotation("SqlServer:Identity", "1, 1"),
-                    KorName = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false, comment: "한글 명"),
-                    EngName = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false, comment: "영문 명"),
-                    Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true, comment: "설명"),
-                    Order = table.Column<short>(type: "smallint", nullable: false, comment: "순서"),
-                    Image = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "이미지"),
-                    RequiredExp = table.Column<int>(type: "int", nullable: false, comment: "최소 경험치(Exp)"),
-                    RequiredCoin = table.Column<int>(type: "int", nullable: false, comment: "최소 코인(Coin)"),
-                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
-                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_MemberGrade", x => x.ID);
-                },
-                comment: "회원 등급");
-
-            migrationBuilder.CreateTable(
-                name: "Member",
-                columns: table => new
-                {
-                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK"),
-                    GradeID = table.Column<int>(type: "int", nullable: true, comment: "회원등급 ID"),
-                    SID = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false, comment: "SID"),
-                    Email = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "이메일"),
-                    Name = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true, comment: "별명"),
-                    FullName = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: true, comment: "본명"),
-                    FirstName = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true, comment: "본명(성)"),
-                    LastName = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: true, comment: "본명(이름)"),
-                    Password = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "비밀번호"),
-                    Intro = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true, comment: "자기소개"),
-                    Summary = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true, comment: "한마디"),
-                    Coin = table.Column<long>(type: "bigint", nullable: false, comment: "코인"),
-                    Exp = table.Column<int>(type: "int", nullable: false, comment: "경험치"),
-                    Phone = table.Column<string>(type: "nvarchar(15)", maxLength: 15, nullable: true, comment: "연락처"),
-                    Birthday = table.Column<DateOnly>(type: "date", maxLength: 10, nullable: true, comment: "생년월일"),
-                    Gender = table.Column<int>(type: "int", maxLength: 6, nullable: true, comment: "성별"),
-                    Photo = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "사진"),
-                    IsEmailVerified = table.Column<bool>(type: "bit", nullable: false, comment: "이메일 인증 여부"),
-                    IsAuthCertified = table.Column<bool>(type: "bit", nullable: false, comment: "본인 인증 여부"),
-                    IsDenied = table.Column<bool>(type: "bit", nullable: false, comment: "차단 여부"),
-                    IsAdmin = table.Column<bool>(type: "bit", nullable: false, comment: "운영진 여부"),
-                    IsWithdraw = table.Column<bool>(type: "bit", nullable: false, comment: "탈퇴 여부"),
-                    Following = table.Column<int>(type: "int", nullable: false, comment: "구독 중"),
-                    Followed = table.Column<int>(type: "int", nullable: false, comment: "구독자"),
-                    DeviceInfo = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: true, comment: "로그인 단말기 정보"),
-                    SignupIP = table.Column<string>(type: "nvarchar(15)", maxLength: 15, nullable: false, comment: "회원가입 시 IP"),
-                    LastLoginIp = table.Column<string>(type: "nvarchar(15)", maxLength: 15, nullable: true, comment: "마지막 로그인 IP"),
-                    LastLoginAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "마지막 로그인 일시"),
-                    EmailVerifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "이메일 인증 일시"),
-                    AuthCertifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "본인인증 일시"),
-                    PasswordUpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "비밀번호 변경 일시"),
-                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "가입 일시"),
-                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
-                    DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "탈퇴 일시")
-                },
-                constraints: table =>
-                {
-                    table.PrimaryKey("PK_Member", x => x.ID);
-                    table.ForeignKey(
-                        name: "FK_Member_MemberApprove_ID",
-                        column: x => x.ID,
-                        principalTable: "MemberApprove",
-                        principalColumn: "ID",
-                        onDelete: ReferentialAction.Cascade);
-                    table.ForeignKey(
-                        name: "FK_Member_MemberGrade_GradeID",
-                        column: x => x.GradeID,
-                        principalTable: "MemberGrade",
-                        principalColumn: "ID");
-                },
-                comment: "회원 정보");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_BannerPosition_IsActive",
-                table: "BannerPosition",
-                column: "IsActive");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Member_Email",
-                table: "Member",
-                column: "Email",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Member_GradeID",
-                table: "Member",
-                column: "GradeID");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Member_Name",
-                table: "Member",
-                column: "Name",
-                unique: true,
-                filter: "[Name] IS NOT NULL");
-
-            migrationBuilder.CreateIndex(
-                name: "IX_Member_SID",
-                table: "Member",
-                column: "SID",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_MemberGrade_EngName",
-                table: "MemberGrade",
-                column: "EngName",
-                unique: true);
-
-            migrationBuilder.CreateIndex(
-                name: "IX_MemberGrade_KorName",
-                table: "MemberGrade",
-                column: "KorName",
-                unique: true);
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-            migrationBuilder.DropTable(
-                name: "Member");
-
-            migrationBuilder.DropTable(
-                name: "MemberApprove");
-
-            migrationBuilder.DropTable(
-                name: "MemberGrade");
-
-            migrationBuilder.DropIndex(
-                name: "IX_BannerPosition_IsActive",
-                table: "BannerPosition");
-
-            migrationBuilder.AlterColumn<string>(
-                name: "Image",
-                table: "BannerItem",
-                type: "nvarchar(1024)",
-                maxLength: 1024,
-                nullable: false,
-                defaultValue: "",
-                oldClrType: typeof(string),
-                oldType: "nvarchar(1024)",
-                oldMaxLength: 1024,
-                oldNullable: true);
-        }
-    }
-}

+ 0 - 668
backend/Migrations/DefaultDb/20250124020320_UpdateMember.Designer.cs

@@ -1,668 +0,0 @@
-// <auto-generated />
-using System;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Metadata;
-using Microsoft.EntityFrameworkCore.Migrations;
-using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    [DbContext(typeof(DefaultDbContext))]
-    [Migration("20250124020320_UpdateMember")]
-    partial class UpdateMember
-    {
-        /// <inheritdoc />
-        protected override void BuildTargetModel(ModelBuilder modelBuilder)
-        {
-#pragma warning disable 612, 618
-            modelBuilder
-                .HasAnnotation("ProductVersion", "8.0.0")
-                .HasAnnotation("Relational:MaxIdentifierLength", 128);
-
-            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
-
-            modelBuilder.Entity("bitforum.Models.Config", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Description")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<string>("Key")
-                        .IsRequired()
-                        .HasColumnType("nvarchar(450)");
-
-                    b.Property<string>("Value")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("Key")
-                        .IsUnique();
-
-                    b.ToTable("Config");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<DateTime?>("EndAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Height")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Image")
-                        .HasMaxLength(1024)
-                        .HasColumnType("nvarchar(1024)");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Link")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<int>("PositionID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("StartAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.Property<int>("Width")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("PositionID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_BannerItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_BannerItem_Order");
-
-                    b.ToTable("BannerItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_BannerPosition_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_BannerPosition_IsActive");
-
-                    b.ToTable("BannerPosition");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Document", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_Document_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
-
-                    b.ToTable("Document");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Code")
-                        .IsRequired()
-                        .HasMaxLength(30)
-                        .HasColumnType("nvarchar(30)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
-
-                    b.ToTable("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Answer")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<int>("CategoryID")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<string>("Question")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("CategoryID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
-
-                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
-
-                    b.ToTable("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    b.Property<string>("Content")
-                        .HasColumnType("nvarchar(max)");
-
-                    b.Property<DateTime?>("CreatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<DateTime?>("EndAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit");
-
-                    b.Property<string>("Link")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<int>("Order")
-                        .HasColumnType("int");
-
-                    b.Property<DateTime?>("StartAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<string>("Subject")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2");
-
-                    b.Property<int>("Views")
-                        .HasColumnType("int");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
-
-                    b.ToTable("Popup");
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.Member", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int")
-                        .HasComment("PK");
-
-                    b.Property<DateTime?>("AuthCertifiedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("본인인증 일시");
-
-                    b.Property<DateOnly?>("Birthday")
-                        .HasMaxLength(10)
-                        .HasColumnType("date")
-                        .HasComment("생년월일");
-
-                    b.Property<long>("Coin")
-                        .HasColumnType("bigint")
-                        .HasComment("코인");
-
-                    b.Property<DateTime>("CreatedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("가입 일시");
-
-                    b.Property<DateTime?>("DeletedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("탈퇴 일시");
-
-                    b.Property<string>("DeviceInfo")
-                        .HasMaxLength(400)
-                        .HasColumnType("nvarchar(400)")
-                        .HasComment("로그인 단말기 정보");
-
-                    b.Property<string>("Email")
-                        .IsRequired()
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)")
-                        .HasComment("이메일");
-
-                    b.Property<DateTime?>("EmailVerifiedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("이메일 인증 일시");
-
-                    b.Property<int>("Exp")
-                        .HasColumnType("int")
-                        .HasComment("경험치");
-
-                    b.Property<string>("FirstName")
-                        .HasMaxLength(20)
-                        .HasColumnType("nvarchar(20)")
-                        .HasComment("본명(성)");
-
-                    b.Property<int>("Followed")
-                        .HasColumnType("int")
-                        .HasComment("구독자");
-
-                    b.Property<int>("Following")
-                        .HasColumnType("int")
-                        .HasComment("구독 중");
-
-                    b.Property<string>("FullName")
-                        .HasMaxLength(40)
-                        .HasColumnType("nvarchar(40)")
-                        .HasComment("본명");
-
-                    b.Property<int?>("Gender")
-                        .HasMaxLength(6)
-                        .HasColumnType("int")
-                        .HasComment("성별");
-
-                    b.Property<int?>("GradeID")
-                        .HasColumnType("int")
-                        .HasComment("회원등급 ID");
-
-                    b.Property<string>("Intro")
-                        .HasMaxLength(1000)
-                        .HasColumnType("nvarchar(1000)")
-                        .HasComment("자기소개");
-
-                    b.Property<bool>("IsAdmin")
-                        .HasColumnType("bit")
-                        .HasComment("운영진 여부");
-
-                    b.Property<bool>("IsAuthCertified")
-                        .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?>("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<string>("Name")
-                        .HasMaxLength(20)
-                        .HasColumnType("nvarchar(20)")
-                        .HasComment("별명");
-
-                    b.Property<string>("Password")
-                        .IsRequired()
-                        .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>("Photo")
-                        .HasMaxLength(255)
-                        .HasColumnType("nvarchar(255)")
-                        .HasComment("사진");
-
-                    b.Property<string>("SID")
-                        .IsRequired()
-                        .HasMaxLength(20)
-                        .HasColumnType("nvarchar(20)")
-                        .HasComment("SID");
-
-                    b.Property<string>("SignupIP")
-                        .IsRequired()
-                        .HasMaxLength(15)
-                        .HasColumnType("nvarchar(15)")
-                        .HasComment("회원가입 시 IP");
-
-                    b.Property<string>("Summary")
-                        .HasMaxLength(50)
-                        .HasColumnType("nvarchar(50)")
-                        .HasComment("한마디");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("수정 일시");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex("GradeID");
-
-                    b.HasIndex(new[] { "Email" }, "IX_Member_Email")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "Name" }, "IX_Member_Name")
-                        .IsUnique()
-                        .HasFilter("[Name] IS NOT NULL");
-
-                    b.HasIndex(new[] { "SID" }, "IX_Member_SID")
-                        .IsUnique();
-
-                    b.ToTable("Member", t =>
-                        {
-                            t.HasComment("회원 정보");
-                        });
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.MemberApprove", b =>
-                {
-                    b.Property<int>("ID")
-                        .ValueGeneratedOnAdd()
-                        .HasColumnType("int")
-                        .HasComment("회원 ID");
-
-                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
-
-                    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.HasKey("ID");
-
-                    b.ToTable("MemberApprove", t =>
-                        {
-                            t.HasComment("회원 동의 및 수신 여부");
-                        });
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.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")
-                        .HasColumnType("nvarchar(max)")
-                        .HasComment("이미지");
-
-                    b.Property<bool>("IsActive")
-                        .HasColumnType("bit")
-                        .HasComment("사용 여부");
-
-                    b.Property<string>("KorName")
-                        .IsRequired()
-                        .HasMaxLength(120)
-                        .HasColumnType("nvarchar(120)")
-                        .HasComment("한글 명");
-
-                    b.Property<short>("Order")
-                        .HasColumnType("smallint")
-                        .HasComment("순서");
-
-                    b.Property<int>("RequiredCoin")
-                        .HasColumnType("int")
-                        .HasComment("최소 코인(Coin)");
-
-                    b.Property<int>("RequiredExp")
-                        .HasColumnType("int")
-                        .HasComment("최소 경험치(Exp)");
-
-                    b.Property<DateTime?>("UpdatedAt")
-                        .HasColumnType("datetime2")
-                        .HasComment("수정 일시");
-
-                    b.HasKey("ID");
-
-                    b.HasIndex(new[] { "EngName" }, "IX_MemberGrade_EngName")
-                        .IsUnique();
-
-                    b.HasIndex(new[] { "KorName" }, "IX_MemberGrade_KorName")
-                        .IsUnique();
-
-                    b.ToTable("MemberGrade", t =>
-                        {
-                            t.HasComment("회원 등급");
-                        });
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Banner.BannerPosition", "BannerPosition")
-                        .WithMany("BannerItem")
-                        .HasForeignKey("PositionID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("BannerPosition");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
-                {
-                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
-                        .WithMany("FaqItem")
-                        .HasForeignKey("CategoryID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("FaqCategory");
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.Member", b =>
-                {
-                    b.HasOne("bitforum.Models.User.MemberGrade", "MemberGrade")
-                        .WithMany()
-                        .HasForeignKey("GradeID");
-
-                    b.HasOne("bitforum.Models.User.MemberApprove", "MemberApproves")
-                        .WithOne("Member")
-                        .HasForeignKey("bitforum.Models.User.Member", "ID")
-                        .OnDelete(DeleteBehavior.Cascade)
-                        .IsRequired();
-
-                    b.Navigation("MemberApproves");
-
-                    b.Navigation("MemberGrade");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
-                {
-                    b.Navigation("BannerItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
-                {
-                    b.Navigation("FaqItem");
-                });
-
-            modelBuilder.Entity("bitforum.Models.User.MemberApprove", b =>
-                {
-                    b.Navigation("Member")
-                        .IsRequired();
-                });
-#pragma warning restore 612, 618
-        }
-    }
-}

+ 0 - 22
backend/Migrations/DefaultDb/20250124020320_UpdateMember.cs

@@ -1,22 +0,0 @@
-using Microsoft.EntityFrameworkCore.Migrations;
-
-#nullable disable
-
-namespace bitforum.Migrations.DefaultDb
-{
-    /// <inheritdoc />
-    public partial class UpdateMember : Migration
-    {
-        /// <inheritdoc />
-        protected override void Up(MigrationBuilder migrationBuilder)
-        {
-
-        }
-
-        /// <inheritdoc />
-        protected override void Down(MigrationBuilder migrationBuilder)
-        {
-
-        }
-    }
-}

+ 1146 - 0
backend/Migrations/DefaultDb/20250220162340_a1.Designer.cs

@@ -0,0 +1,1146 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace bitforum.Migrations.DefaultDb
+{
+    [DbContext(typeof(DefaultDbContext))]
+    [Migration("20250220162340_a1")]
+    partial class a1
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "8.0.13")
+                .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+            modelBuilder.Entity("bitforum.Models.Account.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("Token");
+
+                    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(new[] { "Email" }, "IX_EmailVerifyNumber_Email");
+
+                    b.HasIndex(new[] { "Expiration" }, "IX_EmailVerifyNumber_Expiration");
+
+                    b.HasIndex(new[] { "IsVerified" }, "IX_EmailVerifyNumber_IsVerified");
+
+                    b.HasIndex(new[] { "Type" }, "IX_EmailVerifyNumber_Type");
+
+                    b.ToTable("EmailVerifyNumber", t =>
+                        {
+                            t.HasComment("이메일 인증 번호들");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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(new[] { "Email" }, "IX_EmailVerifyToken_Email");
+
+                    b.HasIndex(new[] { "Expiration" }, "IX_EmailVerifyToken_Expiration");
+
+                    b.HasIndex(new[] { "IsVerified" }, "IX_EmailVerifyToken_IsVerified");
+
+                    b.HasIndex(new[] { "Type" }, "IX_EmailVerifyToken_Type");
+
+                    b.ToTable("EmailVerifyToken", t =>
+                        {
+                            t.HasComment("이메일 인증 토큰들");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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<long>("Coin")
+                        .HasColumnType("bigint")
+                        .HasComment("코인");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("가입 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .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<int>("Exp")
+                        .HasColumnType("int")
+                        .HasComment("경험치");
+
+                    b.Property<string>("FirstName")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("본명(성)");
+
+                    b.Property<int>("Followed")
+                        .HasColumnType("int")
+                        .HasComment("구독자");
+
+                    b.Property<int>("Following")
+                        .HasColumnType("int")
+                        .HasComment("구독 중");
+
+                    b.Property<string>("FullName")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("본명");
+
+                    b.Property<int?>("Gender")
+                        .HasColumnType("int")
+                        .HasComment("성별");
+
+                    b.Property<int?>("GradeID")
+                        .HasColumnType("int")
+                        .HasComment("회원등급 ID");
+
+                    b.Property<string>("Icon")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("아이콘");
+
+                    b.Property<string>("Intro")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("자기소개");
+
+                    b.Property<bool>("IsAdmin")
+                        .HasColumnType("bit")
+                        .HasComment("운영진 여부");
+
+                    b.Property<bool>("IsAuthCertified")
+                        .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?>("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<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>("Photo")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("사진");
+
+                    b.Property<string>("SID")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("SID");
+
+                    b.Property<string>("SignupIP")
+                        .IsRequired()
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("회원가입 시 IP");
+
+                    b.Property<string>("Summary")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("한마디");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("GradeID");
+
+                    b.HasIndex(new[] { "Email" }, "IX_Member_Email")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "FullName" }, "IX_Member_FullName");
+
+                    b.HasIndex(new[] { "IsAdmin" }, "IX_Member_IsAdmin");
+
+                    b.HasIndex(new[] { "IsAuthCertified" }, "IX_Member_IsAuthCertified");
+
+                    b.HasIndex(new[] { "IsDenied" }, "IX_Member_IsDenied");
+
+                    b.HasIndex(new[] { "IsEmailVerified" }, "IX_Member_IsEmailVerified");
+
+                    b.HasIndex(new[] { "IsWithdraw" }, "IX_Member_IsWithdraw");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Member_Name")
+                        .IsUnique()
+                        .HasFilter("[Name] IS NOT NULL");
+
+                    b.HasIndex(new[] { "Phone" }, "IX_Member_Phone");
+
+                    b.HasIndex(new[] { "SID" }, "IX_Member_SID")
+                        .IsUnique();
+
+                    b.ToTable("Member", t =>
+                        {
+                            t.HasComment("회원 정보");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.MemberApprove", b =>
+                {
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    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.HasKey("MemberID");
+
+                    b.ToTable("MemberApprove", t =>
+                        {
+                            t.HasComment("회원 동의 및 수신 여부");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("KorName")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("한글 명");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<int>("RequiredCoin")
+                        .HasColumnType("int")
+                        .HasComment("최소 코인(Coin)");
+
+                    b.Property<int>("RequiredExp")
+                        .HasColumnType("int")
+                        .HasComment("최소 경험치(Exp)");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "EngName" }, "IX_MemberGrade_EngName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "KorName" }, "IX_MemberGrade_KorName")
+                        .IsUnique();
+
+                    b.ToTable("MemberGrade", t =>
+                        {
+                            t.HasComment("회원 등급");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.RefreshToken", 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<DateTime>("Expiration")
+                        .HasColumnType("datetime2")
+                        .HasComment("만료 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("nvarchar(128)")
+                        .HasComment("Token");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("RefreshToken");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Config", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(450)");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Key")
+                        .IsUnique();
+
+                    b.ToTable("Config");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterEmail")
+                        .IsRequired()
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("바꾼 이메일");
+
+                    b.Property<string>("BeforeEmail")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("이전 이메일");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_EmailChangeLog_MemberID");
+
+                    b.ToTable("EmailChangeLog", t =>
+                        {
+                            t.HasComment("이메일 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailLog", 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>("FromAddress")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("발신 주소");
+
+                    b.Property<string>("FromName")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("발신자");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Message")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime?>("ProcessedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("처리 일시");
+
+                    b.Property<string>("Status")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("처리 여부");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("제목");
+
+                    b.Property<string>("ToAddress")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("수신 주소");
+
+                    b.Property<string>("ToName")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("수신자");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_EmailLog_MemberID");
+
+                    b.HasIndex(new[] { "Status" }, "IX_EmailLog_Status");
+
+                    b.ToTable("EmailLog");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.LoginLog", 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(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Reason")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .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("MemberID");
+
+                    b.ToTable("LoginLog", t =>
+                        {
+                            t.HasComment("로그인 기록");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.NameChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterName")
+                        .IsRequired()
+                        .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<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_NameChangeLog_MemberID");
+
+                    b.ToTable("NameChangeLog", t =>
+                        {
+                            t.HasComment("별명 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.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<DateTime?>("EndAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("사용 기간 - 종료");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("int")
+                        .HasComment("세로 크기");
+
+                    b.Property<string>("Image")
+                        .HasMaxLength(1024)
+                        .HasColumnType("nvarchar(1024)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Link")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("주소");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("int")
+                        .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.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("int")
+                        .HasComment("가로 크기");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("PositionID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_BannerItem_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_BannerItem_Order");
+
+                    b.ToTable("BannerItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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(new[] { "Code" }, "IX_BannerPosition_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_BannerPosition_IsActive");
+
+                    b.ToTable("BannerPosition");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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")
+                        .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(new[] { "Code" }, "IX_Document_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
+
+                    b.ToTable("Document");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("분류 명");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
+
+                    b.ToTable("FaqCategory");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Answer")
+                        .HasColumnType("nvarchar(max)")
+                        .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<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<string>("Question")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("질문");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CategoryID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
+
+                    b.ToTable("FaqItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Content")
+                        .HasColumnType("nvarchar(max)")
+                        .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.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_Popup_Order");
+
+                    b.ToTable("Popup");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.Member", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.MemberGrade", "MemberGrade")
+                        .WithMany()
+                        .HasForeignKey("GradeID");
+
+                    b.Navigation("MemberGrade");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.MemberApprove", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithOne("MemberApprove")
+                        .HasForeignKey("bitforum.Models.Account.MemberApprove", "MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailChangeLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("EmailChangeLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("EmailLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.LoginLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("LoginLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.NameChangeLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("NameChangeLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
+                {
+                    b.HasOne("bitforum.Models.Page.Banner.BannerPosition", "BannerPosition")
+                        .WithMany("BannerItem")
+                        .HasForeignKey("PositionID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("BannerPosition");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
+                {
+                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
+                        .WithMany("FaqItem")
+                        .HasForeignKey("CategoryID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("FaqCategory");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.Member", b =>
+                {
+                    b.Navigation("EmailChangeLog");
+
+                    b.Navigation("EmailLog");
+
+                    b.Navigation("LoginLog");
+
+                    b.Navigation("MemberApprove");
+
+                    b.Navigation("NameChangeLog");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
+                {
+                    b.Navigation("BannerItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
+                {
+                    b.Navigation("FaqItem");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 687 - 0
backend/Migrations/DefaultDb/20250220162340_a1.cs

@@ -0,0 +1,687 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace bitforum.Migrations.DefaultDb
+{
+    /// <inheritdoc />
+    public partial class a1 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "BannerPosition",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Code = table.Column<string>(type: "nvarchar(30)", maxLength: 30, nullable: false, comment: "위치 구분"),
+                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "위치 명"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_BannerPosition", x => x.ID);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Config",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false)
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Key = table.Column<string>(type: "nvarchar(450)", nullable: false),
+                    Value = table.Column<string>(type: "nvarchar(max)", nullable: true),
+                    Description = table.Column<string>(type: "nvarchar(max)", nullable: true),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Config", x => x.ID);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Document",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Code = table.Column<string>(type: "nvarchar(30)", maxLength: 30, nullable: false, comment: "주소"),
+                    Subject = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false, comment: "제목"),
+                    Content = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "내용"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
+                    Views = table.Column<int>(type: "int", nullable: false, comment: "조회 수"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Document", x => x.ID);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "EmailVerifyNumber",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Type = table.Column<int>(type: "int", nullable: false, comment: "인증 유형 (이메일 인증 / 비밀번호 재설정)"),
+                    Email = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: false, comment: "이메일"),
+                    Code = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false, comment: "Token"),
+                    IsVerified = table.Column<bool>(type: "bit", nullable: false, comment: "인증 여부"),
+                    Expiration = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "만료 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_EmailVerifyNumber", x => x.ID);
+                },
+                comment: "이메일 인증 번호들");
+
+            migrationBuilder.CreateTable(
+                name: "EmailVerifyToken",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Type = table.Column<int>(type: "int", nullable: false, comment: "인증 유형 (이메일 인증 / 비밀번호 재설정)"),
+                    Email = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: false, comment: "이메일"),
+                    Token = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false, comment: "Token"),
+                    IsVerified = table.Column<bool>(type: "bit", nullable: false, comment: "인증 여부"),
+                    Expiration = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "만료 일시"),
+                    Additional = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "추가 정보(JSON)"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_EmailVerifyToken", x => x.ID);
+                },
+                comment: "이메일 인증 토큰들");
+
+            migrationBuilder.CreateTable(
+                name: "FaqCategory",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Code = table.Column<string>(type: "nvarchar(30)", maxLength: 30, nullable: false, comment: "주소"),
+                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "분류 명"),
+                    Order = table.Column<int>(type: "int", nullable: false, comment: "순서"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_FaqCategory", x => x.ID);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "MemberGrade",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    KorName = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false, comment: "한글 명"),
+                    EngName = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false, comment: "영문 명"),
+                    Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true, comment: "설명"),
+                    Order = table.Column<short>(type: "smallint", nullable: false, comment: "순서"),
+                    Image = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "이미지"),
+                    RequiredExp = table.Column<int>(type: "int", nullable: false, comment: "최소 경험치(Exp)"),
+                    RequiredCoin = table.Column<int>(type: "int", nullable: false, comment: "최소 코인(Coin)"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_MemberGrade", x => x.ID);
+                },
+                comment: "회원 등급");
+
+            migrationBuilder.CreateTable(
+                name: "Popup",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "제목"),
+                    Content = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "내용"),
+                    Link = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "주소"),
+                    StartAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "사용 기간 - 시작"),
+                    EndAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "사용 기간 - 종료"),
+                    Order = table.Column<short>(type: "smallint", nullable: false, comment: "순서"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
+                    Views = table.Column<int>(type: "int", nullable: false, comment: "조회 수"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Popup", x => x.ID);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "RefreshToken",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Token = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false, comment: "Token"),
+                    MemberID = table.Column<int>(type: "int", nullable: false, comment: "회원 ID"),
+                    Expiration = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "만료 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_RefreshToken", x => x.ID);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "BannerItem",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    PositionID = table.Column<int>(type: "int", nullable: false, comment: "배너 위치 ID"),
+                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "배너 명"),
+                    Image = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true, comment: "이미지"),
+                    Width = table.Column<int>(type: "int", nullable: true, comment: "가로 크기"),
+                    Height = table.Column<int>(type: "int", nullable: true, comment: "세로 크기"),
+                    Link = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "주소"),
+                    Order = table.Column<int>(type: "int", nullable: false, comment: "순서"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
+                    StartAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "사용 기간 - 시작"),
+                    EndAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "사용 기간 - 종료"),
+                    Views = table.Column<int>(type: "int", nullable: false, comment: "조회 수"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_BannerItem", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_BannerItem_BannerPosition_PositionID",
+                        column: x => x.PositionID,
+                        principalTable: "BannerPosition",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "FaqItem",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    CategoryID = table.Column<int>(type: "int", nullable: false, comment: "분류 ID"),
+                    Question = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "질문"),
+                    Answer = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "답변"),
+                    Order = table.Column<int>(type: "int", nullable: false, comment: "순서"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
+                    Views = table.Column<int>(type: "int", nullable: false, comment: "조회 수"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_FaqItem", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_FaqItem_FaqCategory_CategoryID",
+                        column: x => x.CategoryID,
+                        principalTable: "FaqCategory",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Member",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    GradeID = table.Column<int>(type: "int", nullable: true, comment: "회원등급 ID"),
+                    SID = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false, comment: "SID"),
+                    Email = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: false, comment: "이메일"),
+                    Name = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true, comment: "별명"),
+                    FullName = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: true, comment: "본명"),
+                    FirstName = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true, comment: "본명(성)"),
+                    LastName = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: true, comment: "본명(이름)"),
+                    Password = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "비밀번호"),
+                    Intro = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true, comment: "자기소개"),
+                    Summary = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true, comment: "한마디"),
+                    Coin = table.Column<long>(type: "bigint", nullable: false, comment: "코인"),
+                    Exp = table.Column<int>(type: "int", nullable: false, comment: "경험치"),
+                    Phone = table.Column<string>(type: "nvarchar(15)", maxLength: 15, nullable: true, comment: "연락처"),
+                    Birthday = table.Column<DateOnly>(type: "date", nullable: true, comment: "생년월일"),
+                    Gender = table.Column<int>(type: "int", nullable: true, comment: "성별"),
+                    Photo = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "사진"),
+                    Icon = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "아이콘"),
+                    IsEmailVerified = table.Column<bool>(type: "bit", nullable: false, comment: "이메일 인증 여부"),
+                    IsAuthCertified = table.Column<bool>(type: "bit", nullable: false, comment: "본인 인증 여부"),
+                    IsDenied = table.Column<bool>(type: "bit", nullable: false, comment: "차단 여부"),
+                    IsAdmin = table.Column<bool>(type: "bit", nullable: false, comment: "운영진 여부"),
+                    IsWithdraw = table.Column<bool>(type: "bit", nullable: false, comment: "탈퇴 여부"),
+                    Following = table.Column<int>(type: "int", nullable: false, comment: "구독 중"),
+                    Followed = table.Column<int>(type: "int", nullable: false, comment: "구독자"),
+                    DeviceInfo = table.Column<string>(type: "nvarchar(400)", maxLength: 400, nullable: true, comment: "로그인 단말기 정보"),
+                    SignupIP = table.Column<string>(type: "nvarchar(15)", maxLength: 15, nullable: false, comment: "회원가입 시 IP"),
+                    LastLoginIp = table.Column<string>(type: "nvarchar(15)", maxLength: 15, nullable: true, comment: "마지막 로그인 IP"),
+                    LastLoginAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "마지막 로그인 일시"),
+                    LastEmailChangedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "마지막 이메일 변경 일시"),
+                    LastNameChangedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "마지막 별명 변경 일시"),
+                    EmailVerifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "이메일 인증 일시"),
+                    AuthCertifiedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "본인인증 일시"),
+                    PasswordUpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "비밀번호 변경 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "가입 일시"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "탈퇴 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Member", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_Member_MemberGrade_GradeID",
+                        column: x => x.GradeID,
+                        principalTable: "MemberGrade",
+                        principalColumn: "ID");
+                },
+                comment: "회원 정보");
+
+            migrationBuilder.CreateTable(
+                name: "EmailChangeLog",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    MemberID = table.Column<int>(type: "int", nullable: false, comment: "회원 ID"),
+                    BeforeEmail = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: true, comment: "이전 이메일"),
+                    AfterEmail = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false, comment: "바꾼 이메일"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_EmailChangeLog", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_EmailChangeLog_Member_MemberID",
+                        column: x => x.MemberID,
+                        principalTable: "Member",
+                        principalColumn: "ID");
+                },
+                comment: "이메일 변경 내역");
+
+            migrationBuilder.CreateTable(
+                name: "EmailLog",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    MemberID = table.Column<int>(type: "int", nullable: true, comment: "회원 ID"),
+                    Status = table.Column<string>(type: "nvarchar(20)", nullable: false, comment: "처리 여부"),
+                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "제목"),
+                    Message = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "내용"),
+                    ToAddress = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false, comment: "수신 주소"),
+                    ToName = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: true, comment: "수신자"),
+                    FromAddress = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false, comment: "발신 주소"),
+                    FromName = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: true, comment: "발신자"),
+                    ProcessedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "처리 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_EmailLog", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_EmailLog_Member_MemberID",
+                        column: x => x.MemberID,
+                        principalTable: "Member",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.SetNull);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "LoginLog",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    MemberID = table.Column<int>(type: "int", nullable: true, comment: "회원 ID"),
+                    Success = table.Column<bool>(type: "bit", nullable: false, comment: "로그인 성공 여부 (0: 실패, 1: 성공)"),
+                    Account = table.Column<string>(type: "nvarchar(120)", maxLength: 120, nullable: false, comment: "로그인 시도한 계정"),
+                    Reason = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "실패 이유"),
+                    Referer = table.Column<string>(type: "nvarchar(max)", nullable: true, comment: "이전 페이지 주소"),
+                    Url = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true, comment: "요청 주소"),
+                    IpAddress = table.Column<string>(type: "nvarchar(15)", maxLength: 15, nullable: true, comment: "IP Address"),
+                    UserAgent = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true, comment: "User Agent"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_LoginLog", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_LoginLog_Member_MemberID",
+                        column: x => x.MemberID,
+                        principalTable: "Member",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.SetNull);
+                },
+                comment: "로그인 기록");
+
+            migrationBuilder.CreateTable(
+                name: "MemberApprove",
+                columns: table => new
+                {
+                    MemberID = table.Column<int>(type: "int", nullable: false, comment: "회원 ID"),
+                    IsReceiveSMS = table.Column<bool>(type: "bit", nullable: false, comment: "SMS 수신 여부"),
+                    IsReceiveEmail = table.Column<bool>(type: "bit", nullable: false, comment: "E-MAIL 수신 여부"),
+                    IsReceiveNote = table.Column<bool>(type: "bit", nullable: false, comment: "쪽지 수신 여부"),
+                    IsDisclosureInvest = table.Column<bool>(type: "bit", nullable: false, comment: "투자 현황 공개 여부")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_MemberApprove", x => x.MemberID);
+                    table.ForeignKey(
+                        name: "FK_MemberApprove_Member_MemberID",
+                        column: x => x.MemberID,
+                        principalTable: "Member",
+                        principalColumn: "ID");
+                },
+                comment: "회원 동의 및 수신 여부");
+
+            migrationBuilder.CreateTable(
+                name: "NameChangeLog",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    MemberID = table.Column<int>(type: "int", nullable: false, comment: "회원 ID"),
+                    BeforeName = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: true, comment: "이전 별명"),
+                    AfterName = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false, comment: "바꾼 별명"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_NameChangeLog", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_NameChangeLog_Member_MemberID",
+                        column: x => x.MemberID,
+                        principalTable: "Member",
+                        principalColumn: "ID");
+                },
+                comment: "별명 변경 내역");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BannerItem_IsActive",
+                table: "BannerItem",
+                column: "IsActive");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BannerItem_Order",
+                table: "BannerItem",
+                column: "Order");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BannerItem_PositionID",
+                table: "BannerItem",
+                column: "PositionID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BannerPosition_Code",
+                table: "BannerPosition",
+                column: "Code",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BannerPosition_IsActive",
+                table: "BannerPosition",
+                column: "IsActive");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Config_Key",
+                table: "Config",
+                column: "Key",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Document_Code",
+                table: "Document",
+                column: "Code",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Document_IsActive",
+                table: "Document",
+                column: "IsActive");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailChangeLog_MemberID",
+                table: "EmailChangeLog",
+                column: "MemberID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailLog_MemberID",
+                table: "EmailLog",
+                column: "MemberID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailLog_Status",
+                table: "EmailLog",
+                column: "Status");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyNumber_Email",
+                table: "EmailVerifyNumber",
+                column: "Email");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyNumber_Expiration",
+                table: "EmailVerifyNumber",
+                column: "Expiration");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyNumber_IsVerified",
+                table: "EmailVerifyNumber",
+                column: "IsVerified");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyNumber_Type",
+                table: "EmailVerifyNumber",
+                column: "Type");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyToken_Email",
+                table: "EmailVerifyToken",
+                column: "Email");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyToken_Expiration",
+                table: "EmailVerifyToken",
+                column: "Expiration");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyToken_IsVerified",
+                table: "EmailVerifyToken",
+                column: "IsVerified");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyToken_Type",
+                table: "EmailVerifyToken",
+                column: "Type");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_FaqCategory_Code",
+                table: "FaqCategory",
+                column: "Code",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_FaqCategory_Order",
+                table: "FaqCategory",
+                column: "Order");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_FaqItem_CategoryID",
+                table: "FaqItem",
+                column: "CategoryID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_FaqItem_IsActive",
+                table: "FaqItem",
+                column: "IsActive");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_FaqItem_Order",
+                table: "FaqItem",
+                column: "Order");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_LoginLog_MemberID",
+                table: "LoginLog",
+                column: "MemberID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_Email",
+                table: "Member",
+                column: "Email",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_FullName",
+                table: "Member",
+                column: "FullName");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_GradeID",
+                table: "Member",
+                column: "GradeID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_IsAdmin",
+                table: "Member",
+                column: "IsAdmin");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_IsAuthCertified",
+                table: "Member",
+                column: "IsAuthCertified");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_IsDenied",
+                table: "Member",
+                column: "IsDenied");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_IsEmailVerified",
+                table: "Member",
+                column: "IsEmailVerified");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_IsWithdraw",
+                table: "Member",
+                column: "IsWithdraw");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_Name",
+                table: "Member",
+                column: "Name",
+                unique: true,
+                filter: "[Name] IS NOT NULL");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_Phone",
+                table: "Member",
+                column: "Phone");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_SID",
+                table: "Member",
+                column: "SID",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MemberGrade_EngName",
+                table: "MemberGrade",
+                column: "EngName",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MemberGrade_KorName",
+                table: "MemberGrade",
+                column: "KorName",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_NameChangeLog_MemberID",
+                table: "NameChangeLog",
+                column: "MemberID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Popup_IsActive",
+                table: "Popup",
+                column: "IsActive");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Popup_Order",
+                table: "Popup",
+                column: "Order");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "BannerItem");
+
+            migrationBuilder.DropTable(
+                name: "Config");
+
+            migrationBuilder.DropTable(
+                name: "Document");
+
+            migrationBuilder.DropTable(
+                name: "EmailChangeLog");
+
+            migrationBuilder.DropTable(
+                name: "EmailLog");
+
+            migrationBuilder.DropTable(
+                name: "EmailVerifyNumber");
+
+            migrationBuilder.DropTable(
+                name: "EmailVerifyToken");
+
+            migrationBuilder.DropTable(
+                name: "FaqItem");
+
+            migrationBuilder.DropTable(
+                name: "LoginLog");
+
+            migrationBuilder.DropTable(
+                name: "MemberApprove");
+
+            migrationBuilder.DropTable(
+                name: "NameChangeLog");
+
+            migrationBuilder.DropTable(
+                name: "Popup");
+
+            migrationBuilder.DropTable(
+                name: "RefreshToken");
+
+            migrationBuilder.DropTable(
+                name: "BannerPosition");
+
+            migrationBuilder.DropTable(
+                name: "FaqCategory");
+
+            migrationBuilder.DropTable(
+                name: "Member");
+
+            migrationBuilder.DropTable(
+                name: "MemberGrade");
+        }
+    }
+}

+ 1160 - 0
backend/Migrations/DefaultDb/20250222130451_a2.Designer.cs

@@ -0,0 +1,1160 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace bitforum.Migrations.DefaultDb
+{
+    [DbContext(typeof(DefaultDbContext))]
+    [Migration("20250222130451_a2")]
+    partial class a2
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "8.0.13")
+                .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+            modelBuilder.Entity("bitforum.Models.Account.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(new[] { "Code" }, "IX_EmailVerifyNumber_Code");
+
+                    b.HasIndex(new[] { "Email" }, "IX_EmailVerifyNumber_Email");
+
+                    b.HasIndex(new[] { "Expiration" }, "IX_EmailVerifyNumber_Expiration");
+
+                    b.HasIndex(new[] { "IsVerified" }, "IX_EmailVerifyNumber_IsVerified");
+
+                    b.HasIndex(new[] { "Type" }, "IX_EmailVerifyNumber_Type");
+
+                    b.ToTable("EmailVerifyNumber", t =>
+                        {
+                            t.HasComment("이메일 인증 번호들");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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(new[] { "Email" }, "IX_EmailVerifyToken_Email");
+
+                    b.HasIndex(new[] { "Expiration" }, "IX_EmailVerifyToken_Expiration");
+
+                    b.HasIndex(new[] { "IsVerified" }, "IX_EmailVerifyToken_IsVerified");
+
+                    b.HasIndex(new[] { "Token" }, "IX_EmailVerifyToken_Token");
+
+                    b.HasIndex(new[] { "Type" }, "IX_EmailVerifyToken_Type");
+
+                    b.ToTable("EmailVerifyToken", t =>
+                        {
+                            t.HasComment("이메일 인증 토큰들");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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<long>("Coin")
+                        .HasColumnType("bigint")
+                        .HasComment("코인");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("가입 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .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<int>("Exp")
+                        .HasColumnType("int")
+                        .HasComment("경험치");
+
+                    b.Property<string>("FirstName")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("본명(성)");
+
+                    b.Property<int>("Followed")
+                        .HasColumnType("int")
+                        .HasComment("구독자");
+
+                    b.Property<int>("Following")
+                        .HasColumnType("int")
+                        .HasComment("구독 중");
+
+                    b.Property<string>("FullName")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("본명");
+
+                    b.Property<int?>("Gender")
+                        .HasColumnType("int")
+                        .HasComment("성별");
+
+                    b.Property<int?>("GradeID")
+                        .HasColumnType("int")
+                        .HasComment("회원등급 ID");
+
+                    b.Property<string>("Icon")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("아이콘");
+
+                    b.Property<string>("Intro")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("자기소개");
+
+                    b.Property<bool>("IsAdmin")
+                        .HasColumnType("bit")
+                        .HasComment("운영진 여부");
+
+                    b.Property<bool>("IsAuthCertified")
+                        .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?>("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<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>("Photo")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("사진");
+
+                    b.Property<string>("SID")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("SID");
+
+                    b.Property<string>("SignupIP")
+                        .IsRequired()
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("회원가입 시 IP");
+
+                    b.Property<string>("Summary")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("한마디");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("GradeID");
+
+                    b.HasIndex(new[] { "CreatedAt" }, "IX_Member_CreatedAt");
+
+                    b.HasIndex(new[] { "DeletedAt" }, "IX_Member_DeletedAt");
+
+                    b.HasIndex(new[] { "Email" }, "IX_Member_Email")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "FullName" }, "IX_Member_FullName");
+
+                    b.HasIndex(new[] { "Gender" }, "IX_Member_Gender");
+
+                    b.HasIndex(new[] { "IsAdmin" }, "IX_Member_IsAdmin");
+
+                    b.HasIndex(new[] { "IsAuthCertified" }, "IX_Member_IsAuthCertified");
+
+                    b.HasIndex(new[] { "IsDenied" }, "IX_Member_IsDenied");
+
+                    b.HasIndex(new[] { "IsEmailVerified" }, "IX_Member_IsEmailVerified");
+
+                    b.HasIndex(new[] { "IsWithdraw" }, "IX_Member_IsWithdraw");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Member_Name")
+                        .IsUnique()
+                        .HasFilter("[Name] IS NOT NULL");
+
+                    b.HasIndex(new[] { "Phone" }, "IX_Member_Phone");
+
+                    b.HasIndex(new[] { "SID" }, "IX_Member_SID")
+                        .IsUnique();
+
+                    b.ToTable("Member", t =>
+                        {
+                            t.HasComment("회원 정보");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.MemberApprove", b =>
+                {
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    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.HasKey("MemberID");
+
+                    b.ToTable("MemberApprove", t =>
+                        {
+                            t.HasComment("회원 동의 및 수신 여부");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("KorName")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("한글 명");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<int>("RequiredCoin")
+                        .HasColumnType("int")
+                        .HasComment("최소 코인(Coin)");
+
+                    b.Property<int>("RequiredExp")
+                        .HasColumnType("int")
+                        .HasComment("최소 경험치(Exp)");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "EngName" }, "IX_MemberGrade_EngName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_MemberGrade_IsActive");
+
+                    b.HasIndex(new[] { "KorName" }, "IX_MemberGrade_KorName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Order" }, "IX_MemberGrade_Order");
+
+                    b.ToTable("MemberGrade", t =>
+                        {
+                            t.HasComment("회원 등급");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.RefreshToken", 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<DateTime>("Expiration")
+                        .HasColumnType("datetime2")
+                        .HasComment("만료 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("nvarchar(128)")
+                        .HasComment("Token");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("RefreshToken");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Config", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(450)");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Key")
+                        .IsUnique();
+
+                    b.ToTable("Config");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterEmail")
+                        .IsRequired()
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("바꾼 이메일");
+
+                    b.Property<string>("BeforeEmail")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("이전 이메일");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_EmailChangeLog_MemberID");
+
+                    b.ToTable("EmailChangeLog", t =>
+                        {
+                            t.HasComment("이메일 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailLog", 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>("FromAddress")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("발신 주소");
+
+                    b.Property<string>("FromName")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("발신자");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Message")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime?>("ProcessedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("처리 일시");
+
+                    b.Property<string>("Status")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("처리 여부");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("제목");
+
+                    b.Property<string>("ToAddress")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("수신 주소");
+
+                    b.Property<string>("ToName")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("수신자");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_EmailLog_MemberID");
+
+                    b.HasIndex(new[] { "Status" }, "IX_EmailLog_Status");
+
+                    b.ToTable("EmailLog");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.LoginLog", 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(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Reason")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .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("MemberID");
+
+                    b.ToTable("LoginLog", t =>
+                        {
+                            t.HasComment("로그인 기록");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.NameChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterName")
+                        .IsRequired()
+                        .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<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_NameChangeLog_MemberID");
+
+                    b.ToTable("NameChangeLog", t =>
+                        {
+                            t.HasComment("별명 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.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<DateTime?>("EndAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("사용 기간 - 종료");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("int")
+                        .HasComment("세로 크기");
+
+                    b.Property<string>("Image")
+                        .HasMaxLength(1024)
+                        .HasColumnType("nvarchar(1024)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Link")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("주소");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("int")
+                        .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.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("int")
+                        .HasComment("가로 크기");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("PositionID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_BannerItem_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_BannerItem_Order");
+
+                    b.ToTable("BannerItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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(new[] { "Code" }, "IX_BannerPosition_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_BannerPosition_IsActive");
+
+                    b.ToTable("BannerPosition");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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")
+                        .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(new[] { "Code" }, "IX_Document_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
+
+                    b.ToTable("Document");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("분류 명");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
+
+                    b.ToTable("FaqCategory");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Answer")
+                        .HasColumnType("nvarchar(max)")
+                        .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<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<string>("Question")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("질문");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CategoryID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
+
+                    b.ToTable("FaqItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Content")
+                        .HasColumnType("nvarchar(max)")
+                        .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.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_Popup_Order");
+
+                    b.ToTable("Popup");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.Member", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.MemberGrade", "MemberGrade")
+                        .WithMany()
+                        .HasForeignKey("GradeID");
+
+                    b.Navigation("MemberGrade");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.MemberApprove", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithOne("MemberApprove")
+                        .HasForeignKey("bitforum.Models.Account.MemberApprove", "MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailChangeLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("EmailChangeLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("EmailLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.LoginLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("LoginLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.NameChangeLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("NameChangeLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
+                {
+                    b.HasOne("bitforum.Models.Page.Banner.BannerPosition", "BannerPosition")
+                        .WithMany("BannerItem")
+                        .HasForeignKey("PositionID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("BannerPosition");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
+                {
+                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
+                        .WithMany("FaqItem")
+                        .HasForeignKey("CategoryID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("FaqCategory");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.Member", b =>
+                {
+                    b.Navigation("EmailChangeLog");
+
+                    b.Navigation("EmailLog");
+
+                    b.Navigation("LoginLog");
+
+                    b.Navigation("MemberApprove");
+
+                    b.Navigation("NameChangeLog");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
+                {
+                    b.Navigation("BannerItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
+                {
+                    b.Navigation("FaqItem");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 105 - 0
backend/Migrations/DefaultDb/20250222130451_a2.cs

@@ -0,0 +1,105 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace bitforum.Migrations.DefaultDb
+{
+    /// <inheritdoc />
+    public partial class a2 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AlterColumn<string>(
+                name: "Code",
+                table: "EmailVerifyNumber",
+                type: "nvarchar(10)",
+                maxLength: 10,
+                nullable: false,
+                comment: "Code",
+                oldClrType: typeof(string),
+                oldType: "nvarchar(10)",
+                oldMaxLength: 10,
+                oldComment: "Token");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MemberGrade_IsActive",
+                table: "MemberGrade",
+                column: "IsActive");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_MemberGrade_Order",
+                table: "MemberGrade",
+                column: "Order");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_CreatedAt",
+                table: "Member",
+                column: "CreatedAt");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_DeletedAt",
+                table: "Member",
+                column: "DeletedAt");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Member_Gender",
+                table: "Member",
+                column: "Gender");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyToken_Token",
+                table: "EmailVerifyToken",
+                column: "Token");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_EmailVerifyNumber_Code",
+                table: "EmailVerifyNumber",
+                column: "Code");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropIndex(
+                name: "IX_MemberGrade_IsActive",
+                table: "MemberGrade");
+
+            migrationBuilder.DropIndex(
+                name: "IX_MemberGrade_Order",
+                table: "MemberGrade");
+
+            migrationBuilder.DropIndex(
+                name: "IX_Member_CreatedAt",
+                table: "Member");
+
+            migrationBuilder.DropIndex(
+                name: "IX_Member_DeletedAt",
+                table: "Member");
+
+            migrationBuilder.DropIndex(
+                name: "IX_Member_Gender",
+                table: "Member");
+
+            migrationBuilder.DropIndex(
+                name: "IX_EmailVerifyToken_Token",
+                table: "EmailVerifyToken");
+
+            migrationBuilder.DropIndex(
+                name: "IX_EmailVerifyNumber_Code",
+                table: "EmailVerifyNumber");
+
+            migrationBuilder.AlterColumn<string>(
+                name: "Code",
+                table: "EmailVerifyNumber",
+                type: "nvarchar(10)",
+                maxLength: 10,
+                nullable: false,
+                comment: "Token",
+                oldClrType: typeof(string),
+                oldType: "nvarchar(10)",
+                oldMaxLength: 10,
+                oldComment: "Code");
+        }
+    }
+}

+ 1862 - 0
backend/Migrations/DefaultDb/20250222131829_a3.Designer.cs

@@ -0,0 +1,1862 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace bitforum.Migrations.DefaultDb
+{
+    [DbContext(typeof(DefaultDbContext))]
+    [Migration("20250222131829_a3")]
+    partial class a3
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "8.0.13")
+                .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+            modelBuilder.Entity("bitforum.Models.Account.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(new[] { "Code" }, "IX_EmailVerifyNumber_Code");
+
+                    b.HasIndex(new[] { "Email" }, "IX_EmailVerifyNumber_Email");
+
+                    b.HasIndex(new[] { "Expiration" }, "IX_EmailVerifyNumber_Expiration");
+
+                    b.HasIndex(new[] { "IsVerified" }, "IX_EmailVerifyNumber_IsVerified");
+
+                    b.HasIndex(new[] { "Type" }, "IX_EmailVerifyNumber_Type");
+
+                    b.ToTable("EmailVerifyNumber", t =>
+                        {
+                            t.HasComment("이메일 인증 번호들");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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(new[] { "Email" }, "IX_EmailVerifyToken_Email");
+
+                    b.HasIndex(new[] { "Expiration" }, "IX_EmailVerifyToken_Expiration");
+
+                    b.HasIndex(new[] { "IsVerified" }, "IX_EmailVerifyToken_IsVerified");
+
+                    b.HasIndex(new[] { "Token" }, "IX_EmailVerifyToken_Token");
+
+                    b.HasIndex(new[] { "Type" }, "IX_EmailVerifyToken_Type");
+
+                    b.ToTable("EmailVerifyToken", t =>
+                        {
+                            t.HasComment("이메일 인증 토큰들");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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<long>("Coin")
+                        .HasColumnType("bigint")
+                        .HasComment("코인");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("가입 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .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<int>("Exp")
+                        .HasColumnType("int")
+                        .HasComment("경험치");
+
+                    b.Property<string>("FirstName")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("본명(성)");
+
+                    b.Property<int>("Followed")
+                        .HasColumnType("int")
+                        .HasComment("구독자");
+
+                    b.Property<int>("Following")
+                        .HasColumnType("int")
+                        .HasComment("구독 중");
+
+                    b.Property<string>("FullName")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("본명");
+
+                    b.Property<int?>("Gender")
+                        .HasColumnType("int")
+                        .HasComment("성별");
+
+                    b.Property<int?>("GradeID")
+                        .HasColumnType("int")
+                        .HasComment("회원등급 ID");
+
+                    b.Property<string>("Icon")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("아이콘");
+
+                    b.Property<string>("Intro")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("자기소개");
+
+                    b.Property<bool>("IsAdmin")
+                        .HasColumnType("bit")
+                        .HasComment("운영진 여부");
+
+                    b.Property<bool>("IsAuthCertified")
+                        .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?>("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<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>("Photo")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("사진");
+
+                    b.Property<string>("SID")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("SID");
+
+                    b.Property<string>("SignupIP")
+                        .IsRequired()
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("회원가입 시 IP");
+
+                    b.Property<string>("Summary")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("한마디");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("GradeID");
+
+                    b.HasIndex(new[] { "CreatedAt" }, "IX_Member_CreatedAt");
+
+                    b.HasIndex(new[] { "DeletedAt" }, "IX_Member_DeletedAt");
+
+                    b.HasIndex(new[] { "Email" }, "IX_Member_Email")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "FullName" }, "IX_Member_FullName");
+
+                    b.HasIndex(new[] { "Gender" }, "IX_Member_Gender");
+
+                    b.HasIndex(new[] { "IsAdmin" }, "IX_Member_IsAdmin");
+
+                    b.HasIndex(new[] { "IsAuthCertified" }, "IX_Member_IsAuthCertified");
+
+                    b.HasIndex(new[] { "IsDenied" }, "IX_Member_IsDenied");
+
+                    b.HasIndex(new[] { "IsEmailVerified" }, "IX_Member_IsEmailVerified");
+
+                    b.HasIndex(new[] { "IsWithdraw" }, "IX_Member_IsWithdraw");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Member_Name")
+                        .IsUnique()
+                        .HasFilter("[Name] IS NOT NULL");
+
+                    b.HasIndex(new[] { "Phone" }, "IX_Member_Phone");
+
+                    b.HasIndex(new[] { "SID" }, "IX_Member_SID")
+                        .IsUnique();
+
+                    b.ToTable("Member", t =>
+                        {
+                            t.HasComment("회원 정보");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.MemberApprove", b =>
+                {
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    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.HasKey("MemberID");
+
+                    b.ToTable("MemberApprove", t =>
+                        {
+                            t.HasComment("회원 동의 및 수신 여부");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("KorName")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("한글 명");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<int>("RequiredCoin")
+                        .HasColumnType("int")
+                        .HasComment("최소 코인(Coin)");
+
+                    b.Property<int>("RequiredExp")
+                        .HasColumnType("int")
+                        .HasComment("최소 경험치(Exp)");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "EngName" }, "IX_MemberGrade_EngName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_MemberGrade_IsActive");
+
+                    b.HasIndex(new[] { "KorName" }, "IX_MemberGrade_KorName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Order" }, "IX_MemberGrade_Order");
+
+                    b.ToTable("MemberGrade", t =>
+                        {
+                            t.HasComment("회원 등급");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.RefreshToken", 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<DateTime>("Expiration")
+                        .HasColumnType("datetime2")
+                        .HasComment("만료 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("nvarchar(128)")
+                        .HasComment("Token");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("RefreshToken");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Board", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardGroupID")
+                        .HasColumnType("int")
+                        .HasComment("분류 ID");
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 주소");
+
+                    b.Property<int>("Comments")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<bool>("IsSearch")
+                        .HasColumnType("bit")
+                        .HasComment("검색 여부");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 이름");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<int>("Posts")
+                        .HasColumnType("int")
+                        .HasComment("게시글 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardGroupID");
+
+                    b.HasIndex(new[] { "Code" }, "IX_Board_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Comments" }, "IX_Board_Comments");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Board_IsActive");
+
+                    b.HasIndex(new[] { "IsSearch" }, "IX_Board_IsSearch");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Board_Name");
+
+                    b.HasIndex(new[] { "Order" }, "IX_Board_Order");
+
+                    b.HasIndex(new[] { "Posts" }, "IX_Board_Posts");
+
+                    b.ToTable("Board");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.BoardGroup", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<long>("Boards")
+                        .HasColumnType("bigint")
+                        .HasComment("게시판 수");
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 분류 주소");
+
+                    b.Property<long>("Comments")
+                        .HasColumnType("bigint")
+                        .HasComment("댓글 수");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 분류 명");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<long>("Posts")
+                        .HasColumnType("bigint")
+                        .HasComment("게시글 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "Boards" }, "IX_BoardGroup_Boards");
+
+                    b.HasIndex(new[] { "Code" }, "IX_BoardGroup_Code");
+
+                    b.HasIndex(new[] { "Comments" }, "IX_BoardGroup_Comments");
+
+                    b.HasIndex(new[] { "Name" }, "IX_BoardGroup_Name");
+
+                    b.HasIndex(new[] { "Order" }, "IX_BoardGroup_Order");
+
+                    b.HasIndex(new[] { "Posts" }, "IX_BoardGroup_Posts");
+
+                    b.ToTable("BoardGroup");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.BoardMeta", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.ToTable("BoardMeta");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Comment", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<long>("Blames")
+                        .HasColumnType("bigint")
+                        .HasComment("신고 수");
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<string>("Content")
+                        .IsRequired()
+                        .HasMaxLength(4000)
+                        .HasColumnType("nvarchar(4000)")
+                        .HasComment("댓글 내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("삭제 일시");
+
+                    b.Property<int>("Depth")
+                        .HasColumnType("int")
+                        .HasComment("댓글 깊이");
+
+                    b.Property<long>("Dislikes")
+                        .HasColumnType("bigint")
+                        .HasComment("싫어요");
+
+                    b.Property<string>("Email")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("회원 이메일");
+
+                    b.Property<string>("IpAddress")
+                        .IsRequired()
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP");
+
+                    b.Property<bool>("IsDeleted")
+                        .HasColumnType("bit")
+                        .HasComment("삭제 여부");
+
+                    b.Property<bool>("IsSecret")
+                        .HasColumnType("bit")
+                        .HasComment("비밀글 여부");
+
+                    b.Property<long>("Likes")
+                        .HasColumnType("bigint")
+                        .HasComment("좋아요");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Name")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 이름");
+
+                    b.Property<int?>("ParentID")
+                        .HasColumnType("int")
+                        .HasComment("부모 댓글 ID");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("댓글 비밀번호");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<long>("Replies")
+                        .HasColumnType("bigint")
+                        .HasComment("대댓글 수");
+
+                    b.Property<string>("SID")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 SID");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex(new[] { "Blames" }, "IX_Comment_Blames");
+
+                    b.HasIndex(new[] { "CreatedAt" }, "IX_Comment_CreatedAt");
+
+                    b.HasIndex(new[] { "Dislikes" }, "IX_Comment_Dislikes");
+
+                    b.HasIndex(new[] { "Email" }, "IX_Comment_Email");
+
+                    b.HasIndex(new[] { "IsDeleted" }, "IX_Comment_IsDeleted");
+
+                    b.HasIndex(new[] { "IsSecret" }, "IX_Comment_IsSecret");
+
+                    b.HasIndex(new[] { "Likes" }, "IX_Comment_Likes");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Comment_Name");
+
+                    b.HasIndex(new[] { "ParentID" }, "IX_Comment_ParentID");
+
+                    b.HasIndex(new[] { "Replies" }, "IX_Comment_Replies");
+
+                    b.HasIndex(new[] { "SID" }, "IX_Comment_SID");
+
+                    b.ToTable("Comment");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.CommentMeta", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<bool>("AllowDeleteProtection")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 보호 기능 (삭제 시)");
+
+                    b.Property<bool>("AllowDisLike")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 비공감 사용");
+
+                    b.Property<bool>("AllowLike")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 공감 사용");
+
+                    b.Property<bool>("AllowSecret")
+                        .HasColumnType("bit")
+                        .HasComment("비밀글 사용");
+
+                    b.Property<bool>("AllowUpdateProtection")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 보호 기능 (수정 시)");
+
+                    b.Property<int>("BlameHideCount")
+                        .HasColumnType("int")
+                        .HasComment("댓글 신고 시 숨김");
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<string>("ContentPlaceholder")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("안내 문구");
+
+                    b.Property<int>("DeleteProtectionDays")
+                        .HasColumnType("int")
+                        .HasComment("댓글 삭제 금지 기간");
+
+                    b.Property<bool>("EnableComment")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 사용");
+
+                    b.Property<bool>("EnableCommentUpdateLog")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 변경 기록");
+
+                    b.Property<bool>("EnableEditor")
+                        .HasColumnType("bit")
+                        .HasComment("웹 에디터 사용");
+
+                    b.Property<int>("MaxContentLength")
+                        .HasColumnType("int")
+                        .HasComment("최대 입력 글자");
+
+                    b.Property<int>("MinContentLength")
+                        .HasColumnType("int")
+                        .HasComment("최소 입력 글자");
+
+                    b.Property<int>("PerPage")
+                        .HasColumnType("int")
+                        .HasComment("목록 표시");
+
+                    b.Property<bool>("ShowMemberIcon")
+                        .HasColumnType("bit")
+                        .HasComment("회원 아이콘 공개");
+
+                    b.Property<bool>("ShowMemberPhoto")
+                        .HasColumnType("bit")
+                        .HasComment("회원 사진 공개");
+
+                    b.Property<int>("UpdateProtectionDays")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수정 금지 기간");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.ToTable("CommentMeta");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Post", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<long>("Blames")
+                        .HasColumnType("bigint")
+                        .HasComment("신고 수");
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<long>("Comments")
+                        .HasColumnType("bigint")
+                        .HasComment("댓글 수");
+
+                    b.Property<string>("Content")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("삭제 일시");
+
+                    b.Property<long>("Dislikes")
+                        .HasColumnType("bigint")
+                        .HasComment("싫어요");
+
+                    b.Property<string>("Email")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("회원 이메일");
+
+                    b.Property<short>("Files")
+                        .HasColumnType("smallint")
+                        .HasComment("파일 수");
+
+                    b.Property<short>("Images")
+                        .HasColumnType("smallint")
+                        .HasComment("이미지 수");
+
+                    b.Property<string>("IpAddress")
+                        .IsRequired()
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP");
+
+                    b.Property<bool>("IsDeleted")
+                        .HasColumnType("bit")
+                        .HasComment("삭제 여부");
+
+                    b.Property<bool>("IsNotice")
+                        .HasColumnType("bit")
+                        .HasComment("일반 공지 여부");
+
+                    b.Property<bool>("IsReply")
+                        .HasColumnType("bit")
+                        .HasComment("답변 여부");
+
+                    b.Property<bool>("IsSecret")
+                        .HasColumnType("bit")
+                        .HasComment("비밀글 여부");
+
+                    b.Property<bool>("IsSpeaker")
+                        .HasColumnType("bit")
+                        .HasComment("전체 공지 여부");
+
+                    b.Property<DateTime?>("LastCommentUpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 댓글 일시");
+
+                    b.Property<DateTime?>("LastReplyUpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 답변 일시");
+
+                    b.Property<long>("Likes")
+                        .HasColumnType("bigint")
+                        .HasComment("좋아요");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Name")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 이름");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("게시글 비밀번호");
+
+                    b.Property<string>("SID")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 SID");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("제목");
+
+                    b.Property<short>("Tags")
+                        .HasColumnType("smallint")
+                        .HasComment("Tag 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.Property<short>("Videos")
+                        .HasColumnType("smallint")
+                        .HasComment("동영상 수");
+
+                    b.Property<long>("Views")
+                        .HasColumnType("bigint")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex(new[] { "Blames" }, "IX_Post_Blames");
+
+                    b.HasIndex(new[] { "Comments" }, "IX_Post_Comments");
+
+                    b.HasIndex(new[] { "CreatedAt" }, "IX_Post_CreatedAt");
+
+                    b.HasIndex(new[] { "Dislikes" }, "IX_Post_Dislikes");
+
+                    b.HasIndex(new[] { "Email" }, "IX_Post_Email");
+
+                    b.HasIndex(new[] { "Files" }, "IX_Post_Files");
+
+                    b.HasIndex(new[] { "Images" }, "IX_Post_Images");
+
+                    b.HasIndex(new[] { "IsDeleted" }, "IX_Post_IsDeleted");
+
+                    b.HasIndex(new[] { "IsNotice" }, "IX_Post_IsNotice");
+
+                    b.HasIndex(new[] { "IsReply" }, "IX_Post_IsReply");
+
+                    b.HasIndex(new[] { "IsSecret" }, "IX_Post_IsSecret");
+
+                    b.HasIndex(new[] { "IsSpeaker" }, "IX_Post_IsSpeaker");
+
+                    b.HasIndex(new[] { "LastCommentUpdatedAt" }, "IX_Post_LastCommentUpdatedAt");
+
+                    b.HasIndex(new[] { "LastReplyUpdatedAt" }, "IX_Post_LastReplyUpdatedAt");
+
+                    b.HasIndex(new[] { "Likes" }, "IX_Post_Likes");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Post_Name");
+
+                    b.HasIndex(new[] { "SID" }, "IX_Post_SID");
+
+                    b.HasIndex(new[] { "Subject" }, "IX_Post_Subject");
+
+                    b.HasIndex(new[] { "Tags" }, "IX_Post_Tags");
+
+                    b.HasIndex(new[] { "Videos" }, "IX_Post_Videos");
+
+                    b.HasIndex(new[] { "Views" }, "IX_Post_Views");
+
+                    b.ToTable("Post");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.PostMeta", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.ToTable("PostMeta");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Config", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(450)");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Key")
+                        .IsUnique();
+
+                    b.ToTable("Config");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterEmail")
+                        .IsRequired()
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("바꾼 이메일");
+
+                    b.Property<string>("BeforeEmail")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("이전 이메일");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_EmailChangeLog_MemberID");
+
+                    b.ToTable("EmailChangeLog", t =>
+                        {
+                            t.HasComment("이메일 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailLog", 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>("FromAddress")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("발신 주소");
+
+                    b.Property<string>("FromName")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("발신자");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Message")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime?>("ProcessedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("처리 일시");
+
+                    b.Property<string>("Status")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("처리 여부");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("제목");
+
+                    b.Property<string>("ToAddress")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("수신 주소");
+
+                    b.Property<string>("ToName")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("수신자");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_EmailLog_MemberID");
+
+                    b.HasIndex(new[] { "Status" }, "IX_EmailLog_Status");
+
+                    b.ToTable("EmailLog");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.LoginLog", 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(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Reason")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .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("MemberID");
+
+                    b.ToTable("LoginLog", t =>
+                        {
+                            t.HasComment("로그인 기록");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.NameChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterName")
+                        .IsRequired()
+                        .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<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_NameChangeLog_MemberID");
+
+                    b.ToTable("NameChangeLog", t =>
+                        {
+                            t.HasComment("별명 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.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<DateTime?>("EndAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("사용 기간 - 종료");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("int")
+                        .HasComment("세로 크기");
+
+                    b.Property<string>("Image")
+                        .HasMaxLength(1024)
+                        .HasColumnType("nvarchar(1024)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Link")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("주소");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("int")
+                        .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.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("int")
+                        .HasComment("가로 크기");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("PositionID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_BannerItem_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_BannerItem_Order");
+
+                    b.ToTable("BannerItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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(new[] { "Code" }, "IX_BannerPosition_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_BannerPosition_IsActive");
+
+                    b.ToTable("BannerPosition");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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")
+                        .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(new[] { "Code" }, "IX_Document_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
+
+                    b.ToTable("Document");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("분류 명");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
+
+                    b.ToTable("FaqCategory");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Answer")
+                        .HasColumnType("nvarchar(max)")
+                        .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<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<string>("Question")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("질문");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CategoryID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
+
+                    b.ToTable("FaqItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Content")
+                        .HasColumnType("nvarchar(max)")
+                        .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.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_Popup_Order");
+
+                    b.ToTable("Popup");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.Member", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.MemberGrade", "MemberGrade")
+                        .WithMany()
+                        .HasForeignKey("GradeID");
+
+                    b.Navigation("MemberGrade");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.MemberApprove", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithOne("MemberApprove")
+                        .HasForeignKey("bitforum.Models.Account.MemberApprove", "MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Board", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.BoardGroup", "BoardGroup")
+                        .WithMany("Board")
+                        .HasForeignKey("BoardGroupID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("BoardGroup");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.BoardMeta", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Comment", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID");
+
+                    b.HasOne("bitforum.Models.BBS.Comment", "Parent")
+                        .WithMany("Reply")
+                        .HasForeignKey("ParentID")
+                        .OnDelete(DeleteBehavior.NoAction);
+
+                    b.HasOne("bitforum.Models.BBS.Post", "Post")
+                        .WithMany("Comment")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Parent");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.CommentMeta", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Post", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany("Post")
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID");
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.PostMeta", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailChangeLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("EmailChangeLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("EmailLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.LoginLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("LoginLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.NameChangeLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("NameChangeLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
+                {
+                    b.HasOne("bitforum.Models.Page.Banner.BannerPosition", "BannerPosition")
+                        .WithMany("BannerItem")
+                        .HasForeignKey("PositionID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("BannerPosition");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
+                {
+                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
+                        .WithMany("FaqItem")
+                        .HasForeignKey("CategoryID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("FaqCategory");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.Member", b =>
+                {
+                    b.Navigation("EmailChangeLog");
+
+                    b.Navigation("EmailLog");
+
+                    b.Navigation("LoginLog");
+
+                    b.Navigation("MemberApprove");
+
+                    b.Navigation("NameChangeLog");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Board", b =>
+                {
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.BoardGroup", b =>
+                {
+                    b.Navigation("Board");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Comment", b =>
+                {
+                    b.Navigation("Reply");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Post", b =>
+                {
+                    b.Navigation("Comment");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
+                {
+                    b.Navigation("BannerItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
+                {
+                    b.Navigation("FaqItem");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 537 - 0
backend/Migrations/DefaultDb/20250222131829_a3.cs

@@ -0,0 +1,537 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace bitforum.Migrations.DefaultDb
+{
+    /// <inheritdoc />
+    public partial class a3 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "BoardGroup",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    Code = table.Column<string>(type: "nvarchar(70)", maxLength: 70, nullable: false, comment: "게시판 분류 주소"),
+                    Name = table.Column<string>(type: "nvarchar(70)", maxLength: 70, nullable: false, comment: "게시판 분류 명"),
+                    Order = table.Column<int>(type: "int", nullable: false, comment: "순서"),
+                    Boards = table.Column<long>(type: "bigint", nullable: false, comment: "게시판 수"),
+                    Posts = table.Column<long>(type: "bigint", nullable: false, comment: "게시글 수"),
+                    Comments = table.Column<long>(type: "bigint", nullable: false, comment: "댓글 수"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_BoardGroup", x => x.ID);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Board",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    BoardGroupID = table.Column<int>(type: "int", nullable: false, comment: "분류 ID"),
+                    Code = table.Column<string>(type: "nvarchar(70)", maxLength: 70, nullable: false, comment: "게시판 주소"),
+                    Name = table.Column<string>(type: "nvarchar(70)", maxLength: 70, nullable: false, comment: "게시판 이름"),
+                    Order = table.Column<int>(type: "int", nullable: false, comment: "순서"),
+                    IsSearch = table.Column<bool>(type: "bit", nullable: false, comment: "검색 여부"),
+                    IsActive = table.Column<bool>(type: "bit", nullable: false, comment: "사용 여부"),
+                    Posts = table.Column<int>(type: "int", nullable: false, comment: "게시글 수"),
+                    Comments = table.Column<int>(type: "int", nullable: false, comment: "댓글 수"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Board", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_Board_BoardGroup_BoardGroupID",
+                        column: x => x.BoardGroupID,
+                        principalTable: "BoardGroup",
+                        principalColumn: "ID");
+                });
+
+            migrationBuilder.CreateTable(
+                name: "BoardMeta",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    BoardID = table.Column<int>(type: "int", nullable: false, comment: "게시판 ID")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_BoardMeta", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_BoardMeta_Board_BoardID",
+                        column: x => x.BoardID,
+                        principalTable: "Board",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "CommentMeta",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    BoardID = table.Column<int>(type: "int", nullable: false, comment: "게시판 ID"),
+                    EnableComment = table.Column<bool>(type: "bit", nullable: false, comment: "댓글 사용"),
+                    PerPage = table.Column<int>(type: "int", nullable: false, comment: "목록 표시"),
+                    AllowLike = table.Column<bool>(type: "bit", nullable: false, comment: "댓글 공감 사용"),
+                    AllowDisLike = table.Column<bool>(type: "bit", nullable: false, comment: "댓글 비공감 사용"),
+                    ShowMemberPhoto = table.Column<bool>(type: "bit", nullable: false, comment: "회원 사진 공개"),
+                    ShowMemberIcon = table.Column<bool>(type: "bit", nullable: false, comment: "회원 아이콘 공개"),
+                    ContentPlaceholder = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true, comment: "안내 문구"),
+                    MinContentLength = table.Column<int>(type: "int", nullable: false, comment: "최소 입력 글자"),
+                    MaxContentLength = table.Column<int>(type: "int", nullable: false, comment: "최대 입력 글자"),
+                    EnableEditor = table.Column<bool>(type: "bit", nullable: false, comment: "웹 에디터 사용"),
+                    AllowSecret = table.Column<bool>(type: "bit", nullable: false, comment: "비밀글 사용"),
+                    BlameHideCount = table.Column<int>(type: "int", nullable: false, comment: "댓글 신고 시 숨김"),
+                    DeleteProtectionDays = table.Column<int>(type: "int", nullable: false, comment: "댓글 삭제 금지 기간"),
+                    UpdateProtectionDays = table.Column<int>(type: "int", nullable: false, comment: "댓글 수정 금지 기간"),
+                    AllowDeleteProtection = table.Column<bool>(type: "bit", nullable: false, comment: "댓글 보호 기능 (삭제 시)"),
+                    AllowUpdateProtection = table.Column<bool>(type: "bit", nullable: false, comment: "댓글 보호 기능 (수정 시)"),
+                    EnableCommentUpdateLog = table.Column<bool>(type: "bit", nullable: false, comment: "댓글 변경 기록")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_CommentMeta", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_CommentMeta_Board_BoardID",
+                        column: x => x.BoardID,
+                        principalTable: "Board",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Post",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    BoardID = table.Column<int>(type: "int", nullable: false, comment: "게시판 ID"),
+                    MemberID = table.Column<int>(type: "int", nullable: true, comment: "회원 ID"),
+                    Subject = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "제목"),
+                    Content = table.Column<string>(type: "nvarchar(max)", nullable: false, comment: "내용"),
+                    SID = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true, comment: "회원 SID"),
+                    Email = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: true, comment: "회원 이메일"),
+                    Name = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true, comment: "회원 이름"),
+                    Password = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "게시글 비밀번호"),
+                    IsReply = table.Column<bool>(type: "bit", nullable: false, comment: "답변 여부"),
+                    IsSecret = table.Column<bool>(type: "bit", nullable: false, comment: "비밀글 여부"),
+                    IsNotice = table.Column<bool>(type: "bit", nullable: false, comment: "일반 공지 여부"),
+                    IsSpeaker = table.Column<bool>(type: "bit", nullable: false, comment: "전체 공지 여부"),
+                    IsDeleted = table.Column<bool>(type: "bit", nullable: false, comment: "삭제 여부"),
+                    Views = table.Column<long>(type: "bigint", nullable: false, comment: "조회 수"),
+                    Likes = table.Column<long>(type: "bigint", nullable: false, comment: "좋아요"),
+                    Dislikes = table.Column<long>(type: "bigint", nullable: false, comment: "싫어요"),
+                    Comments = table.Column<long>(type: "bigint", nullable: false, comment: "댓글 수"),
+                    Blames = table.Column<long>(type: "bigint", nullable: false, comment: "신고 수"),
+                    Files = table.Column<short>(type: "smallint", nullable: false, comment: "파일 수"),
+                    Images = table.Column<short>(type: "smallint", nullable: false, comment: "이미지 수"),
+                    Videos = table.Column<short>(type: "smallint", nullable: false, comment: "동영상 수"),
+                    Tags = table.Column<short>(type: "smallint", nullable: false, comment: "Tag 수"),
+                    IpAddress = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false, comment: "IP"),
+                    UserAgent = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "User-Agent"),
+                    LastReplyUpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "마지막 답변 일시"),
+                    LastCommentUpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "마지막 댓글 일시"),
+                    DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "삭제 일시"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Post", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_Post_Board_BoardID",
+                        column: x => x.BoardID,
+                        principalTable: "Board",
+                        principalColumn: "ID");
+                    table.ForeignKey(
+                        name: "FK_Post_Member_MemberID",
+                        column: x => x.MemberID,
+                        principalTable: "Member",
+                        principalColumn: "ID");
+                });
+
+            migrationBuilder.CreateTable(
+                name: "PostMeta",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    BoardID = table.Column<int>(type: "int", nullable: false, comment: "게시판 ID")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_PostMeta", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_PostMeta_Board_BoardID",
+                        column: x => x.BoardID,
+                        principalTable: "Board",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "Comment",
+                columns: table => new
+                {
+                    ID = table.Column<int>(type: "int", nullable: false, comment: "PK")
+                        .Annotation("SqlServer:Identity", "1, 1"),
+                    BoardID = table.Column<int>(type: "int", nullable: false, comment: "게시판 ID"),
+                    PostID = table.Column<int>(type: "int", nullable: false, comment: "게시글 ID"),
+                    MemberID = table.Column<int>(type: "int", nullable: true, comment: "회원 ID"),
+                    ParentID = table.Column<int>(type: "int", nullable: true, comment: "부모 댓글 ID"),
+                    Depth = table.Column<int>(type: "int", nullable: false, comment: "댓글 깊이"),
+                    Content = table.Column<string>(type: "nvarchar(4000)", maxLength: 4000, nullable: false, comment: "댓글 내용"),
+                    SID = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true, comment: "회원 SID"),
+                    Email = table.Column<string>(type: "nvarchar(60)", maxLength: 60, nullable: true, comment: "회원 이메일"),
+                    Name = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true, comment: "회원 이름"),
+                    Password = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true, comment: "댓글 비밀번호"),
+                    IsSecret = table.Column<bool>(type: "bit", nullable: false, comment: "비밀글 여부"),
+                    IsDeleted = table.Column<bool>(type: "bit", nullable: false, comment: "삭제 여부"),
+                    Likes = table.Column<long>(type: "bigint", nullable: false, comment: "좋아요"),
+                    Dislikes = table.Column<long>(type: "bigint", nullable: false, comment: "싫어요"),
+                    Blames = table.Column<long>(type: "bigint", nullable: false, comment: "신고 수"),
+                    Replies = table.Column<long>(type: "bigint", nullable: false, comment: "대댓글 수"),
+                    IpAddress = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false, comment: "IP"),
+                    UserAgent = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false, comment: "User-Agent"),
+                    DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "삭제 일시"),
+                    UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true, comment: "수정 일시"),
+                    CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, comment: "등록 일시")
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_Comment", x => x.ID);
+                    table.ForeignKey(
+                        name: "FK_Comment_Board_BoardID",
+                        column: x => x.BoardID,
+                        principalTable: "Board",
+                        principalColumn: "ID",
+                        onDelete: ReferentialAction.Cascade);
+                    table.ForeignKey(
+                        name: "FK_Comment_Comment_ParentID",
+                        column: x => x.ParentID,
+                        principalTable: "Comment",
+                        principalColumn: "ID");
+                    table.ForeignKey(
+                        name: "FK_Comment_Member_MemberID",
+                        column: x => x.MemberID,
+                        principalTable: "Member",
+                        principalColumn: "ID");
+                    table.ForeignKey(
+                        name: "FK_Comment_Post_PostID",
+                        column: x => x.PostID,
+                        principalTable: "Post",
+                        principalColumn: "ID");
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Board_BoardGroupID",
+                table: "Board",
+                column: "BoardGroupID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Board_Code",
+                table: "Board",
+                column: "Code",
+                unique: true);
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Board_Comments",
+                table: "Board",
+                column: "Comments");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Board_IsActive",
+                table: "Board",
+                column: "IsActive");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Board_IsSearch",
+                table: "Board",
+                column: "IsSearch");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Board_Name",
+                table: "Board",
+                column: "Name");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Board_Order",
+                table: "Board",
+                column: "Order");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Board_Posts",
+                table: "Board",
+                column: "Posts");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BoardGroup_Boards",
+                table: "BoardGroup",
+                column: "Boards");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BoardGroup_Code",
+                table: "BoardGroup",
+                column: "Code");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BoardGroup_Comments",
+                table: "BoardGroup",
+                column: "Comments");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BoardGroup_Name",
+                table: "BoardGroup",
+                column: "Name");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BoardGroup_Order",
+                table: "BoardGroup",
+                column: "Order");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BoardGroup_Posts",
+                table: "BoardGroup",
+                column: "Posts");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_BoardMeta_BoardID",
+                table: "BoardMeta",
+                column: "BoardID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_Blames",
+                table: "Comment",
+                column: "Blames");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_BoardID",
+                table: "Comment",
+                column: "BoardID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_CreatedAt",
+                table: "Comment",
+                column: "CreatedAt");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_Dislikes",
+                table: "Comment",
+                column: "Dislikes");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_Email",
+                table: "Comment",
+                column: "Email");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_IsDeleted",
+                table: "Comment",
+                column: "IsDeleted");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_IsSecret",
+                table: "Comment",
+                column: "IsSecret");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_Likes",
+                table: "Comment",
+                column: "Likes");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_MemberID",
+                table: "Comment",
+                column: "MemberID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_Name",
+                table: "Comment",
+                column: "Name");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_ParentID",
+                table: "Comment",
+                column: "ParentID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_PostID",
+                table: "Comment",
+                column: "PostID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_Replies",
+                table: "Comment",
+                column: "Replies");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Comment_SID",
+                table: "Comment",
+                column: "SID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_CommentMeta_BoardID",
+                table: "CommentMeta",
+                column: "BoardID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Blames",
+                table: "Post",
+                column: "Blames");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_BoardID",
+                table: "Post",
+                column: "BoardID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Comments",
+                table: "Post",
+                column: "Comments");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_CreatedAt",
+                table: "Post",
+                column: "CreatedAt");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Dislikes",
+                table: "Post",
+                column: "Dislikes");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Email",
+                table: "Post",
+                column: "Email");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Files",
+                table: "Post",
+                column: "Files");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Images",
+                table: "Post",
+                column: "Images");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_IsDeleted",
+                table: "Post",
+                column: "IsDeleted");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_IsNotice",
+                table: "Post",
+                column: "IsNotice");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_IsReply",
+                table: "Post",
+                column: "IsReply");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_IsSecret",
+                table: "Post",
+                column: "IsSecret");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_IsSpeaker",
+                table: "Post",
+                column: "IsSpeaker");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_LastCommentUpdatedAt",
+                table: "Post",
+                column: "LastCommentUpdatedAt");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_LastReplyUpdatedAt",
+                table: "Post",
+                column: "LastReplyUpdatedAt");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Likes",
+                table: "Post",
+                column: "Likes");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_MemberID",
+                table: "Post",
+                column: "MemberID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Name",
+                table: "Post",
+                column: "Name");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_SID",
+                table: "Post",
+                column: "SID");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Subject",
+                table: "Post",
+                column: "Subject");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Tags",
+                table: "Post",
+                column: "Tags");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Videos",
+                table: "Post",
+                column: "Videos");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_Post_Views",
+                table: "Post",
+                column: "Views");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_PostMeta_BoardID",
+                table: "PostMeta",
+                column: "BoardID");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "BoardMeta");
+
+            migrationBuilder.DropTable(
+                name: "Comment");
+
+            migrationBuilder.DropTable(
+                name: "CommentMeta");
+
+            migrationBuilder.DropTable(
+                name: "PostMeta");
+
+            migrationBuilder.DropTable(
+                name: "Post");
+
+            migrationBuilder.DropTable(
+                name: "Board");
+
+            migrationBuilder.DropTable(
+                name: "BoardGroup");
+        }
+    }
+}

+ 2363 - 0
backend/Migrations/DefaultDb/20250222132132_a4.Designer.cs

@@ -0,0 +1,2363 @@
+// <auto-generated />
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace bitforum.Migrations.DefaultDb
+{
+    [DbContext(typeof(DefaultDbContext))]
+    [Migration("20250222132132_a4")]
+    partial class a4
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "8.0.13")
+                .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+            SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+            modelBuilder.Entity("bitforum.Models.Account.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(new[] { "Code" }, "IX_EmailVerifyNumber_Code");
+
+                    b.HasIndex(new[] { "Email" }, "IX_EmailVerifyNumber_Email");
+
+                    b.HasIndex(new[] { "Expiration" }, "IX_EmailVerifyNumber_Expiration");
+
+                    b.HasIndex(new[] { "IsVerified" }, "IX_EmailVerifyNumber_IsVerified");
+
+                    b.HasIndex(new[] { "Type" }, "IX_EmailVerifyNumber_Type");
+
+                    b.ToTable("EmailVerifyNumber", t =>
+                        {
+                            t.HasComment("이메일 인증 번호들");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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(new[] { "Email" }, "IX_EmailVerifyToken_Email");
+
+                    b.HasIndex(new[] { "Expiration" }, "IX_EmailVerifyToken_Expiration");
+
+                    b.HasIndex(new[] { "IsVerified" }, "IX_EmailVerifyToken_IsVerified");
+
+                    b.HasIndex(new[] { "Token" }, "IX_EmailVerifyToken_Token");
+
+                    b.HasIndex(new[] { "Type" }, "IX_EmailVerifyToken_Type");
+
+                    b.ToTable("EmailVerifyToken", t =>
+                        {
+                            t.HasComment("이메일 인증 토큰들");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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<long>("Coin")
+                        .HasColumnType("bigint")
+                        .HasComment("코인");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("가입 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .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<int>("Exp")
+                        .HasColumnType("int")
+                        .HasComment("경험치");
+
+                    b.Property<string>("FirstName")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("본명(성)");
+
+                    b.Property<int>("Followed")
+                        .HasColumnType("int")
+                        .HasComment("구독자");
+
+                    b.Property<int>("Following")
+                        .HasColumnType("int")
+                        .HasComment("구독 중");
+
+                    b.Property<string>("FullName")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("본명");
+
+                    b.Property<int?>("Gender")
+                        .HasColumnType("int")
+                        .HasComment("성별");
+
+                    b.Property<int?>("GradeID")
+                        .HasColumnType("int")
+                        .HasComment("회원등급 ID");
+
+                    b.Property<string>("Icon")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("아이콘");
+
+                    b.Property<string>("Intro")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("자기소개");
+
+                    b.Property<bool>("IsAdmin")
+                        .HasColumnType("bit")
+                        .HasComment("운영진 여부");
+
+                    b.Property<bool>("IsAuthCertified")
+                        .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?>("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<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>("Photo")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("사진");
+
+                    b.Property<string>("SID")
+                        .IsRequired()
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("SID");
+
+                    b.Property<string>("SignupIP")
+                        .IsRequired()
+                        .HasMaxLength(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("회원가입 시 IP");
+
+                    b.Property<string>("Summary")
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("한마디");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("GradeID");
+
+                    b.HasIndex(new[] { "CreatedAt" }, "IX_Member_CreatedAt");
+
+                    b.HasIndex(new[] { "DeletedAt" }, "IX_Member_DeletedAt");
+
+                    b.HasIndex(new[] { "Email" }, "IX_Member_Email")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "FullName" }, "IX_Member_FullName");
+
+                    b.HasIndex(new[] { "Gender" }, "IX_Member_Gender");
+
+                    b.HasIndex(new[] { "IsAdmin" }, "IX_Member_IsAdmin");
+
+                    b.HasIndex(new[] { "IsAuthCertified" }, "IX_Member_IsAuthCertified");
+
+                    b.HasIndex(new[] { "IsDenied" }, "IX_Member_IsDenied");
+
+                    b.HasIndex(new[] { "IsEmailVerified" }, "IX_Member_IsEmailVerified");
+
+                    b.HasIndex(new[] { "IsWithdraw" }, "IX_Member_IsWithdraw");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Member_Name")
+                        .IsUnique()
+                        .HasFilter("[Name] IS NOT NULL");
+
+                    b.HasIndex(new[] { "Phone" }, "IX_Member_Phone");
+
+                    b.HasIndex(new[] { "SID" }, "IX_Member_SID")
+                        .IsUnique();
+
+                    b.ToTable("Member", t =>
+                        {
+                            t.HasComment("회원 정보");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.MemberApprove", b =>
+                {
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    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.HasKey("MemberID");
+
+                    b.ToTable("MemberApprove", t =>
+                        {
+                            t.HasComment("회원 동의 및 수신 여부");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.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")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("KorName")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("한글 명");
+
+                    b.Property<short>("Order")
+                        .HasColumnType("smallint")
+                        .HasComment("순서");
+
+                    b.Property<int>("RequiredCoin")
+                        .HasColumnType("int")
+                        .HasComment("최소 코인(Coin)");
+
+                    b.Property<int>("RequiredExp")
+                        .HasColumnType("int")
+                        .HasComment("최소 경험치(Exp)");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "EngName" }, "IX_MemberGrade_EngName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_MemberGrade_IsActive");
+
+                    b.HasIndex(new[] { "KorName" }, "IX_MemberGrade_KorName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Order" }, "IX_MemberGrade_Order");
+
+                    b.ToTable("MemberGrade", t =>
+                        {
+                            t.HasComment("회원 등급");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.RefreshToken", 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<DateTime>("Expiration")
+                        .HasColumnType("datetime2")
+                        .HasComment("만료 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Token")
+                        .IsRequired()
+                        .HasMaxLength(128)
+                        .HasColumnType("nvarchar(128)")
+                        .HasComment("Token");
+
+                    b.HasKey("ID");
+
+                    b.ToTable("RefreshToken");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Board", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardGroupID")
+                        .HasColumnType("int")
+                        .HasComment("분류 ID");
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 주소");
+
+                    b.Property<int>("Comments")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<bool>("IsSearch")
+                        .HasColumnType("bit")
+                        .HasComment("검색 여부");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 이름");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<int>("Posts")
+                        .HasColumnType("int")
+                        .HasComment("게시글 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardGroupID");
+
+                    b.HasIndex(new[] { "Code" }, "IX_Board_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Comments" }, "IX_Board_Comments");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Board_IsActive");
+
+                    b.HasIndex(new[] { "IsSearch" }, "IX_Board_IsSearch");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Board_Name");
+
+                    b.HasIndex(new[] { "Order" }, "IX_Board_Order");
+
+                    b.HasIndex(new[] { "Posts" }, "IX_Board_Posts");
+
+                    b.ToTable("Board");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.BoardGroup", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<long>("Boards")
+                        .HasColumnType("bigint")
+                        .HasComment("게시판 수");
+
+                    b.Property<string>("Code")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 분류 주소");
+
+                    b.Property<long>("Comments")
+                        .HasColumnType("bigint")
+                        .HasComment("댓글 수");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(70)
+                        .HasColumnType("nvarchar(70)")
+                        .HasComment("게시판 분류 명");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<long>("Posts")
+                        .HasColumnType("bigint")
+                        .HasComment("게시글 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "Boards" }, "IX_BoardGroup_Boards");
+
+                    b.HasIndex(new[] { "Code" }, "IX_BoardGroup_Code");
+
+                    b.HasIndex(new[] { "Comments" }, "IX_BoardGroup_Comments");
+
+                    b.HasIndex(new[] { "Name" }, "IX_BoardGroup_Name");
+
+                    b.HasIndex(new[] { "Order" }, "IX_BoardGroup_Order");
+
+                    b.HasIndex(new[] { "Posts" }, "IX_BoardGroup_Posts");
+
+                    b.ToTable("BoardGroup");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.BoardMeta", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.ToTable("BoardMeta");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Comment", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<long>("Blames")
+                        .HasColumnType("bigint")
+                        .HasComment("신고 수");
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<string>("Content")
+                        .IsRequired()
+                        .HasMaxLength(4000)
+                        .HasColumnType("nvarchar(4000)")
+                        .HasComment("댓글 내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("삭제 일시");
+
+                    b.Property<int>("Depth")
+                        .HasColumnType("int")
+                        .HasComment("댓글 깊이");
+
+                    b.Property<long>("Dislikes")
+                        .HasColumnType("bigint")
+                        .HasComment("싫어요");
+
+                    b.Property<string>("Email")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("회원 이메일");
+
+                    b.Property<string>("IpAddress")
+                        .IsRequired()
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP");
+
+                    b.Property<bool>("IsDeleted")
+                        .HasColumnType("bit")
+                        .HasComment("삭제 여부");
+
+                    b.Property<bool>("IsSecret")
+                        .HasColumnType("bit")
+                        .HasComment("비밀글 여부");
+
+                    b.Property<long>("Likes")
+                        .HasColumnType("bigint")
+                        .HasComment("좋아요");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Name")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 이름");
+
+                    b.Property<int?>("ParentID")
+                        .HasColumnType("int")
+                        .HasComment("부모 댓글 ID");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("댓글 비밀번호");
+
+                    b.Property<int>("PostID")
+                        .HasColumnType("int")
+                        .HasComment("게시글 ID");
+
+                    b.Property<long>("Replies")
+                        .HasColumnType("bigint")
+                        .HasComment("대댓글 수");
+
+                    b.Property<string>("SID")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 SID");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex("PostID");
+
+                    b.HasIndex(new[] { "Blames" }, "IX_Comment_Blames");
+
+                    b.HasIndex(new[] { "CreatedAt" }, "IX_Comment_CreatedAt");
+
+                    b.HasIndex(new[] { "Dislikes" }, "IX_Comment_Dislikes");
+
+                    b.HasIndex(new[] { "Email" }, "IX_Comment_Email");
+
+                    b.HasIndex(new[] { "IsDeleted" }, "IX_Comment_IsDeleted");
+
+                    b.HasIndex(new[] { "IsSecret" }, "IX_Comment_IsSecret");
+
+                    b.HasIndex(new[] { "Likes" }, "IX_Comment_Likes");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Comment_Name");
+
+                    b.HasIndex(new[] { "ParentID" }, "IX_Comment_ParentID");
+
+                    b.HasIndex(new[] { "Replies" }, "IX_Comment_Replies");
+
+                    b.HasIndex(new[] { "SID" }, "IX_Comment_SID");
+
+                    b.ToTable("Comment");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.CommentMeta", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<bool>("AllowDeleteProtection")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 보호 기능 (삭제 시)");
+
+                    b.Property<bool>("AllowDisLike")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 비공감 사용");
+
+                    b.Property<bool>("AllowLike")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 공감 사용");
+
+                    b.Property<bool>("AllowSecret")
+                        .HasColumnType("bit")
+                        .HasComment("비밀글 사용");
+
+                    b.Property<bool>("AllowUpdateProtection")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 보호 기능 (수정 시)");
+
+                    b.Property<int>("BlameHideCount")
+                        .HasColumnType("int")
+                        .HasComment("댓글 신고 시 숨김");
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<string>("ContentPlaceholder")
+                        .HasMaxLength(1000)
+                        .HasColumnType("nvarchar(1000)")
+                        .HasComment("안내 문구");
+
+                    b.Property<int>("DeleteProtectionDays")
+                        .HasColumnType("int")
+                        .HasComment("댓글 삭제 금지 기간");
+
+                    b.Property<bool>("EnableComment")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 사용");
+
+                    b.Property<bool>("EnableCommentUpdateLog")
+                        .HasColumnType("bit")
+                        .HasComment("댓글 변경 기록");
+
+                    b.Property<bool>("EnableEditor")
+                        .HasColumnType("bit")
+                        .HasComment("웹 에디터 사용");
+
+                    b.Property<int>("MaxContentLength")
+                        .HasColumnType("int")
+                        .HasComment("최대 입력 글자");
+
+                    b.Property<int>("MinContentLength")
+                        .HasColumnType("int")
+                        .HasComment("최소 입력 글자");
+
+                    b.Property<int>("PerPage")
+                        .HasColumnType("int")
+                        .HasComment("목록 표시");
+
+                    b.Property<bool>("ShowMemberIcon")
+                        .HasColumnType("bit")
+                        .HasComment("회원 아이콘 공개");
+
+                    b.Property<bool>("ShowMemberPhoto")
+                        .HasColumnType("bit")
+                        .HasComment("회원 사진 공개");
+
+                    b.Property<int>("UpdateProtectionDays")
+                        .HasColumnType("int")
+                        .HasComment("댓글 수정 금지 기간");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.ToTable("CommentMeta");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Post", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<long>("Blames")
+                        .HasColumnType("bigint")
+                        .HasComment("신고 수");
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.Property<long>("Comments")
+                        .HasColumnType("bigint")
+                        .HasComment("댓글 수");
+
+                    b.Property<string>("Content")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("삭제 일시");
+
+                    b.Property<long>("Dislikes")
+                        .HasColumnType("bigint")
+                        .HasComment("싫어요");
+
+                    b.Property<string>("Email")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("회원 이메일");
+
+                    b.Property<short>("Files")
+                        .HasColumnType("smallint")
+                        .HasComment("파일 수");
+
+                    b.Property<short>("Images")
+                        .HasColumnType("smallint")
+                        .HasComment("이미지 수");
+
+                    b.Property<string>("IpAddress")
+                        .IsRequired()
+                        .HasMaxLength(50)
+                        .HasColumnType("nvarchar(50)")
+                        .HasComment("IP");
+
+                    b.Property<bool>("IsDeleted")
+                        .HasColumnType("bit")
+                        .HasComment("삭제 여부");
+
+                    b.Property<bool>("IsNotice")
+                        .HasColumnType("bit")
+                        .HasComment("일반 공지 여부");
+
+                    b.Property<bool>("IsReply")
+                        .HasColumnType("bit")
+                        .HasComment("답변 여부");
+
+                    b.Property<bool>("IsSecret")
+                        .HasColumnType("bit")
+                        .HasComment("비밀글 여부");
+
+                    b.Property<bool>("IsSpeaker")
+                        .HasColumnType("bit")
+                        .HasComment("전체 공지 여부");
+
+                    b.Property<DateTime?>("LastCommentUpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 댓글 일시");
+
+                    b.Property<DateTime?>("LastReplyUpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("마지막 답변 일시");
+
+                    b.Property<long>("Likes")
+                        .HasColumnType("bigint")
+                        .HasComment("좋아요");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Name")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 이름");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("게시글 비밀번호");
+
+                    b.Property<string>("SID")
+                        .HasMaxLength(20)
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("회원 SID");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("제목");
+
+                    b.Property<short>("Tags")
+                        .HasColumnType("smallint")
+                        .HasComment("Tag 수");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<string>("UserAgent")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("User-Agent");
+
+                    b.Property<short>("Videos")
+                        .HasColumnType("smallint")
+                        .HasComment("동영상 수");
+
+                    b.Property<long>("Views")
+                        .HasColumnType("bigint")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.HasIndex("MemberID");
+
+                    b.HasIndex(new[] { "Blames" }, "IX_Post_Blames");
+
+                    b.HasIndex(new[] { "Comments" }, "IX_Post_Comments");
+
+                    b.HasIndex(new[] { "CreatedAt" }, "IX_Post_CreatedAt");
+
+                    b.HasIndex(new[] { "Dislikes" }, "IX_Post_Dislikes");
+
+                    b.HasIndex(new[] { "Email" }, "IX_Post_Email");
+
+                    b.HasIndex(new[] { "Files" }, "IX_Post_Files");
+
+                    b.HasIndex(new[] { "Images" }, "IX_Post_Images");
+
+                    b.HasIndex(new[] { "IsDeleted" }, "IX_Post_IsDeleted");
+
+                    b.HasIndex(new[] { "IsNotice" }, "IX_Post_IsNotice");
+
+                    b.HasIndex(new[] { "IsReply" }, "IX_Post_IsReply");
+
+                    b.HasIndex(new[] { "IsSecret" }, "IX_Post_IsSecret");
+
+                    b.HasIndex(new[] { "IsSpeaker" }, "IX_Post_IsSpeaker");
+
+                    b.HasIndex(new[] { "LastCommentUpdatedAt" }, "IX_Post_LastCommentUpdatedAt");
+
+                    b.HasIndex(new[] { "LastReplyUpdatedAt" }, "IX_Post_LastReplyUpdatedAt");
+
+                    b.HasIndex(new[] { "Likes" }, "IX_Post_Likes");
+
+                    b.HasIndex(new[] { "Name" }, "IX_Post_Name");
+
+                    b.HasIndex(new[] { "SID" }, "IX_Post_SID");
+
+                    b.HasIndex(new[] { "Subject" }, "IX_Post_Subject");
+
+                    b.HasIndex(new[] { "Tags" }, "IX_Post_Tags");
+
+                    b.HasIndex(new[] { "Videos" }, "IX_Post_Videos");
+
+                    b.HasIndex(new[] { "Views" }, "IX_Post_Views");
+
+                    b.ToTable("Post");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.PostMeta", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<int>("BoardID")
+                        .HasColumnType("int")
+                        .HasComment("게시판 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("BoardID");
+
+                    b.ToTable("PostMeta");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Config", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2");
+
+                    b.Property<string>("Description")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(450)");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("nvarchar(max)");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("Key")
+                        .IsUnique();
+
+                    b.ToTable("Config");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterEmail")
+                        .IsRequired()
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("바꾼 이메일");
+
+                    b.Property<string>("BeforeEmail")
+                        .HasMaxLength(40)
+                        .HasColumnType("nvarchar(40)")
+                        .HasComment("이전 이메일");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("등록 일시");
+
+                    b.Property<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_EmailChangeLog_MemberID");
+
+                    b.ToTable("EmailChangeLog", t =>
+                        {
+                            t.HasComment("이메일 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailLog", 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>("FromAddress")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("발신 주소");
+
+                    b.Property<string>("FromName")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("발신자");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Message")
+                        .HasColumnType("nvarchar(max)")
+                        .HasComment("내용");
+
+                    b.Property<DateTime?>("ProcessedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("처리 일시");
+
+                    b.Property<string>("Status")
+                        .IsRequired()
+                        .HasColumnType("nvarchar(20)")
+                        .HasComment("처리 여부");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("제목");
+
+                    b.Property<string>("ToAddress")
+                        .IsRequired()
+                        .HasMaxLength(120)
+                        .HasColumnType("nvarchar(120)")
+                        .HasComment("수신 주소");
+
+                    b.Property<string>("ToName")
+                        .HasMaxLength(60)
+                        .HasColumnType("nvarchar(60)")
+                        .HasComment("수신자");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_EmailLog_MemberID");
+
+                    b.HasIndex(new[] { "Status" }, "IX_EmailLog_Status");
+
+                    b.ToTable("EmailLog");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.LoginLog", 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(15)
+                        .HasColumnType("nvarchar(15)")
+                        .HasComment("IP Address");
+
+                    b.Property<int?>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.Property<string>("Reason")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .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("MemberID");
+
+                    b.ToTable("LoginLog", t =>
+                        {
+                            t.HasComment("로그인 기록");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.NameChangeLog", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("AfterName")
+                        .IsRequired()
+                        .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<int>("MemberID")
+                        .HasColumnType("int")
+                        .HasComment("회원 ID");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "MemberID" }, "IX_NameChangeLog_MemberID");
+
+                    b.ToTable("NameChangeLog", t =>
+                        {
+                            t.HasComment("별명 변경 내역");
+                        });
+                });
+
+            modelBuilder.Entity("bitforum.Models.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<DateTime?>("EndAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("사용 기간 - 종료");
+
+                    b.Property<int?>("Height")
+                        .HasColumnType("int")
+                        .HasComment("세로 크기");
+
+                    b.Property<string>("Image")
+                        .HasMaxLength(1024)
+                        .HasColumnType("nvarchar(1024)")
+                        .HasComment("이미지");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("bit")
+                        .HasComment("사용 여부");
+
+                    b.Property<string>("Link")
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("주소");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("int")
+                        .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.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.Property<int?>("Width")
+                        .HasColumnType("int")
+                        .HasComment("가로 크기");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("PositionID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_BannerItem_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_BannerItem_Order");
+
+                    b.ToTable("BannerItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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(new[] { "Code" }, "IX_BannerPosition_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_BannerPosition_IsActive");
+
+                    b.ToTable("BannerPosition");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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")
+                        .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(new[] { "Code" }, "IX_Document_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
+
+                    b.ToTable("Document");
+                });
+
+            modelBuilder.Entity("bitforum.Models.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<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<string>("Subject")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("분류 명");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "Code" }, "IX_FaqCategory_Code")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "Order" }, "IX_FaqCategory_Order");
+
+                    b.ToTable("FaqCategory");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Answer")
+                        .HasColumnType("nvarchar(max)")
+                        .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<int>("Order")
+                        .HasColumnType("int")
+                        .HasComment("순서");
+
+                    b.Property<string>("Question")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("nvarchar(255)")
+                        .HasComment("질문");
+
+                    b.Property<DateTime?>("UpdatedAt")
+                        .HasColumnType("datetime2")
+                        .HasComment("수정 일시");
+
+                    b.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex("CategoryID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_FaqItem_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_FaqItem_Order");
+
+                    b.ToTable("FaqItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Popup", b =>
+                {
+                    b.Property<int>("ID")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("int")
+                        .HasComment("PK");
+
+                    SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
+
+                    b.Property<string>("Content")
+                        .HasColumnType("nvarchar(max)")
+                        .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.Property<int>("Views")
+                        .HasColumnType("int")
+                        .HasComment("조회 수");
+
+                    b.HasKey("ID");
+
+                    b.HasIndex(new[] { "IsActive" }, "IX_Popup_IsActive");
+
+                    b.HasIndex(new[] { "Order" }, "IX_Popup_Order");
+
+                    b.ToTable("Popup");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.Member", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.MemberGrade", "MemberGrade")
+                        .WithMany()
+                        .HasForeignKey("GradeID");
+
+                    b.Navigation("MemberGrade");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.MemberApprove", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithOne("MemberApprove")
+                        .HasForeignKey("bitforum.Models.Account.MemberApprove", "MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Board", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.BoardGroup", "BoardGroup")
+                        .WithMany("Board")
+                        .HasForeignKey("BoardGroupID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("BoardGroup");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.BoardMeta", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsOne("bitforum.Models.BBS.BoardExpMeta", "Exp", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<int>("CommentWriteExp")
+                                .HasColumnType("int")
+                                .HasComment("댓글 작성");
+
+                            b1.Property<int>("CommentWriteExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("댓글 작성 기한");
+
+                            b1.Property<int>("CommentWriteUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("댓글 작성 취소");
+
+                            b1.Property<bool>("EnableExp")
+                                .HasColumnType("bit")
+                                .HasComment("경험치 기능");
+
+                            b1.Property<short>("FileDownloadExp")
+                                .HasColumnType("smallint")
+                                .HasComment("파일 다운로드");
+
+                            b1.Property<int>("FileUploadExp")
+                                .HasColumnType("int")
+                                .HasComment("파일 업로드");
+
+                            b1.Property<int>("FileUploadExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("파일 업로드 기한");
+
+                            b1.Property<int>("FileUploadUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("파일 업로드 취소");
+
+                            b1.Property<int>("OtherCommentDisLikeExp")
+                                .HasColumnType("int")
+                                .HasComment("댓글 싫어요");
+
+                            b1.Property<int>("OtherCommentDisLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("댓글 싫어요 기한");
+
+                            b1.Property<int>("OtherCommentDisLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("댓글 싫어요 취소");
+
+                            b1.Property<int>("OtherCommentLikeExp")
+                                .HasColumnType("int")
+                                .HasComment("댓글 좋아요");
+
+                            b1.Property<int>("OtherCommentLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("댓글 좋아요 기한");
+
+                            b1.Property<int>("OtherCommentLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("댓글 좋아요 취소");
+
+                            b1.Property<int>("OtherPostDisLikeExp")
+                                .HasColumnType("int")
+                                .HasComment("게시글 싫어요");
+
+                            b1.Property<int>("OtherPostDisLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("게시글 싫어요 기한");
+
+                            b1.Property<int>("OtherPostDisLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("게시글 싫어요 취소");
+
+                            b1.Property<int>("OtherPostLikeExp")
+                                .HasColumnType("int")
+                                .HasComment("게시글 좋아요");
+
+                            b1.Property<int>("OtherPostLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("게시글 좋아요 기한");
+
+                            b1.Property<int>("OtherPostLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("게시글 좋아요 취소");
+
+                            b1.Property<short>("OtherPostReadExp")
+                                .HasColumnType("smallint")
+                                .HasComment("게시글 읽기");
+
+                            b1.Property<int>("OtherPostReadExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("게시글 읽기 기한");
+
+                            b1.Property<int>("OtherPostReadUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("게시글 읽기 취소");
+
+                            b1.Property<short>("OwnCommentDisLikeExp")
+                                .HasColumnType("smallint")
+                                .HasComment("내 댓글 싫어요");
+
+                            b1.Property<int>("OwnCommentDisLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("내 댓글 싫어요 기한");
+
+                            b1.Property<int>("OwnCommentDisLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("내 댓글 싫어요 취소");
+
+                            b1.Property<int>("OwnCommentLikeExp")
+                                .HasColumnType("int")
+                                .HasComment("내 댓글 좋아요");
+
+                            b1.Property<int>("OwnCommentLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("내 댓글 좋아요 기한");
+
+                            b1.Property<int>("OwnCommentLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("내 댓글 좋아요 취소");
+
+                            b1.Property<short>("OwnPostDisLikeExp")
+                                .HasColumnType("smallint")
+                                .HasComment("내 게시글 싫어요");
+
+                            b1.Property<int>("OwnPostDisLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("내 게시글 싫어요 기한");
+
+                            b1.Property<int>("OwnPostDisLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("내 게시글 싫어요 취소");
+
+                            b1.Property<int>("OwnPostLikeExp")
+                                .HasColumnType("int")
+                                .HasComment("내 게시글 좋아요");
+
+                            b1.Property<int>("OwnPostLikeExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("내 게시글 좋아요 기한");
+
+                            b1.Property<int>("OwnPostLikeUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("내 게시글 좋아요 취소");
+
+                            b1.Property<int>("OwnPostReadExp")
+                                .HasColumnType("int")
+                                .HasComment("내 게시글 읽힘");
+
+                            b1.Property<int>("OwnPostReadExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("내 게시글 읽힘 기한");
+
+                            b1.Property<int>("OwnPostReadUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("내 게시글 읽힘 취소");
+
+                            b1.Property<int>("PostWriteExp")
+                                .HasColumnType("int")
+                                .HasComment("게시글 작성");
+
+                            b1.Property<int>("PostWriteExpWithinDays")
+                                .HasColumnType("int")
+                                .HasComment("게시글 작성 기한");
+
+                            b1.Property<int>("PostWriteUndoExp")
+                                .HasColumnType("int")
+                                .HasComment("게시글 작성 취소");
+
+                            b1.Property<bool>("ShowExpGuide")
+                                .HasColumnType("bit")
+                                .HasComment("경험치 안내");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("bitforum.Models.BBS.BoardListMeta", "List", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AlwaysShowWriteButton")
+                                .HasColumnType("bit")
+                                .HasComment("글쓰기 버튼 보이기");
+
+                            b1.Property<bool>("ExceptNotice")
+                                .HasColumnType("bit")
+                                .HasComment("공지사항 제외 여부");
+
+                            b1.Property<bool>("ExceptSpeaker")
+                                .HasColumnType("bit")
+                                .HasComment("전체공지 제외 여부");
+
+                            b1.Property<string>("FooterContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasComment("하단 내용");
+
+                            b1.Property<string>("HeaderContent")
+                                .HasColumnType("nvarchar(max)")
+                                .HasComment("상단 내용");
+
+                            b1.Property<bool>("IsHotIcon")
+                                .HasColumnType("bit")
+                                .HasComment("HOT 사용 여부");
+
+                            b1.Property<bool>("IsNewIcon")
+                                .HasColumnType("bit")
+                                .HasComment("NEW 사용 여부");
+
+                            b1.Property<int?>("Layout")
+                                .HasColumnType("int")
+                                .HasComment("게시판 종류");
+
+                            b1.Property<int?>("OrderBy")
+                                .HasColumnType("int")
+                                .HasComment("기본 정렬");
+
+                            b1.Property<byte>("PerPage")
+                                .HasColumnType("tinyint")
+                                .HasComment("목록 표시");
+
+                            b1.Property<bool>("ShowFooterListView")
+                                .HasColumnType("bit")
+                                .HasComment("하단 목록 보이기");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("bitforum.Models.BBS.BoardNotifyMeta", "Notify", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<int?>("CommentWriteNotify")
+                                .HasColumnType("int")
+                                .HasComment("댓글 작성 시");
+
+                            b1.Property<int?>("PostWriteNotify")
+                                .HasColumnType("int")
+                                .HasComment("게시글 작성 시");
+
+                            b1.Property<int?>("ReplyWriteNotify")
+                                .HasColumnType("int")
+                                .HasComment("답글 작성 시");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.OwnsOne("bitforum.Models.BBS.BoardPermissionMeta", "Permission", b1 =>
+                        {
+                            b1.Property<int>("BoardMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<int>("BoardAccess")
+                                .HasColumnType("int")
+                                .HasComment("게시판 접근");
+
+                            b1.Property<int>("CommentView")
+                                .HasColumnType("int")
+                                .HasComment("댓글 목록");
+
+                            b1.Property<int>("CommentWrite")
+                                .HasColumnType("int")
+                                .HasComment("댓글 작성");
+
+                            b1.Property<int>("FileDownload")
+                                .HasColumnType("int")
+                                .HasComment("파일 다운로드");
+
+                            b1.Property<int>("FileUpload")
+                                .HasColumnType("int")
+                                .HasComment("파일 업로드");
+
+                            b1.Property<int>("PostView")
+                                .HasColumnType("int")
+                                .HasComment("글 열람");
+
+                            b1.Property<int>("PostWrite")
+                                .HasColumnType("int")
+                                .HasComment("글 작성");
+
+                            b1.Property<int>("ReplyWrite")
+                                .HasColumnType("int")
+                                .HasComment("답글 작성");
+
+                            b1.HasKey("BoardMetaID");
+
+                            b1.ToTable("BoardMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("BoardMetaID");
+                        });
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Exp")
+                        .IsRequired();
+
+                    b.Navigation("List")
+                        .IsRequired();
+
+                    b.Navigation("Notify")
+                        .IsRequired();
+
+                    b.Navigation("Permission")
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Comment", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID");
+
+                    b.HasOne("bitforum.Models.BBS.Comment", "Parent")
+                        .WithMany("Reply")
+                        .HasForeignKey("ParentID")
+                        .OnDelete(DeleteBehavior.NoAction);
+
+                    b.HasOne("bitforum.Models.BBS.Post", "Post")
+                        .WithMany("Comment")
+                        .HasForeignKey("PostID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+
+                    b.Navigation("Parent");
+
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.CommentMeta", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("Board");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Post", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany("Post")
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany()
+                        .HasForeignKey("MemberID");
+
+                    b.Navigation("Board");
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.PostMeta", b =>
+                {
+                    b.HasOne("bitforum.Models.BBS.Board", "Board")
+                        .WithMany()
+                        .HasForeignKey("BoardID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.OwnsOne("bitforum.Models.BBS.PostGeneralMeta", "General", b1 =>
+                        {
+                            b1.Property<int>("PostMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AllowDeleteProtection")
+                                .HasColumnType("bit")
+                                .HasComment("게시글 보호 기능 (삭제 시)");
+
+                            b1.Property<bool>("AllowUpdateProtection")
+                                .HasColumnType("bit")
+                                .HasComment("게시글 보호 기능 (수정 시)");
+
+                            b1.Property<int>("DeleteProtectionDays")
+                                .HasColumnType("int")
+                                .HasComment("게시글 삭제 금지 기간");
+
+                            b1.Property<bool>("EnableFileDownLog")
+                                .HasColumnType("bit")
+                                .HasComment("다운로드 기록");
+
+                            b1.Property<bool>("EnablePostUpdateLog")
+                                .HasColumnType("bit")
+                                .HasComment("게시글 변경 기록");
+
+                            b1.Property<int>("UpdateProtectionDays")
+                                .HasColumnType("int")
+                                .HasComment("게시글 수정/삭제 금지 기간");
+
+                            b1.HasKey("PostMetaID");
+
+                            b1.ToTable("PostMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("PostMetaID");
+                        });
+
+                    b.OwnsOne("bitforum.Models.BBS.PostViewMeta", "List", b1 =>
+                        {
+                            b1.Property<int>("PostMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AllowBlame")
+                                .HasColumnType("bit")
+                                .HasComment("신고 기능");
+
+                            b1.Property<bool>("AllowBookmark")
+                                .HasColumnType("bit")
+                                .HasComment("즐겨찾기 기능");
+
+                            b1.Property<bool>("AllowContentLinkTargetBlank")
+                                .HasColumnType("bit")
+                                .HasComment("전체공지 제외 여부");
+
+                            b1.Property<bool>("AllowDislike")
+                                .HasColumnType("bit")
+                                .HasComment("비공감 기능");
+
+                            b1.Property<bool>("AllowLike")
+                                .HasColumnType("bit")
+                                .HasComment("공감 기능");
+
+                            b1.Property<bool>("AllowPostUrlCopy")
+                                .HasColumnType("bit")
+                                .HasComment("주소 복사 버튼");
+
+                            b1.Property<bool>("AllowPostUrlQrCode")
+                                .HasColumnType("bit")
+                                .HasComment("글 주소 QR 코드");
+
+                            b1.Property<bool>("AllowPrevNextBotton")
+                                .HasColumnType("bit")
+                                .HasComment("이전글, 다음글 버튼");
+
+                            b1.Property<bool>("AllowPrint")
+                                .HasColumnType("bit")
+                                .HasComment("본문 인쇄 기능");
+
+                            b1.Property<bool>("AllowSnsShare")
+                                .HasColumnType("bit")
+                                .HasComment("SNS 보내기 기능");
+
+                            b1.Property<int>("BlameHideCount")
+                                .HasColumnType("int")
+                                .HasComment("신고 시 숨김");
+
+                            b1.Property<bool>("ShowMemberIcon")
+                                .HasColumnType("bit")
+                                .HasComment("회원 아이콘 공개");
+
+                            b1.Property<bool>("ShowMemberPhoto")
+                                .HasColumnType("bit")
+                                .HasComment("회원 사진 공개");
+
+                            b1.Property<bool>("ShowMemberRegDate")
+                                .HasColumnType("bit")
+                                .HasComment("회원 가입일 공개");
+
+                            b1.HasKey("PostMetaID");
+
+                            b1.ToTable("PostMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("PostMetaID");
+                        });
+
+                    b.OwnsOne("bitforum.Models.BBS.PostWriteMeta", "Write", b1 =>
+                        {
+                            b1.Property<int>("PostMetaID")
+                                .HasColumnType("int");
+
+                            b1.Property<bool>("AllowEditor")
+                                .HasColumnType("bit")
+                                .HasComment("웹 에디터 사용");
+
+                            b1.Property<bool>("AllowSaveExternalImage")
+                                .HasColumnType("bit")
+                                .HasComment("외부 이미지 수집");
+
+                            b1.Property<bool>("AllowSecret")
+                                .HasColumnType("bit")
+                                .HasComment("비밀글 사용");
+
+                            b1.Property<bool>("AllowTag")
+                                .HasColumnType("bit")
+                                .HasComment("Tag 사용");
+
+                            b1.Property<string>("DefaultContent")
+                                .HasMaxLength(4000)
+                                .HasColumnType("nvarchar(4000)")
+                                .HasComment("기본 내용");
+
+                            b1.Property<string>("DefaultSubject")
+                                .HasMaxLength(255)
+                                .HasColumnType("nvarchar(255)")
+                                .HasComment("기본 제목");
+
+                            b1.Property<bool>("EnableUploadFile")
+                                .HasColumnType("bit")
+                                .HasComment("파일 사용");
+
+                            b1.Property<string>("FooterContent")
+                                .HasMaxLength(4000)
+                                .HasColumnType("nvarchar(4000)")
+                                .HasComment("작성란 하단 내용");
+
+                            b1.Property<string>("HeaderContent")
+                                .HasMaxLength(4000)
+                                .HasColumnType("nvarchar(4000)")
+                                .HasComment("작성란 상단 내용");
+
+                            b1.Property<string>("UploadFileExtension")
+                                .HasMaxLength(200)
+                                .HasColumnType("nvarchar(200)")
+                                .HasComment("파일 허용 확장자");
+
+                            b1.Property<int>("UploadFileMaxSize")
+                                .HasColumnType("int")
+                                .HasComment("파일 용량 제한");
+
+                            b1.Property<byte>("UploadFilesLimit")
+                                .HasColumnType("tinyint")
+                                .HasComment("파일 개수 제한");
+
+                            b1.HasKey("PostMetaID");
+
+                            b1.ToTable("PostMeta");
+
+                            b1.WithOwner()
+                                .HasForeignKey("PostMetaID");
+                        });
+
+                    b.Navigation("Board");
+
+                    b.Navigation("General")
+                        .IsRequired();
+
+                    b.Navigation("List")
+                        .IsRequired();
+
+                    b.Navigation("Write")
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailChangeLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("EmailChangeLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.EmailLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("EmailLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.LoginLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("LoginLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.SetNull);
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Log.NameChangeLog", b =>
+                {
+                    b.HasOne("bitforum.Models.Account.Member", "Member")
+                        .WithMany("NameChangeLog")
+                        .HasForeignKey("MemberID")
+                        .OnDelete(DeleteBehavior.NoAction)
+                        .IsRequired();
+
+                    b.Navigation("Member");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerItem", b =>
+                {
+                    b.HasOne("bitforum.Models.Page.Banner.BannerPosition", "BannerPosition")
+                        .WithMany("BannerItem")
+                        .HasForeignKey("PositionID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("BannerPosition");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqItem", b =>
+                {
+                    b.HasOne("bitforum.Models.Page.Faq.FaqCategory", "FaqCategory")
+                        .WithMany("FaqItem")
+                        .HasForeignKey("CategoryID")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("FaqCategory");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Account.Member", b =>
+                {
+                    b.Navigation("EmailChangeLog");
+
+                    b.Navigation("EmailLog");
+
+                    b.Navigation("LoginLog");
+
+                    b.Navigation("MemberApprove");
+
+                    b.Navigation("NameChangeLog");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Board", b =>
+                {
+                    b.Navigation("Post");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.BoardGroup", b =>
+                {
+                    b.Navigation("Board");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Comment", b =>
+                {
+                    b.Navigation("Reply");
+                });
+
+            modelBuilder.Entity("bitforum.Models.BBS.Post", b =>
+                {
+                    b.Navigation("Comment");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Banner.BannerPosition", b =>
+                {
+                    b.Navigation("BannerItem");
+                });
+
+            modelBuilder.Entity("bitforum.Models.Page.Faq.FaqCategory", b =>
+                {
+                    b.Navigation("FaqItem");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 1163 - 0
backend/Migrations/DefaultDb/20250222132132_a4.cs

@@ -0,0 +1,1163 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace bitforum.Migrations.DefaultDb
+{
+    /// <inheritdoc />
+    public partial class a4 : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<bool>(
+                name: "General_AllowDeleteProtection",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "게시글 보호 기능 (삭제 시)");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "General_AllowUpdateProtection",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "게시글 보호 기능 (수정 시)");
+
+            migrationBuilder.AddColumn<int>(
+                name: "General_DeleteProtectionDays",
+                table: "PostMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 삭제 금지 기간");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "General_EnableFileDownLog",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "다운로드 기록");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "General_EnablePostUpdateLog",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "게시글 변경 기록");
+
+            migrationBuilder.AddColumn<int>(
+                name: "General_UpdateProtectionDays",
+                table: "PostMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 수정/삭제 금지 기간");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowBlame",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "신고 기능");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowBookmark",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "즐겨찾기 기능");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowContentLinkTargetBlank",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "전체공지 제외 여부");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowDislike",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "비공감 기능");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowLike",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "공감 기능");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowPostUrlCopy",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "주소 복사 버튼");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowPostUrlQrCode",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "글 주소 QR 코드");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowPrevNextBotton",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "이전글, 다음글 버튼");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowPrint",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "본문 인쇄 기능");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AllowSnsShare",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "SNS 보내기 기능");
+
+            migrationBuilder.AddColumn<int>(
+                name: "List_BlameHideCount",
+                table: "PostMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "신고 시 숨김");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_ShowMemberIcon",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "회원 아이콘 공개");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_ShowMemberPhoto",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "회원 사진 공개");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_ShowMemberRegDate",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "회원 가입일 공개");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "Write_AllowEditor",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "웹 에디터 사용");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "Write_AllowSaveExternalImage",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "외부 이미지 수집");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "Write_AllowSecret",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "비밀글 사용");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "Write_AllowTag",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "Tag 사용");
+
+            migrationBuilder.AddColumn<string>(
+                name: "Write_DefaultContent",
+                table: "PostMeta",
+                type: "nvarchar(4000)",
+                maxLength: 4000,
+                nullable: true,
+                comment: "기본 내용");
+
+            migrationBuilder.AddColumn<string>(
+                name: "Write_DefaultSubject",
+                table: "PostMeta",
+                type: "nvarchar(255)",
+                maxLength: 255,
+                nullable: true,
+                comment: "기본 제목");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "Write_EnableUploadFile",
+                table: "PostMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "파일 사용");
+
+            migrationBuilder.AddColumn<string>(
+                name: "Write_FooterContent",
+                table: "PostMeta",
+                type: "nvarchar(4000)",
+                maxLength: 4000,
+                nullable: true,
+                comment: "작성란 하단 내용");
+
+            migrationBuilder.AddColumn<string>(
+                name: "Write_HeaderContent",
+                table: "PostMeta",
+                type: "nvarchar(4000)",
+                maxLength: 4000,
+                nullable: true,
+                comment: "작성란 상단 내용");
+
+            migrationBuilder.AddColumn<string>(
+                name: "Write_UploadFileExtension",
+                table: "PostMeta",
+                type: "nvarchar(200)",
+                maxLength: 200,
+                nullable: true,
+                comment: "파일 허용 확장자");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Write_UploadFileMaxSize",
+                table: "PostMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "파일 용량 제한");
+
+            migrationBuilder.AddColumn<byte>(
+                name: "Write_UploadFilesLimit",
+                table: "PostMeta",
+                type: "tinyint",
+                nullable: false,
+                defaultValue: (byte)0,
+                comment: "파일 개수 제한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_CommentWriteExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 작성");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_CommentWriteExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 작성 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_CommentWriteUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 작성 취소");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "Exp_EnableExp",
+                table: "BoardMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "경험치 기능");
+
+            migrationBuilder.AddColumn<short>(
+                name: "Exp_FileDownloadExp",
+                table: "BoardMeta",
+                type: "smallint",
+                nullable: false,
+                defaultValue: (short)0,
+                comment: "파일 다운로드");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_FileUploadExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "파일 업로드");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_FileUploadExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "파일 업로드 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_FileUploadUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "파일 업로드 취소");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherCommentDisLikeExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 싫어요");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherCommentDisLikeExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 싫어요 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherCommentDisLikeUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 싫어요 취소");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherCommentLikeExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 좋아요");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherCommentLikeExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 좋아요 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherCommentLikeUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 좋아요 취소");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherPostDisLikeExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 싫어요");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherPostDisLikeExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 싫어요 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherPostDisLikeUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 싫어요 취소");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherPostLikeExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 좋아요");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherPostLikeExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 좋아요 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherPostLikeUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 좋아요 취소");
+
+            migrationBuilder.AddColumn<short>(
+                name: "Exp_OtherPostReadExp",
+                table: "BoardMeta",
+                type: "smallint",
+                nullable: false,
+                defaultValue: (short)0,
+                comment: "게시글 읽기");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherPostReadExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 읽기 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OtherPostReadUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 읽기 취소");
+
+            migrationBuilder.AddColumn<short>(
+                name: "Exp_OwnCommentDisLikeExp",
+                table: "BoardMeta",
+                type: "smallint",
+                nullable: false,
+                defaultValue: (short)0,
+                comment: "내 댓글 싫어요");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnCommentDisLikeExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 댓글 싫어요 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnCommentDisLikeUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 댓글 싫어요 취소");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnCommentLikeExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 댓글 좋아요");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnCommentLikeExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 댓글 좋아요 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnCommentLikeUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 댓글 좋아요 취소");
+
+            migrationBuilder.AddColumn<short>(
+                name: "Exp_OwnPostDisLikeExp",
+                table: "BoardMeta",
+                type: "smallint",
+                nullable: false,
+                defaultValue: (short)0,
+                comment: "내 게시글 싫어요");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnPostDisLikeExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 게시글 싫어요 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnPostDisLikeUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 게시글 싫어요 취소");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnPostLikeExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 게시글 좋아요");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnPostLikeExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 게시글 좋아요 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnPostLikeUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 게시글 좋아요 취소");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnPostReadExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 게시글 읽힘");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnPostReadExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 게시글 읽힘 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_OwnPostReadUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "내 게시글 읽힘 취소");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_PostWriteExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 작성");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_PostWriteExpWithinDays",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 작성 기한");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Exp_PostWriteUndoExp",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시글 작성 취소");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "Exp_ShowExpGuide",
+                table: "BoardMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "경험치 안내");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_AlwaysShowWriteButton",
+                table: "BoardMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "글쓰기 버튼 보이기");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_ExceptNotice",
+                table: "BoardMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "공지사항 제외 여부");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_ExceptSpeaker",
+                table: "BoardMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "전체공지 제외 여부");
+
+            migrationBuilder.AddColumn<string>(
+                name: "List_FooterContent",
+                table: "BoardMeta",
+                type: "nvarchar(max)",
+                nullable: true,
+                comment: "하단 내용");
+
+            migrationBuilder.AddColumn<string>(
+                name: "List_HeaderContent",
+                table: "BoardMeta",
+                type: "nvarchar(max)",
+                nullable: true,
+                comment: "상단 내용");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_IsHotIcon",
+                table: "BoardMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "HOT 사용 여부");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_IsNewIcon",
+                table: "BoardMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "NEW 사용 여부");
+
+            migrationBuilder.AddColumn<int>(
+                name: "List_Layout",
+                table: "BoardMeta",
+                type: "int",
+                nullable: true,
+                comment: "게시판 종류");
+
+            migrationBuilder.AddColumn<int>(
+                name: "List_OrderBy",
+                table: "BoardMeta",
+                type: "int",
+                nullable: true,
+                comment: "기본 정렬");
+
+            migrationBuilder.AddColumn<byte>(
+                name: "List_PerPage",
+                table: "BoardMeta",
+                type: "tinyint",
+                nullable: false,
+                defaultValue: (byte)0,
+                comment: "목록 표시");
+
+            migrationBuilder.AddColumn<bool>(
+                name: "List_ShowFooterListView",
+                table: "BoardMeta",
+                type: "bit",
+                nullable: false,
+                defaultValue: false,
+                comment: "하단 목록 보이기");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Notify_CommentWriteNotify",
+                table: "BoardMeta",
+                type: "int",
+                nullable: true,
+                comment: "댓글 작성 시");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Notify_PostWriteNotify",
+                table: "BoardMeta",
+                type: "int",
+                nullable: true,
+                comment: "게시글 작성 시");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Notify_ReplyWriteNotify",
+                table: "BoardMeta",
+                type: "int",
+                nullable: true,
+                comment: "답글 작성 시");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Permission_BoardAccess",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "게시판 접근");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Permission_CommentView",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 목록");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Permission_CommentWrite",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "댓글 작성");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Permission_FileDownload",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "파일 다운로드");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Permission_FileUpload",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "파일 업로드");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Permission_PostView",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "글 열람");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Permission_PostWrite",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "글 작성");
+
+            migrationBuilder.AddColumn<int>(
+                name: "Permission_ReplyWrite",
+                table: "BoardMeta",
+                type: "int",
+                nullable: false,
+                defaultValue: 0,
+                comment: "답글 작성");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "General_AllowDeleteProtection",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "General_AllowUpdateProtection",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "General_DeleteProtectionDays",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "General_EnableFileDownLog",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "General_EnablePostUpdateLog",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "General_UpdateProtectionDays",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowBlame",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowBookmark",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowContentLinkTargetBlank",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowDislike",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowLike",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowPostUrlCopy",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowPostUrlQrCode",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowPrevNextBotton",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowPrint",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AllowSnsShare",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_BlameHideCount",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_ShowMemberIcon",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_ShowMemberPhoto",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_ShowMemberRegDate",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_AllowEditor",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_AllowSaveExternalImage",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_AllowSecret",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_AllowTag",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_DefaultContent",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_DefaultSubject",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_EnableUploadFile",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_FooterContent",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_HeaderContent",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_UploadFileExtension",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_UploadFileMaxSize",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Write_UploadFilesLimit",
+                table: "PostMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_CommentWriteExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_CommentWriteExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_CommentWriteUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_EnableExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_FileDownloadExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_FileUploadExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_FileUploadExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_FileUploadUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherCommentDisLikeExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherCommentDisLikeExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherCommentDisLikeUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherCommentLikeExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherCommentLikeExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherCommentLikeUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostDisLikeExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostDisLikeExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostDisLikeUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostLikeExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostLikeExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostLikeUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostReadExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostReadExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OtherPostReadUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnCommentDisLikeExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnCommentDisLikeExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnCommentDisLikeUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnCommentLikeExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnCommentLikeExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnCommentLikeUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostDisLikeExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostDisLikeExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostDisLikeUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostLikeExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostLikeExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostLikeUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostReadExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostReadExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_OwnPostReadUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_PostWriteExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_PostWriteExpWithinDays",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_PostWriteUndoExp",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Exp_ShowExpGuide",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_AlwaysShowWriteButton",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_ExceptNotice",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_ExceptSpeaker",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_FooterContent",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_HeaderContent",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_IsHotIcon",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_IsNewIcon",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_Layout",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_OrderBy",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_PerPage",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "List_ShowFooterListView",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Notify_CommentWriteNotify",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Notify_PostWriteNotify",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Notify_ReplyWriteNotify",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Permission_BoardAccess",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Permission_CommentView",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Permission_CommentWrite",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Permission_FileDownload",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Permission_FileUpload",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Permission_PostView",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Permission_PostWrite",
+                table: "BoardMeta");
+
+            migrationBuilder.DropColumn(
+                name: "Permission_ReplyWrite",
+                table: "BoardMeta");
+        }
+    }
+}

部分文件因为文件数量过多而无法显示