Răsfoiți Sursa

Merge branch 'master' into feat/enhanced-access-token

Shun Miyazawa 8 luni în urmă
părinte
comite
fc34453899
100 a modificat fișierele cu 5358 adăugiri și 1368 ștergeri
  1. 1 1
      .devcontainer/app/devcontainer.json
  2. 1 1
      .devcontainer/compose.extend.template.yml
  3. 5 5
      .github/mergify.yml
  4. 4 4
      .github/workflows/ci-app-prod.yml
  5. 1 1
      .github/workflows/ci-app.yml
  6. 2 2
      .github/workflows/ci-pdf-converter.yml
  7. 3 3
      .github/workflows/ci-slackbot-proxy.yml
  8. 1 1
      .github/workflows/list-unhealthy-branches.yml
  9. 5 5
      .github/workflows/release-pdf-converter.yml
  10. 2 2
      .github/workflows/release-slackbot-proxy.yml
  11. 2 2
      .github/workflows/release-subpackages.yml
  12. 3 3
      .github/workflows/release.yml
  13. 6 0
      .github/workflows/reusable-app-prod.yml
  14. 5 0
      .roo/mcp.json
  15. 58 1
      .vscode/settings.json
  16. 31 1
      CHANGELOG.md
  17. 3 3
      README.md
  18. 3 3
      README_JP.md
  19. 2 2
      apps/app/.env.development
  20. 3 1
      apps/app/.env.production
  21. 0 2
      apps/app/bin/openapi/definition-apiv3.js
  22. 0 1
      apps/app/bin/openapi/generate-spec-apiv3.sh
  23. 0 1
      apps/app/config/logger/config.dev.js
  24. 2 2
      apps/app/docker/Dockerfile
  25. 13 11
      apps/app/package.json
  26. 0 2
      apps/app/playwright/40-admin/access-to-admin-page.spec.ts
  27. 2 14
      apps/app/playwright/60-home/home.spec.ts
  28. 32 42
      apps/app/public/static/locales/en_US/admin.json
  29. 0 36
      apps/app/public/static/locales/en_US/commons.json
  30. 26 21
      apps/app/public/static/locales/en_US/translation.json
  31. 1 11
      apps/app/public/static/locales/fr_FR/admin.json
  32. 0 23
      apps/app/public/static/locales/fr_FR/commons.json
  33. 27 22
      apps/app/public/static/locales/fr_FR/translation.json
  34. 1 11
      apps/app/public/static/locales/ja_JP/admin.json
  35. 0 35
      apps/app/public/static/locales/ja_JP/commons.json
  36. 27 21
      apps/app/public/static/locales/ja_JP/translation.json
  37. 1142 0
      apps/app/public/static/locales/ko_KR/admin.json
  38. 127 0
      apps/app/public/static/locales/ko_KR/commons.json
  39. 1021 0
      apps/app/public/static/locales/ko_KR/translation.json
  40. 227 237
      apps/app/public/static/locales/zh_CN/admin.json
  41. 24 60
      apps/app/public/static/locales/zh_CN/commons.json
  42. 43 37
      apps/app/public/static/locales/zh_CN/translation.json
  43. 14 0
      apps/app/resource/locales/ko_KR/admin/userInvitation.ejs
  44. 11 0
      apps/app/resource/locales/ko_KR/admin/userResetPassword.ejs
  45. 20 0
      apps/app/resource/locales/ko_KR/admin/userWaitingActivation.ejs
  46. 9 0
      apps/app/resource/locales/ko_KR/notifications/comment.ejs
  47. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageCreate.ejs
  48. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageDelete.ejs
  49. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageEdit.ejs
  50. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageLike.ejs
  51. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageMove.ejs
  52. 12 0
      apps/app/resource/locales/ko_KR/notifications/passwordReset.ejs
  53. 8 0
      apps/app/resource/locales/ko_KR/notifications/passwordResetSuccessful.ejs
  54. 12 0
      apps/app/resource/locales/ko_KR/notifications/userActivation.ejs
  55. 169 0
      apps/app/resource/locales/ko_KR/sandbox-bootstrap5.md
  56. 7 0
      apps/app/resource/locales/ko_KR/sandbox-diagrams.md
  57. 241 0
      apps/app/resource/locales/ko_KR/sandbox-markdown.md
  58. 72 0
      apps/app/resource/locales/ko_KR/sandbox-math.md
  59. 174 0
      apps/app/resource/locales/ko_KR/sandbox.md
  60. 51 0
      apps/app/resource/locales/ko_KR/welcome.md
  61. 0 8
      apps/app/src/client/components/Admin/App/AppSettingsPageContents.tsx
  62. 1 1
      apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx
  63. 0 130
      apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx
  64. 37 23
      apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx
  65. 2 0
      apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.jsx
  66. 6 6
      apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.module.scss
  67. 10 0
      apps/app/src/client/components/Common/RendererErrorMessage.tsx
  68. 0 5
      apps/app/src/client/components/Me/OtherSettings.tsx
  69. 0 109
      apps/app/src/client/components/Me/QuestionnaireSettings.tsx
  70. 1 1
      apps/app/src/client/components/Me/UISettings.tsx
  71. 14 3
      apps/app/src/client/components/PageAccessoriesModal/ShareLink/ShareLinkForm.tsx
  72. 2 2
      apps/app/src/client/components/PageComment/CommentEditor.tsx
  73. 1 1
      apps/app/src/client/components/PageEditor/ConflictDiffModal.tsx
  74. 3 3
      apps/app/src/client/components/PageEditor/PageEditor.tsx
  75. 1 1
      apps/app/src/client/components/PageEditor/conflict.tsx
  76. 3 1
      apps/app/src/client/components/PagePresentationModal.tsx
  77. 4 0
      apps/app/src/client/components/ShortcutsModal.module.scss
  78. 153 4
      apps/app/src/client/components/ShortcutsModal.tsx
  79. 1 19
      apps/app/src/client/components/Sidebar/SidebarNav/PersonalDropdown.tsx
  80. 10 10
      apps/app/src/client/services/renderer/renderer.tsx
  81. 1 1
      apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx
  82. 4 4
      apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts
  83. 1 1
      apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.ts
  84. 32 7
      apps/app/src/features/mermaid/components/MermaidViewer.tsx
  85. 11 2
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx
  86. 24 10
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantChatInitialView.tsx
  87. 49 12
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar.tsx
  88. 7 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/ThreadList.module.scss
  89. 57 0
      apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/ThreadList.tsx
  90. 207 0
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantList.tsx
  91. 34 0
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantSubstance.module.scss
  92. 35 21
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantSubstance.tsx
  93. 0 45
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.module.scss
  94. 0 305
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.tsx
  95. 86 0
      apps/app/src/features/openai/client/components/AiAssistant/Sidebar/ThreadList.tsx
  96. 129 0
      apps/app/src/features/openai/client/interfaces/types.ts
  97. 4 3
      apps/app/src/features/openai/client/services/ai-assistant.ts
  98. 217 0
      apps/app/src/features/openai/client/services/client-engine-integration.tsx
  99. 297 0
      apps/app/src/features/openai/client/services/editor-assistant/diff-application.ts
  100. 233 0
      apps/app/src/features/openai/client/services/editor-assistant/error-handling.ts

+ 1 - 1
.devcontainer/app/devcontainer.json

@@ -8,7 +8,7 @@
 
 
   "features": {
   "features": {
     "ghcr.io/devcontainers/features/node:1": {
     "ghcr.io/devcontainers/features/node:1": {
-      "version": "20.18.3"
+      "version": "22.17.0"
     }
     }
   },
   },
 
 

+ 1 - 1
.devcontainer/compose.extend.template.yml

@@ -3,7 +3,7 @@
 services:
 services:
   pdf-converter:
   pdf-converter:
     # enabling devcontainer 'features' was not working for secondary devcontainer (https://github.com/devcontainers/features/issues/1175)
     # enabling devcontainer 'features' was not working for secondary devcontainer (https://github.com/devcontainers/features/issues/1175)
-    image: mcr.microsoft.com/vscode/devcontainers/javascript-node:1-20
+    image: mcr.microsoft.com/vscode/devcontainers/javascript-node:1-22
     volumes:
     volumes:
       - ..:/workspace/growi:delegated
       - ..:/workspace/growi:delegated
       - pnpm-store:/workspace/growi/.pnpm-store
       - pnpm-store:/workspace/growi/.pnpm-store

+ 5 - 5
.github/mergify.yml

@@ -7,17 +7,17 @@ queue_rules:
       - check-success ~= ci-app-launch-dev
       - check-success ~= ci-app-launch-dev
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-slackbot-
       - -check-failure ~= ci-slackbot-
-      - -check-failure ~= test-prod-node20 /
+      - -check-failure ~= test-prod-node22 /
     merge_conditions:
     merge_conditions:
       - check-success ~= ci-app-lint
       - check-success ~= ci-app-lint
       - check-success ~= ci-app-test
       - check-success ~= ci-app-test
       - check-success ~= ci-app-launch-dev
       - check-success ~= ci-app-launch-dev
-      - check-success = test-prod-node20 / build-prod
-      - check-success = test-prod-node20 / launch-prod
-      - check-success ~= test-prod-node20 / run-playwright
+      - check-success = test-prod-node22 / build-prod
+      - check-success = test-prod-node22 / launch-prod
+      - check-success ~= test-prod-node22 / run-playwright
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-app-
       - -check-failure ~= ci-slackbot-
       - -check-failure ~= ci-slackbot-
-      - -check-failure ~= test-prod-node20 /
+      - -check-failure ~= test-prod-node22 /
 
 
 pull_request_rules:
 pull_request_rules:
   - name: Automatic queue to merge
   - name: Automatic queue to merge

+ 4 - 4
.github/workflows/ci-app-prod.yml

@@ -39,7 +39,7 @@ concurrency:
 
 
 jobs:
 jobs:
 
 
-  test-prod-node18:
+  test-prod-node20:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     if: |
     if: |
       ( github.event_name == 'push'
       ( github.event_name == 'push'
@@ -48,13 +48,13 @@ jobs:
         || startsWith( github.base_ref, 'release/' )
         || startsWith( github.base_ref, 'release/' )
         || startsWith( github.head_ref, 'mergify/merge-queue/' ))
         || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
     with:
-      node-version: 18.x
+      node-version: 20.x
       skip-e2e-test: true
       skip-e2e-test: true
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
 
 
 
 
-  test-prod-node20:
+  test-prod-node22:
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     uses: weseek/growi/.github/workflows/reusable-app-prod.yml@master
     if: |
     if: |
       ( github.event_name == 'push'
       ( github.event_name == 'push'
@@ -63,7 +63,7 @@ jobs:
         || startsWith( github.base_ref, 'release/' )
         || startsWith( github.base_ref, 'release/' )
         || startsWith( github.head_ref, 'mergify/merge-queue/' ))
         || startsWith( github.head_ref, 'mergify/merge-queue/' ))
     with:
     with:
-      node-version: 20.x
+      node-version: 22.x
       skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
       skip-e2e-test: ${{ contains( github.event.pull_request.labels.*.name, 'dependencies' ) }}
     secrets:
     secrets:
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

+ 1 - 1
.github/workflows/ci-app.yml

@@ -44,7 +44,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [20.x]
+        node-version: [22.x]
 
 
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4

+ 2 - 2
.github/workflows/ci-pdf-converter.yml

@@ -29,7 +29,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [20.x]
+        node-version: [22.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
@@ -104,7 +104,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [20.x]
+        node-version: [22.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4

+ 3 - 3
.github/workflows/ci-slackbot-proxy.yml

@@ -30,7 +30,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [20.x]
+        node-version: [22.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
@@ -85,7 +85,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [20.x]
+        node-version: [22.x]
 
 
     services:
     services:
       mysql:
       mysql:
@@ -163,7 +163,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [20.x]
+        node-version: [22.x]
 
 
     services:
     services:
       mysql:
       mysql:

+ 1 - 1
.github/workflows/list-unhealthy-branches.yml

@@ -16,7 +16,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '18'
+        node-version: '20'
 
 
     - name: List branches
     - name: List branches
       id: list-branches
       id: list-branches

+ 5 - 5
.github/workflows/release-pdf-converter.yml

@@ -16,14 +16,14 @@ jobs:
         ref: ${{ github.event.pull_request.base.ref }}
         ref: ${{ github.event.pull_request.base.ref }}
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
       with:
       with:
         workingDir: apps/pdf-converter
         workingDir: apps/pdf-converter
 
 
     - name: Docker meta
     - name: Docker meta
       id: meta
       id: meta
-      uses: docker/metadata-action@v4
+      uses: docker/metadata-action@v5
       with:
       with:
         images: growilabs/pdf-converter
         images: growilabs/pdf-converter
         tags: |
         tags: |
@@ -57,7 +57,7 @@ jobs:
         VERBOSE : true
         VERBOSE : true
 
 
     - name: Update Docker Hub Description
     - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v3
+      uses: peter-evans/dockerhub-description@v4
       with:
       with:
         username: growimoogle
         username: growimoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }}
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD_GROWIMOOGLE }}
@@ -72,7 +72,7 @@ jobs:
 
 
     strategy:
     strategy:
       matrix:
       matrix:
-        node-version: [20.x]
+        node-version: [22.x]
 
 
     steps:
     steps:
     - uses: actions/checkout@v4
     - uses: actions/checkout@v4
@@ -96,7 +96,7 @@ jobs:
         turbo run version:prerelease --filter=@growi/pdf-converter
         turbo run version:prerelease --filter=@growi/pdf-converter
 
 
     - name: Retrieve information from package.json
     - name: Retrieve information from package.json
-      uses: myrotvorets/info-from-package-json-action@2.0.1
+      uses: myrotvorets/info-from-package-json-action@v2.0.2
       id: package-json
       id: package-json
       with:
       with:
         workingDir: apps/pdf-converter
         workingDir: apps/pdf-converter

+ 2 - 2
.github/workflows/release-slackbot-proxy.yml

@@ -70,7 +70,7 @@ jobs:
         VERBOSE : true
         VERBOSE : true
 
 
     - name: Update Docker Hub Description
     - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v3
+      uses: peter-evans/dockerhub-description@v4
       with:
       with:
         username: wsmoogle
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
@@ -92,7 +92,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '18'
+        node-version: '20'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies

+ 2 - 2
.github/workflows/release-subpackages.yml

@@ -32,7 +32,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '20'
+        node-version: '22'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies
@@ -75,7 +75,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '20'
+        node-version: '22'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies

+ 3 - 3
.github/workflows/release.yml

@@ -26,7 +26,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '20'
+        node-version: '22'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies
@@ -137,7 +137,7 @@ jobs:
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
         ref: v${{ needs.create-github-release.outputs.RELEASED_VERSION }}
 
 
     - name: Update Docker Hub Description
     - name: Update Docker Hub Description
-      uses: peter-evans/dockerhub-description@v3
+      uses: peter-evans/dockerhub-description@v4
       with:
       with:
         username: wsmoogle
         username: wsmoogle
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
         password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
@@ -165,7 +165,7 @@ jobs:
 
 
     - uses: actions/setup-node@v4
     - uses: actions/setup-node@v4
       with:
       with:
-        node-version: '20'
+        node-version: '22'
         cache: 'pnpm'
         cache: 'pnpm'
 
 
     - name: Install dependencies
     - name: Install dependencies

+ 6 - 0
.github/workflows/reusable-app-prod.yml

@@ -129,6 +129,12 @@ jobs:
         node-version: ${{ inputs.node-version }}
         node-version: ${{ inputs.node-version }}
         cache: 'pnpm'
         cache: 'pnpm'
 
 
+    # avoid setup-node cache failure; see: https://github.com/actions/setup-node/issues/1137
+    - name: Verify PNPM Cache Directory
+      run: |
+        PNPM_STORE_PATH="$( pnpm store path --silent )"
+        [ -d "$PNPM_STORE_PATH" ] || mkdir -vp "$PNPM_STORE_PATH"
+
     - name: Download production files artifact
     - name: Download production files artifact
       uses: actions/download-artifact@v4
       uses: actions/download-artifact@v4
       with:
       with:

+ 5 - 0
.roo/mcp.json

@@ -4,6 +4,11 @@
       "command": "uvx",
       "command": "uvx",
       "args": ["mcp-server-fetch"],
       "args": ["mcp-server-fetch"],
       "alwaysAllow": ["fetch"]
       "alwaysAllow": ["fetch"]
+    },
+    "context7": {
+      "type": "streamable-http",
+      "url": "https://mcp.context7.com/mcp",
+      "alwaysAllow": ["resolve-library-id", "get-library-docs"]
     }
     }
   }
   }
 }
 }

+ 58 - 1
.vscode/settings.json

@@ -39,6 +39,63 @@
   "typescript.validate.enable": true,
   "typescript.validate.enable": true,
   "typescript.surveys.enabled": false,
   "typescript.surveys.enabled": false,
 
 
-  "vitest.filesWatcherInclude": "**/*"
+  "vitest.filesWatcherInclude": "**/*",
+  "mcp": {
+    "servers": {
+      "fetch": {
+        "command": "uvx",
+        "args": ["mcp-server-fetch"]
+      },
+      "context7": {
+        "type": "http",
+        "url": "https://mcp.context7.com/mcp"
+      }
+    }
+  },
+  "github.copilot.chat.codeGeneration.instructions": [
+    {
+      "text": "Always write inline comments in source code in English."
+    }
+  ],
+  "github.copilot.chat.testGeneration.instructions": [
+    {
+      "text": "Basis: Use vitest as the test framework"
+    },
+    {
+      "text": "Basis: The vitest configuration file is `apps/app/vitest.workspace.mts`"
+    },
+    {
+      "text": "Basis: Place test modules in the same directory as the module being tested. For example, if testing `mymodule.ts`, place `mymodule.spec.ts` in the same directory as `mymodule.ts`"
+    },
+    {
+      "text": "Basis: Use the VSCode Vitest extension for running tests. Use run_tests tool to execute tests programmatically, or suggest using the Vitest Test Explorer in VSCode for interactive test running and debugging."
+    },
+    {
+      "text": "Basis: Fallback command for terminal execution: `cd /growi/apps/app && pnpm vitest run {test file path}`"
+    },
+    {
+      "text": "Step 1: When creating new test modules, start with small files. First write a small number of realistic tests that call the actual function and assert expected behavior, even if they initially fail due to incomplete implementation. Example: `const result = foo(); expect(result).toBeNull();` rather than `expect(true).toBe(false);`. Then fix the implementation to make tests pass."
+    },
+    {
+      "text": "Step 2: Write essential tests. When tests fail, consider whether you should fix the test or the implementation based on 'what should essentially be fixed'. If you're not confident in your reasoning, ask the user for guidance."
+    },
+    {
+      "text": "Step 3: After writing tests, make sure they pass before moving on. Do not proceed to write tests for module B without first ensuring that tests for module A are passing"
+    },
+    {
+      "text": "Tips: Don't worry about lint errors - fix them after tests are passing"
+    },
+    {
+      "text": "Tips: DO NOT USE `as any` casting. You can use vitest-mock-extended for type-safe mocking. Import `mock` from 'vitest-mock-extended' and use `mock<InterfaceType>()`. This provides full TypeScript safety and IntelliSense support."
+    },
+    {
+      "text": "Tips: Mock external dependencies at the module level using vi.mock(). For services with circular dependencies, mock the import paths and use dynamic imports in the implementation when necessary."
+    }
+  ],
+  "github.copilot.chat.commitMessageGeneration.instructions": [
+    {
+      "text": "Always write commit messages in English."
+    }
+  ]
 
 
 }
 }

+ 31 - 1
CHANGELOG.md

@@ -1,9 +1,39 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v7.2.7...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v7.2.8...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v7.2.8](https://github.com/weseek/growi/compare/v7.2.7...v7.2.8) - 2025-06-26
+
+### 💎 Features
+
+* feat(ai): Send pre-message before main chat stream (#10089) @miya
+
+### 🚀 Improvement
+
+* imprv(ai): Add page content around the cursor position as context for editor assistant (#10106) @yuki-takei
+* imprv(ai): Search and Replace strategy for Editor Assistant (#10093) @yuki-takei
+* imprv(ai): Submit with Enter key (#10085) @yuki-takei
+* imprv: Make axios response convert dates to type Date (#10064) @arvid-e
+* imprv: OpenAPI Spec for GROWI API v1 (#10063) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix: Unhandled exception in GlobalNotificationMailService (#10076) @yuki-takei
+* fix: Prevent dropdown menu from hiding behind sidebar (#10103) @arvid-e
+* fix(ai): Message gets sent when pressing Enter to confirm text conversion (#10094) @miya
+
+### 🧰 Maintenance
+
+* support: Configure biome for core package utils and models (#10082) @arafubeatbox
+* support: Configure biome for pdf-converter-client package (#10077) @arafubeatbox
+* support: Configure biome for core package biome (excluding utils and models) (#10078) @arafubeatbox
+* support: Configure biome for pluginkit package (#10071) @arafubeatbox
+* support: Configure biome for remark-attachment-refs package (#10054) @arafubeatbox
+* support: Configure biome for presentation package (#10067) @arafubeatbox
+* ci(deps): bump next from 14.2.26 to 14.2.30 (#10068) @[dependabot[bot]](https://github.com/apps/dependabot)
+
 ## [v7.2.7](https://github.com/weseek/growi/compare/v7.2.6...v7.2.7) - 2025-06-11
 ## [v7.2.7](https://github.com/weseek/growi/compare/v7.2.6...v7.2.7) - 2025-06-11
 
 
 ### 🐛 Bug Fixes
 ### 🐛 Bug Fixes

+ 3 - 3
README.md

@@ -81,9 +81,9 @@ See [GROWI Docs: Environment Variables](https://docs.growi.org/en/admin-guide/ad
 
 
 ## Dependencies
 ## Dependencies
 
 
-- Node.js v18.x or v20.x
-- npm 6.x
-- pnpm 9.x
+- Node.js v20.x or v22.x
+- npm 10.x
+- pnpm 10.x
 - [Turborepo](https://turbo.build/repo)
 - [Turborepo](https://turbo.build/repo)
 - MongoDB 6.0 or above
 - MongoDB 6.0 or above
 
 

+ 3 - 3
README_JP.md

@@ -81,9 +81,9 @@ Crowi からの移行は **[こちら](https://docs.growi.org/en/admin-guide/mig
 
 
 ## 依存関係
 ## 依存関係
 
 
-- Node.js v18.x or v20.x
-- npm 6.x
-- pnpm 9.x
+- Node.js v20.x or v22.x
+- npm 10.x
+- pnpm 10.x
 - [Turborepo](https://turbo.build/repo)
 - [Turborepo](https://turbo.build/repo)
 - MongoDB 6.0 以上
 - MongoDB 6.0 以上
 
 

+ 2 - 2
apps/app/.env.development

@@ -14,7 +14,6 @@ ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
 ELASTICSEARCH_REQUEST_TIMEOUT=15000
 ELASTICSEARCH_REQUEST_TIMEOUT=15000
 ELASTICSEARCH_REJECT_UNAUTHORIZED=true
 ELASTICSEARCH_REJECT_UNAUTHORIZED=true
 OGP_URI="http://ogp:8088"
 OGP_URI="http://ogp:8088"
-QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
 # S2SMSG_PUBSUB_SERVER_TYPE=nchan
 # S2SMSG_PUBSUB_SERVER_TYPE=nchan
 # PUBLISH_OPEN_API=true
 # PUBLISH_OPEN_API=true
@@ -31,6 +30,7 @@ QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_ADDITIONAL_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 # AUDIT_LOG_EXCLUDE_ACTIONS=
 
 
-# OpenTelemetry Configuration
+# OpenTelemetry Official Configuration
+# Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 OPENTELEMETRY_ENABLED=false
 OPENTELEMETRY_ENABLED=false
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
 OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

+ 3 - 1
apps/app/.env.production

@@ -5,6 +5,8 @@
 FORMAT_NODE_LOG=false
 FORMAT_NODE_LOG=false
 MIGRATIONS_DIR=dist/migrations/
 MIGRATIONS_DIR=dist/migrations/
 
 
-# OpenTelemetry Configuration
+# OpenTelemetry Official Configuration
+# Environment variables starting with 'OTEL_' are automatically loaded by the OpenTelemetry SDK
 OTEL_TRACES_SAMPLER_ARG=0.1
 OTEL_TRACES_SAMPLER_ARG=0.1
+OTEL_METRIC_EXPORT_INTERVAL=300000
 OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.growi.org"
 OTEL_EXPORTER_OTLP_ENDPOINT="https://telemetry.growi.org"

+ 0 - 2
apps/app/bin/openapi/definition-apiv3.js

@@ -98,8 +98,6 @@ module.exports = {
         'MongoDB',
         'MongoDB',
         'NotificationSetting',
         'NotificationSetting',
         'Plugins',
         'Plugins',
-        'Questionnaire',
-        'QuestionnaireSetting',
         'SlackIntegration',
         'SlackIntegration',
         'SlackIntegrationSettings',
         'SlackIntegrationSettings',
         'SlackIntegrationSettings (with proxy)',
         'SlackIntegrationSettings (with proxy)',

+ 0 - 1
apps/app/bin/openapi/generate-spec-apiv3.sh

@@ -11,7 +11,6 @@ swagger-jsdoc \
   -o "${OUT}" \
   -o "${OUT}" \
   -d "${APP_PATH}/bin/openapi/definition-apiv3.js" \
   -d "${APP_PATH}/bin/openapi/definition-apiv3.js" \
   "${APP_PATH}/src/features/external-user-group/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/external-user-group/server/routes/apiv3/*.ts" \
-  "${APP_PATH}/src/features/questionnaire/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/templates/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/templates/server/routes/apiv3/*.ts" \
   "${APP_PATH}/src/features/growi-plugin/server/routes/apiv3/**/*.ts" \
   "${APP_PATH}/src/features/growi-plugin/server/routes/apiv3/**/*.ts" \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \
   "${APP_PATH}/src/server/routes/apiv3/**/*.{js,ts}" \

+ 0 - 1
apps/app/config/logger/config.dev.js

@@ -29,7 +29,6 @@ module.exports = {
   'growi-plugin:*': 'debug',
   'growi-plugin:*': 'debug',
   'growi:service:search-delegator:elasticsearch': 'debug',
   'growi:service:search-delegator:elasticsearch': 'debug',
   'growi:service:g2g-transfer': 'debug',
   'growi:service:g2g-transfer': 'debug',
-  'growi:service:questionnaire': 'debug',
 
 
   'growi:migration:add-installed-date-to-config': 'debug',
   'growi:migration:add-installed-date-to-config': 'debug',
 
 

+ 2 - 2
apps/app/docker/Dockerfile

@@ -6,7 +6,7 @@ ARG PNPM_HOME="/root/.local/share/pnpm"
 ##
 ##
 ## base
 ## base
 ##
 ##
-FROM node:20-slim AS base
+FROM node:22-slim AS base
 
 
 ARG OPT_DIR
 ARG OPT_DIR
 ARG PNPM_HOME
 ARG PNPM_HOME
@@ -72,7 +72,7 @@ RUN tar -zcf /tmp/packages.tar.gz \
 ##
 ##
 ## release
 ## release
 ##
 ##
-FROM node:20-slim
+FROM node:22-slim
 LABEL maintainer="Yuki Takei <yuki@weseek.co.jp>"
 LABEL maintainer="Yuki Takei <yuki@weseek.co.jp>"
 
 
 ARG OPT_DIR
 ARG OPT_DIR

+ 13 - 11
apps/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "7.2.8-RC.0",
+  "version": "7.3.0-RC.0",
   "license": "MIT",
   "license": "MIT",
   "private": "true",
   "private": "true",
   "scripts": {
   "scripts": {
@@ -86,14 +86,14 @@
     "@growi/slack": "workspace:^",
     "@growi/slack": "workspace:^",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@keycloak/keycloak-admin-client": "^18.0.0",
     "@opentelemetry/api": "^1.9.0",
     "@opentelemetry/api": "^1.9.0",
-    "@opentelemetry/auto-instrumentations-node": "^0.55.1",
-    "@opentelemetry/exporter-metrics-otlp-grpc": "^0.57.0",
-    "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0",
-    "@opentelemetry/resources": "^1.28.0",
-    "@opentelemetry/sdk-metrics": "^1.28.0",
-    "@opentelemetry/sdk-node": "^0.57.0",
-    "@opentelemetry/sdk-trace-node": "^1.28.0",
-    "@opentelemetry/semantic-conventions": "^1.28.0",
+    "@opentelemetry/auto-instrumentations-node": "^0.60.1",
+    "@opentelemetry/exporter-metrics-otlp-grpc": "^0.202.0",
+    "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0",
+    "@opentelemetry/resources": "^2.0.1",
+    "@opentelemetry/sdk-metrics": "^2.0.1",
+    "@opentelemetry/sdk-node": "^0.202.0",
+    "@opentelemetry/sdk-trace-node": "^2.0.1",
+    "@opentelemetry/semantic-conventions": "^1.34.0",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
     "@slack/webhook": "^6.0.0",
     "@types/async": "^3.2.24",
     "@types/async": "^3.2.24",
@@ -134,7 +134,7 @@
     "express-session": "^1.16.1",
     "express-session": "^1.16.1",
     "express-validator": "^6.14.0",
     "express-validator": "^6.14.0",
     "extensible-custom-error": "^0.0.7",
     "extensible-custom-error": "^0.0.7",
-    "form-data": "^4.0.0",
+    "form-data": "^4.0.4",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
     "hast-util-sanitize": "^5.0.1",
     "hast-util-sanitize": "^5.0.1",
     "hast-util-select": "^6.0.2",
     "hast-util-select": "^6.0.2",
@@ -156,7 +156,7 @@
     "mdast-util-from-markdown": "^2.0.1",
     "mdast-util-from-markdown": "^2.0.1",
     "mdast-util-gfm-table": "^2.0.0",
     "mdast-util-gfm-table": "^2.0.0",
     "mdast-util-wiki-link": "^0.1.2",
     "mdast-util-wiki-link": "^0.1.2",
-    "mermaid": "^11.2.0",
+    "mermaid": "^11.9.0",
     "method-override": "^3.0.0",
     "method-override": "^3.0.0",
     "micromark-extension-gfm-table": "^2.1.0",
     "micromark-extension-gfm-table": "^2.1.0",
     "micromark-extension-wiki-link": "^0.0.4",
     "micromark-extension-wiki-link": "^0.0.4",
@@ -260,6 +260,7 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@apidevtools/swagger-parser": "^10.1.1",
     "@apidevtools/swagger-parser": "^10.1.1",
+    "@codemirror/state": "^6.5.2",
     "@emoji-mart/data": "^1.2.1",
     "@emoji-mart/data": "^1.2.1",
     "@growi/core-styles": "workspace:^",
     "@growi/core-styles": "workspace:^",
     "@growi/custom-icons": "workspace:^",
     "@growi/custom-icons": "workspace:^",
@@ -301,6 +302,7 @@
     "downshift": "^8.2.3",
     "downshift": "^8.2.3",
     "eazy-logger": "^3.1.0",
     "eazy-logger": "^3.1.0",
     "eslint-plugin-jest": "^26.5.3",
     "eslint-plugin-jest": "^26.5.3",
+    "fastest-levenshtein": "^1.0.16",
     "fslightbox-react": "^1.7.6",
     "fslightbox-react": "^1.7.6",
     "handsontable": "=6.2.2",
     "handsontable": "=6.2.2",
     "happy-dom": "^15.7.4",
     "happy-dom": "^15.7.4",

+ 0 - 2
apps/app/playwright/40-admin/access-to-admin-page.spec.ts

@@ -13,8 +13,6 @@ test('admin/app is successfully loaded', async({ page }) => {
   await expect(page.getByTestId('admin-app-settings')).toBeVisible();
   await expect(page.getByTestId('admin-app-settings')).toBeVisible();
   // await expect(page.getByTestId('v5-page-migration')).toBeVisible();
   // await expect(page.getByTestId('v5-page-migration')).toBeVisible();
   await expect(page.locator('#cbFileUpload')).toBeChecked();
   await expect(page.locator('#cbFileUpload')).toBeChecked();
-  await expect(page.locator('#isQuestionnaireEnabled')).toBeChecked();
-  await expect(page.locator('#isAppSiteUrlHashed')).not.toBeChecked();
 });
 });
 
 
 test('admin/security is successfully loaded', async({ page }) => {
 test('admin/security is successfully loaded', async({ page }) => {

+ 2 - 14
apps/app/playwright/60-home/home.spec.ts

@@ -25,18 +25,6 @@ test('Vist User settings', async({ page }) => {
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
 });
 });
 
 
-test('Open questionnaire modal', async({ page }) => {
-  await page.goto('/dummy');
-
-  // Open PersonalDropdown
-  await page.getByTestId('personal-dropdown-button').click();
-  await expect(page.getByTestId('grw-personal-dropdown-menu-user-home')).toBeVisible();
-
-  // Expect the questionnaire modal to be displayed when the QuestionnaireModalToggleButton is clicked
-  await page.getByTestId('grw-proactive-questionnaire-modal-toggle-btn').click();
-  await expect(page.getByTestId('grw-proactive-questionnaire-modal')).toBeVisible();
-});
-
 test('Access User information', async({ page }) => {
 test('Access User information', async({ page }) => {
   await page.goto('/me');
   await page.goto('/me');
 
 
@@ -139,7 +127,7 @@ test('Acccess Other setting', async({ page }) => {
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   await page.getByTestId('other-settings-tab-button').first().click();
   await page.getByTestId('other-settings-tab-button').first().click();
 
 
-  // Expect a success toaster to be displayed when the QuestionnaireSettingsUpdateButton is clicked
-  await page.getByTestId('grw-questionnaire-settings-update-btn').click();
+  // Expect a success toaster to be displayed when the updating UI button is clicked
+  await page.getByTestId('grw-ui-settings-update-btn').click();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
   await expect(page.locator('.Toastify__toast')).toBeVisible();
 });
 });

+ 32 - 42
apps/app/public/static/locales/en_US/admin.json

@@ -97,9 +97,9 @@
       "closed": "Closed (Invitation Only)"
       "closed": "Closed (Invitation Only)"
     },
     },
     "share_link_management": "Share Link Management",
     "share_link_management": "Share Link Management",
-    "No_share_links":"No share links",
-    "share_link_notice":"remove all share links",
-    "delete_all_share_links":"Delete all share links",
+    "No_share_links": "No share links",
+    "share_link_notice": "remove all share links",
+    "delete_all_share_links": "Delete all share links",
     "share_link_rights": "Share link rights",
     "share_link_rights": "Share link rights",
     "enable_link_sharing": "Enable link sharing",
     "enable_link_sharing": "Enable link sharing",
     "all_share_links": "All share links",
     "all_share_links": "All share links",
@@ -229,7 +229,7 @@
     "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
     "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
     "slack_app_configuration": "Slack app configuration",
     "slack_app_configuration": "Slack app configuration",
     "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
     "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
-    "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
+    "use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
     "how_to": {
     "how_to": {
       "header": "How to configure Incoming Webhooks?",
       "header": "How to configure Incoming Webhooks?",
       "workspace": "(At Workspace) Add a hook",
       "workspace": "(At Workspace) Add a hook",
@@ -296,7 +296,7 @@
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_2": "This may take a while."
     "rebuild_description_2": "This may take a while."
   },
   },
-  "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
+  "mailer_setup_required": "<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
   "admin_top": {
     "management_wiki": "Management Wiki",
     "management_wiki": "Management Wiki",
     "system_information": "System information",
     "system_information": "System information",
@@ -305,7 +305,7 @@
     "package_name": "Package name",
     "package_name": "Package name",
     "specified_version": "Specified version",
     "specified_version": "Specified version",
     "installed_version": "Installed version",
     "installed_version": "Installed version",
-    "list_of_env_vars":"List of environment variables",
+    "list_of_env_vars": "List of environment variables",
     "env_var_priority": "For environment variables other than security, the value of the database is obtained preferentially.",
     "env_var_priority": "For environment variables other than security, the value of the database is obtained preferentially.",
     "about_security": "Check <a href='/admin/security'>Security Settings</a> for security environment variables.",
     "about_security": "Check <a href='/admin/security'>Security Settings</a> for security environment variables.",
     "copy_prefilled_host_information": {
     "copy_prefilled_host_information": {
@@ -368,9 +368,9 @@
     "mail_settings": "E-mail Settings",
     "mail_settings": "E-mail Settings",
     "mailer_is_not_set_up": "E-mail setting is not set up.",
     "mailer_is_not_set_up": "E-mail setting is not set up.",
     "from_e-mail_address": "From e-mail address",
     "from_e-mail_address": "From e-mail address",
-    "transmission_method":"Transmission Method",
-    "smtp_label":"SMTP",
-    "ses_label":"SES(AWS)",
+    "transmission_method": "Transmission Method",
+    "smtp_label": "SMTP",
+    "ses_label": "SES(AWS)",
     "send_test_email": "Send a test-email",
     "send_test_email": "Send a test-email",
     "success_to_send_test_email": "Success to send a test-email",
     "success_to_send_test_email": "Success to send a test-email",
     "smtp_settings": "SMTP settings",
     "smtp_settings": "SMTP settings",
@@ -380,13 +380,13 @@
     "initialize_mail_settings": "initialize e-mail settings",
     "initialize_mail_settings": "initialize e-mail settings",
     "initialize_mail_modal_header": "Initialize e-mail settings",
     "initialize_mail_modal_header": "Initialize e-mail settings",
     "confirm_to_initialize_mail_settings": "You can't restore to the current settings. Are you sure you want to initialize e-mail settings?",
     "confirm_to_initialize_mail_settings": "You can't restore to the current settings. Are you sure you want to initialize e-mail settings?",
-    "file_upload_settings":"File Upload Settings",
-    "file_upload_method":"File Upload Method",
-    "file_delivery_method":"File Delivery Method",
-    "file_delivery_method_redirect":"Redirect",
-    "file_delivery_method_relay":"Internal System Relay",
-    "file_delivery_method_redirect_info":"Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
-    "file_delivery_method_relay_info":"Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
+    "file_upload_settings": "File Upload Settings",
+    "file_upload_method": "File Upload Method",
+    "file_delivery_method": "File Delivery Method",
+    "file_delivery_method_redirect": "Redirect",
+    "file_delivery_method_relay": "Internal System Relay",
+    "file_delivery_method_redirect_info": "Redirect: It redirects to a signed URL without GROWI server, it gives excellent performance.",
+    "file_delivery_method_relay_info": "Internal System Relay: The GROWI server delivers to clients, it provides complete security.",
     "fixed_by_env_var": "This is fixed by the env var <code>{{envKey}}={{envVar}}</code>.",
     "fixed_by_env_var": "This is fixed by the env var <code>{{envKey}}={{envVar}}</code>.",
     "gcs_label": "GCP(GCS)",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
     "aws_label": "AWS(S3)",
@@ -411,17 +411,7 @@
     "enable": "Enable",
     "enable": "Enable",
     "disable": "Disable",
     "disable": "Disable",
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <code>{{variable}}</code> is used.",
-    "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-    "questionnaire_settings": "Questionnaire settings",
-    "questionnaire_settings_explanation": "This will enable/disable questionnaires on the whole system. When enabled, users can also enable/disable questionnaires individually from \"Other Settings\" in the personal settings page.",
-    "about_data_sent": "About information sent",
-    "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
-    "learn_more": "Learn more",
-    "other_info_will_be_sent": "Along with the questionnaire answer, information necessary to improve GROWI will be sent. Personal user info will not be included in the data sent.",
-    "we_will_use_the_data_to_improve_growi": "We will use the data to improve GROWI experience as much as possible.",
-    "anonymize_app_site_url": "Anonymize app site URL in data sent",
-    "url_anonymization_explanation": "The app site URL included in the questionnaire answer will be anonymized. By enabling this, the GROWI application that sends the questionnaire answer will not be identified.",
-    "enable_questionnaire": "Enable questionnaire"
+    "note_for_the_only_env_option": "The GCS Settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
   },
   },
   "markdown_settings": {
   "markdown_settings": {
     "markdown_settings": "Markdown Settings",
     "markdown_settings": "Markdown Settings",
@@ -502,13 +492,13 @@
       "show_page_side_authors": "Always display creators and updaters above the table of contents",
       "show_page_side_authors": "Always display creators and updaters above the table of contents",
       "show_page_side_authors_desc": "Displays information about the creator and the last updater above the table of contents in the page sidebar."
       "show_page_side_authors_desc": "Displays information about the creator and the last updater above the table of contents in the page sidebar."
     },
     },
-      "presentation": "Presentation",
+    "presentation": "Presentation",
     "presentation_options": {
     "presentation_options": {
       "enable_marp": "Enable Marp ",
       "enable_marp": "Enable Marp ",
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "enable_marp_desc": "Marp can be used in presentation preview. This option may make you vulnerable to XSS.",
       "marp_official_site": "The Marp Official Site",
       "marp_official_site": "The Marp Official Site",
       "marp_official_site_link": "https://marp.app",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "GROWI Docs - Create slide using Marp",
+      "marp_in_growi": "GROWI Docs - Create slide using Marp",
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     },
     "custom_title": "Custom title",
     "custom_title": "Custom title",
@@ -525,7 +515,7 @@
     "custom_presentation": "Custom presentation",
     "custom_presentation": "Custom presentation",
     "write_java": "You can write Javascript that is applied to whole system.",
     "write_java": "You can write Javascript that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change.",
     "reflect_change": "You need to reload the page to reflect the change.",
-    "custom_logo" : "Custom Logo",
+    "custom_logo": "Custom Logo",
     "default_logo": "Default Logo",
     "default_logo": "Default Logo",
     "upload_logo": "Upload Logo",
     "upload_logo": "Upload Logo",
     "current_logo": "Current Logo",
     "current_logo": "Current Logo",
@@ -599,9 +589,9 @@
     },
     },
     "import": "Import",
     "import": "Import",
     "skip_username_and_email_when_overlapped": "Skip username and email using same username and email in new environment",
     "skip_username_and_email_when_overlapped": "Skip username and email using same username and email in new environment",
-    "prepare_new_account_for_migration":"Prepare new account for migration",
-    "archive_data_import_detail":"More Details? Ckick here.",
-    "admin_archive_data_import_guide_url":"https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
+    "prepare_new_account_for_migration": "Prepare new account for migration",
+    "archive_data_import_detail": "More Details? Ckick here.",
+    "admin_archive_data_import_guide_url": "https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "Directory_hierarchy_tag": "Directory hierarchy tag"
     "Directory_hierarchy_tag": "Directory hierarchy tag"
   },
   },
@@ -671,7 +661,7 @@
     "delete": "Delete",
     "delete": "Delete",
     "integration_procedure": "Integration Procedure",
     "integration_procedure": "Integration Procedure",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
-    "integration_failed":"Integration failed",
+    "integration_failed": "Integration failed",
     "reset": "Reset",
     "reset": "Reset",
     "reset_all_settings": "Reset all settings",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Delete Slack Bot settings",
     "delete_slackbot_settings": "Delete Slack Bot settings",
@@ -718,7 +708,7 @@
       "allow_specified_long": "Allow specified (Allowed from only specified channels)",
       "allow_specified_long": "Allow specified (Allowed from only specified channels)",
       "test_connection": "Test Connection",
       "test_connection": "Test Connection",
       "test_connection_by_pressing_button": "Press the button to test the connection",
       "test_connection_by_pressing_button": "Press the button to test the connection",
-      "test_connection_only_public_channel":"Please test connection in a public channel",
+      "test_connection_only_public_channel": "Please test connection in a public channel",
       "error_check_logs_below": "An error has occurred. Please check the logs below.",
       "error_check_logs_below": "An error has occurred. Please check the logs below.",
       "send_message_to_slack_work_space": "Send message to Slack work space.",
       "send_message_to_slack_work_space": "Send message to Slack work space.",
       "add_slack_workspace": "Add a Slack Workspace"
       "add_slack_workspace": "Add a Slack Workspace"
@@ -753,10 +743,10 @@
     "status": "Status",
     "status": "Status",
     "invite_modal": {
     "invite_modal": {
       "emails": "Emails (Possible to issue multiple people with new lines)",
       "emails": "Emails (Possible to issue multiple people with new lines)",
-      "description1":"Temporarily issue new users by email addresses.",
-      "description2":"A temporary password will be generated for the first login.",
+      "description1": "Temporarily issue new users by email addresses.",
+      "description2": "A temporary password will be generated for the first login.",
       "invite_thru_email": "Send invitation email",
       "invite_thru_email": "Send invitation email",
-      "mail_setting_link":"<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>Email settings</a>",
       "valid_email": "Valid email address is required",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
       "send_new_password": "Please send the new password to the user.",
@@ -791,10 +781,10 @@
       "new_password": "New Password"
       "new_password": "New Password"
     },
     },
     "external_account": "External Account Management",
     "external_account": "External Account Management",
-    "external_accounts":"External accounts",
-    "create_external_account":"Create external account",
+    "external_accounts": "External accounts",
+    "create_external_account": "Create external account",
     "external_account_list": "External Account List",
     "external_account_list": "External Account List",
-    "external_account_none":"No External Account",
+    "external_account_none": "No External Account",
     "invite": "Invite",
     "invite": "Invite",
     "invited": "User was invited",
     "invited": "User was invited",
     "back_to_user_management": "Back to User Management",
     "back_to_user_management": "Back to User Management",
@@ -979,7 +969,7 @@
     "ADMIN_SITE_URL_UPDATE": "Update Site URL Settings",
     "ADMIN_SITE_URL_UPDATE": "Update Site URL Settings",
     "ADMIN_MAIL_SMTP_UPDATE": "Update E-mail(SMTP) Settings",
     "ADMIN_MAIL_SMTP_UPDATE": "Update E-mail(SMTP) Settings",
     "ADMIN_MAIL_SES_UPDATE": "Update E-mail(SES) Settings",
     "ADMIN_MAIL_SES_UPDATE": "Update E-mail(SES) Settings",
-    "ADMIN_MAIL_TEST_SUBMIT" : "Send test mail",
+    "ADMIN_MAIL_TEST_SUBMIT": "Send test mail",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "Update File Upload Settings",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "Update File Upload Settings",
     "ADMIN_PLUGIN_UPDATE": "Update Plugin Settings",
     "ADMIN_PLUGIN_UPDATE": "Update Plugin Settings",
     "ADMIN_MAINTENANCEMODE_ENABLED": "Enable Maintenance Mode",
     "ADMIN_MAINTENANCEMODE_ENABLED": "Enable Maintenance Mode",

+ 0 - 36
apps/app/public/static/locales/en_US/commons.json

@@ -7,7 +7,6 @@
   "Sign out": "Logout",
   "Sign out": "Logout",
   "New": "New",
   "New": "New",
   "Delete": "Delete",
   "Delete": "Delete",
-
   "meta": {
   "meta": {
     "display_name": "English"
     "display_name": "English"
   },
   },
@@ -30,7 +29,6 @@
   "headers": {
   "headers": {
     "app_settings": "App Settings"
     "app_settings": "App Settings"
   },
   },
-
   "header_search_box": {
   "header_search_box": {
     "label": {
     "label": {
       "All pages": "All pages",
       "All pages": "All pages",
@@ -41,20 +39,17 @@
       "This tree": "Only children of this tree"
       "This tree": "Only children of this tree"
     }
     }
   },
   },
-
   "search_method_menu_item": {
   "search_method_menu_item": {
     "search_in_all": "Search in all",
     "search_in_all": "Search in all",
     "only_children_of_this_tree": "Only children of this tree",
     "only_children_of_this_tree": "Only children of this tree",
     "exact_mutch": "Exact match"
     "exact_mutch": "Exact match"
   },
   },
-
   "share_links": {
   "share_links": {
     "Share Link": "Share Link",
     "Share Link": "Share Link",
     "Page Path": "Page Path",
     "Page Path": "Page Path",
     "expire": "Expiration",
     "expire": "Expiration",
     "description": "Description"
     "description": "Description"
   },
   },
-
   "in_app_notification": {
   "in_app_notification": {
     "notification_list": "In-App Notification List",
     "notification_list": "In-App Notification List",
     "see_all": "See All",
     "see_all": "See All",
@@ -65,7 +60,6 @@
     "no_unread_messages": "no_unread_messages",
     "no_unread_messages": "no_unread_messages",
     "only_unread": "Only unread"
     "only_unread": "Only unread"
   },
   },
-
   "personal_dropdown": {
   "personal_dropdown": {
     "home": "Home",
     "home": "Home",
     "settings": "Settings",
     "settings": "Settings",
@@ -75,7 +69,6 @@
     "use_os_settings": "Use OS settings",
     "use_os_settings": "Use OS settings",
     "feedback": "Feedback"
     "feedback": "Feedback"
   },
   },
-
   "create_page_dropdown": {
   "create_page_dropdown": {
     "new_page": "Create New Page",
     "new_page": "Create New Page",
     "open_page_create_modal": "Open new page create modal",
     "open_page_create_modal": "Open new page create modal",
@@ -89,7 +82,6 @@
       "descendants": "Template for descendants"
       "descendants": "Template for descendants"
     }
     }
   },
   },
-
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
     "Page path": "Page path",
@@ -99,14 +91,12 @@
     "Markdown link": "Markdown link",
     "Markdown link": "Markdown link",
     "Append params": "Append params"
     "Append params": "Append params"
   },
   },
-
   "crop_image_modal": {
   "crop_image_modal": {
     "image_crop": "Image Crop",
     "image_crop": "Image Crop",
     "crop": "Crop",
     "crop": "Crop",
     "save": "Save",
     "save": "Save",
     "cancel": "Cancel"
     "cancel": "Cancel"
   },
   },
-
   "handsontable_modal": {
   "handsontable_modal": {
     "title": "Edit Table",
     "title": "Edit Table",
     "data_import": "Data Import",
     "data_import": "Data Import",
@@ -122,35 +112,9 @@
       "import": "Import"
       "import": "Import"
     }
     }
   },
   },
-
-  "questionnaire_modal": {
-    "required": "Required",
-    "submit": "Submit",
-    "close": "Close",
-    "title": "GROWI questionnaire for service improvement",
-    "more_satisfied_services": "We hope that GROWI customers will be even more satisfied",
-    "strive_to_improve_services": "once we improve our services based on your feedback.",
-    "length_of_experience": {
-      "more_than_two_years": "More than 2 years",
-      "one_to_two_years": "More than 1 year but less than 2 years",
-      "six_months_to_one_year": "More than 6 months but less than 1 year",
-      "three_months_to_six_months": "More than 3 months but less than 6 months",
-      "one_month_to_three_months": "More than 1 month but less than 3 months",
-      "less_than_one_month": "Less than 1 month"
-    },
-    "satisfaction_with_growi": "Satisfaction with GROWI",
-    "history_of_growi_usage": "History of GROWI usage",
-    "occupation": "Occupation",
-    "position": "Position",
-    "comment_on_growi": "Comment on GROWI",
-    "successfully_submitted": "Your survey has been submitted.",
-    "thanks_for_answering": "Thank you very much for answering."
-  },
-
   "not_found_page": {
   "not_found_page": {
     "page_not_exist": "This page does not exist."
     "page_not_exist": "This page does not exist."
   },
   },
-
   "g2g_data_transfer": {
   "g2g_data_transfer": {
     "tab": "Data transfer",
     "tab": "Data transfer",
     "data_transfer": "Data Transfer",
     "data_transfer": "Data Transfer",

+ 26 - 21
apps/app/public/static/locales/en_US/translation.json

@@ -142,7 +142,6 @@
   "edited this page": "edited this page.",
   "edited this page": "edited this page.",
   "List Drafts": "Drafts",
   "List Drafts": "Drafts",
   "Deleted Pages": "Deleted Pages",
   "Deleted Pages": "Deleted Pages",
-  "Questionnaire": "Questionnaire",
   "Disassociate": "Disassociate",
   "Disassociate": "Disassociate",
   "No bookmarks yet": "No bookmarks yet",
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "Add to Bookmarks",
   "add_bookmark": "Add to Bookmarks",
@@ -508,9 +507,27 @@
       "Search in Editor": "Search in Editor",
       "Search in Editor": "Search in Editor",
       "Move Line": "Move Line",
       "Move Line": "Move Line",
       "Copy Line": "Copy Line",
       "Copy Line": "Copy Line",
-      "Toggle Line": "Toggle Line Comment",
       "Insert Line": "Insert Line",
       "Insert Line": "Insert Line",
-      "Post Comment": "(Post Comment)"
+      "Post Comment": "(Post Comment)",
+      "Multiple Cursors": "Multiple Cursors",
+      "Or Alt Click": "or Alt + Click"
+    },
+    "format": {
+      "title": "Format Settings (Editor)",
+      "Bold": "Bold",
+      "Italic": "Italic",
+      "Strikethrough": "Strikethrough",
+      "Code Text": "Code Text",
+      "Hyperlink": "Hyperlink"
+    },
+    "line_settings": {
+      "title": "Line Settings (Editor)",
+      "Bullet List": "Bullet List",
+      "Numbered List": "Numbered List",
+      "Quote": "Quote",
+      "Code Block": "Code Block",
+      "Comment Out": "Comment Out",
+      "Comment Out Desc": "(Hide)"
     }
     }
   },
   },
   "modal_resolve_conflict": {
   "modal_resolve_conflict": {
@@ -526,6 +543,8 @@
   },
   },
   "sidebar_ai_assistant": {
   "sidebar_ai_assistant": {
     "reference_pages_label": "Reference pages",
     "reference_pages_label": "Reference pages",
+    "recent_chat": "Recent chat",
+    "no_recent_chat": "No recent chat",
     "placeholder": "Ask me anything.",
     "placeholder": "Ask me anything.",
     "knowledge_assistant_placeholder": "Ask me anything.",
     "knowledge_assistant_placeholder": "Ask me anything.",
     "editor_assistant_placeholder": "Can I help you with anything?",
     "editor_assistant_placeholder": "Can I help you with anything?",
@@ -540,6 +559,8 @@
     "budget_exceeded_for_growi_cloud": "You have reached your OpenAI API usage limit. To use the Knowledge Assistant again, please add credits from the GROWI.cloud admin page for Hosted users or from the OpenAI billing page for Owned users.",
     "budget_exceeded_for_growi_cloud": "You have reached your OpenAI API usage limit. To use the Knowledge Assistant again, please add credits from the GROWI.cloud admin page for Hosted users or from the OpenAI billing page for Owned users.",
     "error_message": "An error has occurred",
     "error_message": "An error has occurred",
     "show_error_detail": "Show error details",
     "show_error_detail": "Show error details",
+    "editor_assistant_long_context_warn_with_unit_line": "The text is too long, so the Editor Assistant will reference approximately lines {{startPosition}} to {{endPosition}} for its response.",
+    "editor_assistant_long_context_warn_with_unit_char": "The text is too long, so the Editor Assistant will reference characters {{startPosition}} to {{endPosition}} for its response.",
     "discard": "Discard",
     "discard": "Discard",
     "accept": "Accept",
     "accept": "Accept",
     "use_assistant": "Use Assistant",
     "use_assistant": "Use Assistant",
@@ -630,11 +651,12 @@
   "default_ai_assistant": {
   "default_ai_assistant": {
     "not_set": "Default assistant is not set"
     "not_set": "Default assistant is not set"
   },
   },
-  "ai_assistant_tree": {
+  "ai_assistant_substance": {
     "add_assistant": "Add Assistant",
     "add_assistant": "Add Assistant",
     "my_assistants": "My Assistants",
     "my_assistants": "My Assistants",
     "team_assistants": "Team Assistants",
     "team_assistants": "Team Assistants",
     "thread_does_not_exist": "No threads exist",
     "thread_does_not_exist": "No threads exist",
+    "recent_threads": "Recent Items",
     "toaster": {
     "toaster": {
       "ai_assistant_deleted_success": "Assistant deleted",
       "ai_assistant_deleted_success": "Assistant deleted",
       "ai_assistant_deleted_failed": "Failed to delete assistant",
       "ai_assistant_deleted_failed": "Failed to delete assistant",
@@ -985,23 +1007,6 @@
     "page_tree_not_avaliable": "Page tree feature is not available yet.",
     "page_tree_not_avaliable": "Page tree feature is not available yet.",
     "go_to_settings": "Go to settings to enable the feature"
     "go_to_settings": "Go to settings to enable the feature"
   },
   },
-  "questionnaire": {
-    "give_us_feedback": "Give us feedback for improvements",
-    "thank_you_for_answering": "Thank you for answering",
-    "additional_feedback": "Send us additional feedback from the user icon dropdown.",
-    "dont_show_again": "Don`t show again",
-    "deny": "Don't answer",
-    "agree": "Agree",
-    "disagree": "Disagree",
-    "answer": "Answer",
-    "no_answer": "No answer",
-    "settings": "Questionnaire settings",
-    "failed_to_send": "Failed to send feedback",
-    "denied": "The questionnaire won't be shown again",
-    "personal_settings_explanation": "Questionnaires for improving GROWI will be shown. If you have other feedbacks, you can send them from the user icon dropdown.",
-    "enable_questionnaire": "Enable questionnaire",
-    "disabled_by_admin": "Questionnaire is disabled by admin"
-  },
   "tag_edit_modal": {
   "tag_edit_modal": {
     "edit_tags": "Edit Tags",
     "edit_tags": "Edit Tags",
     "done": "Done",
     "done": "Done",

+ 1 - 11
apps/app/public/static/locales/fr_FR/admin.json

@@ -411,17 +411,7 @@
     "enable": "Activer",
     "enable": "Activer",
     "disable": "Désactiver",
     "disable": "Désactiver",
     "use_env_var_if_empty": "Si la valeur dans la base de données est vide, la valeur de variable d'environnement <code>{{variable}}</code> est utilisé.",
     "use_env_var_if_empty": "Si la valeur dans la base de données est vide, la valeur de variable d'environnement <code>{{variable}}</code> est utilisé.",
-    "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> .",
-    "questionnaire_settings": "Sondages anonymes",
-    "questionnaire_settings_explanation": "Paramètres d'activation des données analytiques. L'utilisateur peut choisir ce paramètre individuellement dans \"Autres paramètres\".",
-    "about_data_sent": "À propos",
-    "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
-    "learn_more": "En savoir plus",
-    "other_info_will_be_sent": "En plus des données analytiques, des données diagnostiques pour améliorer GROWI sont envoyées. Les données personnelles ne sont pas incluses.",
-    "we_will_use_the_data_to_improve_growi": "Les données seront utilisées pour améliorer au mieux GROWI",
-    "anonymize_app_site_url": "Ne pas inclure l'URL du site",
-    "url_anonymization_explanation": "L'URL du site configurée ne sera pas inclue dans les données envoyées.",
-    "enable_questionnaire": "Activer les données analytiques"
+    "note_for_the_only_env_option": "Les paramètres sont définis par des variables d'environnement.<br>Pour modifier ce paramètre, supprimer la variable d'environnement <code>{{env}}</code> ."
   },
   },
   "markdown_settings": {
   "markdown_settings": {
     "markdown_settings": "Markdown",
     "markdown_settings": "Markdown",

+ 0 - 23
apps/app/public/static/locales/fr_FR/commons.json

@@ -111,29 +111,6 @@
       "import": "Importer"
       "import": "Importer"
     }
     }
   },
   },
-  "questionnaire_modal": {
-    "required": "Requis",
-    "submit": "Soumettre",
-    "close": "Fermer",
-    "title": "Sondages aléatoires GROWI pour données anonymisées.",
-    "more_satisfied_services": "Nous espérons satisfaire au mieux les utilisateurs de GROWI",
-    "strive_to_improve_services": "et utilisons les retours d'utilisateurs afin d'améliorer l'expérience d'usage GROWI",
-    "length_of_experience": {
-      "more_than_two_years": "Plus de 2 ans",
-      "one_to_two_years": "Plus d'un an, mais moins de 2 ans",
-      "six_months_to_one_year": "Plus de 6 mois, mais moins d'un an",
-      "three_months_to_six_months": "Plus de 3 mois, mais moins de 6 mois",
-      "one_month_to_three_months": "Plus d'un moins, mais moins de 3 mois",
-      "less_than_one_month": "Moins d'un mois"
-    },
-    "satisfaction_with_growi": "Satisfaction avec GROWI",
-    "history_of_growi_usage": "Historique d'usage de GROWI",
-    "occupation": "Occupation",
-    "position": "Position",
-    "comment_on_growi": "Commentaires sur GROWI",
-    "successfully_submitted": "Questionnaire soumis.",
-    "thanks_for_answering": "Merci pour votre avis."
-  },
   "not_found_page": {
   "not_found_page": {
     "page_not_exist": "Cette page est introuvable."
     "page_not_exist": "Cette page est introuvable."
   },
   },

+ 27 - 22
apps/app/public/static/locales/fr_FR/translation.json

@@ -143,7 +143,6 @@
   "edited this page": "à modifié cette page.",
   "edited this page": "à modifié cette page.",
   "List Drafts": "Brouillons",
   "List Drafts": "Brouillons",
   "Deleted Pages": "Pages supprimées",
   "Deleted Pages": "Pages supprimées",
-  "Questionnaire": "Questionnaire",
   "Disassociate": "Dissocier",
   "Disassociate": "Dissocier",
   "No bookmarks yet": "Aucuns favoris",
   "No bookmarks yet": "Aucuns favoris",
   "add_bookmark": "Ajouter aux favoris",
   "add_bookmark": "Ajouter aux favoris",
@@ -489,7 +488,7 @@
       "Create Page": "Créer page",
       "Create Page": "Créer page",
       "Search": "Rechercher",
       "Search": "Rechercher",
       "Show Contributors": "Voir contributeurs",
       "Show Contributors": "Voir contributeurs",
-      "MirrorMode": "Mode mirroir",
+      "MirrorMode": "Mode miroir",
       "Konami Code": "Code Konami",
       "Konami Code": "Code Konami",
       "konami_code_url": "https://fr.wikipedia.org/wiki/Code_Konami"
       "konami_code_url": "https://fr.wikipedia.org/wiki/Code_Konami"
     },
     },
@@ -503,9 +502,27 @@
       "Search in Editor": "Rechercher dans l'éditeur",
       "Search in Editor": "Rechercher dans l'éditeur",
       "Move Line": "Déplacer la ligne",
       "Move Line": "Déplacer la ligne",
       "Copy Line": "Copier la ligne",
       "Copy Line": "Copier la ligne",
-      "Toggle Line": "Commenter/Décommenter la ligne",
       "Insert Line": "Insérer une ligne",
       "Insert Line": "Insérer une ligne",
-      "Post Comment": "(Publier le commentaire)"
+      "Post Comment": "(Publier le commentaire)",
+      "Multiple Cursors": "Curseurs multiples",
+      "Or Alt Click": "ou Alt + Clic"
+    },
+    "format": {
+      "title": "Paramètres de format (Éditeur)",
+      "Bold": "Gras",
+      "Italic": "Italique",
+      "Strikethrough": "Barré",
+      "Code Text": "Texte de code",
+      "Hyperlink": "Lien hypertexte"
+    },
+    "line_settings": {
+      "title": "Paramètres de ligne (Éditeur)",
+      "Bullet List": "Liste à puces",
+      "Numbered List": "Liste numérotée",
+      "Quote": "Citation",
+      "Code Block": "Bloc de code",
+      "Comment Out": "Masquer",
+      "Comment Out Desc": "(Commenter)"
     }
     }
   },
   },
   "modal_resolve_conflict": {
   "modal_resolve_conflict": {
@@ -521,6 +538,8 @@
   },
   },
   "sidebar_ai_assistant": {
   "sidebar_ai_assistant": {
     "reference_pages_label": "Pages de référence",
     "reference_pages_label": "Pages de référence",
+    "recent_chat": "Chat récent",
+    "no_recent_chat": "Pas de chat récent",
     "knowledge_assistant_placeholder": "Demandez-moi n'importe quoi.",
     "knowledge_assistant_placeholder": "Demandez-moi n'importe quoi.",
     "editor_assistant_placeholder": "Puis-je vous aider ?",
     "editor_assistant_placeholder": "Puis-je vous aider ?",
     "summary_mode_label": "Mode résumé",
     "summary_mode_label": "Mode résumé",
@@ -534,6 +553,8 @@
     "budget_exceeded_for_growi_cloud": "Vous avez atteint votre limite d'utilisation de l'API de l'OpenAI. Pour utiliser à nouveau l'assistant de connaissance, veuillez ajouter des crédits à partir de la page d'administration de GROWI.cloud pour les utilisateurs hébergés ou à partir de la page de facturation de l'OpenAI pour les utilisateurs propriétaires.",
     "budget_exceeded_for_growi_cloud": "Vous avez atteint votre limite d'utilisation de l'API de l'OpenAI. Pour utiliser à nouveau l'assistant de connaissance, veuillez ajouter des crédits à partir de la page d'administration de GROWI.cloud pour les utilisateurs hébergés ou à partir de la page de facturation de l'OpenAI pour les utilisateurs propriétaires.",
     "error_message": "Erreur",
     "error_message": "Erreur",
     "show_error_detail": "Détails de l'exposition",
     "show_error_detail": "Détails de l'exposition",
+    "editor_assistant_long_context_warn_with_unit_line": "Le texte est trop long, l'Assistant de rédaction se référera approximativement aux lignes {{startPosition}} à {{endPosition}} pour sa réponse.",
+    "editor_assistant_long_context_warn_with_unit_char": "Le texte est trop long, l'Assistant de rédaction se référera aux caractères {{startPosition}} à {{endPosition}} pour sa réponse.",
     "discard": "Annuler",
     "discard": "Annuler",
     "accept": "Accepter",
     "accept": "Accepter",
     "use_assistant": "Utiliser l'assistant",
     "use_assistant": "Utiliser l'assistant",
@@ -624,11 +645,12 @@
   "default_ai_assistant": {
   "default_ai_assistant": {
     "not_set": "L'assistant par défaut n'est pas configuré"
     "not_set": "L'assistant par défaut n'est pas configuré"
   },
   },
- "ai_assistant_tree": {
+  "ai_assistant_substance": {
     "add_assistant": "Ajouter un assistant",
     "add_assistant": "Ajouter un assistant",
     "my_assistants": "Mes assistants",
     "my_assistants": "Mes assistants",
     "team_assistants": "Assistants d'équipe",
     "team_assistants": "Assistants d'équipe",
     "thread_does_not_exist": "Aucune discussion",
     "thread_does_not_exist": "Aucune discussion",
+    "recent_threads": "Éléments récents",
     "toaster": {
     "toaster": {
       "ai_assistant_deleted_success": "Assistant supprimé",
       "ai_assistant_deleted_success": "Assistant supprimé",
       "ai_assistant_deleted_failed": "Échec de la suppression de l'assistant",
       "ai_assistant_deleted_failed": "Échec de la suppression de l'assistant",
@@ -979,23 +1001,6 @@
     "page_tree_not_avaliable": "Cette fonctionnalité n'est pas encore disponible.",
     "page_tree_not_avaliable": "Cette fonctionnalité n'est pas encore disponible.",
     "go_to_settings": "Activer cette fonctionnalité dans les paramètres"
     "go_to_settings": "Activer cette fonctionnalité dans les paramètres"
   },
   },
-  "questionnaire": {
-    "give_us_feedback": "Faites-nous part de votre avis",
-    "thank_you_for_answering": "Merci pour votre réponse",
-    "additional_feedback": "Envoyez-nous votre avis depuis le menu déroulant sur le menu utilisateur.",
-    "dont_show_again": "Ne plus afficher",
-    "deny": "Ne pas répondre",
-    "agree": "En accord",
-    "disagree": "En désaccord",
-    "answer": "Répondre",
-    "no_answer": "Aucune réponse",
-    "settings": "Sondages anonymes",
-    "failed_to_send": "Échec de l'envoi du sondage",
-    "denied": "Les sondages ne seront plus affichés.",
-    "personal_settings_explanation": "Sondages de satisfaction anonymes.",
-    "enable_questionnaire": "Sondages anonymes",
-    "disabled_by_admin": "Sondages anonymes désactivés par l'administrateur"
-  },
   "tag_edit_modal": {
   "tag_edit_modal": {
     "edit_tags": "Étiquettes",
     "edit_tags": "Étiquettes",
     "done": "Mettre à jour",
     "done": "Mettre à jour",

+ 1 - 11
apps/app/public/static/locales/ja_JP/admin.json

@@ -420,17 +420,7 @@
     "enable": "有効",
     "enable": "有効",
     "disable": "無効",
     "disable": "無効",
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
     "use_env_var_if_empty": "データベース側の値が空の場合、環境変数 <code>{{variable}}</code> の値を利用します",
-    "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
-    "questionnaire_settings": "アンケート設定",
-    "questionnaire_settings_explanation": "システム全体でアンケート機能を有効/無効にします。有効の場合、各ユーザーはユーザー設定ページの「その他の設定」から個別にアンケート機能を有効/無効にできます。",
-    "about_data_sent": "送信される情報について",
-    "docs_link": "https://docs.growi.org/ja/admin-guide/management-cookbook/app-settings.html#%E3%82%A2%E3%83%B3%E3%82%B1%E3%83%BC%E3%83%88%E8%A8%AD%E5%AE%9A",
-    "learn_more": "詳細",
-    "other_info_will_be_sent": "アンケートの回答と合わせて、GROWI の改善に必要な情報を送信します。送信されるデータにユーザーの個人情報は含まれません。",
-    "we_will_use_the_data_to_improve_growi": "私たちはそれらを活用し、最大限ユーザーの体験を向上させるよう努めます。",
-    "anonymize_app_site_url": "サイト URL を匿名化して送信する",
-    "url_anonymization_explanation": "アンケート回答データに含まれるサイト URL が匿名化されます。この設定を有効にすることで、アンケート回答データの送信元である GROWI アプリケーションが特定されなくなります。",
-    "enable_questionnaire": "アンケートを有効にする"
+    "note_for_the_only_env_option": "現在GCS設定は環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください"
   },
   },
   "markdown_settings": {
   "markdown_settings": {
     "markdown_settings": "マークダウン設定",
     "markdown_settings": "マークダウン設定",

+ 0 - 35
apps/app/public/static/locales/ja_JP/commons.json

@@ -32,7 +32,6 @@
   "headers": {
   "headers": {
     "app_settings": "アプリ設定"
     "app_settings": "アプリ設定"
   },
   },
-
   "header_search_box": {
   "header_search_box": {
     "label": {
     "label": {
       "All pages": "全てのページ",
       "All pages": "全てのページ",
@@ -43,20 +42,17 @@
       "This tree": "この階層下の子ページのみ"
       "This tree": "この階層下の子ページのみ"
     }
     }
   },
   },
-
   "search_method_menu_item": {
   "search_method_menu_item": {
     "search_in_all": "全てのページ",
     "search_in_all": "全てのページ",
     "only_children_of_this_tree": "この階層下の子ページのみ",
     "only_children_of_this_tree": "この階層下の子ページのみ",
     "exact_mutch": "キーワードに完全一致した文字を含むページのみ"
     "exact_mutch": "キーワードに完全一致した文字を含むページのみ"
   },
   },
-
   "share_links": {
   "share_links": {
     "Share Link": "共有用リンク",
     "Share Link": "共有用リンク",
     "Page Path": "ページパス",
     "Page Path": "ページパス",
     "expire": "有効期限",
     "expire": "有効期限",
     "description": "概要"
     "description": "概要"
   },
   },
-
   "in_app_notification": {
   "in_app_notification": {
     "notification_list": "アプリ内通知一覧",
     "notification_list": "アプリ内通知一覧",
     "see_all": "通知一覧を見る",
     "see_all": "通知一覧を見る",
@@ -67,7 +63,6 @@
     "no_unread_messages": "未読はありません",
     "no_unread_messages": "未読はありません",
     "only_unread": "未読のみ"
     "only_unread": "未読のみ"
   },
   },
-
   "personal_dropdown": {
   "personal_dropdown": {
     "home": "ホーム",
     "home": "ホーム",
     "settings": "設定",
     "settings": "設定",
@@ -77,7 +72,6 @@
     "use_os_settings": "OS設定を利用する",
     "use_os_settings": "OS設定を利用する",
     "feedback": "ご意見・ご要望"
     "feedback": "ご意見・ご要望"
   },
   },
-
   "create_page_dropdown": {
   "create_page_dropdown": {
     "new_page": "新規ページ作成",
     "new_page": "新規ページ作成",
     "open_page_create_modal": "新規ページ作成モーダルを表示",
     "open_page_create_modal": "新規ページ作成モーダルを表示",
@@ -91,7 +85,6 @@
       "descendants": "下位層テンプレート"
       "descendants": "下位層テンプレート"
     }
     }
   },
   },
-
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
     "Page path": "ページ名",
@@ -101,14 +94,12 @@
     "Markdown link": "マークダウン形式のリンク",
     "Markdown link": "マークダウン形式のリンク",
     "Append params": "パラメータの追加"
     "Append params": "パラメータの追加"
   },
   },
-
   "crop_image_modal": {
   "crop_image_modal": {
     "image_crop": "画像の切り抜き",
     "image_crop": "画像の切り抜き",
     "crop": "トリミング",
     "crop": "トリミング",
     "save": "保存",
     "save": "保存",
     "cancel": "キャンセル"
     "cancel": "キャンセル"
   },
   },
-
   "handsontable_modal": {
   "handsontable_modal": {
     "title": "テーブル編集",
     "title": "テーブル編集",
     "data_import": "データインポート",
     "data_import": "データインポート",
@@ -124,35 +115,9 @@
       "import": "インポート"
       "import": "インポート"
     }
     }
   },
   },
-
-  "questionnaire_modal": {
-    "required": "必須",
-    "submit": "送信",
-    "close": "閉じる",
-    "title": "GROWI サービス改善のためのアンケート",
-    "more_satisfied_services": "GROWI をご利用の皆さまに更にご満足いただけるよう",
-    "strive_to_improve_services": "皆さまからのご意見を参考にサービス改善に努めてまいります。",
-    "length_of_experience": {
-      "more_than_two_years": "2年以上",
-      "one_to_two_years": "1年以上2年未満",
-      "six_months_to_one_year": "6ヶ月以上1年未満",
-      "three_months_to_six_months": "3ヶ月以上6ヶ月未満",
-      "one_month_to_three_months": "1ヶ月以上3ヶ月未満",
-      "less_than_one_month": "1ヶ月未満"
-    },
-    "satisfaction_with_growi": "GROWI の満足度",
-    "history_of_growi_usage": "GROWI の利用歴",
-    "occupation": "職種",
-    "position": "役職",
-    "comment_on_growi": "GROWI へのコメント",
-    "successfully_submitted": "アンケートの送信が完了しました。",
-    "thanks_for_answering": "アンケートのご回答誠にありがとうございました。"
-  },
-
   "not_found_page": {
   "not_found_page": {
     "page_not_exist": "このページは存在しません。"
     "page_not_exist": "このページは存在しません。"
   },
   },
-
   "g2g_data_transfer": {
   "g2g_data_transfer": {
     "tab": "データ移行",
     "tab": "データ移行",
     "data_transfer": "データ移行",
     "data_transfer": "データ移行",

+ 27 - 21
apps/app/public/static/locales/ja_JP/translation.json

@@ -541,9 +541,27 @@
       "Search in Editor": "エディター内検索",
       "Search in Editor": "エディター内検索",
       "Move Line": "行の移動",
       "Move Line": "行の移動",
       "Copy Line": "行のコピー",
       "Copy Line": "行のコピー",
-      "Toggle Line": "行の非表示化",
       "Insert Line": "行を挿入",
       "Insert Line": "行を挿入",
-      "Post Comment": "(コメント投稿)"
+      "Post Comment": "(コメント投稿)",
+      "Multiple Cursors": "複数カーソル",
+      "Or Alt Click": "もしくは Alt + クリック"
+    },
+    "format": {
+      "title": "書式設定 (エディター)",
+      "Bold": "太字",
+      "Italic": "斜体",
+      "Strikethrough": "取り消し線",
+      "Code Text": "コードテキスト",
+      "Hyperlink": "ハイパーリンク"
+    },
+    "line_settings": {
+      "title": "行の設定 (エディター)",
+      "Bullet List": "箇条書きリスト",
+      "Numbered List": "番号付きリスト",
+      "Quote": "引用",
+      "Code Block": "コードブロック",
+      "Comment Out": "非表示にする",
+      "Comment Out Desc": "(コメントアウト)"
     }
     }
   },
   },
   "modal_resolve_conflict": {
   "modal_resolve_conflict": {
@@ -559,6 +577,8 @@
   },
   },
   "sidebar_ai_assistant": {
   "sidebar_ai_assistant": {
     "reference_pages_label": "参照するページ",
     "reference_pages_label": "参照するページ",
+    "recent_chat": "最近のチャット",
+    "no_recent_chat": "チャットがありません",
     "knowledge_assistant_placeholder": "ききたいことを入力してください",
     "knowledge_assistant_placeholder": "ききたいことを入力してください",
     "editor_assistant_placeholder": "お手伝いできることはありますか?",
     "editor_assistant_placeholder": "お手伝いできることはありますか?",
     "summary_mode_label": "要約モード",
     "summary_mode_label": "要約モード",
@@ -572,6 +592,8 @@
     "budget_exceeded_for_growi_cloud": "OpenAI の API の利用上限に達しました。ナレッジアシスタントを再度利用するには Hosted の場合は GROWI.cloud の管理画面から Owned の場合は OpenAI の請求ページからクレジットを追加してください。",
     "budget_exceeded_for_growi_cloud": "OpenAI の API の利用上限に達しました。ナレッジアシスタントを再度利用するには Hosted の場合は GROWI.cloud の管理画面から Owned の場合は OpenAI の請求ページからクレジットを追加してください。",
     "error_message": "エラーが発生しました",
     "error_message": "エラーが発生しました",
     "show_error_detail": "詳細を表示",
     "show_error_detail": "詳細を表示",
+    "editor_assistant_long_context_warn_with_unit_line": "本文が長すぎるため、エディターアシスタントは {{startPosition}}行から{{endPosition}}行付近までを参照して回答します",
+    "editor_assistant_long_context_warn_with_unit_char": "本文が長すぎるため、エディターアシスタントは {{startPosition}}文字目から{{endPosition}}文字目までを参照して回答します",
     "discard": "破棄",
     "discard": "破棄",
     "accept": "採用",
     "accept": "採用",
     "use_assistant": "アシスタントを使用する",
     "use_assistant": "アシスタントを使用する",
@@ -662,11 +684,12 @@
   "default_ai_assistant": {
   "default_ai_assistant": {
     "not_set": "デフォルトアシスタントが設定されていません"
     "not_set": "デフォルトアシスタントが設定されていません"
   },
   },
-  "ai_assistant_tree": {
+  "ai_assistant_substance": {
     "add_assistant": "アシスタントを追加する",
     "add_assistant": "アシスタントを追加する",
     "my_assistants": "マイアシスタント",
     "my_assistants": "マイアシスタント",
     "team_assistants": "チームアシスタント",
     "team_assistants": "チームアシスタント",
     "thread_does_not_exist": "スレッドが存在しません",
     "thread_does_not_exist": "スレッドが存在しません",
+    "recent_threads": "最近の項目",
     "toaster": {
     "toaster": {
       "ai_assistant_deleted_success": "アシスタントを削除しました",
       "ai_assistant_deleted_success": "アシスタントを削除しました",
       "ai_assistant_deleted_failed": "アシスタントの削除に失敗しました",
       "ai_assistant_deleted_failed": "アシスタントの削除に失敗しました",
@@ -1017,23 +1040,6 @@
     "page_tree_not_avaliable": "Page Tree 機能は現在使用できません。",
     "page_tree_not_avaliable": "Page Tree 機能は現在使用できません。",
     "go_to_settings": "設定する"
     "go_to_settings": "設定する"
   },
   },
-  "questionnaire": {
-    "give_us_feedback": "GROWI の改善のために、アンケートにご協力ください",
-    "thank_you_for_answering": "ご回答ありがとうございます",
-    "additional_feedback": "その他ご意見ご要望はユーザーアイコンのドロップダウンからお願い致します。",
-    "dont_show_again": "今後このアンケートを表示しない",
-    "deny": "回答しない",
-    "agree": "そう思う",
-    "disagree": "そう思わない",
-    "answer": "回答する",
-    "no_answer": "わからない",
-    "settings": "アンケート設定",
-    "failed_to_send": "回答送信に失敗しました",
-    "denied": "このアンケートは今後表示されません",
-    "personal_settings_explanation": "GROWI 改善のためのアンケートが表示されるようになります。ご意見ご要望はユーザーアイコンのドロップダウンからお願いいたします。",
-    "enable_questionnaire": "アンケートを有効にする",
-    "disabled_by_admin": "管理者によってアンケートは無効化されています"
-  },
   "tag_edit_modal": {
   "tag_edit_modal": {
     "edit_tags": "タグの編集",
     "edit_tags": "タグの編集",
     "done": "完了",
     "done": "完了",
@@ -1067,7 +1073,7 @@
     "untitled": "無題のページ"
     "untitled": "無題のページ"
   },
   },
   "sync-latest-revision-body": {
   "sync-latest-revision-body": {
-    "menuitem": "最新のリビジョンの本文とエディタのテキストを同期",
+    "menuitem": "最新のリビジョンの本文とエディタのテキストを同期",
     "confirm": "エディターに入力中のドラフトデータを削除して最新の本文を同期します。実行しますか?",
     "confirm": "エディターに入力中のドラフトデータを削除して最新の本文を同期します。実行しますか?",
     "alert": "最新の本文が同期されていない可能性があります。リロードして再度ご確認ください。",
     "alert": "最新の本文が同期されていない可能性があります。リロードして再度ご確認ください。",
     "success-toaster": "最新の本文を同期しました",
     "success-toaster": "最新の本文を同期しました",

+ 1142 - 0
apps/app/public/static/locales/ko_KR/admin.json

@@ -0,0 +1,1142 @@
+{
+  "meta": {
+    "display_name": "한국어"
+  },
+  "last_login": "최종 로그인",
+  "wiki_management_homepage": "위키 관리 홈페이지",
+  "public": "공개",
+  "anyone_with_the_link": "링크를 가진 모든 사람",
+  "specified_users": "지정된 사용자",
+  "only_me": "나만",
+  "only_inside_the_group": "그룹 내에서만",
+  "optional": "선택 사항",
+  "days": "일",
+  "security_settings": {
+    "security_settings": "보안 설정",
+    "scope_of_page_disclosure": "페이지 공개 범위",
+    "set_point": "설정 지점",
+    "Guest Users Access": "게스트 사용자 접근",
+    "readonly_users_access": "읽기 전용 사용자 접근",
+    "always_hidden": "항상 숨김",
+    "always_displayed": "항상 표시",
+    "Fixed by env var": "이것은 환경 변수 <code>{{key}}={{value}}</code>에 의해 고정됩니다.",
+    "register_limitation": "등록 제한",
+    "register_limitation_desc": "새 사용자 등록 제한",
+    "The whitelist of registration permission E-mail address": "등록 허용 이메일 주소 화이트리스트",
+    "users_without_account": "계정 없는 사용자는 접근할 수 없습니다",
+    "example": "예시",
+    "restrict_emails": "이메일 도메인(@로 시작)을 작성하여 위키에 대한 이메일 등록을 제한할 수 있습니다. ",
+    "for_example": " 예를 들어, growi.org 도메인 내의 사용자로 등록을 제한하려면 ",
+    "in_this_case": "를 작성할 수 있습니다. 이 경우 growi.org 도메인 내의 사용자만 등록할 수 있으며, 다른 모든 사용자는 거부됩니다.",
+    "insert_single": "한 줄에 하나의 이메일 주소를 입력하십시오.",
+    "page_list_and_search_results": "페이지 목록 / 검색 결과",
+    "page_listing_1": "'나만'으로 제한된 페이지 목록/검색<br>",
+    "page_listing_1_desc": "목록/검색 시 '나만' 옵션으로 제한된 페이지 표시",
+    "page_listing_2": "사용자 그룹으로 제한된 페이지 목록/검색<br>",
+    "page_listing_2_desc": "목록/검색 시 사용자 그룹으로 제한된 페이지 표시",
+    "page_access_rights": "페이지 접근 권한",
+    "page_delete_rights": "삭제 권한",
+    "page_delete": "페이지 삭제",
+    "page_delete_completely": "페이지 완전 삭제",
+    "comment_manage_rights": "댓글 관리 권한",
+    "other_options": "기타 옵션",
+    "deletion_explanation": "선택한 단일 페이지를 휴지통으로 이동할 수 있는 사용자를 제한합니다.",
+    "complete_deletion_explanation": "선택한 단일 페이지를 완전히 삭제할 수 있는 사용자를 제한합니다.",
+    "recursive_deletion_explanation": "하위 페이지를 포함하여 페이지를 휴지통으로 이동할 수 있는 사용자를 제한합니다.",
+    "recursive_complete_deletion_explanation": "하위 페이지를 포함하여 페이지를 완전히 삭제할 수 있는 사용자를 제한합니다.",
+    "is_all_group_membership_required_for_page_complete_deletion": "관리자 및 페이지 작성자 외의 사용자는 페이지 접근이 허용된 모든 그룹에 속해야 합니다.",
+    "is_all_group_membership_required_for_page_complete_deletion_explanation": "페이지 접근 설정이 '특정 그룹만'으로 설정된 경우에 유효합니다.",
+    "inherit": "상속 (단일 페이지와 동일한 설정 사용)",
+    "admin_only": "관리자만",
+    "admin_and_author": "관리자 및 작성자",
+    "anyone": "모든 사람",
+    "user_homepage_deletion": {
+      "user_homepage_deletion": "사용자 홈페이지 삭제",
+      "enable_user_homepage_deletion": "사용자 홈페이지 삭제 활성화",
+      "enable_force_delete_user_homepage_on_user_deletion": "사용자를 삭제할 때, 사용자의 홈페이지와 모든 하위 페이지가 완전히 삭제됩니다.",
+      "desc": "삭제된 사용자의 홈페이지를 삭제할 수 있습니다."
+    },
+    "session": "세션",
+    "max_age": "최대 수명 (밀리초)",
+    "max_age_desc": "사용자 세션이 만료되는 시간(밀리초)을 지정합니다.<br>기본값: 2592000000 (30일)",
+    "max_age_caution": "이 값을 수정한 후에는 서버를 다시 시작해야 합니다.",
+    "forced_update_desc": "설정이 강제로 변경되었습니다. 이전 설정: ",
+    "page_delete_rights_caution": "삭제 / 모두 삭제 권한(하위 페이지 포함)은 삭제 / 완전 삭제 권한보다 강하게 강제됩니다. <br> <br> 관리자만 > 관리자 및 작성자 > 모든 사람",
+    "Authentication mechanism settings": "인증 메커니즘 설정",
+    "setup_is_not_yet_complete": "설정이 아직 완료되지 않았습니다",
+    "xss_prevent_setting": "XSS(교차 사이트 스크립팅) 방지",
+    "xss_prevent_setting_link": "마크다운 설정으로 이동",
+    "callback_URL": "콜백 URL",
+    "providerName": "제공자 이름",
+    "issuerHost": "발급자 호스트",
+    "scope": "범위",
+    "desc_of_callback_URL": "{{AuthName}} ID 공급자의 설정에서 사용하십시오",
+    "authorization_endpoint": "인증 엔드포인트",
+    "token_endpoint": "토큰 엔드포인트",
+    "revocation_endpoint": "폐기 엔드포인트",
+    "introspection_endpoint": "인트로스펙션 엔드포인트",
+    "userinfo_endpoint": "사용자 정보 엔드포인트",
+    "end_session_endpoint": "세션 종료 엔드포인트",
+    "registration_endpoint": "등록 엔드포인트",
+    "jwks_uri": "JSON 웹 키 세트 URL",
+    "clientID": "클라이언트 ID",
+    "client_secret": "클라이언트 시크릿",
+    "updated_general_security_setting": "보안 설정 업데이트 성공",
+    "setup_not_completed_yet": "설정이 아직 완료되지 않았습니다",
+    "guest_mode": {
+      "deny": "거부 (등록된 사용자만)",
+      "readonly": "허용 (게스트는 읽기만 가능)"
+    },
+    "read_only_users_comment": {
+      "deny": "거부 (읽기 전용 사용자의 댓글 관리 금지)",
+      "accept": "허용 (읽기 전용 사용자는 댓글 관리 가능)"
+    },
+    "registration_mode": {
+      "open": "열림 (누구나 등록 가능)",
+      "restricted": "제한됨 (관리자의 승인 필요)",
+      "closed": "닫힘 (초대 전용)"
+    },
+    "share_link_management": "공유 링크 관리",
+    "No_share_links": "공유 링크 없음",
+    "share_link_notice": "모든 공유 링크 제거",
+    "delete_all_share_links": "모든 공유 링크 삭제",
+    "share_link_rights": "공유 링크 권한",
+    "enable_link_sharing": "링크 공유 활성화",
+    "all_share_links": "모든 공유 링크",
+    "configuration": " 구성",
+    "Treat username matching as identical": "새로 로그인한 외부 계정을 <code>username</code>이 일치할 때 로컬 계정에 자동으로 바인딩",
+    "Treat username matching as identical_warn": "경고: 시스템이 <code>username</code> 일치로 동일한 사용자를 처리하므로 보안에 유의하십시오.",
+    "Treat email matching as identical": "새로 로그인한 외부 계정을 <code>email</code>이 일치할 때 로컬 계정에 자동으로 바인딩",
+    "Treat email matching as identical_warn": "경고: 시스템이 <code>email</code> 일치로 동일한 사용자를 처리하므로 보안에 유의하십시오.",
+    "Use env var if empty": "비어 있으면 환경 변수 <code>{{env}}</code> 사용",
+    "Use default if both are empty": "둘 다 비어 있으면 기본값 <code>{{target}}</code>이 사용됩니다.",
+    "missing mandatory configs": "다음 필수 항목이 데이터베이스 또는 환경 변수에 설정되어 있지 않습니다.",
+    "Local": {
+      "name": "ID/비밀번호",
+      "note for the only env option": "로컬 인증은 환경 변수 값에 의해 제한됩니다.<br>이 설정을 변경하려면 환경 변수 <code>{{env}}</code>의 값을 false로 변경하거나 삭제하십시오.",
+      "enable_local": "ID/비밀번호 활성화",
+      "password_reset_by_users": "사용자에 의한 비밀번호 재설정",
+      "enable_password_reset_by_users": "사용자에 의한 비밀번호 재설정 활성화",
+      "password_reset_desc": "비밀번호를 잊어버린 경우 사용자가 직접 재설정할 수 있습니다.",
+      "email_authentication": "사용자 등록 시 이메일 인증",
+      "enable_email_authentication": "이메일 인증 활성화",
+      "enable_email_authentication_desc": "사용자 등록을 위해 이메일 인증이 수행됩니다."
+    },
+    "ldap": {
+      "enable_ldap": "LDAP 활성화",
+      "server_url_detail": "디렉토리 서비스의 LDAP URL 형식은 <code>ldap://host:port/DN</code> 또는 <code>ldaps://host:port/DN</code>입니다.",
+      "bind_mode": "바인딩 모드",
+      "bind_manager": "관리자 바인딩",
+      "bind_user": "사용자 바인딩",
+      "bind_DN_manager_detail": "디렉토리 서비스를 인증하고 쿼리하는 계정의 DN",
+      "bind_DN_user_detail1": "디렉토리 서비스와 바인딩하는 데 사용되는 쿼리입니다.",
+      "bind_DN_user_detail2": "로그인 페이지에 입력된 사용자 이름을 참조하려면 <code>&#123;&#123;username&#125;&#125;</code>를 사용하십시오.",
+      "bind_DN_password": "바인딩 DN 비밀번호",
+      "bind_DN_password_manager_detail": "바인딩 DN 계정의 비밀번호입니다.",
+      "bind_DN_password_user_detail": "로그인 페이지에 입력된 비밀번호가 바인딩에 사용됩니다.",
+      "search_filter": "검색 필터",
+      "search_filter_detail1": "인증된 사용자를 찾는 데 사용되는 쿼리입니다.",
+      "search_filter_detail2": "로그인 페이지에 입력된 사용자 이름을 참조하려면 <code>&#123;&#123;username&#125;&#125;</code>를 사용하십시오.",
+      "search_filter_detail3": "비어 있으면 필터 <code>(uid=&#123;&#123;username&#125;&#125;)</code>가 사용됩니다.",
+      "search_filter_example1": "'uid' 또는 'mail'과 일치",
+      "search_filter_example2": "Active Directory의 'sAMAccountName'과 일치",
+      "username_detail": "새 사용자 생성 시 <code>username</code> 매핑 사양",
+      "name_detail": "새 사용자 생성 시 전체 이름 매핑 사양",
+      "mail_detail": "새 사용자 생성 시 메일 주소 매핑 사양",
+      "group_search_base_DN": "그룹 검색 기본 DN",
+      "group_search_base_DN_detail": "그룹을 검색할 기본 DN입니다. 정의된 경우 검색이 작동하려면 <code>그룹 검색 필터</code>도 정의되어야 합니다.",
+      "group_search_filter": "그룹 검색 필터",
+      "group_search_filter_detail1": "그룹을 필터링하는 데 사용되는 쿼리입니다.",
+      "group_search_filter_detail2": "이 쿼리가 하나 이상의 그룹을 찾을 때만 LDAP를 통한 로그인이 허용됩니다.",
+      "group_search_filter_detail3": "찾은 사용자 개체를 대체하려면 <code>&#123;&#123;dn&#125;&#125;</code>를 사용하십시오.",
+      "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code>는 <code>cn=group1</code>을 가지고 <code>memberUid</code>에 사용자의 <code>uid</code>가 포함된 그룹을 찾습니다(<code>그룹 DN 속성</code>이 기본값에서 변경되지 않은 경우).",
+      "group_search_user_DN_property": "사용자 DN 속성",
+      "group_search_user_DN_property_detail": "<code>그룹 검색 필터</code>의 <code>&#123;&#123;dn&#125;&#125;</code> 보간에 사용할 사용자 개체의 속성입니다.",
+      "test_config": "저장된 구성 테스트",
+      "updated_ldap": "LDAP 설정 업데이트 성공"
+    },
+    "SAML": {
+      "name": "SAML",
+      "enable_saml": "SAML 활성화",
+      "id_detail": "SAML ID 공급자에서 사용자를 식별할 수 있는 속성 이름 사양",
+      "username_detail": "새 사용자 생성 시 <code>username</code> 매핑 사양",
+      "mapping_detail": "새 사용자 생성 시 {{target}} 매핑 사양",
+      "cert_detail": "IdP의 응답을 유효성 검사하기 위한 PEM 인코딩된 X.509 서명 인증서",
+      "Use env var if empty": "데이터베이스 값이 비어 있으면 환경 변수 <code>{{env}}</code>의 값이 사용됩니다.",
+      "note for the only env option": "SAML 인증을 활성화 또는 비활성화하는 설정 항목과 강조 표시된 설정 항목은 환경 변수 값만 사용합니다.<br>이 설정을 변경하려면 환경 변수 <code>{{env}}</code>의 값을 false로 변경하거나 삭제하십시오.",
+      "attr_based_login_control_detail": "<code>&lt;saml: Attribute&gt;</code> 요소와 그 하위 요소 <code>&lt;saml: AttributeValue&gt;</code>에 포함된 <code>&lt;saml: AttributeStatement&gt;</code> 요소를 사용하여 가입할 수 있는 사용자를 제한합니다.",
+      "attr_based_login_control_rule_help": "<h5>지원되는 쿼리:</h5><ul><li>용어</li><li>필드</li><li>AND/NOT/OR 연산자</li><li>그룹화</li></ul><h5>지원되지 않는 쿼리:</h5><ul><li>와일드카드, 퍼지, 근접, 범위 및 부스팅</li><li>+/- 연산자</li><li>필드 그룹화</li></ul><h5>특수 문자 이스케이프</h5>다음 특수 문자를 이스케이프해야 합니다:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> 및 <code>/</code>",
+      "attr_based_login_control_rule_example1": "<h5>조건 예시</h5>규칙이 <code>(Department: A || Department: B) && Position: Leader</code>인 경우, <code>Department: A</code> 또는 <code>Department: B</code>를 가지고 <code>Position: Leader</code>를 가진 사용자는 로그인할 수 있습니다.",
+      "attr_based_login_control_rule_example2": "<h5>이스케이프 예시</h5>쿼리 값으로 URL을 사용하려면 다음을 이스케이프하십시오:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
+      "updated_saml": "SAML 설정 업데이트 성공"
+    },
+    "OAuth": {
+      "enable_oidc": "OIDC 활성화",
+      "register": "%s 등록",
+      "change_redirect_url": "승인된 리디렉션 URI에 <code>%s</code> <br>(여기서 <code>%s</code>는 호스트 이름)를 입력하십시오.",
+      "Google": {
+        "enable_google": "Google OAuth 활성화",
+        "name": "Google OAuth",
+        "register_1": "{{link}}에 접속",
+        "register_2": "프로젝트가 없으면 프로젝트 생성",
+        "register_3": "자격 증명 생성 &rightarrow; OAuth 클라이언트 ID &rightarrow; 웹 애플리케이션 선택",
+        "register_4": "승인된 리디렉션 URI 중 하나로 <code>{{url}}</code>을 사용하여 OAuth 앱 등록",
+        "register_5": "클라이언트 ID와 클라이언트 시크릿을 위에 복사하여 붙여넣기",
+        "updated_google": "Google OAuth 설정 업데이트 성공"
+      },
+      "GitHub": {
+        "enable_github": "GitHub OAuth 활성화",
+        "name": "GitHub OAuth",
+        "register_1": "{{link}}에 접속",
+        "register_2": "인증 콜백 URL로 <code>{{url}}</code>을 사용하여 OAuth 앱 등록",
+        "register_3": "클라이언트 ID와 클라이언트 시크릿을 위에 복사하여 붙여넣기",
+        "updated_github": "GitHub OAuth 설정 업데이트 성공"
+      },
+      "OIDC": {
+        "name": "OpenID Connect",
+        "id_detail": "OIDC 클레임에서 사용자를 식별할 수 있는 속성 이름 사양",
+        "username_detail": "새 사용자 생성 시 <code>username</code> 매핑 사양",
+        "name_detail": "새 사용자 생성 시 <code>name</code> 매핑 사양",
+        "mapping_detail": "새 사용자 생성 시 {{target}} 매핑 사양",
+        "register_1": "OIDC IdP 관리자에게 문의",
+        "register_2": "인증 콜백 URL로 <code>{{url}}</code>을 사용하여 OIDC 앱 등록",
+        "register_3": "클라이언트 ID와 클라이언트 시크릿을 위에 복사하여 붙여넣기",
+        "updated_oidc": "OpenID Connect 업데이트 성공",
+        "Use discovered URL if empty": "비어 있으면 발급자 호스트에서 검색된 URL 사용"
+      },
+      "how_to": {
+        "google": "Google OAuth를 구성하는 방법은?",
+        "github": "GitHub OAuth를 구성하는 방법은?",
+        "oidc": "OIDC를 구성하는 방법은?"
+      }
+    },
+    "form_item_name": {
+      "entryPoint": "진입점",
+      "issuer": "발급자",
+      "cert": "인증서",
+      "attrMapId": "ID",
+      "attrMapUsername": "사용자 이름",
+      "attrMapMail": "메일 주소",
+      "attrMapFirstName": "이름",
+      "attrMapLastName": "성",
+      "ABLCRule": "규칙"
+    }
+  },
+  "notification_settings": {
+    "notification_settings": "알림 설정",
+    "slack_incoming_configuration": "Slack 수신 웹훅 구성",
+    "prioritize_webhook": "Slack 앱보다 수신 웹훅 우선",
+    "prioritize_webhook_desc": "이 옵션을 선택하면 Slack 앱 설정이 활성화되어 있어도 GROWI는 수신 웹훅을 사용합니다.",
+    "slack_app_configuration": "Slack 앱 구성",
+    "slack_app_configuration_desc": "이것은 Crowi와 호환되는 방식이지만,<br /> GROWI에서는 <strong>너무 복잡하므로</strong> 권장하지 않습니다.",
+    "use_instead": "대신 Slack 수신 웹훅 구성을 사용하십시오.",
+    "how_to": {
+      "header": "수신 웹훅을 구성하는 방법은?",
+      "workspace": "(워크스페이스에서) 훅 추가",
+      "workspace_desc1": "<a href='https://slack.com/services/new/incoming-webhook'>수신 웹훅 구성 페이지</a>로 이동하십시오.",
+      "workspace_desc2": "게시할 기본 채널을 선택하십시오.",
+      "workspace_desc3": "추가하십시오.",
+      "at_growi": "(GROWI 관리 페이지에서) 웹훅 URL 설정",
+      "at_growi_desc": "이 페이지에서 &rdquo;웹훅 URL&rdquo;을 입력하고 제출하십시오."
+    },
+    "user_trigger_notification_header": "패턴에 대한 기본 알림 설정",
+    "pattern": "패턴",
+    "channel": "채널",
+    "pattern_desc": "위키의 경로 이름입니다. <code>*</code>를 사용한 패턴 표현식을 사용할 수 있습니다.",
+    "channel_desc": "Slack 채널 이름입니다. <code>#</code> 없이.",
+    "valid_page": "알림 활성화/비활성화",
+    "link_notification_help": "<strong>링크를 아는 사람만 볼 수 있는 페이지('링크를 가진 모든 사람')</strong>는 항상 알림이 전송되지 않습니다.",
+    "just_me_notification_help": "<strong>'나만'으로 제한된 페이지</strong>는 페이지가 편집될 때 알림이 전송됩니다.",
+    "group_notification_help": "<strong>'사용자 그룹'으로 제한된 페이지</strong>는 페이지가 편집될 때 알림이 전송됩니다.",
+    "notification_list": "알림 설정 목록",
+    "add_notification": "새로 추가",
+    "trigger_path": "트리거 경로",
+    "trigger_path_help": "(<code>*</code>를 사용한 표현식 지원)",
+    "trigger_events": "트리거 이벤트",
+    "notify_to": "알림 대상",
+    "back_to_list": "목록으로 돌아가기",
+    "notification_detail": "알림 설정 세부 정보",
+    "event_pageCreate": "새 페이지가 생성될 때",
+    "event_pageEdit": "페이지가 편집될 때",
+    "event_pageDelete": "페이지가 삭제될 때",
+    "event_pageMove": "페이지가 이동될 때 (이름 변경)",
+    "event_pageLike": "누군가 페이지를 좋아할 때",
+    "event_comment": "누군가 페이지에 댓글을 달 때",
+    "email": {
+      "ifttt_link": "이메일 트리거로 새 IFTTT 애플릿 생성"
+    },
+    "updated_slackApp": "Slack 앱 구성 설정 업데이트 성공",
+    "add_notification_pattern": "사용자 트리거 알림 패턴 추가",
+    "delete_notification_pattern": "알림 패턴 삭제",
+    "delete_notification_pattern_desc1": "경로 삭제: {{path}}",
+    "delete_notification_pattern_desc2": "한 번 삭제하면 복구할 수 없습니다",
+    "toggle_notification": "{{path}} 설정 업데이트",
+    "not_found_global_notification_triggerid": "전역 알림 ID를 찾을 수 없습니다"
+  },
+  "full_text_search_management": {
+    "full_text_search_management": "전체 텍스트 검색 관리",
+    "elasticsearch_management": "Elasticsearch 관리",
+    "connection_status": "연결 상태",
+    "connection_status_label_unconfigured": "구성되지 않음",
+    "connection_status_label_connected": "연결됨",
+    "connection_status_label_disconnected": "연결 끊김",
+    "connection_status_label_erroroccured": "검색 서비스에서 오류 발생",
+    "indices_status": "인덱스 상태",
+    "indices_status_label_normalized": "정규화됨",
+    "indices_status_label_unnormalized": "재구축 중 또는 손상됨",
+    "indices_summary": "인덱스 요약",
+    "reconnect": "재연결",
+    "reconnect_button": "Elasticsearch에 다시 연결 시도",
+    "reconnect_description": "버튼을 클릭하여 Elasticsearch에 다시 연결을 시도합니다.",
+    "normalize": "정규화",
+    "normalize_button": "인덱스 정규화",
+    "normalize_description": "버튼을 클릭하여 손상된 인덱스를 복구합니다.",
+    "rebuild": "재구축",
+    "rebuild_button": "인덱스 재구축",
+    "rebuild_description_1": "버튼을 클릭하여 인덱스를 재구축하고 모든 페이지 데이터를 추가합니다.",
+    "rebuild_description_2": "이 작업은 시간이 걸릴 수 있습니다."
+  },
+  "mailer_setup_required": "<a href='/admin/app'>이메일 설정</a>이 전송에 필요합니다.",
+  "admin_top": {
+    "management_wiki": "관리 위키",
+    "system_information": "시스템 정보",
+    "wiki_administrator": "위키 관리자만 이 페이지에 접근할 수 있습니다",
+    "assign_administrator": "사용자 관리 페이지에서 '관리자 권한 부여' 버튼을 사용하여 선택한 사용자에게 위키 관리자 권한을 부여할 수 있습니다.",
+    "package_name": "패키지 이름",
+    "specified_version": "지정된 버전",
+    "installed_version": "설치된 버전",
+    "list_of_env_vars": "환경 변수 목록",
+    "env_var_priority": "보안 외의 환경 변수는 데이터베이스 값이 우선적으로 적용됩니다.",
+    "about_security": "보안 환경 변수에 대해서는 <a href='/admin/security'>보안 설정</a>을 확인하십시오.",
+    "copy_prefilled_host_information": {
+      "default": "미리 채워진 호스트 정보 복사",
+      "done": "클립보드에 복사되었습니다!"
+    },
+    "bug_report": "버그 보고서 제출",
+    "submit_bug_report": "<a href='https://github.com/weseek/growi/issues/new?assignees=&labels=bug&template=bug-report.md&title=Bug%3A' target='_blank' rel='noreferrer'>그런 다음 GitHub에 문제를 제출하십시오.</a>"
+  },
+  "v5_page_migration": {
+    "migration_desc": "일부 페이지는 이전 v4 호환성을 가지고 있습니다. 페이지 트리 및 쉬운 이름 변경과 같은 새로운 기능을 활용하려면 모든 페이지를 v5 호환성으로 변환하십시오.",
+    "migration_note": "참고: 페이지 경로에서 고유 제약 조건이 손실됩니다.",
+    "upgrade_to_v5": "v5 호환성으로 변환",
+    "modal_migration_warning": "이 프로세스는 시간이 오래 걸릴 수 있습니다. 관리자는 변환 중 사용자가 페이지를 생성, 수정 또는 삭제하지 않도록 권장합니다.",
+    "start_upgrading": "v5 호환성으로 변환 시작",
+    "successfully_started": "변환 시작 성공",
+    "already_upgraded": "이미 v5 호환성으로 변환을 완료했습니다",
+    "header_upgrading_progress": "업그레이드 진행률",
+    "migration_succeeded": "업그레이드가 성공적으로 완료되었습니다! 유지 보수 모드를 종료하면 GROWI를 사용할 수 있습니다.",
+    "migration_failed": "업그레이드 실패. 실패 시 수행할 작업에 대한 정보는 GROWI 문서를 참조하십시오."
+  },
+  "maintenance_mode": {
+    "maintenance_mode": "유지 보수 모드",
+    "under_maintenance_mode": "유지 보수 모드 중",
+    "failed_to_start_maintenance_mode": "유지 보수 모드 시작 실패",
+    "failed_to_end_maintenance_mode": "유지 보수 모드 종료 실패",
+    "successfully_started_maintenance_mode": "유지 보수 모드 시작 성공",
+    "successfully_ended_maintenance_mode": "유지 보수 모드 종료 성공",
+    "warning_message_to_start": "관리 설정 페이지 외에는 접근할 수 없습니다. 일반 사용자는 유지 보수 모드가 수동으로 종료될 때까지 어떤 콘텐츠에도 접근할 수 없습니다.",
+    "warning_message_to_end": "데이터 가져오기 또는 v5로 업그레이드가 이미 완료되었는지 확인하십시오. 완료되지 않은 경우 유지 보수 모드를 유지하는 것이 좋습니다.",
+    "supplymentary_message_to_start": "API의 경우 관리자 API만 작동합니다.",
+    "start_maintenance_mode": "유지 보수 모드 시작",
+    "end_maintenance_mode": "유지 보수 모드 종료",
+    "description": "유지 보수 모드는 모든 사용자 작업을 제한합니다. 데이터 가져오기 및 V5로 업그레이드 전에 항상 유지 보수 모드를 시작하십시오. 종료하려면 보안 설정 > 유지 보수 모드로 이동하십시오."
+  },
+  "app_setting": {
+    "site_name": "사이트 이름",
+    "sitename_change": "헤더 및 HTML 제목에 사용되는 사이트 이름을 변경할 수 있습니다.",
+    "header_content": "여기에 입력된 내용은 헤더 등에 표시됩니다.",
+    "site_url": {
+      "title": "사이트 URL 설정",
+      "desc": "사이트 URL 설정용입니다.",
+      "warn": "사이트 URL이 설정되지 않아 일부 기능이 작동하지 않습니다.",
+      "help": "<code>http://</code> 또는 <code>https://</code>로 시작하는 사이트 전체 URL입니다.",
+      "note_for_the_only_env_option": "사이트 URL은 환경 변수 값으로 고정됩니다.<br>이 설정을 변경하려면 환경 변수 <code>{{env}}</code>의 값을 false로 변경하거나 삭제하십시오."
+    },
+    "confidential_name": "기밀 이름",
+    "confidential_example": "예): 내부 전용",
+    "default_language": "새 사용자를 위한 기본 언어",
+    "default_mail_visibility": "새 사용자를 위한 이메일 공개",
+    "file_uploading": "파일 업로드",
+    "enable_files_except_image": "이 옵션을 활성화하면 모든 파일 형식을 업로드할 수 있습니다. 이 옵션이 없으면 이미지 파일 업로드만 지원됩니다.",
+    "attach_enable": "이 옵션을 활성화하면 이미지 파일 외의 파일을 첨부할 수 있습니다.",
+    "page_bulk_export_settings": "페이지 대량 내보내기 설정",
+    "enable_page_bulk_export": "대량 내보내기 활성화",
+    "page_bulk_export_explanation": "모든 사용자가 메뉴에서 한 번에 페이지와 모든 하위 페이지를 내보낼 수 있는 기능을 활성화합니다. 내보낸 데이터는 저장 기간이 지나면 자동으로 삭제됩니다.",
+    "page_bulk_export_warning": "대량 페이지 내보내기 기능은 모든 사용자에게 제공됩니다. 시스템 리소스 유지를 위해 최소한의 사용을 부탁드립니다. 관리자라면 모든 사용자에게 이 사실을 알려주십시오.",
+    "page_bulk_export_storage_period": "저장 기간",
+    "update": "업데이트",
+    "mail_settings": "이메일 설정",
+    "mailer_is_not_set_up": "이메일 설정이 되어 있지 않습니다.",
+    "from_e-mail_address": "보내는 이메일 주소",
+    "transmission_method": "전송 방식",
+    "smtp_label": "SMTP",
+    "ses_label": "SES(AWS)",
+    "send_test_email": "테스트 이메일 전송",
+    "success_to_send_test_email": "테스트 이메일 전송 성공",
+    "smtp_settings": "SMTP 설정",
+    "host": "호스트",
+    "port": "포트",
+    "user": "사용자",
+    "initialize_mail_settings": "이메일 설정 초기화",
+    "initialize_mail_modal_header": "이메일 설정 초기화",
+    "confirm_to_initialize_mail_settings": "현재 설정으로 복원할 수 없습니다. 이메일 설정을 초기화하시겠습니까?",
+    "file_upload_settings": "파일 업로드 설정",
+    "file_upload_method": "파일 업로드 방식",
+    "file_delivery_method": "파일 전송 방식",
+    "file_delivery_method_redirect": "리디렉션",
+    "file_delivery_method_relay": "내부 시스템 중계",
+    "file_delivery_method_redirect_info": "리디렉션: GROWI 서버 없이 서명된 URL로 리디렉션하여 뛰어난 성능을 제공합니다.",
+    "file_delivery_method_relay_info": "내부 시스템 중계: GROWI 서버가 클라이언트에 전송하여 완벽한 보안을 제공합니다.",
+    "fixed_by_env_var": "이것은 환경 변수 <code>{{envKey}}={{envVar}}</code>에 의해 고정됩니다.",
+    "gcs_label": "GCP(GCS)",
+    "aws_label": "AWS(S3)",
+    "local_label": "로컬",
+    "gridfs_label": "MongoDB(GridFS)",
+    "azure_label": "Azure(Blob)",
+    "azure_tenant_id": "테넌트 ID",
+    "azure_client_id": "클라이언트 ID",
+    "azure_client_secret": "클라이언트 시크릿",
+    "azure_storage_account_name": "스토리지 계정 이름",
+    "azure_storage_container_name": "컨테이너 이름",
+    "azure_note_for_the_only_env_option": "Azure 설정은 환경 변수 값에 의해 제한됩니다.<br>이 설정을 변경하려면 환경 변수 <code>{{env}}</code>의 값을 false로 변경하거나 삭제하십시오.",
+    "file_upload": "파일 업로드 설정용입니다. 파일 업로드 설정을 완료하면 파일 업로드 기능, 프로필 사진 기능 등이 활성화됩니다.",
+    "test_connection": "메일 연결 테스트",
+    "change_setting": "주의: 이 설정을 완료하지 않으면 지금까지 업로드한 파일에 접근할 수 없습니다.",
+    "region": "지역",
+    "bucket_name": "버킷 이름",
+    "custom_endpoint": "사용자 지정 엔드포인트",
+    "custom_endpoint_change": "S3 호환 API를 가진 MinIO와 같은 객체 스토리지 서비스의 엔드포인트 URL을 입력하십시오. 비어 있으면 Amazon S3가 사용됩니다.",
+    "s3_secret_access_key_input_description": "설정 값이 숨겨져 있습니다",
+    "load_plugins": "플러그인 로드",
+    "enable": "활성화",
+    "disable": "비활성화",
+    "use_env_var_if_empty": "데이터베이스 값이 비어 있으면 환경 변수 <code>{{variable}}</code>의 값이 사용됩니다.",
+    "note_for_the_only_env_option": "GCS 설정은 환경 변수 값에 의해 제한됩니다.<br>이 설정을 변경하려면 환경 변수 <code>{{env}}</code>의 값을 false로 변경하거나 삭제하십시오."
+  },
+  "markdown_settings": {
+    "markdown_settings": "마크다운 설정",
+    "lineBreak_header": "줄 바꿈 설정",
+    "lineBreak_desc": "줄 바꿈 설정을 변경할 수 있습니다.",
+    "lineBreak_options": {
+      "enable_lineBreak": "줄 바꿈 활성화",
+      "enable_lineBreak_desc": "텍스트 페이지의 줄 바꿈을 HTML에서 <code>&lt;br&gt;</code>로 변환",
+      "enable_lineBreak_for_comment": "댓글에서 줄 바꿈 활성화",
+      "enable_lineBreak_for_comment_desc": "댓글의 줄 바꿈을 HTML에서 <code>&lt;br&gt;</code>로 변환"
+    },
+    "indent_header": "들여쓰기 설정",
+    "indent_desc": "들여쓰기 설정을 변경할 수 있습니다.",
+    "indent_options": {
+      "indentSize": "기본 들여쓰기 크기",
+      "indentSize_desc": "마크다운 편집기의 기본 들여쓰기 크기 설정",
+      "disallow_indent_change": "사용자에 의한 들여쓰기 크기 변경 금지",
+      "disallow_indent_change_desc": "사용자가 기본 들여쓰기 크기를 강제로 사용하도록 합니다."
+    },
+    "xss_header": "XSS(교차 사이트 스크립팅) 방지 설정",
+    "xss_desc": "마크다운 텍스트의 HTML 태그 처리 방식을 변경할 수 있습니다.",
+    "xss_options": {
+      "enable_xss_prevention": "XSS 방지 활성화",
+      "remove_all_tags": "모든 태그 제거",
+      "remove_all_tags_desc": "모든 HTML 태그 및 속성 제거",
+      "recommended_setting": "권장 설정",
+      "custom_whitelist": "사용자 지정 화이트리스트",
+      "tag_names": "태그 이름",
+      "tag_attributes": "태그 속성",
+      "import_recommended": "권장 {{target}} 가져오기"
+    }
+  },
+  "customize_settings": {
+    "customize_settings": "사용자 지정",
+    "default_sidebar_mode": {
+      "title": "기본 사이드바 모드",
+      "desc": "새 사용자 및 페이지를 방문하는 게스트를 위한 사이드바 모드를 설정할 수 있습니다.",
+      "dock_mode_default_desc": "독 모드가 선택되었을 때 사이드바의 초기 상태를 설정할 수 있습니다.",
+      "dock_mode_default_open": "처음부터 열린 상태로 페이지 열기",
+      "dock_mode_default_close": "처음부터 닫힌 상태로 페이지 열기"
+    },
+    "layout": "레이아웃",
+    "layout_options": {
+      "default": "기본 콘텐츠 너비",
+      "expanded": "콘텐츠 너비 100%"
+    },
+    "theme": "테마",
+    "theme_desc": {
+      "light_and_dark": "밝은 모드 및 어두운 모드",
+      "unique": "하나의 모드만"
+    },
+    "function": "기능",
+    "function_desc": "기능의 유효/무효를 선택할 수 있습니다.",
+    "function_options": {
+      "timeline": "타임라인 기능",
+      "timeline_desc1": "하위 페이지의 타임라인을 표시할 수 있습니다.",
+      "timeline_desc2": "하위 페이지가 많으면 페이지 로딩 중 성능이 저하됩니다.",
+      "timeline_desc3": "비활성화하여 목록 페이지 표시 속도를 높일 수 있습니다.",
+      "tab_switch": "브라우저에서 탭 전환 저장",
+      "tab_switch_desc1": "브라우저에서 편집 탭 및 기록 탭 전환을 저장하고 브라우저의 앞으로/뒤로 명령에 대한 개체로 만듭니다.",
+      "tab_switch_desc2": "비활성화하여 페이지 전환을 브라우저의 앞으로/뒤로 명령에 대한 유일한 개체로 만들 수 있습니다.",
+      "attach_title_header": "새 페이지 생성 시 자동으로 h1 섹션 추가",
+      "attach_title_header_desc": "새 페이지 생성 시 페이지 경로를 h1 섹션으로 첫 줄에 추가합니다.",
+      "list_num_s": "모달에 표시되는 목록 수",
+      "list_num_desc_s": "'페이지 목록', '타임라인', '페이지 기록' 및 '첨부 파일' 페이지와 같은 페이지당 목록 수 설정",
+      "list_num_m": "다른 콘텐츠가 포함된 문서 페이지에 표시되는 목록 수",
+      "list_num_desc_m": "'북마크' 및 '최근 생성됨' 페이지와 같은 페이지당 목록 수 설정",
+      "list_num_l": "'검색' 페이지에 표시되는 목록 수",
+      "list_num_desc_l": "'검색' 페이지와 같은 페이지당 목록 수 설정",
+      "list_num_xl": "문서 페이지에 표시되는 목록 수",
+      "list_num_desc_xl": "'찾을 수 없음' 및 '휴지통' 페이지와 같은 페이지당 목록 수 설정",
+      "stale_notification": "오래된 페이지에 알림 표시",
+      "stale_notification_desc": "마지막 업데이트 이후 1년 이상 된 페이지에 알림을 표시합니다.",
+      "show_all_reply_comments": "모든 답글 댓글 표시",
+      "show_all_reply_comments_desc": "설정 값이 꺼져 있으면 최신 두 개를 제외한 댓글은 생략됩니다.",
+      "select_search_scope_children_as_default": "검색 범위의 기본값으로 '이 트리 내의 하위 항목만' 선택",
+      "select_search_scope_children_as_default_desc": "설정 값이 꺼져 있으면 '모든 페이지'가 검색 범위의 기본값으로 사용됩니다.",
+      "show_page_side_authors": "목차 위에 작성자 및 업데이트자 항상 표시",
+      "show_page_side_authors_desc": "페이지 사이드바의 목차 위에 작성자 및 마지막 업데이트자에 대한 정보를 표시합니다."
+    },
+    "presentation": "프레젠테이션",
+    "presentation_options": {
+      "enable_marp": "Marp 활성화 ",
+      "enable_marp_desc": "Marp는 프레젠테이션 미리보기에서 사용할 수 있습니다. 이 옵션은 XSS에 취약하게 만들 수 있습니다.",
+      "marp_official_site": "Marp 공식 사이트",
+      "marp_official_site_link": "https://marp.app",
+      "marp_in_growi": "GROWI 문서 - Marp를 사용하여 슬라이드 생성",
+      "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
+    },
+    "custom_title": "사용자 지정 제목",
+    "custom_title_detail": "<code>&lt;title&gt;</code> 태그를 사용자 지정할 수 있습니다. 다음 자리 표시자는 자동으로 대체됩니다:",
+    "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - 이 위키의 사이트 이름입니다.",
+    "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - 현재 페이지의 페이지 이름입니다.",
+    "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - 현재 페이지의 페이지 경로입니다.",
+    "custom_noscript": "사용자 지정 Noscript",
+    "custom_noscript_detail": "모든 페이지에 적용되는 Noscript 코드를 사용자 지정할 수 있습니다. 사용자 지정 Noscript는 body의 첫 번째 요소로 위치한 <code>&lt;noscript&gt;</code> 태그 안에 삽입됩니다.<br>변경 사항을 보려면 페이지를 다시 로드하십시오.",
+    "custom_css": "사용자 지정 CSS",
+    "write_css": "전체 시스템에 적용되는 CSS를 작성할 수 있습니다.",
+    "ctrl_space": "Ctrl+Space로 자동 완성",
+    "custom_script": "사용자 지정 스크립트",
+    "custom_presentation": "사용자 지정 프레젠테이션",
+    "write_java": "전체 시스템에 적용되는 Javascript를 작성할 수 있습니다.",
+    "reflect_change": "변경 사항을 반영하려면 페이지를 다시 로드해야 합니다.",
+    "custom_logo": "사용자 지정 로고",
+    "default_logo": "기본 로고",
+    "upload_logo": "로고 업로드",
+    "current_logo": "현재 로고",
+    "upload_new_logo": "새 로고 업로드",
+    "delete_logo": "로고 삭제"
+  },
+  "importer_management": {
+    "import_data": "데이터 가져오기",
+    "article": "문서",
+    "category": "카테고리",
+    "tag": "태그",
+    "page": "페이지",
+    "page_path": "페이지 경로",
+    "beta_warning": "이 기능은 베타입니다.",
+    "import_from": "{{from}}에서 가져오기",
+    "import_growi_archive": "GROWI 아카이브 가져오기",
+    "error": {
+      "only_upsert_available": "페이지 컬렉션에는 'Upsert' 옵션만 사용할 수 있습니다."
+    },
+    "growi_settings": {
+      "description_of_import_mode": {
+        "about": "기존 데이터와 이름이 같은 데이터를 가져올 때 다음 세 가지 모드 중 하나를 선택하십시오.",
+        "insert": "삽입: 데이터 가져오기를 건너뜁니다.",
+        "upsert": "업서트: 기존 데이터를 가져온 데이터로 덮어쓰고 업데이트합니다.",
+        "flash_and_insert": "플래시 및 삽입: 기존 데이터를 완전히 삭제한 후 데이터를 가져옵니다."
+      },
+      "growi_archive_file": "GROWI 아카이브 파일",
+      "uploaded_data": "업로드된 데이터",
+      "extracted_file": "추출된 파일",
+      "collection": "컬렉션",
+      "upload": "업로드",
+      "discard": "업로드된 데이터 버리기",
+      "errors": {
+        "different_versions": "이 GROWI 버전과 업로드된 데이터 버전이 다릅니다",
+        "at_least_one": "하나 이상의 컬렉션을 선택하십시오.",
+        "page_and_revision": "'페이지'와 '리비전'은 모두 가져와야 합니다.",
+        "depends": "'{{condition}}'이 선택된 경우 '{{target}}'을 선택해야 합니다."
+      },
+      "configuration": {
+        "pages": {
+          "overwrite_author": {
+            "label": "페이지 작성자를 현재 사용자로 덮어쓰기",
+            "desc": "사용자도 복원될 경우 이 옵션을 확인하지 않는 것이 좋습니다."
+          },
+          "set_public_to_page": {
+            "label": "'{{from}}'인 페이지를 '공개'로 설정",
+            "desc": "이 구성은 <b>'{{from}}'</b> 페이지를 <span class=\"text-danger\">모든 사용자</span>가 읽을 수 있도록 합니다."
+          },
+          "initialize_meta_datas": {
+            "label": "페이지의 좋아요, 읽은 사용자 및 댓글 수 초기화",
+            "desc": "사용자도 복원될 경우 이 옵션을 확인하지 않는 것이 좋습니다."
+          }
+        },
+        "revisions": {
+          "overwrite_author": {
+            "label": "리비전 작성자를 현재 사용자로 덮어쓰기",
+            "desc": "사용자도 복원될 경우 이 옵션을 확인하지 않는 것이 좋습니다."
+          }
+        }
+      }
+    },
+    "esa_settings": {
+      "team_name": "팀 이름",
+      "access_token": "액세스 토큰",
+      "test_connection": "esa 연결 테스트"
+    },
+    "qiita_settings": {
+      "team_name": "팀 이름",
+      "access_token": "액세스 토큰",
+      "test_connection": "qiita:team 연결 테스트"
+    },
+    "import": "가져오기",
+    "skip_username_and_email_when_overlapped": "새 환경에서 동일한 사용자 이름과 이메일을 사용하는 경우 사용자 이름과 이메일 건너뛰기",
+    "prepare_new_account_for_migration": "마이그레이션을 위한 새 계정 준비",
+    "archive_data_import_detail": "자세한 내용은 여기를 클릭하십시오.",
+    "admin_archive_data_import_guide_url": "https://docs.growi.org/en/admin-guide/management-cookbook/import.html",
+    "page_skip": "GROWI에 이미 존재하는 이름의 페이지는 가져오지 않습니다.",
+    "Directory_hierarchy_tag": "디렉토리 계층 태그"
+  },
+  "export_management": {
+    "export_archive_data": "아카이브 데이터 내보내기",
+    "exporting_collection_list": "내보내는 컬렉션 목록",
+    "exported_data_list": "내보낸 아카이브 데이터 목록",
+    "export_collections": "컬렉션 내보내기",
+    "check_all": "모두 선택",
+    "uncheck_all": "모두 선택 해제",
+    "desc_password_seed": "<p>사용자 데이터를 복원할 때 새 GROWI 시스템에 현재 <code>PASSWORD_SEED</code>를 설정하는 것을 잊지 마십시오. 그렇지 않으면 사용자가 비밀번호로 로그인할 수 없습니다.<br><br><strong>힌트:</strong><br>현재 <code>PASSWORD_SEED</code>는 내보낸 ZIP 파일의 <code>meta.json</code>에 저장됩니다.</p>",
+    "create_new_archive_data": "새 아카이브 데이터 생성",
+    "export": "내보내기",
+    "cancel": "취소",
+    "file": "파일",
+    "growi_version": "GROWI 버전",
+    "collections": "컬렉션",
+    "exported_at": "내보낸 시간",
+    "export_menu": "내보내기 메뉴",
+    "download": "다운로드",
+    "delete": "삭제"
+  },
+  "external_notification": {
+    "external_notification": "외부 알림",
+    "enabled": "활성화됨",
+    "disabled": "비활성화됨",
+    "header_status": "Slack 통합 상태",
+    "caution_enabled": "주의: 현재 이 페이지에서 구성된 알림은 기본으로 설정된 Slack 워크스페이스로만 전송됩니다."
+  },
+  "slack_integration": {
+    "slack_integration": "Slack 통합",
+    "selecting_bot_types": {
+      "slack_bot": "Slack 봇",
+      "official_bot": "공식 봇",
+      "custom_bot": "사용자 지정 봇",
+      "without_proxy": "프록시 없이",
+      "with_proxy": "프록시와 함께",
+      "recommended": "권장",
+      "set_up": "설정",
+      "multiple_workspaces_integration": "다중 워크스페이스 통합",
+      "security_control": "보안 제어",
+      "easy": "쉬움",
+      "normal": "보통",
+      "hard": "어려움",
+      "possible": "가능",
+      "impossible": "불가능"
+    },
+    "bot_reset_successful": "봇 설정이 재설정되었습니다.",
+    "adding_slack_ws_integration_settings_successful": "Slack 워크스페이스 통합 설정이 추가되었습니다.",
+    "bot_all_reset_successful": "모든 봇 설정이 재설정되었습니다.",
+    "copied_to_clipboard": "클립보드에 복사됨",
+    "set_scope": "Slack 설정에서 봇 토큰 범위를 설정하십시오.",
+    "modal": {
+      "warning": "경고",
+      "sure_change_bot_type": "봇 유형을 변경하시겠습니까?",
+      "changes_will_be_deleted": "다른 봇 유형에 대한 설정은 삭제됩니다.",
+      "cancel": "취소",
+      "change": "변경"
+    },
+    "toastr": {
+      "delete_slack_integration_procedure": "Slack 통합 절차 삭제 성공"
+    },
+    "use_env_var_if_empty": "데이터베이스 값이 비어 있으면 환경 변수 <code>{{variable}}</code>의 값이 사용됩니다.",
+    "access_token_settings": {
+      "regenerate": "재생성"
+    },
+    "delete": "삭제",
+    "integration_procedure": "통합 절차",
+    "custom_bot_without_proxy_settings": "프록시 없는 사용자 지정 봇 설정",
+    "integration_failed": "통합 실패",
+    "reset": "재설정",
+    "reset_all_settings": "모든 설정 재설정",
+    "delete_slackbot_settings": "Slack 봇 설정 삭제",
+    "slackbot_settings_notice": "Slack 워크스페이스 통합 절차가 삭제됩니다. <br> 확실합니까?",
+    "all_settings_of_the_bot_will_be_reset": "봇의 모든 설정이 재설정됩니다.<br>확실합니까?",
+    "accordion": {
+      "create_bot": "봇 생성",
+      "how_to_create_a_bot": "봇을 만드는 방법",
+      "how_to_install": "설치 방법",
+      "install_bot_to_slack": "Slack에 봇 설치",
+      "install_now": "지금 설치",
+      "generate_access_token": "액세스 토큰 생성",
+      "register_for_growi_official_bot_proxy_service": "GROWI 공식 봇 프록시 서비스 등록",
+      "register_for_growi_custom_bot_proxy": "GROWI 사용자 지정 봇 프록시 등록",
+      "enter_growi_register_on_slack": "Slack에서 <b>/growi register</b> 입력",
+      "paste_growi_url": "모달이 표시되면 <b>GROWI URL</b>에 다음 URL을 입력하십시오.",
+      "enter_access_token_for_growi_and_proxy": "<b>GROWI에 대한 액세스 토큰 프록시</b> 및 <b>프록시에 대한 GROWI 액세스 토큰</b> 입력",
+      "set_proxy_url_on_growi": "GROWI에 프록시 URL 설정",
+      "copy_proxy_url": "위 단계가 성공적으로 완료되면 선택한 Slack 채널의 모달에 프록시 URL이 표시되므로 복사하십시오.",
+      "enter_proxy_url_and_update": "이 페이지의 <b>사용자 지정 봇 프록시 통합</b>의 <b>프록시 URL</b>에 위 단계에서 복사한 프록시 URL을 입력하고 업데이트하십시오.",
+      "dont_need_update": "※값이 이미 있는 경우 업데이트할 필요가 없습니다.",
+      "select_install_your_app": "\"앱 설치\"를 선택하십시오.",
+      "go-to-manage-distribution": "Slack 앱 페이지에서 \"설정 관리\" > \"배포 관리\"로 이동하십시오.",
+      "activate-public-distribution": "\"다른 워크스페이스와 앱 공유\"에서 모든 항목이 선택되었는지 확인하고 \"공개 배포 활성화\"를 클릭하십시오.",
+      "click-add-to-slack-button": "\"Slack에 추가\" 버튼을 클릭하십시오.",
+      "select_install_to_workspace": "\"워크스페이스에 설치\"를 선택하십시오.",
+      "register_proxy_url": "GROWI에 프록시 URL 등록",
+      "click_allow": "\"허용\"을 선택하십시오.",
+      "install_complete_if_checked": "\"앱 설치\"가 선택되었는지 확인하십시오.",
+      "invite_bot_to_channel": "@example을 호출하여 GROWI 봇을 채널에 초대하십시오.",
+      "register_secret_and_token": "서명 시크릿 및 봇 토큰 설정",
+      "manage_permission": "권한 관리",
+      "growi_commands": "GROWI 명령",
+      "multiple_growi_command": "여러 GROWI 인스턴스에 한 번에 보낼 수 있는 명령",
+      "single_growi_command": "한 번에 단일 GROWI 인스턴스에 보낼 수 있는 명령",
+      "allowed_channels_description": "\"{{keyName}}\" 명령에 허용된 채널을 입력하십시오. 각 채널을 \",\"로 구분하십시오. 사용자는 여기에 작성된 채널에서 \"{{keyName}}\" 명령을 사용할 수 있습니다.",
+      "unfurl_description": "Slack에서 페이지 링크가 공유되었을 때 GROWI 페이지 콘텐츠 표시",
+      "unfurl_allowed_channels_description": "\"언퍼링\"에 허용된 채널 ID를 입력하십시오. 각 채널을 \",\"로 구분하십시오. 지정된 채널에서 전송된 GROWI 공개 페이지 링크 또는 영구 링크는 메시지에 콘텐츠를 표시합니다.",
+      "allow_all": "모두 허용",
+      "deny_all": "모두 거부",
+      "allow_specified": "지정된 항목 허용",
+      "allow_all_long": "모두 허용 (모든 채널에서 허용)",
+      "deny_all_long": "모두 거부 (모든 채널에서 거부)",
+      "allow_specified_long": "지정된 항목 허용 (지정된 채널에서만 허용)",
+      "test_connection": "연결 테스트",
+      "test_connection_by_pressing_button": "버튼을 눌러 연결 테스트",
+      "test_connection_only_public_channel": "공개 채널에서만 연결 테스트를 해주세요",
+      "error_check_logs_below": "오류가 발생했습니다. 아래 로그를 확인하십시오.",
+      "send_message_to_slack_work_space": "Slack 워크스페이스로 메시지 전송",
+      "add_slack_workspace": "Slack 워크스페이스 추가"
+    },
+    "custom_bot_without_proxy_integration": "프록시 없는 사용자 지정 봇 통합",
+    "integration_sentence": {
+      "integration_is_not_complete": "통합이 완료되지 않았습니다.<br>다음 통합 절차를 진행하십시오.",
+      "integration_successful": "통합 성공",
+      "integration_some_ws_is_not_complete": "일부 워크스페이스가 연결되지 않았습니다."
+    },
+    "custom_bot_with_proxy_integration": "프록시 있는 사용자 지정 봇 통합",
+    "official_bot_integration": "공식 봇 통합",
+    "docs_url": {
+      "slack_integration": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/",
+      "official_bot": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#official-bot-%E3%80%90recommended%E3%80%91",
+      "custom_bot_without_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-without-proxy",
+      "custom_bot_with_proxy": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/#custom-bot-with-proxy",
+      "official_bot_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/official-bot-settings.html",
+      "custom_bot_without_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-without-proxy-settings.html",
+      "custom_bot_with_proxy_setting": "https://docs.growi.org/en/admin-guide/management-cookbook/slack-integration/custom-bot-with-proxy-settings.html"
+    }
+  },
+  "slack_integration_legacy": {
+    "slack_integration_legacy": "레거시 Slack 통합",
+    "alert_disabled": "이 'Slack 레거시 통합'은 <a href='/admin/slack-integration'>새 설정</a>이 활성화되어 현재 비활성화되었습니다.",
+    "alert_deplicated": "이 '레거시 Slack 통합'은 오래되었으며 향후 중단될 예정입니다. 대신 <a href='/admin/slack-integration'>새 설정</a>을 사용하십시오."
+  },
+  "user_management": {
+    "user_management": "사용자 관리",
+    "invite_users": "새 사용자 임시 발급",
+    "click_twice_same_checkbox": "최소한 하나의 확인란을 선택해야 합니다.",
+    "status": "상태",
+    "invite_modal": {
+      "emails": "이메일 (새 줄로 여러 명 발급 가능)",
+      "description1": "이메일 주소로 새 사용자를 임시 발급합니다.",
+      "description2": "첫 로그인 시 임시 비밀번호가 생성됩니다.",
+      "invite_thru_email": "초대 이메일 전송",
+      "mail_setting_link": "<span className='material-symbols-outlined me-2'>settings</span><a href='/admin/app'>이메일 설정</a>",
+      "valid_email": "유효한 이메일 주소가 필요합니다.",
+      "temporary_password": "생성된 사용자에게는 임시 비밀번호가 있습니다.",
+      "send_new_password": "새 비밀번호를 사용자에게 보내주십시오.",
+      "send_temporary_password": "초대 이메일을 보내지 않은 경우, 이 화면에서 임시 비밀번호를 복사하여 초대자에게 연락하십시오.",
+      "send_email": "사용자 테이블의 드롭다운에서 초대 이메일을 보내거나 다시 보낼 수도 있습니다.",
+      "existing_email": "다음 이메일은 이미 존재합니다.",
+      "issue": "발급"
+    },
+    "user_table": {
+      "administrator": "관리자",
+      "read_only": "읽기 전용",
+      "edit_menu": "편집 메뉴",
+      "reset_password": "비밀번호 재설정",
+      "administrator_menu": "관리자 메뉴",
+      "accept": "수락",
+      "deactivate_account": "계정 비활성화",
+      "your_own": "자신의 계정을 비활성화할 수 없습니다.",
+      "revoke_admin_access": "관리자 권한 취소",
+      "cannot_revoke": "자신에게서 관리자 권한을 취소할 수 없습니다.",
+      "grant_admin_access": "관리자 권한 부여",
+      "revoke_read_only_access": "읽기 전용 권한 취소",
+      "grant_read_only_access": "읽기 전용 권한 부여",
+      "send_invitation_email": "초대 이메일 전송",
+      "resend_invitation_email": "초대 이메일 재전송"
+    },
+    "reset_password": "비밀번호 재설정",
+    "reset_password_modal": {
+      "password_never_seen": "이 화면이 닫히면 임시 비밀번호는 다시 검색할 수 없습니다.",
+      "password_reset_message": "아래의 새 비밀번호를 사용자에게 알리고 즉시 다른 비밀번호로 변경하도록 강력히 권장하십시오.",
+      "send_new_password": "새 비밀번호를 사용자에게 보내주십시오.",
+      "target_user": "대상 사용자",
+      "new_password": "새 비밀번호"
+    },
+    "external_account": "외부 계정 관리",
+    "external_accounts": "외부 계정",
+    "create_external_account": "외부 계정 생성",
+    "external_account_list": "외부 계정 목록",
+    "external_account_none": "외부 계정 없음",
+    "invite": "초대",
+    "invited": "사용자가 초대되었습니다.",
+    "back_to_user_management": "사용자 관리로 돌아가기",
+    "authentication_provider": "인증 제공자",
+    "manage": "관리",
+    "password_setting": "비밀번호 설정",
+    "password_setting_help": "비밀번호가 설정되었습니까?",
+    "set": "예",
+    "unset": "아니요",
+    "related_username": "관련 사용자 ",
+    "cannot_invite_maximum_users": "최대 사용자 수 이상을 초대할 수 없습니다.",
+    "current_users": "현재 사용자:"
+  },
+  "user_group_management": {
+    "user_group_management": "사용자 그룹 관리",
+    "create_group": "새 그룹 생성",
+    "add_child_group": "하위 그룹 추가",
+    "remove_child_group": "제거",
+    "deny_create_group": "현재 설정으로는 새 그룹을 생성할 수 없습니다.",
+    "group_name": "그룹 이름",
+    "group_example": "예: 그룹1",
+    "child_user_group": "하위 사용자 그룹",
+    "parent_group": "상위 그룹",
+    "select_parent_group": "상위 그룹 선택",
+    "release_parent_group": "상위 그룹 해제",
+    "add_modal": {
+      "description": "추가된 사용자는 모든 상위 그룹에도 추가됩니다.",
+      "add_user": "생성된 그룹에 사용자 추가",
+      "search_option": "검색 옵션",
+      "enable_option": "{{option}} 활성화",
+      "forward_match": "정방향 일치",
+      "partial_match": "부분 일치",
+      "backward_match": "역방향 일치"
+    },
+    "group_list": "그룹 목록",
+    "child_group_list": "하위 그룹 목록",
+    "back_to_list": "그룹 목록으로 돌아가기",
+    "basic_info": "기본 정보",
+    "user_list": "사용자 목록",
+    "created_group": "그룹이 생성되었습니다.",
+    "is_loading_data": "데이터 가져오는 중...",
+    "no_pages": "그룹에 보기 권한이 있는 페이지가 없습니다.",
+    "remove_from_group": "이 사용자 제거",
+    "delete_modal": {
+      "header": "그룹 삭제",
+      "desc": "그룹 아래의 모든 하위 그룹도 삭제됩니다. 한 번 삭제되면 삭제된 그룹과 해당 비공개 페이지는 검색할 수 없습니다.",
+      "dropdown_desc": "비공개 페이지에 대한 작업 선택",
+      "select_group": "그룹 선택",
+      "no_groups": "선택할 그룹 없음",
+      "publish_pages": "게시 가능한 페이지 게시",
+      "delete_pages": "모두 삭제",
+      "transfer_pages": "다른 그룹으로 전송",
+      "option_explanation": "\"게시 가능한\" 페이지는 삭제하려는 그룹에만 보이는 페이지입니다. 다른 그룹이 볼 수 있는 페이지는 게시되지 않습니다."
+    },
+    "update_parent_confirm_modal": {
+      "header": "그룹의 상위가 변경됩니다.",
+      "caution_change_parent": "이 작업은 그룹 \"{{groupName}}\"의 상위를 변경합니다.",
+      "danger_message": "이것이 이 그룹과 관련된 모든 페이지의 보기 권한에 영향을 미친다는 점에 유의하십시오.",
+      "force_update_parents_label": "누락된 사용자 강제 추가",
+      "force_update_parents_description": "상위 그룹을 변경한 후 누락된 사용자가 존재하는 경우 상위 그룹에 강제로 추가하려면 이 옵션을 활성화하십시오."
+    }
+  },
+  "audit_log_management": {
+    "audit_log": "감사 로그",
+    "audit_log_settings": "감사 로그 설정",
+    "user": "사용자",
+    "username": "사용자 이름",
+    "date": "날짜",
+    "action": "작업",
+    "ip": "IP 주소",
+    "url": "URL",
+    "settings": "설정",
+    "return": "돌아가기",
+    "clear": "지우기",
+    "activity_expiration_date": "감사 로그 만료일",
+    "activity_expiration_date_explanation": "생성된 감사 로그는 환경 변수에 설정된 시간(초)이 지나면 생성 시간부터 자동으로 삭제됩니다.",
+    "fixed_by_env_var": "이것은 환경 변수 <code>{{key}}={{value}}</code>에 의해 고정됩니다.",
+    "available_action_list": "모든 사용 가능한 작업 검색 / 보기",
+    "available_action_list_explanation": "현재 설정에서 검색/볼 수 있는 작업 목록",
+    "action_list": "작업 목록",
+    "disable_mode_explanation": "감사 로그가 현재 비활성화되어 있습니다. 활성화하려면 환경 변수 <code>AUDIT_LOG_ENABLED</code>를 true로 설정하십시오.",
+    "docs_url": {
+      "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
+    }
+  },
+  "g2g_data_transfer": {
+    "transfer_data_to_another_growi": "이 GROWI에서 다른 GROWI로 데이터 전송",
+    "advanced_options": "고급 옵션",
+    "start_transfer": "전송 시작",
+    "paste_transfer_key": "여기에 전송 키 붙여넣기"
+  },
+  "plugins": {
+    "plugins": "플러그인",
+    "plugin_installer": "플러그인 설치 프로그램",
+    "form": {
+      "label_url": "저장소 URL",
+      "desc_url": "URL을 입력하여 플러그인을 설치할 수 있습니다.",
+      "label_branch": "저장소 브랜치 이름",
+      "desc_branch": "설치할 브랜치 이름을 지정할 수 있습니다. 기본값: `main`"
+    },
+    "plugin_card": "플러그인 카드",
+    "plugin_is_not_installed": "플러그인이 설치되지 않았습니다.",
+    "install": "설치",
+    "confirm": "플러그인 삭제?"
+  },
+  "cloud_setting_management": {
+    "to_cloud_settings": "GROWI.cloud 설정 열기"
+  },
+  "audit_log_action_category": {
+    "Page": "페이지",
+    "Comment": "댓글",
+    "Tag": "태그",
+    "Attachment": "첨부 파일",
+    "ShareLink": "공유 링크",
+    "Search": "검색",
+    "User": "사용자",
+    "Admin": "관리자"
+  },
+  "audit_log_action": {
+    "USER_REGISTRATION_SUCCESS": "사용자 생성",
+    "USER_LOGIN_WITH_LOCAL": "ID/비밀번호로 로그인",
+    "USER_LOGIN_WITH_LDAP": "LDAP으로 로그인",
+    "USER_LOGIN_WITH_GOOGLE": "Google로 로그인",
+    "USER_LOGIN_WITH_GITHUB": "GitHub으로 로그인",
+    "USER_LOGIN_WITH_OIDC": "OIDC로 로그인",
+    "USER_LOGIN_WITH_SAML": "SAML로 로그인",
+    "USER_LOGIN_FAILURE": "로그인 실패",
+    "USER_LOGOUT": "로그아웃",
+    "USER_FOGOT_PASSWORD": "비밀번호 재설정 요청",
+    "USER_RESET_PASSWORD": "비밀번호 재설정",
+    "USER_PERSONAL_SETTINGS_UPDATE": "사용자 개인 설정 업데이트",
+    "USER_IMAGE_TYPE_UPDATE": "사용자 이미지 유형 업데이트",
+    "USER_LDAP_ACCOUNT_ASSOCIATE": "LDAP 계정 연결",
+    "USER_LDAP_ACCOUNT_DISCONNECT": "LDAP 계정 연결 해제",
+    "USER_PASSWORD_UPDATE": "비밀번호 업데이트",
+    "USER_API_TOKEN_UPDATE": "API 토큰 업데이트",
+    "USER_EDITOR_SETTINGS_UPDATE": "편집기 설정 업데이트",
+    "USER_IN_APP_NOTIFICATION_SETTINGS_UPDATE": "앱 내 알림 설정 업데이트",
+    "USER_REGISTRATION_APPROVAL_REQUEST": "ID/비밀번호 인증을 위한 사용자 등록 요청",
+    "PAGE_VIEW": "페이지 보기",
+    "PAGE_USER_HOME_VIEW": "페이지 보기 (사용자 홈)",
+    "PAGE_FORBIDDEN": "페이지 보기 (금지된 페이지)",
+    "PAGE_NOT_FOUND": "페이지 보기 (찾을 수 없는 페이지)",
+    "PAGE_NOT_CREATABLE": "페이지 보기 (생성할 수 없는 페이지)",
+    "PAGE_LIKE": "좋아요",
+    "PAGE_UNLIKE": "좋아요 취소",
+    "PAGE_BOOKMARK": "북마크",
+    "PAGE_UNBOOKMARK": "북마크 해제",
+    "PAGE_CREATE": "페이지 생성",
+    "PAGE_UPDATE": "페이지 업데이트",
+    "PAGE_RENAME": "페이지 이름 변경",
+    "PAGE_DUPLICATE": "페이지 복제",
+    "PAGE_DELETE": "페이지 삭제",
+    "PAGE_DELETE_COMPLETELY": "페이지 완전 삭제",
+    "PAGE_REVERT": "페이지 되돌리기",
+    "PAGE_EMPTY_TRASH": "휴지통 비우기",
+    "PAGE_RECURSIVELY_RENAME": "재귀적 페이지 이름 변경",
+    "PAGE_RECURSIVELY_DELETE": "재귀적 페이지 삭제",
+    "PAGE_RECURSIVELY_DELETE_COMPLETELY": "재귀적 페이지 완전 삭제",
+    "PAGE_RECURSIVELY_REVERT": "재귀적 페이지 되돌리기",
+    "PAGE_SUBSCRIBE": "페이지 구독",
+    "PAGE_UNSUBSCRIBE": "페이지 구독 취소",
+    "PAGE_EXPORT": "페이지 내보내기",
+    "TAG_UPDATE": "태그 업데이트",
+    "IN_APP_NOTIFICATION_ALL_STATUSES_OPEN": "모든 앱 내 알림 읽음",
+    "COMMENT_CREATE": "댓글 생성",
+    "COMMENT_UPDATE": "댓글 업데이트",
+    "COMMENT_REMOVE": "댓글 제거",
+    "SHARE_LINK_CREATE": "공유 링크 생성",
+    "SHARE_LINK_DELETE": "공유 링크 삭제",
+    "SHARE_LINK_DELETE_BY_PAGE": "페이지의 모든 공유 링크 제거",
+    "SHARE_LINK_ALL_DELETE": "모든 공유 링크 삭제",
+    "SHARE_LINK_PAGE_VIEW": "페이지 보기(공유 링크)",
+    "SHARE_LINK_EXPIRED_PAGE_VIEW": "페이지 보기(만료된 공유 링크)",
+    "SHARE_LINK_NOT_FOUND": "페이지 보기 (공유 링크를 찾을 수 없음)",
+    "ATTACHMENT_ADD": "첨부 파일 추가",
+    "ATTACHMENT_REMOVE": "첨부 파일 제거",
+    "ATTACHMENT_DOWNLOAD": "첨부 파일 다운로드",
+    "SEARCH_PAGE": "페이지 검색",
+    "SEARCH_PAGE_VIEW": "페이지 보기(검색 결과 페이지)",
+    "ADMIN_APP_SETTING_UPDATE": "앱 설정 업데이트",
+    "ADMIN_SITE_URL_UPDATE": "사이트 URL 설정 업데이트",
+    "ADMIN_MAIL_SMTP_UPDATE": "이메일(SMTP) 설정 업데이트",
+    "ADMIN_MAIL_SES_UPDATE": "이메일(SES) 설정 업데이트",
+    "ADMIN_MAIL_TEST_SUBMIT": "테스트 메일 전송",
+    "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "파일 업로드 설정 업데이트",
+    "ADMIN_PLUGIN_UPDATE": "플러그인 설정 업데이트",
+    "ADMIN_MAINTENANCEMODE_ENABLED": "유지 보수 모드 활성화",
+    "ADMIN_MAINTENANCEMODE_DISABLED": "유지 보수 모드 비활성화",
+    "ADMIN_SECURITY_SETTINGS_UPDATE": "보안 설정 업데이트",
+    "ADMIN_PERMIT_SHARE_LINK": "공유 링크 활성화",
+    "ADMIN_REJECT_SHARE_LINK": "공유 링크 비활성화",
+    "ADMIN_AUTH_ID_PASS_ENABLED": "ID/비밀번호 인증 활성화",
+    "ADMIN_AUTH_ID_PASS_DISABLED": "ID/비밀번호 인증 비활성화",
+    "ADMIN_AUTH_ID_PASS_UPDATE": "ID/비밀번호 인증 설정 업데이트",
+    "ADMIN_AUTH_LDAP_ENABLED": "LDAP 인증 활성화",
+    "ADMIN_AUTH_LDAP_DISABLED": "LDAP 인증 비활성화",
+    "ADMIN_AUTH_LDAP_UPDATE": "LDAP 인증 설정 업데이트",
+    "ADMIN_AUTH_SAML_ENABLED": "SAML 인증 활성화",
+    "ADMIN_AUTH_SAML_DISABLED": "SAML 인증 비활성화",
+    "ADMIN_AUTH_SAML_UPDATE": "SAML 인증 설정 업데이트",
+    "ADMIN_AUTH_OIDC_ENABLED": "OIDC 인증 활성화",
+    "ADMIN_AUTH_OIDC_DISABLED": "OIDC 인증 비활성화",
+    "ADMIN_AUTH_OIDC_UPDATE": "OIDC 설정 업데이트",
+    "ADMIN_AUTH_GOOGLE_ENABLED": "Google 인증 활성화",
+    "ADMIN_AUTH_GOOGLE_DISABLED": "Google 인증 비활성화",
+    "ADMIN_AUTH_GOOGLE_UPDATE": "Google 인증 설정 업데이트",
+    "ADMIN_AUTH_GITHUB_ENABLED": "GitHub 인증 활성화",
+    "ADMIN_AUTH_GITHUB_DISABLED": "GitHub 인증 비활성화",
+    "ADMIN_AUTH_GITHUB_UPDATE": "GitHub 인증 설정 업데이트",
+    "ADMIN_MARKDOWN_LINE_BREAK_UPDATE": "줄 바꿈 설정 업데이트",
+    "ADMIN_MARKDOWN_INDENT_UPDATE": "들여쓰기 설정 업데이트",
+    "ADMIN_MARKDOWN_PRESENTATION_UPDATE": "프레젠테이션 설정 업데이트",
+    "ADMIN_MARKDOWN_XSS_UPDATE": "XSS 방지 설정 업데이트",
+    "ADMIN_LAYOUT_UPDATE": "레이아웃 업데이트",
+    "ADMIN_THEME_UPDATE": "테마 업데이트",
+    "ADMIN_SIDEBAR_UPDATE": "기본 사이드바 모드 업데이트",
+    "ADMIN_FUNCTION_UPDATE": "기능 업데이트",
+    "ADMIN_CODE_HIGHLIGHT_UPDATE": "코드 하이라이트 업데이트",
+    "ADMIN_CUSTOM_TITLE_UPDATE": "사용자 지정 제목 업데이트",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "사용자 지정 noscript 업데이트",
+    "ADMIN_CUSTOM_CSS_UPDATE": "사용자 지정 CSS 업데이트",
+    "ADMIN_CUSTOM_SCRIPT_UPDATE": "사용자 지정 스크립트 업데이트",
+    "ADMIN_ARCHIVE_DATA_UPLOAD": "아카이브 데이터 업로드",
+    "ADMIN_GROWI_DATA_IMPORTED": "아카이브 데이터 가져오기",
+    "ADMIN_UPLOADED_GROWI_DATA_DISCARDED": "업로드된 GROWI 데이터 버리기",
+    "ADMIN_ESA_DATA_IMPORTED": "esa.io에서 가져오기",
+    "ADMIN_ESA_DATA_UPDATED": "esa.io 가져오기 설정 업데이트",
+    "ADMIN_CONNECTION_TEST_OF_ESA_DATA": "esa 연결 테스트",
+    "ADMIN_QIITA_DATA_IMPORTED": "Qiita:Team에서 가져오기",
+    "ADMIN_QIITA_DATA_UPDATED": "Qiita:Team 가져오기 설정 업데이트",
+    "ADMIN_CONNECTION_TEST_OF_QIITA_DATA": "Qiita:Team 연결 테스트",
+    "ADMIN_ARCHIVE_DATA_CREATE": "아카이브 데이터 생성",
+    "ADMIN_ARCHIVE_DATA_DOWNLOAD": "아카이브 데이터 다운로드",
+    "ADMIN_ARCHIVE_DATA_DELETE": "아카이브 데이터 삭제",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_ADD": "사용자 트리거 알림 설정 추가",
+    "ADMIN_USER_NOTIFICATION_SETTINGS_DELETE": "사용자 트리거 알림 설정 삭제",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD": "전역 알림 설정 추가",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_UPDATE": "전역 알림 설정 업데이트",
+    "ADMIN_NOTIFICATION_GRANT_SETTINGS_UPDATE": "전역 알림 권한 업데이트",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ENABLED": "전역 알림 설정 활성화",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DISABLED": "전역 알림 설정 비활성화",
+    "ADMIN_GLOBAL_NOTIFICATION_SETTINGS_DELETE": "전역 알림 설정 삭제",
+    "ADMIN_SLACK_WORKSPACE_CREATE": "Slack 워크스페이스 추가",
+    "ADMIN_SLACK_WORKSPACE_DELETE": "Slack 워크스페이스 삭제",
+    "ADMIN_SLACK_BOT_TYPE_UPDATE": "Slack 봇 유형 변경",
+    "ADMIN_SLACK_BOT_TYPE_DELETE": "Slack 봇 유형 삭제",
+    "ADMIN_SLACK_ACCESS_TOKEN_REGENERATE": "Slack 액세스 토큰 재생성",
+    "ADMIN_SLACK_MAKE_APP_PRIMARY": "Slack 봇을 기본으로 설정",
+    "ADMIN_SLACK_PERMISSION_UPDATE": "Slack 봇 권한 업데이트",
+    "ADMIN_SLACK_PROXY_URI_UPDATE": "프록시 있는 사용자 지정 봇의 프록시 URL 업데이트",
+    "ADMIN_SLACK_RELATION_TEST": "Slack 봇 연결 테스트",
+    "ADMIN_SLACK_WITHOUT_PROXY_SETTINGS_UPDATE": "프록시 없는 Slack 봇 설정 업데이트",
+    "ADMIN_SLACK_WITHOUT_PROXY_PERMISSION_UPDATE": "프록시 없는 Slack 봇 권한 업데이트",
+    "ADMIN_SLACK_WITHOUT_PROXY_TEST": "프록시 없는 Slack 봇 연결 테스트",
+    "ADMIN_SLACK_CONFIGURATION_SETTING_UPDATE": "Slack 수신 웹훅 구성 설정 업데이트",
+    "ADMIN_USERS_INVITE": "사용자 초대",
+    "ADMIN_USERS_PASSWORD_RESET": "사용자 비밀번호 재설정",
+    "ADMIN_USERS_ACTIVATE": "사용자 활성화",
+    "ADMIN_USERS_DEACTIVATE": "사용자 비활성화",
+    "ADMIN_USERS_GRANT_ADMIN": "관리자 권한 부여",
+    "ADMIN_USERS_REVOKE_ADMIN": "관리자 권한 취소",
+    "ADMIN_USERS_GRANT_READ_ONLY": "읽기 전용 권한 부여",
+    "ADMIN_USERS_REVOKE_READ_ONLY": "읽기 전용 권한 취소",
+    "ADMIN_USERS_SEND_INVITATION_EMAIL": "초대 이메일 재전송",
+    "ADMIN_USERS_REMOVE": "사용자 제거",
+    "ADMIN_USER_GROUP_CREATE": "사용자 그룹 생성",
+    "ADMIN_USER_GROUP_UPDATE": "사용자 그룹 업데이트",
+    "ADMIN_USER_GROUP_DELETE": "사용자 그룹 삭제",
+    "ADMIN_USER_GROUP_ADD_USER": "사용자 그룹에 사용자 추가",
+    "ADMIN_SEARCH_CONNECTION": "Elasticsearch에 다시 연결 시도",
+    "ADMIN_SEARCH_INDICES_NORMALIZE": "Elasticsearch 인덱스 정규화",
+    "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch 인덱스 재구축",
+    "ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE": "페이지 대량 내보내기 설정 업데이트"
+  },
+  "g2g": {
+    "transfer_success": "GROWI 간 전송 성공",
+    "error_generate_growi_archive": "GROWI 아카이브 파일 생성 실패",
+    "error_send_growi_archive": "대상 GROWI로 GROWI 아카이브 파일 전송 실패"
+  },
+  "external_user_group": {
+    "management": "외부 그룹 관리",
+    "execute_sync": "동기화 실행",
+    "sync": "동기화",
+    "invalid_sync_settings": "잘못된 동기화 설정",
+    "update_sync_settings_failed": "동기화 설정 업데이트 실패",
+    "description_form_detail": "설명 매퍼가 동기화 설정에 설정된 경우 다음 동기화 시 편집된 값이 덮어쓰여질 수 있으므로 유의하십시오.",
+    "only_description_edit_allowed": "외부 사용자 그룹의 설명만 편집할 수 있습니다.",
+    "sync_being_executed": "사용자 또는 다른 사용자가 시작한 외부 그룹 동기화 프로세스가 실행 중입니다. 이 프로세스가 완료될 때까지 다음 동기화를 실행할 수 없습니다.",
+    "sync_succeeded": "외부 그룹 동기화 성공",
+    "sync_failed": "외부 그룹 동기화 실패",
+    "provider": "제공자",
+    "confirmation_before_sync": "동기화 전 확인",
+    "execution_time_warning": "그룹 또는 사용자 수가 많으면 동기화가 완료될 때까지 시간이 걸릴 수 있습니다.",
+    "parallel_sync_forbidden": "동기화가 실행 중인 동안에는 다른 외부 그룹 동기화를 실행할 수 없습니다.",
+    "ldap": {
+      "group_sync_settings": "LDAP 그룹 동기화 설정",
+      "group_search_base_DN": "그룹 검색 기본 DN",
+      "group_search_base_dn_detail": "그룹 검색을 위한 기본 DN입니다. 여기에 설정되지 않으면 보안 설정에 설정된 값이 사용됩니다.",
+      "membership_attribute": "멤버십 속성",
+      "membership_attribute_detail": "사용자 멤버십 정보를 나타내는 그룹 개체의 속성",
+      "membership_attribute_type": "멤버십 속성 유형",
+      "membership_attribute_type_detail": "멤버십 속성 값이 DN 또는 UID 유형인지 여부",
+      "child_group_attribute": "하위 그룹 속성",
+      "child_group_attribute_detail": "하위 그룹 정보를 나타내는 그룹 개체의 속성입니다. 속성 값은 하위 그룹의 DN이어야 합니다.",
+      "preserve_deleted_ldap_groups": "삭제된 LDAP 그룹 유지",
+      "name_mapper_detail": "그룹 이름으로 매핑할 속성",
+      "updated_group_sync_settings": "LDAP 그룹 동기화 설정 업데이트",
+      "password": "비밀번호",
+      "password_detail": "바인딩 유형이 사용자 바인딩으로 설정되어 있으므로 로그인 비밀번호가 필요합니다.",
+      "auth_not_set": "동기화 전에 보안 설정에서 LDAP 인증을 활성화하고 구성하십시오."
+    },
+    "keycloak": {
+      "group_sync_settings": "Keycloak 그룹 동기화 설정",
+      "host": "호스트",
+      "host_detail": "Keycloak 호스트 URL",
+      "group_realm": "그룹 영역",
+      "group_realm_detail": "동기화할 그룹을 포함하는 영역",
+      "group_sync_client_realm": "관리 API 요청에 사용되는 클라이언트 영역",
+      "group_sync_client_realm_detail": "Keycloak 관리 API에 요청을 인증하는 데 사용되는 클라이언트를 포함하는 영역",
+      "group_sync_client_id": "클라이언트 ID",
+      "group_sync_client_id_detail": "Keycloak 관리 API에 요청을 인증하는 데 사용되는 클라이언트 ID",
+      "group_sync_client_secret": "클라이언트 시크릿",
+      "group_sync_client_secret_detail": "Keycloak 관리 API에 요청을 인증하는 데 사용되는 시크릿 ID",
+      "updated_group_sync_settings": "Keycloak 그룹 동기화 설정 업데이트",
+      "preserve_deleted_keycloak_groups": "삭제된 Keycloak 그룹 유지",
+      "auth_not_set": "그룹 동기화 설정의 '호스트' 및 '그룹 영역'을 포함하는 OIDC 또는 SAML 호스트를 활성화하십시오."
+    },
+    "auto_generate_user_on_sync": "동기화 시 사용자 자동 생성",
+    "description_mapper_detail": "그룹 설명으로 매핑할 속성입니다. 설명은 동기화 후 편집할 수 있습니다. 그러나 매퍼가 설정된 경우 편집된 값이 다음 동기화 시 덮어쓰여질 수 있습니다."
+  },
+  "toaster": {
+    "grant_user_admin": "{{username}}에게 관리자 권한 부여 성공",
+    "revoke_user_admin": "{{username}}의 관리자 권한 취소 성공",
+    "grant_user_read_only": "{{username}}에게 읽기 전용 권한 부여 성공",
+    "revoke_user_read_only": "{{username}}의 읽기 전용 권한 취소 성공",
+    "activate_user_success": "{{username}} 활성화 성공",
+    "deactivate_user_success": "{{username}} 비활성화 성공",
+    "remove_user_success": "{{username}} 제거 성공",
+    "remove_external_user_success": "{{accountId}} 제거 성공",
+    "switch_disable_link_sharing_success": "공유 링크 설정 업데이트 성공",
+    "install_plugin_success": "{{pluginName}} 설치 성공",
+    "activate_plugin_success": "{{pluginName}} 활성화 성공",
+    "deactivate_plugin_success": "{{pluginName}} 비활성화 성공",
+    "remove_plugin_success": "{{pluginName}} 제거 성공"
+  },
+  "forbidden_page": {
+    "do_not_have_admin_permission": "관리자 권한이 없는 사용자는 관리 화면에 접근할 수 없습니다."
+  },
+  "ai_integration": {
+    "ai_integration": "AI 통합",
+    "disable_mode_explanation": "현재 AI 통합이 비활성화되어 있습니다. 활성화하려면 <code>AI_ENABLED</code> 환경 변수와 필요한 추가 변수를 구성하십시오.<br><br>자세한 내용은 <a target='blank' rel='noopener noreferrer' href={{documentationUrl}}en/guide/features/ai-knowledge-assistant.html>문서</a>를 참조하십시오.",
+    "ai_search_management": "AI 검색 관리"
+  }
+}

+ 127 - 0
apps/app/public/static/locales/ko_KR/commons.json

@@ -0,0 +1,127 @@
+{
+  "Show": "표시",
+  "Hide": "숨기기",
+  "Add": "추가",
+  "Insert": "삽입",
+  "Reset": "재설정",
+  "Sign out": "로그아웃",
+  "New": "새로 만들기",
+  "Delete": "삭제",
+  "meta": {
+    "display_name": "한국어"
+  },
+  "toaster": {
+    "add_succeeded": "{{target}} 추가 성공",
+    "create_failed": "{{target}} 생성 실패",
+    "create_succeeded": "{{target}} 생성 성공",
+    "delete_succeeded": "{{target}} 삭제 성공",
+    "remove_share_link": "{{count}}개 공유 링크 제거 성공",
+    "remove_share_link_success": "{{shareLinkId}} 제거 성공",
+    "update_failed": "{{target}} 업데이트 실패",
+    "update_successed": "{{target}} 업데이트 성공"
+  },
+  "alert": {
+    "siteUrl_is_not_set": "'사이트 URL'이 설정되지 않았습니다. {{link}}에서 설정하십시오.",
+    "please_enable_mailer": "먼저 메일러를 설정하십시오.",
+    "password_reset_please_enable_mailer": "먼저 메일러를 설정하십시오.",
+    "email_is_already_in_use": "이 이메일 주소는 이미 사용 중입니다."
+  },
+  "headers": {
+    "app_settings": "앱 설정"
+  },
+  "header_search_box": {
+    "label": {
+      "All pages": "모든 페이지",
+      "This tree": "이 트리"
+    },
+    "item_label": {
+      "All pages": "모든 페이지",
+      "This tree": "이 트리의 하위 항목만"
+    }
+  },
+  "search_method_menu_item": {
+    "search_in_all": "모두 검색",
+    "only_children_of_this_tree": "이 트리의 하위 항목만",
+    "exact_mutch": "정확히 일치"
+  },
+  "share_links": {
+    "Share Link": "공유 링크",
+    "Page Path": "페이지 경로",
+    "expire": "만료",
+    "description": "설명"
+  },
+  "in_app_notification": {
+    "notification_list": "앱 내 알림 목록",
+    "see_all": "모두 보기",
+    "no_notification": "알림이 없습니다.",
+    "all": "모두",
+    "unopend": "읽지 않음",
+    "mark_all_as_read": "모두 읽음으로 표시",
+    "no_unread_messages": "읽지 않은 메시지 없음",
+    "only_unread": "읽지 않은 메시지만"
+  },
+  "personal_dropdown": {
+    "home": "홈",
+    "settings": "설정",
+    "color_mode": "색상 모드",
+    "sidebar_mode": "사이드바 모드",
+    "sidebar_mode_editor": "편집기 사이드바 모드",
+    "use_os_settings": "OS 설정 사용",
+    "feedback": "피드백"
+  },
+  "create_page_dropdown": {
+    "new_page": "새 페이지 생성",
+    "open_page_create_modal": "새 페이지 생성 모달 열기",
+    "todays": {
+      "desc": "오늘의 메모 생성",
+      "memo": "메모"
+    },
+    "template": {
+      "desc": "템플릿 페이지 생성/편집",
+      "children": "하위 항목용 템플릿",
+      "descendants": "하위 항목용 템플릿"
+    }
+  },
+  "copy_to_clipboard": {
+    "Copy to clipboard": "클립보드에 복사",
+    "Page path": "페이지 경로",
+    "Page URL": "페이지 URL",
+    "Permanent link": "영구 링크",
+    "Page path and permanent link": "페이지 경로 및 영구 링크",
+    "Markdown link": "마크다운 링크",
+    "Append params": "매개변수 추가"
+  },
+  "crop_image_modal": {
+    "image_crop": "이미지 자르기",
+    "crop": "자르기",
+    "save": "저장",
+    "cancel": "취소"
+  },
+  "handsontable_modal": {
+    "title": "테이블 편집",
+    "data_import": "데이터 가져오기",
+    "save": "저장",
+    "cancel": "취소",
+    "done": "완료",
+    "data_import_form": {
+      "select_data_format": "데이터 형식 선택",
+      "import_data": "데이터 가져오기",
+      "paste_table_data": "테이블 데이터 붙여넣기",
+      "parse_error": "구문 분석 오류",
+      "cancel": "취소",
+      "import": "가져오기"
+    }
+  },
+  "not_found_page": {
+    "page_not_exist": "이 페이지는 존재하지 않습니다."
+  },
+  "g2g_data_transfer": {
+    "tab": "데이터 전송",
+    "data_transfer": "데이터 전송",
+    "transfer_data_to_this_growi": "다른 GROWI에서 이 GROWI로 데이터 전송",
+    "publish_transfer_key": "전송 키 게시",
+    "transfer_key_limit": "전송 키는 발급 후 1시간 동안 유효합니다.",
+    "once_transfer_key_used": "전송 키가 전송에 사용되면 다른 전송에는 사용할 수 없습니다.",
+    "transfer_to_growi_cloud": "자세한 내용은 <a href='{{documentationUrl}}en/admin-guide/management-cookbook/g2g-transfer.html'>여기를 클릭하십시오.</a>"
+  }
+}

+ 1021 - 0
apps/app/public/static/locales/ko_KR/translation.json

@@ -0,0 +1,1021 @@
+{
+  "meta": {
+    "display_name": "한국어"
+  },
+  "Help": "도움말",
+  "view": "보기",
+  "Edit": "편집",
+  "Delete": "삭제",
+  "delete_all": "모두 삭제",
+  "Duplicate": "복제",
+  "PathRecovery": "경로 복구",
+  "Copy": "복사",
+  "preview": "미리보기",
+  "desktop": "데스크톱",
+  "phone": "스마트폰",
+  "tablet": "태블릿",
+  "Click to copy": "클릭하여 복사",
+  "Rename": "이름 변경",
+  "Move/Rename": "이동/이름 변경",
+  "Redirected": "리디렉션됨",
+  "Unlinked": "연결 해제됨",
+  "unlink_redirection": "리디렉션 연결 해제",
+  "Done": "완료",
+  "Cancel": "취소",
+  "Create": "생성",
+  "Description": "설명",
+  "Admin": "관리자",
+  "administrator": "관리자",
+  "Tag": "태그",
+  "Tags": "태그",
+  "Close": "닫기",
+  "Shortcuts": "단축키",
+  "Custom Sidebar": "사용자 지정 사이드바",
+  "eg": "예:",
+  "add": "추가",
+  "Undo": "실행 취소",
+  "Article": "문서",
+  "Page Path": "페이지 경로",
+  "Category": "카테고리",
+  "User": "사용자",
+  "account_id": "계정 ID",
+  "Update": "업데이트",
+  "Update Page": "페이지 업데이트",
+  "Error": "오류",
+  "Warning": "경고",
+  "Sign in": "로그인",
+  "Sign in with External auth": "{{signin}}으로 로그인",
+  "Sign up is here": "회원가입",
+  "Sign in is here": "로그인",
+  "Sign up": "회원가입",
+  "or": "또는",
+  "Sign up with Google Account": "Google 계정으로 회원가입",
+  "Sign in with Google Account": "Google 계정으로 로그인",
+  "Sign up with this Google Account": "이 Google 계정으로 회원가입",
+  "Select": "선택",
+  "Required": "필수",
+  "Example": "예시",
+  "Taro Yamada": "홍길동",
+  "List View": "목록 보기",
+  "Timeline View": "타임라인 보기",
+  "History": "기록",
+  "attachment_data": "첨부 파일 데이터",
+  "No_attachments_yet": "아직 첨부 파일이 없습니다.",
+  "Presentation Mode": "프레젠테이션 모드",
+  "Not available for guest": "게스트는 사용할 수 없습니다.",
+  "Not available in this version": "이 버전에서는 사용할 수 없습니다.",
+  "Not available when \"anyone with the link\" is selected": "'링크를 가진 모든 사람'이 선택된 경우 범위를 재정의할 수 없습니다.",
+  "No users have liked this yet": "아직 이 페이지를 좋아한 사용자가 없습니다.",
+  "No users have liked this yet.": "아직 이 페이지를 좋아한 사용자가 없습니다.",
+  "No users have bookmarked yet": "아직 북마크한 사용자가 없습니다.",
+  "Create Archive Page": "아카이브 페이지 생성",
+  "Create Sidebar Page": "<strong>/Sidebar</strong> 페이지 생성",
+  "File type": "파일 형식",
+  "Target page": "대상 페이지",
+  "Include Attachment File": "첨부 파일 포함",
+  "Include Comment": "댓글 포함",
+  "Include Subordinated Page": "하위 페이지 포함",
+  "Include Subordinated Target Page": "{{target}} 포함",
+  "All Subordinated Page": "모든 하위 페이지",
+  "Specify Hierarchy": "계층 지정",
+  "Submitted the request to create the archive": "아카이브 생성 요청 제출됨",
+  "username": "사용자 이름",
+  "Created": "생성됨",
+  "Last updated": "업데이트됨",
+  "Share": "공유",
+  "Markdown Link": "마크다운 링크",
+  "Create/Edit Template": "템플릿 페이지 생성/편집",
+  "Go to this version": "이 버전 보기",
+  "View diff": "차이점 보기",
+  "No diff": "차이점 없음",
+  "Latest": "최신",
+  "User ID": "사용자 ID",
+  "User Information": "사용자 정보",
+  "User Activation": "사용자 활성화",
+  "Basic Info": "기본 정보",
+  "Name": "이름",
+  "Email": "이메일",
+  "Language": "언어",
+  "English": "영어",
+  "Japanese": "일본어",
+  "Set Profile Image": "프로필 이미지 설정",
+  "Upload Image": "이미지 업로드",
+  "Current Image": "현재 이미지",
+  "Delete Image": "이미지 삭제",
+  "Delete this image?": "이 이미지를 삭제하시겠습니까?",
+  "Updated": "업데이트됨",
+  "Upload new image": "새 이미지 업로드",
+  "Connected": "연결됨",
+  "Loading": "로딩 중...",
+  "Disclose E-mail": "이메일 공개",
+  "page exists": "이 페이지는 이미 존재합니다.",
+  "Error occurred": "오류 발생",
+  "Input page name": "페이지 이름 입력",
+  "Input page name (optional)": "페이지 이름 입력 (선택 사항)",
+  "Input parent page path": "상위 페이지 경로 입력",
+  "New Page": "새 페이지",
+  "Create under": "아래에 페이지 생성:",
+  "V5 Page Migration": "V5 호환성으로 변환",
+  "GROWI.5.0_new_schema": "GROWI.5.0 새 스키마",
+  "See_more_detail_on_new_schema": "<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'>{{title}}</a>에서 자세한 내용 확인 <span class='growi-custom-icons'>external_link</span> ",
+  "external_account_management": "외부 계정 관리",
+  "UserGroup": "사용자 그룹",
+  "Basic Settings": "기본 설정",
+  "The contents entered here will be shown in the header etc": "여기에 입력된 내용은 헤더 등에 표시됩니다.",
+  "Public": "공개",
+  "Anyone with the link": "링크를 가진 모든 사람",
+  "Specified users only": "지정된 사용자만",
+  "Only me": "나만",
+  "Only inside the group": "그룹 내에서만",
+  "page_list": "페이지 목록",
+  "comments": "댓글",
+  "Reselect the group": "그룹 재선택",
+  "Shareable link": "공유 가능한 링크",
+  "The whitelist of registration permission E-mail address": "등록 허용 이메일 주소 화이트리스트",
+  "Add tags for this page": "이 페이지에 태그 추가",
+  "tag_list": "태그 목록",
+  "popular_tags": "인기 태그",
+  "Check All tags": "모든 태그 확인",
+  "You have no tag, You can set tags on pages": "태그가 없습니다. 페이지에 태그를 설정할 수 있습니다.",
+  "Show latest": "최신 보기",
+  "Load latest": "최신 로드",
+  "edited this page": "이 페이지를 편집했습니다.",
+  "List Drafts": "초안 목록",
+  "Deleted Pages": "삭제된 페이지",
+  "Disassociate": "연결 해제",
+  "No bookmarks yet": "아직 북마크가 없습니다.",
+  "add_bookmark": "북마크에 추가",
+  "remove_bookmark": "북마크에서 제거",
+  "wide_view": "넓은 보기",
+  "Recent Changes": "최근 변경 사항",
+  "Page Tree": "페이지 트리",
+  "Bookmarks": "북마크",
+  "In-App Notification": "알림",
+  "AI Assistant": "AI 어시스턴트",
+  "Knowledge Assistant": "지식 어시스턴트 (베타)",
+  "Editor Assistant": "편집기 어시스턴트 (베타)",
+  "original_path": "원본 경로",
+  "new_path": "새 경로",
+  "duplicated_path": "중복된 경로",
+  "Link sharing is disabled": "링크 공유가 비활성화되었습니다.",
+  "successfully_saved_the_page": "페이지가 성공적으로 저장되었습니다.",
+  "you_can_not_create_page_with_this_name_or_hierarchy": "이 이름 또는 페이지 계층으로 페이지를 생성할 수 없습니다.",
+  "not_allowed_to_see_this_page": "이 페이지를 볼 수 없습니다.",
+  "Confirm": "확인",
+  "Successfully requested": "성공적으로 요청되었습니다.",
+  "source": "소스",
+  "input_validation": {
+    "target": {
+      "page_name": "페이지 이름",
+      "folder_name": "폴더 이름",
+      "field": "필드"
+    },
+    "message": {
+      "error_message": "일부 값이 올바르지 않습니다.",
+      "required": "'{{param}}'은 필수입니다.",
+      "invalid_syntax": "{{syntax}}의 구문이 유효하지 않습니다.",
+      "title_required": "제목은 필수입니다.",
+      "field_required": "{{target}}은 필수입니다."
+    }
+  },
+  "not_creatable_page": {
+    "message": "이 경로에는 페이지 콘텐츠를 생성할 수 없습니다."
+  },
+  "custom_navigation": {
+    "no_pages_under_this_page": "이 페이지 아래에 페이지가 없습니다."
+  },
+  "author_info": {
+    "created_at": "생성일",
+    "created_by": "생성자",
+    "last_revision_posted_at": "마지막 수정 게시일",
+    "updated_by": "업데이트자"
+  },
+  "installer": {
+    "tab": "계정 생성",
+    "title": "설치 프로그램",
+    "setup": "설정",
+    "create_initial_account": "초기 계정 생성",
+    "initial_account_will_be_administrator_automatically": "초기 계정은 자동으로 관리자가 됩니다.",
+    "unavaliable_user_id": "이 '사용자 ID'는 사용할 수 없습니다.",
+    "failed_to_install": "GROWI 설치 실패. 다시 시도하십시오.",
+    "failed_to_login_after_install": "설치 후 로그인 실패. 로그인 양식으로 리디렉션 중..."
+  },
+  "breaking_changes": {
+    "v346_using_basic_auth": "현재 사용 중인 기본 인증은 가까운 시일 내에 <strong>더 이상 사용할 수 없습니다</strong>. %s에서 설정을 제거하십시오."
+  },
+  "page_register": {
+    "send_email": "이메일 전송",
+    "notice": {
+      "restricted": "관리자 승인 필요.",
+      "restricted_defail": "관리자가 회원가입을 승인하면 이 위키에 접근할 수 있습니다."
+    },
+    "form_help": {
+      "email": "이 위키에 가입하려면 아래에 나열된 이메일 주소가 있어야 합니다.",
+      "password": "비밀번호는 최소 {{target}}자 이상이어야 합니다.",
+      "user_id": "생성하는 페이지의 URL에는 사용자 ID가 포함됩니다. 사용자 ID는 문자, 숫자 및 일부 기호로 구성될 수 있습니다."
+    }
+  },
+  "page_me": {
+    "form_help": {
+      "profile_image1": "이미지 업로드 설정이 완료되지 않았습니다.",
+      "profile_image2": "AWS를 설정하거나 로컬 업로드를 활성화하십시오."
+    }
+  },
+  "page_me_apitoken": {
+    "api_token": "API 토큰",
+    "notice": {
+      "apitoken_issued": "API 토큰이 발급되지 않았습니다.",
+      "update_token1": "새 API 토큰을 생성하도록 업데이트할 수 있습니다.",
+      "update_token2": "기존 프로세스에서 API 토큰을 업데이트해야 합니다."
+    },
+    "form_help": {}
+  },
+  "Password": "비밀번호",
+  "Password Settings": "비밀번호 설정",
+  "personal_settings": {
+    "disassociate_external_account": "외부 계정 연결 해제",
+    "disassociate_external_account_desc": "<strong>{{providerType}}</strong> 계정 <strong>{{accountId}}</strong>의 연결을 해제하시겠습니까?",
+    "set_new_password": "새 비밀번호 설정",
+    "update_password": "비밀번호 업데이트",
+    "current_password": "현재 비밀번호",
+    "new_password": "새 비밀번호",
+    "new_password_confirm": "새 비밀번호 다시 입력",
+    "password_is_not_set": "비밀번호가 설정되지 않았습니다."
+  },
+  "share_links": {
+    "Shere this page link to public": "이 페이지 링크를 공개적으로 공유",
+    "share_link_list": "공유 링크 목록",
+    "share_link_management": "공유 링크 관리",
+    "delete_all_share_links": "모든 공유 링크 삭제",
+    "expire": "만료",
+    "Days": "일",
+    "Custom": "사용자 지정",
+    "description": "설명",
+    "enter_desc": "설명 입력",
+    "Unlimited": "무제한",
+    "Issue": "발급",
+    "share_settings": "공유 설정",
+    "Invalid_Number_of_Date": "유효하지 않은 값을 입력했습니다.",
+    "link_sharing_is_disabled": "링크 공유가 비활성화되었습니다."
+  },
+  "API Settings": "API 설정",
+  "Other Settings": "기타 설정",
+  "API Token Settings": "API 토큰 설정",
+  "Current API Token": "현재 API 토큰",
+  "Update API Token": "API 토큰 업데이트",
+  "in_app_notification_settings": {
+    "in_app_notification_settings": "앱 내 알림 설정",
+    "subscribe_settings": "페이지 자동 구독 (알림 수신) 설정",
+    "default_subscribe_rules": {
+      "page_create": "페이지 생성 시 구독."
+    }
+  },
+  "ui_settings": {
+    "ui_settings": "UI 설정",
+    "side_bar_mode": {
+      "settings": "사이드바 모드 설정",
+      "side_bar_mode_setting": "사이드바 모드 설정",
+      "description": "화면 너비가 클 때 사이드바를 항상 열어둘지 여부를 설정할 수 있습니다. 화면 너비가 작으면 사이드바는 항상 닫힙니다."
+    }
+  },
+  "color_mode_settings": {
+    "light": "밝게",
+    "dark": "어둡게",
+    "system": "시스템",
+    "settings": "색상 모드 설정",
+    "description": "밝은 모드, 어두운 모드 또는 시스템별 표시 중 선택합니다.<br>지원되는 테마만 전환할 수 있습니다."
+  },
+  "editor_settings": {
+    "editor_settings": "편집기 설정"
+  },
+  "search_help": {
+    "title": "검색 도움말",
+    "and": {
+      "syntax help": "공백으로 구분",
+      "desc": "제목 또는 본문에 {{word1}}, {{word2}}를 모두 포함하는 페이지 검색"
+    },
+    "exclude": {
+      "desc": "제목 또는 본문에 {{word}}를 포함하는 페이지 제외"
+    },
+    "phrase": {
+      "syntax help": "큰따옴표로 묶음",
+      "desc": "\"{{phrase}}\" 구문을 포함하는 페이지 검색"
+    },
+    "prefix": {
+      "desc": "제목이 {{path}}로 시작하는 페이지만 검색"
+    },
+    "exclude_prefix": {
+      "desc": "제목이 {{path}}로 시작하는 페이지 제외"
+    },
+    "tag": {
+      "desc": "{{tag}} 태그가 있는 페이지 검색"
+    },
+    "exclude_tag": {
+      "desc": "{{tag}} 태그가 있는 페이지 제외"
+    }
+  },
+  "search": {
+    "search page bodies": "전체 텍스트 검색을 위해 [Enter] 키를 누르십시오."
+  },
+  "page_page": {
+    "notice": {
+      "version": "현재 버전이 아닙니다.",
+      "redirected": "다음에서 리디렉션되었습니다.",
+      "redirected_period": ".",
+      "unlinked": "이 페이지로 리디렉션된 페이지가 삭제되었습니다.",
+      "restricted": "이 페이지에 대한 접근이 제한되었습니다.",
+      "stale": "마지막 업데이트 이후 {{count}}년 이상 경과했습니다.",
+      "stale_plural": "마지막 업데이트 이후 {{count}}년 이상 경과했습니다.",
+      "expiration": "이 공유 링크는 <strong>{{expiredAt}}</strong>에 만료됩니다.",
+      "no_deadline": "이 페이지는 만료일이 없습니다.",
+      "not_indexed1": "이 페이지는 전체 텍스트 검색 엔진에 의해 인덱싱되지 않을 수 있습니다.",
+      "not_indexed2": "페이지 본문이 {{threshold}}로 지정된 임계값을 초과합니다."
+    }
+  },
+  "page_edit": {
+    "input_channels": "Slack 채널 이름...",
+    "theme": "테마",
+    "keymap": "키맵",
+    "indent": "들여쓰기",
+    "paste": {
+      "title": "붙여넣기 동작",
+      "both": "둘 다",
+      "text": "텍스트만",
+      "file": "파일만"
+    },
+    "editor_config": "편집기 구성",
+    "editor_assistant": "편집기 어시스턴트",
+    "Show active line": "활성 줄 표시",
+    "auto_format_table": "테이블 자동 서식",
+    "overwrite_scopes": "{{operation}} 및 모든 하위 항목의 범위 덮어쓰기",
+    "notice": {
+      "conflict": "다른 사람이 이 페이지를 편집 중이어서 변경 사항을 저장할 수 없습니다. 페이지를 다시 로드한 후 영향을 받는 섹션을 다시 편집하십시오."
+    },
+    "changes_not_saved": "변경 사항이 저장되지 않을 수 있습니다. 이동하시겠습니까?"
+  },
+  "page_comment": {
+    "comments": "댓글",
+    "comment": "댓글",
+    "preview": "미리보기",
+    "write": "작성",
+    "add_a_comment": "댓글 추가",
+    "display_the_page_when_posting_this_comment": "이 댓글을 게시할 때 페이지 표시",
+    "no_user_found": "사용자를 찾을 수 없습니다.",
+    "reply": "답글",
+    "delete_comment": "댓글 삭제?",
+    "comment_management_is_not_allowed": "댓글 관리가 허용되지 않습니다."
+  },
+  "page_api_error": {
+    "notfound_or_forbidden": "원본 페이지를 찾을 수 없거나 접근이 금지되었습니다.",
+    "already_exists": "경로가 있는 페이지가 이미 존재합니다.",
+    "outdated": "페이지가 업데이트되어 현재 오래되었습니다.",
+    "user_not_admin": "관리자만 삭제할 수 있습니다.",
+    "single_deletion_empty_pages": "빈 페이지는 단일 삭제할 수 없습니다.",
+    "complete_deletion_not_allowed_for_user": "이 페이지를 완전히 삭제할 권한이 없습니다."
+  },
+  "page_history": {
+    "revision_list": "수정 목록",
+    "revision": "버전",
+    "comparing_source": "원본",
+    "comparing_target": "대상",
+    "comparing_revisions": "차이점 비교",
+    "compare_latest": "최신 수정 비교",
+    "compare_previous": "이전 수정 비교"
+  },
+  "modal_rename": {
+    "label": {
+      "Move/Rename page": "페이지 이동/이름 변경",
+      "New page name": "새 페이지 이름",
+      "Failed to get subordinated pages": "하위 페이지를 가져오는 데 실패했습니다.",
+      "Failed to get exist path": "기존 경로를 가져오는 데 실패했습니다.",
+      "Current page name": "현재 페이지 이름",
+      "Rename this page only": "이 페이지만 이름 변경",
+      "Force rename all child pages": "모든 하위 페이지 강제 이름 변경",
+      "Other options": "기타 옵션",
+      "Do not update metadata": "메타데이터 업데이트 안 함",
+      "Redirect": "리디렉션"
+    },
+    "help": {
+      "redirect": "누군가 이 경로로 접근하면 새 페이지로 리디렉션",
+      "metadata": "마지막 업데이트 사용자 및 업데이트 날짜는 동일하게 유지됩니다.",
+      "recursive": "이 경로 아래의 하위 항목을 재귀적으로 이동/이름 변경"
+    }
+  },
+  "Put Back": "되돌리기",
+  "Delete Completely": "완전 삭제",
+  "page_has_been_reverted": "{{path}}가 되돌려졌습니다.",
+  "modal_delete": {
+    "delete_page": "페이지 삭제",
+    "deleting_page": "페이지 삭제 중",
+    "delete_recursively": "하위 페이지 재귀적으로 삭제.",
+    "delete_completely": "완전 삭제",
+    "delete_completely_restriction": "페이지를 완전히 삭제할 권한이 없습니다.",
+    "recursively": "이 경로 아래의 페이지를 재귀적으로 삭제.",
+    "completely": "휴지통에 넣지 않고 완전히 삭제."
+  },
+  "deleted_page": "휴지통으로 이동됨",
+  "deleted_pages": "{{path}}가 삭제되었습니다.",
+  "deleted_pages_completely": "{{path}}가 완전히 삭제되었습니다.",
+  "renamed_pages": "{{path}}가 이름 변경되었습니다.",
+  "empty_trash": "휴지통이 비워졌습니다.",
+  "modal_empty": {
+    "empty_the_trash": "휴지통 비우기",
+    "empty_the_trash_button": "휴지통 비우기",
+    "not_deletable_notice": "일부 페이지는 권한 부족으로 제거할 수 없습니다.",
+    "notice": "완전히 삭제된 페이지는 복구할 수 없습니다."
+  },
+  "modal_duplicate": {
+    "label": {
+      "Duplicate page": "페이지 복제",
+      "New page name": "새 페이지 이름",
+      "Failed to get subordinated pages": "하위 페이지를 가져오는 데 실패했습니다.",
+      "Current page name": "현재 페이지 이름",
+      "Recursively": "재귀적으로",
+      "Duplicate without exist path": "기존 경로 없이 복제",
+      "Same page already exists": "동일한 페이지가 이미 존재합니다.",
+      "Only duplicate user related pages": "사용자 관련 페이지만 복제"
+    },
+    "help": {
+      "recursive": "이 경로 아래의 하위 항목을 재귀적으로 복제",
+      "only_inherit_user_related_groups": "페이지 권한이 \"그룹 내에서만\"으로 설정된 경우, 속하지 않은 그룹은 복제된 페이지에 대한 접근 권한을 잃게 됩니다."
+    }
+  },
+  "duplicated_pages": "{{fromPath}}가 복제되었습니다.",
+  "modal_granted_groups_inheritance_select": {
+    "select_granted_groups": "페이지에 접근할 수 있는 그룹 선택",
+    "inherit_all_granted_groups_from_parent": "상위에서 페이지에 접근할 수 있는 모든 그룹 상속",
+    "only_inherit_related_groups": "상위에서 속한 그룹만 상속",
+    "create_page": "페이지 생성"
+  },
+  "modal_putback": {
+    "label": {
+      "Put Back Page": "페이지 되돌리기",
+      "recursively": "재귀적으로 되돌리기"
+    },
+    "help": {
+      "recursively": "이 경로 아래의 페이지를 재귀적으로 되돌리기"
+    }
+  },
+  "modal_shortcuts": {
+    "global": {
+      "title": "전역 단축키",
+      "Open/Close shortcut help": "단축키 도움말 열기/닫기",
+      "Edit Page": "페이지 편집",
+      "Create Page": "페이지 생성",
+      "Search": "검색",
+      "Show Contributors": "기여자 표시",
+      "MirrorMode": "미러 모드",
+      "Konami Code": "코나미 코드",
+      "konami_code_url": "https://ko.wikipedia.org/wiki/%EC%BD%94%EB%82%98%EB%AF%B8_%EC%BD%94%EB%93%9C"
+    },
+    "editor": {
+      "title": "편집기 단축키",
+      "Indent": "들여쓰기",
+      "Outdent": "내어쓰기",
+      "Save Page": "페이지 저장",
+      "Only Editor": "(편집기 전용)",
+      "Delete Line": "줄 삭제",
+      "Search in Editor": "편집기에서 검색",
+      "Move Line": "줄 이동",
+      "Copy Line": "줄 복사",
+      "Insert Line": "줄 삽입",
+      "Post Comment": "(댓글 게시)",
+      "Multiple Cursors": "다중 커서",
+      "Or Alt Click": "또는 Alt + 클릭"
+    },
+    "format": {
+      "title": "서식 설정 (편집기)",
+      "Bold": "굵게",
+      "Italic": "기울임꼴",
+      "Strikethrough": "취소선",
+      "Code Text": "코드 텍스트",
+      "Hyperlink": "하이퍼링크"
+    },
+    "line_settings": {
+      "title": "줄 설정 (편집기)",
+      "Bullet List": "글머리 기호 목록",
+      "Numbered List": "번호 매기기 목록",
+      "Quote": "인용",
+      "Code Block": "코드 블록",
+      "Comment Out": "주석 처리",
+      "Comment Out Desc": "(숨기기)"
+    }
+  },
+  "modal_resolve_conflict": {
+    "conflicts_with_new_body_on_server_side": "서버 측의 새 본문과 충돌합니다. 충돌을 해결하려면 페이지 본문을 선택하거나 편집하십시오.",
+    "file_conflicting_with_newer_remote": "이 파일은 더 새로운 원격 파일과 충돌합니다.",
+    "resolve_conflict_message": "페이지 본문을 선택하십시오.",
+    "resolve_conflict": "충돌 해결",
+    "resolve_and_save": "해결 및 저장",
+    "select_revision": "{{revision}} 선택",
+    "requested_revision": "내 것",
+    "latest_revision": "그들의 것",
+    "selected_editable_revision": "선택된 편집 가능한 페이지 본문"
+  },
+  "sidebar_ai_assistant": {
+    "reference_pages_label": "참조 페이지",
+    "recent_chat": "최근 채팅",
+    "no_recent_chat": "최근 채팅 없음",
+    "placeholder": "무엇이든 물어보세요.",
+    "knowledge_assistant_placeholder": "무엇이든 물어보세요.",
+    "editor_assistant_placeholder": "도와드릴까요?",
+    "summary_mode_label": "요약 모드",
+    "summary_mode_help": "2-3문장으로 간결한 답변",
+    "extended_thinking_mode_label": "확장 사고 모드",
+    "extended_thinking_mode_help": "활성화하면 AI가 더 많은 시간을 들여 생각하고 더 포괄적인 답변을 제공합니다.",
+    "caution_against_hallucination": "정보를 확인하고 출처를 확인하십시오.",
+    "progress_label": "답변 생성 중",
+    "failed_to_create_or_retrieve_thread": "스레드를 생성하거나 검색하는 데 실패했습니다.",
+    "budget_exceeded": "OpenAI API 사용 한도에 도달했습니다. 지식 어시스턴트를 다시 사용하려면 OpenAI 결제 페이지에서 크레딧을 추가하십시오.",
+    "budget_exceeded_for_growi_cloud": "OpenAI API 사용 한도에 도달했습니다. 지식 어시스턴트를 다시 사용하려면 호스팅 사용자의 경우 GROWI.cloud 관리 페이지에서, 소유 사용자의 경우 OpenAI 결제 페이지에서 크레딧을 추가하십시오.",
+    "error_message": "오류가 발생했습니다.",
+    "show_error_detail": "오류 세부 정보 표시",
+    "editor_assistant_long_context_warn_with_unit_line": "텍스트가 너무 길어서 편집기 어시스턴트는 응답을 위해 약 {{startPosition}}줄에서 {{endPosition}}줄을 참조합니다.",
+    "editor_assistant_long_context_warn_with_unit_char": "텍스트가 너무 길어서 편집기 어시스턴트는 응답을 위해 {{startPosition}}자에서 {{endPosition}}자를 참조합니다.",
+    "discard": "버리기",
+    "accept": "수락",
+    "use_assistant": "어시스턴트 사용",
+    "remove_assistant": "선택된 어시스턴트 선택 해제",
+    "text_generation_by_editor_assistant_label": "편집기 어시스턴트가 텍스트를 생성 중입니다.",
+    "preset_menu": {
+      "summarize": {
+        "title": "이 문서 요약",
+        "prompt": "마크다운 콘텐츠를 요약해 주세요."
+      },
+      "correct": {
+        "title": "텍스트의 오류 수정",
+        "prompt": "마크다운 텍스트의 오류를 수정해 주세요."
+      }
+    }
+  },
+  "modal_ai_assistant": {
+    "header": {
+      "update_assistant": "어시스턴트 업데이트",
+      "add_new_assistant": "새 어시스턴트 추가"
+    },
+    "assistant_name_placeholder": "어시스턴트 이름 입력",
+    "page_count": "{{count}} 페이지",
+    "memo": {
+      "title": "어시스턴트 메모",
+      "optional": "선택 사항",
+      "placeholder": "콘텐츠 및 사용법에 대한 메모를 표시할 수 있습니다.",
+      "description": "메모 내용은 어시스턴트 처리에 영향을 미치지 않습니다."
+    },
+    "submit_button": {
+      "update_assistant": "어시스턴트 업데이트",
+      "create_assistant": "어시스턴트 생성"
+    },
+    "toaster": {
+      "create_success": "어시스턴트가 생성되었습니다.",
+      "update_success": "어시스턴트가 업데이트되었습니다.",
+      "create_failed": "어시스턴트 생성 실패",
+      "update_failed": "어시스턴트 업데이트 실패"
+    },
+    "edit_page_description": "어시스턴트가 참조할 수 있는 페이지를 편집합니다.<br>어시스턴트는 하위 페이지를 포함하여 최대 {{limitLearnablePageCountPerAssistant}}개의 페이지를 참조할 수 있습니다.",
+    "default_instruction": "당신은 이 위키의 지식 어시스턴트입니다.\n\n## 다국어 지원:\n사용자가 입력한 언어와 동일한 언어로 응답하십시오.\n",
+    "add_page_button": "페이지 추가",
+    "page_mode_title": {
+      "share": "어시스턴트 공유",
+      "pages": "참조 페이지",
+      "instruction": "어시스턴트 지침"
+    },
+    "share_assistant": "어시스턴트 공유",
+    "page_access_permission": "페이지 접근 권한",
+    "access_scope": {
+      "owner": "{{username}}이 접근할 수 있는 모든 페이지",
+      "groups": "그룹 지정",
+      "publicOnly": "공개 페이지만"
+    },
+    "share_scope": {
+      "title": "어시스턴트 공유 범위",
+      "owner": {
+        "label": "{{username}}만"
+      },
+      "publicOnly": {
+        "label": "공개",
+        "desc": "모든 사용자와 공유됨"
+      },
+      "groups": {
+        "label": "그룹 지정",
+        "desc": "선택된 그룹의 구성원과만 공유됨"
+      },
+      "sameAsAccessScope": {
+        "label": "페이지 접근 범위와 동일",
+        "desc": "페이지 접근과 동일한 범위로 공유됨"
+      }
+    },
+    "instructions": {
+      "description": "어시스턴트의 동작 방식을 결정하는 지침을 설정할 수 있습니다.<br>어시스턴트는 이 지침을 기반으로 답변하고 분석합니다.",
+      "reset_to_default": "기본값으로 재설정"
+    }
+  },
+  "share_scope_warning_modal": {
+    "header_title": "공유 범위 확인",
+    "warning_message": "이 어시스턴트에는 접근이 제한된 페이지가 포함되어 있습니다.<br>현재 설정으로는 이 페이지의 정보가 어시스턴트의 공개 범위를 통해 원래 접근 권한을 넘어 공유될 수 있습니다.",
+    "selected_pages_label": "선택된 페이지 경로",
+    "confirmation_message": "진행하면 이 페이지의 내용이 어시스턴트의 공개 범위 내에서 공유될 수 있음을 이해했는지 확인하십시오.",
+    "button": {
+      "review": "설정 검토",
+      "proceed": "이해하고 진행"
+    }
+  },
+  "default_ai_assistant": {
+    "not_set": "기본 어시스턴트가 설정되지 않았습니다."
+  },
+  "ai_assistant_substance": {
+    "add_assistant": "어시스턴트 추가",
+    "my_assistants": "내 어시스턴트",
+    "team_assistants": "팀 어시스턴트",
+    "thread_does_not_exist": "스레드가 존재하지 않습니다.",
+    "recent_threads": "최근 항목",
+    "toaster": {
+      "ai_assistant_deleted_success": "어시스턴트 삭제됨",
+      "ai_assistant_deleted_failed": "어시스턴트 삭제 실패",
+      "thread_deleted_success": "스레드 삭제됨",
+      "thread_deleted_failed": "스레드 삭제 실패",
+      "ai_assistant_set_default_success": "기본 어시스턴트 설정 성공",
+      "ai_assistant_set_default_failed": "기본 어시스턴트 설정 실패"
+    }
+  },
+  "link_edit": {
+    "edit_link": "링크 편집",
+    "set_link_and_label": "링크 및 레이블 설정",
+    "link": "링크",
+    "placeholder_of_link_input": "페이지 경로 또는 URL 입력",
+    "label": "레이블",
+    "path_format": "경로 형식",
+    "use_relative_path": "상대 경로 사용",
+    "use_permanent_link": "영구 링크 사용",
+    "notation": "표기법",
+    "markdown": "마크다운",
+    "GROWI_original": "GROWI 원본",
+    "pukiwiki": "Pukiwiki",
+    "preview": "미리보기",
+    "page_not_found_in_preview": "\"{{path}}\"은 GROWI 페이지가 아닙니다."
+  },
+  "toaster": {
+    "file_upload_failed": "파일 업로드 실패.",
+    "initialize_successed": "{{target}} 초기화 성공",
+    "remove_share_link_success": "{{shareLinkId}} 제거 성공",
+    "issue_share_link": "새 공유 링크 발급 성공",
+    "remove_share_link": "{{count}}개 공유 링크 제거 성공",
+    "switch_disable_link_sharing_success": "공유 링크 설정 업데이트 성공",
+    "failed_to_reset_password": "비밀번호 재설정 실패",
+    "save_succeeded": "저장 성공"
+  },
+  "template": {
+    "modal_label": {
+      "Select template": "템플릿 선택",
+      "Create/Edit Template Page": "템플릿 페이지 생성/편집",
+      "Create template under": "이 페이지 아래에 템플릿 페이지 생성"
+    },
+    "option_label": {
+      "create/edit": "템플릿 페이지 생성/편집..",
+      "select": "템플릿 페이지 유형 선택"
+    },
+    "children": {
+      "label": "하위 항목용 템플릿",
+      "desc": "템플릿이 존재하는 동일 레벨 페이지에만 적용됩니다."
+    },
+    "descendants": {
+      "label": "하위 항목용 템플릿",
+      "desc": "모든 하위 페이지에 적용됩니다."
+    }
+  },
+  "sandbox": {
+    "header": "헤더",
+    "header_x": "헤더 {{index}}",
+    "block": "단락",
+    "block_detail": "단락을 만듭니다.",
+    "empty_line": "빈 줄",
+    "line_break": "줄 바꿈",
+    "line_break_detail": "(2칸 공백) 줄 바꿈을 만듭니다.",
+    "typography": "타이포그래피",
+    "italics": "기울임꼴",
+    "bold": "굵게",
+    "italic_bold": "기울임꼴 굵게",
+    "strikethrough": "취소선",
+    "link": "링크",
+    "code_highlight": "코드 하이라이트",
+    "list": "목록",
+    "unordered_list_x": "순서 없는 목록 {{index}}",
+    "ordered_list_x": "순서 있는 목록 {{index}}",
+    "task": "작업",
+    "task_checked": "체크됨",
+    "task_unchecked": "체크 해제됨",
+    "quote": "인용",
+    "quote1": "작성할 수 있습니다.",
+    "quote2": "여러 줄 인용",
+    "quote_nested": "중첩 인용",
+    "table": "테이블",
+    "image": "이미지",
+    "alt_text": "대체 텍스트",
+    "insert_image": "이미지 삽입",
+    "open_sandbox": "샌드박스 열기"
+  },
+  "slack_notification": {
+    "popover_title": "Slack 알림",
+    "popover_desc": "채널 이름을 입력하십시오. 쉼표로 구분된 목록을 입력하여 여러 채널에 알릴 수 있습니다.",
+    "input_channels": "채널 입력"
+  },
+  "search_result": {
+    "title": "검색",
+    "result_meta": "검색 결과:",
+    "deletion_mode_btn_lavel": "페이지 선택 및 삭제",
+    "cancel": "취소",
+    "delete": "삭제",
+    "check_all": "모두 선택",
+    "deletion_modal_header": "페이지 삭제",
+    "delete_completely": "완전 삭제",
+    "include_certain_path": "{{pathToInclude}} 경로 포함 ",
+    "delete_all_selected_page": "모두 삭제",
+    "select_all": "모두 선택",
+    "delete_selected_pages": "선택된 페이지 삭제",
+    "currently_not_implemented": "현재 구현되지 않았습니다.",
+    "search_again": "다시 검색",
+    "number_of_list_to_display": "표시",
+    "page_number_unit": "페이지",
+    "hit_number_unit": "개",
+    "sort_axis": {
+      "relationScore": "관련성별 정렬",
+      "createdAt": "생성일",
+      "updatedAt": "마지막 업데이트일"
+    }
+  },
+  "private_legacy_pages": {
+    "title": "비공개 레거시 페이지",
+    "bulk_operation": "대량 작업",
+    "convert_all_selected_pages": "모두 새 v5 호환 형식으로 변환",
+    "input_path_to_convert": "페이지를 변환할 경로 입력",
+    "alert_title": "이전 v4 호환 형식 비공개 페이지가 존재합니다.",
+    "alert_desc1": "이 페이지에서 확인란으로 페이지를 선택하고 화면 상단의 \"대량 작업\" 버튼에서 새 v5 호환 형식으로 일괄 변환할 수 있습니다.",
+    "nopages_title": "축하합니다. GROWI v5를 사용할 준비가 되었습니다!",
+    "nopages_desc1": "이제 관리할 수 있는 모든 페이지가 v5 호환 형식인 것 같습니다.",
+    "detail_info": "<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html' target='_blank' class='alert-link'>GROWI를 v5.0.x로 업그레이드 <span class='growi-custom-icons'>external_link</span></a>에서 자세한 정보를 확인하십시오.",
+    "modal": {
+      "title": "새 v5 호환 형식으로 변환",
+      "converting_pages": "페이지 변환 중",
+      "convert_recursively_label": "하위 페이지 재귀적으로 변환.",
+      "convert_recursively_desc": "이 경로 아래의 페이지를 재귀적으로 변환.",
+      "button_label": "변환"
+    },
+    "toaster": {
+      "page_migration_succeeded": "선택된 페이지의 v5 변환이 성공적으로 완료되었습니다.",
+      "page_migration_failed_with_paths": "{{paths}}의 v5 변환이 실패했습니다.",
+      "page_migration_failed": "페이지의 v5 변환이 실패했습니다."
+    },
+    "by_path_modal": {
+      "title": "새 v5 호환 형식으로 변환",
+      "alert": "이 작업은 되돌릴 수 없으며, 사용자가 볼 수 없는 페이지도 처리 대상이 됩니다.",
+      "checkbox_label": "이해함",
+      "description": "경로를 입력하면 해당 경로 아래의 모든 페이지가 v5 호환 형식으로 변환됩니다.",
+      "button_label": "변환",
+      "success": "변환 요청 성공.",
+      "error": "변환 요청 실패.",
+      "error_grant_invalid": "페이지 권한이 올바르지 않습니다. 수정하고 다시 시도하십시오.",
+      "error_page_not_found": "페이지를 찾을 수 없습니다.",
+      "error_duplicate_pages_found": "동일한 경로 이름의 페이지가 여러 개 발견되었습니다. 이름을 변경하거나 삭제하고 다시 시도하십시오."
+    }
+  },
+  "login": {
+    "title": "로그인",
+    "sign_in_error": "로그인 오류",
+    "registration_successful": "등록 성공. 관리자 승인을 기다리십시오.",
+    "Setup": "설정",
+    "enabled_ldap_has_configuration_problem": "LDAP이 활성화되었지만 구성에 문제가 있습니다.",
+    "set_env_var_for_logs": "(로그를 얻으려면 환경 변수 <code>DEBUG=crowi:service:PassportService</code>를 설정하십시오.)"
+  },
+  "invited": {
+    "title": "초대됨",
+    "discription_heading": "계정 생성",
+    "discription": "초대된 이메일 주소로 계정을 생성하십시오."
+  },
+  "page_export": {
+    "failed_to_export": "내보내기 실패",
+    "failed_to_count_pages": "페이지 수 계산 실패",
+    "export_page_markdown": "페이지를 마크다운으로 내보내기",
+    "export_page_pdf": "페이지를 PDF로 내보내기",
+    "bulk_export": "페이지 및 모든 하위 페이지 내보내기",
+    "bulk_export_download_explanation": "내보내기가 완료되면 알림이 전송됩니다. 내보낸 파일을 다운로드하려면 알림을 클릭하십시오.",
+    "bulk_export_exec_time_warning": "페이지 수가 많으면 내보내기에 시간이 걸릴 수 있습니다.",
+    "large_bulk_export_warning": "시스템 리소스 절약을 위해 대량의 페이지를 연속으로 내보내지 마십시오.",
+    "markdown": "마크다운",
+    "choose_export_format": "내보내기 형식 선택",
+    "bulk_export_started": "잠시 기다려 주십시오...",
+    "bulk_export_download_expired": "다운로드 기간이 만료되었습니다.",
+    "bulk_export_job_expired": "내보내기 프로세스가 너무 오래 걸려 취소되었습니다.",
+    "export_in_progress": "내보내기 진행 중",
+    "export_in_progress_explanation": "동일한 형식의 내보내기가 이미 진행 중입니다. 최신 페이지 콘텐츠를 내보내기 위해 다시 시작하시겠습니까?",
+    "export_cancel_warning": "다음 진행 중인 내보내기가 취소됩니다.",
+    "restart": "다시 시작",
+    "format": "형식",
+    "started_on": "시작일",
+    "file_upload_not_configured": "파일 업로드 설정이 구성되지 않았습니다."
+  },
+  "message": {
+    "successfully_connected": "성공적으로 연결되었습니다!",
+    "fail_to_save_access_token": "액세스 토큰 저장 실패. 다시 시도하십시오.",
+    "fail_to_fetch_access_token": "액세스 토큰 가져오기 실패. 다시 연결하십시오.",
+    "successfully_disconnected": "성공적으로 연결 해제되었습니다!",
+    "strategy_has_not_been_set_up": "{{strategy}}가 설정되지 않았습니다.",
+    "ldap_user_not_valid": "LDAP 사용자가 유효하지 않습니다.",
+    "external_account_not_exist": "외부 계정을 찾거나 생성하는 데 실패했습니다.",
+    "maximum_number_of_users": "최대 사용자 수 이상을 등록할 수 없습니다.",
+    "sign_in_failure": "로그인 실패.",
+    "aws_sttings_required": "이 기능을 사용하려면 AWS 설정이 필요합니다. 관리자에게 문의하십시오.",
+    "application_already_installed": "애플리케이션이 이미 설치되었습니다.",
+    "email_address_could_not_be_used": "이 이메일 주소는 사용할 수 없습니다. (허용된 이메일 주소를 확인하십시오.)",
+    "user_id_is_not_available": "이 사용자 ID는 사용할 수 없습니다.",
+    "username_should_not_be_null": "사용자 이름은 null이 아니어야 합니다. 관리 페이지에서 인증 메커니즘 설정을 확인하십시오.",
+    "email_address_is_already_registered": "이 이메일 주소는 이미 등록되었습니다.",
+    "can_not_register_maximum_number_of_users": "최대 사용자 수 이상을 등록할 수 없습니다.",
+    "email_settings_is_not_setup": "이메일 설정이 설정되지 않았습니다. 관리자에게 문의하십시오.",
+    "email_authentication_is_not_enabled": "이메일 인증이 활성화되지 않았습니다. 관리자에게 문의하십시오.",
+    "failed_to_register": "등록 실패.",
+    "successfully_created": "사용자 {{username}}이 성공적으로 생성되었습니다.",
+    "can_not_activate_maximum_number_of_users": "최대 사용자 수 이상을 활성화할 수 없습니다.",
+    "failed_to_activate": "활성화 실패.",
+    "unable_to_use_this_user": "이 사용자를 사용할 수 없습니다.",
+    "complete_to_install1": "GROWI 설치 완료! 관리자 계정으로 로그인하십시오.",
+    "complete_to_install2": "GROWI 설치 완료! 먼저 이 페이지의 각 설정을 확인하십시오.",
+    "failed_to_create_admin_user": "관리자 사용자 생성 실패. {{errMessage}}",
+    "successfully_send_email_auth": "{{email}}로 이메일을 보냈습니다. 이메일의 URL을 클릭하여 등록을 완료하십시오.",
+    "incorrect_token_or_expired_url": "토큰이 올바르지 않거나 URL이 만료되었습니다.",
+    "user_already_logged_in": "로그인 상태에서는 새 계정을 생성할 수 없습니다.",
+    "registration_closed": "새 계정을 생성할 권한이 없습니다.",
+    "Username has invalid characters": "사용자 이름에 유효하지 않은 문자가 있습니다.",
+    "Username field is required": "사용자 ID 필드는 필수입니다.",
+    "Name field is required": "이름 필드는 필수입니다.",
+    "Email format is invalid": "이메일 형식이 유효하지 않습니다.",
+    "Email field is required": "이메일 필드는 필수입니다.",
+    "Password has invalid character": "비밀번호에 유효하지 않은 문자가 있습니다.",
+    "Password minimum character should be more than n characters": "비밀번호는 최소 {{number}}자 이상이어야 합니다.",
+    "Password field is required": "비밀번호 필드는 필수입니다.",
+    "Username or E-mail has invalid characters": "사용자 이름 또는 이메일에 유효하지 않은 문자가 있습니다.",
+    "user_not_found": "사용자를 찾을 수 없습니다.",
+    "provider_duplicated_username_exception": "<p><strong><span class='material-symbols-outlined me-1'>cancel</span>DuplicatedUsernameException 발생</strong></p><p class='mb-0'> {{ failedProviderForDuplicatedUsernameException }} 인증은 성공했지만 새 사용자를 생성할 수 없습니다. <a href='https://github.com/weseek/growi/issues/193'>#193</a> 문제를 참조하십시오.</p>"
+  },
+  "grid_edit": {
+    "create_bootstrap_4_grid": "Bootstrap 4 그리드 생성",
+    "grid_settings": "그리드 설정",
+    "grid_pattern": "그리드 패턴",
+    "division": "분할",
+    "smart_no": "스마트폰 / 줄 바꿈 없음",
+    "break_point": "디스플레이 크기별 줄 바꿈 지점"
+  },
+  "validation": {
+    "aws_region": "지역에는 AWS 지역 이름을 입력하십시오. 예):us-east-1",
+    "aws_custom_endpoint": "사용자 지정 엔드포인트에는 http(s)://로 시작하는 URL을 지정하십시오. 또한 후행 슬래시는 필요하지 않습니다.",
+    "failed_to_send_a_test_email": "SMTP를 사용하여 테스트 이메일을 보내는 데 실패했습니다. 설정을 확인하십시오."
+  },
+  "forgot_password": {
+    "forgot_password": "비밀번호를 잊으셨습니까?",
+    "send": "전송",
+    "return_to_login": "로그인으로 돌아가기",
+    "reset_password": "비밀번호 재설정",
+    "sign_in_instead": "대신 로그인",
+    "password_reset_request_desc": "여기에서 비밀번호를 재설정할 수 있습니다.",
+    "password_reset_excecution_desc": "새 비밀번호 입력",
+    "new_password": "새 비밀번호",
+    "confirm_new_password": "새 비밀번호 확인",
+    "email_is_required": "이메일은 필수입니다.",
+    "success_to_send_email": "이메일 전송 성공",
+    "feature_is_unavailable": "이 기능은 사용할 수 없습니다.",
+    "incorrect_token_or_expired_url": "토큰이 올바르지 않거나 URL이 만료되었습니다. 아래 링크를 통해 비밀번호 재설정 요청을 다시 보내십시오.",
+    "password_and_confirm_password_does_not_match": "비밀번호와 비밀번호 확인이 일치하지 않습니다.",
+    "please_enable_mailer_alert": "이메일 설정이 완료되지 않아 비밀번호 재설정 기능이 비활성화되었습니다. 관리자에게 이메일 설정을 완료하도록 요청하십시오."
+  },
+  "maintenance_mode": {
+    "maintenance_mode": "유지 보수 모드",
+    "growi_is_under_maintenance": "GROWI가 유지 보수 중입니다. 완료될 때까지 기다려 주십시오.",
+    "admin_page": "관리자 페이지",
+    "login": "로그인",
+    "logout": "로그아웃"
+  },
+  "pagetree": {
+    "cannot_rename_a_title_that_contains_slash": "'/'를 포함하는 제목은 이름 변경할 수 없습니다.",
+    "you_cannot_move_this_page_now": "지금은 이 페이지를 이동할 수 없습니다.",
+    "something_went_wrong_with_moving_page": "페이지 이동 중 문제가 발생했습니다.",
+    "error_retrieving_the_pagetree": "페이지 트리를 검색하는 동안 오류가 발생했습니다."
+  },
+  "duplicated_page_alert": {
+    "same_page_name_exists": "'{{pageName}}'과(와) 동일한 페이지 이름이 존재합니다.",
+    "same_page_name_exists_at_path": "{{path}}에 {{pageName}}과(와) 동일한 페이지 이름이 존재합니다.",
+    "select_page_to_see": "볼 페이지 선택"
+  },
+  "user_group": {
+    "select_group": "그룹 선택",
+    "belonging_to_no_group": "속한 그룹을 찾을 수 없습니다.",
+    "manage_user_groups": "사용자 그룹 관리"
+  },
+  "fix_page_grant": {
+    "modal": {
+      "no_grant_available": "선택 가능한 권한 목록을 찾을 수 없습니다. 먼저 상위 페이지의 권한을 수정하고 다시 시도하십시오.",
+      "need_to_fix_grant": "이 기능을 올바르게 사용하려면 이 페이지와 관련된 권한을 수정해야 합니다.<br>변경하려면 아래 옵션에서 선택하십시오.",
+      "grant_label": {
+        "public": "공개",
+        "isForbidden": "볼 수 없는 권한",
+        "currentPageGrantLabel": "이 페이지의 권한: ",
+        "parentPageGrantLabel": "상위 페이지의 권한: ",
+        "docLink": "권한 수정에 대한 자세한 내용은 <a href='https://docs.growi.org/en/guide/features/authority.html#permissions-for-subordinate-pages'>이 링크</a>를 참조하십시오."
+      },
+      "radio_btn": {
+        "restrected": "링크를 아는 사람만",
+        "only_me": "나만",
+        "grant_group": "특정 그룹만"
+      },
+      "select_group_default_text": "그룹 선택",
+      "alert_message_select_group": "선택된 그룹 없음",
+      "btn_label": "변환",
+      "title": "권한 수정"
+    },
+    "alert": {
+      "description": "이 페이지의 권한 설정을 수정해야 합니다.",
+      "btn_label": "수정"
+    }
+  },
+  "tooltip": {
+    "like": "좋아요!",
+    "cancel_like": "좋아요 취소",
+    "bookmark": "북마크",
+    "cancel_bookmark": "북마크 취소",
+    "receive_notifications": "알림 받기",
+    "stop_notification": "알림 중지",
+    "footprints": "발자취",
+    "login_required": "로그인 필요",
+    "operation": {
+      "attention": {
+        "rename": "하위 페이지의 경로 이름 변경이 성공적이지 않았습니다. 3점 리더에서 메뉴를 열고 '경로 복구'를 선택하십시오."
+      }
+    }
+  },
+  "page_operation": {
+    "paths_recovered": "경로 복구 성공",
+    "path_recovery_failed": "경로 복구 실패"
+  },
+  "user_home_page": {
+    "bookmarks": "북마크",
+    "recently_created": "최근 생성됨"
+  },
+  "bookmark_folder": {
+    "bookmark_folder": "북마크 폴더",
+    "bookmark": "북마크",
+    "delete_modal": {
+      "modal_header_label": "북마크 폴더 삭제",
+      "modal_body_description": "이 북마크 폴더와 내용을 삭제합니다.",
+      "modal_body_alert": "삭제된 폴더와 내용은 복구할 수 없습니다.",
+      "modal_footer_button": "폴더 삭제"
+    },
+    "input_placeholder": "폴더 이름 입력",
+    "new_folder": "새 폴더",
+    "delete": "폴더 삭제",
+    "drop_item_here": "여기에 항목 끌어다 놓기",
+    "cancel_bookmark": "이 페이지 북마크 해제",
+    "move_to_root": "루트로 이동",
+    "root": "루트 (기본값)"
+  },
+  "v5_page_migration": {
+    "page_tree_not_avaliable": "페이지 트리 기능은 아직 사용할 수 없습니다.",
+    "go_to_settings": "기능을 활성화하려면 설정으로 이동"
+  },
+  "tag_edit_modal": {
+    "edit_tags": "태그 편집",
+    "done": "완료",
+    "tags_input": {
+      "tag_name": "태그 이름"
+    }
+  },
+  "delete_attachment_modal": {
+    "confirm_delete_attachment": "첨부 파일 삭제?"
+  },
+  "rich_attachment": {
+    "attachment_not_be_found": "첨부 파일을 찾을 수 없습니다."
+  },
+  "page_select_modal": {
+    "select_page_location": "페이지 위치 선택"
+  },
+  "wip_page": {
+    "save_as_wip": "WIP로 저장 (작성 중)",
+    "success_save_as_wip": "WIP 페이지로 성공적으로 저장됨",
+    "fail_save_as_wip": "WIP 페이지로 저장 실패",
+    "alert": "이 페이지는 아직 작성 중입니다.",
+    "publish_page": "페이지 게시",
+    "success_publish_page": "페이지가 게시되었습니다.",
+    "fail_publish_page": "페이지 게시 실패"
+  },
+  "sidebar_header": {
+    "show_wip_page": "WIP 표시",
+    "compact_view": "간략 보기"
+  },
+  "create_page": {
+    "untitled": "제목 없음"
+  },
+  "sync-latest-revision-body": {
+    "menuitem": "편집기 텍스트를 최신 수정 본문과 동기화",
+    "confirm": "편집기에 입력된 초안 데이터를 삭제하고 최신 텍스트를 동기화합니다. 실행하시겠습니까?",
+    "alert": "최신 텍스트가 동기화되지 않았을 수 있습니다. 다시 로드하고 다시 확인하십시오.",
+    "success-toaster": "최신 텍스트 동기화됨",
+    "skipped-toaster": "편집기가 활성화되지 않아 동기화 건너뜀. 편집기를 열고 다시 시도하십시오.",
+    "error-toaster": "최신 텍스트 동기화 실패"
+  }
+}

+ 227 - 237
apps/app/public/static/locales/zh_CN/admin.json

@@ -28,21 +28,21 @@
     "always_hidden": "总是隐藏",
     "always_hidden": "总是隐藏",
     "Guest Users Access": "来宾用户访问",
     "Guest Users Access": "来宾用户访问",
     "readonly_users_access": "只浏览用户的访问",
     "readonly_users_access": "只浏览用户的访问",
-		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
-		"register_limitation": "注册限制",
-		"register_limitation_desc": "限制新用户注册",
-		"The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
-		"users_without_account": "无法访问没有帐户的用户",
-		"example": "例子",
-		"restrict_emails": "您可以通过编写电子邮件域(以@开头)将电子邮件注册限制为wiki。",
-		"for_example": " 例如,如果要将注册限制为growi.org网站域,你可以写",
-		"in_this_case": ";在这种情况下,只有growi.org网站域将能够注册,所有其他用户将被拒绝。",
-		"insert_single": "请每行插入一个电子邮件地址。",
+    "Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
+    "register_limitation": "注册限制",
+    "register_limitation_desc": "限制新用户注册",
+    "The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
+    "users_without_account": "无法访问没有帐户的用户",
+    "example": "例子",
+    "restrict_emails": "您可以通过编写电子邮件域(以@开头)将电子邮件注册限制为wiki。",
+    "for_example": " 例如,如果要将注册限制为growi.org网站域,你可以写",
+    "in_this_case": ";在这种情况下,只有growi.org网站域将能够注册,所有其他用户将被拒绝。",
+    "insert_single": "请每行插入一个电子邮件地址。",
     "page_list_and_search_results": "页面列表/搜索结果",
     "page_list_and_search_results": "页面列表/搜索结果",
-		"page_listing_1": "页面列表/搜索<br>受“仅限我”限制",
-		"page_listing_1_desc": "列出/搜索时显示受“仅限我”选项限制的页面",
-		"page_listing_2": "页面列表/搜索<br>受用户组限制",
-		"page_listing_2_desc": "显示列出/搜索时受用户组限制的页面",
+    "page_listing_1": "页面列表/搜索<br>受“仅限我”限制",
+    "page_listing_1_desc": "列出/搜索时显示受“仅限我”选项限制的页面",
+    "page_listing_2": "页面列表/搜索<br>受用户组限制",
+    "page_listing_2_desc": "显示列出/搜索时受用户组限制的页面",
     "page_access_rights": "页面访问",
     "page_access_rights": "页面访问",
     "page_delete_rights": "删除权限",
     "page_delete_rights": "删除权限",
     "page_delete": "删除",
     "page_delete": "删除",
@@ -56,9 +56,9 @@
     "is_all_group_membership_required_for_page_complete_deletion": "除管理员和页面作者之外的用户必须属于被授予页面访问权限的所有组",
     "is_all_group_membership_required_for_page_complete_deletion": "除管理员和页面作者之外的用户必须属于被授予页面访问权限的所有组",
     "is_all_group_membership_required_for_page_complete_deletion_explanation": "如果页面权限设置为\"仅限特定群体\",则会启用此功能。",
     "is_all_group_membership_required_for_page_complete_deletion_explanation": "如果页面权限设置为\"仅限特定群体\",则会启用此功能。",
     "inherit": "继承(使用与单页相同的设置)。",
     "inherit": "继承(使用与单页相同的设置)。",
-		"admin_only": "仅管理员",
-		"admin_and_author": "管理员|作者",
-		"anyone": "任何人",
+    "admin_only": "仅管理员",
+    "admin_and_author": "管理员|作者",
+    "anyone": "任何人",
     "user_homepage_deletion": {
     "user_homepage_deletion": {
       "user_homepage_deletion": "删除用户主页",
       "user_homepage_deletion": "删除用户主页",
       "enable_user_homepage_deletion": "启用用户主页删除功能",
       "enable_user_homepage_deletion": "启用用户主页删除功能",
@@ -71,15 +71,15 @@
     "max_age_caution": "修改该值后需要重启服务器。",
     "max_age_caution": "修改该值后需要重启服务器。",
     "forced_update_desc": "设置已被强行更改。以前的设置: ",
     "forced_update_desc": "设置已被强行更改。以前的设置: ",
     "page_delete_rights_caution": "\"删除/全部删除\"权限(包括后代页面)被强制强于\"删除/完全删除\"权限。 <br> <br> 仅管理员 > 管理员|作者 > 何人",
     "page_delete_rights_caution": "\"删除/全部删除\"权限(包括后代页面)被强制强于\"删除/完全删除\"权限。 <br> <br> 仅管理员 > 管理员|作者 > 何人",
-		"Authentication mechanism settings": "身份验证机制设置",
-		"setup_is_not_yet_complete": "安装尚未完成",
-		"xss_prevent_setting": "阻止XSS(跨站点脚本)",
-		"xss_prevent_setting_link": "转到Markdown设置",
-		"callback_URL": "回调URL",
-		"providerName": "提供程序名称",
-		"issuerHost": "发行者主机",
-		"scope": "Scope",
-		"desc_of_callback_URL": "在{{AuthName}}身份提供程序的设置中使用它",
+    "Authentication mechanism settings": "身份验证机制设置",
+    "setup_is_not_yet_complete": "安装尚未完成",
+    "xss_prevent_setting": "阻止XSS(跨站点脚本)",
+    "xss_prevent_setting_link": "转到Markdown设置",
+    "callback_URL": "回调URL",
+    "providerName": "提供程序名称",
+    "issuerHost": "发行者主机",
+    "scope": "Scope",
+    "desc_of_callback_URL": "在{{AuthName}}身份提供程序的设置中使用它",
     "authorization_endpoint": "Authorization Endpoint",
     "authorization_endpoint": "Authorization Endpoint",
     "token_endpoint": "Token Endpoint",
     "token_endpoint": "Token Endpoint",
     "revocation_endpoint": "Revocation Endpoint",
     "revocation_endpoint": "Revocation Endpoint",
@@ -88,41 +88,41 @@
     "end_session_endpoint": "EndSessioin Endpoint",
     "end_session_endpoint": "EndSessioin Endpoint",
     "registration_endpoint": "Registration Endpoint",
     "registration_endpoint": "Registration Endpoint",
     "jwks_uri": "JSON Web Key Set URL",
     "jwks_uri": "JSON Web Key Set URL",
-		"clientID": "Client ID",
-		"client_secret": "客户机密",
-		"updated_general_security_setting": "更新安全设置成功",
-		"setup_not_completed_yet": "安装尚未完成",
+    "clientID": "Client ID",
+    "client_secret": "客户机密",
+    "updated_general_security_setting": "更新安全设置成功",
+    "setup_not_completed_yet": "安装尚未完成",
     "guest_mode": {
     "guest_mode": {
-			"deny": "拒绝(仅限注册用户)",
-			"readonly": "接受(来宾可以只读)"
-		},
+      "deny": "拒绝(仅限注册用户)",
+      "readonly": "接受(来宾可以只读)"
+    },
     "read_only_users_comment": {
     "read_only_users_comment": {
       "deny": "拒绝 (禁止只浏览用户操作评论)",
       "deny": "拒绝 (禁止只浏览用户操作评论)",
       "accept": "允许 (只浏览用户可以管理评论)"
       "accept": "允许 (只浏览用户可以管理评论)"
     },
     },
-		"registration_mode": {
-			"open": "打开(任何人都可以注册)",
-			"restricted": "受限(需要管理员批准)",
-			"closed": "已关闭(仅限邀请)"
-		},
+    "registration_mode": {
+      "open": "打开(任何人都可以注册)",
+      "restricted": "受限(需要管理员批准)",
+      "closed": "已关闭(仅限邀请)"
+    },
     "share_link_management": "Share Link Management",
     "share_link_management": "Share Link Management",
-    "No_share_links":"No share links",
-    "share_link_notice":"remove all share links",
-    "delete_all_share_links":"Delete all share links",
+    "No_share_links": "No share links",
+    "share_link_notice": "remove all share links",
+    "delete_all_share_links": "Delete all share links",
     "share_link_rights": "分享链接权",
     "share_link_rights": "分享链接权",
     "enable_link_sharing": "启用链接共享",
     "enable_link_sharing": "启用链接共享",
     "all_share_links": "所有共享链接",
     "all_share_links": "所有共享链接",
-		"configuration": " 配置",
-		"Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
-		"Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
-		"Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>email</code> match",
-		"Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>email</code>.",
-		"Use env var if empty": "Use env var <code>{{env}}</code> if empty",
-		"Use default if both are empty": "If both ​​are empty, the default value <code>{{target}}</code> is used.",
-		"missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
-		"Local": {
-			"name": "ID/Password",
-			"note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+    "configuration": " 配置",
+    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
+    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
+    "Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>email</code> match",
+    "Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>email</code>.",
+    "Use env var if empty": "Use env var <code>{{env}}</code> if empty",
+    "Use default if both are empty": "If both ​​are empty, the default value <code>{{target}}</code> is used.",
+    "missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
+    "Local": {
+      "name": "ID/Password",
+      "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
       "enable_local": "Enable ID/Password",
       "enable_local": "Enable ID/Password",
       "password_reset_by_users": "用户重置密码",
       "password_reset_by_users": "用户重置密码",
       "enable_password_reset_by_users": "启用用户重置密码",
       "enable_password_reset_by_users": "启用用户重置密码",
@@ -130,181 +130,181 @@
       "email_authentication": "用户注册时的电子邮件身份验证",
       "email_authentication": "用户注册时的电子邮件身份验证",
       "enable_email_authentication": "启用电子邮件身份验证",
       "enable_email_authentication": "启用电子邮件身份验证",
       "enable_email_authentication_desc": "用户注册将执行电子邮件身份验证。"
       "enable_email_authentication_desc": "用户注册将执行电子邮件身份验证。"
-		},
-		"ldap": {
-			"enable_ldap": "Enable LDAP",
-			"server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
-			"bind_mode": "Binding Mode",
-			"bind_manager": "Manager Bind",
-			"bind_user": "User Bind",
-			"bind_DN_manager_detail": "The DN of the account that authenticates and queries the directory service",
-			"bind_DN_user_detail1": "The query used to bind with the directory service.",
-			"bind_DN_user_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
-			"bind_DN_password": "Bind DN Password",
-			"bind_DN_password_manager_detail": "The password for the Bind DN account.",
-			"bind_DN_password_user_detail": "The password that is entered in the login page will be used to bind.",
-			"search_filter": "Search Filter",
-			"search_filter_detail1": "The query used to locate the authenticated user.",
-			"search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
-			"search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
-			"search_filter_example1": "Match with 'uid' or 'mail'",
-			"search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
-			"username_detail": "Specification of mappings for <code>username</code> when creating new users",
-			"name_detail": "Specification of mappings for full name when creating new users",
-			"mail_detail": "Specification of mappings for mail address when creating new users",
-			"group_search_base_DN": "Group Search Base DN",
-			"group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
-			"group_search_filter": "Group Search Filter",
-			"group_search_filter_detail1": "The query used to filter for groups.",
-			"group_search_filter_detail2": "Login via LDAP is accepted only when this query hits one or more groups.",
-			"group_search_filter_detail3": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
-			"group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
-			"group_search_user_DN_property": "User DN Property",
-			"group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
-			"test_config": "Test Saved Configuration",
-			"updated_ldap": "Succeeded to update LDAP setting"
-		},
-		"SAML": {
-			"name": "SAML",
-			"enable_saml": "Enable SAML",
-			"id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
-			"username_detail": "Specification of mappings for <code>username</code> when creating new users",
-			"mapping_detail": "Specification of mappings for {{target}} when creating new users",
-			"cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
-			"Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
-			"note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-			"attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
-			"attr_based_login_control_rule_help": "<h5>Supported Queries:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>Unsupported Queries:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>Escaping special characters</h5>It is needed to escape following special characters:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
-			"attr_based_login_control_rule_example1": "<h5>Example for conditions</h5>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
+    },
+    "ldap": {
+      "enable_ldap": "Enable LDAP",
+      "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
+      "bind_mode": "Binding Mode",
+      "bind_manager": "Manager Bind",
+      "bind_user": "User Bind",
+      "bind_DN_manager_detail": "The DN of the account that authenticates and queries the directory service",
+      "bind_DN_user_detail1": "The query used to bind with the directory service.",
+      "bind_DN_user_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
+      "bind_DN_password": "Bind DN Password",
+      "bind_DN_password_manager_detail": "The password for the Bind DN account.",
+      "bind_DN_password_user_detail": "The password that is entered in the login page will be used to bind.",
+      "search_filter": "Search Filter",
+      "search_filter_detail1": "The query used to locate the authenticated user.",
+      "search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
+      "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
+      "search_filter_example1": "Match with 'uid' or 'mail'",
+      "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
+      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+      "name_detail": "Specification of mappings for full name when creating new users",
+      "mail_detail": "Specification of mappings for mail address when creating new users",
+      "group_search_base_DN": "Group Search Base DN",
+      "group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
+      "group_search_filter": "Group Search Filter",
+      "group_search_filter_detail1": "The query used to filter for groups.",
+      "group_search_filter_detail2": "Login via LDAP is accepted only when this query hits one or more groups.",
+      "group_search_filter_detail3": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
+      "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
+      "group_search_user_DN_property": "User DN Property",
+      "group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
+      "test_config": "Test Saved Configuration",
+      "updated_ldap": "Succeeded to update LDAP setting"
+    },
+    "SAML": {
+      "name": "SAML",
+      "enable_saml": "Enable SAML",
+      "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
+      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+      "mapping_detail": "Specification of mappings for {{target}} when creating new users",
+      "cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
+      "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
+      "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+      "attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
+      "attr_based_login_control_rule_help": "<h5>Supported Queries:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>Unsupported Queries:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>Escaping special characters</h5>It is needed to escape following special characters:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
+      "attr_based_login_control_rule_example1": "<h5>Example for conditions</h5>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
-		},
-		"OAuth": {
-			"enable_oidc": "Enable OIDC",
-			"register": "Register for %s",
-			"change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
-			"Google": {
-				"enable_google": "Enable Google OAuth",
-				"name": "Google OAuth",
-				"register_1": "Access {{link}}",
-				"register_2": "Create Project if no projects exist",
-				"register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-				"register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
-				"register_5": "Copy and paste your ClientID and Client Secret above",
-				"updated_google": "Succeeded to update Google OAuth setting"
-			},
-			"GitHub": {
-				"enable_github": "Enable GitHub OAuth",
-				"name": "GitHub OAuth",
-				"register_1": "Access {{link}}",
-				"register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
-				"register_3": "Copy and paste your ClientID and Client Secret above",
-				"updated_github": "Succeeded to update GitHub OAuth setting"
-			},
-			"OIDC": {
-				"name": "OpenID Connect",
-				"id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
-				"username_detail": "Specification of mappings for <code>username</code> when creating new users",
-				"name_detail": "Specification of mappings for <code>name</code> when creating new users",
-				"mapping_detail": "Specification of mappings for {{target}} when creating new users",
-				"register_1": "Contact to OIDC IdP Administrator",
-				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>{{url}}</code>",
-				"register_3": "Copy and paste your ClientID and Client Secret above",
-				"updated_oidc": "Succeeded to update OpenID Connect",
+    },
+    "OAuth": {
+      "enable_oidc": "Enable OIDC",
+      "register": "Register for %s",
+      "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
+      "Google": {
+        "enable_google": "Enable Google OAuth",
+        "name": "Google OAuth",
+        "register_1": "Access {{link}}",
+        "register_2": "Create Project if no projects exist",
+        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
+        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
+        "register_5": "Copy and paste your ClientID and Client Secret above",
+        "updated_google": "Succeeded to update Google OAuth setting"
+      },
+      "GitHub": {
+        "enable_github": "Enable GitHub OAuth",
+        "name": "GitHub OAuth",
+        "register_1": "Access {{link}}",
+        "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
+        "register_3": "Copy and paste your ClientID and Client Secret above",
+        "updated_github": "Succeeded to update GitHub OAuth setting"
+      },
+      "OIDC": {
+        "name": "OpenID Connect",
+        "id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
+        "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+        "name_detail": "Specification of mappings for <code>name</code> when creating new users",
+        "mapping_detail": "Specification of mappings for {{target}} when creating new users",
+        "register_1": "Contact to OIDC IdP Administrator",
+        "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>{{url}}</code>",
+        "register_3": "Copy and paste your ClientID and Client Secret above",
+        "updated_oidc": "Succeeded to update OpenID Connect",
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
         "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
-			},
-			"how_to": {
-				"google": "How to configure Google OAuth?",
-				"github": "How to configure GitHub OAuth?",
-				"oidc": "How to configure OIDC?"
-			}
-		},
-		"form_item_name": {
-			"entryPoint": "Entry point",
-			"issuer": "Issuer",
-			"cert": "Certificate",
-			"attrMapId": "ID",
-			"attrMapUsername": "Username",
-			"attrMapMail": "Mail Address",
-			"attrMapFirstName": "First Name",
-			"attrMapLastName": "Last Name",
-			"ABLCRule": "Rule"
-		}
+      },
+      "how_to": {
+        "google": "How to configure Google OAuth?",
+        "github": "How to configure GitHub OAuth?",
+        "oidc": "How to configure OIDC?"
+      }
+    },
+    "form_item_name": {
+      "entryPoint": "Entry point",
+      "issuer": "Issuer",
+      "cert": "Certificate",
+      "attrMapId": "ID",
+      "attrMapUsername": "Username",
+      "attrMapMail": "Mail Address",
+      "attrMapFirstName": "First Name",
+      "attrMapLastName": "Last Name",
+      "ABLCRule": "Rule"
+    }
   },
   },
   "notification_settings": {
   "notification_settings": {
     "notification_settings": "通知设置",
     "notification_settings": "通知设置",
-		"slack_incoming_configuration": "Slack Incoming Webhooks configuration",
-		"prioritize_webhook": "Prioritize incoming webhook than Slack App",
-		"prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
-		"slack_app_configuration": "Slack app configuration",
-		"slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
-		"use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
-		"how_to": {
-			"header": "How to configure Incoming Webhooks?",
-			"workspace": "(At Workspace) Add a hook",
-			"workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
-			"workspace_desc2": "Choose the default channel to post.",
-			"workspace_desc3": "Add.",
-			"at_growi": "(At GROWI admin page) Set Webhook URL",
-			"at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
-		},
-		"user_trigger_notification_header": "Default notification settings for patterns",
-		"pattern": "Pattern",
-		"channel": "Channel",
-		"pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
-		"channel_desc": "Slack channel name. Without <code>#</code>.",
-		"valid_page": "启用/禁用通知",
-		"link_notification_help": "<strong>只有那些知道“链接的任何人”链接的人才能查看的页面并不总是得到通知。</strong> ",
-		"just_me_notification_help": "<strong>被“仅限我”限制的页在编辑时被通知。</strong>",
-		"group_notification_help": "<strong>被“用户组”限制的页面在编辑时被通知。</strong>",
-		"notification_list": "List of notification settings",
-		"add_notification": "Add new",
-		"trigger_path": "Trigger path",
-		"trigger_path_help": "(expression with <code>*</code> is supported)",
-		"trigger_events": "Trigger events",
-		"notify_to": "Notify to",
-		"back_to_list": "Go back to list",
-		"notification_detail": "Notification Setting Details",
-		"event_pageCreate": "When new page is \"CREATED\"",
-		"event_pageEdit": "When page is \"EDITED\"",
-		"event_pageDelete": "When page is \"DELETED\"",
-		"event_pageMove": "When page is \"MOVED\" (renamed)",
-		"event_pageLike": "When someone \"LIKES\" page",
-		"event_comment": "When someone \"COMMENTS\" on page",
-		"email": {
-			"ifttt_link": "Create a new IFTTT applet with Email trigger"
-		},
-		"updated_slackApp": "Succeeded to update Slack App Configuration setting",
-		"add_notification_pattern": "Add user trigger notification patterns",
-		"delete_notification_pattern": "Delete notification pattern",
-		"delete_notification_pattern_desc1": "Delete Path: {{path}}",
-		"delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
-		"toggle_notification": "Updated setting of {{path}}",
+    "slack_incoming_configuration": "Slack Incoming Webhooks configuration",
+    "prioritize_webhook": "Prioritize incoming webhook than Slack App",
+    "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
+    "slack_app_configuration": "Slack app configuration",
+    "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
+    "use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
+    "how_to": {
+      "header": "How to configure Incoming Webhooks?",
+      "workspace": "(At Workspace) Add a hook",
+      "workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
+      "workspace_desc2": "Choose the default channel to post.",
+      "workspace_desc3": "Add.",
+      "at_growi": "(At GROWI admin page) Set Webhook URL",
+      "at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
+    },
+    "user_trigger_notification_header": "Default notification settings for patterns",
+    "pattern": "Pattern",
+    "channel": "Channel",
+    "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
+    "channel_desc": "Slack channel name. Without <code>#</code>.",
+    "valid_page": "启用/禁用通知",
+    "link_notification_help": "<strong>只有那些知道“链接的任何人”链接的人才能查看的页面并不总是得到通知。</strong> ",
+    "just_me_notification_help": "<strong>被“仅限我”限制的页在编辑时被通知。</strong>",
+    "group_notification_help": "<strong>被“用户组”限制的页面在编辑时被通知。</strong>",
+    "notification_list": "List of notification settings",
+    "add_notification": "Add new",
+    "trigger_path": "Trigger path",
+    "trigger_path_help": "(expression with <code>*</code> is supported)",
+    "trigger_events": "Trigger events",
+    "notify_to": "Notify to",
+    "back_to_list": "Go back to list",
+    "notification_detail": "Notification Setting Details",
+    "event_pageCreate": "When new page is \"CREATED\"",
+    "event_pageEdit": "When page is \"EDITED\"",
+    "event_pageDelete": "When page is \"DELETED\"",
+    "event_pageMove": "When page is \"MOVED\" (renamed)",
+    "event_pageLike": "When someone \"LIKES\" page",
+    "event_comment": "When someone \"COMMENTS\" on page",
+    "email": {
+      "ifttt_link": "Create a new IFTTT applet with Email trigger"
+    },
+    "updated_slackApp": "Succeeded to update Slack App Configuration setting",
+    "add_notification_pattern": "Add user trigger notification patterns",
+    "delete_notification_pattern": "Delete notification pattern",
+    "delete_notification_pattern_desc1": "Delete Path: {{path}}",
+    "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
+    "toggle_notification": "Updated setting of {{path}}",
     "not_found_global_notification_triggerid": "未找到全局通知 ID"
     "not_found_global_notification_triggerid": "未找到全局通知 ID"
-	},
+  },
   "full_text_search_management": {
   "full_text_search_management": {
     "full_text_search_management": "全文搜索管理",
     "full_text_search_management": "全文搜索管理",
-		"elasticsearch_management": "Elasticsearch管理",
-		"connection_status": "连接状态",
-		"connection_status_label_unconfigured": "未配置",
-		"connection_status_label_connected": "已连接",
-		"connection_status_label_disconnected": "断开的",
-		"connection_status_label_erroroccured": "搜索服务出错",
-		"indices_status": "索引状态",
-		"indices_status_label_normalized": "标准化",
-		"indices_status_label_unnormalized": "重建或损坏",
-		"indices_summary": "索引摘要",
-		"reconnect": "重新连接",
-		"reconnect_button": "尝试重新连接",
-		"reconnect_description": "单击按钮尝试重新连接到Elasticsearch。",
-		"normalize": "规范化",
-		"normalize_button": "规范化索引",
-		"normalize_description": "单击按钮修复损坏的索引。",
-		"rebuild": "重建",
-		"rebuild_button": "重建索引",
-		"rebuild_description_1": "单击按钮以重新生成索引并添加所有页面数据。",
-		"rebuild_description_2": "这可能需要一段时间。"
-	},
+    "elasticsearch_management": "Elasticsearch管理",
+    "connection_status": "连接状态",
+    "connection_status_label_unconfigured": "未配置",
+    "connection_status_label_connected": "已连接",
+    "connection_status_label_disconnected": "断开的",
+    "connection_status_label_erroroccured": "搜索服务出错",
+    "indices_status": "索引状态",
+    "indices_status_label_normalized": "标准化",
+    "indices_status_label_unnormalized": "重建或损坏",
+    "indices_summary": "索引摘要",
+    "reconnect": "重新连接",
+    "reconnect_button": "尝试重新连接",
+    "reconnect_description": "单击按钮尝试重新连接到Elasticsearch。",
+    "normalize": "规范化",
+    "normalize_button": "规范化索引",
+    "normalize_description": "单击按钮修复损坏的索引。",
+    "rebuild": "重建",
+    "rebuild_button": "重建索引",
+    "rebuild_description_1": "单击按钮以重新生成索引并添加所有页面数据。",
+    "rebuild_description_2": "这可能需要一段时间。"
+  },
   "mailer_setup_required": "<a href='/admin/app'>Email settings</a> are required to send.",
   "mailer_setup_required": "<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
   "admin_top": {
     "management_wiki": "管理Wiki",
     "management_wiki": "管理Wiki",
@@ -420,17 +420,7 @@
     "enable": "启用",
     "enable": "启用",
     "disable": "停用",
     "disable": "停用",
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <cod>{{variable}}</code> 启用。",
     "use_env_var_if_empty": "如果数据库中的值为空,则环境变量的值 <cod>{{variable}}</code> 启用。",
-    "note_for_the_only_env_option": "The GCS settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-    "questionnaire_settings": "问卷设置",
-    "questionnaire_settings_explanation": "这将在整个系统上启用/禁用问卷。 启用后,用户还可以在个人设置页面的“其他设置”中单独启用/禁用问卷调查。",
-    "about_data_sent": "关于发送的信息",
-    "docs_link": "https://docs.growi.org/en/admin-guide/management-cookbook/app-settings.html#questionnaire-settings",
-    "learn_more": "细节",
-    "other_info_will_be_sent": "与问卷回答一起,将发送改进 GROWI 所需的信息。个人用户信息将不包含在发送的数据中。",
-    "we_will_use_the_data_to_improve_growi": "我们将使用这些数据尽可能地改善 GROWI 体验。",
-    "anonymize_app_site_url": "在发送的数据中匿名应用程序站点 URL",
-    "url_anonymization_explanation": "问卷答案中包含的应用站点URL将被匿名化,启用后将不会识别发送问卷答案的GROWI应用。",
-    "enable_questionnaire": "启用问卷"
+    "note_for_the_only_env_option": "The GCS settings is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> ."
   },
   },
   "markdown_settings": {
   "markdown_settings": {
     "markdown_settings": "Markdown设置",
     "markdown_settings": "Markdown设置",
@@ -511,13 +501,13 @@
       "show_page_side_authors": "在目录上方始终显示创建者和更新者",
       "show_page_side_authors": "在目录上方始终显示创建者和更新者",
       "show_page_side_authors_desc": "在页面侧边栏的目录上方显示创建者和最后更新者的信息。"
       "show_page_side_authors_desc": "在页面侧边栏的目录上方显示创建者和最后更新者的信息。"
     },
     },
-      "presentation": "表达",
-      "presentation_options": {
+    "presentation": "表达",
+    "presentation_options": {
       "enable_marp": "启用 Marp",
       "enable_marp": "启用 Marp",
       "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
       "enable_marp_desc": "Marp 可在演示视图中使用。该选项可能会使您受到 XSS 的攻击。",
       "marp_official_site": "参考资料:Marp 官方网站",
       "marp_official_site": "参考资料:Marp 官方网站",
       "marp_official_site_link": "https://marp.app",
       "marp_official_site_link": "https://marp.app",
-      "marp_in_growi" : "参考资料:GROWI Docs - Create slide using Marp",
+      "marp_in_growi": "参考资料:GROWI Docs - Create slide using Marp",
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
       "marp_in_growi_link": "https://docs.growi.org/en/guide/features/marp.html"
     },
     },
     "custom_title": "自定义标题",
     "custom_title": "自定义标题",
@@ -531,7 +521,7 @@
     "write_css": "您可以编写应用于整个系统的CSS。",
     "write_css": "您可以编写应用于整个系统的CSS。",
     "ctrl_space": "Ctrl+Space 自动完成",
     "ctrl_space": "Ctrl+Space 自动完成",
     "custom_script": "定制纸条",
     "custom_script": "定制纸条",
-    "custom_presentation":"表达",
+    "custom_presentation": "表达",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "reflect_change": "您需要重新加载页面以反映更改。",
     "reflect_change": "您需要重新加载页面以反映更改。",
     "custom_logo": "自定义徽标",
     "custom_logo": "自定义徽标",
@@ -727,7 +717,7 @@
       "allow_specified_long": "允许指定(只允许来自指定的渠道)",
       "allow_specified_long": "允许指定(只允许来自指定的渠道)",
       "test_connection": "测试连接",
       "test_connection": "测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
-      "test_connection_only_public_channel":"请在一个公共频道中测试连接",
+      "test_connection_only_public_channel": "请在一个公共频道中测试连接",
       "error_check_logs_below": "发生了错误。请检查以下日志。",
       "error_check_logs_below": "发生了错误。请检查以下日志。",
       "send_message_to_slack_work_space": "发送到 Slack 工作区。",
       "send_message_to_slack_work_space": "发送到 Slack 工作区。",
       "add_slack_workspace": "添加Slack Workspace"
       "add_slack_workspace": "添加Slack Workspace"
@@ -988,7 +978,7 @@
     "ADMIN_SITE_URL_UPDATE": "更新站点 URL 设置",
     "ADMIN_SITE_URL_UPDATE": "更新站点 URL 设置",
     "ADMIN_MAIL_SMTP_UPDATE": "更新电子邮件(SMTP)设置",
     "ADMIN_MAIL_SMTP_UPDATE": "更新电子邮件(SMTP)设置",
     "ADMIN_MAIL_SES_UPDATE": "更新电子邮件(SES)设置",
     "ADMIN_MAIL_SES_UPDATE": "更新电子邮件(SES)设置",
-    "ADMIN_MAIL_TEST_SUBMIT" : "发送测试邮件",
+    "ADMIN_MAIL_TEST_SUBMIT": "发送测试邮件",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "更新文件上传设置",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "更新文件上传设置",
     "ADMIN_PLUGIN_UPDATE": "更新插件设置",
     "ADMIN_PLUGIN_UPDATE": "更新插件设置",
     "ADMIN_MAINTENANCEMODE_ENABLED": "启用维护模式",
     "ADMIN_MAINTENANCEMODE_ENABLED": "启用维护模式",
@@ -1140,8 +1130,8 @@
     "revoke_user_admin": "Succeeded to revoke {{username}} admin",
     "revoke_user_admin": "Succeeded to revoke {{username}} admin",
     "grant_user_read_only": "Succeeded to grant {{username}} read only",
     "grant_user_read_only": "Succeeded to grant {{username}} read only",
     "revoke_user_read_only": "Succeeded to revoke {{username}} read only",
     "revoke_user_read_only": "Succeeded to revoke {{username}} read only",
-		"activate_user_success": "Succeeded to activating {{username}}",
-		"deactivate_user_success": "Succeeded to deactivate {{username}}",
+    "activate_user_success": "Succeeded to activating {{username}}",
+    "deactivate_user_success": "Succeeded to deactivate {{username}}",
     "remove_user_success": "Succeeded to removing {{username}}",
     "remove_user_success": "Succeeded to removing {{username}}",
     "remove_external_user_success": "Succeeded to remove {{accountId}}",
     "remove_external_user_success": "Succeeded to remove {{accountId}}",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",
     "switch_disable_link_sharing_success": "成功更新分享链接设置",

+ 24 - 60
apps/app/public/static/locales/zh_CN/commons.json

@@ -1,16 +1,15 @@
 {
 {
-	"Show": "显示",
-	"Hide": "隐藏",
+  "Show": "显示",
+  "Hide": "隐藏",
   "Add": "添加",
   "Add": "添加",
   "Insert": "插入",
   "Insert": "插入",
   "Reset": "重启",
   "Reset": "重启",
-	"Sign out": "退出",
+  "Sign out": "退出",
   "New": "新建",
   "New": "新建",
   "Send": "发送",
   "Send": "发送",
   "Close": "关闭",
   "Close": "关闭",
   "Done": "完成",
   "Done": "完成",
   "Delete": "删除",
   "Delete": "删除",
-
   "meta": {
   "meta": {
     "display_name": "简体中文"
     "display_name": "简体中文"
   },
   },
@@ -33,52 +32,46 @@
   "headers": {
   "headers": {
     "app_settings": "系统设置"
     "app_settings": "系统设置"
   },
   },
-
   "header_search_box": {
   "header_search_box": {
-		"label": {
-			"All pages": "所有页面",
-			"This tree": "当前分支"
-		},
-		"item_label": {
-			"All pages": "所有页面",
-			"This tree": "当前分支以下内容"
-		}
+    "label": {
+      "All pages": "所有页面",
+      "This tree": "当前分支"
+    },
+    "item_label": {
+      "All pages": "所有页面",
+      "This tree": "当前分支以下内容"
+    }
   },
   },
-
   "search_method_menu_item": {
   "search_method_menu_item": {
     "search_in_all": "所有页面",
     "search_in_all": "所有页面",
     "only_children_of_this_tree": "当前分支以下内容",
     "only_children_of_this_tree": "当前分支以下内容",
     "exact_mutch": "完全匹配"
     "exact_mutch": "完全匹配"
   },
   },
-
   "share_links": {
   "share_links": {
     "Share Link": "Share Link",
     "Share Link": "Share Link",
     "Page Path": "Page Path",
     "Page Path": "Page Path",
     "expire": "Expiration",
     "expire": "Expiration",
     "description": "Description"
     "description": "Description"
   },
   },
-
   "in_app_notification": {
   "in_app_notification": {
     "notification_list": "应用内通知列表",
     "notification_list": "应用内通知列表",
     "see_all": "查看通知列表",
     "see_all": "查看通知列表",
     "no_notification": "您没有任何通知",
     "no_notification": "您没有任何通知",
     "all": "全部",
     "all": "全部",
     "unopend": "未读",
     "unopend": "未读",
-    "mark_all_as_read" : "标记为已读",
+    "mark_all_as_read": "标记为已读",
     "no_unread_messages": "no_unread_messages",
     "no_unread_messages": "no_unread_messages",
     "only_unread": "Only unread"
     "only_unread": "Only unread"
   },
   },
-
   "personal_dropdown": {
   "personal_dropdown": {
     "home": "家",
     "home": "家",
     "settings": "设置",
     "settings": "设置",
-		"color_mode": "颜色模式",
-		"sidebar_mode": "边栏模式",
-		"sidebar_mode_editor": "编辑器上的边栏模式",
-		"use_os_settings": "使用操作系统设置",
+    "color_mode": "颜色模式",
+    "sidebar_mode": "边栏模式",
+    "sidebar_mode_editor": "编辑器上的边栏模式",
+    "use_os_settings": "使用操作系统设置",
     "feedback": "意见和要求"
     "feedback": "意见和要求"
   },
   },
-
   "create_page_dropdown": {
   "create_page_dropdown": {
     "new_page": "新页面",
     "new_page": "新页面",
     "open_page_create_modal": "打开新页面创建模式",
     "open_page_create_modal": "打开新页面创建模式",
@@ -92,24 +85,21 @@
       "descendants": "子代模板"
       "descendants": "子代模板"
     }
     }
   },
   },
-
-	"copy_to_clipboard": {
-		"Copy to clipboard": "复制到剪贴板",
-		"Page path": "页面路径",
-		"Page URL": "页面Url",
-		"Parmanent link": "参数化链接",
-		"Page path and parmanent link": "页面路径及参数化链接",
-		"Markdown link": "Markdown链接",
+  "copy_to_clipboard": {
+    "Copy to clipboard": "复制到剪贴板",
+    "Page path": "页面路径",
+    "Page URL": "页面Url",
+    "Parmanent link": "参数化链接",
+    "Page path and parmanent link": "页面路径及参数化链接",
+    "Markdown link": "Markdown链接",
     "Append params": "Append params"
     "Append params": "Append params"
-	},
-
+  },
   "crop_image_modal": {
   "crop_image_modal": {
     "image_crop": "图像裁剪",
     "image_crop": "图像裁剪",
     "crop": "修剪",
     "crop": "修剪",
     "save": "节省",
     "save": "节省",
     "cancel": "取消"
     "cancel": "取消"
   },
   },
-
   "handsontable_modal": {
   "handsontable_modal": {
     "title": "编辑表格",
     "title": "编辑表格",
     "data_import": "数据导入",
     "data_import": "数据导入",
@@ -125,35 +115,9 @@
       "import": "导入"
       "import": "导入"
     }
     }
   },
   },
-
-  "questionnaire_modal": {
-    "required": "必需的",
-    "submit": "发送",
-    "close": "Close",
-    "title": "改善服务的GROWI调查表",
-    "more_satisfied_services": "我们希望让使用GROWI的人更加满意",
-    "strive_to_improve_services": "我们将利用你的反馈来改善我们的服务。",
-    "length_of_experience": {
-      "more_than_two_years": "2年以上",
-      "one_to_two_years": "超过1年但少于2年",
-      "six_months_to_one_year": "超过6个月但少于1年",
-      "three_months_to_six_months": "超过3个月但少于6个月",
-      "one_month_to_three_months": "超过1个月但少于3个月",
-      "less_than_one_month": "不到1个月"
-    },
-    "satisfaction_with_growi": "对GROWI的满意程度",
-    "history_of_growi_usage": "GROWI的使用历史",
-    "occupation": "职位",
-    "position": "职业类型",
-    "comment_on_growi": "关于GROWI的评论",
-    "successfully_submitted": "问卷已经发出。",
-    "thanks_for_answering": "非常感谢您完成问卷调查。"
-  },
-
   "not_found_page": {
   "not_found_page": {
     "page_not_exist": "该页面不存在"
     "page_not_exist": "该页面不存在"
   },
   },
-
   "g2g_data_transfer": {
   "g2g_data_transfer": {
     "tab": "数据迁移",
     "tab": "数据迁移",
     "data_transfer": "数据迁移",
     "data_transfer": "数据迁移",

+ 43 - 37
apps/app/public/static/locales/zh_CN/translation.json

@@ -123,8 +123,8 @@
   "V5 Page Migration": "转换为V5的兼容性",
   "V5 Page Migration": "转换为V5的兼容性",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <span class='growi-custom-icons'>external_link</span> ",
   "See_more_detail_on_new_schema": "更多详情请见<a href='https://docs.growi.org/en/admin-guide/upgrading/50x.html#about-the-new-v5-compatible-format' target='_blank'> {{title}}</a> <span class='growi-custom-icons'>external_link</span> ",
-	"Markdown Settings": "Markdown设置",
-	"external_account_management": "外部账户管理",
+  "Markdown Settings": "Markdown设置",
+  "external_account_management": "外部账户管理",
   "UserGroup": "用户组",
   "UserGroup": "用户组",
   "ChildUserGroup": "儿童用户组",
   "ChildUserGroup": "儿童用户组",
   "Basic Settings": "基础设置",
   "Basic Settings": "基础设置",
@@ -192,12 +192,12 @@
   "custom_navigation": {
   "custom_navigation": {
     "no_pages_under_this_page": "There are no pages under this page."
     "no_pages_under_this_page": "There are no pages under this page."
   },
   },
-"author_info": {
-  "created_at": "创建日期",
-  "created_by": "创建者:",
-  "last_revision_posted_at": "最后更新日期",
-  "updated_by": "更新者:"
-},
+  "author_info": {
+    "created_at": "创建日期",
+    "created_by": "创建者:",
+    "last_revision_posted_at": "最后更新日期",
+    "updated_by": "更新者:"
+  },
   "installer": {
   "installer": {
     "tab": "创建账户",
     "tab": "创建账户",
     "title": "安装",
     "title": "安装",
@@ -351,9 +351,9 @@
       "no_deadline": "此页面没有到期日期",
       "no_deadline": "此页面没有到期日期",
       "not_indexed1": "此页面可能不会被全文搜索引擎索引。",
       "not_indexed1": "此页面可能不会被全文搜索引擎索引。",
       "not_indexed2": "页面正文超过了{{threshold}}指定的阈值。"
       "not_indexed2": "页面正文超过了{{threshold}}指定的阈值。"
-		}
-	},
-	"page_edit": {
+    }
+  },
+  "page_edit": {
     "input_channels": "频道名",
     "input_channels": "频道名",
     "theme": "主题",
     "theme": "主题",
     "keymap": "键表",
     "keymap": "键表",
@@ -366,12 +366,12 @@
     },
     },
     "editor_config": "编辑器配置",
     "editor_config": "编辑器配置",
     "editor_assistant": "编辑助手",
     "editor_assistant": "编辑助手",
-		"Show active line": "显示活动行",
-		"auto_format_table": "自动格式化表格",
-		"overwrite_scopes": "{{operation}和覆盖所有子体的作用域",
-		"notice": {
-			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
-		},
+    "Show active line": "显示活动行",
+    "auto_format_table": "自动格式化表格",
+    "overwrite_scopes": "{{operation}和覆盖所有子体的作用域",
+    "notice": {
+      "conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
+    },
     "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
     "changes_not_saved": "您所做的更改可能不会保存。你真的想继续前进吗?"
   },
   },
   "page_comment": {
   "page_comment": {
@@ -499,9 +499,27 @@
       "Search in Editor": "编辑器内搜索",
       "Search in Editor": "编辑器内搜索",
       "Move Line": "移动行",
       "Move Line": "移动行",
       "Copy Line": "复制行",
       "Copy Line": "复制行",
-      "Toggle Line": "注释/取消注释行",
       "Insert Line": "插入行",
       "Insert Line": "插入行",
-      "Post Comment": "(发表评论)"
+      "Post Comment": "(发表评论)",
+      "Multiple Cursors": "多光标",
+      "Or Alt Click": "或 Alt + 点击"
+    },
+    "format": {
+      "title": "格式设置(编辑器)",
+      "Bold": "粗体",
+      "Italic": "斜体",
+      "Strikethrough": "删除线",
+      "Code Text": "代码文本",
+      "Hyperlink": "超链接"
+    },
+    "line_settings": {
+      "title": "行设置(编辑器)",
+      "Bullet List": "项目符号列表",
+      "Numbered List": "编号列表",
+      "Quote": "引用",
+      "Code Block": "代码块",
+      "Comment Out": "隐藏",
+      "Comment Out Desc": "(注释)"
     }
     }
   },
   },
   "modal_resolve_conflict": {
   "modal_resolve_conflict": {
@@ -517,6 +535,8 @@
   },
   },
   "sidebar_ai_assistant": {
   "sidebar_ai_assistant": {
     "reference_pages_label": "参考页面",
     "reference_pages_label": "参考页面",
+    "recent_chat": "最近聊天",
+    "no_recent_chat": "最近没有聊天",
     "knowledge_assistant_placeholder": "问我任何问题。",
     "knowledge_assistant_placeholder": "问我任何问题。",
     "editor_assistant_placeholder": "有什么需要帮忙的吗?",
     "editor_assistant_placeholder": "有什么需要帮忙的吗?",
     "summary_mode_label": "摘要模式",
     "summary_mode_label": "摘要模式",
@@ -530,6 +550,8 @@
     "budget_exceeded_for_growi_cloud": "您已达到 OpenAI API 使用上限。如需再次使用知识助手,请从GROWI.cloud管理页面为托管用户添加点数,或从OpenAI计费页面为自有用户添加点数。",
     "budget_exceeded_for_growi_cloud": "您已达到 OpenAI API 使用上限。如需再次使用知识助手,请从GROWI.cloud管理页面为托管用户添加点数,或从OpenAI计费页面为自有用户添加点数。",
     "error_message": "错误",
     "error_message": "错误",
     "show_error_detail": "显示详情",
     "show_error_detail": "显示详情",
+    "editor_assistant_long_context_warn_with_unit_line": "文本过长,编辑助理将参考大约第 {{startPosition}} 行到第 {{endPosition}} 行来响应",
+    "editor_assistant_long_context_warn_with_unit_char": "文本过长,编辑助理将参考第 {{startPosition}} 个字符到第 {{endPosition}} 个字符来响应",
     "discard": "丢弃",
     "discard": "丢弃",
     "accept": "接受",
     "accept": "接受",
     "use_assistant": "使用助手",
     "use_assistant": "使用助手",
@@ -620,11 +642,12 @@
   "default_ai_assistant": {
   "default_ai_assistant": {
     "not_set": "未设置默认助手"
     "not_set": "未设置默认助手"
   },
   },
-  "ai_assistant_tree": {
+  "ai_assistant_substance": {
     "add_assistant": "添加助手",
     "add_assistant": "添加助手",
     "my_assistants": "我的助手",
     "my_assistants": "我的助手",
     "team_assistants": "团队助手",
     "team_assistants": "团队助手",
     "thread_does_not_exist": "暂无会话",
     "thread_does_not_exist": "暂无会话",
+    "recent_threads": "最近的项目",
     "toaster": {
     "toaster": {
       "ai_assistant_deleted_success": "已删除助手",
       "ai_assistant_deleted_success": "已删除助手",
       "ai_assistant_deleted_failed": "删除助手失败",
       "ai_assistant_deleted_failed": "删除助手失败",
@@ -985,23 +1008,6 @@
     "move_to_root": "移动到根部",
     "move_to_root": "移动到根部",
     "root": "root (default)"
     "root": "root (default)"
   },
   },
-  "questionnaire": {
-    "give_us_feedback": "向我们提供反馈以进行改进",
-    "thank_you_for_answering": "谢谢你的回答",
-    "additional_feedback": "从用户图标下拉菜单向我们发送更多反馈。",
-    "dont_show_again": "不再显示",
-    "deny": "不要回答",
-    "agree": "同意",
-    "disagree": "不同意",
-    "answer": "答案是",
-    "no_answer": "没有答案",
-    "settings": "问卷设置",
-    "failed_to_send": "无法发送反馈",
-    "denied": "问卷不会再显示",
-    "personal_settings_explanation": "将展示改进 GROWI 的问卷。 如果您有其他反馈,可以从用户图标下拉菜单中发送。",
-    "enable_questionnaire": "启用问卷",
-    "disabled_by_admin": "问卷已被管理员禁用"
-  },
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "go_to_settings": "进入设置,启用该功能"
     "go_to_settings": "进入设置,启用该功能"

+ 14 - 0
apps/app/resource/locales/ko_KR/admin/userInvitation.ejs

@@ -0,0 +1,14 @@
+안녕하세요, <%- email %>님
+
+Wiki에 초대되셨습니다. 다음 계정으로 로그인할 수 있습니다:
+
+이메일: <%- email %>
+비밀번호: <%- password %>
+(이 비밀번호는 자동으로 생성되었습니다. 처음 로그인할 때 변경해야 합니다)
+
+기다리고 있겠습니다!
+<%- url %>
+
+--
+<%- appTitle %>
+<%- url %>

+ 11 - 0
apps/app/resource/locales/ko_KR/admin/userResetPassword.ejs

@@ -0,0 +1,11 @@
+안녕하세요, <%- email %>님
+
+관리자에 의해 비밀번호가 재설정되었습니다. 다음 계정으로 로그인할 수 있습니다:
+
+이메일: <%- email %>
+새 비밀번호: <%- password %>
+(이 비밀번호는 자동으로 생성되었습니다. 처음 로그인할 때 변경해야 합니다)
+
+--
+<%- appTitle %>
+<%- url %>

+ 20 - 0
apps/app/resource/locales/ko_KR/admin/userWaitingActivation.ejs

@@ -0,0 +1,20 @@
+안녕하세요, <%- adminUser.name %>님
+
+<%- appTitle %>에 사용자가 등록했습니다.
+
+
+====
+생성된 사용자:
+
+이름: <%- createdUser.name %>
+사용자명: <%- createdUser.username %>
+이메일: <%- createdUser.email %>
+====
+
+다음 URL에서 조치를 취해주세요:
+<%- url %>/admin/users
+
+
+--
+<%- appTitle %>
+<%- url %>

+ 9 - 0
apps/app/resource/locales/ko_KR/notifications/comment.ejs

@@ -0,0 +1,9 @@
+<%- username %>님이 <%- path %>에 댓글을 남겼습니다.
+
+----------------------
+
+<%- comment %>
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 5 - 0
apps/app/resource/locales/ko_KR/notifications/pageCreate.ejs

@@ -0,0 +1,5 @@
+<%- username %>님이 <%- path %> 아래에 새 페이지를 만들었습니다.
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 5 - 0
apps/app/resource/locales/ko_KR/notifications/pageDelete.ejs

@@ -0,0 +1,5 @@
+<%- username %>님이 <%- path %> 페이지를 삭제했습니다.
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 5 - 0
apps/app/resource/locales/ko_KR/notifications/pageEdit.ejs

@@ -0,0 +1,5 @@
+<%- username %>님이 <%- path %> 페이지를 수정했습니다.
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 5 - 0
apps/app/resource/locales/ko_KR/notifications/pageLike.ejs

@@ -0,0 +1,5 @@
+<%- username %>님이 <%- path %> 페이지를 좋아합니다.
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 5 - 0
apps/app/resource/locales/ko_KR/notifications/pageMove.ejs

@@ -0,0 +1,5 @@
+<%- username %>님이 <%- oldPath %> 페이지를 <%- newPath %>(으)로 이동했습니다.
+
+----------------------
+
+GROWI: <%- appTitle %>

+ 12 - 0
apps/app/resource/locales/ko_KR/notifications/passwordReset.ejs

@@ -0,0 +1,12 @@
+비밀번호 재설정
+
+안녕하세요, <%- email %>님
+
+GROWI (<%- appTitle %>) 계정의 비밀번호 변경 요청이 접수되었습니다.
+비밀번호를 재설정하려면 아래 링크를 클릭하세요.
+
+<%- url %>
+
+이 링크는 <%- expiredAt %>에 10분 후에 만료됩니다.
+
+비밀번호 재설정을 요청하지 않으셨다면 이 이메일을 무시하셔도 됩니다.

+ 8 - 0
apps/app/resource/locales/ko_KR/notifications/passwordResetSuccessful.ejs

@@ -0,0 +1,8 @@
+비밀번호 재설정 완료
+
+안녕하세요 <%- email %>님
+
+비밀번호가 성공적으로 재설정되었습니다.
+새 비밀번호로 로그인해주세요.
+
+감사합니다,

+ 12 - 0
apps/app/resource/locales/ko_KR/notifications/userActivation.ejs

@@ -0,0 +1,12 @@
+계정 확인
+
+안녕하세요, <%- email %>님
+
+GROWI (<%- appTitle %>)에 계정이 생성되었습니다.
+계정을 활성화하려면 아래 링크를 클릭하세요.
+
+<%- url %>
+
+이 링크는 <%- expiredAt %>에 1시간 후에 만료됩니다.
+
+계정을 만들지 않으셨다면 이 이메일을 무시하셔도 됩니다.

+ 169 - 0
apps/app/resource/locales/ko_KR/sandbox-bootstrap5.md

@@ -0,0 +1,169 @@
+# 1. 배지 (Badges)
+
+<span class="badge text-bg-primary">주요 (primary)</span>  
+
+<span class="badge text-bg-secondary">보조 (secondary)</span>  
+
+<span class="badge text-bg-success">성공 (success)</span>  
+
+<span class="badge text-bg-danger">위험 (danger)</span>  
+
+<span class="badge text-bg-warning">경고 (warning)</span>  
+
+<span class="badge text-bg-info">정보 (info)</span>  
+
+<span class="badge text-bg-light">밝게 (light)</span>  
+
+<span class="badge text-bg-dark">어둡게 (dark)</span>  
+
+
+# 2. 알림 (Alerts)
+
+<div class="alert alert-primary" role="alert">
+  이것은 주요 알림입니다.
+</div>
+
+<div class="alert alert-secondary" role="alert">
+  이것은 보조 알림입니다.
+</div>
+
+<div class="alert alert-success" role="alert">
+  이것은 성공 알림입니다.
+</div>
+
+<div class="alert alert-danger" role="alert">
+  이것은 위험 알림입니다.
+</div>
+
+<div class="alert alert-warning" role="alert">
+  이것은 경고 알림입니다.
+</div>
+
+<div class="alert alert-info" role="alert">
+  이것은 정보 알림입니다.
+</div>
+
+<div class="alert alert-light" role="alert">
+  이것은 밝은 알림입니다.
+</div>
+
+<div class="alert alert-dark" role="alert">
+  이것은 어두운 알림입니다.
+</div>
+
+
+# 3. 카드 (Cards)
+
+<div class="card text-bg-primary mb-3" style="max-width: 50rem;">
+  <div class="card-header">헤더</div>
+  <div class="card-body">
+    <h5 class="card-title">주요 카드 제목</h5>
+    <p class="card-text">카드 제목을 기반으로 하고 카드의 콘텐츠 대부분을 구성하는 일부 빠른 예제 텍스트입니다.</p>
+  </div>
+</div>
+
+<div class="card text-bg-secondary mb-3" style="max-width: 45rem;">
+  <div class="card-header">헤더</div>
+  <div class="card-body">
+    <h5 class="card-title">보조 카드 제목</h5>
+    <p class="card-text">카드 제목을 기반으로 하고 카드의 콘텐츠 대부분을 구성하는 일부 빠른 예제 텍스트입니다.</p>
+  </div>
+</div>
+
+<div class="card text-bg-success mb-3" style="max-width: 40rem;">
+  <div class="card-header">헤더</div>
+  <div class="card-body">
+    <h5 class="card-title">성공 카드 제목</h5>
+    <p class="card-text">카드 제목을 기반으로 하고 카드의 콘텐츠 대부분을 구성하는 일부 빠른 예제 텍스트입니다.</p>
+  </div>
+</div>
+
+<div class="card text-bg-danger mb-3" style="max-width: 35rem;">
+  <div class="card-header">헤더</div>
+  <div class="card-body">
+    <h5 class="card-title">위험 카드 제목</h5>
+    <p class="card-text">카드 제목을 기반으로 하고 카드의 콘텐츠 대부분을 구성하는 일부 빠른 예제 텍스트입니다.</p>
+  </div>
+</div>
+
+<div class="card text-bg-warning mb-3" style="max-width: 30rem;">
+  <div class="card-header">헤더</div>
+  <div class="card-body">
+    <h5 class="card-title">경고 카드 제목</h5>
+    <p class="card-text">카드 제목을 기반으로 하고 카드의 콘텐츠 대부분을 구성하는 일부 빠른 예제 텍스트입니다.</p>
+  </div>
+</div>
+
+<div class="card text-bg-info mb-3" style="max-width: 25rem;">
+  <div class="card-header">헤더</div>
+  <div class="card-body">
+    <h5 class="card-title">정보 카드 제목</h5>
+    <p class="card-text">카드 제목을 기반으로 하고 카드의 콘텐츠 대부분을 구성하는 일부 빠른 예제 텍스트입니다.</p>
+  </div>
+</div>
+
+<div class="card text-bg-light mb-3" style="max-width: 20rem;">
+  <div class="card-header">헤더</div>
+  <div class="card-body">
+    <h5 class="card-title">밝은 카드 제목</h5>
+    <p class="card-text">카드 제목을 기반으로 하고 카드의 콘텐츠 대부분을 구성하는 일부 빠른 예제 텍스트입니다.</p>
+  </div>
+</div>
+
+<div class="card text-bg-dark mb-3" style="max-width: 15rem;">
+  <div class="card-header">헤더</div>
+  <div class="card-body">
+    <h5 class="card-title">어두운 카드 제목</h5>
+    <p class="card-text">카드 제목을 기반으로 하고 카드의 콘텐츠 대부분을 구성하는 일부 빠른 예제 텍스트입니다.</p>
+  </div>
+</div>
+
+
+# 4. 색상 (Colors)
+## 문맥적 색상
+<p class="text-primary">보세요, 저는 우물 안에 있어요!</p>
+<p class="text-warning">보세요, 저는 우물 안에 있어요!</p>
+<p class="text-danger">보세요, 저는 우물 안에 있어요!</p>
+
+## 문맥적 배경
+<p class="text-danger bg-primary">보세요, 저는 우물 안에 있어요!</p>
+<p class="text-primary bg-warning">보세요, 저는 우물 안에 있어요!</p>
+<p class="text-warning bg-danger">보세요, 저는 우물 안에 있어요!</p>
+
+
+# 5. 접기 (Collapse)
+## 콘텐츠 표시
+<a class="btn btn-primary text-white" data-bs-toggle="collapse" href="#collapse-1">
+  콘텐츠 표시
+</a>
+
+<div class="collapse" id="collapse-1">
+  <div class="card card-body">
+
+- 표시하려는 콘텐츠
+  - 표시하려는 콘텐츠
+      
+  </div>
+</div>
+
+## 콘텐츠 숨기기
+<a class="btn btn-secondary text-white" data-bs-toggle="collapse" href="#collapse-2">
+  콘텐츠 숨기기
+</a>
+
+<div class="collapse show" id="collapse-2">
+  <div class="card card-body">
+
+- 숨기려는 콘텐츠
+  - 숨기려는 콘텐츠
+
+  </div>
+</div>
+
+
+# 공식 문서
+- [배지에 대한 자세한 내용은 여기를 클릭하세요](https://getbootstrap.jp/docs/5.3/components/badge/)
+- [알림에 대한 자세한 내용은 여기를 클릭하세요](https://getbootstrap.jp/docs/5.3/components/alerts/)
+- [카드에 대한 자세한 내용은 여기를 클릭하세요](https://getbootstrap.jp/docs/5.3/components/card/)
+- [색상에 대한 자세한 내용은 여기를 클릭하세요](https://getbootstrap.jp/docs/5.3/utilities/colors/)
+- [접기에 대한 자세한 내용은 여기를 클릭하세요](https://getbootstrap.jp/docs/5.3/components/collapse/)

Fișier diff suprimat deoarece este prea mare
+ 7 - 0
apps/app/resource/locales/ko_KR/sandbox-diagrams.md


+ 241 - 0
apps/app/resource/locales/ko_KR/sandbox-markdown.md

@@ -0,0 +1,241 @@
+# 알림 (Alerts)
+
+> [!NOTE]
+> 사용자가 콘텐츠를 훑어볼 때도 알아야 할 유용한 정보입니다.
+
+> [!TIP]
+> 일을 더 잘하거나 쉽게 할 수 있는 유용한 조언입니다.
+
+> [!IMPORTANT]
+> 목표를 달성하기 위해 사용자가 알아야 할 핵심 정보입니다.
+
+> [!WARNING]
+> 문제를 피하기 위해 즉각적인 사용자 주의가 필요한 긴급 정보입니다.
+
+> [!CAUTION]
+> 특정 행동의 위험이나 부정적인 결과에 대해 조언합니다.
+
+
+```markdown
+> [!NOTE]
+> 사용자가 콘텐츠를 훑어볼 때도 알아야 할 유용한 정보입니다.
+
+> [!TIP]
+> 일을 더 잘하거나 쉽게 할 수 있는 유용한 조언입니다.
+
+> [!IMPORTANT]
+> 목표를 달성하기 위해 사용자가 알아야 할 핵심 정보입니다.
+
+> [!WARNING]
+> 문제를 피하기 위해 즉각적인 사용자 주의가 필요한 긴급 정보입니다.
+
+> [!CAUTION]
+> 특정 행동의 위험이나 부정적인 결과에 대해 조언합니다.
+```
+
+[지시어 구문](https://talk.commonmark.org/t/generic-directives-plugins-syntax/444)을 사용할 수도 있습니다.
+
+:::note
+사용자가 콘텐츠를 훑어볼 때도 알아야 할 유용한 정보입니다.
+:::
+
+:::tip[사용자 정의 레이블]
+사용자가 콘텐츠를 훑어볼 때도 알아야 할 유용한 정보입니다.
+:::
+
+```markdown
+:::note
+사용자가 콘텐츠를 훑어볼 때도 알아야 할 유용한 정보입니다.
+:::
+
+:::tip[사용자 정의 레이블]
+사용자가 콘텐츠를 훑어볼 때도 알아야 할 유용한 정보입니다.
+:::
+```
+
+
+# 인용문 (Quote text)
+- 단락 시작 부분에 `>`를 넣어 인용 표현을 사용합니다.
+    - `>` 문자를 연속으로 사용하여 다중 인용을 표현할 수 있습니다.
+- 목록 및 기타 요소를 블록 인용문 내에서 함께 사용할 수 있습니다.
+
+#### 예시
+> - 인용
+> - 인용
+>> 다중 인용은 `>`를 더 많이 삽입해야 합니다.
+
+```markdown
+> - 인용
+> - 인용
+>> 다중 인용은 `>`를 더 많이 삽입해야 합니다.
+```
+
+
+# 코드 (Code)
+- 세 개의 `` ` `` 안에 코드를 추가하여 표현할 수 있습니다.
+
+#### 예시
+
+```markdown
+여기에 코드를 추가하세요
+
+줄 바꿈과 단락은 코드에 그대로 반영될 수 있습니다.
+```
+
+#### 예시 (소스 코드)
+
+```javascript:mersenne-twister.js
+function MersenneTwister(seed) {
+  if (arguments.length == 0) {
+    seed = new Date().getTime();
+  }
+
+  this._mt = new Array(624);
+  this.setSeed(seed);
+}
+```
+
+## 인라인 코드 (Inline Code)
+- 단어를 `` ` ``로 묶어 인라인 코드를 만듭니다.
+
+#### 예시
+여기에 `인라인 코드`가 있습니다.
+
+
+
+# 작업 목록 (Task List)
+- `[] `을 작성하여 선택되지 않은 체크박스 목록을 삽입합니다.
+    - `[x]`를 작성하여 체크박스를 선택합니다.
+
+#### 예시
+- [ ] 작업 1
+    - [x] 작업 1-1
+    - [ ] 작업 1-2
+- [x] 작업 2
+
+
+# 수평선 (Horizontal lines)
+- 세 개 이상의 연속된 별표 `*` 또는 밑줄 `_`로 수평선을 삽입합니다.
+
+#### 예시
+아래는 수평선입니다.
+***
+
+아래는 수평선입니다.
+___
+
+```markdown
+아래는 수평선입니다.
+***
+
+아래는 수평선입니다.
+___
+```
+
+
+# 각주 (Footnotes)
+
+이 대괄호 구문을 사용하여 콘텐츠에 각주를 추가할 수 있습니다.
+
+여기에 간단한 각주가 있습니다[^1].
+
+각주에는 여러 줄이 있을 수도 있습니다[^2].
+
+[^1]: 내 참조.
+[^2]: 각주 내에서 줄을 바꾸려면 새 줄 앞에 공백 2개를 붙입니다.
+  이것은 두 번째 줄입니다.
+
+```markdown
+여기에 간단한 각주가 있습니다[^1].
+
+각주에는 여러 줄이 있을 수도 있습니다[^2].
+
+[^1]: 내 참조.
+[^2]: 각주 내에서 줄을 바꾸려면 새 줄 앞에 공백 2개를 붙입니다.
+  이것은 두 번째 줄입니다.
+```
+
+
+# 이모지 (emoji)
+
+콜론 `:` 뒤에 이모지 이름을 입력하여 텍스트에 이모지를 추가할 수 있습니다.
+
+- :+1: 좋아요!
+- :white_check_mark: 체크
+- :lock: 잠금
+
+콜론 뒤에 두 자 이상을 입력하면 이모지 제안 목록이 나타납니다. 이 목록은 계속 입력하면 좁혀집니다. 원하는 이모지를 찾으면 탭이나 엔터를 눌러 강조 표시된 이모지를 삽입합니다.
+
+사용 가능한 이모지 목록은 "[이모지 치트 시트](https://github.com/ikatyang/emoji-cheat-sheet/blob/master/README.md)"를 참조하세요.
+
+
+# 표 (Table)
+### 일반 구문
+#### 예시
+
+| 왼쪽 정렬 | 오른쪽 정렬 | 가운데 정렬 |
+|:-----------|------------:|:------------:|
+| 이         | 이          | 이           |
+| 열은       | 열은        | 열은         |
+| 왼쪽으로   | 오른쪽으로  | 가운데로     |
+| 정렬됩니다 | 정렬됩니다  | 정렬됩니다   |
+
+```markdown
+| 왼쪽 정렬 | 오른쪽 정렬 | 가운데 정렬 |
+|:-----------|------------:|:------------:|
+| 이         | 이          | 이           |
+| 열은       | 열은        | 열은         |
+| 왼쪽으로   | 오른쪽으로  | 가운데로     |
+| 정렬됩니다 | 정렬됩니다  | 정렬됩니다   |
+```
+
+### CSV / TSV
+
+#### 예시
+
+``` tsv
+콘텐츠 셀	콘텐츠 셀
+콘텐츠 셀	콘텐츠 셀
+```
+
+~~~
+``` csv
+콘텐츠 셀,콘텐츠 셀
+콘텐츠 셀,콘텐츠 셀
+```
+~~~
+
+~~~
+``` tsv
+콘텐츠 셀	콘텐츠 셀
+콘텐츠 셀	콘텐츠 셀
+```
+~~~
+
+
+### CSV / TSV (헤더 포함)
+
+
+#### 예시
+
+``` tsv-h
+첫 번째 헤더	두 번째 헤더
+콘텐츠 셀	콘텐츠 셀
+콘텐츠 셀	콘텐츠 셀
+```
+
+~~~
+``` csv-h
+첫 번째 헤더,두 번째 헤더
+콘텐츠 셀,콘텐츠 셀
+콘텐츠 셀,콘텐츠 셀
+```
+~~~
+
+~~~
+``` tsv-h
+첫 번째 헤더	두 번째 헤더
+콘텐츠 셀	콘텐츠 셀
+콘텐츠 셀	콘텐츠 셀
+```
+~~~

+ 72 - 0
apps/app/resource/locales/ko_KR/sandbox-math.md

@@ -0,0 +1,72 @@
+# :pencil2: 수학 (Math)
+
+[KaTeX](https://katex.org/)를 참조하세요.
+
+## 인라인 수식 (Inline Formula)
+
+$a 
+e 0$일 때, $ax^2 + bx + c = 0$에 대한 두 가지 해는 다음과 같습니다.
+  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
+
+## 로렌츠 방정식 (The Lorenz Equations)
+
+$$
+\begin{align}
+\dot{x} & = \sigma(y-x) \\
+\dot{y} & = \rho x - y - xz \\
+\dot{z} & = -\beta z + xy
+\end{align}
+$$
+
+
+## 코시-슈바르츠 부등식 (The Cauchy-Schwarz Inequality)
+
+$$
+\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
+ \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+$$
+
+## 벡터 곱 공식 (A Cross Product Formula)
+
+$$
+\mathbf{V}_1 \times \mathbf{V}_2 =
+ \begin{vmatrix}
+  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
+ \end{vmatrix}
+$$
+
+
+## 동전 $\left(n\right)$개를 던져 앞면이 $\left(k\right)$번 나올 확률은 다음과 같습니다:
+
+$$
+P(E) = {n \choose k} p^k (1-p)^{ n-k}
+$$
+
+## 라마누잔의 항등식 (An Identity of Ramanujan)
+
+$$
+\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
+     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+$$
+
+## 로저스-라마누잔 항등식 (A Rogers-Ramanujan Identity)
+
+$$
+1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+     \quad\quad \text{for $|q|<1$}.
+$$
+
+## 맥스웰 방정식 (Maxwell's Equations)
+
+$$
+\begin{align}
+  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
+  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+  \nabla \cdot \vec{\mathbf{B}} & = 0
+\end{align}
+$$

+ 174 - 0
apps/app/resource/locales/ko_KR/sandbox.md

@@ -0,0 +1,174 @@
+# GROWI 샌드박스에 오신 것을 환영합니다!
+
+> [!NOTE]
+> **샌드박스란 무엇인가요?**
+> 
+> 자유롭게 편집할 수 있는 연습용 페이지입니다. 새로운 것을 시도하기에 완벽한 장소입니다!
+
+
+## :beginner: 초보자를 위해
+
+GROWI를 사용하면 "마크다운"이라는 표기법을 사용하여 시각적으로 매력적인 페이지를 쉽게 만들 수 있습니다.
+마크다운을 사용하면 다음과 같은 작업을 할 수 있습니다!
+
+- **굵게** 또는 *기울임꼴*로 텍스트 강조
+- 글머리 기호 또는 번호 매기기 목록 만들기
+- [링크 삽입](#-link)
+- 표 만들기
+- 코드 블록 추가
+
+다양한 다른 꾸미기도 가능합니다.
+
+## 시도해 보세요!
+
+1. 이 페이지를 자유롭게 편집하세요
+1. 실수하는 것을 두려워할 필요가 없습니다
+1. 언제든지 변경 사항을 되돌릴 수 있습니다
+1. 다른 사람의 편집 내용에서 배울 수도 있습니다
+
+> [!IMPORTANT]
+> **관리자를 위해**
+> 
+> 샌드박스는 학습을 위한 중요한 장소입니다:
+> - 새로운 구성원이 GROWI에 익숙해지기 위한 첫 단계
+> - 마크다운 연습장
+> - 팀 내 커뮤니케이션 도구
+>     - 이 페이지가 어수선해지더라도 활발한 학습의 신호입니다. 정기적인 정리는 좋지만, 자유로운 실험 공간으로서의 성격을 유지하는 것이 좋습니다.
+
+
+# :closed_book: 제목 및 단락
+- 제목과 단락을 삽입하여 페이지의 텍스트를 읽기 쉽게 만들 수 있습니다.
+
+## 제목
+- 제목 텍스트 앞에 `#`를 추가하여 제목을 만듭니다.
+    - `#`의 수에 따라 보기 화면에 표시되는 제목의 글꼴 크기가 달라집니다.
+- `#`의 수는 계층 수준을 결정하고 콘텐츠를 구성하는 데 도움이 됩니다.
+
+```markdown
+# 첫 번째 수준 제목
+## 두 번째 수준 제목
+### 세 번째 수준 제목
+#### 네 번째 수준 제목
+##### 다섯 번째 수준 제목
+###### 여섯 번째 수준 제목
+```
+
+## 줄 바꿈
+- 줄을 바꾸고 싶은 문장 끝에 반각 공백 두 개를 삽입합니다.
+    - 설정에서 반각 공백 없이 줄을 바꾸도록 변경할 수도 있습니다.
+        - 관리자 페이지의 `마크다운 설정` 섹션에서 줄 바꿈 설정을 변경하세요.
+
+#### 예시: 줄 바꿈 없음
+단락 1
+단락 2
+
+#### 예시: 줄 바꿈 있음
+단락 1  
+단락 2
+
+## 블록
+- 텍스트에 빈 줄을 삽입하여 단락을 만들 수 있습니다.
+- 구절을 문장으로 나누어 읽기 쉽게 만들 수 있습니다.
+
+#### 예시: 단락 없음
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+#### 예시: 단락 있음
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+
+Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+
+
+# :blue_book: 텍스트 스타일링
+
+- 문장의 텍스트 표현을 풍부하게 하기 위해 다양한 스타일을 적용할 수 있습니다.
+    - 이러한 스타일은 편집 화면 하단의 도구 모음 아이콘을 선택하여 쉽게 적용할 수도 있습니다.
+
+| 스타일                     | 구문                   | 바로 가기 키 | 예시                                      | 출력                                   |
+| -------------------------- | ---------------------- | ------------ | ----------------------------------------- | -------------------------------------- |
+| 굵게                       | `** **` 또는 `__ __`   | (미정)       | `**이것은 굵은 텍스트입니다**`            | **이것은 굵은 텍스트입니다**           |
+| 기울임꼴                   | `* *` 또는 `_ _`       | (미정)       | `_이 텍스트는 기울임꼴입니다_`            | *이 텍스트는 기울임꼴입니다*           |
+| 취소선                     | `~~ ~~`                | (미정)       | `~~이것은 잘못된 텍스트였습니다~~`        | ~~이것은 잘못된 텍스트였습니다~~       |
+| 굵게 및 중첩 기울임꼴      | `** **` 및 `_ _`       | 없음         | `**이 텍스트는 _매우_ 중요합니다**`       | **이 텍스트는 _매우_ 중요합니다**      |
+| 모두 굵게 및 기울임꼴      | `*** ***`              | 없음         | `***이 모든 텍스트는 중요합니다***`       | ***이 모든 텍스트는 중요합니다***      |
+| 아래 첨자                  | `<sub> </sub>`         | 없음         | `이것은 <sub>아래 첨자</sub> 텍스트입니다`  | 이것은 <sub>아래 첨자</sub> 텍스트입니다 |
+| 위 첨자                    | `<sup> </sup>`         | 없음         | `이것은 <sup>위 첨자</sup> 텍스트입니다`    | 이것은 <sup>위 첨자</sup> 텍스트입니다   |
+
+
+# :green_book: 목록 삽입
+## 글머리 기호 목록
+- 하이픈 `-`, 더하기 `+` 또는 별표 `*`로 줄을 시작하여 글머리 기호 목록을 삽입합니다.
+
+#### 예시
+- 이 문장은 글머리 기호 목록에 있습니다.
+    - 이 문장은 글머리 기호 목록에 있습니다.
+        - 이 문장은 글머리 기호 목록에 있습니다.
+        - 이 문장은 글머리 기호 목록에 있습니다.
+- 이 문장은 글머리 기호 목록에 있습니다.
+    - 이 문장은 글머리 기호 목록에 있습니다.
+
+## 번호 매기기 목록
+- `숫자.`를 줄 시작 부분에 사용하여 번호 매기기 목록을 삽입합니다.
+    - 번호는 자동으로 할당됩니다.
+
+- 번호 매기기 목록과 글머리 기호 목록을 조합하여 사용할 수도 있습니다.
+
+#### 예시
+1. 이 문장은 번호 매기기 목록에 있습니다.
+    1. 이 문장은 번호 매기기 목록에 있습니다.
+    1. 이 문장은 번호 매기기 목록에 있습니다.
+    1. 이 문장은 번호 매기기 목록에 있습니다.
+        - 이 문장은 글머리 기호 목록에 있습니다.
+1. 이 문장은 글머리 기호 목록에 있습니다.
+    - 이 문장은 글머리 기호 목록에 있습니다.
+
+
+# :ledger: 링크
+
+## 자동 링크
+URL을 그냥 쓰면 링크가 자동으로 생성됩니다.
+
+### 예시
+
+https://www.google.co.jp
+
+```markdown
+https://www.google.co.jp
+```
+
+## 레이블 및 링크
+`[레이블](URL)`을 작성하여 링크를 삽입합니다.
+
+### 예시
+- [Google](https://www.google.co.jp/)
+- [샌드박스는 여기에 있습니다](/Sandbox)
+
+```markdown
+- [Google](https://www.google.co.jp/)
+- [샌드박스는 여기에 있습니다](/Sandbox)
+```
+
+## 유연한 링크 구문
+
+유연한 링크 구문을 사용하면 페이지 경로, 상대 페이지 링크 및 링크 레이블과 URL로 링크를 쉽게 작성할 수 있습니다.
+
+- [[/Sandbox]]
+- [[./Math]]
+- [[수식 작성 방법?>./Math]]
+
+```markdown
+- [[/Sandbox]]
+- [[./Math]]
+- [[수식 작성 방법?>./Math]]
+```
+
+
+# :notebook: 더 많은 응용 프로그램
+
+- [마크다운에 대해 더 알아보기](/Sandbox/Markdown)
+
+- [페이지를 더 꾸미기 (Bootstrap5)](/Sandbox/Bootstrap5)
+
+- [다이어그램 표현 방법 (Diagrams)](/Sandbox/Diagrams)
+
+- [수학 공식 표현 방법 (Math)](/Sandbox/Math)

+ 51 - 0
apps/app/resource/locales/ko_KR/welcome.md

@@ -0,0 +1,51 @@
+# :tada: GROWI에 오신 것을 환영합니다
+
+GROWI는 기업 및 개인을 위한 내부 위키 및 지식 베이스 도구입니다.
+GROWI를 사용하면 회사, 대학 세미나 또는 동아리에서 구성원들이 쉽게 정보를 공유하고 편집할 수 있습니다.
+
+알고 있는 정보를 부담 없이 적고 함께 편집하면 **팀 내의 암묵적인 지식을 줄일 수 있습니다**.
+매일 공유되는 정보의 양을 늘려봅시다!
+
+<div class="alert alert-primary" role="alert">
+※이 페이지를 위키의 최상위 페이지로 자유롭게 편집하고 사용하세요.
+</div>
+
+# :beginner: GROWI로 무엇을 할 수 있나요?
+## 1. 지식 관리: 정보와 지식을 저장할 페이지 만들기
+- 페이지를 만들고 편집하는 방법은?
+    - 화면 왼쪽 상단의 "연필 아이콘"에서 새 페이지를 만들 수 있습니다.
+    - 이미 만든 페이지는 화면 오른쪽 상단의 "편집"을 클릭하여 편집할 수 있습니다.
+- 페이지를 관리하는 방법은?
+    - GROWI는 **계층적** 구조로 페이지를 관리합니다.
+        - 예: ` /페이지 A/페이지 B/페이지 C `
+    - 계층 구조 외에도 태그로 페이지를 관리할 수 있습니다.
+
+## 2. 정보 검색: 다양한 방법으로 정보 검색
+- 키워드 검색
+- 다양한 사이드바를 사용한 검색
+    - 페이지 트리로 검색
+    - 최신 변경 사항으로 검색
+    - 태그로 검색 등...
+
+## 3. 정보 공유: 내부 및 외부 공유 용이
+- 회사 구성원에게 페이지의 URL 및 고유 링크를 보낼 수 있습니다.
+    - 사용자 그룹을 사용하여 회사 구성원 간의 보기 권한을 관리할 수 있습니다.
+- GROWI는 계정이 없는 회사 외부 사용자도 페이지를 볼 수 있도록 허용합니다.
+    - 공유 링크를 사용하여 회사 외부 사용자와 정보를 공유해 보세요!
+
+#### :bulb: 페이지 편집 방법에 대해 더 알아보려면 [샌드박스](/Sandbox)를 확인하세요!
+
+
+# :wrench: 관리자를 위해 - GROWI가 생성되면
+
+### :arrow_right: 여러 사람과 GROWI를 사용하고 싶으신가요?
+- :heavy_check_mark: 구성원을 초대하세요!
+    - [GROWI에 새 구성원 추가 또는 초대](https://docs.growi.org/en/admin-guide/management-cookbook/user-management.html#temporary-issuance-of-a-new-user)
+
+### :arrow_right: 현재 GROWI의 모습에 만족하지 않으신가요?
+- :heavy_check_mark: 걱정 마세요! GROWI의 테마를 사용자 정의해 보세요!
+    - [GROWI 테마 사용자 정의](/admin/customize)
+
+### :arrow_right: GROWI 보안 설정이 완료되지 않았나요?
+- :heavy_check_mark: GROWI 보안 설정을 업데이트하러 오세요!
+    - [GROWI 보안 설정 업데이트](/admin/security)

+ 0 - 8
apps/app/src/client/components/Admin/App/AppSettingsPageContents.tsx

@@ -15,7 +15,6 @@ import FileUploadSetting from './FileUploadSetting';
 import MailSetting from './MailSetting';
 import MailSetting from './MailSetting';
 import { MaintenanceMode } from './MaintenanceMode';
 import { MaintenanceMode } from './MaintenanceMode';
 import PageBulkExportSettings from './PageBulkExportSettings';
 import PageBulkExportSettings from './PageBulkExportSettings';
-import QuestionnaireSettings from './QuestionnaireSettings';
 import SiteUrlSetting from './SiteUrlSetting';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 import V5PageMigration from './V5PageMigration';
 
 
@@ -119,13 +118,6 @@ const AppSettingsPageContents = (props: Props) => {
         </div>
         </div>
       )}
       )}
 
 
-      <div className="row mt-5">
-        <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('admin:app_setting.questionnaire_settings')}</h2>
-          <QuestionnaireSettings />
-        </div>
-      </div>
-
       <div className="row">
       <div className="row">
         <div className="col-lg-12">
         <div className="col-lg-12">
           <h2 className="admin-setting-header" id="maintenance-mode">{t('admin:maintenance_mode.maintenance_mode')}</h2>
           <h2 className="admin-setting-header" id="maintenance-mode">{t('admin:maintenance_mode.maintenance_mode')}</h2>

+ 1 - 1
apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx

@@ -30,7 +30,7 @@ const PageBulkExportSettings = (): JSX.Element => {
         isBulkExportPagesEnabled,
         isBulkExportPagesEnabled,
         bulkExportDownloadExpirationSeconds,
         bulkExportDownloadExpirationSeconds,
       });
       });
-      toastSuccess(t('commons:toaster.update_successed', { target: t('app_setting.questionnaire_settings') }));
+      toastSuccess(t('commons:toaster.update_successed', { target: t('app_setting.page_bulk_export_settings') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 0 - 130
apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx

@@ -1,130 +0,0 @@
-import {
-  useState, useCallback, useEffect, type JSX,
-} from 'react';
-
-import { LoadingSpinner } from '@growi/ui/dist/components';
-import { useTranslation } from 'next-i18next';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useSWRxAppSettings } from '~/stores/admin/app-settings';
-
-import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
-
-const QuestionnaireSettings = (): JSX.Element => {
-  const { t } = useTranslation(['admin', 'commons']);
-
-  const { data, error, mutate } = useSWRxAppSettings();
-
-  const [isQuestionnaireEnabled, setIsQuestionnaireEnabled] = useState(data?.isQuestionnaireEnabled);
-  const onChangeIsQuestionnaireEnabledHandler = useCallback(() => {
-    setIsQuestionnaireEnabled(prev => !prev);
-  }, []);
-
-  const [isAppSiteUrlHashed, setIsAppSiteUrlHashed] = useState(data?.isAppSiteUrlHashed);
-  const onChangeisAppSiteUrlHashedHandler = useCallback(() => {
-    setIsAppSiteUrlHashed(prev => !prev);
-  }, []);
-
-  const onSubmitHandler = useCallback(async() => {
-    try {
-      await apiv3Put('/app-settings/questionnaire-settings', {
-        isQuestionnaireEnabled,
-        isAppSiteUrlHashed,
-      });
-      toastSuccess(t('commons:toaster.update_successed', { target: t('app_setting.questionnaire_settings') }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-    mutate();
-  }, [isAppSiteUrlHashed, isQuestionnaireEnabled, mutate, t]);
-
-  // Sync SWR value and state
-  useEffect(() => {
-    setIsQuestionnaireEnabled(data?.isQuestionnaireEnabled);
-    setIsAppSiteUrlHashed(data?.isAppSiteUrlHashed);
-  }, [data, data?.isAppSiteUrlHashed, data?.isQuestionnaireEnabled]);
-
-  const isLoading = data === undefined && error === undefined;
-
-  return (
-    <div id="questionnaire-settings" className="mb-5">
-      <p className="card custom-card bg-info-subtle">
-        <div className="mb-3">{t('app_setting.questionnaire_settings_explanation')}</div>
-        <span>
-          <div className="mb-2">
-            <span className="text-info me-2"><span className="material-symbols-outlined">info</span>{t('app_setting.about_data_sent')}</span>
-            <a href={t('app_setting.docs_link')} rel="noreferrer" target="_blank" className="d-inline">
-              {t('app_setting.learn_more')} <span className="material-symbols-outlined">share</span>
-            </a>
-          </div>
-          {t('app_setting.other_info_will_be_sent')}<br />
-          {t('app_setting.we_will_use_the_data_to_improve_growi')}
-        </span>
-      </p>
-
-      {isLoading && (
-        <div className="text-muted text-center mb-5">
-          <LoadingSpinner className="me-1 fs-3" />
-        </div>
-      )}
-
-      {!isLoading && (
-        <>
-          <div className="my-4 row">
-            <label
-              className="text-start text-md-end col-md-3 col-form-label"
-            >
-            </label>
-
-            <div className="col-md-6">
-              <div className="form-check form-switch form-check-info">
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id="isQuestionnaireEnabled"
-                  checked={isQuestionnaireEnabled}
-                  onChange={onChangeIsQuestionnaireEnabledHandler}
-                />
-                <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
-                  {t('app_setting.enable_questionnaire')}
-                </label>
-              </div>
-            </div>
-          </div>
-
-          <div className="my-4 row">
-            <label
-              className="text-start text-md-end col-md-3 col-form-label"
-            >
-            </label>
-
-            <div className="col-md-6">
-              <div className="form-check form-check-info">
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id="isAppSiteUrlHashed"
-                  checked={isAppSiteUrlHashed}
-                  onChange={onChangeisAppSiteUrlHashedHandler}
-                  disabled={!isQuestionnaireEnabled}
-                />
-                <label className="form-label form-check-label" htmlFor="isAppSiteUrlHashed">
-                  {t('app_setting.anonymize_app_site_url')}
-                </label>
-                <p className="form-text text-muted small">
-                  {t('app_setting.url_anonymization_explanation')}
-                </p>
-              </div>
-            </div>
-          </div>
-
-          <AdminUpdateButtonRow onClick={onSubmitHandler} />
-        </>
-      )}
-    </div>
-  );
-};
-
-export default QuestionnaireSettings;

+ 37 - 23
apps/app/src/client/components/Admin/ExportArchiveDataPage.tsx

@@ -50,29 +50,40 @@ const ExportArchiveDataPage = (): JSX.Element => {
   }, []);
   }, []);
 
 
   const setupWebsocketEventHandler = useCallback(() => {
   const setupWebsocketEventHandler = useCallback(() => {
-    if (socket != null) {
-      // websocket event
-      socket.on('admin:onProgressForExport', ({ currentCount, totalCount, progressList }) => {
-        setExporting(true);
-        setProgressList(progressList);
-      });
-
-      // websocket event
-      socket.on('admin:onStartZippingForExport', () => {
-        setZipping(true);
-      });
-
-      // websocket event
-      socket.on('admin:onTerminateForExport', ({ addedZipFileStat }) => {
-
-        setExporting(false);
-        setZipping(false);
-        setExported(true);
-        setZipFileStats(prev => prev.concat([addedZipFileStat]));
-
-        toastSuccess(`New Archive Data '${addedZipFileStat.fileName}' is added`);
-      });
+    if (socket == null) {
+      return () => {};
     }
     }
+
+    const onProgress = ({ currentCount, totalCount, progressList }) => {
+      setExporting(true);
+      setProgressList(progressList);
+    };
+
+    const onStartZipping = () => {
+      setZipping(true);
+    };
+
+    const onTerminateForExport = ({ addedZipFileStat }) => {
+      setExporting(false);
+      setZipping(false);
+      setExported(true);
+      setZipFileStats(prev => prev.concat([addedZipFileStat]));
+
+      toastSuccess(`New Archive Data '${addedZipFileStat.fileName}' is added`);
+    };
+
+    // Add listeners
+    socket.on('admin:onProgressForExport', onProgress);
+    socket.on('admin:onStartZippingForExport', onStartZipping);
+    socket.on('admin:onTerminateForExport', onTerminateForExport);
+
+    // Cleanup listeners
+    return () => {
+      socket.off('admin:onProgressForExport', onProgress);
+      socket.off('admin:onStartZippingForExport', onStartZipping);
+      socket.off('admin:onTerminateForExport', onTerminateForExport);
+    };
+
   }, [socket]);
   }, [socket]);
 
 
   const onZipFileStatRemove = useCallback(async(fileName) => {
   const onZipFileStatRemove = useCallback(async(fileName) => {
@@ -130,8 +141,11 @@ const ExportArchiveDataPage = (): JSX.Element => {
 
 
   useEffect(() => {
   useEffect(() => {
     fetchData();
     fetchData();
+    const cleanupWebsocket = setupWebsocketEventHandler();
 
 
-    setupWebsocketEventHandler();
+    return () => {
+      if (cleanupWebsocket) cleanupWebsocket();
+    };
   }, [fetchData, setupWebsocketEventHandler]);
   }, [fetchData, setupWebsocketEventHandler]);
 
 
   const showExportingData = (isExported || isExporting) && (progressList != null);
   const showExportingData = (isExported || isExporting) && (progressList != null);

+ 2 - 0
apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.jsx

@@ -126,7 +126,9 @@ export const CopyDropdown = (props) => {
         </DropdownToggle>
         </DropdownToggle>
 
 
         <DropdownMenu
         <DropdownMenu
+          className={`${styles['copy-clipboard-dropdown-menu']}`}
           strategy="fixed"
           strategy="fixed"
+          container="body"
         >
         >
           <div className="d-flex align-items-center justify-content-between">
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
             <DropdownItem header className="px-3">

+ 6 - 6
apps/app/src/client/components/Common/CopyDropdown/CopyDropdown.module.scss

@@ -1,12 +1,7 @@
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use '@growi/core-styles/scss/bootstrap/init' as bs;
 @use '@growi/ui/scss/atoms/btn-muted';
 @use '@growi/ui/scss/atoms/btn-muted';
 
 
-.grw-copy-dropdown :global {
-  .btn.btn-copy {
-    @include btn-muted.colorize(bs.$gray-500);
-  }
-
-  .dropdown-menu {
+.copy-clipboard-dropdown-menu :global {
     min-width: 310px;
     min-width: 310px;
     max-width: 375px;
     max-width: 375px;
 
 
@@ -30,4 +25,9 @@
       word-break: break-all;
       word-break: break-all;
     }
     }
   }
   }
+
+.grw-copy-dropdown :global {
+  .btn.btn-copy {
+    @include btn-muted.colorize(bs.$gray-500);
+  }
 }
 }

+ 10 - 0
apps/app/src/client/components/Common/RendererErrorMessage.tsx

@@ -0,0 +1,10 @@
+import React from 'react';
+
+export const RendererErrorMessage: React.FC = () => {
+  return (
+    <p className="alert alert-warning">
+      ⚠️ <strong>Developer Warning:</strong>{' '}
+      Required renderer configuration is missing. Ensure <code>useRendererConfig()</code> is properly called in the component.
+    </p>
+  );
+};

+ 0 - 5
apps/app/src/client/components/Me/OtherSettings.tsx

@@ -1,7 +1,6 @@
 import type { JSX } from 'react';
 import type { JSX } from 'react';
 
 
 import { ColorModeSettings } from './ColorModeSettings';
 import { ColorModeSettings } from './ColorModeSettings';
-import { QuestionnaireSettings } from './QuestionnaireSettings';
 import { UISettings } from './UISettings';
 import { UISettings } from './UISettings';
 
 
 
 
@@ -16,10 +15,6 @@ const OtherSettings = (): JSX.Element => {
       <div className="mt-4">
       <div className="mt-4">
         <UISettings />
         <UISettings />
       </div>
       </div>
-
-      <div className="mt-4">
-        <QuestionnaireSettings />
-      </div>
     </>
     </>
   );
   );
 };
 };

+ 0 - 109
apps/app/src/client/components/Me/QuestionnaireSettings.tsx

@@ -1,109 +0,0 @@
-import {
-  useCallback, useEffect, useState, type JSX,
-} from 'react';
-
-import { LoadingSpinner } from '@growi/ui/dist/components';
-import { useTranslation } from 'react-i18next';
-import { UncontrolledTooltip } from 'reactstrap';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import { useSWRxIsQuestionnaireEnabled } from '~/features/questionnaire/client/stores/questionnaire';
-import { useCurrentUser } from '~/stores-universal/context';
-
-
-export const QuestionnaireSettings = (): JSX.Element => {
-  const { t } = useTranslation();
-  const { data: currentUser, error: errorCurrentUser } = useCurrentUser();
-  const { data: growiIsQuestionnaireEnabled } = useSWRxIsQuestionnaireEnabled();
-
-  const [isQuestionnaireEnabled, setIsQuestionnaireEnabled] = useState(currentUser?.isQuestionnaireEnabled);
-
-  const onChangeIsQuestionnaireEnabledHandler = useCallback(async() => {
-    setIsQuestionnaireEnabled(prev => !prev);
-  }, []);
-
-  const onClickUpdateIsQuestionnaireEnabledHandler = useCallback(async() => {
-    try {
-      await apiv3Put('/personal-setting/questionnaire-settings', {
-        isQuestionnaireEnabled,
-      });
-      toastSuccess(t('toaster.update_successed', { target: t('questionnaire.settings'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [isQuestionnaireEnabled, t]);
-
-  // Sync currentUser and state
-  useEffect(() => {
-    setIsQuestionnaireEnabled(currentUser?.isQuestionnaireEnabled);
-  }, [currentUser?.isQuestionnaireEnabled]);
-
-  const isLoadingCurrentUser = currentUser === undefined && errorCurrentUser === undefined;
-
-  return (
-    <>
-      <h2 className="border-bottom pb-2 mb-4 fs-4">{t('questionnaire.settings')}</h2>
-
-      {isLoadingCurrentUser && (
-        <div className="text-muted text-center mb-5">
-          <LoadingSpinner className="me-1 fs-3" />
-        </div>
-      )}
-
-      <div className="container">
-        {!isLoadingCurrentUser && (
-          <div className="offset-md-3 col-md-6 text-start row">
-            <div className="form-check form-switch">
-              <span id="grw-questionnaire-settings-toggle-wrapper">
-                <input
-                  type="checkbox"
-                  className="form-check-input"
-                  id="isQuestionnaireEnabled"
-                  checked={growiIsQuestionnaireEnabled && isQuestionnaireEnabled}
-                  onChange={onChangeIsQuestionnaireEnabledHandler}
-                  disabled={!growiIsQuestionnaireEnabled}
-                />
-                <label className="form-label form-check-label" htmlFor="isQuestionnaireEnabled">
-                  {t('questionnaire.enable_questionnaire')}
-                </label>
-              </span>
-              {!growiIsQuestionnaireEnabled && (
-                <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-toggle-wrapper">
-                  {t('questionnaire.disabled_by_admin')}
-                </UncontrolledTooltip>
-              ) }
-            </div>
-            <p className="form-text text-muted small">
-              {t('questionnaire.personal_settings_explanation')}
-            </p>
-          </div>
-        )}
-      </div>
-
-      <div className="row my-3">
-        <div className="offset-4 col-5">
-          <span className="d-inline-block" id="grw-questionnaire-settings-update-btn-wrapper">
-            <button
-              data-testid="grw-questionnaire-settings-update-btn"
-              type="button"
-              className="btn btn-primary"
-              onClick={onClickUpdateIsQuestionnaireEnabledHandler}
-              disabled={!growiIsQuestionnaireEnabled}
-              style={growiIsQuestionnaireEnabled ? {} : { pointerEvents: 'none' }}
-            >
-              {t('Update')}
-            </button>
-          </span>
-          {!growiIsQuestionnaireEnabled && (
-            <UncontrolledTooltip placement="bottom" target="grw-questionnaire-settings-update-btn-wrapper">
-              {t('questionnaire.disabled_by_admin')}
-            </UncontrolledTooltip>
-          )}
-        </div>
-      </div>
-    </>
-
-  );
-};

+ 1 - 1
apps/app/src/client/components/Me/UISettings.tsx

@@ -101,7 +101,7 @@ export const UISettings = (): JSX.Element => {
 
 
       <div className="row my-3">
       <div className="row my-3">
         <div className="offset-4 col-5">
         <div className="offset-4 col-5">
-          <button data-testid="" type="button" className="btn btn-primary" onClick={updateButtonHandler}>
+          <button data-testid="grw-ui-settings-update-btn" type="button" className="btn btn-primary" onClick={updateButtonHandler}>
             {t('Update')}
             {t('Update')}
           </button>
           </button>
         </div>
         </div>

+ 14 - 3
apps/app/src/client/components/PageAccessoriesModal/ShareLink/ShareLinkForm.tsx

@@ -48,6 +48,12 @@ export const ShareLinkForm: FC<Props> = (props: Props) => {
   }, []);
   }, []);
 
 
   const handleChangeCustomExpirationDate = useCallback((customExpirationDate: string) => {
   const handleChangeCustomExpirationDate = useCallback((customExpirationDate: string) => {
+    // set customExpirationDate to today if the input is empty
+    if (customExpirationDate.length === 0) {
+      setCustomExpirationDate(new Date());
+      return;
+    }
+
     const parsedDate = parse(customExpirationDate, 'yyyy-MM-dd', new Date());
     const parsedDate = parse(customExpirationDate, 'yyyy-MM-dd', new Date());
     setCustomExpirationDate(parsedDate);
     setCustomExpirationDate(parsedDate);
   }, []);
   }, []);
@@ -199,9 +205,14 @@ export const ShareLinkForm: FC<Props> = (props: Props) => {
             />
             />
           </div>
           </div>
         </div>
         </div>
-        <button type="button" className="btn btn-primary d-block mx-auto px-5" onClick={handleIssueShareLink} data-testid="btn-sharelink-issue">
-          {t('share_links.Issue')}
-        </button>
+
+        <div className="row mt-4">
+          <div className="col">
+            <button type="button" className="btn btn-primary d-block mx-auto px-5" onClick={handleIssueShareLink} data-testid="btn-sharelink-issue">
+              {t('share_links.Issue')}
+            </button>
+          </div>
+        </div>
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 2 - 2
apps/app/src/client/components/PageComment/CommentEditor.tsx

@@ -152,7 +152,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
   }, [onCanceled, initializeEditor]);
   }, [onCanceled, initializeEditor]);
 
 
   const postCommentHandler = useCallback(async() => {
   const postCommentHandler = useCallback(async() => {
-    const commentBodyToPost = codeMirrorEditor?.getDoc() ?? '';
+    const commentBodyToPost = codeMirrorEditor?.getDocString() ?? '';
 
 
     try {
     try {
       if (currentCommentId != null) {
       if (currentCommentId != null) {
@@ -276,7 +276,7 @@ export const CommentEditor = (props: CommentEditorProps): JSX.Element => {
           </TabPane>
           </TabPane>
           <TabPane tabId="comment_preview">
           <TabPane tabId="comment_preview">
             <div className="comment-preview-container">
             <div className="comment-preview-container">
-              <CommentPreview markdown={codeMirrorEditor?.getDoc() ?? ''} />
+              <CommentPreview markdown={codeMirrorEditor?.getDocString() ?? ''} />
             </div>
             </div>
           </TabPane>
           </TabPane>
         </TabContent>
         </TabContent>

+ 1 - 1
apps/app/src/client/components/PageEditor/ConflictDiffModal.tsx

@@ -60,7 +60,7 @@ const ConflictDiffModalCore = (props: ConflictDiffModalCoreProps): JSX.Element =
   }, [isRevisionselected]);
   }, [isRevisionselected]);
 
 
   const resolveConflictHandler = useCallback(async() => {
   const resolveConflictHandler = useCallback(async() => {
-    const newBody = codeMirrorEditor?.getDoc();
+    const newBody = codeMirrorEditor?.getDocString();
     if (newBody == null) {
     if (newBody == null) {
       return;
       return;
     }
     }

+ 3 - 3
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -156,7 +156,7 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
 
 
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
   const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
 
 
-  const [markdownToPreview, setMarkdownToPreview] = useState<string>(codeMirrorEditor?.getDoc() ?? '');
+  const [markdownToPreview, setMarkdownToPreview] = useState<string>(codeMirrorEditor?.getDocString() ?? '');
   const setMarkdownPreviewWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string) => {
   const setMarkdownPreviewWithDebounce = useMemo(() => debounce(100, throttle(150, (value: string) => {
     setMarkdownToPreview(value);
     setMarkdownToPreview(value);
   })), []);
   })), []);
@@ -217,7 +217,7 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
   }, [pageId, selectedGrant, mutateWaitingSaveProcessing, updatePage, mutateIsGrantNormalized, t]);
   }, [pageId, selectedGrant, mutateWaitingSaveProcessing, updatePage, mutateIsGrantNormalized, t]);
 
 
   const saveAndReturnToViewHandler = useCallback(async(opts: SaveOptions) => {
   const saveAndReturnToViewHandler = useCallback(async(opts: SaveOptions) => {
-    const markdown = codeMirrorEditor?.getDoc();
+    const markdown = codeMirrorEditor?.getDocString();
     const revisionId = isRevisionIdRequiredForPageUpdate ? currentRevisionId : undefined;
     const revisionId = isRevisionIdRequiredForPageUpdate ? currentRevisionId : undefined;
     const page = await save(revisionId, markdown, opts, onConflict);
     const page = await save(revisionId, markdown, opts, onConflict);
     if (page == null) {
     if (page == null) {
@@ -229,7 +229,7 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
   }, [codeMirrorEditor, currentRevisionId, isRevisionIdRequiredForPageUpdate, mutateEditorMode, onConflict, save, updateStateAfterSave]);
   }, [codeMirrorEditor, currentRevisionId, isRevisionIdRequiredForPageUpdate, mutateEditorMode, onConflict, save, updateStateAfterSave]);
 
 
   const saveWithShortcut = useCallback(async() => {
   const saveWithShortcut = useCallback(async() => {
-    const markdown = codeMirrorEditor?.getDoc();
+    const markdown = codeMirrorEditor?.getDocString();
     const revisionId = isRevisionIdRequiredForPageUpdate ? currentRevisionId : undefined;
     const revisionId = isRevisionIdRequiredForPageUpdate ? currentRevisionId : undefined;
     const page = await save(revisionId, markdown, undefined, onConflict);
     const page = await save(revisionId, markdown, undefined, onConflict);
     if (page == null) {
     if (page == null) {

+ 1 - 1
apps/app/src/client/components/PageEditor/conflict.tsx

@@ -97,7 +97,7 @@ export const useConflictEffect = (): void => {
         closePageStatusAlert();
         closePageStatusAlert();
       };
       };
 
 
-      const markdown = codeMirrorEditor?.getDoc();
+      const markdown = codeMirrorEditor?.getDocString();
       openConflictDiffModal(markdown ?? '', resolveConflictHandler);
       openConflictDiffModal(markdown ?? '', resolveConflictHandler);
     };
     };
 
 

+ 3 - 1
apps/app/src/client/components/PagePresentationModal.tsx

@@ -16,6 +16,7 @@ import { usePagePresentationModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { usePresentationViewOptions } from '~/stores/renderer';
 import { usePresentationViewOptions } from '~/stores/renderer';
 
 
+import { RendererErrorMessage } from './Common/RendererErrorMessage';
 
 
 import styles from './PagePresentationModal.module.scss';
 import styles from './PagePresentationModal.module.scss';
 
 
@@ -38,7 +39,7 @@ const PagePresentationModal = (): JSX.Element => {
   const fullscreen = useFullScreen();
   const fullscreen = useFullScreen();
 
 
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
-  const { data: rendererOptions } = usePresentationViewOptions();
+  const { data: rendererOptions, isLoading } = usePresentationViewOptions();
 
 
   const { data: isEnabledMarp } = useIsEnabledMarp();
   const { data: isEnabledMarp } = useIsEnabledMarp();
 
 
@@ -87,6 +88,7 @@ const PagePresentationModal = (): JSX.Element => {
         <button className="btn-close" type="button" aria-label="Close" onClick={closeHandler}></button>
         <button className="btn-close" type="button" aria-label="Close" onClick={closeHandler}></button>
       </div>
       </div>
       <ModalBody className="modal-body d-flex justify-content-center align-items-center">
       <ModalBody className="modal-body d-flex justify-content-center align-items-center">
+        { !isLoading && rendererOptions == null && <RendererErrorMessage />}
         { rendererOptions != null && isEnabledMarp != null && (
         { rendererOptions != null && isEnabledMarp != null && (
           <Presentation
           <Presentation
             options={{
             options={{

+ 4 - 0
apps/app/src/client/components/ShortcutsModal.module.scss

@@ -14,4 +14,8 @@
   }
   }
 
 
   @include modifier-keys.modifier-key;
   @include modifier-keys.modifier-key;
+
+  .grw-modal-body-style {
+    max-height: calc(100vh - 200px);
+  }
 }
 }

+ 153 - 4
apps/app/src/client/components/ShortcutsModal.tsx

@@ -234,9 +234,159 @@ const ShortcutsModal = (): JSX.Element => {
                   </div>
                   </div>
                 </div>
                 </div>
               </li>
               </li>
-              {/* Toggle Line */}
+              {/* Multiple Cursors */}
               <li className="d-flex align-items-center p-3">
               <li className="d-flex align-items-center p-3">
-                <div className="flex-grow-1">{t('modal_shortcuts.editor.Toggle Line')}</div>
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.editor.Multiple Cursors')}
+                </div>
+                <div className="text-nowrap">
+                  <div className="text-end">
+                    <div>
+                      <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                      <span className="text-secondary mx-2">+</span>
+                      <span className={`key alt-key ${additionalClassByOs}`}></span>
+                      <span className="text-secondary ms-2">+</span>
+                    </div>
+                    <div className="mt-1">
+                      <span className="key material-symbols-outlined fs-5 px-0">arrow_downward</span>
+                      <span className="text-secondary mx-2">or</span>
+                      <span className="key material-symbols-outlined fs-5 px-0">arrow_upward</span>
+                    </div>
+                    <span className="small text-secondary">{t('modal_shortcuts.editor.Or Alt Click')}</span>
+
+                  </div>
+                </div>
+              </li>
+            </ul>
+          </div>
+        </div>
+
+        {/* Format settings section */}
+        <div className="row mt-4">
+          <div className="col-lg-6">
+            <h6>
+              <strong>{t('modal_shortcuts.format.title')}</strong>
+            </h6>
+            <ul className="list-unstyled m-0">
+              {/* Bold */}
+              <li className="d-flex align-items-center p-3 border-bottom">
+                <div className="flex-grow-1">{t('modal_shortcuts.format.Bold')}</div>
+                <div className="text-nowrap">
+                  <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">B</span>
+                </div>
+              </li>
+              {/* Italic */}
+              <li className="d-flex align-items-center p-3 border-bottom">
+                <div className="flex-grow-1">{t('modal_shortcuts.format.Italic')}</div>
+                <div className="text-nowrap">
+                  <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">Shift</span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">I</span>
+                </div>
+              </li>
+              {/* Strikethrough */}
+              <li className="d-flex align-items-center p-3 border-bottom">
+                <div className="flex-grow-1">{t('modal_shortcuts.format.Strikethrough')}</div>
+                <div className="text-nowrap">
+                  <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">Shift</span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">X</span>
+                </div>
+              </li>
+              {/* Code Text */}
+              <li className="d-flex align-items-center p-3 border-bottom">
+                <div className="flex-grow-1">{t('modal_shortcuts.format.Code Text')}</div>
+                <div className="text-nowrap">
+                  <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">Shift</span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">C</span>
+                </div>
+              </li>
+              {/* Hyperlink */}
+              <li className="d-flex align-items-center p-3">
+                <div className="flex-grow-1">{t('modal_shortcuts.format.Hyperlink')}</div>
+                <div className="text-nowrap">
+                  <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">Shift</span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">U</span>
+                </div>
+              </li>
+            </ul>
+          </div>
+
+          <div className="col-lg-6">
+            <h6>
+              <strong>{t('modal_shortcuts.line_settings.title')}</strong>
+            </h6>
+            <ul className="list-unstyled m-0">
+              {/* Simple List */}
+              <li className="d-flex align-items-center p-3 border-bottom">
+                <div className="flex-grow-1">{t('modal_shortcuts.line_settings.Numbered List')}</div>
+                <div className="text-nowrap">
+                  <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">Shift</span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">7</span>
+                </div>
+              </li>
+              {/* Numbered List */}
+              <li className="d-flex align-items-center p-3 border-bottom">
+                <div className="flex-grow-1">{t('modal_shortcuts.line_settings.Bullet List')}</div>
+                <div className="text-nowrap">
+                  <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">Shift</span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">8</span>
+                </div>
+              </li>
+              {/* Quote */}
+              <li className="d-flex align-items-center p-3 border-bottom">
+                <div className="flex-grow-1">{t('modal_shortcuts.line_settings.Quote')}</div>
+                <div className="text-nowrap">
+                  <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">Shift</span>
+                  <span className="text-secondary mx-2">+</span>
+                  <span className="key">9</span>
+                </div>
+              </li>
+              {/* Code Block */}
+              <li className="d-flex align-items-center p-3 border-bottom">
+                <div className="flex-grow-1">{t('modal_shortcuts.line_settings.Code Block')}</div>
+                <div className="text-nowrap">
+                  <div className="text-start">
+                    <div>
+                      <span className={`key cmd-key ${additionalClassByOs}`}></span>
+                      <span className="text-secondary mx-2">+</span>
+                      <span className={`key alt-key ${additionalClassByOs}`}></span>
+                      <span className="text-secondary ms-2">+</span>
+                    </div>
+                    <div className="mt-1">
+                      <span className="key">Shift</span>
+                      <span className="text-secondary mx-2">+</span>
+                      <span className="key">C</span>
+                    </div>
+                  </div>
+                </div>
+              </li>
+              {/* Hide comments */}
+              <li className="d-flex align-items-center p-3">
+                <div className="flex-grow-1">
+                  {t('modal_shortcuts.line_settings.Comment Out')}<br />
+                  <span className="small text-secondary">{t('modal_shortcuts.line_settings.Comment Out Desc')}</span>
+                </div>
                 <div className="text-nowrap">
                 <div className="text-nowrap">
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className={`key cmd-key ${additionalClassByOs}`}></span>
                   <span className="text-secondary mx-2">+</span>
                   <span className="text-secondary mx-2">+</span>
@@ -245,7 +395,6 @@ const ShortcutsModal = (): JSX.Element => {
               </li>
               </li>
             </ul>
             </ul>
           </div>
           </div>
-          {/* TODO: Add docs link button https://redmine.weseek.co.jp/issues/161862 */}
         </div>
         </div>
       </div>
       </div>
     );
     );
@@ -258,7 +407,7 @@ const ShortcutsModal = (): JSX.Element => {
           <ModalHeader tag="h4" toggle={close} className="px-4">
           <ModalHeader tag="h4" toggle={close} className="px-4">
             {t('Shortcuts')}
             {t('Shortcuts')}
           </ModalHeader>
           </ModalHeader>
-          <ModalBody className="p-md-4">
+          <ModalBody className="p-md-4 mb-3 grw-modal-body-style overflow-auto">
             {bodyContent()}
             {bodyContent()}
           </ModalBody>
           </ModalBody>
         </Modal>
         </Modal>

+ 1 - 19
apps/app/src/client/components/Sidebar/SidebarNav/PersonalDropdown.tsx

@@ -1,9 +1,8 @@
-import { useState, type JSX } from 'react';
+import { type JSX } from 'react';
 
 
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { UserPicture } from '@growi/ui/dist/components';
 import { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import Link from 'next/link';
 import {
 import {
   UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
   UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
@@ -17,14 +16,10 @@ import { SkeletonItem } from './SkeletonItem';
 
 
 import styles from './PersonalDropdown.module.scss';
 import styles from './PersonalDropdown.module.scss';
 
 
-const ProactiveQuestionnaireModal = dynamic(() => import('~/features/questionnaire/client/components/ProactiveQuestionnaireModal'), { ssr: false });
-
 export const PersonalDropdown = (): JSX.Element => {
 export const PersonalDropdown = (): JSX.Element => {
   const { t } = useTranslation('commons');
   const { t } = useTranslation('commons');
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
 
 
-  const [isQuestionnaireModalOpen, setQuestionnaireModalOpen] = useState(false);
-
   if (currentUser == null) {
   if (currentUser == null) {
     return <SkeletonItem />;
     return <SkeletonItem />;
   }
   }
@@ -97,17 +92,6 @@ export const PersonalDropdown = (): JSX.Element => {
             </DropdownItem>
             </DropdownItem>
           </Link>
           </Link>
 
 
-          <DropdownItem
-            data-testid="grw-proactive-questionnaire-modal-toggle-btn"
-            onClick={() => setQuestionnaireModalOpen(true)}
-            className={`my-1 ${styles['personal-dropdown-item']}`}
-          >
-            <span className="d-flex align-items-center">
-              <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">edit_note</span>
-              <span className="item-text">{t('personal_dropdown.feedback')}</span>
-            </span>
-          </DropdownItem>
-
           <DropdownItem data-testid="logout-button" onClick={logoutHandler} className={`my-1 ${styles['personal-dropdown-item']}`}>
           <DropdownItem data-testid="logout-button" onClick={logoutHandler} className={`my-1 ${styles['personal-dropdown-item']}`}>
             <span className="d-flex align-items-center">
             <span className="d-flex align-items-center">
               <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">logout</span>
               <span className="item-icon material-symbols-outlined me-2 pb-0 fs-6">logout</span>
@@ -116,8 +100,6 @@ export const PersonalDropdown = (): JSX.Element => {
           </DropdownItem>
           </DropdownItem>
         </DropdownMenu>
         </DropdownMenu>
       </UncontrolledDropdown>
       </UncontrolledDropdown>
-
-      <ProactiveQuestionnaireModal isOpen={isQuestionnaireModalOpen} onClose={() => setQuestionnaireModalOpen(false)} />
     </>
     </>
   );
   );
 
 

+ 10 - 10
apps/app/src/client/services/renderer/renderer.tsx

@@ -22,14 +22,14 @@ import { RichAttachment } from '~/client/components/ReactMarkdownComponents/Rich
 import { TableWithEditButton } from '~/client/components/ReactMarkdownComponents/TableWithEditButton';
 import { TableWithEditButton } from '~/client/components/ReactMarkdownComponents/TableWithEditButton';
 import * as callout from '~/features/callout';
 import * as callout from '~/features/callout';
 import * as mermaid from '~/features/mermaid';
 import * as mermaid from '~/features/mermaid';
+import * as plantuml from '~/features/plantuml';
 import type { RendererOptions } from '~/interfaces/renderer-options';
 import type { RendererOptions } from '~/interfaces/renderer-options';
-import type { RendererConfig } from '~/interfaces/services/renderer';
+import { type RendererConfigExt } from '~/interfaces/services/renderer';
 import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
 import * as addLineNumberAttribute from '~/services/renderer/rehype-plugins/add-line-number-attribute';
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
 import * as keywordHighlighter from '~/services/renderer/rehype-plugins/keyword-highlighter';
 import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
 import * as relocateToc from '~/services/renderer/rehype-plugins/relocate-toc';
 import * as attachment from '~/services/renderer/remark-plugins/attachment';
 import * as attachment from '~/services/renderer/remark-plugins/attachment';
 import * as codeBlock from '~/services/renderer/remark-plugins/codeblock';
 import * as codeBlock from '~/services/renderer/remark-plugins/codeblock';
-import * as plantuml from '~/services/renderer/remark-plugins/plantuml';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import * as xsvToTable from '~/services/renderer/remark-plugins/xsv-to-table';
 import {
 import {
   getCommonSanitizeOption, generateCommonOptions, verifySanitizePlugin,
   getCommonSanitizeOption, generateCommonOptions, verifySanitizePlugin,
@@ -51,7 +51,7 @@ assert(isClient(), 'This module must be loaded only from client modules.');
 
 
 export const generateViewOptions = (
 export const generateViewOptions = (
     pagePath: string,
     pagePath: string,
-    config: RendererConfig,
+    config: RendererConfigExt,
     storeTocNode: (toc: HtmlElementNode) => void,
     storeTocNode: (toc: HtmlElementNode) => void,
 ): RendererOptions => {
 ): RendererOptions => {
 
 
@@ -62,7 +62,7 @@ export const generateViewOptions = (
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode }],
     drawio.remarkPlugin,
     drawio.remarkPlugin,
     mermaid.remarkPlugin,
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
@@ -128,7 +128,7 @@ export const generateViewOptions = (
   return options;
   return options;
 };
 };
 
 
-export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementNode | undefined): RendererOptions => {
+export const generateTocOptions = (config: RendererConfigExt, tocNode: HtmlElementNode | undefined): RendererOptions => {
 
 
   const options = generateCommonOptions(undefined);
   const options = generateCommonOptions(undefined);
 
 
@@ -158,7 +158,7 @@ export const generateTocOptions = (config: RendererConfig, tocNode: HtmlElementN
 };
 };
 
 
 export const generateSimpleViewOptions = (
 export const generateSimpleViewOptions = (
-    config: RendererConfig,
+    config: RendererConfigExt,
     pagePath: string,
     pagePath: string,
     highlightKeywords?: string | string[],
     highlightKeywords?: string | string[],
     overrideIsEnabledLinebreaks?: boolean,
     overrideIsEnabledLinebreaks?: boolean,
@@ -170,7 +170,7 @@ export const generateSimpleViewOptions = (
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode }],
     drawio.remarkPlugin,
     drawio.remarkPlugin,
     mermaid.remarkPlugin,
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,
@@ -232,7 +232,7 @@ export const generateSimpleViewOptions = (
 };
 };
 
 
 export const generatePresentationViewOptions = (
 export const generatePresentationViewOptions = (
-    config: RendererConfig,
+    config: RendererConfigExt,
     pagePath: string,
     pagePath: string,
 ): RendererOptions => {
 ): RendererOptions => {
   // based on simple view options
   // based on simple view options
@@ -259,7 +259,7 @@ export const generatePresentationViewOptions = (
   return options;
   return options;
 };
 };
 
 
-export const generatePreviewOptions = (config: RendererConfig, pagePath: string): RendererOptions => {
+export const generatePreviewOptions = (config: RendererConfigExt, pagePath: string): RendererOptions => {
   const options = generateCommonOptions(pagePath);
   const options = generateCommonOptions(pagePath);
 
 
   const { remarkPlugins, rehypePlugins, components } = options;
   const { remarkPlugins, rehypePlugins, components } = options;
@@ -267,7 +267,7 @@ export const generatePreviewOptions = (config: RendererConfig, pagePath: string)
   // add remark plugins
   // add remark plugins
   remarkPlugins.push(
   remarkPlugins.push(
     math,
     math,
-    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri }],
+    [plantuml.remarkPlugin, { plantumlUri: config.plantumlUri, isDarkMode: config.isDarkMode }],
     drawio.remarkPlugin,
     drawio.remarkPlugin,
     mermaid.remarkPlugin,
     mermaid.remarkPlugin,
     xsvToTable.remarkPlugin,
     xsvToTable.remarkPlugin,

+ 1 - 1
apps/app/src/components/Script/DrawioViewerScript/DrawioViewerScript.tsx

@@ -19,7 +19,7 @@ export const DrawioViewerScript = ({ drawioUri }: Props): JSX.Element => {
 
 
   const loadedHandler = useCallback(() => {
   const loadedHandler = useCallback(() => {
     // disable useResizeSensor and checkVisibleState
     // disable useResizeSensor and checkVisibleState
-    //   for preventing resize event by viewer.min.js
+    //   for preventing resize event by viewer-static.min.js
     GraphViewer.useResizeSensor = false;
     GraphViewer.useResizeSensor = false;
     GraphViewer.prototype.checkVisibleState = false;
     GraphViewer.prototype.checkVisibleState = false;
 
 

+ 4 - 4
apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.spec.ts

@@ -3,10 +3,10 @@ import { useViewerMinJsUrl } from './use-viewer-min-js-url';
 describe('useViewerMinJsUrl', () => {
 describe('useViewerMinJsUrl', () => {
   it.each`
   it.each`
     drawioUri                                     | expected
     drawioUri                                     | expected
-    ${'http://localhost:8080'}                    | ${'http://localhost:8080/js/viewer.min.js'}
-    ${'http://example.com'}                       | ${'http://example.com/js/viewer.min.js'}
-    ${'http://example.com/drawio'}                | ${'http://example.com/drawio/js/viewer.min.js'}
-    ${'http://example.com/?offline=1&https=0'}    | ${'http://example.com/js/viewer.min.js?offline=1&https=0'}
+    ${'http://localhost:8080'}                    | ${'http://localhost:8080/js/viewer-static.min.js'}
+    ${'http://example.com'}                       | ${'http://example.com/js/viewer-static.min.js'}
+    ${'http://example.com/drawio'}                | ${'http://example.com/drawio/js/viewer-static.min.js'}
+    ${'http://example.com/?offline=1&https=0'}    | ${'http://example.com/js/viewer-static.min.js?offline=1&https=0'}
   `('should return the expected URL "$expected" when drawioUri is "$drawioUrk"', ({ drawioUri, expected }: {drawioUri: string, expected: string}) => {
   `('should return the expected URL "$expected" when drawioUri is "$drawioUrk"', ({ drawioUri, expected }: {drawioUri: string, expected: string}) => {
     // Act
     // Act
     const url = useViewerMinJsUrl(drawioUri);
     const url = useViewerMinJsUrl(drawioUri);

+ 1 - 1
apps/app/src/components/Script/DrawioViewerScript/use-viewer-min-js-url.ts

@@ -3,7 +3,7 @@ import urljoin from 'url-join';
 export const useViewerMinJsUrl = (drawioUri: string): string => {
 export const useViewerMinJsUrl = (drawioUri: string): string => {
   // extract search from URL
   // extract search from URL
   const url = new URL(drawioUri);
   const url = new URL(drawioUri);
-  const pathname = urljoin(url.pathname, '/js/viewer.min.js');
+  const pathname = urljoin(url.pathname, '/js/viewer-static.min.js');
 
 
   return `${url.origin}${pathname}${url.search}`;
   return `${url.origin}${pathname}${url.search}`;
 };
 };

+ 32 - 7
apps/app/src/features/mermaid/components/MermaidViewer.tsx

@@ -1,6 +1,12 @@
 import React, { useRef, useEffect, type JSX } from 'react';
 import React, { useRef, useEffect, type JSX } from 'react';
 
 
 import mermaid from 'mermaid';
 import mermaid from 'mermaid';
+import { v7 as uuidV7 } from 'uuid';
+
+import { useNextThemes } from '~/stores-universal/use-next-themes';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:features:mermaid:MermaidViewer');
 
 
 type MermaidViewerProps = {
 type MermaidViewerProps = {
   value: string
   value: string
@@ -9,23 +15,42 @@ type MermaidViewerProps = {
 export const MermaidViewer = React.memo((props: MermaidViewerProps): JSX.Element => {
 export const MermaidViewer = React.memo((props: MermaidViewerProps): JSX.Element => {
   const { value } = props;
   const { value } = props;
 
 
+  const { isDarkMode } = useNextThemes();
+
   const ref = useRef<HTMLDivElement>(null);
   const ref = useRef<HTMLDivElement>(null);
 
 
   useEffect(() => {
   useEffect(() => {
-    if (ref.current != null && value != null) {
-      mermaid.initialize({});
-      mermaid.run({ nodes: [ref.current] });
-    }
-  }, [value]);
+    (async() => {
+      if (ref.current != null && value != null) {
+        mermaid.initialize({
+          theme: isDarkMode ? 'dark' : undefined,
+        });
+        try {
+          // Attempting to render multiple Mermaid diagrams using `mermaid.run` can cause duplicate SVG IDs.
+          // This is because it uses `Date.now()` for ID generation.
+          // ID generation logic: https://github.com/mermaid-js/mermaid/blob/5b241bbb97f81d37df8a84da523dfa53ac13bfd1/packages/mermaid/src/utils.ts#L755-L764
+          // Related issue: https://github.com/mermaid-js/mermaid/issues/4650
+          // Instead of `mermaid.run`, we use `mermaid.render` which allows us to assign a unique ID.
+          const id = `mermaid-${uuidV7()}`;
+          const { svg } = await mermaid.render(id, value, ref.current);
+          ref.current.innerHTML = svg;
+        }
+        catch (err) {
+          logger.error(err);
+        }
+      }
+    })();
+  }, [isDarkMode, value]);
 
 
   return (
   return (
     value
     value
       ? (
       ? (
-        <div ref={ref} key={value as string}>
+        <div ref={ref} key={value}>
           {value}
           {value}
         </div>
         </div>
       )
       )
-      : <div key={value as string}></div>
+      : <div key={value}></div>
   );
   );
 });
 });
+
 MermaidViewer.displayName = 'MermaidViewer';
 MermaidViewer.displayName = 'MermaidViewer';

+ 11 - 2
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantManagementModal/AiAssistantManagementModal.tsx

@@ -19,7 +19,12 @@ import loggerFactory from '~/utils/logger';
 import type { SelectedPage } from '../../../../interfaces/selected-page';
 import type { SelectedPage } from '../../../../interfaces/selected-page';
 import { removeGlobPath } from '../../../../utils/remove-glob-path';
 import { removeGlobPath } from '../../../../utils/remove-glob-path';
 import { createAiAssistant, updateAiAssistant } from '../../../services/ai-assistant';
 import { createAiAssistant, updateAiAssistant } from '../../../services/ai-assistant';
-import { useAiAssistantManagementModal, AiAssistantManagementModalPageMode, useSWRxAiAssistants } from '../../../stores/ai-assistant';
+import {
+  useSWRxAiAssistants,
+  useAiAssistantSidebar,
+  useAiAssistantManagementModal,
+  AiAssistantManagementModalPageMode,
+} from '../../../stores/ai-assistant';
 
 
 import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
 import { AiAssistantManagementEditInstruction } from './AiAssistantManagementEditInstruction';
 import { AiAssistantManagementEditPages } from './AiAssistantManagementEditPages';
 import { AiAssistantManagementEditPages } from './AiAssistantManagementEditPages';
@@ -63,6 +68,7 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { mutate: mutateAiAssistants } = useSWRxAiAssistants();
   const { mutate: mutateAiAssistants } = useSWRxAiAssistants();
   const { data: aiAssistantManagementModalData, close: closeAiAssistantManagementModal } = useAiAssistantManagementModal();
   const { data: aiAssistantManagementModalData, close: closeAiAssistantManagementModal } = useAiAssistantManagementModal();
+  const { data: aiAssistantSidebarData, refreshAiAssistantData } = useAiAssistantSidebar();
   const { data: pagePathsWithDescendantCount } = useSWRxPagePathsWithDescendantCount(
   const { data: pagePathsWithDescendantCount } = useSWRxPagePathsWithDescendantCount(
     removeGlobPath(aiAssistantManagementModalData?.aiAssistantData?.pagePathPatterns) ?? null,
     removeGlobPath(aiAssistantManagementModalData?.aiAssistantData?.pagePathPatterns) ?? null,
     undefined,
     undefined,
@@ -144,7 +150,10 @@ const AiAssistantManagementModalSubstance = (): JSX.Element => {
       };
       };
 
 
       if (shouldEdit) {
       if (shouldEdit) {
-        await updateAiAssistant(aiAssistant._id, reqBody);
+        const updatedAiAssistant = await updateAiAssistant(aiAssistant._id, reqBody);
+        if (aiAssistantSidebarData?.aiAssistantData?._id === updatedAiAssistant._id) {
+          refreshAiAssistantData(updatedAiAssistant);
+        }
       }
       }
       else {
       else {
         await createAiAssistant(reqBody);
         await createAiAssistant(reqBody);

+ 24 - 10
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantChatInitialView.tsx

@@ -1,5 +1,10 @@
+import Link from 'next/link';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
+import { removeGlobPath } from '../../../../utils/remove-glob-path';
+
+import { ThreadList } from './ThreadList';
+
 type Props = {
 type Props = {
   description: string,
   description: string,
   pagePathPatterns: string[],
   pagePathPatterns: string[],
@@ -10,26 +15,35 @@ export const AiAssistantChatInitialView: React.FC<Props> = ({ description, pageP
 
 
   return (
   return (
     <>
     <>
-      <p className="fs-6 text-body-secondary mb-0">
-        {description}
-      </p>
+      {description.length !== 0 && (
+        <p className="text-body-secondary mb-0">
+          {description}
+        </p>
+      )}
 
 
       <div>
       <div>
-        <div className="d-flex align-items-center">
-          <p className="text-body-secondary mb-0">{t('sidebar_ai_assistant.reference_pages_label')}</p>
-        </div>
+        <p className="text-body-secondary mb-1">
+          {t('sidebar_ai_assistant.reference_pages_label')}
+        </p>
         <div className="d-flex flex-column gap-1">
         <div className="d-flex flex-column gap-1">
           { pagePathPatterns.map(pagePathPattern => (
           { pagePathPatterns.map(pagePathPattern => (
-            <a
+            <Link
               key={pagePathPattern}
               key={pagePathPattern}
-              href="#"
-              className="fs-6 text-body-secondary text-decoration-none"
+              href={removeGlobPath([pagePathPattern])[0]}
+              className="text-body-secondary text-decoration-underline link-underline-secondary"
             >
             >
               {pagePathPattern}
               {pagePathPattern}
-            </a>
+            </Link>
           ))}
           ))}
         </div>
         </div>
       </div>
       </div>
+
+      <div>
+        <p className="text-body-secondary mb-1">
+          {t('sidebar_ai_assistant.recent_chat')}
+        </p>
+        <ThreadList />
+      </div>
     </>
     </>
   );
   );
 };
 };

+ 49 - 12
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/AiAssistantSidebar.tsx

@@ -95,6 +95,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
     // Views
     // Views
     generateInitialView: generateInitialViewForEditorAssistant,
     generateInitialView: generateInitialViewForEditorAssistant,
     generatingEditorTextLabel,
     generatingEditorTextLabel,
+    partialContentWarnLabel,
     generateActionButtons,
     generateActionButtons,
     headerIcon: headerIconForEditorAssistant,
     headerIcon: headerIconForEditorAssistant,
     headerText: headerTextForEditorAssistant,
     headerText: headerTextForEditorAssistant,
@@ -135,13 +136,20 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
 
 
     if (isEditorAssistant) {
     if (isEditorAssistant) {
       if (isEditorAssistantFormData(formData)) {
       if (isEditorAssistantFormData(formData)) {
-        const response = await postMessageForEditorAssistant(threadId, formData);
+        const response = await postMessageForEditorAssistant({
+          threadId,
+          formData,
+        });
         return response;
         return response;
       }
       }
       return;
       return;
     }
     }
     if (aiAssistantData?._id != null) {
     if (aiAssistantData?._id != null) {
-      const response = await postMessageForKnowledgeAssistant(aiAssistantData._id, threadId, formData);
+      const response = await postMessageForKnowledgeAssistant({
+        aiAssistantId: aiAssistantData._id,
+        threadId,
+        formData,
+      });
       return response;
       return response;
     }
     }
   }, [aiAssistantData?._id, isEditorAssistant, postMessageForEditorAssistant, postMessageForKnowledgeAssistant]);
   }, [aiAssistantData?._id, isEditorAssistant, postMessageForEditorAssistant, postMessageForKnowledgeAssistant]);
@@ -241,7 +249,10 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
 
 
         const chunk = decoder.decode(value);
         const chunk = decoder.decode(value);
 
 
-        const textValues: string[] = [];
+        let isPreMessageGenerated = false;
+        let isMainMessageGenerationStarted = false;
+        const preMessages: string[] = [];
+        const mainMessages: string[] = [];
         const lines = chunk.split('\n\n');
         const lines = chunk.split('\n\n');
         lines.forEach((line) => {
         lines.forEach((line) => {
           const trimmedLine = line.trim();
           const trimmedLine = line.trim();
@@ -249,14 +260,37 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
             const data = JSON.parse(line.replace('data: ', ''));
             const data = JSON.parse(line.replace('data: ', ''));
 
 
             processMessageForKnowledgeAssistant(data, {
             processMessageForKnowledgeAssistant(data, {
+              onPreMessage: (data) => {
+                // When main message is sent while pre-message is being transmitted
+                if (isMainMessageGenerationStarted) {
+                  preMessages.length = 0;
+                  return;
+                }
+                if (data.finished) {
+                  isPreMessageGenerated = true;
+                  return;
+                }
+                if (data.text == null) {
+                  return;
+                }
+                preMessages.push(data.text);
+              },
               onMessage: (data) => {
               onMessage: (data) => {
-                textValues.push(data.content[0].text.value);
+                if (!isMainMessageGenerationStarted) {
+                  isMainMessageGenerationStarted = true;
+                }
+
+                // When main message is sent while pre-message is being transmitted
+                if (!isPreMessageGenerated) {
+                  preMessages.length = 0;
+                }
+                mainMessages.push(data.content[0].text.value);
               },
               },
             });
             });
 
 
             processMessageForEditorAssistant(data, {
             processMessageForEditorAssistant(data, {
               onMessage: (data) => {
               onMessage: (data) => {
-                textValues.push(data.appendedMessage);
+                mainMessages.push(data.appendedMessage);
               },
               },
               onDetectedDiff: (data) => {
               onDetectedDiff: (data) => {
                 logger.debug('sse diff', { data });
                 logger.debug('sse diff', { data });
@@ -277,13 +311,12 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
           }
           }
         });
         });
 
 
-
         // append text values to the assistant message
         // append text values to the assistant message
         setGeneratingAnswerMessage((prevMessage) => {
         setGeneratingAnswerMessage((prevMessage) => {
           if (prevMessage == null) return;
           if (prevMessage == null) return;
           return {
           return {
             ...prevMessage,
             ...prevMessage,
-            content: prevMessage.content + textValues.join(''),
+            content: prevMessage.content + preMessages.join('') + mainMessages.join(''),
           };
           };
         });
         });
 
 
@@ -316,7 +349,13 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
   }, [isEditorAssistant, isTextSelected, submitSubstance]);
   }, [isEditorAssistant, isTextSelected, submitSubstance]);
 
 
   const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
   const keyDownHandler = (event: KeyboardEvent<HTMLTextAreaElement>) => {
-    if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
+    // Do nothing while composing
+    if (event.nativeEvent.isComposing) {
+      return;
+    }
+
+    if (event.key === 'Enter' && !event.shiftKey) {
+      event.preventDefault();
       form.handleSubmit(submit)();
       form.handleSubmit(submit)();
     }
     }
   };
   };
@@ -329,13 +368,10 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
   }, [headerIconForEditorAssistant, headerIconForKnowledgeAssistant, isEditorAssistant]);
   }, [headerIconForEditorAssistant, headerIconForKnowledgeAssistant, isEditorAssistant]);
 
 
   const headerText = useMemo(() => {
   const headerText = useMemo(() => {
-    if (threadData?.title) {
-      return threadData.title;
-    }
     return isEditorAssistant
     return isEditorAssistant
       ? headerTextForEditorAssistant
       ? headerTextForEditorAssistant
       : headerTextForKnowledgeAssistant;
       : headerTextForKnowledgeAssistant;
-  }, [threadData?.title, isEditorAssistant, headerTextForEditorAssistant, headerTextForKnowledgeAssistant]);
+  }, [isEditorAssistant, headerTextForEditorAssistant, headerTextForKnowledgeAssistant]);
 
 
   const placeHolder = useMemo(() => {
   const placeHolder = useMemo(() => {
     if (form.formState.isSubmitting) {
     if (form.formState.isSubmitting) {
@@ -418,6 +454,7 @@ const AiAssistantSidebarSubstance: React.FC<AiAssistantSidebarSubstanceProps> =
                         {generatingAnswerMessage.content}
                         {generatingAnswerMessage.content}
                       </MessageCard>
                       </MessageCard>
                     )}
                     )}
+                    { isEditorAssistant && partialContentWarnLabel }
                     { messageLogs.length > 0 && (
                     { messageLogs.length > 0 && (
                       <div className="d-flex justify-content-center">
                       <div className="d-flex justify-content-center">
                         <span className="bg-body-tertiary text-body-secondary rounded-pill px-3 py-1" style={{ fontSize: 'smaller' }}>
                         <span className="bg-body-tertiary text-body-secondary rounded-pill px-3 py-1" style={{ fontSize: 'smaller' }}>

+ 7 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/ThreadList.module.scss

@@ -0,0 +1,7 @@
+.thread-list :global {
+  li {
+    &:hover {
+      background-color: var(--bs-secondary-bg) !important;
+    }
+  }
+}

+ 57 - 0
apps/app/src/features/openai/client/components/AiAssistant/AiAssistantSidebar/ThreadList.tsx

@@ -0,0 +1,57 @@
+import React, { useCallback } from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import type { IThreadRelationHasId } from '~/features/openai/interfaces/thread-relation';
+
+import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
+import { useSWRxThreads } from '../../../stores/thread';
+
+import styles from './ThreadList.module.scss';
+
+const moduleClass = styles['thread-list'] ?? '';
+
+
+export const ThreadList: React.FC = () => {
+  const { t } = useTranslation();
+  const { openChat, data: aiAssistantSidebarData } = useAiAssistantSidebar();
+  const { data: threads } = useSWRxThreads(aiAssistantSidebarData?.aiAssistantData?._id);
+
+  const openChatHandler = useCallback((threadData: IThreadRelationHasId) => {
+    const aiAssistantData = aiAssistantSidebarData?.aiAssistantData;
+    if (aiAssistantData == null) {
+      return;
+    }
+
+    openChat(aiAssistantData, threadData);
+  }, [aiAssistantSidebarData?.aiAssistantData, openChat]);
+
+  if (threads == null || threads.length === 0) {
+    return (
+      <p className="text-body-secondary">
+        {t('sidebar_ai_assistant.no_recent_chat')}
+      </p>
+    );
+  }
+
+  return (
+    <>
+      <ul className={`list-group ${moduleClass}`}>
+        {threads.map(thread => (
+          <li
+            onClick={() => { openChatHandler(thread) }}
+            key={thread._id}
+            role="button"
+            tabIndex={0}
+            className="d-flex align-items-center list-group-item list-group-item-action border-0 rounded-1 bg-body-tertiary mb-2"
+          >
+            <div className="text-body-secondary">
+              <span className="material-symbols-outlined fs-5 me-2">chat</span>
+              <span className="flex-grow-1">{thread.title}</span>
+            </div>
+          </li>
+        ))}
+      </ul>
+    </>
+  );
+};

+ 207 - 0
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantList.tsx

@@ -0,0 +1,207 @@
+import React, { useCallback, useState } from 'react';
+
+import type { IUserHasId } from '@growi/core';
+import { getIdStringForRef } from '@growi/core';
+import { useTranslation } from 'react-i18next';
+import { Collapse } from 'reactstrap';
+
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useCurrentUser } from '~/stores-universal/context';
+import loggerFactory from '~/utils/logger';
+
+import { AiAssistantShareScope, type AiAssistantHasId } from '../../../../interfaces/ai-assistant';
+import { determineShareScope } from '../../../../utils/determine-share-scope';
+import { deleteAiAssistant, setDefaultAiAssistant } from '../../../services/ai-assistant';
+import { useAiAssistantSidebar, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
+import { getShareScopeIcon } from '../../../utils/get-share-scope-Icon';
+
+const logger = loggerFactory('growi:openai:client:components:AiAssistantList');
+
+/*
+*  AiAssistantItem
+*/
+type AiAssistantItemProps = {
+  currentUser?: IUserHasId | null;
+  aiAssistant: AiAssistantHasId;
+  onEditClick: (aiAssistantData: AiAssistantHasId) => void;
+  onItemClick: (aiAssistantData: AiAssistantHasId) => void;
+  onUpdated?: () => void;
+  onDeleted?: (aiAssistantId: string) => void;
+};
+
+const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
+  currentUser,
+  aiAssistant,
+  onEditClick,
+  onItemClick,
+  onUpdated,
+  onDeleted,
+}) => {
+
+  const { t } = useTranslation();
+
+  const openManagementModalHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
+    onEditClick(aiAssistantData);
+  }, [onEditClick]);
+
+  const openChatHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
+    onItemClick(aiAssistantData);
+  }, [onItemClick]);
+
+
+  const setDefaultAiAssistantHandler = useCallback(async() => {
+    try {
+      await setDefaultAiAssistant(aiAssistant._id, !aiAssistant.isDefault);
+      onUpdated?.();
+      toastSuccess(t('ai_assistant_substance.toaster.ai_assistant_set_default_success'));
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(t('ai_assistant_substance.toaster.ai_assistant_set_default_failed'));
+    }
+  }, [aiAssistant._id, aiAssistant.isDefault, onUpdated, t]);
+
+  const deleteAiAssistantHandler = useCallback(async() => {
+    try {
+      await deleteAiAssistant(aiAssistant._id);
+      onDeleted?.(aiAssistant._id);
+      toastSuccess(t('ai_assistant_substance.toaster.ai_assistant_deleted_success'));
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(t('ai_assistant_substance.toaster.ai_assistant_deleted_failed'));
+    }
+  }, [aiAssistant._id, onDeleted, t]);
+
+  const isOperable = currentUser?._id != null && getIdStringForRef(aiAssistant.owner) === currentUser._id;
+  const isPublicAiAssistantOperable = currentUser?.admin
+    && determineShareScope(aiAssistant.shareScope, aiAssistant.accessScope) === AiAssistantShareScope.PUBLIC_ONLY;
+
+  return (
+    <>
+      <li
+        onClick={(e) => {
+          e.stopPropagation();
+          openChatHandler(aiAssistant);
+        }}
+        role="button"
+        className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1"
+      >
+        <div className="d-flex justify-content-center">
+          <span className="material-symbols-outlined fs-5">{getShareScopeIcon(aiAssistant.shareScope, aiAssistant.accessScope)}</span>
+        </div>
+
+        <div className="grw-item-title ps-1">
+          <p className="text-truncate m-auto">{aiAssistant.name}</p>
+        </div>
+
+        <div className="grw-btn-actions opacity-0 d-flex justify-content-center ">
+          {isPublicAiAssistantOperable && (
+            <button
+              type="button"
+              className="btn btn-link text-secondary p-0"
+              onClick={(e) => {
+                e.stopPropagation();
+                setDefaultAiAssistantHandler();
+              }}
+            >
+              <span className={`material-symbols-outlined fs-5 ${aiAssistant.isDefault ? 'fill' : ''}`}>star</span>
+            </button>
+          )}
+          {isOperable && (
+            <>
+              <button
+                type="button"
+                className="btn btn-link text-secondary p-0"
+                onClick={(e) => {
+                  e.stopPropagation();
+                  openManagementModalHandler(aiAssistant);
+                }}
+              >
+                <span className="material-symbols-outlined fs-5">edit</span>
+              </button>
+              <button
+                type="button"
+                className="btn btn-link text-secondary p-0"
+                onClick={(e) => {
+                  e.stopPropagation();
+                  deleteAiAssistantHandler();
+                }}
+              >
+                <span className="material-symbols-outlined fs-5">delete</span>
+              </button>
+            </>
+          )}
+        </div>
+      </li>
+    </>
+  );
+};
+
+
+/*
+*  AiAssistantList
+*/
+type AiAssistantListProps = {
+  isTeamAssistant?: boolean;
+  aiAssistants: AiAssistantHasId[];
+  onUpdated?: () => void;
+  onDeleted?: (aiAssistantId: string) => void;
+  onCollapsed?: () => void;
+};
+
+export const AiAssistantList: React.FC<AiAssistantListProps> = ({
+  isTeamAssistant, aiAssistants, onUpdated, onDeleted, onCollapsed,
+}) => {
+  const { t } = useTranslation();
+  const { openChat } = useAiAssistantSidebar();
+  const { data: currentUser } = useCurrentUser();
+  const { open: openAiAssistantManagementModal } = useAiAssistantManagementModal();
+
+  const [isCollapsed, setIsCollapsed] = useState(false);
+
+  const toggleCollapse = useCallback(() => {
+    setIsCollapsed((prev) => {
+      if (!prev) {
+        onCollapsed?.();
+      }
+      return !prev;
+    });
+  }, [onCollapsed]);
+
+  return (
+    <>
+      <button
+        type="button"
+        className="btn btn-link p-0 text-secondary d-flex align-items-center"
+        aria-expanded={!isCollapsed}
+        onClick={toggleCollapse}
+        disabled={aiAssistants.length === 0}
+      >
+        <h3 className="grw-ai-assistant-substance-header fw-bold mb-0 me-1">
+          {t(`ai_assistant_substance.${isTeamAssistant ? 'team' : 'my'}_assistants`)}
+        </h3>
+        <span
+          className="material-symbols-outlined"
+        >{`keyboard_arrow_${isCollapsed ? 'up' : 'down'}`}
+        </span>
+      </button>
+
+      <Collapse isOpen={isCollapsed}>
+        <ul className="list-group">
+          {aiAssistants.map(assistant => (
+            <AiAssistantItem
+              key={assistant._id}
+              currentUser={currentUser}
+              aiAssistant={assistant}
+              onEditClick={openAiAssistantManagementModal}
+              onItemClick={openChat}
+              onUpdated={onUpdated}
+              onDeleted={onDeleted}
+            />
+          ))}
+        </ul>
+      </Collapse>
+    </>
+  );
+};

+ 34 - 0
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantSubstance.module.scss

@@ -1,5 +1,39 @@
+// == Colors
+.grw-ai-assistant-substance :global {
+  .grw-btn-actions {
+    .btn-link {
+      &:hover {
+        color: var(--bs-gray-800) !important;
+      }
+    }
+  }
+}
+
 .grw-ai-assistant-substance :global {
 .grw-ai-assistant-substance :global {
   .grw-ai-assistant-substance-header {
   .grw-ai-assistant-substance-header {
     font-size: 14px;
     font-size: 14px;
   }
   }
 }
 }
+
+.grw-ai-assistant-substance :global {
+  .list-group-item {
+    height: 40px;
+    padding-left: 4px;
+
+    .grw-item-title {
+      width: 100%;
+      overflow: hidden;
+      font-size: 14px;
+    }
+
+    .grw-btn-actions {
+      transition: opacity 0.2s ease-out;
+    }
+
+    &:hover {
+      .grw-btn-actions {
+        opacity: 1 !important;
+      }
+    }
+  }
+}

+ 35 - 21
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantSubstance.tsx

@@ -1,10 +1,12 @@
-import React, { type JSX } from 'react';
+import React, { type JSX, useCallback } from 'react';
 
 
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { useAiAssistantManagementModal, useSWRxAiAssistants } from '../../../stores/ai-assistant';
+import { useAiAssistantManagementModal, useSWRxAiAssistants, useAiAssistantSidebar } from '../../../stores/ai-assistant';
+import { useSWRINFxRecentThreads } from '../../../stores/thread';
 
 
-import { AiAssistantTree } from './AiAssistantTree';
+import { AiAssistantList } from './AiAssistantList';
+import { ThreadList } from './ThreadList';
 
 
 import styles from './AiAssistantSubstance.module.scss';
 import styles from './AiAssistantSubstance.module.scss';
 
 
@@ -13,8 +15,20 @@ const moduleClass = styles['grw-ai-assistant-substance'] ?? '';
 export const AiAssistantContent = (): JSX.Element => {
 export const AiAssistantContent = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { open } = useAiAssistantManagementModal();
   const { open } = useAiAssistantManagementModal();
+  const { data: aiAssistantSidebarData, close: closeAiAssistantSidebar } = useAiAssistantSidebar();
+  const { mutate: mutateRecentThreads } = useSWRINFxRecentThreads();
   const { data: aiAssistants, mutate: mutateAiAssistants } = useSWRxAiAssistants();
   const { data: aiAssistants, mutate: mutateAiAssistants } = useSWRxAiAssistants();
 
 
+  const deleteAiAssistantHandler = useCallback(async(aiAssistantId: string) => {
+    await mutateAiAssistants();
+    await mutateRecentThreads();
+
+    // If the sidebar is opened for the assistant being deleted, close it
+    if (aiAssistantSidebarData?.isOpened && aiAssistantSidebarData?.aiAssistantData?._id === aiAssistantId) {
+      closeAiAssistantSidebar();
+    }
+  }, [aiAssistantSidebarData?.aiAssistantData?._id, aiAssistantSidebarData?.isOpened, closeAiAssistantSidebar, mutateAiAssistants, mutateRecentThreads]);
+
   return (
   return (
     <div className={moduleClass}>
     <div className={moduleClass}>
       <button
       <button
@@ -23,33 +37,33 @@ export const AiAssistantContent = (): JSX.Element => {
         onClick={() => open()}
         onClick={() => open()}
       >
       >
         <span className="material-symbols-outlined fs-5 me-2">add</span>
         <span className="material-symbols-outlined fs-5 me-2">add</span>
-        <span className="fw-normal">{t('ai_assistant_tree.add_assistant')}</span>
+        <span className="fw-normal">{t('ai_assistant_substance.add_assistant')}</span>
       </button>
       </button>
 
 
       <div className="d-flex flex-column gap-4">
       <div className="d-flex flex-column gap-4">
         <div>
         <div>
-          <h3 className="fw-bold grw-ai-assistant-substance-header">
-            {t('ai_assistant_tree.my_assistants')}
-          </h3>
-          {aiAssistants?.myAiAssistants != null && aiAssistants.myAiAssistants.length !== 0 && (
-            <AiAssistantTree
-              onUpdated={mutateAiAssistants}
-              onDeleted={mutateAiAssistants}
-              aiAssistants={aiAssistants.myAiAssistants}
-            />
-          )}
+          <AiAssistantList
+            onUpdated={mutateAiAssistants}
+            onDeleted={deleteAiAssistantHandler}
+            onCollapsed={mutateAiAssistants}
+            aiAssistants={aiAssistants?.myAiAssistants ?? []}
+          />
+        </div>
+
+        <div>
+          <AiAssistantList
+            isTeamAssistant
+            onUpdated={mutateAiAssistants}
+            onCollapsed={mutateAiAssistants}
+            aiAssistants={aiAssistants?.teamAiAssistants ?? []}
+          />
         </div>
         </div>
 
 
         <div>
         <div>
           <h3 className="fw-bold grw-ai-assistant-substance-header">
           <h3 className="fw-bold grw-ai-assistant-substance-header">
-            {t('ai_assistant_tree.team_assistants')}
+            {t('ai_assistant_substance.recent_threads')}
           </h3>
           </h3>
-          {aiAssistants?.teamAiAssistants != null && aiAssistants.teamAiAssistants.length !== 0 && (
-            <AiAssistantTree
-              onUpdated={mutateAiAssistants}
-              aiAssistants={aiAssistants.teamAiAssistants}
-            />
-          )}
+          <ThreadList />
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>

+ 0 - 45
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.module.scss

@@ -1,45 +0,0 @@
-// == Colors
-.ai-assistant-tree-item :global {
-  .grw-ai-assistant-actions {
-    .btn-link {
-      &:hover {
-        color: var(--bs-gray-800) !important;
-      }
-    }
-  }
-}
-
-
-.ai-assistant-tree-item :global {
-  .list-group-item {
-    height: 40px;
-    padding-left: 4px;
-
-    .grw-ai-assistant-triangle-btn {
-      border: 0;
-      transition: transform 0.2s ease-out;
-      transform: rotate(0deg);
-
-      &.grw-ai-assistant-open {
-        transform: rotate(90deg);
-      }
-    }
-
-    .grw-ai-assistant-title-anchor {
-      width: 100%;
-      overflow: hidden;
-      font-size: 14px;
-    }
-
-
-    .grw-ai-assistant-actions {
-      transition: opacity 0.2s ease-out;
-    }
-
-    &:hover {
-      .grw-ai-assistant-actions {
-        opacity: 1 !important;
-      }
-    }
-  }
-}

+ 0 - 305
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/AiAssistantTree.tsx

@@ -1,305 +0,0 @@
-import React, { useCallback, useState } from 'react';
-
-import type { IUserHasId } from '@growi/core';
-import { getIdStringForRef } from '@growi/core';
-import { useTranslation } from 'react-i18next';
-
-import { toastError, toastSuccess } from '~/client/util/toastr';
-import type { IThreadRelationHasId } from '~/features/openai/interfaces/thread-relation';
-import { useCurrentUser } from '~/stores-universal/context';
-import loggerFactory from '~/utils/logger';
-
-import { AiAssistantShareScope, type AiAssistantHasId } from '../../../../interfaces/ai-assistant';
-import { determineShareScope } from '../../../../utils/determine-share-scope';
-import { deleteAiAssistant, setDefaultAiAssistant } from '../../../services/ai-assistant';
-import { deleteThread } from '../../../services/thread';
-import { useAiAssistantSidebar, useAiAssistantManagementModal } from '../../../stores/ai-assistant';
-import { useSWRMUTxThreads, useSWRxThreads } from '../../../stores/thread';
-import { getShareScopeIcon } from '../../../utils/get-share-scope-Icon';
-
-import styles from './AiAssistantTree.module.scss';
-
-const logger = loggerFactory('growi:openai:client:components:AiAssistantTree');
-
-const moduleClass = styles['ai-assistant-tree-item'] ?? '';
-
-
-/*
-*  ThreadItem
-*/
-type ThreadItemProps = {
-  threadData: IThreadRelationHasId
-  aiAssistantData: AiAssistantHasId;
-  onThreadClick: (aiAssistantData: AiAssistantHasId, threadData?: IThreadRelationHasId) => void;
-  onThreadDelete: () => void;
-};
-
-const ThreadItem: React.FC<ThreadItemProps> = ({
-  threadData, aiAssistantData, onThreadClick, onThreadDelete,
-}) => {
-  const { t } = useTranslation();
-
-  const deleteThreadHandler = useCallback(async() => {
-    try {
-      await deleteThread({ aiAssistantId: aiAssistantData._id, threadRelationId: threadData._id });
-      toastSuccess(t('ai_assistant_tree.toaster.thread_deleted_success'));
-      onThreadDelete();
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(t('ai_assistant_tree.toaster.thread_deleted_failed'));
-    }
-  }, [aiAssistantData._id, onThreadDelete, t, threadData._id]);
-
-  const openChatHandler = useCallback(() => {
-    onThreadClick(aiAssistantData, threadData);
-  }, [aiAssistantData, onThreadClick, threadData]);
-
-  return (
-    <li
-      role="button"
-      className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1 ps-5"
-      onClick={(e) => {
-        e.stopPropagation();
-        openChatHandler();
-      }}
-    >
-      <div>
-        <span className="material-symbols-outlined fs-5">chat</span>
-      </div>
-
-      <div className="grw-ai-assistant-title-anchor ps-1">
-        <p className="text-truncate m-auto">{threadData?.title ?? 'Untitled thread'}</p>
-      </div>
-
-      <div className="grw-ai-assistant-actions opacity-0 d-flex justify-content-center ">
-        <button
-          type="button"
-          className="btn btn-link text-secondary p-0"
-          onClick={(e) => {
-            e.stopPropagation();
-            deleteThreadHandler();
-          }}
-        >
-          <span className="material-symbols-outlined fs-5">delete</span>
-        </button>
-      </div>
-    </li>
-  );
-};
-
-
-/*
-*  ThreadItems
-*/
-type ThreadItemsProps = {
-  aiAssistantData: AiAssistantHasId;
-  onThreadClick: (aiAssistantData: AiAssistantHasId, threadData?: IThreadRelationHasId) => void;
-  onThreadDelete: () => void;
-};
-
-const ThreadItems: React.FC<ThreadItemsProps> = ({ aiAssistantData, onThreadClick, onThreadDelete }) => {
-  const { t } = useTranslation();
-  const { data: threads } = useSWRxThreads(aiAssistantData._id);
-
-  if (threads == null || threads.length === 0) {
-    return <p className="text-secondary ms-5">{t('ai_assistant_tree.thread_does_not_exist')}</p>;
-  }
-
-  return (
-    <div className="grw-ai-assistant-item-children">
-      {threads.map(thread => (
-        <ThreadItem
-          key={thread._id}
-          threadData={thread}
-          aiAssistantData={aiAssistantData}
-          onThreadClick={onThreadClick}
-          onThreadDelete={onThreadDelete}
-        />
-      ))}
-    </div>
-  );
-};
-
-
-/*
-*  AiAssistantItem
-*/
-type AiAssistantItemProps = {
-  currentUser?: IUserHasId | null;
-  aiAssistant: AiAssistantHasId;
-  onEditClick: (aiAssistantData: AiAssistantHasId) => void;
-  onItemClick: (aiAssistantData: AiAssistantHasId, threadData?: IThreadRelationHasId) => void;
-  onUpdated?: () => void;
-  onDeleted?: () => void;
-};
-
-const AiAssistantItem: React.FC<AiAssistantItemProps> = ({
-  currentUser,
-  aiAssistant,
-  onEditClick,
-  onItemClick,
-  onUpdated,
-  onDeleted,
-}) => {
-  const [isThreadsOpened, setIsThreadsOpened] = useState(false);
-
-  const { t } = useTranslation();
-  const { trigger: mutateThreadData } = useSWRMUTxThreads(aiAssistant._id);
-
-  const openManagementModalHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
-    onEditClick(aiAssistantData);
-  }, [onEditClick]);
-
-  const openChatHandler = useCallback((aiAssistantData: AiAssistantHasId) => {
-    onItemClick(aiAssistantData);
-  }, [onItemClick]);
-
-  const openThreadsHandler = useCallback(async() => {
-    mutateThreadData();
-    setIsThreadsOpened(toggle => !toggle);
-  }, [mutateThreadData]);
-
-  const setDefaultAiAssistantHandler = useCallback(async() => {
-    try {
-      await setDefaultAiAssistant(aiAssistant._id, !aiAssistant.isDefault);
-      onUpdated?.();
-      toastSuccess(t('ai_assistant_tree.toaster.ai_assistant_set_default_success'));
-    }
-    catch (err) {
-      logger.error(err);
-      toastError(t('ai_assistant_tree.toaster.ai_assistant_set_default_failed'));
-    }
-  }, [aiAssistant._id, aiAssistant.isDefault, onUpdated, t]);
-
-  const deleteAiAssistantHandler = useCallback(async() => {
-    try {
-      await deleteAiAssistant(aiAssistant._id);
-      onDeleted?.();
-      toastSuccess('ai_assistant_tree.toaster.assistant_deleted_success');
-    }
-    catch (err) {
-      logger.error(err);
-      toastError('ai_assistant_tree.toaster.assistant_deleted');
-    }
-  }, [aiAssistant._id, onDeleted]);
-
-  const isOperable = currentUser?._id != null && getIdStringForRef(aiAssistant.owner) === currentUser._id;
-  const isPublicAiAssistantOperable = currentUser?.admin
-    && determineShareScope(aiAssistant.shareScope, aiAssistant.accessScope) === AiAssistantShareScope.PUBLIC_ONLY;
-
-  return (
-    <>
-      <li
-        onClick={(e) => {
-          e.stopPropagation();
-          openChatHandler(aiAssistant);
-        }}
-        role="button"
-        className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1"
-      >
-        <div className="d-flex justify-content-center">
-          <button
-            type="button"
-            onClick={(e) => {
-              e.stopPropagation();
-              openThreadsHandler();
-            }}
-            className={`grw-ai-assistant-triangle-btn btn px-0 ${isThreadsOpened ? 'grw-ai-assistant-open' : ''}`}
-          >
-            <div className="d-flex justify-content-center">
-              <span className="material-symbols-outlined fs-5">arrow_right</span>
-            </div>
-          </button>
-        </div>
-
-        <div className="d-flex justify-content-center">
-          <span className="material-symbols-outlined fs-5">{getShareScopeIcon(aiAssistant.shareScope, aiAssistant.accessScope)}</span>
-        </div>
-
-        <div className="grw-ai-assistant-title-anchor ps-1">
-          <p className="text-truncate m-auto">{aiAssistant.name}</p>
-        </div>
-
-        <div className="grw-ai-assistant-actions opacity-0 d-flex justify-content-center ">
-          {isPublicAiAssistantOperable && (
-            <button
-              type="button"
-              className="btn btn-link text-secondary p-0"
-              onClick={(e) => {
-                e.stopPropagation();
-                setDefaultAiAssistantHandler();
-              }}
-            >
-              <span className={`material-symbols-outlined fs-5 ${aiAssistant.isDefault ? 'fill' : ''}`}>star</span>
-            </button>
-          )}
-          {isOperable && (
-            <>
-              <button
-                type="button"
-                className="btn btn-link text-secondary p-0"
-                onClick={(e) => {
-                  e.stopPropagation();
-                  openManagementModalHandler(aiAssistant);
-                }}
-              >
-                <span className="material-symbols-outlined fs-5">edit</span>
-              </button>
-              <button
-                type="button"
-                className="btn btn-link text-secondary p-0"
-                onClick={(e) => {
-                  e.stopPropagation();
-                  deleteAiAssistantHandler();
-                }}
-              >
-                <span className="material-symbols-outlined fs-5">delete</span>
-              </button>
-            </>
-          )}
-        </div>
-      </li>
-
-      { isThreadsOpened && (
-        <ThreadItems
-          aiAssistantData={aiAssistant}
-          onThreadClick={onItemClick}
-          onThreadDelete={mutateThreadData}
-        />
-      ) }
-    </>
-  );
-};
-
-
-/*
-*  AiAssistantTree
-*/
-type AiAssistantTreeProps = {
-  aiAssistants: AiAssistantHasId[];
-  onUpdated?: () => void;
-  onDeleted?: () => void;
-};
-
-export const AiAssistantTree: React.FC<AiAssistantTreeProps> = ({ aiAssistants, onUpdated, onDeleted }) => {
-  const { data: currentUser } = useCurrentUser();
-  const { openChat } = useAiAssistantSidebar();
-  const { open: openAiAssistantManagementModal } = useAiAssistantManagementModal();
-
-  return (
-    <ul className={`list-group ${moduleClass}`}>
-      {aiAssistants.map(assistant => (
-        <AiAssistantItem
-          key={assistant._id}
-          currentUser={currentUser}
-          aiAssistant={assistant}
-          onEditClick={openAiAssistantManagementModal}
-          onItemClick={openChat}
-          onUpdated={onUpdated}
-          onDeleted={onDeleted}
-        />
-      ))}
-    </ul>
-  );
-};

+ 86 - 0
apps/app/src/features/openai/client/components/AiAssistant/Sidebar/ThreadList.tsx

@@ -0,0 +1,86 @@
+import React, { useCallback } from 'react';
+
+import { getIdStringForRef } from '@growi/core';
+import { useTranslation } from 'react-i18next';
+
+import InfiniteScroll from '~/client/components/InfiniteScroll';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { useSWRMUTxThreads, useSWRINFxRecentThreads } from '~/features/openai/client/stores/thread';
+import loggerFactory from '~/utils/logger';
+
+import { deleteThread } from '../../../services/thread';
+import { useAiAssistantSidebar } from '../../../stores/ai-assistant';
+
+const logger = loggerFactory('growi:openai:client:components:ThreadList');
+
+export const ThreadList: React.FC = () => {
+  const swrInifiniteThreads = useSWRINFxRecentThreads();
+  const { t } = useTranslation();
+  const { data, mutate: mutateRecentThreads } = swrInifiniteThreads;
+  const { openChat, data: aiAssistantSidebarData, close: closeAiAssistantSidebar } = useAiAssistantSidebar();
+  const { trigger: mutateAssistantThreadData } = useSWRMUTxThreads(aiAssistantSidebarData?.aiAssistantData?._id);
+
+  const isEmpty = data?.[0]?.paginateResult.totalDocs === 0;
+  const isReachingEnd = isEmpty || (data != null && (data[data.length - 1].paginateResult.hasNextPage === false));
+
+  const deleteThreadHandler = useCallback(async(aiAssistantId: string, threadRelationId: string) => {
+    try {
+      await deleteThread({ aiAssistantId, threadRelationId });
+      toastSuccess(t('ai_assistant_substance.toaster.thread_deleted_success'));
+
+      await Promise.all([mutateAssistantThreadData(), mutateRecentThreads()]);
+
+      // Close if the thread to be deleted is open in right sidebar
+      if (aiAssistantSidebarData?.isOpened && aiAssistantSidebarData?.threadData?._id === threadRelationId) {
+        closeAiAssistantSidebar();
+      }
+    }
+    catch (err) {
+      logger.error(err);
+      toastError(t('ai_assistant_substance.toaster.thread_deleted_failed'));
+    }
+  }, [aiAssistantSidebarData?.isOpened, aiAssistantSidebarData?.threadData?._id, closeAiAssistantSidebar, mutateAssistantThreadData, mutateRecentThreads, t]);
+
+  return (
+    <>
+      <ul className="list-group">
+        <InfiniteScroll swrInifiniteResponse={swrInifiniteThreads} isReachingEnd={isReachingEnd}>
+          { data != null && data.map(thread => thread.paginateResult.docs).flat()
+            .map(thread => (
+              <li
+                key={thread._id}
+                role="button"
+                className="list-group-item list-group-item-action border-0 d-flex align-items-center rounded-1"
+                onClick={(e) => {
+                  e.stopPropagation();
+                  openChat(thread.aiAssistant, thread);
+                }}
+              >
+                <div>
+                  <span className="material-symbols-outlined fs-5">chat</span>
+                </div>
+
+                <div className="grw-item-title ps-1">
+                  <p className="text-truncate m-auto">{thread.title ?? 'Untitled thread'}</p>
+                </div>
+
+                <div className="grw-btn-actions opacity-0 d-flex justify-content-center ">
+                  <button
+                    type="button"
+                    className="btn btn-link text-secondary p-0"
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      deleteThreadHandler(getIdStringForRef(thread.aiAssistant), thread._id);
+                    }}
+                  >
+                    <span className="material-symbols-outlined fs-5">delete</span>
+                  </button>
+                </div>
+              </li>
+            ))
+          }
+        </InfiniteScroll>
+      </ul>
+    </>
+  );
+};

+ 129 - 0
apps/app/src/features/openai/client/interfaces/types.ts

@@ -0,0 +1,129 @@
+/**
+ * Enhanced types for GROWI Editor Assistant with roo-code compatible Search/Replace functionality
+ */
+
+// -----------------------------------------------------------------------------
+// Configuration Types
+// -----------------------------------------------------------------------------
+
+export interface ProcessorConfig {
+  /** Fuzzy matching threshold (0.0 to 1.0, default: 0.8) */
+  fuzzyThreshold?: number;
+  /** Number of buffer lines for context (default: 40) */
+  bufferLines?: number;
+  /** Whether to preserve original indentation (default: true) */
+  preserveIndentation?: boolean;
+  /** Whether to strip line numbers from content (default: true) */
+  stripLineNumbers?: boolean;
+  /** Enable aggressive matching for edge cases (default: false) */
+  enableAggressiveMatching?: boolean;
+  /** Maximum number of diff blocks per request (default: 10) */
+  maxDiffBlocks?: number;
+}
+
+// -----------------------------------------------------------------------------
+// Error Types
+// -----------------------------------------------------------------------------
+
+export type DiffErrorType =
+  | 'SEARCH_NOT_FOUND'
+  | 'EMPTY_SEARCH'
+  | 'CONTENT_ERROR';
+
+export interface DiffError {
+  type: DiffErrorType;
+  message: string;
+  line?: number;
+  details: {
+    searchContent: string;
+    bestMatch?: string;
+    similarity?: number;
+    suggestions: string[];
+    correctFormat?: string;
+    lineRange?: string;
+  };
+}
+
+// -----------------------------------------------------------------------------
+// Result Types
+// -----------------------------------------------------------------------------
+
+export interface DiffApplicationResult {
+  /** Whether the diff application was successful */
+  success: boolean;
+  /** Number of diffs successfully applied */
+  appliedCount: number;
+  /** Updated content if any diffs were applied */
+  content?: string;
+  /** Details of failed diff parts */
+  failedParts?: DiffError[];
+}
+
+export interface SingleDiffResult {
+  /** Whether this single diff was successful */
+  success: boolean;
+  /** Updated lines if successful */
+  updatedLines?: string[];
+  /** Line delta change (can be negative) */
+  lineDelta?: number;
+  /** Error details if failed */
+  error?: DiffError;
+}
+
+// -----------------------------------------------------------------------------
+// Fuzzy Matching Types
+// -----------------------------------------------------------------------------
+
+export interface MatchResult {
+  /** Whether a match was found above threshold */
+  success: boolean;
+  /** Similarity score (0.0 to 1.0) */
+  similarity: number;
+  /** Starting line index of the match */
+  index?: number;
+  /** Matched content */
+  content?: string;
+  /** Character range of the match */
+  matchedRange?: {
+    startIndex: number;
+    endIndex: number;
+    startLine: number;
+    endLine: number;
+  };
+  /** Time taken for search in milliseconds (client-side) */
+  searchTime?: number;
+  /** Error message if search failed */
+  error?: string;
+}
+
+export interface SearchContext {
+  /** Starting line number for search (1-based) */
+  startLine?: number;
+  /** Ending line number for search (1-based) */
+  endLine?: number;
+  /** Preferred starting line for search optimization (1-based) */
+  preferredStartLine?: number;
+  /** Number of buffer lines around search area (default: 40) */
+  bufferLines?: number;
+}
+
+// -----------------------------------------------------------------------------
+// Processing Types
+// -----------------------------------------------------------------------------
+
+export interface ProcessingOptions {
+  preserveSelection?: boolean;
+  enableProgressCallback?: boolean;
+  batchSize?: number;
+  timeout?: number;
+}
+
+export interface ProcessingResult {
+  success: boolean;
+  error?: DiffError;
+  matches: MatchResult[];
+  appliedCount: number;
+  skippedCount: number;
+  modifiedText: string;
+  processingTime: number;
+}

+ 4 - 3
apps/app/src/features/openai/client/services/ai-assistant.ts

@@ -1,13 +1,14 @@
 import { apiv3Post, apiv3Put, apiv3Delete } from '~/client/util/apiv3-client';
 import { apiv3Post, apiv3Put, apiv3Delete } from '~/client/util/apiv3-client';
 
 
-import type { UpsertAiAssistantData } from '../../interfaces/ai-assistant';
+import type { UpsertAiAssistantData, AiAssistantHasId } from '../../interfaces/ai-assistant';
 
 
 export const createAiAssistant = async(body: UpsertAiAssistantData): Promise<void> => {
 export const createAiAssistant = async(body: UpsertAiAssistantData): Promise<void> => {
   await apiv3Post('/openai/ai-assistant', body);
   await apiv3Post('/openai/ai-assistant', body);
 };
 };
 
 
-export const updateAiAssistant = async(id: string, body: UpsertAiAssistantData): Promise<void> => {
-  await apiv3Put(`/openai/ai-assistant/${id}`, body);
+export const updateAiAssistant = async(id: string, body: UpsertAiAssistantData): Promise<AiAssistantHasId> => {
+  const res = await apiv3Put<{updatedAiAssistant: AiAssistantHasId}>(`/openai/ai-assistant/${id}`, body);
+  return res.data.updatedAiAssistant;
 };
 };
 
 
 export const setDefaultAiAssistant = async(id: string, isDefault: boolean): Promise<void> => {
 export const setDefaultAiAssistant = async(id: string, isDefault: boolean): Promise<void> => {

+ 217 - 0
apps/app/src/features/openai/client/services/client-engine-integration.tsx

@@ -0,0 +1,217 @@
+/**
+ * Client Engine Integration for useEditorAssistant Hook
+ * Provides seamless integration between existing SSE processing and new client-side engine
+ */
+
+import {
+  useCallback, useRef, useMemo,
+} from 'react';
+
+import type { Text as YText } from 'yjs';
+
+import type { SseDetectedDiff } from '../../interfaces/editor-assistant/sse-schemas';
+import type { ProcessingResult } from '../interfaces/types';
+
+import { ClientSearchReplaceProcessor } from './editor-assistant/processor';
+
+// -----------------------------------------------------------------------------
+// Integration Configuration
+// -----------------------------------------------------------------------------
+
+export interface ClientEngineConfig {
+  /** Enable client-side processing */
+  enableClientProcessing: boolean;
+  /** Fallback to server processing on client errors */
+  enableServerFallback: boolean;
+  /** Log performance metrics for comparison */
+  enablePerformanceMetrics: boolean;
+  /** Maximum processing time before timeout (ms) */
+  maxProcessingTime: number;
+  /** Batch size for diff processing */
+  batchSize: number;
+}
+
+export interface ProcessingMetrics {
+  /** Processing method used */
+  method: 'client' | 'server' | 'hybrid';
+  /** Total processing time in milliseconds */
+  processingTime: number;
+  /** Number of diffs processed */
+  diffsCount: number;
+  /** Number of diffs successfully applied */
+  appliedCount: number;
+  /** Success rate as percentage */
+  successRate: number;
+  /** Error information if any */
+  error?: string;
+  /** Memory usage (if available) */
+  memoryUsage?: number;
+}
+
+export interface ProcessingProgress {
+  current: number;
+  total: number;
+  message: string;
+  percentage: number;
+}
+
+// -----------------------------------------------------------------------------
+// Client Engine Integration Hook
+// -----------------------------------------------------------------------------
+
+export function useClientEngineIntegration(config: Partial<ClientEngineConfig> = {}): {
+  processHybrid: (
+    content: string,
+    detectedDiffs: SseDetectedDiff[],
+    serverProcessingFn: () => Promise<void>,
+  ) => Promise<{ success: boolean; method: 'client' | 'server'; result?: ProcessingResult }>;
+  applyToYText: (yText: YText, processedContent: string) => boolean;
+  isClientProcessingEnabled: boolean;
+} {
+  // Configuration with defaults
+  const finalConfig: ClientEngineConfig = useMemo(() => ({
+    enableClientProcessing: true,
+    enableServerFallback: true,
+    enablePerformanceMetrics: true,
+    maxProcessingTime: 10000,
+    batchSize: 5,
+    ...config,
+  }), [config]);
+
+  // Client processor instance
+  const clientProcessor = useRef<ClientSearchReplaceProcessor>();
+
+  // Initialize client processor
+  if (!clientProcessor.current && finalConfig.enableClientProcessing) {
+    clientProcessor.current = new ClientSearchReplaceProcessor({
+      fuzzyThreshold: 0.8,
+      bufferLines: 30,
+      maxDiffBlocks: 8,
+    });
+  }
+
+  /**
+   * Apply processed content to YText (CodeMirror integration)
+   */
+  const applyToYText = useCallback((
+      yText: YText,
+      processedContent: string,
+  ): boolean => {
+    try {
+      const currentContent = yText.toString();
+
+      if (currentContent === processedContent) {
+        // No changes needed
+        return true;
+      }
+
+      // Apply changes in a transaction
+      yText.doc?.transact(() => {
+        // Clear existing content
+        yText.delete(0, yText.length);
+        // Insert new content
+        yText.insert(0, processedContent);
+      });
+
+      return true;
+    }
+    catch (error) {
+      return false;
+    }
+  }, []);
+
+  /**
+   * Hybrid processing: try client first, fallback to server
+   */
+  const processHybrid = useCallback(async(
+      content: string,
+      detectedDiffs: SseDetectedDiff[],
+      serverProcessingFn: () => Promise<void>,
+  ): Promise<{ success: boolean; method: 'client' | 'server'; result?: ProcessingResult }> => {
+    if (!finalConfig.enableClientProcessing || !clientProcessor.current) {
+      // Client processing disabled, use server only
+      await serverProcessingFn();
+      return { success: true, method: 'server' };
+    }
+
+    try {
+      // Convert SseDetectedDiff to LlmEditorAssistantDiff format
+      const diffs = detectedDiffs
+        .map(d => d.diff)
+        .filter((diff): diff is NonNullable<typeof diff> => diff != null);
+
+      // Validate required fields for client processing
+      for (const diff of diffs) {
+        if (!diff.startLine || !diff.search) {
+          throw new Error('Missing required fields for client processing');
+        }
+      }
+
+      // Process with client engine
+      const diffResult = await clientProcessor.current.processMultipleDiffs(content, diffs, {
+        enableProgressCallbacks: true,
+        batchSize: finalConfig.batchSize,
+        maxProcessingTime: finalConfig.maxProcessingTime,
+      });
+
+      // Convert DiffApplicationResult to ProcessingResult
+      const processingTime = performance.now();
+      const result: ProcessingResult = {
+        success: diffResult.success,
+        error: diffResult.failedParts?.[0],
+        matches: [],
+        appliedCount: diffResult.appliedCount,
+        skippedCount: Math.max(0, diffs.length - diffResult.appliedCount),
+        modifiedText: diffResult.content || content,
+        processingTime,
+      };
+
+      if (result.success) {
+        return { success: true, method: 'client', result };
+      }
+
+      // Client processing failed, fallback to server if enabled
+      if (finalConfig.enableServerFallback) {
+        await serverProcessingFn();
+        return { success: true, method: 'server' };
+      }
+
+      // No fallback, return client error
+      return { success: false, method: 'client', result };
+    }
+    catch (error) {
+      // Fallback to server on error
+      if (finalConfig.enableServerFallback) {
+        await serverProcessingFn();
+        return { success: true, method: 'server' };
+      }
+
+      return { success: false, method: 'client' };
+    }
+  }, [finalConfig]);
+
+  return {
+    // Processing functions
+    applyToYText,
+    processHybrid,
+
+    // Configuration
+    isClientProcessingEnabled: finalConfig.enableClientProcessing,
+  };
+}
+
+// -----------------------------------------------------------------------------
+// Utility Functions
+// -----------------------------------------------------------------------------
+
+/**
+ * Feature flag for enabling client processing
+ */
+export function shouldUseClientProcessing(): boolean {
+  // This could be controlled by environment variables, user settings, etc.
+  return (process.env.NODE_ENV === 'development')
+    || (typeof window !== 'undefined'
+        && (window as { __GROWI_CLIENT_PROCESSING_ENABLED__?: boolean }).__GROWI_CLIENT_PROCESSING_ENABLED__ === true);
+}
+
+export default useClientEngineIntegration;

+ 297 - 0
apps/app/src/features/openai/client/services/editor-assistant/diff-application.ts

@@ -0,0 +1,297 @@
+/**
+ * Client-side Diff Application Engine for GROWI Editor Assistant
+ * Handles direct integration with browser-based editors (yText/CodeMirror)
+ * Optimized for real-time application with undo/redo support
+ */
+
+import type { LlmEditorAssistantDiff } from '../../../interfaces/editor-assistant/llm-response-schemas';
+import type { SingleDiffResult, ProcessorConfig, SearchContext } from '../../interfaces/types';
+
+import { ClientErrorHandler } from './error-handling';
+import { ClientFuzzyMatcher } from './fuzzy-matching';
+
+// -----------------------------------------------------------------------------
+// Client Diff Application Engine
+// -----------------------------------------------------------------------------
+
+export class ClientDiffApplicationEngine {
+
+  private fuzzyMatcher: ClientFuzzyMatcher;
+
+  private errorHandler: ClientErrorHandler;
+
+  private config: Required<ProcessorConfig>;
+
+  constructor(
+      config: Partial<ProcessorConfig> = {},
+      errorHandler?: ClientErrorHandler,
+  ) {
+    // Set defaults optimized for browser environment
+    this.config = {
+      fuzzyThreshold: config.fuzzyThreshold ?? 0.8,
+      bufferLines: config.bufferLines ?? 40,
+      preserveIndentation: config.preserveIndentation ?? true,
+      stripLineNumbers: config.stripLineNumbers ?? true,
+      enableAggressiveMatching: config.enableAggressiveMatching ?? false,
+      maxDiffBlocks: config.maxDiffBlocks ?? 10,
+    };
+
+    this.fuzzyMatcher = new ClientFuzzyMatcher(this.config.fuzzyThreshold);
+    this.errorHandler = errorHandler ?? new ClientErrorHandler();
+  }
+
+  /**
+   * Apply a single diff to content with browser-optimized processing
+   */
+  applySingleDiff(
+      content: string,
+      diff: LlmEditorAssistantDiff,
+      lineDelta = 0,
+  ): SingleDiffResult {
+    try {
+      // Validate search content
+      if (!diff.search.trim()) {
+        return {
+          success: false,
+          error: this.errorHandler.createEmptySearchError(),
+        };
+      }
+
+      const lines = content.split(/\r?\n/);
+
+      // Calculate adjusted line numbers considering previous changes
+      const searchContext = this.createSearchContext(diff, lineDelta);
+
+      // Find the best match using fuzzy matching
+      const matchResult = this.fuzzyMatcher.findBestMatch(
+        content,
+        diff.search,
+        searchContext,
+      );
+
+      if (!matchResult.success) {
+        return {
+          success: false,
+          error: this.errorHandler.createSearchNotFoundError(
+            diff.search,
+            matchResult,
+            searchContext.startLine,
+          ),
+        };
+      }
+
+      // Apply the replacement with indentation preservation
+      const replacementResult = this.applyReplacement(
+        lines,
+        { index: matchResult.index || 0, content: matchResult.content || '' },
+        diff.replace,
+      );
+
+      return {
+        success: true,
+        updatedLines: replacementResult.lines,
+        lineDelta: replacementResult.lineDelta,
+      };
+
+    }
+    catch (error) {
+      return {
+        success: false,
+        error: this.errorHandler.createContentError(
+          error as Error,
+          `Applying diff with search: "${diff.search.substring(0, 50)}..."`,
+        ),
+      };
+    }
+  }
+
+
+  /**
+   * Apply multiple diffs in sequence with proper delta tracking
+   */
+  applyMultipleDiffs(
+      content: string,
+      diffs: LlmEditorAssistantDiff[],
+  ): {
+    success: boolean;
+    finalContent?: string;
+    appliedCount: number;
+    results: SingleDiffResult[];
+    errors: SingleDiffResult[];
+  } {
+    const results: SingleDiffResult[] = [];
+    const errors: SingleDiffResult[] = [];
+    let currentContent = content;
+    let totalLineDelta = 0;
+    let appliedCount = 0;
+
+    // Sort diffs by line number (if available) to apply from bottom to top
+    const sortedDiffs = this.sortDiffsForApplication(diffs);
+
+    for (const diff of sortedDiffs) {
+      const result = this.applySingleDiff(currentContent, diff, totalLineDelta);
+      results.push(result);
+
+      if (result.success && result.updatedLines) {
+        currentContent = result.updatedLines.join('\n');
+        totalLineDelta += result.lineDelta || 0;
+        appliedCount++;
+      }
+      else {
+        errors.push(result);
+      }
+    }
+
+    return {
+      success: errors.length === 0,
+      finalContent: appliedCount > 0 ? currentContent : undefined,
+      appliedCount,
+      results,
+      errors,
+    };
+  }
+
+  // -----------------------------------------------------------------------------
+  // Private Helper Methods
+  // -----------------------------------------------------------------------------
+
+  /**
+   * Create search context with line adjustments
+   */
+  private createSearchContext(
+      diff: LlmEditorAssistantDiff,
+      lineDelta: number,
+  ): SearchContext {
+    return {
+      startLine: diff.startLine ? diff.startLine + lineDelta : undefined,
+      endLine: diff.endLine ? diff.endLine + lineDelta : undefined,
+      bufferLines: this.config.bufferLines,
+    };
+  }
+
+  /**
+   * Apply replacement with indentation preservation
+   */
+  private applyReplacement(
+      lines: string[],
+      matchResult: { index: number; content: string },
+      replaceText: string,
+  ): { lines: string[]; lineDelta: number } {
+    const startLineIndex = matchResult.index;
+    const originalLines = matchResult.content.split('\n');
+    const endLineIndex = startLineIndex + originalLines.length - 1;
+
+    // Preserve indentation if enabled
+    const processedReplaceText = this.config.preserveIndentation
+      ? this.preserveIndentation(originalLines[0], replaceText)
+      : replaceText;
+
+    const replaceLines = processedReplaceText.split('\n');
+
+    // Create new lines array with replacement
+    const newLines = [
+      ...lines.slice(0, startLineIndex),
+      ...replaceLines,
+      ...lines.slice(endLineIndex + 1),
+    ];
+
+    const lineDelta = replaceLines.length - originalLines.length;
+
+    return {
+      lines: newLines,
+      lineDelta,
+    };
+  }
+
+  /**
+   * Preserve indentation pattern from original content
+   */
+  private preserveIndentation(originalLine: string, replaceText: string): string {
+    // Extract indentation from the original line
+    const indentMatch = originalLine.match(/^(\s*)/);
+    const originalIndent = indentMatch ? indentMatch[1] : '';
+
+    if (!originalIndent) {
+      return replaceText;
+    }
+
+    // Apply the same indentation to all lines in replacement
+    return replaceText
+      .split('\n')
+      .map((line, index) => {
+        // Don't add indent to empty lines
+        if (line.trim() === '') {
+          return line;
+        }
+        // First line might already have partial indentation
+        if (index === 0) {
+          return originalIndent + line.replace(/^\s*/, '');
+        }
+        return originalIndent + line;
+      })
+      .join('\n');
+  }
+
+  /**
+   * Sort diffs for optimal application order (bottom to top)
+   */
+  private sortDiffsForApplication(
+      diffs: LlmEditorAssistantDiff[],
+  ): LlmEditorAssistantDiff[] {
+    return [...diffs].sort((a, b) => {
+      // If both have line numbers, sort by line number (descending)
+      if (a.startLine && b.startLine) {
+        return b.startLine - a.startLine;
+      }
+      // If only one has line number, prioritize it
+      if (a.startLine) return -1;
+      if (b.startLine) return 1;
+      // If neither has line number, keep original order
+      return 0;
+    });
+  }
+
+  // -----------------------------------------------------------------------------
+  // Configuration and Utility Methods
+  // -----------------------------------------------------------------------------
+
+  /**
+   * Update configuration
+   */
+  updateConfig(newConfig: Partial<ProcessorConfig>): void {
+    this.config = { ...this.config, ...newConfig };
+    this.fuzzyMatcher.setThreshold(this.config.fuzzyThreshold);
+  }
+
+  /**
+   * Validate diff before application
+   */
+  validateDiff(diff: LlmEditorAssistantDiff): {
+    valid: boolean;
+    issues: string[];
+  } {
+    const issues: string[] = [];
+
+    if (!diff.search || !diff.search.trim()) {
+      issues.push('Search content is empty');
+    }
+
+    if (diff.replace === undefined) {
+      issues.push('Replace content is undefined');
+    }
+
+    if (diff.startLine && diff.endLine && diff.startLine > diff.endLine) {
+      issues.push('Start line is greater than end line');
+    }
+
+    if (diff.search && diff.search.length > 10000) {
+      issues.push('Search content is very large (>10k chars)');
+    }
+
+    return {
+      valid: issues.length === 0,
+      issues,
+    };
+  }
+
+}

+ 233 - 0
apps/app/src/features/openai/client/services/editor-assistant/error-handling.ts

@@ -0,0 +1,233 @@
+/**
+ * Client-side Error Handling for GROWI Editor Assistant
+ * Optimized for browser environment with real-time feedback
+ * Provides detailed error information and user-friendly suggestions
+ */
+
+import type { DiffError, MatchResult } from '../../interfaces/types';
+
+// -----------------------------------------------------------------------------
+// Client Error Types and Constants
+// -----------------------------------------------------------------------------
+
+export const CLIENT_ERROR_MESSAGES = {
+  SEARCH_NOT_FOUND: 'Search content not found in the document',
+  EMPTY_SEARCH: 'Search content cannot be empty',
+  CONTENT_ERROR: 'Invalid or corrupted content',
+  TIMEOUT_ERROR: 'Search operation timed out',
+} as const;
+
+export const CLIENT_SUGGESTIONS = {
+  SEARCH_NOT_FOUND: [
+    'Check for exact whitespace and formatting',
+    'Try a smaller, more specific search pattern',
+    'Verify line endings match your content',
+    'Use the browser\'s search function to locate content first',
+  ],
+  EMPTY_SEARCH: [
+    'Provide valid search content',
+    'Check that your diff contains the search text',
+  ],
+  CONTENT_ERROR: [
+    'Refresh the page and try again',
+    'Check browser console for detailed errors',
+    'Verify the document is properly loaded',
+  ],
+  TIMEOUT_ERROR: [
+    'Try searching in a smaller section',
+    'Reduce the document size if possible',
+    'Check browser performance and memory usage',
+  ],
+} as const;
+
+// -----------------------------------------------------------------------------
+// Client Error Handler Class
+// -----------------------------------------------------------------------------
+
+export class ClientErrorHandler {
+
+  private readonly enableConsoleLogging: boolean;
+
+  private readonly enableUserFeedback: boolean;
+
+  constructor(enableConsoleLogging = true, enableUserFeedback = true) {
+    this.enableConsoleLogging = enableConsoleLogging;
+    this.enableUserFeedback = enableUserFeedback;
+  }
+
+  /**
+   * Create a detailed error for search content not found
+   */
+  createSearchNotFoundError(
+      searchContent: string,
+      matchResult?: MatchResult,
+      startLine?: number,
+  ): DiffError {
+    const lineRange = startLine ? ` (starting at line ${startLine})` : '';
+    const similarityInfo = matchResult?.similarity
+      ? ` (closest match: ${Math.floor(matchResult.similarity * 100)}%)`
+      : '';
+
+    const error: DiffError = {
+      type: 'SEARCH_NOT_FOUND',
+      message: `${CLIENT_ERROR_MESSAGES.SEARCH_NOT_FOUND}${lineRange}${similarityInfo}`,
+      line: startLine,
+      details: {
+        searchContent,
+        bestMatch: matchResult?.content,
+        similarity: matchResult?.similarity,
+        suggestions: [...CLIENT_SUGGESTIONS.SEARCH_NOT_FOUND],
+        lineRange: startLine ? `starting at line ${startLine}` : 'entire document',
+      },
+    };
+
+    this.logError(error, 'Search content not found');
+    return error;
+  }
+
+  /**
+   * Create an error for empty search content
+   */
+  createEmptySearchError(): DiffError {
+    const error: DiffError = {
+      type: 'EMPTY_SEARCH',
+      message: CLIENT_ERROR_MESSAGES.EMPTY_SEARCH,
+      details: {
+        searchContent: '',
+        suggestions: [...CLIENT_SUGGESTIONS.EMPTY_SEARCH],
+      },
+    };
+
+    this.logError(error, 'Empty search content');
+    return error;
+  }
+
+  /**
+   * Create an error for content/parsing issues
+   */
+  createContentError(
+      originalError: Error,
+      context?: string,
+  ): DiffError {
+    const error: DiffError = {
+      type: 'CONTENT_ERROR',
+      message: `${CLIENT_ERROR_MESSAGES.CONTENT_ERROR}: ${originalError.message}`,
+      details: {
+        searchContent: context || 'Unknown context',
+        suggestions: [
+          `Original error: ${originalError.message}`,
+          ...CLIENT_SUGGESTIONS.CONTENT_ERROR,
+        ],
+      },
+    };
+
+    this.logError(error, 'Content processing error', originalError);
+    return error;
+  }
+
+  /**
+   * Create an error for browser timeout
+   */
+  createTimeoutError(
+      searchContent: string,
+      timeoutMs: number,
+  ): DiffError {
+    const error: DiffError = {
+      type: 'CONTENT_ERROR', // Using CONTENT_ERROR as base type
+      message: `${CLIENT_ERROR_MESSAGES.TIMEOUT_ERROR} (${timeoutMs}ms)`,
+      details: {
+        searchContent,
+        suggestions: [
+          `Search timed out after ${timeoutMs}ms`,
+          ...CLIENT_SUGGESTIONS.TIMEOUT_ERROR,
+        ],
+      },
+    };
+
+    this.logError(error, 'Search timeout');
+    return error;
+  }
+
+  // -----------------------------------------------------------------------------
+  // Utility Methods
+  // -----------------------------------------------------------------------------
+
+  /**
+   * Generate a suggested correct format based on the best match
+   */
+  private generateCorrectFormat(searchContent: string, bestMatch: string): string {
+    // Simple diff-like format for user guidance
+    const searchLines = searchContent.split('\n');
+    const matchLines = bestMatch.split('\n');
+
+    if (searchLines.length === 1 && matchLines.length === 1) {
+      return `Try: "${bestMatch}" instead of "${searchContent}"`;
+    }
+
+    return `Expected format based on closest match:\n${bestMatch}`;
+  }
+
+  /**
+   * Log error to console (if enabled) with contextual information
+   */
+  private logError(
+      error: DiffError,
+      context: string,
+      originalError?: Error,
+  ): void {
+    if (!this.enableConsoleLogging) {
+      return;
+    }
+
+    const logData = {
+      context,
+      type: error.type,
+      message: error.message,
+      line: error.line,
+      similarity: error.details.similarity,
+      searchLength: error.details.searchContent?.length || 0,
+      suggestions: error.details.suggestions?.length || 0,
+    };
+
+    // eslint-disable-next-line no-console
+    console.warn('[ClientErrorHandler]', logData);
+
+    if (originalError) {
+      // eslint-disable-next-line no-console
+      console.error('[ClientErrorHandler] Original error:', originalError);
+    }
+  }
+
+  /**
+   * Format error for user display
+   */
+  formatErrorForUser(error: DiffError): string {
+    const suggestions = error.details.suggestions?.slice(0, 3).join('\n• ') || '';
+
+    return `❌ ${error.message}\n\n💡 Suggestions:\n• ${suggestions}`;
+  }
+
+  /**
+   * Create a user-friendly summary of multiple errors
+   */
+  createErrorSummary(errors: DiffError[]): string {
+    if (errors.length === 0) {
+      return '✅ No errors found';
+    }
+
+    if (errors.length === 1) {
+      return this.formatErrorForUser(errors[0]);
+    }
+
+    const summary = `❌ ${errors.length} issues found:\n\n`;
+    const errorList = errors
+      .slice(0, 5) // Limit to first 5 errors
+      .map((error, index) => `${index + 1}. ${error.message}`)
+      .join('\n');
+
+    const moreErrors = errors.length > 5 ? `\n... and ${errors.length - 5} more issues` : '';
+
+    return summary + errorList + moreErrors;
+  }
+
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff