X\choro 10 ヶ月 前
コミット
14d5fe7a64
86 ファイル変更4920 行追加622 行削除
  1. BIN
      backend/.vs/ProjectEvaluation/bitforum.metadata.v9.bin
  2. BIN
      backend/.vs/ProjectEvaluation/bitforum.projects.v9.bin
  3. BIN
      backend/.vs/ProjectEvaluation/bitforum.strings.v9.bin
  4. BIN
      backend/.vs/bitforum/CopilotIndices/17.12.38.29086/CodeChunks.db
  5. BIN
      backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db
  6. BIN
      backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db-shm
  7. BIN
      backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db-wal
  8. BIN
      backend/.vs/bitforum/DesignTimeBuild/.dtbcache.v2
  9. BIN
      backend/.vs/bitforum/v17/.futdcache.v2
  10. BIN
      backend/.vs/bitforum/v17/.suo
  11. 61 95
      backend/.vs/bitforum/v17/DocumentLayout.backup.json
  12. 50 101
      backend/.vs/bitforum/v17/DocumentLayout.json
  13. 10 4
      backend/Constants/MenuData.cs
  14. 299 0
      backend/Controllers/Page/Banner/ItemController.cs
  15. 131 0
      backend/Controllers/Page/Banner/PositionController.cs
  16. 7 6
      backend/Controllers/Page/DocumentController.cs
  17. 25 33
      backend/Controllers/Page/Faq/CategoryController.cs
  18. 265 0
      backend/Controllers/Page/Faq/ItemController.cs
  19. 244 0
      backend/Controllers/Page/PopupController.cs
  20. 4 0
      backend/Database/DefaultDbContext.cs
  21. 6 0
      backend/Helpers/Func.cs
  22. 251 0
      backend/Migrations/DefaultDb/20250119234313_AddPopupTable.Designer.cs
  23. 68 0
      backend/Migrations/DefaultDb/20250119234313_AddPopupTable.cs
  24. 250 0
      backend/Migrations/DefaultDb/20250120034312_AddBannerTables.Designer.cs
  25. 40 0
      backend/Migrations/DefaultDb/20250120034312_AddBannerTables.cs
  26. 364 0
      backend/Migrations/DefaultDb/20250120035304_UpdateFieldInBannerItem.Designer.cs
  27. 94 0
      backend/Migrations/DefaultDb/20250120035304_UpdateFieldInBannerItem.cs
  28. 163 2
      backend/Migrations/DefaultDb/DefaultDbContextModelSnapshot.cs
  29. 75 0
      backend/Models/Page/Banner/Item.cs
  30. 41 0
      backend/Models/Page/Banner/Position.cs
  31. 1 1
      backend/Models/Page/Document.cs
  32. 6 3
      backend/Models/Page/Faq/Item.cs
  33. 55 0
      backend/Models/Page/Popup.cs
  34. 89 0
      backend/Models/Pagination.cs
  35. 2 1
      backend/Program.cs
  36. 93 0
      backend/Services/FileUploadService.cs
  37. 188 0
      backend/Views/Page/Banner/Item/Edit.cshtml
  38. 144 0
      backend/Views/Page/Banner/Item/Index.cshtml
  39. 149 0
      backend/Views/Page/Banner/Item/Write.cshtml
  40. 197 0
      backend/Views/Page/Banner/Position/Index.cshtml
  41. 17 0
      backend/Views/Page/Banner/_Navbar.cshtml
  42. 22 24
      backend/Views/Page/Document/Edit.cshtml
  43. 19 13
      backend/Views/Page/Document/Index.cshtml
  44. 14 14
      backend/Views/Page/Document/Write.cshtml
  45. 29 23
      backend/Views/Page/Faq/Category/Index.cshtml
  46. 84 0
      backend/Views/Page/Faq/Item/Edit.cshtml
  47. 113 0
      backend/Views/Page/Faq/Item/Index.cshtml
  48. 70 0
      backend/Views/Page/Faq/Item/Write.cshtml
  49. 17 0
      backend/Views/Page/Faq/_Navbar.cshtml
  50. 105 0
      backend/Views/Page/Popup/Edit.cshtml
  51. 117 0
      backend/Views/Page/Popup/Index.cshtml
  52. 85 0
      backend/Views/Page/Popup/Write.cshtml
  53. 15 7
      backend/Views/SCSS/admin.scss
  54. 15 0
      backend/Views/SCSS/site.scss
  55. 2 1
      backend/Views/Shared/_Layout.cshtml
  56. 14 2
      backend/Views/Shared/_MenuItem.cshtml
  57. 53 0
      backend/Views/Shared/_Pagination.cshtml
  58. BIN
      backend/bin/Debug/net8.0/bitforum.dll
  59. BIN
      backend/bin/Debug/net8.0/bitforum.exe
  60. BIN
      backend/bin/Debug/net8.0/bitforum.pdb
  61. 148 66
      backend/bin/Debug/net8.0/bitforum.staticwebassets.endpoints.json
  62. 0 0
      backend/bin/Debug/net8.0/bitforum.staticwebassets.runtime.json
  63. 27 0
      backend/bitforum.csproj
  64. BIN
      backend/obj/Debug/net8.0/apphost.exe
  65. 5 4
      backend/obj/Debug/net8.0/bitforum.AssemblyInfo.cs
  66. 1 1
      backend/obj/Debug/net8.0/bitforum.AssemblyInfoInputs.cache
  67. 54 2
      backend/obj/Debug/net8.0/bitforum.GeneratedMSBuildEditorConfig.editorconfig
  68. 1 1
      backend/obj/Debug/net8.0/bitforum.csproj.CoreCompileInputs.cache
  69. BIN
      backend/obj/Debug/net8.0/bitforum.dll
  70. BIN
      backend/obj/Debug/net8.0/bitforum.pdb
  71. BIN
      backend/obj/Debug/net8.0/ref/bitforum.dll
  72. BIN
      backend/obj/Debug/net8.0/refint/bitforum.dll
  73. 148 66
      backend/obj/Debug/net8.0/staticwebassets.build.endpoints.json
  74. 180 77
      backend/obj/Debug/net8.0/staticwebassets.build.json
  75. 0 0
      backend/obj/Debug/net8.0/staticwebassets.development.json
  76. 4 0
      backend/obj/Debug/net8.0/staticwebassets.pack.json
  77. 1 0
      backend/obj/Debug/net8.0/staticwebassets.removed.txt
  78. 1 0
      backend/obj/Debug/net8.0/staticwebassets.upToDateCheck.txt
  79. 40 28
      backend/obj/Debug/net8.0/staticwebassets/msbuild.bitforum.Microsoft.AspNetCore.StaticWebAssetEndpoints.props
  80. 28 10
      backend/obj/Debug/net8.0/staticwebassets/msbuild.bitforum.Microsoft.AspNetCore.StaticWebAssets.props
  81. 14 1
      backend/wwwroot/css/admin.css
  82. 1 1
      backend/wwwroot/css/admin.min.css
  83. 1 1
      backend/wwwroot/css/site.css
  84. 1 1
      backend/wwwroot/css/site.min.css
  85. BIN
      backend/wwwroot/editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png
  86. 102 33
      backend/wwwroot/js/site.js

BIN
backend/.vs/ProjectEvaluation/bitforum.metadata.v9.bin


BIN
backend/.vs/ProjectEvaluation/bitforum.projects.v9.bin


BIN
backend/.vs/ProjectEvaluation/bitforum.strings.v9.bin


BIN
backend/.vs/bitforum/CopilotIndices/17.12.38.29086/CodeChunks.db


BIN
backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db


BIN
backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db-shm


BIN
backend/.vs/bitforum/CopilotIndices/17.12.38.29086/SemanticSymbols.db-wal


BIN
backend/.vs/bitforum/DesignTimeBuild/.dtbcache.v2


BIN
backend/.vs/bitforum/v17/.futdcache.v2


BIN
backend/.vs/bitforum/v17/.suo


+ 61 - 95
backend/.vs/bitforum/v17/DocumentLayout.backup.json

@@ -3,36 +3,28 @@
   "WorkspaceRootPath": "E:\\workspace\\bitforum\\backend\\",
   "Documents": [
     {
-      "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\\views\\page\\faq\\item\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\faq\\item\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     },
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\faq\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\faq\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\faq\\item\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\faq\\item\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     },
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\views\\faqcategoryviewmodel.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\views\\faqcategoryviewmodel.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\controllers\\page\\banner\\itemcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:controllers\\page\\banner\\itemcontroller.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
     },
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\page\\faq\\category.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\page\\faq\\category.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\banner\\item\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\banner\\item\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     },
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\page\\faq\\item.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\page\\faq\\item.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\banner\\item\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\banner\\item\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     },
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\page\\document.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\page\\document.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
-    },
-    {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\document\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\document\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
-    },
-    {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\document\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\document\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\banner\\item\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\banner\\item\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     }
   ],
   "DocumentGroupContainers": [
@@ -42,7 +34,7 @@
       "DocumentGroups": [
         {
           "DockedWidth": 200,
-          "SelectedChildIndex": 12,
+          "SelectedChildIndex": 5,
           "Children": [
             {
               "$type": "Bookmark",
@@ -66,106 +58,80 @@
             },
             {
               "$type": "Document",
-              "DocumentIndex": 2,
-              "Title": "FaqCategoryViewModel.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\Views\\FaqCategoryViewModel.cs",
-              "RelativeDocumentMoniker": "Models\\Views\\FaqCategoryViewModel.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\Views\\FaqCategoryViewModel.cs",
-              "RelativeToolTip": "Models\\Views\\FaqCategoryViewModel.cs",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAYAAAAhAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T16:34:07.431Z",
-              "EditorCaption": ""
-            },
-            {
-              "$type": "Document",
-              "DocumentIndex": 4,
-              "Title": "Item.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Faq\\Item.cs",
-              "RelativeDocumentMoniker": "Models\\Page\\Faq\\Item.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Faq\\Item.cs",
-              "RelativeToolTip": "Models\\Page\\Faq\\Item.cs",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAAB0AAAAbAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T16:10:05.653Z",
-              "EditorCaption": ""
-            },
-            {
-              "$type": "Document",
-              "DocumentIndex": 7,
+              "DocumentIndex": 0,
               "Title": "Edit.cshtml",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Document\\Edit.cshtml",
-              "RelativeDocumentMoniker": "Views\\Page\\Document\\Edit.cshtml",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Document\\Edit.cshtml",
-              "RelativeToolTip": "Views\\Page\\Document\\Edit.cshtml",
-              "ViewState": "AgIAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Item\\Edit.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Faq\\Item\\Edit.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Item\\Edit.cshtml",
+              "RelativeToolTip": "Views\\Page\\Faq\\Item\\Edit.cshtml",
+              "ViewState": "AgIAACcAAAAAAAAAAAAAAE8AAAAjAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
-              "WhenOpened": "2025-01-17T16:09:21.702Z",
+              "WhenOpened": "2025-01-20T16:10:00.435Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
-              "DocumentIndex": 6,
-              "Title": "Index.cshtml",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Document\\Index.cshtml",
-              "RelativeDocumentMoniker": "Views\\Page\\Document\\Index.cshtml",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Document\\Index.cshtml",
-              "RelativeToolTip": "Views\\Page\\Document\\Index.cshtml",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+              "DocumentIndex": 1,
+              "Title": "Write.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Item\\Write.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Faq\\Item\\Write.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Item\\Write.cshtml",
+              "RelativeToolTip": "Views\\Page\\Faq\\Item\\Write.cshtml",
+              "ViewState": "AgIAAA8AAAAAAAAAAAAAADgAAAAeAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
-              "WhenOpened": "2025-01-17T16:09:21.109Z",
+              "WhenOpened": "2025-01-20T16:09:13.483Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
               "DocumentIndex": 5,
-              "Title": "Document.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Document.cs",
-              "RelativeDocumentMoniker": "Models\\Page\\Document.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Document.cs",
-              "RelativeToolTip": "Models\\Page\\Document.cs",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T16:09:17.952Z",
+              "Title": "Index.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Item\\Index.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Banner\\Item\\Index.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Item\\Index.cshtml",
+              "RelativeToolTip": "Views\\Page\\Banner\\Item\\Index.cshtml",
+              "ViewState": "AgIAABUAAAAAAAAAAAAAACQAAAAnAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-01-20T16:00:44.979Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
-              "DocumentIndex": 3,
-              "Title": "Category.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Faq\\Category.cs",
-              "RelativeDocumentMoniker": "Models\\Page\\Faq\\Category.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Faq\\Category.cs",
-              "RelativeToolTip": "Models\\Page\\Faq\\Category.cs",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAABsAAAAbAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T15:59:47.814Z",
+              "DocumentIndex": 4,
+              "Title": "Edit.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Item\\Edit.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Banner\\Item\\Edit.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Item\\Edit.cshtml",
+              "RelativeToolTip": "Views\\Page\\Banner\\Item\\Edit.cshtml",
+              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAoAAAAlAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-01-20T16:00:18.225Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
-              "DocumentIndex": 1,
-              "Title": "Index.cshtml",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Index.cshtml",
-              "RelativeDocumentMoniker": "Views\\Page\\Faq\\Index.cshtml",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Index.cshtml",
-              "RelativeToolTip": "Views\\Page\\Faq\\Index.cshtml",
-              "ViewState": "AgIAAHoAAAAAAAAAAAAQwIEAAAAeAAAAAAAAAA==",
+              "DocumentIndex": 3,
+              "Title": "Write.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Item\\Write.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Banner\\Item\\Write.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Item\\Write.cshtml",
+              "RelativeToolTip": "Views\\Page\\Banner\\Item\\Write.cshtml",
+              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
-              "WhenOpened": "2025-01-17T15:56:23.647Z",
+              "WhenOpened": "2025-01-20T15:59:49.056Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
-              "DocumentIndex": 0,
-              "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": "AgIAABwAAAAAAAAAAAAuwDUAAABSAAAAAAAAAA==",
+              "DocumentIndex": 2,
+              "Title": "ItemController.cs",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Banner\\ItemController.cs",
+              "RelativeDocumentMoniker": "Controllers\\Page\\Banner\\ItemController.cs",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Controllers\\Page\\Banner\\ItemController.cs",
+              "RelativeToolTip": "Controllers\\Page\\Banner\\ItemController.cs",
+              "ViewState": "AgIAAC0AAAAAAAAAAAAAADwAAABPAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T15:55:02.561Z",
+              "WhenOpened": "2025-01-20T15:59:35.594Z",
               "EditorCaption": ""
             }
           ]

+ 50 - 101
backend/.vs/bitforum/v17/DocumentLayout.json

@@ -3,36 +3,24 @@
   "WorkspaceRootPath": "E:\\workspace\\bitforum\\backend\\",
   "Documents": [
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\faq\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\faq\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\popup\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\popup\\write.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\\views\\page\\popup\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\popup\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     },
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\views\\faqcategoryviewmodel.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\views\\faqcategoryviewmodel.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\banner\\item\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\banner\\item\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     },
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\page\\faq\\category.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\page\\faq\\category.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\faq\\item\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\faq\\item\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     },
     {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\page\\faq\\item.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\page\\faq\\item.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
-    },
-    {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\models\\page\\document.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:models\\page\\document.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
-    },
-    {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\document\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\document\\index.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
-    },
-    {
-      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\document\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
-      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\document\\edit.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
+      "AbsoluteMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|e:\\workspace\\bitforum\\backend\\views\\page\\faq\\item\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}",
+      "RelativeMoniker": "D:0:0:{07B22B2C-9DC6-4B8E-AA16-8F826E758BDD}|bitforum.csproj|solutionrelative:views\\page\\faq\\item\\write.cshtml||{40D31677-CBC0-4297-A9EF-89D907823A98}"
     }
   ],
   "DocumentGroupContainers": [
@@ -42,7 +30,7 @@
       "DocumentGroups": [
         {
           "DockedWidth": 200,
-          "SelectedChildIndex": 11,
+          "SelectedChildIndex": 5,
           "Children": [
             {
               "$type": "Bookmark",
@@ -66,106 +54,67 @@
             },
             {
               "$type": "Document",
-              "DocumentIndex": 2,
-              "Title": "FaqCategoryViewModel.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\Views\\FaqCategoryViewModel.cs",
-              "RelativeDocumentMoniker": "Models\\Views\\FaqCategoryViewModel.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\Views\\FaqCategoryViewModel.cs",
-              "RelativeToolTip": "Models\\Views\\FaqCategoryViewModel.cs",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAYAAAAhAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T16:34:07.431Z",
-              "EditorCaption": ""
-            },
-            {
-              "$type": "Document",
-              "DocumentIndex": 4,
-              "Title": "Item.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Faq\\Item.cs",
-              "RelativeDocumentMoniker": "Models\\Page\\Faq\\Item.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Faq\\Item.cs",
-              "RelativeToolTip": "Models\\Page\\Faq\\Item.cs",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAAB0AAAAbAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T16:10:05.653Z",
+              "DocumentIndex": 0,
+              "Title": "Write.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Popup\\Write.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Popup\\Write.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Popup\\Write.cshtml",
+              "RelativeToolTip": "Views\\Page\\Popup\\Write.cshtml",
+              "ViewState": "AgIAACcAAAAAAAAAAAAAAEIAAABVAAAAAAAAAA==",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-01-20T16:18:20.61Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
-              "DocumentIndex": 7,
+              "DocumentIndex": 1,
               "Title": "Edit.cshtml",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Document\\Edit.cshtml",
-              "RelativeDocumentMoniker": "Views\\Page\\Document\\Edit.cshtml",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Document\\Edit.cshtml",
-              "RelativeToolTip": "Views\\Page\\Document\\Edit.cshtml",
-              "ViewState": "AgIAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Popup\\Edit.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Popup\\Edit.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Popup\\Edit.cshtml",
+              "RelativeToolTip": "Views\\Page\\Popup\\Edit.cshtml",
+              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
-              "WhenOpened": "2025-01-17T16:09:21.702Z",
+              "WhenOpened": "2025-01-20T16:18:13.432Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
-              "DocumentIndex": 6,
-              "Title": "Index.cshtml",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Document\\Index.cshtml",
-              "RelativeDocumentMoniker": "Views\\Page\\Document\\Index.cshtml",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Document\\Index.cshtml",
-              "RelativeToolTip": "Views\\Page\\Document\\Index.cshtml",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
+              "DocumentIndex": 2,
+              "Title": "Write.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Item\\Write.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Banner\\Item\\Write.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Banner\\Item\\Write.cshtml",
+              "RelativeToolTip": "Views\\Page\\Banner\\Item\\Write.cshtml",
+              "ViewState": "AgIAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
-              "WhenOpened": "2025-01-17T16:09:21.109Z",
+              "WhenOpened": "2025-01-20T16:18:08.376Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
-              "DocumentIndex": 5,
-              "Title": "Document.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Document.cs",
-              "RelativeDocumentMoniker": "Models\\Page\\Document.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Document.cs",
-              "RelativeToolTip": "Models\\Page\\Document.cs",
+              "DocumentIndex": 4,
+              "Title": "Write.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Item\\Write.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Faq\\Item\\Write.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Item\\Write.cshtml",
+              "RelativeToolTip": "Views\\Page\\Faq\\Item\\Write.cshtml",
               "ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T16:09:17.952Z",
+              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
+              "WhenOpened": "2025-01-20T16:17:54.339Z",
               "EditorCaption": ""
             },
             {
               "$type": "Document",
               "DocumentIndex": 3,
-              "Title": "Category.cs",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Faq\\Category.cs",
-              "RelativeDocumentMoniker": "Models\\Page\\Faq\\Category.cs",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Models\\Page\\Faq\\Category.cs",
-              "RelativeToolTip": "Models\\Page\\Faq\\Category.cs",
-              "ViewState": "AgIAAAAAAAAAAAAAAAAAABsAAAAbAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T15:59:47.814Z",
-              "EditorCaption": ""
-            },
-            {
-              "$type": "Document",
-              "DocumentIndex": 0,
-              "Title": "Index.cshtml",
-              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Index.cshtml",
-              "RelativeDocumentMoniker": "Views\\Page\\Faq\\Index.cshtml",
-              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Index.cshtml",
-              "RelativeToolTip": "Views\\Page\\Faq\\Index.cshtml",
-              "ViewState": "AgIAACAAAAAAAAAAAAAQwDoAAAARAAAAAAAAAA==",
+              "Title": "Edit.cshtml",
+              "DocumentMoniker": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Item\\Edit.cshtml",
+              "RelativeDocumentMoniker": "Views\\Page\\Faq\\Item\\Edit.cshtml",
+              "ToolTip": "E:\\workspace\\bitforum\\backend\\Views\\Page\\Faq\\Item\\Edit.cshtml",
+              "RelativeToolTip": "Views\\Page\\Faq\\Item\\Edit.cshtml",
+              "ViewState": "AgIAAAAAAAAAAAAAAAAAACAAAAAjAAAAAAAAAA==",
               "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000759|",
-              "WhenOpened": "2025-01-17T15:56:23.647Z",
-              "EditorCaption": ""
-            },
-            {
-              "$type": "Document",
-              "DocumentIndex": 1,
-              "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": "AgIAAGgAAAAAAAAAAAAAAEQAAAAQAAAAAAAAAA==",
-              "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
-              "WhenOpened": "2025-01-17T15:55:02.561Z",
+              "WhenOpened": "2025-01-20T16:17:53.526Z",
               "EditorCaption": ""
             }
           ]

+ 10 - 4
backend/Constants/MenuData.cs

@@ -38,7 +38,7 @@ namespace bitforum.Constants
                             Name = "서버 정보",
                             Path = "/Setting/Server"
                         },
-                         new Menu
+                        new Menu
                         {
                             Id = 202,
                             Name = "환경변수",
@@ -99,15 +99,21 @@ namespace bitforum.Constants
                         },
                         new Menu
                         {
-                            Id = 303,
+                            Id = 302,
                             Name = "FAQ 관리",
-                            Path = "/Page/Faq"
+                            Path = "/Page/Faq/Item"
                         },
                         new Menu
                         {
-                            Id = 304,
+                            Id = 303,
                             Name = "팝업 관리",
                             Path = "/Page/Popup"
+                        },
+                        new Menu
+                        {
+                            Id = 304,
+                            Name = "배너 관리",
+                            Path = "/Page/Banner/Item"
                         }
                     }
                 },

+ 299 - 0
backend/Controllers/Page/Banner/ItemController.cs

@@ -0,0 +1,299 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.WebUtilities;
+using bitforum.Models;
+using bitforum.Models.Page.Banner;
+using bitforum.Services;
+
+namespace bitforum.Controllers.Page.Banner
+{
+    [Authorize]
+    [Route("Page/Banner")]
+    public class ItemController : Controller
+    {
+        private readonly ILogger<ItemController> _logger;
+        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;
+
+        public ItemController(ILogger<ItemController> logger, DefaultDbContext db, FileUploadService fileUploadService)
+        {
+            _logger = logger;
+            _db = db;
+            _fileUploadService = fileUploadService;
+        }
+
+        [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 = QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).ToDictionary(k => k.Key, v => string.Join(",", v.Value));
+            base.OnActionExecuting(context);
+        }
+
+        [HttpGet("Item/{positionID?}")]
+        public IActionResult Index(int? positionID, [FromQuery] int page = 1)
+        {
+            ViewBag.positionID = positionID;
+
+            // 위치 목록
+            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();
+            if (positionID.HasValue)
+            {
+                bannerItems = bannerItems.Where(c => c.PositionID == positionID);
+            }
+            ViewBag.BannerItems = bannerItems.ToList();
+            ViewBag.Total = bannerItems.Count();
+            ViewBag.Pagination = new Pagination(ViewBag.Total, page, 20, null);
+
+            return View(_IndexViewPath);
+        }
+
+        [HttpGet("Item/Write")]
+        public IActionResult Write()
+        {
+            ViewBag.BannerPositions = _db.BannerPosition.Where(c => c.IsActive).ToList();
+
+            return View(_WriteViewPath);
+        }
+        
+        [HttpPost("Item/Create")]
+        public async Task<IActionResult> Create(BannerItem request, IFormFile? Image)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                if (request.PositionID <= 0 || !_db.BannerPosition.Any(c => c.ID == request.PositionID))
+                {
+                    throw new Exception("유효한 위치를 선택하세요.");
+                }
+
+                // 이미지 저장
+                request.Image = await _fileUploadService.UploadImageAsync(Image, UploadFolder.Banner);
+                request.UpdatedAt = null;
+                request.CreatedAt = DateTime.Now;
+
+                _db.BannerItem.Add(request);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("배너 등록 중 오류가 발생했습니다.");
+                }
+
+                string message = "배너가 정상적으로 등록되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return Redirect("/Page/Banner/Item");
+            }
+            catch (ArgumentException e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return Write();
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return Write();
+            }
+        }
+
+        [HttpGet("Item/{id}/Edit")]
+        public async Task<IActionResult> Edit(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var bannerItem = await _db.BannerItem.FirstAsync(c => c.ID == id);
+                if (bannerItem is null)
+                {
+                    throw new Exception("FAQ 정보를 찾을 수 없습니다.");
+                }
+
+                // 위치 목록
+                ViewBag.BannerPositions = new SelectList(_db.BannerPosition.Where(c => c.IsActive).ToList(), "ID", "Subject", bannerItem.PositionID);
+
+                return View(_EditViewPath, bannerItem);
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return Redirect("/Page/Banner/Item");
+            }
+        }
+
+        [HttpPost("Item/Update")]
+        public async Task<IActionResult> Update(BannerItem request, IFormFile? Image, [FromForm] bool IsImageRemove = false)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                if (request.PositionID <= 0 || !_db.BannerPosition.Any(c => c.ID == request.PositionID))
+                {
+                    throw new Exception("유효한 분류를 선택하세요.");
+                }
+
+                if (request.EndAt < request.StartAt)
+                {
+                    throw new Exception("사용 기간을 확인해주세요.");
+                }
+
+                var bannerItem = await _db.BannerItem.FirstAsync(c => c.ID == request.ID);
+                if (bannerItem is null)
+                {
+                    throw new Exception("배너 정보를 찾을 수 없습니다.");
+                }
+
+                // 이미지 저장
+                if (IsImageRemove)
+                {
+                    // 실제 파일 삭제
+                    _fileUploadService.RemoveFile(bannerItem.Image);
+                    bannerItem.Image = null;
+                }
+                else if (Image is not null)
+                {
+                    bannerItem.Image = await _fileUploadService.UploadImageAsync(Image, UploadFolder.Banner);
+                }
+
+                bannerItem.PositionID = request.PositionID;
+                bannerItem.Subject = request.Subject;
+                bannerItem.Width = request.Width;
+                bannerItem.Height = request.Height;
+                bannerItem.Link = request.Link;
+                bannerItem.Order = request.Order;
+                bannerItem.IsActive = request.IsActive;
+                bannerItem.StartAt = request.StartAt;
+                bannerItem.EndAt = request.EndAt;
+                bannerItem.UpdatedAt = DateTime.Now;
+
+                _db.BannerItem.Update(bannerItem);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("배너 수정 중 오류가 발생했습니다.");
+                }
+
+                string message = "배너가 정상적으로 수정되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return Redirect($"/Page/Banner/Item/{request.ID}/Edit");
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return await Edit(request.ID);
+            }
+        }
+
+        [HttpGet("Item/{id}/Delete")]
+        public async Task<IActionResult> Delete(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var bannerItem = await _db.BannerItem.FindAsync(id);
+                if (bannerItem == null)
+                {
+                    throw new Exception("배너 정보를 찾을 수 없습니다.");
+                }
+
+                _fileUploadService.RemoveFile(bannerItem.Image);
+                _db.BannerItem.Remove(bannerItem);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("배너 삭제 중 오류가 발생했습니다.");
+                }
+
+                string message = "배너가 정상적으로 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+            }
+
+            return Redirect("/Page/Banner/Item");
+        }
+
+        [HttpPost("Item/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 bannerItem = await _db.BannerItem.FindAsync(id);
+                    if (bannerItem == null)
+                    {
+                        throw new Exception("배너 정보를 찾을 수 없습니다.");
+                    }
+
+                    _fileUploadService.RemoveFile(bannerItem.Image);
+                    _db.BannerItem.Remove(bannerItem);
+
+                    int affectedRows = await _db.SaveChangesAsync();
+                    if (affectedRows <= 0)
+                    {
+                        throw new Exception($"{id}번호의 배너 삭제 중 오류가 발생했습니다.");
+                    }
+                }
+
+                string message = "배너가 정상적으로 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+            }
+
+            return Redirect("/Page/Banner/Item");
+        }
+    }
+}

+ 131 - 0
backend/Controllers/Page/Banner/PositionController.cs

@@ -0,0 +1,131 @@
+using System.Diagnostics;
+using bitforum.Models;
+using bitforum.Models.Page.Banner;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+namespace bitforum.Controllers.Page
+{
+    [Authorize]
+    [Route("Page")]
+    public class PositionController : Controller
+    {
+        private readonly ILogger<PositionController> _logger;
+        private readonly DefaultDbContext _db;
+        private readonly string _ViewPath = "~/Views/Page/Banner/Position/Index.cshtml";
+
+        public PositionController(ILogger<PositionController> 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("Banner")]
+        public IActionResult Index()
+        {
+            ViewBag.BannerPositions = _db.BannerPosition.Include(c => c.BannerItem).ToList();
+
+            return View(_ViewPath);
+        }
+
+        [HttpPost("Banner")]
+        public async Task<IActionResult> Save([FromForm] List<BannerPosition> request)
+        {
+            using var transaction = await _db.Database.BeginTransactionAsync();
+
+            try
+            {
+                if (request == null || !request.Any())
+                {
+                    // 전체 삭제
+                    var bannerPositions = await _db.BannerPosition.ToListAsync();
+                    if (bannerPositions.Any())
+                    {
+                        _db.BannerPosition.RemoveRange(bannerPositions);
+                        await _db.SaveChangesAsync();
+                    }
+
+                    await transaction.CommitAsync();
+                    return RedirectToAction("Index");
+                }
+
+                if (!ModelState.IsValid)
+                {
+                    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: 요청 데이터에 없는 항목
+
+                // 삭제 대상 항목 제거
+                if (idsToDelete.Any())
+                {
+                    var selectedRows = await _db.BannerPosition.Where(c => idsToDelete.Contains(c.ID) && !c.BannerItem.Any()).ToListAsync();
+                    _db.BannerPosition.RemoveRange(selectedRows);
+                }
+
+                foreach (var row in request)
+                {
+                    // 중복 확인
+                    if (await _db.BannerPosition.AnyAsync(c => c.Code == row.Code && c.ID != row.ID))
+                    {
+                        throw new Exception($"이미 존재하는 배너 위치입니다: {row.Code}");
+                    }
+
+                    if (row.ID == 0)
+                    {
+                        row.UpdatedAt = null;
+                        row.CreatedAt = DateTime.Now;
+                        await _db.BannerPosition.AddAsync(row);
+                    }
+                    else
+                    {
+                        var existing = await _db.BannerPosition.FirstOrDefaultAsync(c => c.ID == row.ID);
+                        if (existing == null)
+                        {
+                            throw new Exception($"ID {row.ID}에 해당하는 데이터가 없습니다.");
+                        }
+
+                        existing.Code = row.Code;
+                        existing.Subject = row.Subject;
+                        existing.IsActive = row.IsActive;
+                        existing.UpdatedAt = DateTime.Now;
+
+                        _db.BannerPosition.Update(existing);
+                    }
+                }
+
+                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();
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+
+                ViewBag.BannerPositions = _db.BannerPosition.Include(c => c.BannerItem).ToList();
+
+                return View(_ViewPath, request);
+            }
+        }
+    }
+}

+ 7 - 6
backend/Controllers/Page/DocumentController.cs

@@ -14,7 +14,7 @@ namespace bitforum.Controllers.Page
         private readonly ILogger<DocumentController> _logger;
         private readonly DefaultDbContext _db;
         private readonly IConfiguration _config;
-        private readonly string _ViewPath = "~/Views/Page/Document/Index.cshtml";
+        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";
 
@@ -35,9 +35,9 @@ namespace bitforum.Controllers.Page
         public IActionResult Index()
         {
             ViewBag.siteURL = _config["AppConfig:AppName"];
-            var viewModel = _db.Document.OrderByDescending(c => c.ID).ToList();
+            ViewBag.Documents = _db.Document.OrderByDescending(c => c.ID).ToList();
 
-            return View(_ViewPath, viewModel);
+            return View(_IndexViewPath);
         }
 
         [HttpGet("Document/Write")]
@@ -113,7 +113,7 @@ namespace bitforum.Controllers.Page
             {
                 _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return RedirectToAction("Edit", new { id });
+                return Index();
             }
         }
 
@@ -158,7 +158,7 @@ namespace bitforum.Controllers.Page
                 string message = "문서가 정상적으로 수정되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
-                return RedirectToAction("Edit", new { id = request.ID });
+                return RedirectToAction("Edit", new { request.ID });
             }
             catch (Exception e)
             {
@@ -185,6 +185,7 @@ namespace bitforum.Controllers.Page
                 }
 
                 _db.Document.Remove(document);
+
                 int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
                 {
@@ -200,7 +201,7 @@ namespace bitforum.Controllers.Page
             {
                 _logger.LogError(e, e.Message);
                 TempData["ErrorMessages"] = e.Message;
-                return View(_ViewPath);
+                return Index();
             }
         }
     }

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

@@ -1,12 +1,11 @@
 using System.Diagnostics;
-using bitforum.Models;
-using bitforum.Models.Page.Faq;
-using bitforum.Models.Views;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
+using bitforum.Models;
+using bitforum.Models.Page.Faq;
 
-namespace bitforum.Controllers.Page
+namespace bitforum.Controllers.Page.Faq
 {
     [Authorize]
     [Route("Page")]
@@ -14,7 +13,7 @@ namespace bitforum.Controllers.Page
     {
         private readonly ILogger<CategoryController> _logger;
         private readonly DefaultDbContext _db;
-        private readonly string _ViewPath = "~/Views/Page/Faq/Index.cshtml";
+        private readonly string _ViewPath = "~/Views/Page/Faq/Category/Index.cshtml";
 
         public CategoryController(ILogger<CategoryController> logger, DefaultDbContext db)
         {
@@ -31,29 +30,30 @@ namespace bitforum.Controllers.Page
         [HttpGet("Faq")]
         public IActionResult Index()
         {
-            var viewModel = new FaqCategoryViewModel
-            {
-                FaqCategories = _db.FaqCategory .Include(c => c.FaqItem).OrderByDescending(c => c.Order).ToList()
-            };
-
-            ViewBag.ItemCounts = viewModel.FaqCategories.ToDictionary(
-                category => category.ID,
-                category => category.FaqItem.Count
-            );
+            ViewBag.FaqCategories = _db.FaqCategory.Include(c => c.FaqItem).OrderBy(c => c.Order).ToList();
 
-            return View(_ViewPath, viewModel);
+            return View(_ViewPath);
         }
 
-        [HttpPost]
-        public async Task<IActionResult> Save(FaqCategoryViewModel request)
+        [HttpPost("Faq")]
+        public async Task<IActionResult> Save([FromForm] List<FaqCategory> request)
         {
             using var transaction = await _db.Database.BeginTransactionAsync();
 
             try
             {
-                if (request.FaqCategories == null || !request.FaqCategories.Any())
+                if (request == null || !request.Any())
                 {
-                    throw new Exception("저장할 데이터가 없습니다.");
+                    // 전체 삭제
+                    var faqCategories = await _db.FaqCategory.ToListAsync();
+                    if (faqCategories.Any())
+                    {
+                        _db.FaqCategory.RemoveRange(faqCategories);
+                        await _db.SaveChangesAsync();
+                    }
+
+                    await transaction.CommitAsync();
+                    return RedirectToAction("Index");
                 }
 
                 if (!ModelState.IsValid)
@@ -61,25 +61,23 @@ namespace bitforum.Controllers.Page
                     throw new Exception("유효성 검사에 실패하였습니다.");
                 }
                 
-                var requestIds = request.FaqCategories.Select(x => x.ID).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())
                 {
-                    var rr = await _db.FaqCategory.Where(c => idsToDelete.Contains(c.ID)).ToListAsync();
-                    _db.FaqCategory.RemoveRange(
-                        rr
-                    );
+                    var selectedRows = await _db.FaqCategory.Where(c => idsToDelete.Contains(c.ID) && !c.FaqItem.Any()).ToListAsync();
+                    _db.FaqCategory.RemoveRange(selectedRows);
                 }
 
-                foreach (var row in request.FaqCategories)
+                foreach (var row in request)
                 {
                     // 중복 확인
                     if (await _db.FaqCategory.AnyAsync(c => c.Code == row.Code && c.ID != row.ID))
                     {
-                        throw new Exception($"이미 존재하는 Code 주소입니다: {row.Code}");
+                        throw new Exception($"이미 존재하는 분류 주소입니다: {row.Code}");
                     }
 
                     if (row.ID == 0)
@@ -107,24 +105,18 @@ namespace bitforum.Controllers.Page
                     }
                 }
 
+                await transaction.CommitAsync();
                 int affectedRows = await _db.SaveChangesAsync();
                 if (affectedRows <= 0)
                 {
                     throw new Exception("FAQ 저장 중 오류가 발생했습니다.");
                 }
 
-                await transaction.CommitAsync();
                 string message = "FAQ 분류가 정상적으로 저장되었습니다.";
                 TempData["SuccessMessage"] = message;
                 _logger.LogInformation(message);
                 return RedirectToAction("Index");
             }
-            catch (DbUpdateException ex)
-            {
-                await transaction.RollbackAsync();
-                _logger.LogError(ex, ex.InnerException?.Message ?? ex.Message);
-                throw new Exception("데이터 저장 중 오류 발생: " + (ex.InnerException?.Message ?? ex.Message));
-            }
             catch (Exception e)
             {
                 await transaction.RollbackAsync();

+ 265 - 0
backend/Controllers/Page/Faq/ItemController.cs

@@ -0,0 +1,265 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.AspNetCore.WebUtilities;
+using bitforum.Models;
+using bitforum.Models.Page.Faq;
+
+namespace bitforum.Controllers.Page.Faq
+{
+    [Authorize]
+    [Route("Page/Faq")]
+    public class ItemController : Controller
+    {
+        private readonly ILogger<ItemController> _logger;
+        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;
+
+        public ItemController(ILogger<ItemController> 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 = QueryHelpers.ParseQuery(HttpContext.Request.QueryString.Value).ToDictionary(k => k.Key, v => string.Join(",", v.Value));
+            base.OnActionExecuting(context);
+        }
+
+        [HttpGet("Item/{categoryID?}")]
+        public IActionResult Index(int? categoryID, [FromQuery] int page = 1)
+        {
+            ViewBag.CategoryID = categoryID;
+
+            // 분류 목록
+            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();
+            if (categoryID.HasValue)
+            {
+                faqItems = faqItems.Where(c => c.CategoryID == categoryID);
+            }
+
+            ViewBag.FaqItems = faqItems.ToList();
+            ViewBag.Total = faqItems.Count();
+            ViewBag.Pagination = new Pagination(ViewBag.Total, page, 20, null);
+
+            return View(_IndexViewPath);
+        }
+
+        [HttpGet("Item/Write")]
+        public IActionResult Write()
+        {
+            ViewBag.FaqCategory = _db.FaqCategory.Where(c => c.IsActive).OrderBy(c => c.Order).ToList();
+
+            return View(_WriteViewPath);
+        }
+        
+        [HttpPost("Item/Create")]
+        public async Task<IActionResult> Create(FaqItem request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                if (request.CategoryID <= 0 || !_db.FaqCategory.Any(c => c.ID == request.CategoryID))
+                {
+                    throw new Exception("유효한 분류를 선택하세요.");
+                }
+
+                request.UpdatedAt = null;
+                request.CreatedAt = DateTime.Now;
+
+                _db.FaqItem.Add(request);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("FAQ 등록 중 오류가 발생했습니다.");
+                }
+
+                string message = "FAQ가 정상적으로 등록되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return Redirect("/Page/Faq/Item");
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return Write();
+            }
+        }
+
+        [HttpGet("Item/{id}/Edit")]
+        public async Task<IActionResult> Edit(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var faqItem = await _db.FaqItem.FirstAsync(c => c.ID == id);
+                if (faqItem is null)
+                {
+                    throw new Exception("FAQ 정보를 찾을 수 없습니다.");
+                }
+
+                // 분류 목록
+                ViewBag.FaqCategory = new SelectList(_db.FaqCategory.Where(c => c.IsActive).OrderBy(c => c.Order).ToList(), "ID", "Subject", faqItem.CategoryID);
+
+                return View(_EditViewPath, faqItem);
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return Redirect("/Page/Faq/Item");
+            }
+        }
+
+        [HttpPost("Item/Update")]
+        public async Task<IActionResult> Update(FaqItem request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                if (request.CategoryID <= 0 || !_db.FaqCategory.Any(c => c.ID == request.CategoryID))
+                {
+                    throw new Exception("유효한 분류를 선택하세요.");
+                }
+
+                var faqItem = await _db.FaqItem.FirstAsync(c => c.ID == request.ID);
+                if (faqItem is null)
+                {
+                    throw new Exception("FAQ 정보를 찾을 수 없습니다.");
+                }
+
+                faqItem.CategoryID = request.CategoryID;
+                faqItem.Question = request.Question;
+                faqItem.Answer = request.Answer;
+                faqItem.IsActive = request.IsActive;
+                faqItem.UpdatedAt = DateTime.Now;
+
+                _db.FaqItem.Update(faqItem);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("FAQ 수정 중 오류가 발생했습니다.");
+                }
+
+                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);
+            }
+        }
+
+        [HttpGet("Item/{id}/Delete")]
+        public async Task<IActionResult> Delete(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var faqItem = await _db.FaqItem.FindAsync(id);
+                if (faqItem == null)
+                {
+                    throw new Exception("FAQ 정보를 찾을 수 없습니다.");
+                }
+
+                _db.FaqItem.Remove(faqItem);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("FAQ 삭제 중 오류가 발생했습니다.");
+                }
+
+                string message = "FAQ가 정상적으로 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+            }
+
+            return Redirect("/Page/Faq/Item");
+        }
+
+        [HttpPost("Item/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 faqItem = await _db.FaqItem.FindAsync(id);
+                    if (faqItem == null)
+                    {
+                        throw new Exception("FAQ 정보를 찾을 수 없습니다.");
+                    }
+
+                    _db.FaqItem.Remove(faqItem);
+
+                    int affectedRows = await _db.SaveChangesAsync();
+                    if (affectedRows <= 0)
+                    {
+                        throw new Exception($"{id}번호의 FAQ 삭제 중 오류가 발생했습니다.");
+                    }
+                }
+
+                string message = "FAQ가 정상적으로 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+            }
+
+            return Redirect("/Page/Faq/Item");
+        }
+    }
+}

+ 244 - 0
backend/Controllers/Page/PopupController.cs

@@ -0,0 +1,244 @@
+using System.Diagnostics;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using bitforum.Models;
+using bitforum.Models.Page;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace bitforum.Controllers.Page
+{
+    [Authorize]
+    [Route("Page")]
+    public class PopupController : Controller
+    {
+        private readonly ILogger<PopupController> _logger;
+        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)
+        {
+            _logger = logger;
+            _db = db;
+            _config = config;
+        }
+
+        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
+        public IActionResult Error()
+        {
+            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
+        }
+
+        [HttpGet("Popup")]
+        public IActionResult Index([FromQuery] int page = 1)
+        {
+            ViewBag.Popups = _db.Popup.OrderByDescending(c => c.ID).ToList();
+            ViewBag.Total = ViewBag.Popups.Count;
+            ViewBag.Pagination = new Pagination(ViewBag.Total, page, 20, null);
+
+            return View(_IndexViewPath);
+        }
+
+        [HttpGet("Popup/Write")]
+        public IActionResult Write()
+        {
+            return View(_WriteViewPath);
+        }
+
+        [HttpPost("Popup/Create")]
+        public async Task<IActionResult> Create(Popup request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                if (request.EndAt < request.StartAt)
+                {
+                    throw new Exception("사용 기간을 확인해주세요.");
+                }
+
+                request.UpdatedAt = null;
+                request.CreatedAt = DateTime.Now;
+
+                _db.Popup.Add(request);
+
+                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)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return View(_WriteViewPath, request);
+            }
+        }
+
+        [HttpGet("Popup/{id}/Edit")]
+        public async Task<IActionResult> Edit(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 접근입니다.");
+                }
+
+                var popup = await _db.Popup.FirstAsync(c => c.ID == id);
+                if (popup is null)
+                {
+                    throw new Exception("팝업 정보를 찾을 수 없습니다.");
+                }
+
+                return View(_EditViewPath, popup);
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return RedirectToAction("Index");
+            }
+        }
+
+        [HttpPost("Popup/Update")]
+        public async Task<IActionResult> Update(Popup request)
+        {
+            try
+            {
+                if (!ModelState.IsValid)
+                {
+                    throw new Exception("유효성 검사에 실패하였습니다.");
+                }
+
+                if (request.EndAt < request.StartAt)
+                {
+                    throw new Exception("사용 기간을 확인해주세요.");
+                }
+
+                var popup = await _db.Popup.FirstAsync(c => c.ID == request.ID);
+                if (popup is null)
+                {
+                    throw new Exception("팝업 정보를 찾을 수 없습니다.");
+                }
+
+                popup.Subject = request.Subject;
+                popup.Content = request.Content;
+                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);
+
+                int affectedRows = await _db.SaveChangesAsync();
+                if (affectedRows <= 0)
+                {
+                    throw new Exception("팝업 수정 중 오류가 발생했습니다.");
+                }
+
+                string message = "팝업이 정상적으로 수정되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return RedirectToAction("Edit", new { request.ID });
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return View(_EditViewPath, request);
+            }
+        }
+
+        [HttpGet("Popup/Delete/{id}")]
+        public async Task<IActionResult> Delete(int id)
+        {
+            try
+            {
+                if (id <= 0)
+                {
+                    throw new Exception("유효하지 않은 문서 ID입니다.");
+                }
+
+                var popup = await _db.Popup.FindAsync(id);
+                if (popup == null)
+                {
+                    throw new Exception("팝업 정보를 찾을 수 없습니다.");
+                }
+
+                _db.Popup.Remove(popup);
+
+                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)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return Index();
+            }
+        }
+
+        [HttpPost("Popup/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 popup = await _db.Popup.FindAsync(id);
+                    if (popup == null)
+                    {
+                        throw new Exception("팝업 정보를 찾을 수 없습니다.");
+                    }
+
+                    _db.Popup.Remove(popup);
+
+                    int affectedRows = await _db.SaveChangesAsync();
+                    if (affectedRows <= 0)
+                    {
+                        throw new Exception($"{id}번호의 팝업 삭제 중 오류가 발생했습니다.");
+                    }
+                }
+
+                string message = "팝업이 정상적으로 삭제되었습니다.";
+                TempData["SuccessMessage"] = message;
+                _logger.LogInformation(message);
+                return RedirectToAction("Index");
+            }
+            catch (Exception e)
+            {
+                _logger.LogError(e, e.Message);
+                TempData["ErrorMessages"] = e.Message;
+                return RedirectToAction("Index");
+            }
+        }
+    }
+}

+ 4 - 0
backend/Database/DefaultDbContext.cs

@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
 using bitforum.Models;
 using bitforum.Models.Page;
 using bitforum.Models.Page.Faq;
+using bitforum.Models.Page.Banner;
 
 public class DefaultDbContext : DbContext
 {
@@ -11,4 +12,7 @@ public class DefaultDbContext : DbContext
     public DbSet<Document> Document { get; set; }
     public DbSet<FaqCategory> FaqCategory { get; set; }
     public DbSet<FaqItem> FaqItem { get; set; }
+    public DbSet<Popup> Popup { get; set; }
+    public DbSet<BannerPosition> BannerPosition { get; set; }
+    public DbSet<BannerItem> BannerItem { get; set; }
 }

+ 6 - 0
backend/Helpers/Func.cs

@@ -42,5 +42,11 @@ namespace bitforum.Helpers
 
             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;
+        }
     }
 }

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

@@ -0,0 +1,251 @@
+// <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
+        }
+    }
+}

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

@@ -0,0 +1,68 @@
+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);
+        }
+    }
+}

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

@@ -0,0 +1,250 @@
+// <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
+        }
+    }
+}

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

@@ -0,0 +1,40 @@
+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);
+        }
+    }
+}

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

@@ -0,0 +1,364 @@
+// <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
+        }
+    }
+}

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

@@ -0,0 +1,94 @@
+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");
+        }
+    }
+}

+ 163 - 2
backend/Migrations/DefaultDb/DefaultDbContextModelSnapshot.cs

@@ -50,6 +50,104 @@ namespace bitforum.Migrations.DefaultDb
                     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")
@@ -88,8 +186,7 @@ namespace bitforum.Migrations.DefaultDb
                     b.HasIndex(new[] { "Code" }, "IX_Document_Code")
                         .IsUnique();
 
-                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive")
-                        .IsUnique();
+                    b.HasIndex(new[] { "IsActive" }, "IX_Document_IsActive");
 
                     b.ToTable("Document");
                 });
@@ -179,6 +276,65 @@ namespace bitforum.Migrations.DefaultDb
                     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")
@@ -190,6 +346,11 @@ namespace bitforum.Migrations.DefaultDb
                     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");

+ 75 - 0
backend/Models/Page/Banner/Item.cs

@@ -0,0 +1,75 @@
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace bitforum.Models.Page.Banner
+{
+    [Index(nameof(Order), Name = "IX_BannerItem_Order")]
+    [Index(nameof(IsActive), Name = "IX_BannerItem_IsActive")]
+    public class BannerItem
+    {
+        [Key]
+        public int ID { get; set; }
+
+        [Required]
+        [ForeignKey("BannerPosition")]
+        [DisplayName("배너 위치ID")]
+        public int PositionID { get; set; }
+
+        [Required]
+        [DisplayName("배너 명")]
+        [DataType(DataType.Text)]
+        [MaxLength(255)]
+        public string Subject { get; set; }
+
+        [DisplayName("이미지")]
+        [RegularExpression(@"\.(jpg|jpeg|png|gif)$", ErrorMessage = "이미지 파일은 jpg, jpeg, png, gif 형식이어야 합니다.")]
+        [MaxLength(1024)]
+        public string? Image { get; set; } = null;
+
+        [DisplayName("가로 크기")]
+        [Range(1, int.MaxValue, ErrorMessage = "숫자는 1 이상이어야 합니다.")]
+        public int Width { get; set; } = 0;
+
+        [DisplayName("세로 크기")]
+        [Range(1, int.MaxValue, ErrorMessage = "숫자는 1 이상이어야 합니다.")]
+        public int Height { get; set; } = 0;
+
+        [DisplayName("주소")]
+        [DataType(DataType.Url)]
+        [MaxLength(255)]
+        public string? Link { get; set; } = null;
+
+        [Required]
+        [DisplayName("순서")]
+        public int Order { get; set; } = 0;
+
+        [Required]
+        [DisplayName("사용 여부")]
+        public bool IsActive { get; set; } = false;
+
+        [DisplayName("사용 기간 - 시작")]
+        [DataType(DataType.DateTime)]
+        public DateTime? StartAt { get; set; } = null;
+
+        [DisplayName("사용 기간 - 종료")]
+        [DataType(DataType.DateTime)]
+        public DateTime? EndAt { get; set; } = null;
+
+        [DisplayName("조회 수")]
+        public int Views { get; set; } = 0;
+
+        [DisplayName("수정일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? CreatedAt { get; set; } = null;
+
+        [ValidateNever]
+        public virtual BannerPosition? BannerPosition { get; set; } = null;
+    }
+}

+ 41 - 0
backend/Models/Page/Banner/Position.cs

@@ -0,0 +1,41 @@
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.Models.Page.Banner
+{
+    [Index(nameof(Code), Name = "IX_BannerPosition_Code", IsUnique = true)]
+    [Index(nameof(IsActive), Name = "IX_BannerPosition_IsActive")]
+    public class BannerPosition
+    {
+        [Key]
+        public int ID { get; set; }
+
+        [Required]
+        [DisplayName("위치 구분")]
+        [DataType(DataType.Text)]
+        [MaxLength(30)]
+        [RegularExpression(@"^[a-zA-Z0-9]+$", ErrorMessage = "위치 구분은 영문 및 숫자로만 구성되어야 합니다.")]
+        public string Code { get; set; }
+
+        [Required]
+        [DisplayName("위치 명")]
+        [DataType(DataType.Text)]
+        [MaxLength(255)]
+        public string Subject { get; set; }
+
+        [Required]
+        [DisplayName("사용 여부")]
+        public bool IsActive { get; set; } = false;
+
+        [DisplayName("수정일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? CreatedAt { get; set; } = null;
+
+        public virtual ICollection<BannerItem> BannerItem { get; set; } = new List<BannerItem>();
+    }
+}

+ 1 - 1
backend/Models/Page/Document.cs

@@ -5,7 +5,7 @@ using System.ComponentModel.DataAnnotations;
 namespace bitforum.Models.Page
 {
     [Index(nameof(Code), Name = "IX_Document_Code", IsUnique = true)]
-    [Index(nameof(IsActive), Name = "IX_Document_IsActive", IsUnique = true)]
+    [Index(nameof(IsActive), Name = "IX_Document_IsActive")]
     public class Document
     {
         [Key]

+ 6 - 3
backend/Models/Page/Faq/Item.cs

@@ -1,3 +1,4 @@
+using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
 using Microsoft.EntityFrameworkCore;
 using System.ComponentModel;
 using System.ComponentModel.DataAnnotations;
@@ -17,6 +18,7 @@ namespace bitforum.Models.Page.Faq
         [DisplayName("분류ID")]
         public int CategoryID { get; set; }
 
+        [Required]
         [DisplayName("질문")]
         [DataType(DataType.Text)]
         [MaxLength(255)]
@@ -37,14 +39,15 @@ namespace bitforum.Models.Page.Faq
         [DisplayName("조회 수")]
         public int Views { get; set; } = 0;
 
-        [Display(Name = "수정일시")]
+        [DisplayName("수정일시")]
         [DataType(DataType.DateTime)]
         public DateTime? UpdatedAt { get; set; } = null;
 
-        [Display(Name = "등록일시")]
+        [DisplayName("등록일시")]
         [DataType(DataType.DateTime)]
         public DateTime? CreatedAt { get; set; } = null;
 
-        public virtual FaqCategory FaqCategory { get; set; } = new FaqCategory();
+        [ValidateNever]
+        public virtual FaqCategory FaqCategory { get; set; } = null;
     }
 }

+ 55 - 0
backend/Models/Page/Popup.cs

@@ -0,0 +1,55 @@
+using Microsoft.EntityFrameworkCore;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+
+namespace bitforum.Models.Page
+{
+    [Index(nameof(IsActive), Name = "IX_Popup_IsActive")]
+    public class Popup
+    {
+        [Key]
+        public int ID { get; set; }
+
+        [Required]
+        [DisplayName("제목")]
+        [DataType(DataType.Text)]
+        [MaxLength(255)]
+        public string Subject { get; set; }
+
+        [DisplayName("내용")]
+        [DataType(DataType.Html)]
+        public string? Content { get; set; } = null;
+
+        [DisplayName("주소")]
+        [DataType(DataType.Url)]
+        [MaxLength(255)]
+        public string? Link { get; set; } = null;
+
+        [Display(Name = "사용 기간 - 시작")]
+        [DataType(DataType.DateTime)]
+        public DateTime? StartAt { get; set; } = null;
+
+        [Display(Name = "사용 기간 - 종료")]
+        [DataType(DataType.DateTime)]
+        public DateTime? EndAt { get; set; } = null;
+
+        [Required]
+        [DisplayName("순서")]
+        public int Order { get; set; } = 0;
+
+        [Required]
+        [DisplayName("사용 여부")]
+        public bool IsActive { get; set; } = false;
+
+        [DisplayName("조회 수")]
+        public int Views { get; set; } = 0;
+
+        [DisplayName("수정일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? UpdatedAt { get; set; } = null;
+
+        [DisplayName("등록일시")]
+        [DataType(DataType.DateTime)]
+        public DateTime? CreatedAt { get; set; } = null;
+    }
+}

+ 89 - 0
backend/Models/Pagination.cs

@@ -0,0 +1,89 @@
+using Microsoft.AspNetCore.WebUtilities;
+using System.Reflection;
+
+namespace bitforum.Models
+{
+    public class Pagination
+    {
+        public int Page { get; set; } = 0; // 현재 페이지
+        public int TotalRows { get; set; } = 0; // 전체 항목 수
+        public int TotalPage { get; set; } = 0; // 전체 페이지 수
+        public int PerPage = 10;
+
+        public int StartPage = 0; // 현재 페이지의 시작 번호
+        public int EndPage = 0; // 현재 페이지의 마지막 번호
+        public int PageGroupSize = 10; // 한번에 보여줄 페이지 번호 개수
+        public int CurrentPageGroup = 0; // 현재 페이지가 속한 페이지 그룹의 시작번호
+
+        public int GroupStartPage = 0; // 페이지 그룹의 시작 페이지 번호
+        public int GroupEndPage = 0; // 페이지 그룹의 마지막 페이지 번호
+
+        public int PrevGroupPage = 0; // 이전 페이지 그룹의 페이지 번호
+        public int NextGroupPage = 0; // 다음 페이지 그룹 페이지 번호
+
+        private Dictionary<string, string> QueryStringParams;
+
+        public Pagination(int? totalRows, int page, int perPage, object? queryParams)
+        {
+            Page = page;
+            PerPage = 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);
+
+            GroupStartPage = (GroupStartPage - PageGroupSize);
+            GroupEndPage = (((Page - 1) / PageGroupSize + 1) * PageGroupSize);
+
+            PrevGroupPage = Math.Max(StartPage - PageGroupSize, 1); // 이전 페이지 그룹의 첫 페이지 계산
+            NextGroupPage = Math.Min(EndPage + 1, TotalPage); // 다음 페이지 그룹의 첫 페이지 계산
+
+            // 객체를 쿼리스트링으로 변환
+            QueryStringParams = ToDictionary(queryParams);
+        }
+
+        // 이전 페이지 여부
+        public bool HasPreviousPage => (Page > 1);
+
+        // 다음 페이지 여부
+        public bool HasNextPage => (Page < TotalPage);
+
+        // 객체를 Dictionary로 변환하는 메서드
+        private Dictionary<string, string> ToDictionary(object? obj)
+        {
+            var dictionary = new Dictionary<string, string>();
+
+            if (obj is null)
+            {
+                return dictionary;
+            }
+
+            var properties = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+            foreach (var prop in properties)
+            {
+                var value = prop.GetValue(obj)?.ToString();
+                if (!string.IsNullOrEmpty(value))
+                {
+                    dictionary.Add(prop.Name, value);
+                }
+            }
+
+            return dictionary;
+        }
+
+        // 최종 쿼리스트링을 반환하는 메서드
+        public string BuildQueryString()
+        {
+            if (QueryStringParams == null || QueryStringParams.Count == 0)
+            {
+                return "";
+            }
+
+            // 쿼리스트링이 있을 경우, AddQueryString으로 변환 후 ? 제거
+            return "&" + QueryHelpers.AddQueryString("", QueryStringParams).TrimStart('?');
+        }
+    }
+}

+ 2 - 1
backend/Program.cs

@@ -68,10 +68,11 @@ builder.Services.ConfigureApplicationCookie(options =>
 
 /**
  * =======================================================================================================================================================
+ * Repository, Service 추가
  */
 
 builder.Services.AddScoped<ConfigRepository>();
-
+builder.Services.AddScoped<FileUploadService, FileUploadService>();
 /**
  * =======================================================================================================================================================
  */

+ 93 - 0
backend/Services/FileUploadService.cs

@@ -0,0 +1,93 @@
+using static NuGet.Packaging.PackagingConstants;
+
+namespace bitforum.Services
+{
+    public enum UploadFolder
+    {
+        Document,
+        Faq,
+        Popup,
+        Banner
+    }
+
+    public interface IFileUploadService
+    {
+        Task<string> UploadImageAsync(IFormFile file, UploadFolder folor);
+        Task<string> UploadFileAsync(IFormFile file, string[] allowedExtensions, UploadFolder folor);
+    }
+
+    public class FileUploadService : IFileUploadService
+    {
+        private readonly IWebHostEnvironment _environment;
+
+        public FileUploadService(IWebHostEnvironment environment)
+        {
+            _environment = environment;
+        }
+
+        // 이미지 저장
+        public async Task<string?> UploadImageAsync(IFormFile? file, UploadFolder folor)
+        {
+            return await UploadFileAsync(file, [".jpg", ".jpeg", ".png", ".gif"], folor);
+        }
+
+        // 파일 저장
+        public async Task<string?> UploadFileAsync(IFormFile? file, string[] allowedExtensions, UploadFolder folder)
+        {
+            if (file == null || file.Length == 0)
+            {
+                return null;
+            }
+
+            var extension = Path.GetExtension(file.FileName).ToLower();
+            if (!Array.Exists(allowedExtensions, ext => ext == extension))
+            {
+                throw new ArgumentException("허용되지 않는 파일 형식입니다.");
+            }
+
+            string uploadFolder = folder switch
+            {
+                UploadFolder.Document => "editor/images",
+                UploadFolder.Faq => "editor/faq",
+                UploadFolder.Popup => "editor/popup",
+                UploadFolder.Banner => "upload/banner",
+                _ => throw new ArgumentException("유효하지 않은 경로입니다.")
+            };
+
+            var uploadPath = Path.Combine(_environment.WebRootPath, uploadFolder);
+            if (!Directory.Exists(uploadPath))
+            {
+                Directory.CreateDirectory(uploadPath);
+            }
+
+            var uniqueFileName = $"{Guid.NewGuid()}{extension}";
+            var filePath = Path.Combine(uploadPath, uniqueFileName);
+
+            using (var stream = new FileStream(filePath, FileMode.Create))
+            {
+                await file.CopyToAsync(stream);
+            }
+
+            return $"/{uploadFolder}/{uniqueFileName}";
+        }
+
+        // 파일 삭제
+        public bool RemoveFile(string? filePath)
+        {
+            if (string.IsNullOrEmpty(filePath))
+            {
+                return false;
+            }
+
+            var fullPath = Path.Combine(_environment.WebRootPath, filePath.TrimStart('/'));
+
+            if (File.Exists(fullPath))
+            {
+                File.Delete(fullPath);
+                return true;
+            }
+
+            return false;
+        }
+    }
+}

+ 188 - 0
backend/Views/Page/Banner/Item/Edit.cshtml

@@ -0,0 +1,188 @@
+@model bitforum.Models.Page.Banner.BannerItem
+@{
+    ViewData["Title"] = "배너 수정";
+    var bannerPositions = ViewBag.BannerPositions as SelectList;
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Page/Banner/Item/Update" enctype="multipart/form-data">
+        <input type="hidden" asp-for="ID" />
+
+        <div class="row mb-2">
+            <label for="PositionID" class="col-sm-2 col-form-label">위치</label>
+            <div class="col-sm-10">
+                <select asp-for="PositionID" asp-items="bannerPositions" class="form-select w-auto" required></select>
+                <span asp-validation-for="PositionID" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Subject" class="col-sm-2 col-form-label">배너 명</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Subject" class="form-control" required />
+                <span asp-validation-for="Subject" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Image" class="col-sm-2 col-form-label">이미지</label>
+            <div class="col-sm-10">
+                @if (Model.Image is not null && Model.Image != string.Empty)
+                {
+                    <img src="@Url.Content(Model.Image)" class="img-fluid img-thumbnail" alt="@Model.Subject" />
+                    <div class="form-check-inline">
+                        <input type="checkbox" name="IsImageRemove" id="IsImageRemove" class="form-check-input" value="true" />
+                        <label for="IsImageRemove" class="form-check-label">삭제</label>
+                    </div>
+                    
+                    <input type="hidden" asp-for="Image" class="form-control" value="" />
+                }
+                else
+                {
+                    <div id="previewBanner" hidden><img class="img-fluid img-thumbnail" alt="이미지 미리보기" /></div>
+                    <input type="file" asp-for="Image" class="form-control" accept="image/*" />
+                    <span asp-validation-for="Image" class="text-danger"></span>
+                }
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">크기</label>
+            <div class="col-sm-10">
+                <div class="row g-2">
+                    <div class="col col-md-auto">
+                        <div class="input-group">
+                            <label for="Width" class="input-group-text">가로</label>
+                            <input type="number" asp-for="Width" class="form-control" min="1" />
+                        </div>
+                        <span asp-validation-for="Width" class="text-danger"></span>
+                    </div>
+                    <div class="col-auto d-none d-md-block align-self-center">~</div>
+                    <div class="col col-md-auto">
+                        <div class="input-group">
+                            <label for="Height" class="input-group-text">세로</label>
+                            <input type="number" asp-for="Height" class="form-control" min="1" />
+                            <span asp-validation-for="Height" class="text-danger"></span>
+                        </div>
+                    </div>
+                </div>
+                <span class="text-muted form-text">
+                    설정하지 않으면 자동 크기로 사용됩니다.
+                </span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Link" class="col-sm-2 col-form-label">주소</label>
+            <div class="col-sm-10">
+                <input type="url" asp-for="Link" class="form-control" />
+                <span asp-validation-for="Link" class="text-danger"></span>
+                <span class="text-muted form-text">
+                    배너 클릭 시 페이지 이동 주소를 지정합니다.
+                </span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Order" class="col-sm-2 col-form-label">순서</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="number" asp-for="Order" class="form-control" min="-999" max="999" required />
+                    <span asp-validation-for="Order" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">
+                        사용합니다.
+                    </label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">사용 기간</label>
+            <div class="col-sm-10">
+                <div class="row g-2">
+                    <div class="col col-md-auto">
+                        <input type="date" asp-for="StartAt" class="form-control" />
+                        <span asp-validation-for="StartAt" class="text-danger"></span>
+                    </div>
+                    <div class="col-auto d-none d-md-block align-self-center">~</div>
+                    <div class="col col-md-auto">
+                        <input type="date" asp-for="EndAt" class="form-control" />
+                        <span asp-validation-for="EndAt" class="text-danger"></span>
+                    </div>
+                </div>
+                <span class="text-muted form-text">
+                    사용 기간을 설정하지 않으면 무제한으로 사용됩니다.
+                </span>
+            </div>
+        </div>
+        @if (Model.UpdatedAt is not null)
+        {
+            <div class="row mb-2">
+                <label class="col-sm-2 col-form-label">수정일시</label>
+                <div class="col-sm-10">
+                    <input asp-for="UpdatedAt" class="form-control-plaintext" type="text" readonly />
+                </div>
+            </div>
+        }
+        @if (Model.CreatedAt is not null)
+        {
+            <div class="row mb-2">
+                <label class="col-sm-2 col-form-label">등록일시</label>
+                <div class="col-sm-10">
+                    <input asp-for="CreatedAt" class="form-control-plaintext" type="text" readonly />
+                </div>
+            </div>
+        }
+        <hr />
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a asp-action="Index" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>
+
+<script type="module">
+    document.addEventListener("DOMContentLoaded", function () {
+        const previewImage = document.getElementById("previewBanner");
+        document.getElementById("Image").addEventListener("change", function (e) {
+            console.log(1);
+            const file = e.target.files[0];
+            const image = previewImage.children[0];
+            if (file && file.type.startsWith("image/")) {
+                const reader = new FileReader();
+
+                reader.onload = function (e) {
+                    image.src = e.target.result;
+                    previewImage.removeAttribute("hidden");
+                };
+
+                reader.readAsDataURL(file);
+            } else {
+                image.src = "";
+                previewImage.setAttribute("hidden");
+            }
+        });
+
+        // 배너 이미지 삭제
+        let oldImageSrc = "";
+        document.getElementById("isImageRemove").addEventListener("change", function (e) {
+            let image = document.getElementById("Image");
+            if (e.target.checked) {
+                oldImageSrc = image.src;
+                image.src = "";
+            } else {
+                image.src = oldImageSrc;
+            }
+        });
+    });
+</script>

+ 144 - 0
backend/Views/Page/Banner/Item/Index.cshtml

@@ -0,0 +1,144 @@
+@using bitforum.Helpers;
+@{
+    ViewData["Title"] = "배너 관리";
+
+    var bannerPositions = ViewBag.BannerPositions as List<bitforum.Models.Page.Banner.BannerPosition>;
+    var bannerItems = ViewBag.BannerItems as List<bitforum.Models.Page.Banner.BannerItem>;
+    var total = (ViewBag.Total ?? 0).ToString("N0");
+    var pagination = ViewBag.Pagination as bitforum.Models.Pagination;
+}
+
+<partial name="~/Views/Page/Banner/_Navbar.cshtml" />
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <div class="row g-2 mb-2">
+        <div class="col">
+            <select name="position_id" id="positionID" class="form-select w-auto">
+                <option value="">선택하세요.</option>
+                @foreach (var row in bannerPositions)
+                {
+                    <option value="@row.ID" selected="@(row.ID == ViewBag.PositionID ? "selected" : null)">@row.Subject (@row.BannerItem.Count)</option>
+                }
+            </select>
+        </div>
+    </div>
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @total
+        </div>
+        <div class="col text-end">
+            <button type="button" id="btnListDelete" class="btn btn-sm btn-danger" form="fAdminList" data-action="/Page/Banner/Item/Delete">삭제</button>
+            <a href="/Page/Banner/Item/Write" class="btn btn-sm btn-success">추가</a>
+        </div>
+    </div>
+
+    <form name="f_admin_list" id="fAdminList" method="post" accept-charset="utf-8" autocomplete="off"></form>
+
+    <div class="table-responsive">
+        <table class="table table-striped table-bordered table-hover mt-3">
+            <colgroup>
+                <col width="2%" />
+                <col width="5%" />
+                <col width="*" />
+                <col width="30%" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><input type="checkbox" id="checkedAll" class="form-check-input" value="1" form="fAdminList" /></th>
+                    <th>ID</th>
+                    <th>이미지</th>
+                    <th>제목</th>
+                    <th>순서</th>
+                    <th>사용</th>
+                    <th>조회 수</th>
+                    <th>등록일시</th>
+                    <th>수정일시</th>
+                    <th>비고</th>
+                </tr>
+            </thead>
+            <tbody>
+                @if (bannerItems == null || !bannerItems.Any())
+                {
+                    <tr>
+                        <td colspan="10">No Data.</td>
+                    </tr>
+                }
+                else
+                {
+                    @foreach (var row in bannerItems)
+                    {
+                        <tr>
+                            <td>
+                                <input type="checkbox" name="ids[]" class="form-check-input list-check-box" value="@row.ID" form="fAdminList" />
+                            </td>
+                            <td>@row.ID</td>
+                            <td>
+                                @if (row.Image is not null && row.Image != string.Empty)
+                                {
+                                    <img src="@Url.Content(row.Image)" class="img-fluid img-thumbnail" alt="@row.Subject" />
+                                }
+                                else
+                                {
+                                    <text>-</text>
+                                }
+                            </td>
+                            <td>
+                                <span class="badge text-bg-secondary">[@row.BannerPosition.Subject]</span>
+                                <strong>@row.Subject</strong><br/>
+                                <small>
+                                    사용 기한:
+                                    @if (row.StartAt != null && row.EndAt != null)
+                                    {
+                                        <text>@row.StartAt.GetDateAt() ~ @row.EndAt.GetDateAt()</text>
+                                    } @if (row.StartAt != null && row.EndAt is null)
+                                    {
+                                        <text>@row.StartAt.GetDateAt() ~</text>
+                                    } @if (row.StartAt is null && row.EndAt != null)
+                                    {
+                                        <text>~ @row.EndAt.GetDateAt()</text>
+                                    }
+                                    else
+                                    {
+                                        <text>[무기한]</text>
+                                    }
+                                </small><br/>
+                                <small>크기:@row.Width x @row.Height</small>
+                            </td>
+                            <td>@row.Order</td>
+                            <td>@(row.IsActive ? "Y" : "N")</td>
+                            <td>@row.Views</td>
+                            <td>@row.CreatedAt.GetDateAt()</td>
+                            <td>@row.UpdatedAt.GetDateAt()</td>
+                            <td>
+                                <div class="d-grid gap-2 d-md-block">
+                                    <a href="/Page/Banner/Item/@row.ID/Edit" class="btn btn-sm btn-outline-info">수정</a>
+                                    <a href="/Page/Banner/Item/@row.ID/Delete" class="btn btn-sm btn-outline-danger btn-row-delete">삭제</a>
+                                </div>
+                            </td>
+                        </tr>
+                    }
+                }
+            </tbody>
+        </table>
+
+        <partial name="_Pagination" model="pagination" />
+    </div>
+</div>
+
+<script type="module">
+    $(document).on("change", "#positionID", function () {
+        location.href = ("/Page/Banner/Item/" + $(this).val());
+    });
+</script>

+ 149 - 0
backend/Views/Page/Banner/Item/Write.cshtml

@@ -0,0 +1,149 @@
+@model bitforum.Models.Page.Banner.BannerItem
+@{
+    ViewData["Title"] = "배너 등록";
+    var bannerPositions = ViewBag.BannerPositions as List<bitforum.Models.Page.Banner.BannerPosition>;
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Page/Banner/Item/Create" enctype="multipart/form-data">
+        <div class="row mb-2">
+            <label for="PositionID" class="col-sm-2 col-form-label">위치</label>
+            <div class="col-sm-10">
+                <select asp-for="PositionID" class="form-select w-auto" required>
+                    <option value="">선택하세요.</option>
+                    @foreach (var row in bannerPositions)
+                    {
+                        <option value="@row.ID">@row.Subject</option>
+                    }
+                </select>
+                <span asp-validation-for="PositionID" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Subject" class="col-sm-2 col-form-label">배너 명</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Subject" class="form-control" required />
+                <span asp-validation-for="Subject" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Image" class="col-sm-2 col-form-label">이미지</label>
+            <div class="col-sm-10">
+                <div id="previewBanner" hidden><img class="img-fluid img-thumbnail" alt="이미지 미리보기" /></div>
+                <input type="file" asp-for="Image" class="form-control" accept="image/*" />
+                <span asp-validation-for="Image" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">크기</label>
+            <div class="col-sm-10">
+                <div class="row g-2">
+                    <div class="col col-md-auto">
+                        <div class="input-group">
+                            <label for="Width" class="input-group-text">가로</label>
+                            <input type="number" asp-for="Width" class="form-control" min="1" />
+                        </div>
+                        <span asp-validation-for="Width" class="text-danger"></span>
+                    </div>
+                    <div class="col-auto d-none d-md-block align-self-center">~</div>
+                    <div class="col col-md-auto">
+                        <div class="input-group">
+                            <label for="Height" class="input-group-text">세로</label>
+                            <input type="number" asp-for="Height" class="form-control" min="1" />
+                            <span asp-validation-for="Height" class="text-danger"></span>
+                        </div>
+                    </div>
+                </div>
+                <span class="text-muted form-text">
+                    설정하지 않으면 자동 크기로 사용됩니다.
+                </span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Link" class="col-sm-2 col-form-label">주소</label>
+            <div class="col-sm-10">
+                <input type="url" asp-for="Link" class="form-control" />
+                <span asp-validation-for="Link" class="text-danger"></span>
+                <span class="text-muted form-text">
+                    배너 클릭 시 페이지 이동 주소를 지정합니다.
+                </span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Order" class="col-sm-2 col-form-label">순서</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="number" asp-for="Order" class="form-control" min="-999" max="999" required />
+                    <span asp-validation-for="Order" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">
+                        사용합니다.
+                    </label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">사용 기간</label>
+            <div class="col-sm-10">
+                <div class="row g-2">
+                    <div class="col col-md-auto">
+                        <input type="date" asp-for="StartAt" class="form-control" />
+                        <span asp-validation-for="StartAt" class="text-danger"></span>
+                    </div>
+                    <div class="col-auto d-none d-md-block align-self-center">~</div>
+                    <div class="col col-md-auto">
+                        <input type="date" asp-for="EndAt" class="form-control" />
+                        <span asp-validation-for="EndAt" class="text-danger"></span>
+                    </div>
+                </div>
+                <span class="text-muted form-text">
+                    사용 기간을 설정하지 않으면 무제한으로 사용됩니다.
+                </span>
+            </div>
+        </div>
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a asp-action="Index" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>
+
+<script type="module">
+document.addEventListener("DOMContentLoaded", function () {
+    const previewImage = document.getElementById("previewBanner");
+    document.getElementById("Image").addEventListener("change", function (e) {
+        const file = e.target.files[0];
+        const image = previewImage.children[0];
+            console.log(previewImage);
+        if (file && file.type.startsWith("image/")) {
+            const reader = new FileReader();
+
+            reader.onload = function (e) {
+                image.src = e.target.result;
+                previewImage.removeAttribute("hidden");
+            };
+
+            reader.readAsDataURL(file);
+        } else {
+            image.src = "";
+            previewImage.setAttribute("hidden");
+        }
+    });
+});
+</script>

+ 197 - 0
backend/Views/Page/Banner/Position/Index.cshtml

@@ -0,0 +1,197 @@
+@using bitforum.Helpers;
+@{
+    ViewData["Title"] = "배너 위치";
+    var bannerPositions = ViewBag.BannerPositions as List<bitforum.Models.Page.Banner.BannerPosition>;
+    var total = (bannerPositions?.Count ?? 0).ToString("N0");
+}
+
+<partial name="~/Views/Page/Banner/_Navbar.cshtml" />
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @total
+        </div>
+        <div class="col text-end">
+            <button type="button" id="btnAdd" class="btn btn-sm btn-primary" form="fAdminWrite">추가</button>
+            <button type="submit" id="btnSave" class="btn btn-sm btn-success" form="fAdminWrite">저장</button>
+        </div>
+    </div>
+
+    <div class="table-responsive">
+        <form id="fAdminWrite" asp-action="Save" method="post" accept-charset="utf-8" autocomplete="off"></form>
+
+        <table class="table table-striped table-bordered table-hover mt-3">
+            <caption>
+                위치에 등록된 배너가 있다면 삭제가 불가합니다.<br/>
+                위치를 삭제하려면 등록된 배너를 먼저 삭제해주세요.
+            </caption>
+            <colgroup>
+                <col width="5%" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+            </colgroup>
+            <thead>
+                <tr class="text-center">
+                    <th>ID</th>
+                    <th>Code</th>
+                    <th>위치 명</th>
+                    <th>배너 수</th>
+                    <th>사용</th>
+                    <th>등록일시</th>
+                    <th>수정일시</th>
+                    <th>비고</th>
+                </tr>
+            </thead>
+            <tbody id="categories">
+                @if (bannerPositions == null || !bannerPositions.Any())
+                {
+                    <tr>
+                        <td colspan="8" class="text-center align-middle">No Data.</td>
+                    </tr>
+                }
+                else
+                {
+                    @foreach (var row in bannerPositions)
+                    {
+                        var index = bannerPositions.IndexOf(row);
+
+                        <tr class="align-middle text-center">
+                            <td>
+                                <input type="text" name="request[@index].ID" readonly class="form-control-plaintext text-center" required form="fAdminWrite" value="@row.ID" />
+                            </td>
+                            <td>
+                                <input type="text" name="request[@index].Code" class="form-control" maxlength="30" required form="fAdminWrite" value="@row.Code" />
+                            </td>
+                            <td>
+                                <input type="text" name="request[@index].Subject" class="form-control" maxlength="255" required form="fAdminWrite" value="@row.Subject" />
+                            </td>
+                            <td>@row.BannerItem.Count</td>
+                            <td>
+                                <div class="form-check-inline">
+                                    <input class="form-check-input" type="checkbox" id="request_@(index)_IsActive" name="request[@index].IsActive" @(bannerPositions[index].IsActive ? "checked" : "") form="fAdminWrite" value="true" />
+                                    <label class="form-check-label" for="request_@(index)_IsActive">
+                                        사용
+                                    </label>
+                                </div>
+                            </td>
+                            <td><input type="text" name="request[@index].CreatedAt" readonly class="form-control-plaintext text-center" form="fAdminWrite" value="@row.CreatedAt.GetDateAt()" /></td>
+                            <td><input type="text" name="request[@index].UpdatedAt" readonly class="form-control-plaintext text-center" form="fAdminWrite" value="@row.UpdatedAt.GetDateAt()" /></td>
+                            <td>
+                                <button type="button" class="btn btn-sm btn-danger btn-delete">삭제</button>
+                            </td>
+                        </tr>
+                    }
+                }
+            </tbody>
+        </table>
+    </div>
+</div>
+
+<script type="module">
+    $(function() {
+        let $categories = $("#categories");
+        let total = Number(@total);
+
+        // 추가
+        $(document).on("click", "#btnAdd", function() {
+            if (total <= 0) {
+                $categories.empty();
+            }
+
+            let tableRow = `
+                <tr class="align-middle text-center">
+                    <td>-</td>
+                    <td>
+                        <input type="text" name="request[${total}].Code" class="form-control" maxlength="30" required form="fAdminWrite" />
+                    </td>
+                    <td>
+                        <input type="text" name="request[${total}].Subject" class="form-control" maxlength="255" required form="fAdminWrite" />
+                    </td>
+                    <td>-</td>
+                    <td>
+                        <div class="form-check-inline">
+                            <input class="form-check-input" type="checkbox" id="request_${total}_IsActive" name="request[${total}].IsActive" checked form="fAdminWrite" value="true" />
+                            <label class="form-check-label" for="request_${total}_IsActive">
+                                사용
+                            </label>
+                        </div>
+                    </td>
+                    <td>-</td>
+                    <td>-</td>
+                    <td>
+                        <button type="button" class="btn btn-danger btn-sm btn-delete">삭제</button>
+                    </td>
+                </tr>
+            `;
+
+            $categories.append(tableRow);
+            total++;
+            recalculateIndices();
+        });
+
+        // 삭제
+        $(document).on("click", "button.btn-delete", function(e) {
+            e.target.closest("tr").remove();
+            total--;
+
+            if (total <= 0) {
+                $categories.append(
+                    `<tr><td colspan="8" class="text-center align-middle">No Data.</td></tr>`
+                );
+                total = 0;
+             } else {
+                recalculateIndices();
+             }
+        });
+
+        // 저장
+        $(document).on("click", "#btnSave", function() {
+            if (confirm("저장 하시겠습니까?")) {
+                document.getElementById("fAdminWrite").submit();
+            }
+            return false;
+        });
+
+        // 인덱스 재계산 함수
+        function recalculateIndices() {
+            $categories.find("tr").each(function(index, tr) {
+                $(tr)
+                    .find("input, label")
+                    .each(function() {
+                        let name = $(this).attr("name");
+                        let id = $(this).attr("id");
+
+                        if (name) {
+                            $(this).attr("name", name.replace(/\[\d+\]/, `[${index}]`));
+                        }
+
+                        if (id) {
+                            $(this).attr("id", id.replace(/_\d+_/, `_${index}_`));
+                        }
+                    });
+
+                // 인덱스 기반으로 라벨의 `for` 속성도 수정
+                $(tr)
+                    .find("label")
+                    .each(function() {
+                        let labelFor = $(this).attr("for");
+                        if (labelFor) {
+                            $(this).attr("for", labelFor.replace(/_\d+_/, `_${index}_`));
+                        }
+                    });
+            });
+        }
+    });
+</script>

+ 17 - 0
backend/Views/Page/Banner/_Navbar.cshtml

@@ -0,0 +1,17 @@
+@{
+    string currentAction = ViewContext.RouteData.Values["action"]?.ToString();
+    string currentController = ViewContext.RouteData.Values["controller"]?.ToString();
+}
+
+<ul class="nav nav-pills justify-content-center">
+    <li class="nav-item">
+        <a class="nav-link @(currentController == "Item" && currentAction == "Index" ? "active" : "")" href="@Url.Content("~/Page/Banner/Item")">
+            배너 관리
+        </a>
+    </li>
+    <li class="nav-item">
+        <a class="nav-link @(currentController == "Position" && currentAction == "Index" ? "active" : "")" href="@Url.Content("~/Page/Banner")">
+            배너 위치
+        </a>
+    </li>
+</ul>

+ 22 - 24
backend/Views/Page/Document/Edit.cshtml

@@ -13,22 +13,11 @@
     <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" asp-controller="Document" asp-action="Update">
         <input type="hidden" asp-for="ID" />
 
-        <div class="row mb-2">
-            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
-            <div class="col-sm-10 align-content-center">
-                <div class="form-check-inline">
-                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
-                    <label class="form-check-label" for="IsDisplay">
-                        사용합니다.
-                    </label>
-                    <span asp-validation-for="IsActive" class="text-danger"></span>
-                </div>
-            </div>
-        </div>
+      
         <div class="row mb-2">
             <label for="Code" class="col-sm-2 col-form-label">주소</label>
             <div class="col-sm-10">
-                @ViewBag.siteURL /docs/ <input type="text" asp-for="Code" class="form-control d-inline w-auto"/>
+                @ViewBag.siteURL /docs/ <input type="text" asp-for="Code" class="form-control d-inline w-auto" required />
                 <span asp-validation-for="Code" class="text-danger"></span>
             </div>
         </div>
@@ -36,37 +25,46 @@
             <label for="Subject" class="col-sm-2 col-form-label">제목</label>
             <div class="col-sm-10">
                 <input asp-for="Subject" class="form-control"/>
-                <span asp-validation-for="Subject" class="text-danger"></span>
+                <span asp-validation-for="Subject" class="text-danger" required></span>
             </div>
         </div>
         <div class="row mb-2">
             <label for="Content" class="col-sm-2 col-form-label">내용</label>
             <div class="col-sm-10">
-                <textarea asp-for="Content" class="form-control ck-editor"></textarea>
+                <textarea asp-for="Content" class="form-control ck-editor" required></textarea>
                 <span asp-validation-for="Content" class="text-danger"></span>
             </div>
         </div>
-
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">
+                        사용합니다.
+                    </label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
         @if (Model.UpdatedAt is not null)
         {
             <div class="row mb-2">
                 <label class="col-sm-2 col-form-label">수정일시</label>
-            <div class="col-sm-10">
-                <input asp-for="UpdatedAt" class="form-control-plaintext" type="text" readonly />
+                <div class="col-sm-10">
+                    <input asp-for="UpdatedAt" class="form-control-plaintext" type="text" readonly />
+                </div>
             </div>
-        </div>
         }
-
-         @if (Model.CreatedAt is not null)
+        @if (Model.CreatedAt is not null)
         {
             <div class="row mb-2">
                 <label class="col-sm-2 col-form-label">등록일시</label>
-            <div class="col-sm-10">
+                <div class="col-sm-10">
                     <input asp-for="CreatedAt" class="form-control-plaintext" type="text" readonly />
+                </div>
             </div>
-        </div>
         }
-
         <hr/>
         <div class="d-grid gap-2 text-center d-md-block">
             <button type="submit" class="btn btn-sm btn-success">저장</button>

+ 19 - 13
backend/Views/Page/Document/Index.cshtml

@@ -1,6 +1,9 @@
-@model List<bitforum.Models.Page.Document>
+@using bitforum.Helpers;
 @{
     ViewData["Title"] = "문서 관리";
+
+    var documents = ViewBag.Documents as List<bitforum.Models.Page.Document>;
+    var total = (documents?.Count ?? 0).ToString("N0");
 }
 
 <div class="container-fluid">
@@ -11,10 +14,10 @@
 
     <div class="row g-2 align-items-end">
         <div class="col">
-            Total : @((Model?.Count ?? 0).ToString("N0"))
+            Total : @total
         </div>
         <div class="col text-end">
-            <a class="btn btn-sm btn-success" asp-controller="Document" asp-action="Write">새로등록</a>
+            <a class="btn btn-sm btn-success" asp-controller="Document" asp-action="Write">추가</a>
         </div>
     </div>
 
@@ -27,13 +30,15 @@
                 <col width="10%"/>
                 <col width="9%"/>
                 <col width="9%"/>
+                <col width="9%"/>
                 <col width="8%"/>
             </colgroup>
             <thead>
-                <tr class="text-center">
+                <tr>
                     <th>ID</th>
                     <th>제목</th>
                     <th>주소</th>
+                    <th>사용</th>
                     <th>조회 수</th>
                     <th>등록일시</th>
                     <th>수정일시</th>
@@ -41,19 +46,19 @@
                 </tr>
             </thead>
             <tbody>
-                @if (Model == null || !Model.Any())
+                @if (documents == null || !documents.Any())
                 {
                     <tr>
-                        <td colspan="7" class="text-center align-middle">No Data.</td>
+                        <td colspan="8" class="text-center align-middle">No Data.</td>
                     </tr>
                 }
                 else
                 {
-                    @foreach (var row in Model)
+                    @foreach (var row in documents)
                     {
                         string url = $"https://{ViewBag.siteURL}/{row.Code}";
 
-                        <tr class="text-center align-middle">
+                        <tr>
                             <td>@row.ID</td>
                             <td>@row.Subject</td>
                             <td>
@@ -61,13 +66,14 @@
                                     @url
                                 </a>
                             </td>
+                            <td>@(row.IsActive ? "Y" : "N")</td>
                             <td>@row.Views</td>
-                            <td>@row.CreatedAt</td>
-                            <td>@row.UpdatedAt</td>
+                            <td>@row.CreatedAt.GetDateAt()</td>
+                            <td>@row.UpdatedAt.GetDateAt()</td>
                             <td>
-                                <div class="d-grid gap-2">
-                                    <a class="btn btn-sm btn-info text-white" asp-controller="Document" asp-action="Edit" asp-route-id="@row.ID">수정</a>
-                                    <a class="btn btn-sm btn-danger text-white btn-delete-row" asp-controller="Document" asp-action="Delete" asp-route-id="@row.ID">삭제</a>
+                                <div class="d-grid gap-2 d-md-block">
+                                    <a class="btn btn-sm btn-outline-info" asp-controller="Document" asp-action="Edit" asp-route-id="@row.ID">수정</a>
+                                    <a class="btn btn-sm btn-outline-danger btn-row-delete" asp-controller="Document" asp-action="Delete" asp-route-id="@row.ID">삭제</a>
                                 </div>
                             </td>
                         </tr>

+ 14 - 14
backend/Views/Page/Document/Write.cshtml

@@ -11,29 +11,17 @@
     <partial name="_Editor" />
 
     <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" asp-controller="Document" asp-action="Create">
-        <div class="row mb-2">
-            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
-            <div class="col-sm-10 align-content-center">
-                <div class="form-check-inline">
-                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
-                    <label class="form-check-label" for="IsDisplay">
-                        사용합니다.
-                    </label>
-                    <span asp-validation-for="IsActive" class="text-danger"></span>
-                </div>
-            </div>
-        </div>
         <div class="row mb-2">
             <label for="Code" class="col-sm-2 col-form-label">주소</label>
             <div class="col-sm-10">
-                @ViewBag.siteURL /docs/ <input type="text" asp-for="Code" class="form-control d-inline w-auto"/>
+                @ViewBag.siteURL /docs/ <input type="text" asp-for="Code" class="form-control d-inline w-auto" required />
                 <span asp-validation-for="Code" class="text-danger"></span>
             </div>
         </div>
         <div class="row mb-2">
             <label for="Subject" class="col-sm-2 col-form-label">제목</label>
             <div class="col-sm-10">
-                <input asp-for="Subject" class="form-control"/>
+                <input asp-for="Subject" class="form-control" required />
                 <span asp-validation-for="Subject" class="text-danger"></span>
             </div>
         </div>
@@ -44,6 +32,18 @@
                 <span asp-validation-for="Content" class="text-danger"></span>
             </div>
         </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">
+                        사용합니다.
+                    </label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
         <hr/>
         <div class="d-grid gap-2 text-center d-md-block">
             <button type="submit" class="btn btn-sm btn-success">저장</button>

+ 29 - 23
backend/Views/Page/Faq/Index.cshtml → backend/Views/Page/Faq/Category/Index.cshtml

@@ -1,9 +1,12 @@
-@model bitforum.Models.Views.FaqCategoryViewModel
+@using bitforum.Helpers;
 @{
-    ViewData["Title"] = "FAQ 관리";
-    var total = (Model?.FaqCategories?.Count ?? 0).ToString("N0");
+    ViewData["Title"] = "FAQ 분류";
+    var faqCategories = ViewBag.FaqCategories as List<bitforum.Models.Page.Faq.FaqCategory>;
+    var total = (faqCategories?.Count ?? 0).ToString("N0");
 }
 
+<partial name="~/Views/Page/Faq/_Navbar.cshtml" />
+
 <div class="container-fluid">
     <h3>@ViewData["Title"]</h3>
     <hr />
@@ -24,6 +27,10 @@
         <form id="fAdminWrite" asp-action="Save" method="post" accept-charset="utf-8" autocomplete="off"></form>
 
         <table class="table table-striped table-bordered table-hover mt-3">
+            <caption>
+                분류에 등록된 FAQ 가 있다면 삭제가 불가합니다.<br/>
+                분류를 삭제하려면 해당 FAQ 를 먼저 삭제해주세요.
+            </caption>
             <colgroup>
                 <col width="5%" />
                 <col width="*" />
@@ -40,7 +47,7 @@
                     <th>ID</th>
                     <th>Code</th>
                     <th>분류 명</th>
-                    <th>게시글 수</th>
+                    <th>FAQ 수</th>
                     <th>순서</th>
                     <th>사용</th>
                     <th>등록일시</th>
@@ -49,7 +56,7 @@
                 </tr>
             </thead>
             <tbody id="categories">
-                @if (Model?.FaqCategories == null || !Model.FaqCategories.Any())
+                @if (faqCategories == null || !faqCategories.Any())
                 {
                     <tr>
                         <td colspan="9" class="text-center align-middle">No Data.</td>
@@ -57,37 +64,36 @@
                 }
                 else
                 {
-                    @foreach (var row in Model.FaqCategories)
+                    @foreach (var row in faqCategories)
                     {
-                        var index = Model.FaqCategories.IndexOf(row);
-                        var faqItemRows = (ViewBag?.ItemCounts != null && ViewBag.ItemCounts.ContainsKey(row.ID) ? ViewBag.ItemCounts[row.ID] : 0);
+                        var index = faqCategories.IndexOf(row);
 
                         <tr class="align-middle text-center">
                             <td>
-                                <input type="text" name="FaqCategories[@index].ID" readonly class="form-control-plaintext text-center" required form="fAdminWrite" value="@row.ID" />
+                                <input type="text" name="request[@index].ID" readonly class="form-control-plaintext text-center" required form="fAdminWrite" value="@row.ID" />
                             </td>
                             <td>
-                                <input type="text" name="FaqCategories[@index].Code" class="form-control" maxlength="30" required form="fAdminWrite" value="@row.Code" />
+                                <input type="text" name="request[@index].Code" class="form-control" maxlength="30" required form="fAdminWrite" value="@row.Code" />
                             </td>
                             <td>
-                                <input type="text" name="FaqCategories[@index].Subject" class="form-control" maxlength="255" required form="fAdminWrite" value="@row.Subject" />
+                                <input type="text" name="request[@index].Subject" class="form-control" maxlength="255" required form="fAdminWrite" value="@row.Subject" />
                             </td>
-                            <td>@faqItemRows</td>
+                            <td>@row.FaqItem.Count</td>
                             <td>
-                                <input type="number" name="FaqCategories[@index].Order" class="form-control" min="-999" max="999" required form="fAdminWrite" value="@row.Order" />
+                                <input type="number" name="request[@index].Order" class="form-control" min="-999" max="999" required form="fAdminWrite" value="@row.Order" />
                             </td>
                             <td>
                                 <div class="form-check-inline">
-                                    <input class="form-check-input" type="checkbox" id="FaqCategories_@(index)_IsActive" name="FaqCategories[@index].IsActive" @(Model.FaqCategories[index].IsActive ? "checked" : "") form="fAdminWrite" value="true" />
-                                    <label class="form-check-label" for="FaqCategories_@(index)_IsActive">
+                                    <input class="form-check-input" type="checkbox" id="request_@(index)_IsActive" name="request[@index].IsActive" @(faqCategories[index].IsActive ? "checked" : "") form="fAdminWrite" value="true" />
+                                    <label class="form-check-label" for="request_@(index)_IsActive">
                                         사용
                                     </label>
                                 </div>
                             </td>
-                            <td><input type="text" name="FaqCategories[@index].CreatedAt" readonly class="form-control-plaintext text-center" form="fAdminWrite" value="@row.CreatedAt" /></td>
-                            <td><input type="text" name="FaqCategories[@index].UpdatedAt" readonly class="form-control-plaintext text-center" form="fAdminWrite" value="@row.UpdatedAt" /></td>
+                            <td><input type="text" name="request[@index].CreatedAt" readonly class="form-control-plaintext text-center" form="fAdminWrite" value="@row.CreatedAt.GetDateAt()" /></td>
+                            <td><input type="text" name="request[@index].UpdatedAt" readonly class="form-control-plaintext text-center" form="fAdminWrite" value="@row.UpdatedAt.GetDateAt()" /></td>
                             <td>
-                                <button type="button" class="btn btn-danger btn-sm btn-delete">삭제</button>
+                                <button type="button" class="btn btn-sm btn-danger btn-delete">삭제</button>
                             </td>
                         </tr>
                     }
@@ -112,19 +118,19 @@
                 <tr class="align-middle text-center">
                     <td>-</td>
                     <td>
-                        <input type="text" name="FaqCategories[${total}].Code" class="form-control" maxlength="30" required form="fAdminWrite" />
+                        <input type="text" name="request[${total}].Code" class="form-control" maxlength="30" required form="fAdminWrite" />
                     </td>
                     <td>
-                        <input type="text" name="FaqCategories[${total}].Subject" class="form-control" maxlength="255" required form="fAdminWrite" />
+                        <input type="text" name="request[${total}].Subject" class="form-control" maxlength="255" required form="fAdminWrite" />
                     </td>
                     <td>-</td>
                     <td>
-                        <input type="number" name="FaqCategories[${total}].Order" class="form-control" min="-999" max="999" required form="fAdminWrite" />
+                        <input type="number" name="request[${total}].Order" class="form-control" min="-999" max="999" required form="fAdminWrite" />
                     </td>
                     <td>
                         <div class="form-check-inline">
-                            <input class="form-check-input" type="checkbox" id="FaqCategories_${total}_IsActive" name="FaqCategories[${total}].IsActive" checked form="fAdminWrite" value="true" />
-                            <label class="form-check-label" for="FaqCategories_${total}_IsActive">
+                            <input class="form-check-input" type="checkbox" id="request_${total}_IsActive" name="request[${total}].IsActive" checked form="fAdminWrite" value="true" />
+                            <label class="form-check-label" for="request_${total}_IsActive">
                                 사용
                             </label>
                         </div>

+ 84 - 0
backend/Views/Page/Faq/Item/Edit.cshtml

@@ -0,0 +1,84 @@
+@model bitforum.Models.Page.Faq.FaqItem
+@{
+    ViewData["Title"] = "FAQ 수정";
+    var faqCategories = ViewBag.FaqCategory as SelectList;
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+
+    <form name="f_admin_edit" id="fAdminEdit" method="post" accept-charset="utf-8" autocomplete="off" action="/Page/Faq/Item/Update">
+        <input type="hidden" asp-for="ID" />
+
+        <div class="row mb-2">
+            <label for="CategoryID" class="col-sm-2 col-form-label">분류</label>
+            <div class="col-sm-10">
+                <select asp-for="CategoryID" asp-items="faqCategories" class="form-select w-auto" required></select>
+                <span asp-validation-for="CategoryID" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Question" class="col-sm-2 col-form-label">질문</label>
+            <div class="col-sm-10">
+                <input asp-for="Question" class="form-control required" />
+                <span asp-validation-for="Question" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Answer" class="col-sm-2 col-form-label">답변</label>
+            <div class="col-sm-10">
+                <textarea asp-for="Answer" class="form-control ck-editor"></textarea>
+                <span asp-validation-for="Answer" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Order" class="col-sm-2 col-form-label">순서</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="number" asp-for="Order" class="form-control" min="-999" max="999" required />
+                    <span asp-validation-for="Order" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">
+                        사용합니다.
+                    </label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        @if (Model.UpdatedAt is not null)
+        {
+            <div class="row mb-2">
+                <label class="col-sm-2 col-form-label">수정일시</label>
+                <div class="col-sm-10">
+                    <input asp-for="UpdatedAt" class="form-control-plaintext" type="text" readonly />
+                </div>
+            </div>
+        }
+        @if (Model.CreatedAt is not null)
+        {
+            <div class="row mb-2">
+                <label class="col-sm-2 col-form-label">등록일시</label>
+                <div class="col-sm-10">
+                    <input asp-for="CreatedAt" class="form-control-plaintext" type="text" readonly />
+                </div>
+            </div>
+        }
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Page/Faq/Item" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>

+ 113 - 0
backend/Views/Page/Faq/Item/Index.cshtml

@@ -0,0 +1,113 @@
+@using bitforum.Helpers;
+@{
+    ViewData["Title"] = "FAQ 관리";
+
+    var faqCategories = ViewBag.FaqCategories as List<bitforum.Models.Page.Faq.FaqCategory>;
+    var faqItems = ViewBag.FaqItems as List<bitforum.Models.Page.Faq.FaqItem>;
+    var total = (ViewBag.Total ?? 0).ToString("N0");
+    var pagination = ViewBag.Pagination as bitforum.Models.Pagination;
+}
+
+<partial name="~/Views/Page/Faq/_Navbar.cshtml" />
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <div class="row g-2 mb-2">
+        <div class="col">
+            <select name="category_id" id="categoryID" class="form-select w-auto">
+                <option value="">선택하세요.</option>
+                @foreach (var row in faqCategories)
+                {
+                    <option value="@row.ID" selected="@(row.ID == ViewBag.CategoryID ? "selected" : null)">@row.Subject (@row.FaqItem.Count)</option>
+                }
+            </select>
+        </div>
+    </div>
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @total
+        </div>
+        <div class="col text-end">
+            <button type="button" id="btnListDelete" class="btn btn-sm btn-danger" form="fAdminList" data-action="/Page/Faq/Item/Delete">삭제</button>
+            <a href="/Page/Faq/Item/Write" class="btn btn-sm btn-success">추가</a>
+        </div>
+    </div>
+
+    <form name="f_admin_list" id="fAdminList" method="post" accept-charset="utf-8" autocomplete="off"></form>
+
+    <div class="table-responsive">
+        <table class="table table-striped table-bordered table-hover mt-3">
+            <colgroup>
+                <col width="2%" />
+                <col width="5%" />
+                <col width="30%" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+                <col width="*" />
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><input type="checkbox" id="checkedAll" class="form-check-input" value="1" form="fAdminList" /></th>
+                    <th>ID</th>
+                    <th>질문</th>
+                    <th>순서</th>
+                    <th>사용</th>
+                    <th>조회 수</th>
+                    <th>등록일시</th>
+                    <th>수정일시</th>
+                    <th>비고</th>
+                </tr>
+            </thead>
+            <tbody>
+                @if (faqItems == null || !faqItems.Any())
+                {
+                    <tr>
+                        <td colspan="9">No Data.</td>
+                    </tr>
+                }
+                else
+                {
+                    @foreach (var row in faqItems)
+                    {
+                        <tr>
+                            <td>
+                                <input type="checkbox" name="ids[]" class="form-check-input list-check-box" value="@row.ID" form="fAdminList" />
+                            </td>
+                            <td>@row.ID</td>
+                            <td>
+                                <span class="badge text-bg-secondary">[@row.FaqCategory.Subject]</span> @row.Question
+                            </td>
+                            <td>@row.Order</td>
+                            <td>@(row.IsActive ? "Y" : "N")</td>
+                            <td>@row.Views</td>
+                            <td>@row.CreatedAt.GetDateAt()</td>
+                            <td>@row.UpdatedAt.GetDateAt()</td>
+                            <td>
+                                <div class="d-grid gap-2 d-md-block">
+                                    <a href="/Page/Faq/Item/@row.ID/Edit" class=" btn btn-sm btn-outline-info">수정</a>
+                                    <a href="/Page/Faq/Item/@row.ID/Delete" class="btn btn-sm btn-outline-danger btn-row-delete">삭제</a>
+                                </div>
+                            </td>
+                        </tr>
+                    }
+                }
+            </tbody>
+        </table>
+
+        <partial name="_Pagination" model="pagination" />
+    </div>
+</div>
+
+<script type="module">
+    $(document).on("change", "#categoryID", function () {
+        location.href = ("/Page/Faq/Item/" + $(this).val());
+    });
+</script>

+ 70 - 0
backend/Views/Page/Faq/Item/Write.cshtml

@@ -0,0 +1,70 @@
+@model bitforum.Models.Page.Faq.FaqItem
+@{
+    ViewData["Title"] = "FAQ 등록";
+    var faqCategories = ViewBag.FaqCategory as List<bitforum.Models.Page.Faq.FaqCategory>;
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" action="/Page/Faq/Item/Create">
+        <div class="row mb-2">
+            <label for="CategoryID" class="col-sm-2 col-form-label">분류</label>
+            <div class="col-sm-10">
+                <select asp-for="CategoryID" class="form-select w-auto" required>
+                    <option value="">선택하세요.</option>
+                    @foreach (var row in faqCategories)
+                    {
+                        <option value="@row.ID">@row.Subject</option>
+                    }
+                </select>
+                <span asp-validation-for="CategoryID" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Question" class="col-sm-2 col-form-label">질문</label>
+            <div class="col-sm-10">
+                <input asp-for="Question" class="form-control" required />
+                <span asp-validation-for="Question" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Answer" class="col-sm-2 col-form-label">답변</label>
+            <div class="col-sm-10">
+                <textarea asp-for="Answer" class="form-control ck-editor"></textarea>
+                <span asp-validation-for="Answer" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Order" class="col-sm-2 col-form-label">순서</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="number" asp-for="Order" class="form-control" min="-999" max="999" required />
+                    <span asp-validation-for="Order" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">
+                        사용합니다.
+                    </label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a href="/Page/Faq/Item" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>

+ 17 - 0
backend/Views/Page/Faq/_Navbar.cshtml

@@ -0,0 +1,17 @@
+@{
+    string currentAction = ViewContext.RouteData.Values["action"]?.ToString();
+    string currentController = ViewContext.RouteData.Values["controller"]?.ToString();
+}
+
+<ul class="nav nav-pills justify-content-center">
+    <li class="nav-item">
+        <a class="nav-link @(currentController == "Item" && currentAction == "Index" ? "active" : "")" href="@Url.Content("~/Page/Faq/Item")">
+            FAQ 관리
+        </a>
+    </li>
+    <li class="nav-item">
+        <a class="nav-link @(currentController == "Category" && currentAction == "Index" ? "active" : "")" href="@Url.Content("~/Page/Faq")">
+            FAQ 분류
+        </a>
+    </li>
+</ul>

+ 105 - 0
backend/Views/Page/Popup/Edit.cshtml

@@ -0,0 +1,105 @@
+@model bitforum.Models.Page.Popup
+@{
+    ViewData["Title"] = "팝업 수정";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" asp-controller="Popup" asp-action="Update">
+        <input type="hidden" asp-for="ID" />
+
+        <div class="row mb-2">
+            <label for="Subject" class="col-sm-2 col-form-label">제목</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Subject" class="form-control" required />
+                <span asp-validation-for="Subject" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Content" class="col-sm-2 col-form-label">내용</label>
+            <div class="col-sm-10">
+                <textarea asp-for="Content" class="form-control ck-editor"></textarea>
+                <span asp-validation-for="Content" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Link" class="col-sm-2 col-form-label">주소</label>
+            <div class="col-sm-10">
+                <input type="url" asp-for="Link" class="form-control" />
+                <span asp-validation-for="Link" class="text-danger"></span>
+                <span class="text-muted form-text">
+                    팝업 클릭 시 페이지 이동 주소를 지정합니다.
+                </span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Order" class="col-sm-2 col-form-label">순서</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="number" asp-for="Order" class="form-control" min="-999" max="999" required />
+                    <span asp-validation-for="Order" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">
+                        사용합니다.
+                    </label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">사용 기간</label>
+            <div class="col-sm-10">
+                <div class="row g-2">
+                    <div class="col col-md-auto">
+                        <input type="date" asp-for="StartAt" class="form-control" />
+                        <span asp-validation-for="StartAt" class="text-danger"></span>
+                    </div>
+                    <div class="col-auto d-none d-md-block align-self-center">~</div>
+                    <div class="col col-md-auto">
+                        <input type="date" asp-for="EndAt" class="form-control" />
+                        <span asp-validation-for="EndAt" class="text-danger"></span>
+                    </div>
+                </div>
+                <span class="text-muted form-text">
+                    사용 기간을 설정하지 않으면 무제한으로 사용됩니다.
+                </span>
+            </div>
+        </div>
+        @if (Model.UpdatedAt is not null)
+        {
+            <div class="row mb-2">
+                <label class="col-sm-2 col-form-label">수정일시</label>
+                <div class="col-sm-10">
+                    <input asp-for="UpdatedAt" class="form-control-plaintext" type="text" readonly />
+                </div>
+            </div>
+        }
+        @if (Model.CreatedAt is not null)
+        {
+            <div class="row mb-2">
+                <label class="col-sm-2 col-form-label">등록일시</label>
+                <div class="col-sm-10">
+                    <input asp-for="CreatedAt" class="form-control-plaintext" type="text" readonly />
+                </div>
+            </div>
+        }
+        <hr />
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a asp-action="Index" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br />
+    </form>
+</div>

+ 117 - 0
backend/Views/Page/Popup/Index.cshtml

@@ -0,0 +1,117 @@
+@using bitforum.Helpers;
+@{
+    ViewData["Title"] = "팝업 관리";
+
+    var popups = ViewBag.Popups as List<bitforum.Models.Page.Popup>;
+    var total = (popups?.Count ?? 0).ToString("N0");
+    var pagination = ViewBag.Pagination as bitforum.Models.Pagination;
+}
+
+<div class="container-fluid">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+
+    <div class="row g-2 align-items-end">
+        <div class="col">
+            Total : @total
+        </div>
+        <div class="col text-end">
+            <button type="button" id="btnListDelete" class="btn btn-sm btn-danger" form="fAdminList" data-action="/Page/Popup/Delete">삭제</button>
+            <a class="btn btn-sm btn-success" asp-controller="Popup" asp-action="Write">추가</a>
+        </div>
+    </div>
+
+    <form name="f_admin_list" id="fAdminList" method="post" accept-charset="utf-8" autocomplete="off"></form>
+
+    <div class="table-responsive">
+        <table class="table table-striped table-bordered table-hover mt-3">
+            <colgroup>
+                <col width="2%"/>
+                <col width="5%"/>
+                <col width="*"/>
+                <col width="*"/>
+                <col width="10%"/>
+                <col width="10%"/>
+                <col width="9%"/>
+                <col width="9%"/>
+                <col width="8%"/>
+            </colgroup>
+            <thead>
+                <tr>
+                    <th><input type="checkbox" id="checkedAll" class="form-check-input" value="1" form="fAdminList" /></th>
+                    <th>ID</th>
+                    <th>제목</th>
+                    <th>주소</th>
+                    <th>조회 수</th>
+                    <th>사용 여부</th>
+                    <th>등록일시</th>
+                    <th>수정일시</th>
+                    <th>비고</th>
+                </tr>
+            </thead>
+            <tbody>
+                @if (popups == null || !popups.Any())
+                {
+                    <tr>
+                        <td colspan="9" class="text-center align-middle">No Data.</td>
+                    </tr>
+                }
+                else
+                {
+                    @foreach (var row in popups)
+                    {
+                        <tr>
+                            <td>
+                                <input type="checkbox" name="ids[]" class="form-check-input list-check-box" value="@row.ID" form="fAdminList" />
+                            </td>
+                            <td>@row.ID</td>
+                            <td>
+                                <strong>@row.Subject</strong><br/>
+                                <small>
+                                    사용 기한:
+                                    @if (row.StartAt != null && row.EndAt != null)
+                                    {
+                                        <text>@row.StartAt.GetDateAt() ~ @row.EndAt.GetDateAt()</text>
+                                    } @if (row.StartAt != null && row.EndAt is null)
+                                    {
+                                        <text>@row.StartAt.GetDateAt() ~</text>
+                                    } @if (row.StartAt is null && row.EndAt != null)
+                                    {
+                                        <text>~ @row.EndAt.GetDateAt()</text>
+                                    }
+                                    else
+                                    {
+                                        <text>[무기한]</text>
+                                    }
+                                </small>
+                            </td>
+                            <td>
+                                @if (@row.Link is not null) {
+                                    <a href="@row.Link" target="_blank" rel="external">
+                                        @row.Link
+                                    </a>
+                                } else {
+                                    <text>-</text>
+                                }
+                            </td>
+                            <td>@row.Views</td>
+                            <td>@(row.IsActive ? "Y" : "N")</td>
+                            <td>@row.CreatedAt.GetDateAt()</td>
+                            <td>@row.UpdatedAt.GetDateAt()</td>
+                            <td>
+                                <div class="d-grid gap-2 d-md-block">
+                                    <a class="btn btn-sm btn-outline-info" asp-controller="Popup" asp-action="Edit" asp-route-id="@row.ID">수정</a>
+                                    <a class="btn btn-sm btn-outline-danger btn-row-delete" asp-controller="Popup" asp-action="Delete" asp-route-id="@row.ID">삭제</a>
+                                </div>
+                            </td>
+                        </tr>
+                    }
+                }
+            </tbody>
+        </table>
+
+        <partial name="_Pagination" model="pagination" />
+    </div>
+</div>

+ 85 - 0
backend/Views/Page/Popup/Write.cshtml

@@ -0,0 +1,85 @@
+@model bitforum.Models.Page.Popup
+@{
+    ViewData["Title"] = "팝업 등록";
+}
+
+<div class="container">
+    <h3>@ViewData["Title"]</h3>
+    <hr />
+
+    <partial name="_StatusMessage" />
+    <partial name="_Editor" />
+
+    <form name="f_admin_write" id="fAdminWrite" method="post" accept-charset="utf-8" autocomplete="off" asp-controller="Popup" asp-action="Create">
+        <div class="row mb-2">
+            <label for="Subject" class="col-sm-2 col-form-label">제목</label>
+            <div class="col-sm-10">
+                <input type="text" asp-for="Subject" class="form-control" required />
+                <span asp-validation-for="Subject" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Content" class="col-sm-2 col-form-label">내용</label>
+            <div class="col-sm-10">
+                <textarea asp-for="Content" class="form-control ck-editor"></textarea>
+                <span asp-validation-for="Content" class="text-danger"></span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Link" class="col-sm-2 col-form-label">주소</label>
+            <div class="col-sm-10">
+                <input type="url" asp-for="Link" class="form-control" />
+                <span asp-validation-for="Link" class="text-danger"></span>
+                <span class="text-muted form-text">
+                    팝업 클릭 시 페이지 이동 주소를 지정합니다.
+                </span>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="Order" class="col-sm-2 col-form-label">순서</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="number" asp-for="Order" class="form-control" min="-999" max="999" required />
+                    <span asp-validation-for="Order" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label for="IsActive" class="col-sm-2 col-form-label">사용 여부</label>
+            <div class="col-sm-10 align-content-center">
+                <div class="form-check-inline">
+                    <input type="checkbox" asp-for="IsActive" class="form-check-input" />
+                    <label class="form-check-label" for="IsActive">
+                        사용합니다.
+                    </label>
+                    <span asp-validation-for="IsActive" class="text-danger"></span>
+                </div>
+            </div>
+        </div>
+        <div class="row mb-2">
+            <label class="col-sm-2 col-form-label">사용 기간</label>
+            <div class="col-sm-10">
+                <div class="row g-2">
+                    <div class="col col-md-auto">
+                        <input type="date" asp-for="StartAt" class="form-control" />
+                        <span asp-validation-for="StartAt" class="text-danger"></span>
+                    </div>
+                    <div class="col-auto d-none d-md-block align-self-center">~</div>
+                    <div class="col col-md-auto">
+                        <input type="date" asp-for="EndAt" class="form-control" />
+                        <span asp-validation-for="EndAt" class="text-danger"></span>
+                    </div>
+                </div>
+                <span class="text-muted form-text">
+                    사용 기간을 설정하지 않으면 무제한으로 사용됩니다.
+                </span>
+            </div>
+        </div>
+        <hr/>
+        <div class="d-grid gap-2 text-center d-md-block">
+            <button type="submit" class="btn btn-sm btn-success">저장</button>
+            <a asp-action="Index" class="btn btn-sm btn-secondary">취소</a>
+        </div>
+        <br/>
+    </form>
+</div>

+ 15 - 7
backend/Views/SCSS/admin.scss

@@ -1,13 +1,21 @@
-#server {
-    table {
-        width: 100%;
-        table-layout: fixed;
+table {
+    width: 100%;
+    min-width: 800px;
 
+    tr {
+        th, td {
+            -ms-word-wrap: inherit;
+            word-wrap: inherit;
+            overflow-wrap: break-word;
+            text-align: center;
+            vertical-align: middle;
+        }
+    }
+
+    thead {
         tr {
             th, td {
-                -ms-word-wrap: inherit;
-                word-wrap: inherit;
-                overflow-wrap: break-word;
+                text-align: center;
             }
         }
     }

+ 15 - 0
backend/Views/SCSS/site.scss

@@ -65,6 +65,11 @@ body {
                     &:hover {
                         background-color: #f0f0f0;
                     }
+
+                    &.active {
+                        background-color: #e9e9e9;
+                        outline: 1px solid #ddd;
+                    }
                 }
             }
         }
@@ -73,6 +78,16 @@ body {
             padding: 0.781rem;
             text-align: center;
             border-top: 1px solid #ddd;
+
+            a {
+                color: #333;
+                text-decoration: none;
+
+                &:hover {
+                    color: blue;
+                    text-decoration: underline;
+                }
+            }
         }
     }
 

+ 2 - 1
backend/Views/Shared/_Layout.cshtml

@@ -25,6 +25,7 @@
     <link rel="stylesheet" href="~/node_modules/bootstrap/dist/css/bootstrap.min.css" />
     <link rel="stylesheet" href="~/node_modules/bootstrap-icons/font/bootstrap-icons.min.css" />
     <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
+    <link rel="stylesheet" href="~/css/admin.css" asp-append-version="true" />
 
     @await RenderSectionAsync("Styles", required: false)
 </head>
@@ -39,7 +40,7 @@
                 @Html.Partial("_MenuItem", menu)
             }
         </ul>
-        <footer>ⓒ PLAYR. All Rights Reserved</footer>
+        <footer>ⓒ <a href="https://playr.co.kr" target="_blank" rel="external">PLAYR</a>. All Rights Reserved</footer>
     </aside>
 
     <!-- 우측 -->

+ 14 - 2
backend/Views/Shared/_MenuItem.cshtml

@@ -1,9 +1,21 @@
 @model bitforum.Constants.Menu;
+@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor
 
-@* 좌측 메뉴 *@
+@{
+    // 현재 요청 경로 가져오기
+    var currentPath = HttpContextAccessor.HttpContext?.Request?.Path.ToString()?.TrimEnd('/');
 
+    // 고정된 메뉴 경로
+    var menuPath = (Model.Path?.TrimEnd('/') ?? string.Empty);
+
+    // 활성화 여부 확인
+    var isActive = !string.IsNullOrEmpty(currentPath) && !string.IsNullOrEmpty(menuPath) &&
+                   (currentPath.Equals(menuPath, StringComparison.OrdinalIgnoreCase) ||
+                    currentPath.StartsWith(menuPath + "/", StringComparison.OrdinalIgnoreCase) ||
+                    menuPath.StartsWith(currentPath + "/", StringComparison.OrdinalIgnoreCase));
+}
 <li class="nav-item">
-    <a href="@Model.Path" class="nav-link"
+    <a href="@Model.Path" class="nav-link @(isActive ? "active" : "")"
         @Html.Raw(Model.Children != null && Model.Children.Any() ? "data-bs-toggle=\"collapse\"" : "")
         @Html.Raw(Model.Children != null && Model.Children.Any() ? $"data-bs-target=\"#menu-{Model.Id}\"" : "")
     >

+ 53 - 0
backend/Views/Shared/_Pagination.cshtml

@@ -0,0 +1,53 @@
+@using Microsoft.AspNetCore.WebUtilities
+@model bitforum.Models.Pagination
+
+@if (Model.TotalRows > 0)
+{
+<nav id="pagination" aria-label="Page navigation">
+    <ul class="pagination justify-content-center">
+
+        @if (Model.Page > Model.PageGroupSize)
+        {
+            <li class="page-item">
+                <a class="page-link" href="?page=@Model.PrevGroupPage&perPage=@Model.PerPage@Model.BuildQueryString()">이전</a>
+            </li>
+        }
+        else
+        {
+            <li class="page-item disabled">
+                <span class="page-link">이전</span>
+            </li>
+        }
+
+        <!-- 페이지 번호 표시 -->
+        @for (int i = Model.StartPage; i <= Model.EndPage; i++)
+        {
+            if (i == Model.Page)
+            {
+                <li class="page-item active">
+                    <span class="page-link">@i</span>
+                </li>
+            }
+            else
+            {
+                <li class="page-item">
+                    <a class="page-link" href="?page=@i&perPage=@Model.PerPage@Model.BuildQueryString()">@i</a>
+                </li>
+            }
+        }
+
+        @if (Model.HasNextPage && Model.NextGroupPage <= Model.TotalPage)
+        {
+            <li class="page-item">
+                <a class="page-link" href="?page=@Model.NextGroupPage&perPage=@Model.PerPage@Model.BuildQueryString()">다음</a>
+            </li>
+        }
+        else
+        {
+            <li class="page-item disabled">
+                <span class="page-link">다음</span>
+            </li>
+        }
+    </ul>
+</nav>
+}

BIN
backend/bin/Debug/net8.0/bitforum.dll


BIN
backend/bin/Debug/net8.0/bitforum.exe


BIN
backend/bin/Debug/net8.0/bitforum.pdb


+ 148 - 66
backend/bin/Debug/net8.0/bitforum.staticwebassets.endpoints.json

@@ -2483,7 +2483,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "153"
+          "Value": "265"
         },
         {
           "Name": "Content-Type",
@@ -2491,22 +2491,22 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=\""
+          "Value": "\"ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4="
+          "Value": "sha256-ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8="
         }
       ]
     },
     {
-      "Route": "css/admin.illdtnevdp.css",
+      "Route": "css/admin.dovpgmbu0s.css",
       "AssetFile": "css/admin.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2520,7 +2520,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "153"
+          "Value": "265"
         },
         {
           "Name": "Content-Type",
@@ -2528,21 +2528,21 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=\""
+          "Value": "\"ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "illdtnevdp"
+          "Value": "dovpgmbu0s"
         },
         {
           "Name": "integrity",
-          "Value": "sha256-lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4="
+          "Value": "sha256-ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8="
         },
         {
           "Name": "label",
@@ -2565,7 +2565,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "154"
+          "Value": "222"
         },
         {
           "Name": "Content-Type",
@@ -2573,22 +2573,22 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=\""
+          "Value": "\"NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs="
+          "Value": "sha256-NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o="
         }
       ]
     },
     {
-      "Route": "css/admin.min.qxtxag1zp9.css",
+      "Route": "css/admin.min.u21bc84aoq.css",
       "AssetFile": "css/admin.min.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2602,7 +2602,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "154"
+          "Value": "222"
         },
         {
           "Name": "Content-Type",
@@ -2610,21 +2610,21 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=\""
+          "Value": "\"NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "qxtxag1zp9"
+          "Value": "u21bc84aoq"
         },
         {
           "Name": "integrity",
-          "Value": "sha256-zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs="
+          "Value": "sha256-NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o="
         },
         {
           "Name": "label",
@@ -2633,7 +2633,44 @@
       ]
     },
     {
-      "Route": "css/site.bh06sh7otq.css",
+      "Route": "css/site.css",
+      "AssetFile": "css/site.css",
+      "Selectors": [],
+      "ResponseHeaders": [
+        {
+          "Name": "Accept-Ranges",
+          "Value": "bytes"
+        },
+        {
+          "Name": "Cache-Control",
+          "Value": "no-cache"
+        },
+        {
+          "Name": "Content-Length",
+          "Value": "1615"
+        },
+        {
+          "Name": "Content-Type",
+          "Value": "text/css"
+        },
+        {
+          "Name": "ETag",
+          "Value": "\"D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=\""
+        },
+        {
+          "Name": "Last-Modified",
+          "Value": "Sat, 18 Jan 2025 11:44:19 GMT"
+        }
+      ],
+      "EndpointProperties": [
+        {
+          "Name": "integrity",
+          "Value": "sha256-D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs="
+        }
+      ]
+    },
+    {
+      "Route": "css/site.l7qd4nl7uy.css",
       "AssetFile": "css/site.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2647,7 +2684,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1413"
+          "Value": "1615"
         },
         {
           "Name": "Content-Type",
@@ -2655,21 +2692,21 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8=\""
+          "Value": "\"D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:44:19 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "bh06sh7otq"
+          "Value": "l7qd4nl7uy"
         },
         {
           "Name": "integrity",
-          "Value": "sha256-EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8="
+          "Value": "sha256-D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs="
         },
         {
           "Name": "label",
@@ -2678,8 +2715,8 @@
       ]
     },
     {
-      "Route": "css/site.css",
-      "AssetFile": "css/site.css",
+      "Route": "css/site.min.css",
+      "AssetFile": "css/site.min.css",
       "Selectors": [],
       "ResponseHeaders": [
         {
@@ -2692,7 +2729,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1413"
+          "Value": "1617"
         },
         {
           "Name": "Content-Type",
@@ -2700,22 +2737,22 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8=\""
+          "Value": "\"YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:42:16 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8="
+          "Value": "sha256-YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ="
         }
       ]
     },
     {
-      "Route": "css/site.min.504tlq0gfj.css",
+      "Route": "css/site.min.kjqov8ufz2.css",
       "AssetFile": "css/site.min.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2729,7 +2766,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1412"
+          "Value": "1617"
         },
         {
           "Name": "Content-Type",
@@ -2737,21 +2774,21 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA=\""
+          "Value": "\"YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:42:16 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "504tlq0gfj"
+          "Value": "kjqov8ufz2"
         },
         {
           "Name": "integrity",
-          "Value": "sha256-hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA="
+          "Value": "sha256-YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ="
         },
         {
           "Name": "label",
@@ -2760,8 +2797,8 @@
       ]
     },
     {
-      "Route": "css/site.min.css",
-      "AssetFile": "css/site.min.css",
+      "Route": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.eqhicanzrr.png",
+      "AssetFile": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
       "Selectors": [],
       "ResponseHeaders": [
         {
@@ -2770,29 +2807,74 @@
         },
         {
           "Name": "Cache-Control",
-          "Value": "no-cache"
+          "Value": "max-age=31536000, immutable"
         },
         {
           "Name": "Content-Length",
-          "Value": "1412"
+          "Value": "23093"
         },
         {
           "Name": "Content-Type",
-          "Value": "text/css"
+          "Value": "image/png"
         },
         {
           "Name": "ETag",
-          "Value": "\"hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA=\""
+          "Value": "\"urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Mon, 20 Jan 2025 13:07:42 GMT"
+        }
+      ],
+      "EndpointProperties": [
+        {
+          "Name": "fingerprint",
+          "Value": "eqhicanzrr"
+        },
+        {
+          "Name": "integrity",
+          "Value": "sha256-urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo="
+        },
+        {
+          "Name": "label",
+          "Value": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png"
+        }
+      ]
+    },
+    {
+      "Route": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
+      "AssetFile": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
+      "Selectors": [],
+      "ResponseHeaders": [
+        {
+          "Name": "Accept-Ranges",
+          "Value": "bytes"
+        },
+        {
+          "Name": "Cache-Control",
+          "Value": "max-age=3600, must-revalidate"
+        },
+        {
+          "Name": "Content-Length",
+          "Value": "23093"
+        },
+        {
+          "Name": "Content-Type",
+          "Value": "image/png"
+        },
+        {
+          "Name": "ETag",
+          "Value": "\"urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=\""
+        },
+        {
+          "Name": "Last-Modified",
+          "Value": "Mon, 20 Jan 2025 13:07:42 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA="
+          "Value": "sha256-urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo="
         }
       ]
     },
@@ -2961,7 +3043,7 @@
       ]
     },
     {
-      "Route": "js/site.4a80i8ynpk.js",
+      "Route": "js/site.js",
       "AssetFile": "js/site.js",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2971,11 +3053,11 @@
         },
         {
           "Name": "Cache-Control",
-          "Value": "max-age=31536000, immutable"
+          "Value": "no-cache"
         },
         {
           "Name": "Content-Length",
-          "Value": "3419"
+          "Value": "5375"
         },
         {
           "Name": "Content-Type",
@@ -2983,30 +3065,22 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=\""
+          "Value": "\"wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Thu, 16 Jan 2025 09:40:22 GMT"
+          "Value": "Sun, 19 Jan 2025 12:22:11 GMT"
         }
       ],
       "EndpointProperties": [
-        {
-          "Name": "fingerprint",
-          "Value": "4a80i8ynpk"
-        },
         {
           "Name": "integrity",
-          "Value": "sha256-FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc="
-        },
-        {
-          "Name": "label",
-          "Value": "js/site.js"
+          "Value": "sha256-wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU="
         }
       ]
     },
     {
-      "Route": "js/site.js",
+      "Route": "js/site.r1ctmgv487.js",
       "AssetFile": "js/site.js",
       "Selectors": [],
       "ResponseHeaders": [
@@ -3016,11 +3090,11 @@
         },
         {
           "Name": "Cache-Control",
-          "Value": "no-cache"
+          "Value": "max-age=31536000, immutable"
         },
         {
           "Name": "Content-Length",
-          "Value": "3419"
+          "Value": "5375"
         },
         {
           "Name": "Content-Type",
@@ -3028,17 +3102,25 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=\""
+          "Value": "\"wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Thu, 16 Jan 2025 09:40:22 GMT"
+          "Value": "Sun, 19 Jan 2025 12:22:11 GMT"
         }
       ],
       "EndpointProperties": [
+        {
+          "Name": "fingerprint",
+          "Value": "r1ctmgv487"
+        },
         {
           "Name": "integrity",
-          "Value": "sha256-FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc="
+          "Value": "sha256-wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU="
+        },
+        {
+          "Name": "label",
+          "Value": "js/site.js"
         }
       ]
     },

ファイルの差分が大きいため隠しています
+ 0 - 0
backend/bin/Debug/net8.0/bitforum.staticwebassets.runtime.json


+ 27 - 0
backend/bitforum.csproj

@@ -35,5 +35,32 @@
 	<ItemGroup>
 	  <Folder Include="Models\Board\Comment\" />
 	  <Folder Include="Models\Board\Post\" />
+	  <Folder Include="wwwroot\editor\popup\" />
+	  <Folder Include="wwwroot\editor\faq\" />
+	  <Folder Include="wwwroot\editor\document\" />
+	  <Folder Include="wwwroot\upload\banner\" />
+	</ItemGroup>
+
+	<ItemGroup>
+	  <Content Update="Views\Page\Banner\Position\Index.cshtml">
+	    <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+	    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+	  </Content>
+	  <Content Update="Views\Page\Banner\Item\Edit.cshtml">
+	    <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+	    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+	  </Content>
+	  <Content Update="Views\Page\Banner\Item\Index.cshtml">
+	    <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+	    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+	  </Content>
+	  <Content Update="Views\Page\Banner\Item\Write.cshtml">
+	    <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+	    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+	  </Content>
+	  <Content Update="Views\Page\Banner\_Navbar.cshtml">
+	    <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+	    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+	  </Content>
 	</ItemGroup>
 </Project>

BIN
backend/obj/Debug/net8.0/apphost.exe


+ 5 - 4
backend/obj/Debug/net8.0/bitforum.AssemblyInfo.cs

@@ -1,9 +1,10 @@
 //------------------------------------------------------------------------------
 // <auto-generated>
-//     This code was generated by a tool.
+//     이 코드는 도구를 사용하여 생성되었습니다.
+//     런타임 버전:4.0.30319.42000
 //
-//     Changes to this file may cause incorrect behavior and will be lost if
-//     the code is regenerated.
+//     파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
+//     이러한 변경 내용이 손실됩니다.
 // </auto-generated>
 //------------------------------------------------------------------------------
 
@@ -14,7 +15,7 @@ using System.Reflection;
 [assembly: System.Reflection.AssemblyCompanyAttribute("bitforum")]
 [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
 [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
-[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+9bb28e4b1894457fa7b529c5db64fe8bd27103f2")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+3cea9e312c3937845f9ab1910ca8e39bcadbc7ac")]
 [assembly: System.Reflection.AssemblyProductAttribute("bitforum")]
 [assembly: System.Reflection.AssemblyTitleAttribute("bitforum")]
 [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

+ 1 - 1
backend/obj/Debug/net8.0/bitforum.AssemblyInfoInputs.cache

@@ -1 +1 @@
-96e111cad15f8680d7cd8721d345d39de8d7fb605f744f0995b09fbe0794a016
+1c2586375e8b8ba845e40954ba4cde413881a797fce7b0065b18aa37cdb0cb59

+ 54 - 2
backend/obj/Debug/net8.0/bitforum.GeneratedMSBuildEditorConfig.editorconfig

@@ -216,6 +216,26 @@ build_metadata.AdditionalFiles.CssScope =
 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcSG9tZVxQcml2YWN5LmNzaHRtbA==
 build_metadata.AdditionalFiles.CssScope = 
 
+[E:/workspace/bitforum/backend/Views/Page/Banner/Item/Edit.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxCYW5uZXJcSXRlbVxFZGl0LmNzaHRtbA==
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Banner/Item/Index.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxCYW5uZXJcSXRlbVxJbmRleC5jc2h0bWw=
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Banner/Item/Write.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxCYW5uZXJcSXRlbVxXcml0ZS5jc2h0bWw=
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Banner/Position/Index.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxCYW5uZXJcUG9zaXRpb25cSW5kZXguY3NodG1s
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Banner/_Navbar.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxCYW5uZXJcX05hdmJhci5jc2h0bWw=
+build_metadata.AdditionalFiles.CssScope = 
+
 [E:/workspace/bitforum/backend/Views/Page/Document/Edit.cshtml]
 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxEb2N1bWVudFxFZGl0LmNzaHRtbA==
 build_metadata.AdditionalFiles.CssScope = 
@@ -228,8 +248,36 @@ build_metadata.AdditionalFiles.CssScope =
 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxEb2N1bWVudFxXcml0ZS5jc2h0bWw=
 build_metadata.AdditionalFiles.CssScope = 
 
-[E:/workspace/bitforum/backend/Views/Page/Faq/Index.cshtml]
-build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxGYXFcSW5kZXguY3NodG1s
+[E:/workspace/bitforum/backend/Views/Page/Faq/Category/Index.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxGYXFcQ2F0ZWdvcnlcSW5kZXguY3NodG1s
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Faq/Item/Edit.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxGYXFcSXRlbVxFZGl0LmNzaHRtbA==
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Faq/Item/Index.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxGYXFcSXRlbVxJbmRleC5jc2h0bWw=
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Faq/Item/Write.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxGYXFcSXRlbVxXcml0ZS5jc2h0bWw=
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Faq/_Navbar.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxGYXFcX05hdmJhci5jc2h0bWw=
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Popup/Edit.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxQb3B1cFxFZGl0LmNzaHRtbA==
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Popup/Index.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxQb3B1cFxJbmRleC5jc2h0bWw=
+build_metadata.AdditionalFiles.CssScope = 
+
+[E:/workspace/bitforum/backend/Views/Page/Popup/Write.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcUGFnZVxQb3B1cFxXcml0ZS5jc2h0bWw=
 build_metadata.AdditionalFiles.CssScope = 
 
 [E:/workspace/bitforum/backend/Views/Setting/Basic.cshtml]
@@ -276,6 +324,10 @@ build_metadata.AdditionalFiles.CssScope =
 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9NZW51SXRlbS5jc2h0bWw=
 build_metadata.AdditionalFiles.CssScope = 
 
+[E:/workspace/bitforum/backend/Views/Shared/_Pagination.cshtml]
+build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9QYWdpbmF0aW9uLmNzaHRtbA==
+build_metadata.AdditionalFiles.CssScope = 
+
 [E:/workspace/bitforum/backend/Views/Shared/_StatusMessage.cshtml]
 build_metadata.AdditionalFiles.TargetPath = Vmlld3NcU2hhcmVkXF9TdGF0dXNNZXNzYWdlLmNzaHRtbA==
 build_metadata.AdditionalFiles.CssScope = 

+ 1 - 1
backend/obj/Debug/net8.0/bitforum.csproj.CoreCompileInputs.cache

@@ -1 +1 @@
-2a60147307099777cd9f328ca5b2f06604d6db929516b44f01addedfb6000f2e
+020718fb23fd63cdc0277b5cb0884a557956d61d5e3249e5cb4a695c2c8c2851

BIN
backend/obj/Debug/net8.0/bitforum.dll


BIN
backend/obj/Debug/net8.0/bitforum.pdb


BIN
backend/obj/Debug/net8.0/ref/bitforum.dll


BIN
backend/obj/Debug/net8.0/refint/bitforum.dll


+ 148 - 66
backend/obj/Debug/net8.0/staticwebassets.build.endpoints.json

@@ -2483,7 +2483,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "153"
+          "Value": "265"
         },
         {
           "Name": "Content-Type",
@@ -2491,22 +2491,22 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=\""
+          "Value": "\"ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4="
+          "Value": "sha256-ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8="
         }
       ]
     },
     {
-      "Route": "css/admin.illdtnevdp.css",
+      "Route": "css/admin.dovpgmbu0s.css",
       "AssetFile": "css/admin.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2520,7 +2520,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "153"
+          "Value": "265"
         },
         {
           "Name": "Content-Type",
@@ -2528,21 +2528,21 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=\""
+          "Value": "\"ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "illdtnevdp"
+          "Value": "dovpgmbu0s"
         },
         {
           "Name": "integrity",
-          "Value": "sha256-lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4="
+          "Value": "sha256-ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8="
         },
         {
           "Name": "label",
@@ -2565,7 +2565,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "154"
+          "Value": "222"
         },
         {
           "Name": "Content-Type",
@@ -2573,22 +2573,22 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=\""
+          "Value": "\"NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs="
+          "Value": "sha256-NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o="
         }
       ]
     },
     {
-      "Route": "css/admin.min.qxtxag1zp9.css",
+      "Route": "css/admin.min.u21bc84aoq.css",
       "AssetFile": "css/admin.min.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2602,7 +2602,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "154"
+          "Value": "222"
         },
         {
           "Name": "Content-Type",
@@ -2610,21 +2610,21 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=\""
+          "Value": "\"NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "qxtxag1zp9"
+          "Value": "u21bc84aoq"
         },
         {
           "Name": "integrity",
-          "Value": "sha256-zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs="
+          "Value": "sha256-NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o="
         },
         {
           "Name": "label",
@@ -2633,7 +2633,44 @@
       ]
     },
     {
-      "Route": "css/site.bh06sh7otq.css",
+      "Route": "css/site.css",
+      "AssetFile": "css/site.css",
+      "Selectors": [],
+      "ResponseHeaders": [
+        {
+          "Name": "Accept-Ranges",
+          "Value": "bytes"
+        },
+        {
+          "Name": "Cache-Control",
+          "Value": "no-cache"
+        },
+        {
+          "Name": "Content-Length",
+          "Value": "1615"
+        },
+        {
+          "Name": "Content-Type",
+          "Value": "text/css"
+        },
+        {
+          "Name": "ETag",
+          "Value": "\"D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=\""
+        },
+        {
+          "Name": "Last-Modified",
+          "Value": "Sat, 18 Jan 2025 11:44:19 GMT"
+        }
+      ],
+      "EndpointProperties": [
+        {
+          "Name": "integrity",
+          "Value": "sha256-D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs="
+        }
+      ]
+    },
+    {
+      "Route": "css/site.l7qd4nl7uy.css",
       "AssetFile": "css/site.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2647,7 +2684,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1413"
+          "Value": "1615"
         },
         {
           "Name": "Content-Type",
@@ -2655,21 +2692,21 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8=\""
+          "Value": "\"D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:44:19 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "bh06sh7otq"
+          "Value": "l7qd4nl7uy"
         },
         {
           "Name": "integrity",
-          "Value": "sha256-EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8="
+          "Value": "sha256-D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs="
         },
         {
           "Name": "label",
@@ -2678,8 +2715,8 @@
       ]
     },
     {
-      "Route": "css/site.css",
-      "AssetFile": "css/site.css",
+      "Route": "css/site.min.css",
+      "AssetFile": "css/site.min.css",
       "Selectors": [],
       "ResponseHeaders": [
         {
@@ -2692,7 +2729,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1413"
+          "Value": "1617"
         },
         {
           "Name": "Content-Type",
@@ -2700,22 +2737,22 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8=\""
+          "Value": "\"YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:42:16 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8="
+          "Value": "sha256-YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ="
         }
       ]
     },
     {
-      "Route": "css/site.min.504tlq0gfj.css",
+      "Route": "css/site.min.kjqov8ufz2.css",
       "AssetFile": "css/site.min.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2729,7 +2766,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1412"
+          "Value": "1617"
         },
         {
           "Name": "Content-Type",
@@ -2737,21 +2774,21 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA=\""
+          "Value": "\"YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:42:16 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "504tlq0gfj"
+          "Value": "kjqov8ufz2"
         },
         {
           "Name": "integrity",
-          "Value": "sha256-hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA="
+          "Value": "sha256-YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ="
         },
         {
           "Name": "label",
@@ -2760,8 +2797,8 @@
       ]
     },
     {
-      "Route": "css/site.min.css",
-      "AssetFile": "css/site.min.css",
+      "Route": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.eqhicanzrr.png",
+      "AssetFile": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
       "Selectors": [],
       "ResponseHeaders": [
         {
@@ -2770,29 +2807,74 @@
         },
         {
           "Name": "Cache-Control",
-          "Value": "no-cache"
+          "Value": "max-age=31536000, immutable"
         },
         {
           "Name": "Content-Length",
-          "Value": "1412"
+          "Value": "23093"
         },
         {
           "Name": "Content-Type",
-          "Value": "text/css"
+          "Value": "image/png"
         },
         {
           "Name": "ETag",
-          "Value": "\"hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA=\""
+          "Value": "\"urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Mon, 20 Jan 2025 13:07:42 GMT"
+        }
+      ],
+      "EndpointProperties": [
+        {
+          "Name": "fingerprint",
+          "Value": "eqhicanzrr"
+        },
+        {
+          "Name": "integrity",
+          "Value": "sha256-urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo="
+        },
+        {
+          "Name": "label",
+          "Value": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png"
+        }
+      ]
+    },
+    {
+      "Route": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
+      "AssetFile": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
+      "Selectors": [],
+      "ResponseHeaders": [
+        {
+          "Name": "Accept-Ranges",
+          "Value": "bytes"
+        },
+        {
+          "Name": "Cache-Control",
+          "Value": "max-age=3600, must-revalidate"
+        },
+        {
+          "Name": "Content-Length",
+          "Value": "23093"
+        },
+        {
+          "Name": "Content-Type",
+          "Value": "image/png"
+        },
+        {
+          "Name": "ETag",
+          "Value": "\"urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=\""
+        },
+        {
+          "Name": "Last-Modified",
+          "Value": "Mon, 20 Jan 2025 13:07:42 GMT"
         }
       ],
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA="
+          "Value": "sha256-urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo="
         }
       ]
     },
@@ -2961,7 +3043,7 @@
       ]
     },
     {
-      "Route": "js/site.4a80i8ynpk.js",
+      "Route": "js/site.js",
       "AssetFile": "js/site.js",
       "Selectors": [],
       "ResponseHeaders": [
@@ -2971,11 +3053,11 @@
         },
         {
           "Name": "Cache-Control",
-          "Value": "max-age=31536000, immutable"
+          "Value": "no-cache"
         },
         {
           "Name": "Content-Length",
-          "Value": "3419"
+          "Value": "5375"
         },
         {
           "Name": "Content-Type",
@@ -2983,30 +3065,22 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=\""
+          "Value": "\"wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Thu, 16 Jan 2025 09:40:22 GMT"
+          "Value": "Sun, 19 Jan 2025 12:22:11 GMT"
         }
       ],
       "EndpointProperties": [
-        {
-          "Name": "fingerprint",
-          "Value": "4a80i8ynpk"
-        },
         {
           "Name": "integrity",
-          "Value": "sha256-FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc="
-        },
-        {
-          "Name": "label",
-          "Value": "js/site.js"
+          "Value": "sha256-wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU="
         }
       ]
     },
     {
-      "Route": "js/site.js",
+      "Route": "js/site.r1ctmgv487.js",
       "AssetFile": "js/site.js",
       "Selectors": [],
       "ResponseHeaders": [
@@ -3016,11 +3090,11 @@
         },
         {
           "Name": "Cache-Control",
-          "Value": "no-cache"
+          "Value": "max-age=31536000, immutable"
         },
         {
           "Name": "Content-Length",
-          "Value": "3419"
+          "Value": "5375"
         },
         {
           "Name": "Content-Type",
@@ -3028,17 +3102,25 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=\""
+          "Value": "\"wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Thu, 16 Jan 2025 09:40:22 GMT"
+          "Value": "Sun, 19 Jan 2025 12:22:11 GMT"
         }
       ],
       "EndpointProperties": [
+        {
+          "Name": "fingerprint",
+          "Value": "r1ctmgv487"
+        },
         {
           "Name": "integrity",
-          "Value": "sha256-FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc="
+          "Value": "sha256-wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU="
+        },
+        {
+          "Name": "label",
+          "Value": "js/site.js"
         }
       ]
     },

+ 180 - 77
backend/obj/Debug/net8.0/staticwebassets.build.json

@@ -1,6 +1,6 @@
 {
   "Version": 1,
-  "Hash": "qq4eCrjIwpcfviTxqcKFsxxKXwJ/HCcTgKZ9/rXw3bU=",
+  "Hash": "9CtFNE5iI8RSj9oETGIlPAdhqOhahS7VMWSiH4FeFVU=",
   "Source": "bitforum",
   "BasePath": "_content/bitforum",
   "Mode": "Default",
@@ -1375,8 +1375,8 @@
       "RelatedAsset": "",
       "AssetTraitName": "",
       "AssetTraitValue": "",
-      "Fingerprint": "illdtnevdp",
-      "Integrity": "lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=",
+      "Fingerprint": "dovpgmbu0s",
+      "Integrity": "ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=",
       "CopyToOutputDirectory": "Never",
       "CopyToPublishDirectory": "PreserveNewest",
       "OriginalItemSpec": "wwwroot\\css\\admin.css"
@@ -1396,8 +1396,8 @@
       "RelatedAsset": "",
       "AssetTraitName": "",
       "AssetTraitValue": "",
-      "Fingerprint": "qxtxag1zp9",
-      "Integrity": "zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=",
+      "Fingerprint": "u21bc84aoq",
+      "Integrity": "NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o=",
       "CopyToOutputDirectory": "Never",
       "CopyToPublishDirectory": "PreserveNewest",
       "OriginalItemSpec": "wwwroot\\css\\admin.min.css"
@@ -1417,8 +1417,8 @@
       "RelatedAsset": "",
       "AssetTraitName": "",
       "AssetTraitValue": "",
-      "Fingerprint": "bh06sh7otq",
-      "Integrity": "EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8=",
+      "Fingerprint": "l7qd4nl7uy",
+      "Integrity": "D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=",
       "CopyToOutputDirectory": "Never",
       "CopyToPublishDirectory": "PreserveNewest",
       "OriginalItemSpec": "wwwroot\\css\\site.css"
@@ -1438,12 +1438,33 @@
       "RelatedAsset": "",
       "AssetTraitName": "",
       "AssetTraitValue": "",
-      "Fingerprint": "504tlq0gfj",
-      "Integrity": "hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA=",
+      "Fingerprint": "kjqov8ufz2",
+      "Integrity": "YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=",
       "CopyToOutputDirectory": "Never",
       "CopyToPublishDirectory": "PreserveNewest",
       "OriginalItemSpec": "wwwroot\\css\\site.min.css"
     },
+    {
+      "Identity": "E:\\workspace\\bitforum\\backend\\wwwroot\\editor\\banner\\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
+      "SourceId": "bitforum",
+      "SourceType": "Discovered",
+      "ContentRoot": "E:\\workspace\\bitforum\\backend\\wwwroot\\",
+      "BasePath": "_content/bitforum",
+      "RelativePath": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a#[.{fingerprint}]?.png",
+      "AssetKind": "All",
+      "AssetMode": "All",
+      "AssetRole": "Primary",
+      "AssetMergeBehavior": "",
+      "AssetMergeSource": "",
+      "RelatedAsset": "",
+      "AssetTraitName": "",
+      "AssetTraitValue": "",
+      "Fingerprint": "eqhicanzrr",
+      "Integrity": "urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=",
+      "CopyToOutputDirectory": "Never",
+      "CopyToPublishDirectory": "PreserveNewest",
+      "OriginalItemSpec": "wwwroot\\editor\\banner\\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png"
+    },
     {
       "Identity": "E:\\workspace\\bitforum\\backend\\wwwroot\\images\\favicon.ico",
       "SourceId": "bitforum",
@@ -1501,8 +1522,8 @@
       "RelatedAsset": "",
       "AssetTraitName": "",
       "AssetTraitValue": "",
-      "Fingerprint": "4a80i8ynpk",
-      "Integrity": "FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=",
+      "Fingerprint": "r1ctmgv487",
+      "Integrity": "wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU=",
       "CopyToOutputDirectory": "Never",
       "CopyToPublishDirectory": "PreserveNewest",
       "OriginalItemSpec": "wwwroot\\js\\site.js"
@@ -7812,7 +7833,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "153"
+          "Value": "265"
         },
         {
           "Name": "Content-Type",
@@ -7820,11 +7841,11 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=\""
+          "Value": "\"ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         },
         {
           "Name": "Cache-Control",
@@ -7834,12 +7855,12 @@
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4="
+          "Value": "sha256-ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8="
         }
       ]
     },
     {
-      "Route": "css/admin.illdtnevdp.css",
+      "Route": "css/admin.dovpgmbu0s.css",
       "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\admin.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -7849,7 +7870,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "153"
+          "Value": "265"
         },
         {
           "Name": "Content-Type",
@@ -7857,11 +7878,11 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=\""
+          "Value": "\"ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         },
         {
           "Name": "Cache-Control",
@@ -7871,7 +7892,7 @@
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "illdtnevdp"
+          "Value": "dovpgmbu0s"
         },
         {
           "Name": "label",
@@ -7879,7 +7900,7 @@
         },
         {
           "Name": "integrity",
-          "Value": "sha256-lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4="
+          "Value": "sha256-ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8="
         }
       ]
     },
@@ -7894,7 +7915,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "154"
+          "Value": "222"
         },
         {
           "Name": "Content-Type",
@@ -7902,11 +7923,11 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=\""
+          "Value": "\"NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         },
         {
           "Name": "Cache-Control",
@@ -7916,12 +7937,12 @@
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs="
+          "Value": "sha256-NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o="
         }
       ]
     },
     {
-      "Route": "css/admin.min.qxtxag1zp9.css",
+      "Route": "css/admin.min.u21bc84aoq.css",
       "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\admin.min.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -7931,7 +7952,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "154"
+          "Value": "222"
         },
         {
           "Name": "Content-Type",
@@ -7939,11 +7960,11 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=\""
+          "Value": "\"NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sun, 19 Jan 2025 12:12:29 GMT"
         },
         {
           "Name": "Cache-Control",
@@ -7953,7 +7974,7 @@
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "qxtxag1zp9"
+          "Value": "u21bc84aoq"
         },
         {
           "Name": "label",
@@ -7961,12 +7982,12 @@
         },
         {
           "Name": "integrity",
-          "Value": "sha256-zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs="
+          "Value": "sha256-NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o="
         }
       ]
     },
     {
-      "Route": "css/site.bh06sh7otq.css",
+      "Route": "css/site.css",
       "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\site.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -7976,7 +7997,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1413"
+          "Value": "1615"
         },
         {
           "Name": "Content-Type",
@@ -7984,11 +8005,48 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8=\""
+          "Value": "\"D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:44:19 GMT"
+        },
+        {
+          "Name": "Cache-Control",
+          "Value": "no-cache"
+        }
+      ],
+      "EndpointProperties": [
+        {
+          "Name": "integrity",
+          "Value": "sha256-D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs="
+        }
+      ]
+    },
+    {
+      "Route": "css/site.l7qd4nl7uy.css",
+      "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\site.css",
+      "Selectors": [],
+      "ResponseHeaders": [
+        {
+          "Name": "Accept-Ranges",
+          "Value": "bytes"
+        },
+        {
+          "Name": "Content-Length",
+          "Value": "1615"
+        },
+        {
+          "Name": "Content-Type",
+          "Value": "text/css"
+        },
+        {
+          "Name": "ETag",
+          "Value": "\"D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=\""
+        },
+        {
+          "Name": "Last-Modified",
+          "Value": "Sat, 18 Jan 2025 11:44:19 GMT"
         },
         {
           "Name": "Cache-Control",
@@ -7998,7 +8056,7 @@
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "bh06sh7otq"
+          "Value": "l7qd4nl7uy"
         },
         {
           "Name": "label",
@@ -8006,13 +8064,13 @@
         },
         {
           "Name": "integrity",
-          "Value": "sha256-EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8="
+          "Value": "sha256-D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs="
         }
       ]
     },
     {
-      "Route": "css/site.css",
-      "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\site.css",
+      "Route": "css/site.min.css",
+      "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\site.min.css",
       "Selectors": [],
       "ResponseHeaders": [
         {
@@ -8021,7 +8079,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1413"
+          "Value": "1617"
         },
         {
           "Name": "Content-Type",
@@ -8029,11 +8087,11 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8=\""
+          "Value": "\"YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:42:16 GMT"
         },
         {
           "Name": "Cache-Control",
@@ -8043,12 +8101,12 @@
       "EndpointProperties": [
         {
           "Name": "integrity",
-          "Value": "sha256-EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8="
+          "Value": "sha256-YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ="
         }
       ]
     },
     {
-      "Route": "css/site.min.504tlq0gfj.css",
+      "Route": "css/site.min.kjqov8ufz2.css",
       "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\site.min.css",
       "Selectors": [],
       "ResponseHeaders": [
@@ -8058,7 +8116,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1412"
+          "Value": "1617"
         },
         {
           "Name": "Content-Type",
@@ -8066,11 +8124,11 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA=\""
+          "Value": "\"YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Sat, 18 Jan 2025 11:42:16 GMT"
         },
         {
           "Name": "Cache-Control",
@@ -8080,7 +8138,7 @@
       "EndpointProperties": [
         {
           "Name": "fingerprint",
-          "Value": "504tlq0gfj"
+          "Value": "kjqov8ufz2"
         },
         {
           "Name": "label",
@@ -8088,13 +8146,13 @@
         },
         {
           "Name": "integrity",
-          "Value": "sha256-hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA="
+          "Value": "sha256-YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ="
         }
       ]
     },
     {
-      "Route": "css/site.min.css",
-      "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\site.min.css",
+      "Route": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.eqhicanzrr.png",
+      "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\editor\\banner\\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
       "Selectors": [],
       "ResponseHeaders": [
         {
@@ -8103,29 +8161,74 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "1412"
+          "Value": "23093"
         },
         {
           "Name": "Content-Type",
-          "Value": "text/css"
+          "Value": "image/png"
         },
         {
           "Name": "ETag",
-          "Value": "\"hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA=\""
+          "Value": "\"urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Wed, 15 Jan 2025 13:34:04 GMT"
+          "Value": "Mon, 20 Jan 2025 13:07:42 GMT"
         },
         {
           "Name": "Cache-Control",
-          "Value": "no-cache"
+          "Value": "max-age=31536000, immutable"
         }
       ],
       "EndpointProperties": [
+        {
+          "Name": "fingerprint",
+          "Value": "eqhicanzrr"
+        },
+        {
+          "Name": "label",
+          "Value": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png"
+        },
         {
           "Name": "integrity",
-          "Value": "sha256-hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA="
+          "Value": "sha256-urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo="
+        }
+      ]
+    },
+    {
+      "Route": "editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
+      "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\editor\\banner\\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
+      "Selectors": [],
+      "ResponseHeaders": [
+        {
+          "Name": "Accept-Ranges",
+          "Value": "bytes"
+        },
+        {
+          "Name": "Content-Length",
+          "Value": "23093"
+        },
+        {
+          "Name": "Content-Type",
+          "Value": "image/png"
+        },
+        {
+          "Name": "ETag",
+          "Value": "\"urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=\""
+        },
+        {
+          "Name": "Last-Modified",
+          "Value": "Mon, 20 Jan 2025 13:07:42 GMT"
+        },
+        {
+          "Name": "Cache-Control",
+          "Value": "max-age=3600, must-revalidate"
+        }
+      ],
+      "EndpointProperties": [
+        {
+          "Name": "integrity",
+          "Value": "sha256-urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo="
         }
       ]
     },
@@ -10514,7 +10617,7 @@
       ]
     },
     {
-      "Route": "js/site.4a80i8ynpk.js",
+      "Route": "js/site.js",
       "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\js\\site.js",
       "Selectors": [],
       "ResponseHeaders": [
@@ -10524,7 +10627,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "3419"
+          "Value": "5375"
         },
         {
           "Name": "Content-Type",
@@ -10532,34 +10635,26 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=\""
+          "Value": "\"wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Thu, 16 Jan 2025 09:40:22 GMT"
+          "Value": "Sun, 19 Jan 2025 12:22:11 GMT"
         },
         {
           "Name": "Cache-Control",
-          "Value": "max-age=31536000, immutable"
+          "Value": "no-cache"
         }
       ],
       "EndpointProperties": [
-        {
-          "Name": "fingerprint",
-          "Value": "4a80i8ynpk"
-        },
-        {
-          "Name": "label",
-          "Value": "js/site.js"
-        },
         {
           "Name": "integrity",
-          "Value": "sha256-FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc="
+          "Value": "sha256-wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU="
         }
       ]
     },
     {
-      "Route": "js/site.js",
+      "Route": "js/site.r1ctmgv487.js",
       "AssetFile": "E:\\workspace\\bitforum\\backend\\wwwroot\\js\\site.js",
       "Selectors": [],
       "ResponseHeaders": [
@@ -10569,7 +10664,7 @@
         },
         {
           "Name": "Content-Length",
-          "Value": "3419"
+          "Value": "5375"
         },
         {
           "Name": "Content-Type",
@@ -10577,21 +10672,29 @@
         },
         {
           "Name": "ETag",
-          "Value": "\"FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=\""
+          "Value": "\"wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU=\""
         },
         {
           "Name": "Last-Modified",
-          "Value": "Thu, 16 Jan 2025 09:40:22 GMT"
+          "Value": "Sun, 19 Jan 2025 12:22:11 GMT"
         },
         {
           "Name": "Cache-Control",
-          "Value": "no-cache"
+          "Value": "max-age=31536000, immutable"
         }
       ],
       "EndpointProperties": [
+        {
+          "Name": "fingerprint",
+          "Value": "r1ctmgv487"
+        },
+        {
+          "Name": "label",
+          "Value": "js/site.js"
+        },
         {
           "Name": "integrity",
-          "Value": "sha256-FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc="
+          "Value": "sha256-wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU="
         }
       ]
     },

ファイルの差分が大きいため隠しています
+ 0 - 0
backend/obj/Debug/net8.0/staticwebassets.development.json


+ 4 - 0
backend/obj/Debug/net8.0/staticwebassets.pack.json

@@ -28,6 +28,10 @@
       "Id": "E:\\workspace\\bitforum\\backend\\wwwroot\\css\\site.min.css",
       "PackagePath": "staticwebassets\\css\\site.min.css"
     },
+    {
+      "Id": "E:\\workspace\\bitforum\\backend\\wwwroot\\editor\\banner\\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png",
+      "PackagePath": "staticwebassets\\editor\\banner\\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png"
+    },
     {
       "Id": "E:\\workspace\\bitforum\\backend\\wwwroot\\images\\favicon.ico",
       "PackagePath": "staticwebassets\\images\\favicon.ico"

+ 1 - 0
backend/obj/Debug/net8.0/staticwebassets.removed.txt

@@ -0,0 +1 @@
+E:\workspace\bitforum\backend\wwwroot\upload\banner\e35f9044-43ce-4dd5-bd76-6d8e2c0b66b6.png

+ 1 - 0
backend/obj/Debug/net8.0/staticwebassets.upToDateCheck.txt

@@ -4,6 +4,7 @@ wwwroot\css\admin.css
 wwwroot\css\admin.min.css
 wwwroot\css\site.css
 wwwroot\css\site.min.css
+wwwroot\editor\banner\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png
 wwwroot\images\favicon.ico
 wwwroot\js\func.js
 wwwroot\js\site.js

+ 40 - 28
backend/obj/Debug/net8.0/staticwebassets/msbuild.bitforum.Microsoft.AspNetCore.StaticWebAssetEndpoints.props

@@ -39,50 +39,62 @@
     <StaticWebAssetEndpoint Include="_content/bitforum/css/admin.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\admin.css'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4="}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"153"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=\u0022"},{"Name":"Last-Modified","Value":"Wed, 15 Jan 2025 13:34:04 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-ncT3W\u002BlyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8="}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"265"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022ncT3W\u002BlyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=\u0022"},{"Name":"Last-Modified","Value":"Sun, 19 Jan 2025 12:12:29 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
-    <StaticWebAssetEndpoint Include="_content/bitforum/css/admin.illdtnevdp.css">
+    <StaticWebAssetEndpoint Include="_content/bitforum/css/admin.dovpgmbu0s.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\admin.css'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"illdtnevdp"},{"Name":"integrity","Value":"sha256-lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4="},{"Name":"label","Value":"_content/bitforum/css/admin.css"}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"153"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=\u0022"},{"Name":"Last-Modified","Value":"Wed, 15 Jan 2025 13:34:04 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"dovpgmbu0s"},{"Name":"integrity","Value":"sha256-ncT3W\u002BlyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8="},{"Name":"label","Value":"_content/bitforum/css/admin.css"}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"265"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022ncT3W\u002BlyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=\u0022"},{"Name":"Last-Modified","Value":"Sun, 19 Jan 2025 12:12:29 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
     <StaticWebAssetEndpoint Include="_content/bitforum/css/admin.min.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\admin.min.css'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs="}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"154"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=\u0022"},{"Name":"Last-Modified","Value":"Wed, 15 Jan 2025 13:34:04 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K\u002Bo="}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"222"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K\u002Bo=\u0022"},{"Name":"Last-Modified","Value":"Sun, 19 Jan 2025 12:12:29 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
-    <StaticWebAssetEndpoint Include="_content/bitforum/css/admin.min.qxtxag1zp9.css">
+    <StaticWebAssetEndpoint Include="_content/bitforum/css/admin.min.u21bc84aoq.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\admin.min.css'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"qxtxag1zp9"},{"Name":"integrity","Value":"sha256-zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs="},{"Name":"label","Value":"_content/bitforum/css/admin.min.css"}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"154"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=\u0022"},{"Name":"Last-Modified","Value":"Wed, 15 Jan 2025 13:34:04 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"u21bc84aoq"},{"Name":"integrity","Value":"sha256-NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K\u002Bo="},{"Name":"label","Value":"_content/bitforum/css/admin.min.css"}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"222"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K\u002Bo=\u0022"},{"Name":"Last-Modified","Value":"Sun, 19 Jan 2025 12:12:29 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
-    <StaticWebAssetEndpoint Include="_content/bitforum/css/site.bh06sh7otq.css">
+    <StaticWebAssetEndpoint Include="_content/bitforum/css/site.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\site.css'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"bh06sh7otq"},{"Name":"integrity","Value":"sha256-EcgUTDCHpb/IHwu4B65lmHVa764F\u002BLnQRCR2qD2l4Y8="},{"Name":"label","Value":"_content/bitforum/css/site.css"}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"1413"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022EcgUTDCHpb/IHwu4B65lmHVa764F\u002BLnQRCR2qD2l4Y8=\u0022"},{"Name":"Last-Modified","Value":"Wed, 15 Jan 2025 13:34:04 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-D\u002Bo020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs="}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"1615"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022D\u002Bo020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=\u0022"},{"Name":"Last-Modified","Value":"Sat, 18 Jan 2025 11:44:19 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
-    <StaticWebAssetEndpoint Include="_content/bitforum/css/site.css">
+    <StaticWebAssetEndpoint Include="_content/bitforum/css/site.l7qd4nl7uy.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\site.css'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-EcgUTDCHpb/IHwu4B65lmHVa764F\u002BLnQRCR2qD2l4Y8="}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"1413"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022EcgUTDCHpb/IHwu4B65lmHVa764F\u002BLnQRCR2qD2l4Y8=\u0022"},{"Name":"Last-Modified","Value":"Wed, 15 Jan 2025 13:34:04 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"l7qd4nl7uy"},{"Name":"integrity","Value":"sha256-D\u002Bo020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs="},{"Name":"label","Value":"_content/bitforum/css/site.css"}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"1615"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022D\u002Bo020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=\u0022"},{"Name":"Last-Modified","Value":"Sat, 18 Jan 2025 11:44:19 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
-    <StaticWebAssetEndpoint Include="_content/bitforum/css/site.min.504tlq0gfj.css">
+    <StaticWebAssetEndpoint Include="_content/bitforum/css/site.min.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\site.min.css'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"504tlq0gfj"},{"Name":"integrity","Value":"sha256-hZbigZL0YKYOCNY30G1NM/Yl3De\u002BNXmBCR67Db\u002BlzwA="},{"Name":"label","Value":"_content/bitforum/css/site.min.css"}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"1412"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022hZbigZL0YKYOCNY30G1NM/Yl3De\u002BNXmBCR67Db\u002BlzwA=\u0022"},{"Name":"Last-Modified","Value":"Wed, 15 Jan 2025 13:34:04 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ="}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"1617"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=\u0022"},{"Name":"Last-Modified","Value":"Sat, 18 Jan 2025 11:42:16 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
-    <StaticWebAssetEndpoint Include="_content/bitforum/css/site.min.css">
+    <StaticWebAssetEndpoint Include="_content/bitforum/css/site.min.kjqov8ufz2.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\site.min.css'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-hZbigZL0YKYOCNY30G1NM/Yl3De\u002BNXmBCR67Db\u002BlzwA="}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"1412"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022hZbigZL0YKYOCNY30G1NM/Yl3De\u002BNXmBCR67Db\u002BlzwA=\u0022"},{"Name":"Last-Modified","Value":"Wed, 15 Jan 2025 13:34:04 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"kjqov8ufz2"},{"Name":"integrity","Value":"sha256-YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ="},{"Name":"label","Value":"_content/bitforum/css/site.min.css"}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"1617"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"\u0022YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=\u0022"},{"Name":"Last-Modified","Value":"Sat, 18 Jan 2025 11:42:16 GMT"}]]]></ResponseHeaders>
+    </StaticWebAssetEndpoint>
+    <StaticWebAssetEndpoint Include="_content/bitforum/editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.eqhicanzrr.png">
+      <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\editor\banner\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png'))</AssetFile>
+      <Selectors><![CDATA[[]]]></Selectors>
+      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"eqhicanzrr"},{"Name":"integrity","Value":"sha256-urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo="},{"Name":"label","Value":"_content/bitforum/editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png"}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"23093"},{"Name":"Content-Type","Value":"image/png"},{"Name":"ETag","Value":"\u0022urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=\u0022"},{"Name":"Last-Modified","Value":"Mon, 20 Jan 2025 13:07:42 GMT"}]]]></ResponseHeaders>
+    </StaticWebAssetEndpoint>
+    <StaticWebAssetEndpoint Include="_content/bitforum/editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png">
+      <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\editor\banner\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png'))</AssetFile>
+      <Selectors><![CDATA[[]]]></Selectors>
+      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo="}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=3600, must-revalidate"},{"Name":"Content-Length","Value":"23093"},{"Name":"Content-Type","Value":"image/png"},{"Name":"ETag","Value":"\u0022urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=\u0022"},{"Name":"Last-Modified","Value":"Mon, 20 Jan 2025 13:07:42 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
     <StaticWebAssetEndpoint Include="_content/bitforum/images/favicon.0dvehdwh9a.ico">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\images\favicon.ico'))</AssetFile>
@@ -108,17 +120,17 @@
       <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-gdzI5MbOC4NqTYkN/Mm2cfVgQ7DVcOQunPUNGgCwkI4="}]]]></EndpointProperties>
       <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"1028"},{"Name":"Content-Type","Value":"text/javascript"},{"Name":"ETag","Value":"\u0022gdzI5MbOC4NqTYkN/Mm2cfVgQ7DVcOQunPUNGgCwkI4=\u0022"},{"Name":"Last-Modified","Value":"Wed, 08 Jan 2025 01:32:59 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
-    <StaticWebAssetEndpoint Include="_content/bitforum/js/site.4a80i8ynpk.js">
+    <StaticWebAssetEndpoint Include="_content/bitforum/js/site.js">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\site.js'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"4a80i8ynpk"},{"Name":"integrity","Value":"sha256-FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc="},{"Name":"label","Value":"_content/bitforum/js/site.js"}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"3419"},{"Name":"Content-Type","Value":"text/javascript"},{"Name":"ETag","Value":"\u0022FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=\u0022"},{"Name":"Last-Modified","Value":"Thu, 16 Jan 2025 09:40:22 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-wRft5TuZsQu5\u002BahgX20hV5kHWU3m0LNeVevSMAbQsWU="}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"5375"},{"Name":"Content-Type","Value":"text/javascript"},{"Name":"ETag","Value":"\u0022wRft5TuZsQu5\u002BahgX20hV5kHWU3m0LNeVevSMAbQsWU=\u0022"},{"Name":"Last-Modified","Value":"Sun, 19 Jan 2025 12:22:11 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
-    <StaticWebAssetEndpoint Include="_content/bitforum/js/site.js">
+    <StaticWebAssetEndpoint Include="_content/bitforum/js/site.r1ctmgv487.js">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\site.js'))</AssetFile>
       <Selectors><![CDATA[[]]]></Selectors>
-      <EndpointProperties><![CDATA[[{"Name":"integrity","Value":"sha256-FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc="}]]]></EndpointProperties>
-      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"no-cache"},{"Name":"Content-Length","Value":"3419"},{"Name":"Content-Type","Value":"text/javascript"},{"Name":"ETag","Value":"\u0022FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=\u0022"},{"Name":"Last-Modified","Value":"Thu, 16 Jan 2025 09:40:22 GMT"}]]]></ResponseHeaders>
+      <EndpointProperties><![CDATA[[{"Name":"fingerprint","Value":"r1ctmgv487"},{"Name":"integrity","Value":"sha256-wRft5TuZsQu5\u002BahgX20hV5kHWU3m0LNeVevSMAbQsWU="},{"Name":"label","Value":"_content/bitforum/js/site.js"}]]]></EndpointProperties>
+      <ResponseHeaders><![CDATA[[{"Name":"Accept-Ranges","Value":"bytes"},{"Name":"Cache-Control","Value":"max-age=31536000, immutable"},{"Name":"Content-Length","Value":"5375"},{"Name":"Content-Type","Value":"text/javascript"},{"Name":"ETag","Value":"\u0022wRft5TuZsQu5\u002BahgX20hV5kHWU3m0LNeVevSMAbQsWU=\u0022"},{"Name":"Last-Modified","Value":"Sun, 19 Jan 2025 12:22:11 GMT"}]]]></ResponseHeaders>
     </StaticWebAssetEndpoint>
     <StaticWebAssetEndpoint Include="_content/bitforum/lib/bootstrap/dist/css/bootstrap-grid.agp80tu62r.css">
       <AssetFile>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\lib\bootstrap\dist\css\bootstrap-grid.css'))</AssetFile>

+ 28 - 10
backend/obj/Debug/net8.0/staticwebassets/msbuild.bitforum.Microsoft.AspNetCore.StaticWebAssets.props

@@ -66,8 +66,8 @@
       <RelatedAsset></RelatedAsset>
       <AssetTraitName></AssetTraitName>
       <AssetTraitValue></AssetTraitValue>
-      <Fingerprint>illdtnevdp</Fingerprint>
-      <Integrity>lkcDNEB5lcFJ4FNyO4UgORDBuqBc9LEytJ2PvSRDEP4=</Integrity>
+      <Fingerprint>dovpgmbu0s</Fingerprint>
+      <Integrity>ncT3W+lyVj8yLBsHLZRbV9R3clnMZo5J0msxWbwgcg8=</Integrity>
       <CopyToOutputDirectory>Never</CopyToOutputDirectory>
       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
       <OriginalItemSpec>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\admin.css'))</OriginalItemSpec>
@@ -84,8 +84,8 @@
       <RelatedAsset></RelatedAsset>
       <AssetTraitName></AssetTraitName>
       <AssetTraitValue></AssetTraitValue>
-      <Fingerprint>qxtxag1zp9</Fingerprint>
-      <Integrity>zm/B82eA9Z4lUjeURB5A5aAzsnKX3qRgusT8bPLamBs=</Integrity>
+      <Fingerprint>u21bc84aoq</Fingerprint>
+      <Integrity>NkbrlUcoVcpI9JjoQKAwK8n84zY4u9CCO7QOdEc5K+o=</Integrity>
       <CopyToOutputDirectory>Never</CopyToOutputDirectory>
       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
       <OriginalItemSpec>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\admin.min.css'))</OriginalItemSpec>
@@ -102,8 +102,8 @@
       <RelatedAsset></RelatedAsset>
       <AssetTraitName></AssetTraitName>
       <AssetTraitValue></AssetTraitValue>
-      <Fingerprint>bh06sh7otq</Fingerprint>
-      <Integrity>EcgUTDCHpb/IHwu4B65lmHVa764F+LnQRCR2qD2l4Y8=</Integrity>
+      <Fingerprint>l7qd4nl7uy</Fingerprint>
+      <Integrity>D+o020fCT3GwrP2eKAMmhzX0rXakhunUStGw/apl2Gs=</Integrity>
       <CopyToOutputDirectory>Never</CopyToOutputDirectory>
       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
       <OriginalItemSpec>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\site.css'))</OriginalItemSpec>
@@ -120,12 +120,30 @@
       <RelatedAsset></RelatedAsset>
       <AssetTraitName></AssetTraitName>
       <AssetTraitValue></AssetTraitValue>
-      <Fingerprint>504tlq0gfj</Fingerprint>
-      <Integrity>hZbigZL0YKYOCNY30G1NM/Yl3De+NXmBCR67Db+lzwA=</Integrity>
+      <Fingerprint>kjqov8ufz2</Fingerprint>
+      <Integrity>YNtlRpBptblxBxVTcEfouxwJZ0GcXFLdllOCw0c5jMQ=</Integrity>
       <CopyToOutputDirectory>Never</CopyToOutputDirectory>
       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
       <OriginalItemSpec>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\site.min.css'))</OriginalItemSpec>
     </StaticWebAsset>
+    <StaticWebAsset Include="$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\editor\banner\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png'))">
+      <SourceType>Package</SourceType>
+      <SourceId>bitforum</SourceId>
+      <ContentRoot>$(MSBuildThisFileDirectory)..\staticwebassets\</ContentRoot>
+      <BasePath>_content/bitforum</BasePath>
+      <RelativePath>editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png</RelativePath>
+      <AssetKind>All</AssetKind>
+      <AssetMode>All</AssetMode>
+      <AssetRole>Primary</AssetRole>
+      <RelatedAsset></RelatedAsset>
+      <AssetTraitName></AssetTraitName>
+      <AssetTraitValue></AssetTraitValue>
+      <Fingerprint>eqhicanzrr</Fingerprint>
+      <Integrity>urFgU8JmREmIe35ajDWOd09HTIvZK7490ObvTc90Gjo=</Integrity>
+      <CopyToOutputDirectory>Never</CopyToOutputDirectory>
+      <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+      <OriginalItemSpec>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\editor\banner\a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png'))</OriginalItemSpec>
+    </StaticWebAsset>
     <StaticWebAsset Include="$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\images\favicon.ico'))">
       <SourceType>Package</SourceType>
       <SourceId>bitforum</SourceId>
@@ -174,8 +192,8 @@
       <RelatedAsset></RelatedAsset>
       <AssetTraitName></AssetTraitName>
       <AssetTraitValue></AssetTraitValue>
-      <Fingerprint>4a80i8ynpk</Fingerprint>
-      <Integrity>FITMTC/ZAZWo7IhHMLw8U/kpuVpkFx3EH2xwdIvHXqc=</Integrity>
+      <Fingerprint>r1ctmgv487</Fingerprint>
+      <Integrity>wRft5TuZsQu5+ahgX20hV5kHWU3m0LNeVevSMAbQsWU=</Integrity>
       <CopyToOutputDirectory>Never</CopyToOutputDirectory>
       <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
       <OriginalItemSpec>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\site.js'))</OriginalItemSpec>

+ 14 - 1
backend/wwwroot/css/admin.css

@@ -1 +1,14 @@
-#server table{width:100%;table-layout:fixed}#server table tr th,#server table tr td{-ms-word-wrap:inherit;word-wrap:inherit;overflow-wrap:break-word}
+table {
+  width: 100%;
+  min-width: 800px;
+}
+table tr th, table tr td {
+  -ms-word-wrap: inherit;
+  word-wrap: inherit;
+  overflow-wrap: break-word;
+  text-align: center;
+  vertical-align: middle;
+}
+table thead tr th, table thead tr td {
+  text-align: center;
+}

+ 1 - 1
backend/wwwroot/css/admin.min.css

@@ -1 +1 @@
-#server table{width:100%;table-layout:fixed;}#server table tr th,#server table tr td{-ms-word-wrap:inherit;word-wrap:inherit;overflow-wrap:break-word;}
+table{width:100%;min-width:800px;}table tr th,table tr td{-ms-word-wrap:inherit;word-wrap:inherit;overflow-wrap:break-word;text-align:center;vertical-align:middle;}table thead tr th,table thead tr td{text-align:center;}

+ 1 - 1
backend/wwwroot/css/site.css

@@ -1 +1 @@
-html,body{height:100vh;margin:0;padding:0}body{display:grid;grid-template-rows:auto 1fr;grid-template-columns:minmax(200px, 0.17fr) 1fr;grid-gap:0}body.hidden-aside{grid-template-columns:0 auto}body.hidden-aside aside{transform:translateX(-100%);opacity:0}body aside{position:relative;height:inherit;background-color:#f9f9f9;border-right:1px solid #ddd;overflow-y:auto;transition:transform 0.3s ease, opacity 0.3s ease;transform:translateX(0);display:flex;-ms-flex-direction:inherit;-webkit-flex-direction:inherit;flex-direction:column}body aside>ul.nav{flex-grow:1}body aside>ul.nav>li.nav-item:first-of-type>a{border-bottom:1px solid #ddd}body aside>ul.nav>li.nav-item:not(:first-of-type){border-bottom:1px solid #ddd}body aside>ul.nav>li.nav-item:not(:first-of-type)>a:not(.collapsed){background-color:#f1f1f1}body aside>ul.nav>li.nav-item>ul{padding:0.625rem 0.625rem 0.625rem 0;border-top:1px solid #ddd}body aside>ul.nav>li.nav-item a{display:block;padding:0.438rem 0.781rem;color:#333;text-decoration:none;cursor:pointer}body aside>ul.nav>li.nav-item a:hover{background-color:#f0f0f0}body aside footer{padding:0.781rem;text-align:center;border-top:1px solid #ddd}body main{background-color:#ffffff;height:inherit;overflow-y:auto}body main>header{border-bottom:1px solid #ddd;padding:0.313rem 0.781rem}body main>header .logo img{position:relative;top:-2px}body main>div.container-fluid{padding:0.781rem}
+html,body{height:100vh;margin:0;padding:0}body{display:grid;grid-template-rows:auto 1fr;grid-template-columns:minmax(200px, 0.17fr) 1fr;grid-gap:0}body.hidden-aside{grid-template-columns:0 auto}body.hidden-aside aside{transform:translateX(-100%);opacity:0}body aside{position:relative;height:inherit;background-color:#f9f9f9;border-right:1px solid #ddd;overflow-y:auto;transition:transform 0.3s ease, opacity 0.3s ease;transform:translateX(0);display:flex;-ms-flex-direction:inherit;-webkit-flex-direction:inherit;flex-direction:column}body aside>ul.nav{flex-grow:1}body aside>ul.nav>li.nav-item:first-of-type>a{border-bottom:1px solid #ddd}body aside>ul.nav>li.nav-item:not(:first-of-type){border-bottom:1px solid #ddd}body aside>ul.nav>li.nav-item:not(:first-of-type)>a:not(.collapsed){background-color:#f1f1f1}body aside>ul.nav>li.nav-item>ul{padding:0.625rem 0.625rem 0.625rem 0;border-top:1px solid #ddd}body aside>ul.nav>li.nav-item a{display:block;padding:0.438rem 0.781rem;color:#333;text-decoration:none;cursor:pointer}body aside>ul.nav>li.nav-item a:hover{background-color:#f0f0f0}body aside>ul.nav>li.nav-item a.active{background-color:#e9e9e9;outline:1px solid #ddd}body aside footer{padding:0.781rem;text-align:center;border-top:1px solid #ddd}body aside footer a{color:#333;text-decoration:none}body aside footer a:hover{color:blue;text-decoration:underline}body main{background-color:#ffffff;height:inherit;overflow-y:auto}body main>header{border-bottom:1px solid #ddd;padding:0.313rem 0.781rem}body main>header .logo img{position:relative;top:-2px}body main>div.container-fluid{padding:0.781rem}

+ 1 - 1
backend/wwwroot/css/site.min.css

@@ -1 +1 @@
-html,body{height:100vh;margin:0;padding:0;}body{display:grid;grid-template-rows:auto 1fr;grid-template-columns:minmax(200px,.17fr) 1fr;grid-gap:0;}body.hidden-aside{grid-template-columns:0 auto;}body.hidden-aside aside{transform:translateX(-100%);opacity:0;}body aside{position:relative;height:inherit;background-color:#f9f9f9;border-right:1px solid #ddd;overflow-y:auto;transition:transform .3s ease,opacity .3s ease;transform:translateX(0);display:flex;-ms-flex-direction:inherit;-webkit-flex-direction:inherit;flex-direction:column;}body aside>ul.nav{flex-grow:1;}body aside>ul.nav>li.nav-item:first-of-type>a{border-bottom:1px solid #ddd;}body aside>ul.nav>li.nav-item:not(:first-of-type){border-bottom:1px solid #ddd;}body aside>ul.nav>li.nav-item:not(:first-of-type)>a:not(.collapsed){background-color:#f1f1f1;}body aside>ul.nav>li.nav-item>ul{padding:.625rem .625rem .625rem 0;border-top:1px solid #ddd;}body aside>ul.nav>li.nav-item a{display:block;padding:.438rem .781rem;color:#333;text-decoration:none;cursor:pointer;}body aside>ul.nav>li.nav-item a:hover{background-color:#f0f0f0;}body aside footer{padding:.781rem;text-align:center;border-top:1px solid #ddd;}body main{background-color:#fff;height:inherit;overflow-y:auto;}body main>header{border-bottom:1px solid #ddd;padding:.313rem .781rem;}body main>header .logo img{position:relative;top:-2px;}body main>div.container-fluid{padding:.781rem;}
+html,body{height:100vh;margin:0;padding:0;}body{display:grid;grid-template-rows:auto 1fr;grid-template-columns:minmax(200px,.17fr) 1fr;grid-gap:0;}body.hidden-aside{grid-template-columns:0 auto;}body.hidden-aside aside{transform:translateX(-100%);opacity:0;}body aside{position:relative;height:inherit;background-color:#f9f9f9;border-right:1px solid #ddd;overflow-y:auto;transition:transform .3s ease,opacity .3s ease;transform:translateX(0);display:flex;-ms-flex-direction:inherit;-webkit-flex-direction:inherit;flex-direction:column;}body aside>ul.nav{flex-grow:1;}body aside>ul.nav>li.nav-item:first-of-type>a{border-bottom:1px solid #ddd;}body aside>ul.nav>li.nav-item:not(:first-of-type){border-bottom:1px solid #ddd;}body aside>ul.nav>li.nav-item:not(:first-of-type)>a:not(.collapsed){background-color:#f1f1f1;}body aside>ul.nav>li.nav-item>ul{padding:.625rem .625rem .625rem 0;border-top:1px solid #ddd;}body aside>ul.nav>li.nav-item a{display:block;padding:.438rem .781rem;color:#333;text-decoration:none;cursor:pointer;}body aside>ul.nav>li.nav-item a:hover{background-color:#f0f0f0;}body aside>ul.nav>li.nav-item a.active{background-color:#e9e9e9;outline:1px solid #ddd;}body aside footer{padding:.781rem;text-align:center;border-top:1px solid #ddd;}body aside footer a{color:#333;text-decoration:none;}body aside footer a:hover{color:#00f;text-decoration:underline;}body main{background-color:#fff;height:inherit;overflow-y:auto;}body main>header{border-bottom:1px solid #ddd;padding:.313rem .781rem;}body main>header .logo img{position:relative;top:-2px;}body main>div.container-fluid{padding:.781rem;}

BIN
backend/wwwroot/editor/banner/a83814d0-245e-42d0-9871-c3aa9c0a2a0a.png


+ 102 - 33
backend/wwwroot/js/site.js

@@ -3,6 +3,18 @@
 
 // Write your JavaScript code.
 
+$(function () {
+    // 사이드 바 토글 유지
+    let isHidden = aside.isHidden();
+
+    if (isHidden) {
+        aside.hideAside();
+    } else {
+        aside.showAside();
+    }
+});
+
+// 좌측 메뉴 처리
 class Aside {
     constructor() {
         this.body = document.body;
@@ -60,17 +72,6 @@ class Aside {
 
 const aside = new Aside();
 
-$(function () {
-    // 사이드 바 토글 유지
-    let isHidden = aside.isHidden();
-
-    if (isHidden) {
-        aside.hideAside();
-    } else {
-        aside.showAside();
-    }
-});
-
 document.getElementById("btnAsideToggle").addEventListener("click", (e) => aside.toggleAside(e));
 
 // 화면이 작아지면
@@ -83,33 +84,101 @@ window.addEventListener("resize", function () {
 });
 
 // 메뉴 토글 유지
-const collapseElementList = document.querySelectorAll('.collapse');
-collapseElementList.forEach(collapseEl => {
-    const collapseID = collapseEl.id;
+document.querySelectorAll(".collapse").forEach(e => {
+    const collapseID = e.id;
     const collapseState = getCookie(collapseID);
-
-    if (collapseState === 'open') {
-        new bootstrap.Collapse(collapseEl, { show: true });
-    } else if (collapseState === 'closed') {
-        new bootstrap.Collapse(collapseEl, { toggle: false });
+    if (collapseState === "open") {
+        new bootstrap.Collapse(e, { show: true });
+    } else if (collapseState === "closed") {
+        new bootstrap.Collapse(e, { toggle: false });
     }
-    collapseEl.addEventListener('show.bs.collapse', () => {
-        setCookie(collapseID, 'open', 7);
+    e.addEventListener("show.bs.collapse", () => {
+        setCookie(collapseID, "open", 7);
     });
-
-    collapseEl.addEventListener('hide.bs.collapse', () => {
-        setCookie(collapseID, 'closed', 7);
+    e.addEventListener("hide.bs.collapse", () => {
+        setCookie(collapseID, "closed", 7);
     });
 });
 
 // 드롭박스 적용
-const dropdownElementList = document.querySelectorAll('.dropdown-toggle');
-const dropdownList = [...dropdownElementList].map(dropdownToggleEl => new bootstrap.Dropdown(dropdownToggleEl));
-
-// 삭제 처리
-$(document).on("click", ".btn-delete-row", function (e) {
-    e.preventDefault();
-    if (confirm("삭제하시겠습니까?")) {
-        location.href = e.target.closest("a").href;
+const dropdownList = [...document.querySelectorAll('.dropdown-toggle')].map(e => new bootstrap.Dropdown(e));
+
+// 목록 버튼상자
+class ActionButtons {
+    constructor() {
+        this.form = document.getElementById("fAdminList");
+    }
+
+    validate() {
+        let checked = $("input:checkbox.list-check-box:checked");
+        if (checked.length < 1) {
+            alert("자료를 하나 이상 선택하세요.");
+            return false;
+        }
+        return true;
+    }
+
+    checkout(e) {
+        const action = (e.target.dataset.action || e.target.closest("a").href);
+        if (!action) {
+            alert("처리 주소를 확인하세요.");
+            return false;
+        }
+
+        this.form.action = action;
+        this.form.submit();
+    }
+
+    Update(e) {
+        if (!this.validate()) {
+            return false;
+        }
+
+        if (confirm("선택한 자료를 정말 수정 하시겠습니까?")) {
+            this.checkout(e);
+        }
+    }
+
+    Delete(e) {
+        if (!this.validate()) {
+            return false;
+        }
+
+        if (confirm("선택한 자료를 정말 삭제 하시겠습니까?")) {
+            this.checkout(e);
+        }
+    }
+
+    Recover(e) {
+        if (!this.validate()) {
+            return false;
+        }
+
+        if (confirm("선택한 자료를 정말 복원 하시겠습니까?")) {
+            this.checkout(e);
+        }
+    }
+
+    checkedAll(e) {
+        let chk = document.getElementsByClassName("list-check-box");
+        for (let i = 0; i < chk.length; i++) {
+            if (e.target.getAttribute("form") == chk[i].getAttribute("form")) {
+                chk[i].checked = e.target.checked;
+            }
+        }
+        if(e.target.checked) {
+            $("[data-action]").prop("disabled", false);
+        } else {
+            $("[data-action]").prop("disabled", true);
+        }
     }
-});
+}
+
+const actionButtons = new ActionButtons();
+$(document).on("click", "#btnListUpdate", (e) => actionButtons.Update(e));
+$(document).on("click", "#btnListDelete", (e) => actionButtons.Delete(e));
+$(document).on("click", "#btnListRecover", (e) => actionButtons.Recover(e));
+$(document).on("click", ".btn-row-delete", () => confirm("정말 삭제 하시겠습니까?"));
+
+// 모든 라디오, 체크박스 선택/해제
+$(document).on("click", "#checkedAll", (e) => actionButtons.checkedAll(e));

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません