Преглед изворни кода

Merge branch 'master' into support/support-es9

Shun Miyazawa пре 8 месеци
родитељ
комит
7bd3c45f52
100 измењених фајлова са 3454 додато и 3525 уклоњено
  1. 0 1
      apps/app/.env.development
  2. 0 2
      apps/app/bin/openapi/definition-apiv3.js
  3. 0 1
      apps/app/bin/openapi/generate-spec-apiv3.sh
  4. 0 1
      apps/app/config/logger/config.dev.js
  5. 1 1
      apps/app/package.json
  6. 0 2
      apps/app/playwright/40-admin/access-to-admin-page.spec.ts
  7. 2 14
      apps/app/playwright/60-home/home.spec.ts
  8. 32 42
      apps/app/public/static/locales/en_US/admin.json
  9. 0 36
      apps/app/public/static/locales/en_US/commons.json
  10. 0 18
      apps/app/public/static/locales/en_US/translation.json
  11. 1 11
      apps/app/public/static/locales/fr_FR/admin.json
  12. 0 23
      apps/app/public/static/locales/fr_FR/commons.json
  13. 1 19
      apps/app/public/static/locales/fr_FR/translation.json
  14. 1 11
      apps/app/public/static/locales/ja_JP/admin.json
  15. 0 35
      apps/app/public/static/locales/ja_JP/commons.json
  16. 1 18
      apps/app/public/static/locales/ja_JP/translation.json
  17. 1142 0
      apps/app/public/static/locales/ko_KR/admin.json
  18. 127 0
      apps/app/public/static/locales/ko_KR/commons.json
  19. 1021 0
      apps/app/public/static/locales/ko_KR/translation.json
  20. 227 237
      apps/app/public/static/locales/zh_CN/admin.json
  21. 24 60
      apps/app/public/static/locales/zh_CN/commons.json
  22. 0 17
      apps/app/public/static/locales/zh_CN/translation.json
  23. 14 0
      apps/app/resource/locales/ko_KR/admin/userInvitation.ejs
  24. 11 0
      apps/app/resource/locales/ko_KR/admin/userResetPassword.ejs
  25. 20 0
      apps/app/resource/locales/ko_KR/admin/userWaitingActivation.ejs
  26. 9 0
      apps/app/resource/locales/ko_KR/notifications/comment.ejs
  27. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageCreate.ejs
  28. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageDelete.ejs
  29. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageEdit.ejs
  30. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageLike.ejs
  31. 5 0
      apps/app/resource/locales/ko_KR/notifications/pageMove.ejs
  32. 12 0
      apps/app/resource/locales/ko_KR/notifications/passwordReset.ejs
  33. 8 0
      apps/app/resource/locales/ko_KR/notifications/passwordResetSuccessful.ejs
  34. 12 0
      apps/app/resource/locales/ko_KR/notifications/userActivation.ejs
  35. 169 0
      apps/app/resource/locales/ko_KR/sandbox-bootstrap5.md
  36. 7 0
      apps/app/resource/locales/ko_KR/sandbox-diagrams.md
  37. 241 0
      apps/app/resource/locales/ko_KR/sandbox-markdown.md
  38. 72 0
      apps/app/resource/locales/ko_KR/sandbox-math.md
  39. 174 0
      apps/app/resource/locales/ko_KR/sandbox.md
  40. 51 0
      apps/app/resource/locales/ko_KR/welcome.md
  41. 0 8
      apps/app/src/client/components/Admin/App/AppSettingsPageContents.tsx
  42. 1 1
      apps/app/src/client/components/Admin/App/PageBulkExportSettings.tsx
  43. 0 130
      apps/app/src/client/components/Admin/App/QuestionnaireSettings.tsx
  44. 0 5
      apps/app/src/client/components/Me/OtherSettings.tsx
  45. 0 109
      apps/app/src/client/components/Me/QuestionnaireSettings.tsx
  46. 1 1
      apps/app/src/client/components/Me/UISettings.tsx
  47. 2 2
      apps/app/src/client/components/ShortcutsModal.tsx
  48. 1 19
      apps/app/src/client/components/Sidebar/SidebarNav/PersonalDropdown.tsx
  49. 27 8
      apps/app/src/features/mermaid/components/MermaidViewer.tsx
  50. 6 6
      apps/app/src/features/openai/server/routes/edit/README.ja.md
  51. 1 1
      apps/app/src/features/opentelemetry/server/custom-metrics/application-metrics.ts
  52. 2 2
      apps/app/src/features/opentelemetry/server/custom-metrics/user-counts-metrics.spec.ts
  53. 1 1
      apps/app/src/features/opentelemetry/server/custom-metrics/user-counts-metrics.ts
  54. 1 1
      apps/app/src/features/opentelemetry/server/custom-resource-attributes/application-resource-attributes.spec.ts
  55. 1 1
      apps/app/src/features/opentelemetry/server/custom-resource-attributes/application-resource-attributes.ts
  56. 1 0
      apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts
  57. 2 2
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/index.ts
  58. 1 0
      apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/steps/export-pages-to-fs-async.ts
  59. 0 152
      apps/app/src/features/questionnaire/client/components/ProactiveQuestionnaireModal.tsx
  60. 0 52
      apps/app/src/features/questionnaire/client/components/Question.tsx
  61. 0 167
      apps/app/src/features/questionnaire/client/components/QuestionnaireModal.tsx
  62. 0 9
      apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.module.scss
  63. 0 51
      apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.tsx
  64. 0 85
      apps/app/src/features/questionnaire/client/components/QuestionnaireToast.tsx
  65. 0 79
      apps/app/src/features/questionnaire/client/services/guest-questionnaire-answer-status.ts
  66. 0 40
      apps/app/src/features/questionnaire/client/stores/model.tsx
  67. 0 24
      apps/app/src/features/questionnaire/client/stores/questionnaire.tsx
  68. 0 4
      apps/app/src/features/questionnaire/interfaces/answer.ts
  69. 0 25
      apps/app/src/features/questionnaire/interfaces/condition.ts
  70. 0 18
      apps/app/src/features/questionnaire/interfaces/growi-app-info.ts
  71. 0 27
      apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts
  72. 0 15
      apps/app/src/features/questionnaire/interfaces/question.ts
  73. 0 16
      apps/app/src/features/questionnaire/interfaces/questionnaire-answer-status.ts
  74. 0 21
      apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts
  75. 0 21
      apps/app/src/features/questionnaire/interfaces/questionnaire-order.ts
  76. 0 12
      apps/app/src/features/questionnaire/interfaces/user-info.ts
  77. 0 28
      apps/app/src/features/questionnaire/server/models/proactive-questionnaire-answer.ts
  78. 0 19
      apps/app/src/features/questionnaire/server/models/questionnaire-answer-status.ts
  79. 0 25
      apps/app/src/features/questionnaire/server/models/questionnaire-answer.ts
  80. 0 35
      apps/app/src/features/questionnaire/server/models/questionnaire-order.ts
  81. 0 10
      apps/app/src/features/questionnaire/server/models/schema/answer.ts
  82. 0 29
      apps/app/src/features/questionnaire/server/models/schema/condition.ts
  83. 0 42
      apps/app/src/features/questionnaire/server/models/schema/growi-info.ts
  84. 0 16
      apps/app/src/features/questionnaire/server/models/schema/question.ts
  85. 0 10
      apps/app/src/features/questionnaire/server/models/schema/user-info.ts
  86. 0 384
      apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts
  87. 0 515
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.integ.ts
  88. 0 106
      apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts
  89. 0 301
      apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts
  90. 0 74
      apps/app/src/features/questionnaire/server/service/questionnaire.ts
  91. 0 70
      apps/app/src/features/questionnaire/server/util/condition.ts
  92. 0 128
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts
  93. 0 40
      apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts
  94. 0 3
      apps/app/src/interfaces/activity.ts
  95. 0 1
      apps/app/src/interfaces/res/admin/app-settings.ts
  96. 0 2
      apps/app/src/pages/[[...path]].page.tsx
  97. 1 0
      apps/app/src/pages/utils/commons.ts
  98. 0 13
      apps/app/src/server/crowi/index.js
  99. 0 4
      apps/app/src/server/crowi/setup-models.ts
  100. 0 6
      apps/app/src/server/models/user.js

+ 0 - 1
apps/app/.env.development

@@ -14,7 +14,6 @@ ELASTICSEARCH_URI="http://elasticsearch:9200/growi"
 ELASTICSEARCH_REQUEST_TIMEOUT=15000
 ELASTICSEARCH_REJECT_UNAUTHORIZED=true
 OGP_URI="http://ogp:8088"
-QUESTIONNAIRE_SERVER_ORIGIN="http://host.docker.internal:3003"
 # DRAWIO_URI="http://localhost:8080/?offline=1&https=0"
 # S2SMSG_PUBSUB_SERVER_TYPE=nchan
 # PUBLISH_OPEN_API=true

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

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

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

@@ -11,7 +11,6 @@ swagger-jsdoc \
   -o "${OUT}" \
   -d "${APP_PATH}/bin/openapi/definition-apiv3.js" \
   "${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/growi-plugin/server/routes/apiv3/**/*.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:service:search-delegator:elasticsearch': 'debug',
   'growi:service:g2g-transfer': 'debug',
-  'growi:service:questionnaire': 'debug',
 
   'growi:migration:add-installed-date-to-config': 'debug',
 

+ 1 - 1
apps/app/package.json

@@ -135,7 +135,7 @@
     "express-session": "^1.16.1",
     "express-validator": "^6.14.0",
     "extensible-custom-error": "^0.0.7",
-    "form-data": "^4.0.0",
+    "form-data": "^4.0.4",
     "graceful-fs": "^4.1.11",
     "hast-util-sanitize": "^5.0.1",
     "hast-util-select": "^6.0.2",

+ 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('v5-page-migration')).toBeVisible();
   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 }) => {

+ 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();
 });
 
-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 }) => {
   await page.goto('/me');
 
@@ -116,7 +104,7 @@ test('Acccess Other setting', async({ page }) => {
   await expect(page.getByTestId('grw-user-settings')).toBeVisible();
   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();
 });

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

@@ -97,9 +97,9 @@
       "closed": "Closed (Invitation Only)"
     },
     "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",
@@ -229,7 +229,7 @@
     "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.",
+    "use_instead": "Please use Slack Incoming Webhooks Configuration instead.",
     "how_to": {
       "header": "How to configure Incoming Webhooks?",
       "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_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": {
     "management_wiki": "Management Wiki",
     "system_information": "System information",
@@ -305,7 +305,7 @@
     "package_name": "Package name",
     "specified_version": "Specified 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.",
     "about_security": "Check <a href='/admin/security'>Security Settings</a> for security environment variables.",
     "copy_prefilled_host_information": {
@@ -368,9 +368,9 @@
     "mail_settings": "E-mail Settings",
     "mailer_is_not_set_up": "E-mail setting is not set up.",
     "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",
     "success_to_send_test_email": "Success to send a test-email",
     "smtp_settings": "SMTP settings",
@@ -380,13 +380,13 @@
     "initialize_mail_settings": "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?",
-    "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>.",
     "gcs_label": "GCP(GCS)",
     "aws_label": "AWS(S3)",
@@ -411,17 +411,7 @@
     "enable": "Enable",
     "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.",
-    "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",
@@ -502,13 +492,13 @@
       "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."
     },
-      "presentation": "Presentation",
+    "presentation": "Presentation",
     "presentation_options": {
       "enable_marp": "Enable Marp ",
       "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_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"
     },
     "custom_title": "Custom title",
@@ -525,7 +515,7 @@
     "custom_presentation": "Custom presentation",
     "write_java": "You can write Javascript that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change.",
-    "custom_logo" : "Custom Logo",
+    "custom_logo": "Custom Logo",
     "default_logo": "Default Logo",
     "upload_logo": "Upload Logo",
     "current_logo": "Current Logo",
@@ -599,9 +589,9 @@
     },
     "import": "Import",
     "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",
     "Directory_hierarchy_tag": "Directory hierarchy tag"
   },
@@ -671,7 +661,7 @@
     "delete": "Delete",
     "integration_procedure": "Integration Procedure",
     "custom_bot_without_proxy_settings": "Custom Bot without proxy Settings",
-    "integration_failed":"Integration failed",
+    "integration_failed": "Integration failed",
     "reset": "Reset",
     "reset_all_settings": "Reset all settings",
     "delete_slackbot_settings": "Delete Slack Bot settings",
@@ -718,7 +708,7 @@
       "allow_specified_long": "Allow specified (Allowed from only specified channels)",
       "test_connection": "Test 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.",
       "send_message_to_slack_work_space": "Send message to Slack work space.",
       "add_slack_workspace": "Add a Slack Workspace"
@@ -753,10 +743,10 @@
     "status": "Status",
     "invite_modal": {
       "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",
-      "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",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
@@ -791,10 +781,10 @@
       "new_password": "New Password"
     },
     "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_none":"No External Account",
+    "external_account_none": "No External Account",
     "invite": "Invite",
     "invited": "User was invited",
     "back_to_user_management": "Back to User Management",
@@ -979,7 +969,7 @@
     "ADMIN_SITE_URL_UPDATE": "Update Site URL Settings",
     "ADMIN_MAIL_SMTP_UPDATE": "Update E-mail(SMTP) 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_PLUGIN_UPDATE": "Update Plugin Settings",
     "ADMIN_MAINTENANCEMODE_ENABLED": "Enable Maintenance Mode",

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

@@ -7,7 +7,6 @@
   "Sign out": "Logout",
   "New": "New",
   "Delete": "Delete",
-
   "meta": {
     "display_name": "English"
   },
@@ -30,7 +29,6 @@
   "headers": {
     "app_settings": "App Settings"
   },
-
   "header_search_box": {
     "label": {
       "All pages": "All pages",
@@ -41,20 +39,17 @@
       "This tree": "Only children of this tree"
     }
   },
-
   "search_method_menu_item": {
     "search_in_all": "Search in all",
     "only_children_of_this_tree": "Only children of this tree",
     "exact_mutch": "Exact match"
   },
-
   "share_links": {
     "Share Link": "Share Link",
     "Page Path": "Page Path",
     "expire": "Expiration",
     "description": "Description"
   },
-
   "in_app_notification": {
     "notification_list": "In-App Notification List",
     "see_all": "See All",
@@ -65,7 +60,6 @@
     "no_unread_messages": "no_unread_messages",
     "only_unread": "Only unread"
   },
-
   "personal_dropdown": {
     "home": "Home",
     "settings": "Settings",
@@ -75,7 +69,6 @@
     "use_os_settings": "Use OS settings",
     "feedback": "Feedback"
   },
-
   "create_page_dropdown": {
     "new_page": "Create New Page",
     "open_page_create_modal": "Open new page create modal",
@@ -89,7 +82,6 @@
       "descendants": "Template for descendants"
     }
   },
-
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
@@ -99,14 +91,12 @@
     "Markdown link": "Markdown link",
     "Append params": "Append params"
   },
-
   "crop_image_modal": {
     "image_crop": "Image Crop",
     "crop": "Crop",
     "save": "Save",
     "cancel": "Cancel"
   },
-
   "handsontable_modal": {
     "title": "Edit Table",
     "data_import": "Data Import",
@@ -122,35 +112,9 @@
       "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": {
     "page_not_exist": "This page does not exist."
   },
-
   "g2g_data_transfer": {
     "tab": "Data transfer",
     "data_transfer": "Data Transfer",

+ 0 - 18
apps/app/public/static/locales/en_US/translation.json

@@ -142,7 +142,6 @@
   "edited this page": "edited this page.",
   "List Drafts": "Drafts",
   "Deleted Pages": "Deleted Pages",
-  "Questionnaire": "Questionnaire",
   "Disassociate": "Disassociate",
   "No bookmarks yet": "No bookmarks yet",
   "add_bookmark": "Add to Bookmarks",
@@ -979,23 +978,6 @@
     "page_tree_not_avaliable": "Page tree feature is not available yet.",
     "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": {
     "edit_tags": "Edit Tags",
     "done": "Done",

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

@@ -411,17 +411,7 @@
     "enable": "Activer",
     "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é.",
-    "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",

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

@@ -111,29 +111,6 @@
       "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": {
     "page_not_exist": "Cette page est introuvable."
   },

+ 1 - 19
apps/app/public/static/locales/fr_FR/translation.json

@@ -143,7 +143,6 @@
   "edited this page": "à modifié cette page.",
   "List Drafts": "Brouillons",
   "Deleted Pages": "Pages supprimées",
-  "Questionnaire": "Questionnaire",
   "Disassociate": "Dissocier",
   "No bookmarks yet": "Aucuns favoris",
   "add_bookmark": "Ajouter aux favoris",
@@ -617,7 +616,7 @@
   "default_ai_assistant": {
     "not_set": "L'assistant par défaut n'est pas configuré"
   },
- "ai_assistant_substance": {
+  "ai_assistant_substance": {
     "add_assistant": "Ajouter un assistant",
     "my_assistants": "Mes assistants",
     "team_assistants": "Assistants d'équipe",
@@ -973,23 +972,6 @@
     "page_tree_not_avaliable": "Cette fonctionnalité n'est pas encore disponible.",
     "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": {
     "edit_tags": "Étiquettes",
     "done": "Mettre à jour",

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

@@ -420,17 +420,7 @@
     "enable": "有効",
     "disable": "無効",
     "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": "マークダウン設定",

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

@@ -32,7 +32,6 @@
   "headers": {
     "app_settings": "アプリ設定"
   },
-
   "header_search_box": {
     "label": {
       "All pages": "全てのページ",
@@ -43,20 +42,17 @@
       "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": "通知一覧を見る",
@@ -67,7 +63,6 @@
     "no_unread_messages": "未読はありません",
     "only_unread": "未読のみ"
   },
-
   "personal_dropdown": {
     "home": "ホーム",
     "settings": "設定",
@@ -77,7 +72,6 @@
     "use_os_settings": "OS設定を利用する",
     "feedback": "ご意見・ご要望"
   },
-
   "create_page_dropdown": {
     "new_page": "新規ページ作成",
     "open_page_create_modal": "新規ページ作成モーダルを表示",
@@ -91,7 +85,6 @@
       "descendants": "下位層テンプレート"
     }
   },
-
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
@@ -101,14 +94,12 @@
     "Markdown link": "マークダウン形式のリンク",
     "Append params": "パラメータの追加"
   },
-
   "crop_image_modal": {
     "image_crop": "画像の切り抜き",
     "crop": "トリミング",
     "save": "保存",
     "cancel": "キャンセル"
   },
-
   "handsontable_modal": {
     "title": "テーブル編集",
     "data_import": "データインポート",
@@ -124,35 +115,9 @@
       "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": {
     "page_not_exist": "このページは存在しません。"
   },
-
   "g2g_data_transfer": {
     "tab": "データ移行",
     "data_transfer": "データ移行",

+ 1 - 18
apps/app/public/static/locales/ja_JP/translation.json

@@ -1011,23 +1011,6 @@
     "page_tree_not_avaliable": "Page Tree 機能は現在使用できません。",
     "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": {
     "edit_tags": "タグの編集",
     "done": "完了",
@@ -1061,7 +1044,7 @@
     "untitled": "無題のページ"
   },
   "sync-latest-revision-body": {
-    "menuitem": "最新のリビジョンの本文とエディタのテキストを同期",
+    "menuitem": "最新のリビジョンの本文とエディタのテキストを同期",
     "confirm": "エディターに入力中のドラフトデータを削除して最新の本文を同期します。実行しますか?",
     "alert": "最新の本文が同期されていない可能性があります。リロードして再度ご確認ください。",
     "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": "总是隐藏",
     "Guest 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_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_delete_rights": "删除权限",
     "page_delete": "删除",
@@ -56,9 +56,9 @@
     "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": "任何人",
+    "admin_only": "仅管理员",
+    "admin_and_author": "管理员|作者",
+    "anyone": "任何人",
     "user_homepage_deletion": {
       "user_homepage_deletion": "删除用户主页",
       "enable_user_homepage_deletion": "启用用户主页删除功能",
@@ -71,15 +71,15 @@
     "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": "转到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",
     "token_endpoint": "Token Endpoint",
     "revocation_endpoint": "Revocation Endpoint",
@@ -88,41 +88,41 @@
     "end_session_endpoint": "EndSessioin Endpoint",
     "registration_endpoint": "Registration Endpoint",
     "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": {
-			"deny": "拒绝(仅限注册用户)",
-			"readonly": "接受(来宾可以只读)"
-		},
+      "deny": "拒绝(仅限注册用户)",
+      "readonly": "接受(来宾可以只读)"
+    },
     "read_only_users_comment": {
       "deny": "拒绝 (禁止只浏览用户操作评论)",
       "accept": "允许 (只浏览用户可以管理评论)"
     },
-		"registration_mode": {
-			"open": "打开(任何人都可以注册)",
-			"restricted": "受限(需要管理员批准)",
-			"closed": "已关闭(仅限邀请)"
-		},
+    "registration_mode": {
+      "open": "打开(任何人都可以注册)",
+      "restricted": "受限(需要管理员批准)",
+      "closed": "已关闭(仅限邀请)"
+    },
     "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": "分享链接权",
     "enable_link_sharing": "启用链接共享",
     "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",
       "password_reset_by_users": "用户重置密码",
       "enable_password_reset_by_users": "启用用户重置密码",
@@ -130,181 +130,181 @@
       "email_authentication": "用户注册时的电子邮件身份验证",
       "enable_email_authentication": "启用电子邮件身份验证",
       "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>",
       "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"
-			},
-			"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": "通知设置",
-		"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"
-	},
+  },
   "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.",
   "admin_top": {
     "management_wiki": "管理Wiki",
@@ -420,17 +420,7 @@
     "enable": "启用",
     "disable": "停用",
     "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设置",
@@ -511,13 +501,13 @@
       "show_page_side_authors": "在目录上方始终显示创建者和更新者",
       "show_page_side_authors_desc": "在页面侧边栏的目录上方显示创建者和最后更新者的信息。"
     },
-      "presentation": "表达",
-      "presentation_options": {
+    "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 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"
     },
     "custom_title": "自定义标题",
@@ -531,7 +521,7 @@
     "write_css": "您可以编写应用于整个系统的CSS。",
     "ctrl_space": "Ctrl+Space 自动完成",
     "custom_script": "定制纸条",
-    "custom_presentation":"表达",
+    "custom_presentation": "表达",
     "write_java": "您可以编写应用于整个系统的Javascript。",
     "reflect_change": "您需要重新加载页面以反映更改。",
     "custom_logo": "自定义徽标",
@@ -727,7 +717,7 @@
       "allow_specified_long": "允许指定(只允许来自指定的渠道)",
       "test_connection": "测试连接",
       "test_connection_by_pressing_button": "按下按钮以测试连接",
-      "test_connection_only_public_channel":"请在一个公共频道中测试连接",
+      "test_connection_only_public_channel": "请在一个公共频道中测试连接",
       "error_check_logs_below": "发生了错误。请检查以下日志。",
       "send_message_to_slack_work_space": "发送到 Slack 工作区。",
       "add_slack_workspace": "添加Slack Workspace"
@@ -988,7 +978,7 @@
     "ADMIN_SITE_URL_UPDATE": "更新站点 URL 设置",
     "ADMIN_MAIL_SMTP_UPDATE": "更新电子邮件(SMTP)设置",
     "ADMIN_MAIL_SES_UPDATE": "更新电子邮件(SES)设置",
-    "ADMIN_MAIL_TEST_SUBMIT" : "发送测试邮件",
+    "ADMIN_MAIL_TEST_SUBMIT": "发送测试邮件",
     "ADMIN_FILE_UPLOAD_CONFIG_UPDATE": "更新文件上传设置",
     "ADMIN_PLUGIN_UPDATE": "更新插件设置",
     "ADMIN_MAINTENANCEMODE_ENABLED": "启用维护模式",
@@ -1140,8 +1130,8 @@
     "revoke_user_admin": "Succeeded to revoke {{username}} admin",
     "grant_user_read_only": "Succeeded to grant {{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_external_user_success": "Succeeded to remove {{accountId}}",
     "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": "添加",
   "Insert": "插入",
   "Reset": "重启",
-	"Sign out": "退出",
+  "Sign out": "退出",
   "New": "新建",
   "Send": "发送",
   "Close": "关闭",
   "Done": "完成",
   "Delete": "删除",
-
   "meta": {
     "display_name": "简体中文"
   },
@@ -33,52 +32,46 @@
   "headers": {
     "app_settings": "系统设置"
   },
-
   "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_in_all": "所有页面",
     "only_children_of_this_tree": "当前分支以下内容",
     "exact_mutch": "完全匹配"
   },
-
   "share_links": {
     "Share Link": "Share Link",
     "Page Path": "Page Path",
     "expire": "Expiration",
     "description": "Description"
   },
-
   "in_app_notification": {
     "notification_list": "应用内通知列表",
     "see_all": "查看通知列表",
     "no_notification": "您没有任何通知",
     "all": "全部",
     "unopend": "未读",
-    "mark_all_as_read" : "标记为已读",
+    "mark_all_as_read": "标记为已读",
     "no_unread_messages": "no_unread_messages",
     "only_unread": "Only unread"
   },
-
   "personal_dropdown": {
     "home": "家",
     "settings": "设置",
-		"color_mode": "颜色模式",
-		"sidebar_mode": "边栏模式",
-		"sidebar_mode_editor": "编辑器上的边栏模式",
-		"use_os_settings": "使用操作系统设置",
+    "color_mode": "颜色模式",
+    "sidebar_mode": "边栏模式",
+    "sidebar_mode_editor": "编辑器上的边栏模式",
+    "use_os_settings": "使用操作系统设置",
     "feedback": "意见和要求"
   },
-
   "create_page_dropdown": {
     "new_page": "新页面",
     "open_page_create_modal": "打开新页面创建模式",
@@ -92,24 +85,21 @@
       "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"
-	},
-
+  },
   "crop_image_modal": {
     "image_crop": "图像裁剪",
     "crop": "修剪",
     "save": "节省",
     "cancel": "取消"
   },
-
   "handsontable_modal": {
     "title": "编辑表格",
     "data_import": "数据导入",
@@ -125,35 +115,9 @@
       "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": {
     "page_not_exist": "该页面不存在"
   },
-
   "g2g_data_transfer": {
     "tab": "数据迁移",
     "data_transfer": "数据迁移",

+ 0 - 17
apps/app/public/static/locales/zh_CN/translation.json

@@ -978,23 +978,6 @@
     "move_to_root": "移动到根部",
     "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": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "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/)

Разлика између датотеке није приказан због своје велике величине
+ 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 { MaintenanceMode } from './MaintenanceMode';
 import PageBulkExportSettings from './PageBulkExportSettings';
-import QuestionnaireSettings from './QuestionnaireSettings';
 import SiteUrlSetting from './SiteUrlSetting';
 import V5PageMigration from './V5PageMigration';
 
@@ -119,13 +118,6 @@ const AppSettingsPageContents = (props: Props) => {
         </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="col-lg-12">
           <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,
         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) {
       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;

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

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

+ 2 - 2
apps/app/src/client/components/ShortcutsModal.tsx

@@ -331,7 +331,7 @@ const ShortcutsModal = (): JSX.Element => {
             <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.Bullet List')}</div>
+                <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>
@@ -342,7 +342,7 @@ const ShortcutsModal = (): JSX.Element => {
               </li>
               {/* Numbered 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="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>

+ 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 { UserPicture } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
-import dynamic from 'next/dynamic';
 import Link from 'next/link';
 import {
   UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
@@ -17,14 +16,10 @@ import { SkeletonItem } from './SkeletonItem';
 
 import styles from './PersonalDropdown.module.scss';
 
-const ProactiveQuestionnaireModal = dynamic(() => import('~/features/questionnaire/client/components/ProactiveQuestionnaireModal'), { ssr: false });
-
 export const PersonalDropdown = (): JSX.Element => {
   const { t } = useTranslation('commons');
   const { data: currentUser } = useCurrentUser();
 
-  const [isQuestionnaireModalOpen, setQuestionnaireModalOpen] = useState(false);
-
   if (currentUser == null) {
     return <SkeletonItem />;
   }
@@ -97,17 +92,6 @@ export const PersonalDropdown = (): JSX.Element => {
             </DropdownItem>
           </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']}`}>
             <span className="d-flex align-items-center">
               <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>
         </DropdownMenu>
       </UncontrolledDropdown>
-
-      <ProactiveQuestionnaireModal isOpen={isQuestionnaireModalOpen} onClose={() => setQuestionnaireModalOpen(false)} />
     </>
   );
 

+ 27 - 8
apps/app/src/features/mermaid/components/MermaidViewer.tsx

@@ -1,8 +1,12 @@
 import React, { useRef, useEffect, type JSX } from 'react';
 
 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 = {
   value: string
@@ -16,22 +20,37 @@ export const MermaidViewer = React.memo((props: MermaidViewerProps): JSX.Element
   const ref = useRef<HTMLDivElement>(null);
 
   useEffect(() => {
-    if (ref.current != null && value != null) {
-      mermaid.initialize({
-        theme: isDarkMode ? 'dark' : undefined,
-      });
-      mermaid.run({ nodes: [ref.current] });
-    }
+    (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 (
     value
       ? (
-        <div ref={ref} key={value as string}>
+        <div ref={ref} key={value}>
           {value}
         </div>
       )
-      : <div key={value as string}></div>
+      : <div key={value}></div>
   );
 });
+
 MermaidViewer.displayName = 'MermaidViewer';

+ 6 - 6
apps/app/src/features/openai/server/routes/edit/README.ja.md

@@ -2,7 +2,7 @@
 
 ## 要求仕様
 
-Editor Assistant API は、OpenAI AssistantAPI を使用して、マークダウンエディタの編集をサポートする機能です。主な要件は以下の通りです:
+Editor Assistant API は、OpenAI AssistantAPI を使用して、マークダウンエディタの編集をサポートする機能です。主な要件は以下の通りです:
 
 1. **ストリーミング処理**:
    - OpenAI からの応答をストリーミングで受け取り、Server-Sent Events (SSE) でクライアントにリアルタイムに転送
@@ -31,7 +31,7 @@ Editor Assistant API は、OpenAI AssistantAPI を使用して、マークダウ
 
 2. **型定義**:
    - `message-error.ts`: エラー型と定義
-   - `schema.ts`: エディタアシスタントのメッセージと差分の Zod スキーマ定義
+   - `schema.ts`: エディタアシスタントのメッセージと差分の Zod スキーマ定義
 
 ### 今後のリファクタリングに重要なインプット
 
@@ -81,12 +81,12 @@ Editor Assistant API は、OpenAI AssistantAPI を使用して、マークダウ
 
 ### 2. 差分検出と適応的送信制御
 
-エディタアシスタントの核心部分は、OpenAI APIからのレスポンスから差分情報を適切に抽出し、効率的にクライアントに送信する機能です。以下のような工夫を行っています:
+エディタアシスタントの核心部分は、OpenAI APIからのレスポンスから差分情報を適切に抽出し、効率的にクライアントに送信する機能です。以下のような工夫を行っています:
 
 - **メッセージと差分の処理の統合と最適化**:
   - UI/UX要件に基づく設計として、メッセージと差分の処理を単一ループで効率的に実装しています。
   - **メッセージ処理**:メッセージの「増分」(新しく追加された部分)のみをクライアントに送信します。これにより通信量を削減し、クライアント側の処理負荷を軽減します。
-  - **差分処理**:JSONノードとして確定した差分は即座に検出し通知します。ただし、確定していない(変更中の可能性がある)差分は送信を控えることでエディタの過剰な更新を防止します。
+  - **差分処理**:JSONノードとして確定した差分は即座に検出し通知します。ただし、確定していない(変更中の可能性がある)差分は送信を控えることでエディタの過剰な更新を防止します。
 
 - **処理効率の向上メカニズム**:
   - `processedMessages` Mapを使って、各メッセージ要素の前回の内容を記録し、差分のみを計算します。
@@ -109,7 +109,7 @@ Editor Assistant API は、OpenAI AssistantAPI を使用して、マークダウ
     // 差分を確定して送信リストに追加
   }
   ```
-  - この条件判定は単なる技術的工夫ではなく、UXの向上を目的としています。確定していない差分を頻繁に送信すると、エディタが頻繁に更新されてユーザー体験が悪化するためです。
+  - この条件判定は単なる技術的工夫ではなく、UXの向上を目的としています。確定していない差分を頻繁に送信すると、エディタが頻繁に更新されてユーザー体験が悪化するためです。
 
 - **重複防止メカニズム**:
   - 差分の重複送信を避けるため、一意のキーを生成する`getDiffKey`メソッドを実装しています。
@@ -142,5 +142,5 @@ Editor Assistant API は、OpenAI AssistantAPI を使用して、マークダウ
   - ストリームの終了を適切に検出し、完全な結果を送信してから接続を終了する機構を設けています。
   - エラー時でも可能な限り正常な形でレスポンスを返し、クライアント側での復旧を容易にします。
 
-このような設計と実装により、リアルタイム性と正確性を両立したエディタアシスタント機能を実現しています。ストリーミング処理の特性を活かしつつ、効率的なデータ処理と適応的な通知制御によって優れたユーザー体験を提供しています。
+このような設計と実装により、リアルタイム性と正確性を両立したエディタアシスタント機能を実現しています。ストリーミング処理の特性を活かしつつ、効率的なデータ処理と適応的な通知制御によって優れたユーザー体験を提供しています。
 

+ 1 - 1
apps/app/src/features/opentelemetry/server/custom-metrics/application-metrics.ts

@@ -32,7 +32,7 @@ export function addApplicationMetrics(): void {
       try {
         // Dynamic import to avoid circular dependencies
         const { growiInfoService } = await import('~/server/service/growi-info');
-        const growiInfo = await growiInfoService.getGrowiInfo(true);
+        const growiInfo = await growiInfoService.getGrowiInfo({ includeAttachmentInfo: true });
 
         const isAppSiteUrlHashed = configManager.getConfig('otel:isAppSiteUrlHashed');
 

+ 2 - 2
apps/app/src/features/opentelemetry/server/custom-metrics/user-counts-metrics.spec.ts

@@ -25,7 +25,7 @@ vi.mock('@opentelemetry/api', () => ({
 const mockGrowiInfoService = {
   getGrowiInfo: vi.fn(),
 };
-vi.mock('~/server/service/growi-info', () => ({
+vi.mock('~/server/service/growi-info', async() => ({
   growiInfoService: mockGrowiInfoService,
 }));
 
@@ -85,7 +85,7 @@ describe('addUserCountsMetrics', () => {
       const callback = mockMeter.addBatchObservableCallback.mock.calls[0][0];
       await callback(mockResult);
 
-      expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith(true);
+      expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith({ includeUserCountInfo: true });
       expect(mockResult.observe).toHaveBeenCalledWith(mockUserCountGauge, 150);
       expect(mockResult.observe).toHaveBeenCalledWith(mockActiveUserCountGauge, 75);
     });

+ 1 - 1
apps/app/src/features/opentelemetry/server/custom-metrics/user-counts-metrics.ts

@@ -29,7 +29,7 @@ export function addUserCountsMetrics(): void {
         // Dynamic import to avoid circular dependencies
         const { growiInfoService } = await import('~/server/service/growi-info');
 
-        const growiInfo = await growiInfoService.getGrowiInfo(true);
+        const growiInfo = await growiInfoService.getGrowiInfo({ includeUserCountInfo: true });
 
         // Observe user count metrics
         result.observe(userCountGauge, growiInfo.additionalInfo?.currentUsersCount || 0);

+ 1 - 1
apps/app/src/features/opentelemetry/server/custom-resource-attributes/application-resource-attributes.spec.ts

@@ -43,7 +43,7 @@ describe('getApplicationResourceAttributes', () => {
       'growi.installedAt': '2023-01-01T00:00:00.000Z',
       'growi.installedAt.by_oldest_user': '2023-01-01T00:00:00.000Z',
     });
-    expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith(true);
+    expect(mockGrowiInfoService.getGrowiInfo).toHaveBeenCalledWith({ includeInstalledInfo: true });
   });
 
   it('should handle missing additionalInfo gracefully', async() => {

+ 1 - 1
apps/app/src/features/opentelemetry/server/custom-resource-attributes/application-resource-attributes.ts

@@ -15,7 +15,7 @@ export async function getApplicationResourceAttributes(): Promise<Attributes> {
     // Dynamic import to avoid circular dependencies
     const { growiInfoService } = await import('~/server/service/growi-info');
 
-    const growiInfo = await growiInfoService.getGrowiInfo(true);
+    const growiInfo = await growiInfoService.getGrowiInfo({ includeInstalledInfo: true });
 
     const attributes: Attributes = {
       // Service configuration (rarely changes after system setup)

+ 1 - 0
apps/app/src/features/opentelemetry/server/node-sdk-configuration.ts

@@ -41,6 +41,7 @@ export const generateNodeSDKConfiguration = (opts?: Option): Configuration => {
       traceExporter: new OTLPTraceExporter(),
       metricReader: new PeriodicExportingMetricReader({
         exporter: new OTLPMetricExporter(),
+        exportIntervalMillis: 300000, // 5 minute
       }),
       instrumentations: [getNodeAutoInstrumentations({
         '@opentelemetry/instrumentation-bunyan': {

+ 2 - 2
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/index.ts

@@ -113,8 +113,8 @@ class PageBulkExportJobCronService extends CronService implements IPageBulkExpor
       }
     }
 
-    const basePath = isHtmlPath ? path.join(this.tmpOutputRootDir, 'html') : this.tmpOutputRootDir;
-    return path.join(basePath, appId ?? '', jobId);
+    const basePath = path.join(this.tmpOutputRootDir, appId ?? '');
+    return isHtmlPath ? path.join(basePath, 'html', jobId) : path.join(basePath, jobId);
   }
 
   /**

+ 1 - 0
apps/app/src/features/page-bulk-export/server/service/page-bulk-export-job-cron/steps/export-pages-to-fs-async.ts

@@ -39,6 +39,7 @@ async function getPageWritable(this: IPageBulkExportJobCronService, pageBulkExpo
   // define before the stream starts to avoid creating multiple instances
   const htmlConverter = unified()
     .use(remarkParse)
+    // !!! DO NOT DISABLE HTML ESCAPING WHILE --no-sandbox IS PASSED TO PUPPETEER INSIDE pdf-converter !!!
     .use(remarkHtml);
   return new Writable({
     objectMode: true,

+ 0 - 152
apps/app/src/features/questionnaire/client/components/ProactiveQuestionnaireModal.tsx

@@ -1,152 +0,0 @@
-import { useState, useCallback, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import {
-  Modal, ModalBody,
-} from 'reactstrap';
-
-import { apiv3Post } from '~/client/util/apiv3-client';
-
-type ModalProps = {
-  isOpen: boolean,
-  onClose: () => void,
-};
-
-const QuestionnaireCompletionModal = (props: ModalProps): JSX.Element => {
-  const { t } = useTranslation('commons');
-
-  const { isOpen, onClose } = props;
-
-  return (
-    <Modal
-      size="lg"
-      isOpen={isOpen}
-      toggle={onClose}
-      centered
-    >
-      <ModalBody className="overflow-hidden p-0" style={{ borderRadius: 8 }}>
-        <div className="m-2 p-4" style={{ borderRadius: 8 }}>
-          <div className="text-center">
-            <h2 className="my-4">{t('questionnaire_modal.title')}</h2>
-            <p className="mb-1">{t('questionnaire_modal.successfully_submitted')}</p>
-            <p>{t('questionnaire_modal.thanks_for_answering')}</p>
-          </div>
-          <div className="text-center my-3">
-            <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={onClose}>{t('Close')}</span>
-          </div>
-        </div>
-      </ModalBody>
-    </Modal>
-  );
-};
-
-const ProactiveQuestionnaireModal = (props: ModalProps): JSX.Element => {
-  const { t } = useTranslation('commons');
-
-  const { isOpen, onClose } = props;
-
-  const [isQuestionnaireCompletionModal, setQuestionnaireCompletionModal] = useState(false);
-
-  const submitHandler = useCallback(async(e) => {
-    e.preventDefault();
-
-    const formData = e.target.elements;
-
-    const {
-      satisfaction: { value: satisfaction },
-      lengthOfExperience: { value: lengthOfExperience },
-      occupation: { value: occupation },
-      position: { value: position },
-      commentText: { value: commentText },
-    } = formData;
-
-    const sendValues = {
-      satisfaction: Number(satisfaction),
-      lengthOfExperience,
-      occupation,
-      position,
-      commentText,
-    };
-
-    apiv3Post('/questionnaire/proactive/answer', sendValues);
-
-    onClose();
-    setQuestionnaireCompletionModal(true);
-  }, [onClose]);
-
-  return (
-    <>
-      <Modal
-        data-testid="grw-proactive-questionnaire-modal"
-        size="lg"
-        isOpen={isOpen}
-        toggle={onClose}
-        centered
-      >
-        <ModalBody className="overflow-hidden p-0" style={{ borderRadius: 8 }}>
-          <div className="m-2 p-4" style={{ borderRadius: 8 }}>
-            <div className="text-center">
-              <h2 className="my-4">{t('questionnaire_modal.title')}</h2>
-              <p className="mb-1">{t('questionnaire_modal.more_satisfied_services')}</p>
-              <p>{t('questionnaire_modal.strive_to_improve_services')}</p>
-            </div>
-            <form className="px-5" onSubmit={submitHandler}>
-              <div className="row mt-5">
-                <label className="col-sm-5 col-form-label" htmlFor="satisfaction">
-                  <span className="badge bg-primary me-2">{t('questionnaire_modal.required')}</span>{t('questionnaire_modal.satisfaction_with_growi')}
-                </label>
-                <select className="col-sm-7 form-control" name="satisfaction" id="satisfaction" required>
-                  <option value="">▼ {t('Select')}</option>
-                  <option>1</option>
-                  <option>2</option>
-                  <option>3</option>
-                  <option>4</option>
-                  <option>5</option>
-                </select>
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="lengthOfExperience">{t('questionnaire_modal.history_of_growi_usage')}</label>
-                <select
-                  name="lengthOfExperience"
-                  id="lengthOfExperience"
-                  className="col-sm-7 form-control"
-                >
-                  <option value="">▼ {t('Select')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.more_than_two_years')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.one_to_two_years')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.six_months_to_one_year')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.three_months_to_six_months')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.one_month_to_three_months')}</option>
-                  <option>{t('questionnaire_modal.length_of_experience.less_than_one_month')}</option>
-                </select>
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="occupation">{t('questionnaire_modal.occupation')}</label>
-                <input className="col-sm-7 form-control" type="text" name="occupation" id="occupation" />
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="position">{t('questionnaire_modal.position')}</label>
-                <input className="col-sm-7 form-control" type="text" name="position" id="position" />
-              </div>
-              <div className="row mt-3">
-                <label className="col-sm-5 col-form-label" htmlFor="commentText">
-                  <span className="badge bg-primary me-2">{t('questionnaire_modal.required')}</span>{t('questionnaire_modal.comment_on_growi')}
-                </label>
-                <textarea className="col-sm-7 form-control" name="commentText" id="commentText" rows={5} required />
-              </div>
-              <div className="text-center mt-5">
-                <button type="submit" className="btn btn-primary">{t('questionnaire_modal.submit')}</button>
-              </div>
-              <div className="text-center my-3">
-                <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={onClose}>{t('questionnaire_modal.close')}</span>
-              </div>
-            </form>
-          </div>
-        </ModalBody>
-      </Modal>
-      <QuestionnaireCompletionModal isOpen={isQuestionnaireCompletionModal} onClose={() => setQuestionnaireCompletionModal(false)} />
-    </>
-  );
-};
-
-export default ProactiveQuestionnaireModal;

+ 0 - 52
apps/app/src/features/questionnaire/client/components/Question.tsx

@@ -1,52 +0,0 @@
-import type { JSX } from 'react';
-
-import { useCurrentUser } from '~/stores-universal/context';
-
-import type { IQuestionHasId } from '../../interfaces/question';
-
-
-type QuestionProps = {
-  question: IQuestionHasId,
-  inputNamePrefix: string,
-}
-
-const Question = ({ question, inputNamePrefix }: QuestionProps): JSX.Element => {
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const questionText = lang === 'en_US' ? question.text.en_US : question.text.ja_JP;
-
-  return (
-    <div className="row mt-4">
-      <div className="col-6 d-flex align-items-center">
-        <span>
-          {questionText}
-        </span>
-      </div>
-      <div className="col-6 d-flex align-items-center ps-0">
-        <div className="btn-group flex-fill grw-questionnaire-btn-group">
-          <label className="form-label btn btn-outline-primary active me-4 rounded">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-noAnswer`} value="0" defaultChecked /> -
-          </label>
-          <label className="form-label btn btn-outline-primary rounded-start">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option1`} value="1" /> 1
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option2`} value="2" /> 2
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option3`} value="3" /> 3
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option4`} value="4" /> 4
-          </label>
-          <label className="form-label btn btn-outline-primary">
-            <input type="radio" name={`${inputNamePrefix + question._id}`} id={`${question._id}-option5`} value="5" /> 5
-          </label>
-        </div>
-      </div>
-    </div>
-  );
-};
-
-export default Question;

+ 0 - 167
apps/app/src/features/questionnaire/client/components/QuestionnaireModal.tsx

@@ -1,167 +0,0 @@
-import { useCallback, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-import { Modal, ModalBody } from 'reactstrap';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useQuestionnaireModal } from '~/features/questionnaire/client/stores/model';
-import type { IAnswer } from '~/features/questionnaire/interfaces/answer';
-import { StatusType } from '~/features/questionnaire/interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '~/features/questionnaire/interfaces/questionnaire-order';
-import { useCurrentUser } from '~/stores-universal/context';
-import loggerFactory from '~/utils/logger';
-
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-
-import Question from './Question';
-
-const logger = loggerFactory('growi:QuestionnaireModal');
-
-type QuestionnaireModalProps = {
-  questionnaireOrder: IQuestionnaireOrderHasId
-}
-
-const QuestionnaireModal = ({ questionnaireOrder }: QuestionnaireModalProps): JSX.Element => {
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const { data: questionnaireModalData, close: closeQuestionnaireModal } = useQuestionnaireModal();
-  const isOpened = questionnaireModalData?.openedQuestionnaireId === questionnaireOrder._id;
-
-  const inputNamePrefix = 'question-';
-
-  const { t } = useTranslation(['translation', 'commons']);
-
-  const sendAnswer = useCallback(async(answers: IAnswer[]) => {
-    try {
-      await apiv3Put('/questionnaire/answer', {
-        questionnaireOrderId: questionnaireOrder._id,
-        answers,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.answered);
-      }
-      toastSuccess(
-        <>
-          <div className="fw-bold">{t('questionnaire.thank_you_for_answering')}</div>
-          <div className="pt-2">{t('questionnaire.additional_feedback')}</div>
-        </>,
-        {
-          autoClose: 3000,
-          closeButton: true,
-        },
-      );
-    }
-    catch (e) {
-      logger.error(e);
-      toastError(t('questionnaire.failed_to_send'));
-    }
-  }, [questionnaireOrder._id, t, currentUser]);
-
-  const submitHandler = useCallback(async(event) => {
-    event.preventDefault();
-
-    const answers: IAnswer[] = questionnaireOrder.questions.map((question) => {
-      const answerValue = event.target[`${inputNamePrefix + question._id}`].value;
-      return { question: question._id, value: answerValue };
-    });
-
-    sendAnswer(answers);
-
-    const shouldCloseToast = true;
-    closeQuestionnaireModal(shouldCloseToast);
-  }, [closeQuestionnaireModal, questionnaireOrder.questions, sendAnswer]);
-
-  const denyBtnClickHandler = useCallback(async() => {
-    try {
-      apiv3Put('/questionnaire/deny', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
-      }
-      toastSuccess(t('questionnaire.denied'));
-    }
-    catch (e) {
-      logger.error(e);
-    }
-    const shouldCloseToast = true;
-    closeQuestionnaireModal(shouldCloseToast);
-  }, [closeQuestionnaireModal, questionnaireOrder._id, t, currentUser]);
-
-  // No showing toasts since not important
-  const closeBtnClickHandler = useCallback(async(shouldCloseToast: boolean) => {
-    closeQuestionnaireModal(shouldCloseToast);
-
-    try {
-      await apiv3Put('/questionnaire/skip', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
-      }
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [closeQuestionnaireModal, questionnaireOrder._id, currentUser]);
-
-  const closeBtnClickHandlerClosingToast = useCallback(async() => {
-    closeBtnClickHandler(true);
-  }, [closeBtnClickHandler]);
-
-  const questionnaireOrderTitle = lang === 'en_US' ? questionnaireOrder.title.en_US : questionnaireOrder.title.ja_JP;
-
-  return (
-    <Modal
-      size="lg"
-      isOpen={isOpened}
-      toggle={closeBtnClickHandlerClosingToast}
-      centered
-    >
-      <form onSubmit={submitHandler}>
-        <ModalBody className="bg-primary overflow-hidden p-0" style={{ borderRadius: 8 }}>
-          <div className="bg-white m-2 p-4" style={{ borderRadius: 8 }}>
-            <div className="text-center mb-2">
-              <h2 className="my-4">{questionnaireOrderTitle}</h2>
-              <p className="mb-1">{t('commons:questionnaire_modal.more_satisfied_services')}</p>
-              <p>{t('commons:questionnaire_modal.strive_to_improve_services')}</p>
-            </div>
-            <div className="container">
-              <div className="row mt-4">
-                <div className="col-md-2 offset-md-5 fw-bold text-end align-items-center p-0">{t('questionnaire.no_answer')}</div>
-                <div className="col-md-5 d-flex justify-content-between align-items-center">
-                  <span className="fw-bold">{t('questionnaire.disagree')}</span>
-                  <span className="fw-bold">{t('questionnaire.agree')}</span>
-                </div>
-              </div>
-              {questionnaireOrder.questions?.map((question) => {
-                return <Question question={question} inputNamePrefix={inputNamePrefix} key={question._id} />;
-              })}
-            </div>
-            <div className="text-center mt-5">
-              <button type="submit" className="btn btn-primary">{t('questionnaire.answer')}</button>
-            </div>
-            <div className="text-center cursor-pointer text-decoration-underline my-3">
-              <span style={{ cursor: 'pointer', textDecoration: 'underline' }} onClick={denyBtnClickHandler}>{t('questionnaire.dont_show_again')}</span>
-            </div>
-
-            {currentUser?.admin && (
-              <a href="/admin/app#questionnaire-settings">
-                <i className="material-symbols-outlined me-1">admin_panel_settings</i>
-              </a>
-            )}
-            {currentUser != null && (
-              <a href="/me#other_settings">
-                <i className="material-symbols-outlined">settings</i>
-              </a>
-            )}
-          </div>
-        </ModalBody>
-      </form>
-    </Modal>
-  );
-};
-
-export default QuestionnaireModal;

+ 0 - 9
apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.module.scss

@@ -1,9 +0,0 @@
-@use '@growi/core-styles/scss/bootstrap/init' as bs;
-
-.grw-questionnaire-toasts :global {
-  position: fixed;
-  right: 20px;
-  bottom: 52px;
-  z-index: bs.$zindex-fixed + 1;
-  width: 230px;
-}

+ 0 - 51
apps/app/src/features/questionnaire/client/components/QuestionnaireModalManager.tsx

@@ -1,51 +0,0 @@
-import { useCallback, type JSX } from 'react';
-
-import { useCurrentUser } from '~/stores-universal/context';
-
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-import { useSWRxQuestionnaireOrders } from '../stores/questionnaire';
-
-import QuestionnaireModal from './QuestionnaireModal';
-import QuestionnaireToast from './QuestionnaireToast';
-
-import styles from './QuestionnaireModalManager.module.scss';
-
-const QuestionnaireModalManager = ():JSX.Element => {
-  const { data: questionnaireOrders } = useSWRxQuestionnaireOrders();
-  const { data: currentUser } = useCurrentUser();
-
-  const questionnaireOrdersToShow = useCallback((questionnaireOrders: IQuestionnaireOrderHasId[] | undefined) => {
-    const guestQuestionnaireAnswerStorage = GuestQuestionnaireAnswerStatusService.getStorage();
-    if (currentUser != null || guestQuestionnaireAnswerStorage == null) {
-      return questionnaireOrders;
-    }
-
-    return questionnaireOrders?.filter((questionnaireOrder) => {
-      const localAnswerStatus = guestQuestionnaireAnswerStorage[questionnaireOrder._id];
-      return localAnswerStatus == null || localAnswerStatus.status === StatusType.not_answered;
-    });
-  }, [currentUser]);
-
-  return (
-    <>
-      {questionnaireOrders?.map((questionnaireOrder) => {
-        return (
-          <QuestionnaireModal
-            questionnaireOrder={questionnaireOrder}
-            key={questionnaireOrder._id}
-          />
-        );
-      })}
-      <div className={styles['grw-questionnaire-toasts']}>
-        {questionnaireOrdersToShow(questionnaireOrders)?.map((questionnaireOrder) => {
-          return <QuestionnaireToast questionnaireOrder={questionnaireOrder} key={questionnaireOrder._id} />;
-        })}
-      </div>
-    </>
-  );
-};
-
-export default QuestionnaireModalManager;

+ 0 - 85
apps/app/src/features/questionnaire/client/components/QuestionnaireToast.tsx

@@ -1,85 +0,0 @@
-import { useCallback, useState, type JSX } from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastSuccess } from '~/client/util/toastr';
-import { useQuestionnaireModal } from '~/features/questionnaire/client/stores/model';
-import { useCurrentUser } from '~/stores-universal/context';
-import loggerFactory from '~/utils/logger';
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
-import { GuestQuestionnaireAnswerStatusService } from '../services/guest-questionnaire-answer-status';
-
-const logger = loggerFactory('growi:QuestionnaireToast');
-
-type QuestionnaireToastProps = {
-  questionnaireOrder: IQuestionnaireOrderHasId,
-}
-
-const QuestionnaireToast = ({ questionnaireOrder }: QuestionnaireToastProps): JSX.Element => {
-  const { open: openQuestionnaireModal } = useQuestionnaireModal();
-  const { data: currentUser } = useCurrentUser();
-  const lang = currentUser?.lang;
-
-  const [isOpen, setIsOpen] = useState(true);
-
-  const { t } = useTranslation();
-
-  const answerBtnClickHandler = useCallback(() => {
-    openQuestionnaireModal(questionnaireOrder._id, () => setIsOpen(false));
-  }, [openQuestionnaireModal, questionnaireOrder._id]);
-
-  const denyBtnClickHandler = useCallback(async() => {
-    // Immediately close
-    setIsOpen(false);
-
-    try {
-      await apiv3Put('/questionnaire/deny', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.denied);
-      }
-      toastSuccess(t('questionnaire.denied'));
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [questionnaireOrder._id, t, currentUser]);
-
-  // No showing toasts since not important
-  const closeBtnClickHandler = useCallback(async() => {
-    setIsOpen(false);
-
-    try {
-      await apiv3Put('/questionnaire/skip', {
-        questionnaireOrderId: questionnaireOrder._id,
-      });
-      if (currentUser == null) {
-        GuestQuestionnaireAnswerStatusService.setStatus(questionnaireOrder._id, StatusType.skipped);
-      }
-    }
-    catch (e) {
-      logger.error(e);
-    }
-  }, [questionnaireOrder._id, currentUser]);
-
-  const questionnaireOrderShortTitle = lang === 'en_US' ? questionnaireOrder.shortTitle.en_US : questionnaireOrder.shortTitle.ja_JP;
-
-  return (
-    <div className={`toast ${isOpen ? 'show' : 'hide'}`}>
-      <div className="toast-header bg-primary">
-        <strong className="me-auto text-light">{questionnaireOrderShortTitle}</strong>
-        <button type="button" className="ms-2 mb-1 btn-close" onClick={closeBtnClickHandler} aria-label="Close"></button>
-      </div>
-      <div className="toast-body bg-light text-dark d-flex justify-content-end">
-        <button type="button" className="btn btn-secondary me-3" onClick={answerBtnClickHandler}>{t('questionnaire.answer')}</button>
-        <button type="button" className="btn btn-secondary" onClick={denyBtnClickHandler}>{t('questionnaire.deny')}</button>
-      </div>
-    </div>
-  );
-};
-
-export default QuestionnaireToast;

+ 0 - 79
apps/app/src/features/questionnaire/client/services/guest-questionnaire-answer-status.ts

@@ -1,79 +0,0 @@
-// A service to manage questionnaire answer statuses for guest user.
-// Saves statuses in localStorage.
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-
-interface GuestQuestionnaireAnswerStatus {
-  status: StatusType
-  updatedDate: string
-}
-
-interface GuestQuestionnaireAnswerStatusStorage {
-  [key: string]: GuestQuestionnaireAnswerStatus
-}
-
-const storageKey = 'guestQuestionnaireAnswerStatuses';
-const DAYS_UNTIL_EXPIRATION = 30;
-
-/**
- * Get all answer statuses stored in localStorage as GuestQuestionnaireAnswerStatusStorage,
- * and update outdated information.
- */
-const getStorage = (): GuestQuestionnaireAnswerStatusStorage | null => {
-  if (typeof window === 'undefined') { return null }
-
-  const currentStorage = localStorage.getItem(storageKey);
-
-  if (currentStorage == null) { return null }
-
-  const storageJson: GuestQuestionnaireAnswerStatusStorage = JSON.parse(currentStorage);
-  // delete status if outdated to prevent localStorage overflow
-  // change skipped to not_answered if different date than when skipped
-  Object.keys(storageJson).forEach((key) => {
-    const answerStatus = storageJson[key];
-    const updatedDate = new Date(answerStatus.updatedDate);
-    const expirationDate = new Date(updatedDate.setDate(updatedDate.getDate() + DAYS_UNTIL_EXPIRATION));
-    if (expirationDate < new Date()) {
-      delete storageJson[key];
-    }
-    else if (answerStatus.status === StatusType.skipped
-          && new Date().toDateString() !== answerStatus.updatedDate) {
-      storageJson[key] = {
-        status: StatusType.not_answered,
-        updatedDate: new Date().toDateString(),
-      };
-    }
-  });
-
-  return storageJson;
-};
-
-/**
- * Set answer status for questionnaire order in GuestQuestionnaireAnswerStatusStorage,
- * and save it in localStorage.
- */
-const setStatus = (questionnaireOrderId: string, status: StatusType): void => {
-  if (typeof window === 'undefined') { return }
-
-  const guestQuestionnaireAnswerStatus: GuestQuestionnaireAnswerStatus = {
-    status,
-    updatedDate: new Date().toDateString(),
-  };
-
-  const storage = getStorage();
-
-  if (storage != null) {
-    storage[questionnaireOrderId] = guestQuestionnaireAnswerStatus;
-    localStorage.setItem(storageKey, JSON.stringify(storage));
-    return;
-  }
-
-  const initialStorage: GuestQuestionnaireAnswerStatusStorage = { [questionnaireOrderId]: guestQuestionnaireAnswerStatus };
-  localStorage.setItem(storageKey, JSON.stringify(initialStorage));
-
-};
-
-export const GuestQuestionnaireAnswerStatusService = {
-  setStatus,
-  getStorage,
-};

+ 0 - 40
apps/app/src/features/questionnaire/client/stores/model.tsx

@@ -1,40 +0,0 @@
-import type { SWRResponse } from 'swr';
-
-import { useStaticSWR } from '~/stores/use-static-swr';
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:stores:modal');
-
-/*
-* QuestionnaireModals
-*/
-type QuestionnaireModalStatuses = {
-  openedQuestionnaireId: string | null,
-  closeToast?: () => void | Promise<void>,
-}
-
-type QuestionnaireModalStatusUtils = {
-  open(string: string, closeToast: () => void | Promise<void>): Promise<QuestionnaireModalStatuses | undefined>
-  close(shouldCloseToast?: boolean): Promise<QuestionnaireModalStatuses | undefined>
-}
-
-export const useQuestionnaireModal = (status?: QuestionnaireModalStatuses): SWRResponse<QuestionnaireModalStatuses, Error> & QuestionnaireModalStatusUtils => {
-  const initialData: QuestionnaireModalStatuses = { openedQuestionnaireId: null };
-  const swrResponse = useStaticSWR<QuestionnaireModalStatuses, Error>('questionnaireModalStatus', status, { fallbackData: initialData });
-
-  return {
-    ...swrResponse,
-    open: (questionnaireOrderId: string, closeToast: () => void | Promise<void>) => swrResponse.mutate({
-      openedQuestionnaireId: questionnaireOrderId,
-      closeToast,
-    }),
-    close: (shouldCloseToast?: boolean) => {
-      if (shouldCloseToast) {
-        swrResponse.data?.closeToast?.();
-        if (swrResponse.data?.closeToast === undefined) logger.debug('Tried to run `swrResponse.data?.closeToast` but it was `undefined`');
-      }
-
-      return swrResponse.mutate({ openedQuestionnaireId: null });
-    },
-  };
-};

+ 0 - 24
apps/app/src/features/questionnaire/client/stores/questionnaire.tsx

@@ -1,24 +0,0 @@
-import type { SWRResponse } from 'swr';
-import useSWR from 'swr';
-
-import { apiv3Get } from '~/client/util/apiv3-client';
-
-import type { IQuestionnaireOrderHasId } from '../../interfaces/questionnaire-order';
-
-export const useSWRxQuestionnaireOrders = (): SWRResponse<IQuestionnaireOrderHasId[], Error> => {
-  return useSWR(
-    '/questionnaire/orders',
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return response.data.questionnaireOrders;
-    }),
-  );
-};
-
-export const useSWRxIsQuestionnaireEnabled = (): SWRResponse<boolean, Error> => {
-  return useSWR(
-    '/questionnaire/is-enabled',
-    endpoint => apiv3Get(endpoint)
-      .then(response => !!response.data.isEnabled)
-      .catch(() => false),
-  );
-};

+ 0 - 4
apps/app/src/features/questionnaire/interfaces/answer.ts

@@ -1,4 +0,0 @@
-export interface IAnswer<ID = string> {
-  question: ID
-  value: string
-}

+ 0 - 25
apps/app/src/features/questionnaire/interfaces/condition.ts

@@ -1,25 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-import type { GrowiServiceType } from '@growi/core/dist/consts';
-
-import type { UserType } from './user-info';
-
-
-interface UserCondition {
-  types: UserType[] // user types to show questionnaire
-  daysSinceCreation?: {
-    moreThanOrEqualTo?: number
-    lessThanOrEqualTo?: number
-  }
-}
-
-interface GrowiCondition {
-  types: GrowiServiceType[] // GROWI types to show questionnaire in
-  versionRegExps: string[] // GROWI versions to show questionnaire in
-}
-
-export interface ICondition {
-  user: UserCondition
-  growi: GrowiCondition
-}
-
-export type IConditionHasId = ICondition & HasObjectId;

+ 0 - 18
apps/app/src/features/questionnaire/interfaces/growi-app-info.ts

@@ -1,18 +0,0 @@
-import type { IGrowiAdditionalInfo, IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { AttachmentMethodType } from '~/interfaces/attachment';
-import type { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
-
-
-export type IGrowiAppAdditionalInfo = IGrowiAdditionalInfo & {
-  attachmentType: AttachmentMethodType
-  activeExternalAccountTypes?: IExternalAuthProviderType[]
-}
-
-// legacy properties (extracted from additionalInfo for growi-questionnaire)
-// see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
-export type IGrowiAppInfoLegacy = Omit<IGrowiInfo<IGrowiAppAdditionalInfo>, 'additionalInfo'>
-  & IGrowiAppAdditionalInfo
-  & {
-    appSiteUrlHashed: string,
-  };

+ 0 - 27
apps/app/src/features/questionnaire/interfaces/proactive-questionnaire-answer.ts

@@ -1,27 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
-import type { IUserInfo } from './user-info';
-
-
-export interface IProactiveQuestionnaireAnswer {
-  satisfaction: number,
-  commentText: string,
-  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>,
-  userInfo: IUserInfo,
-  answeredAt: Date,
-  lengthOfExperience?: string,
-  position?: string,
-  occupation?: string,
-}
-
-export interface IProactiveQuestionnaireAnswerLegacy {
-  satisfaction: number,
-  commentText: string,
-  growiInfo: IGrowiAppInfoLegacy,
-  userInfo: IUserInfo,
-  answeredAt: Date,
-  lengthOfExperience?: string,
-  position?: string,
-  occupation?: string,
-}

+ 0 - 15
apps/app/src/features/questionnaire/interfaces/question.ts

@@ -1,15 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-
-export const QuestionType = { points: 'points', text: 'text' } as const;
-
-type QuestionType = typeof QuestionType[keyof typeof QuestionType];
-
-export interface IQuestion {
-  type: QuestionType
-  text: {
-    ja_JP: string
-    en_US: string
-  }
-}
-
-export type IQuestionHasId = IQuestion & HasObjectId;

+ 0 - 16
apps/app/src/features/questionnaire/interfaces/questionnaire-answer-status.ts

@@ -1,16 +0,0 @@
-// eslint-disable-next-line max-len
-// see: https://dev.growi.org/6385911e1632aa30f4dae6a4#mdcont-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%94%E3%81%A8%E3%81%AB%E5%9B%9E%E7%AD%94%E6%B8%88%E3%81%BF%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E3%82%92%E4%BF%9D%E5%AD%98%E3%81%99%E3%82%8B
-
-import type { Types } from 'mongoose';
-
-export const StatusType = {
-  not_answered: 'not_answered', answered: 'answered', skipped: 'skipped', denied: 'denied',
-} as const;
-
-export type StatusType = typeof StatusType[keyof typeof StatusType];
-
-export interface IQuestionnaireAnswerStatus {
-  user: Types.ObjectId | string // user that answered questionnaire
-  questionnaireOrderId: string
-  status: StatusType
-}

+ 0 - 21
apps/app/src/features/questionnaire/interfaces/questionnaire-answer.ts

@@ -1,21 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { IAnswer } from './answer';
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from './growi-app-info';
-import type { IUserInfo } from './user-info';
-
-export interface IQuestionnaireAnswer<ID = string> {
-  answers: IAnswer[]
-  answeredAt: Date
-  growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>
-  userInfo: IUserInfo
-  questionnaireOrder: ID
-}
-
-export interface IQuestionnaireAnswerLegacy<ID = string> {
-  answers: IAnswer[]
-  answeredAt: Date
-  growiInfo: IGrowiAppInfoLegacy,
-  userInfo: IUserInfo
-  questionnaireOrder: ID
-}

+ 0 - 21
apps/app/src/features/questionnaire/interfaces/questionnaire-order.ts

@@ -1,21 +0,0 @@
-import type { HasObjectId } from '@growi/core';
-
-import type { ICondition, IConditionHasId } from './condition';
-import type { IQuestion, IQuestionHasId } from './question';
-
-export interface IQuestionnaireOrder<TQUESTION = IQuestion, TCONDITION = ICondition> {
-  shortTitle: {
-    ja_JP: string
-    en_US: string
-  }
-  title: {
-    ja_JP: string
-    en_US: string
-  }
-  showFrom: Date
-  showUntil: Date
-  questions: TQUESTION[]
-  condition: TCONDITION
-}
-
-export type IQuestionnaireOrderHasId = IQuestionnaireOrder<IQuestionHasId, IConditionHasId> & HasObjectId;

+ 0 - 12
apps/app/src/features/questionnaire/interfaces/user-info.ts

@@ -1,12 +0,0 @@
-export const UserType = { admin: 'admin', general: 'general', guest: 'guest' } as const;
-
-type guestType = typeof UserType.guest
-export type UserType = typeof UserType[keyof typeof UserType];
-
-export type IUserInfo = {
-  userIdHash: string // userId hash generated by using appSiteUrl as salt
-  type: Exclude<UserType, guestType>
-  userCreatedAt: Date // createdAt of user that answered the questionnaire
-} | {
-  type: guestType
-}

+ 0 - 28
apps/app/src/features/questionnaire/server/models/proactive-questionnaire-answer.ts

@@ -1,28 +0,0 @@
-import type { Model } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IProactiveQuestionnaireAnswer } from '../../interfaces/proactive-questionnaire-answer';
-
-import { growiInfoSchema } from './schema/growi-info';
-import { userInfoSchema } from './schema/user-info';
-
-interface ProactiveQuestionnaireAnswerDocument extends IProactiveQuestionnaireAnswer, Document {}
-
-type ProactiveQuestionnaireAnswerModel = Model<ProactiveQuestionnaireAnswerDocument>
-
-export const proactiveQuestionnaireAnswerSchema = new Schema<ProactiveQuestionnaireAnswerDocument>({
-  satisfaction: { type: Number, required: true },
-  lengthOfExperience: { type: String },
-  position: { type: String },
-  occupation: { type: String },
-  commentText: { type: String, required: true },
-  growiInfo: { type: growiInfoSchema, required: true },
-  userInfo: { type: userInfoSchema, required: true },
-  answeredAt: { type: Date },
-}, { timestamps: true });
-
-export default getOrCreateModel<ProactiveQuestionnaireAnswerDocument, ProactiveQuestionnaireAnswerModel>(
-  'ProactiveQuestionnaireAnswer', proactiveQuestionnaireAnswerSchema,
-);

+ 0 - 19
apps/app/src/features/questionnaire/server/models/questionnaire-answer-status.ts

@@ -1,19 +0,0 @@
-import type { Model, Document } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireAnswerStatus } from '../../interfaces/questionnaire-answer-status';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-
-export interface QuestionnaireAnswerStatusDocument extends IQuestionnaireAnswerStatus, Document {}
-
-export type QuestionnaireAnswerStatusModel = Model<QuestionnaireAnswerStatusDocument>
-
-const questionnaireOrderSchema = new Schema<QuestionnaireAnswerStatusDocument>({
-  user: { type: Schema.Types.ObjectId, required: true },
-  questionnaireOrderId: { type: String, required: true },
-  status: { type: String, enum: Object.values(StatusType), default: StatusType.not_answered },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireAnswerStatusDocument, QuestionnaireAnswerStatusModel>('QuestionnaireAnswerStatus', questionnaireOrderSchema);

+ 0 - 25
apps/app/src/features/questionnaire/server/models/questionnaire-answer.ts

@@ -1,25 +0,0 @@
-import type { Document, Model } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireAnswer } from '../../interfaces/questionnaire-answer';
-
-import { answerSchema } from './schema/answer';
-import { growiInfoSchema } from './schema/growi-info';
-import { userInfoSchema } from './schema/user-info';
-
-interface QuestionnaireAnswerDocument extends IQuestionnaireAnswer<ObjectIdLike>, Document {}
-
-type QuestionnaireAnswerModel = Model<QuestionnaireAnswerDocument>
-
-const questionnaireAnswerSchema = new Schema<QuestionnaireAnswerDocument>({
-  answers: [answerSchema],
-  answeredAt: { type: Date, required: true },
-  growiInfo: { type: growiInfoSchema, required: true },
-  userInfo: { type: userInfoSchema, required: true },
-  questionnaireOrder: { type: Schema.Types.ObjectId, ref: 'QuestionnaireOrder' },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireAnswerDocument, QuestionnaireAnswerModel>('QuestionnaireAnswer', questionnaireAnswerSchema);

+ 0 - 35
apps/app/src/features/questionnaire/server/models/questionnaire-order.ts

@@ -1,35 +0,0 @@
-import type { Model, Document } from 'mongoose';
-import { Schema } from 'mongoose';
-
-import { getOrCreateModel } from '~/server/util/mongoose-utils';
-
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-
-import conditionSchema from './schema/condition';
-import questionSchema from './schema/question';
-
-export interface QuestionnaireOrderDocument extends IQuestionnaireOrder, Document {}
-
-export type QuestionnaireOrderModel = Model<QuestionnaireOrderDocument>
-
-const questionnaireOrderTitleSchema = new Schema<IQuestionnaireOrder['title']>({
-  ja_JP: { type: String, required: true },
-  en_US: { type: String, required: true },
-}, { _id: false });
-
-const questionnaireOrderSchema = new Schema<QuestionnaireOrderDocument>({
-  shortTitle: { type: questionnaireOrderTitleSchema, required: true },
-  title: { type: questionnaireOrderTitleSchema, required: true },
-  showFrom: { type: Date, required: true },
-  showUntil: {
-    type: Date,
-    required: true,
-    validate: [function(value) {
-      return this.showFrom <= value;
-    }, 'showFrom must be before showUntil'],
-  },
-  questions: [questionSchema],
-  condition: { type: conditionSchema, required: true },
-}, { timestamps: true });
-
-export default getOrCreateModel<QuestionnaireOrderDocument, QuestionnaireOrderModel>('QuestionnaireOrder', questionnaireOrderSchema);

+ 0 - 10
apps/app/src/features/questionnaire/server/models/schema/answer.ts

@@ -1,10 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-
-import type { IAnswer } from '../../../interfaces/answer';
-
-export const answerSchema = new Schema<IAnswer<ObjectIdLike>>({
-  question: { type: Schema.Types.ObjectId, ref: 'Question', required: true },
-  value: { type: String, required: true },
-});

+ 0 - 29
apps/app/src/features/questionnaire/server/models/schema/condition.ts

@@ -1,29 +0,0 @@
-import { GrowiServiceType } from '@growi/core/dist/consts';
-import { Schema } from 'mongoose';
-
-import type { ICondition } from '../../../interfaces/condition';
-import { UserType } from '../../../interfaces/user-info';
-
-const conditionSchema = new Schema<ICondition>({
-  user: {
-    types: [{ type: String, enum: Object.values(UserType) }],
-    daysSinceCreation: {
-      moreThanOrEqualTo: { type: Number, min: 0 },
-      lessThanOrEqualTo: {
-        type: Number,
-        min: 0,
-        validate: [
-          function(value) {
-            return this.user.daysSinceCreation.moreThanOrEqualTo == null || this.user.daysSinceCreation.moreThanOrEqualTo <= value;
-          }, 'daysSinceCreation.lessThanOrEqualTo must be greater than moreThanOrEqualTo',
-        ],
-      },
-    },
-  },
-  growi: {
-    types: [{ type: String, enum: Object.values(GrowiServiceType) }],
-    versionRegExps: [String],
-  },
-});
-
-export default conditionSchema;

+ 0 - 42
apps/app/src/features/questionnaire/server/models/schema/growi-info.ts

@@ -1,42 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { GrowiWikiType } from '@growi/core/dist/interfaces';
-import { Schema } from 'mongoose';
-
-import type { IGrowiAppAdditionalInfo } from '~/features/questionnaire/interfaces/growi-app-info';
-import { AttachmentMethodType } from '~/interfaces/attachment';
-import { IExternalAuthProviderType } from '~/interfaces/external-auth-provider';
-
-const growiAdditionalInfoSchema = new Schema<IGrowiAppAdditionalInfo>({
-  installedAt: { type: Date, required: true },
-  installedAtByOldestUser: { type: Date, required: true },
-  currentUsersCount: { type: Number, required: true },
-  currentActiveUsersCount: { type: Number, required: true },
-  attachmentType: { type: String, required: true, enum: Object.values(AttachmentMethodType) },
-  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
-});
-
-export const growiInfoSchema = new Schema<IGrowiInfo<IGrowiAppAdditionalInfo> & IGrowiAppAdditionalInfo>({
-  version: { type: String, required: true },
-  appSiteUrl: { type: String },
-  serviceInstanceId: { type: String, required: true },
-  type: { type: String, required: true, enum: Object.values(GrowiServiceType) },
-  wikiType: { type: String, required: true, enum: Object.values(GrowiWikiType) },
-  osInfo: {
-    type: { type: String },
-    platform: String,
-    arch: String,
-    totalmem: Number,
-  },
-  deploymentType: { type: String, enum: (<(string | null)[]>Object.values(GrowiDeploymentType)).concat([null]) },
-  additionalInfo: growiAdditionalInfoSchema,
-
-  // legacy properties (extracted from additionalInfo for growi-questionnaire)
-  // see: https://gitlab.weseek.co.jp/tech/growi/growi-questionnaire
-  installedAt: { type: Date },
-  installedAtByOldestUser: { type: Date },
-  currentUsersCount: { type: Number },
-  currentActiveUsersCount: { type: Number },
-  attachmentType: { type: String, enum: Object.values(AttachmentMethodType) },
-  activeExternalAccountTypes: [{ type: String, enum: Object.values(IExternalAuthProviderType) }],
-});

+ 0 - 16
apps/app/src/features/questionnaire/server/models/schema/question.ts

@@ -1,16 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { IQuestion } from '../../../interfaces/question';
-import { QuestionType } from '../../../interfaces/question';
-
-const questionTextSchema = new Schema<IQuestion['text']>({
-  ja_JP: { type: String, required: true },
-  en_US: { type: String, required: true },
-}, { _id: false });
-
-const questionSchema = new Schema<IQuestion>({
-  type: { type: String, required: true, enum: Object.values(QuestionType) },
-  text: { type: questionTextSchema, required: true },
-}, { timestamps: true });
-
-export default questionSchema;

+ 0 - 10
apps/app/src/features/questionnaire/server/models/schema/user-info.ts

@@ -1,10 +0,0 @@
-import { Schema } from 'mongoose';
-
-import type { IUserInfo } from '../../../interfaces/user-info';
-import { UserType } from '../../../interfaces/user-info';
-
-export const userInfoSchema = new Schema<IUserInfo>({
-  userIdHash: { type: String },
-  type: { type: String, required: true, enum: Object.values(UserType) },
-  userCreatedAt: { type: Date },
-});

+ 0 - 384
apps/app/src/features/questionnaire/server/routes/apiv3/questionnaire.ts

@@ -1,384 +0,0 @@
-import type { IUserHasId } from '@growi/core';
-import type { Request } from 'express';
-import { Router } from 'express';
-import { body, validationResult } from 'express-validator';
-
-import type Crowi from '~/server/crowi';
-import { accessTokenParser } from '~/server/middlewares/access-token-parser';
-import type { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
-import { configManager } from '~/server/service/config-manager';
-import { growiInfoService } from '~/server/service/growi-info';
-import axios from '~/utils/axios';
-import loggerFactory from '~/utils/logger';
-
-import type { IAnswer } from '../../../interfaces/answer';
-import type { IProactiveQuestionnaireAnswer } from '../../../interfaces/proactive-questionnaire-answer';
-import type { IQuestionnaireAnswer } from '../../../interfaces/questionnaire-answer';
-import { StatusType } from '../../../interfaces/questionnaire-answer-status';
-import ProactiveQuestionnaireAnswer from '../../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../../models/questionnaire-answer-status';
-import { convertToLegacyFormat, getSiteUrlHashed } from '../../util/convert-to-legacy-format';
-
-
-const logger = loggerFactory('growi:routes:apiv3:questionnaire');
-
-const router = Router();
-
-interface AuthorizedRequest extends Request {
-  user?: any
-}
-
-module.exports = (crowi: Crowi): Router => {
-  const loginRequired = require('~/server/middlewares/login-required')(crowi, true);
-
-  const validators = {
-    proactiveAnswer: [
-      body('satisfaction').exists().isNumeric(),
-      body('lengthOfExperience').isString(),
-      body('position').isString(),
-      body('occupation').isString(),
-      body('commentText').exists().isString(),
-    ],
-    answer: [body('questionnaireOrderId').exists().isString(), body('answers').exists().isArray({ min: 1 })],
-    skipDeny: [body('questionnaireOrderId').exists().isString()],
-  };
-
-  const changeAnswerStatus = async(user, questionnaireOrderId, status) => {
-    const result = await QuestionnaireAnswerStatus.updateOne({
-      user: { $eq: user },
-      questionnaireOrderId: { $eq: questionnaireOrderId },
-    }, {
-      status,
-    }, { upsert: true });
-
-    if (result.modifiedCount === 1) {
-      return 204;
-    }
-    if (result.upsertedCount === 1) {
-      return 201;
-    }
-    return 404;
-  };
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/orders:
-   *   get:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/orders
-   *     description: Get questionnaire orders
-   *     responses:
-   *       200:
-   *         description: OK
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *               properties:
-   *                 questionnaireOrders:
-   *                   type: array
-   *                   items:
-   *                     type: object
-   */
-  router.get('/orders', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const growiInfo = await growiInfoService.getGrowiInfo(true);
-    const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-    try {
-      const questionnaireOrders = await crowi.questionnaireService!.getQuestionnaireOrdersToShow(userInfo, growiInfo, req.user?._id ?? null);
-
-      return res.apiv3({ questionnaireOrders });
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/is-enabled:
-   *   get:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/is-enabled
-   *     description: Get questionnaire is enabled
-   *     responses:
-   *       200:
-   *         description: OK
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *               properties:
-   *                 isEnabled:
-   *                   type: boolean
-   */
-  router.get('/is-enabled', accessTokenParser, loginRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const isEnabled = configManager.getConfig('questionnaire:isQuestionnaireEnabled');
-    return res.apiv3({ isEnabled });
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/proactive/answer:
-   *   post:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/proactive/answer
-   *     description: Post proactive questionnaire answer
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       200:
-   *         description: Success
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   */
-  router.post('/proactive/answer', accessTokenParser, loginRequired, validators.proactiveAnswer, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const sendQuestionnaireAnswer = async() => {
-      const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
-      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-      const growiInfo = await growiInfoService.getGrowiInfo(true);
-      const userInfo = crowi.questionnaireService.getUserInfo(req.user ?? null, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-      const proactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
-        satisfaction: req.body.satisfaction,
-        lengthOfExperience: req.body.lengthOfExperience,
-        position: req.body.position,
-        occupation: req.body.occupation,
-        commentText: req.body.commentText,
-        growiInfo,
-        userInfo,
-        answeredAt: new Date(),
-      };
-
-      const proactiveQuestionnaireAnswerLegacy = convertToLegacyFormat(proactiveQuestionnaireAnswer, isAppSiteUrlHashed);
-
-      try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive`, proactiveQuestionnaireAnswerLegacy);
-      }
-      catch (err) {
-        if (err.request != null) {
-          // when failed to send, save to resend in cronjob
-          await ProactiveQuestionnaireAnswer.create(proactiveQuestionnaireAnswer);
-        }
-        else {
-          throw err;
-        }
-      }
-    };
-
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      await sendQuestionnaireAnswer();
-      return res.apiv3({});
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/answer:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/answer
-   *     description: Post questionnaire answer
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/answer', accessTokenParser, loginRequired, validators.answer, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const sendQuestionnaireAnswer = async(user: IUserHasId, answers: IAnswer[]) => {
-      const questionnaireServerOrigin = crowi.configManager.getConfig('app:questionnaireServerOrigin');
-      const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-      const growiInfo = await growiInfoService.getGrowiInfo(true);
-      const userInfo = crowi.questionnaireService.getUserInfo(user, getSiteUrlHashed(growiInfo.appSiteUrl));
-
-      const questionnaireAnswer: IQuestionnaireAnswer = {
-        growiInfo,
-        userInfo,
-        answers,
-        answeredAt: new Date(),
-        questionnaireOrder: req.body.questionnaireOrderId,
-      };
-
-      const questionnaireAnswerLegacy = convertToLegacyFormat(questionnaireAnswer, isAppSiteUrlHashed);
-
-      try {
-        await axios.post(`${questionnaireServerOrigin}/questionnaire-answer`, questionnaireAnswerLegacy);
-      }
-      catch (err) {
-        if (err.request != null) {
-          // when failed to send, save to resend in cronjob
-          await QuestionnaireAnswer.create(questionnaireAnswer);
-        }
-        else {
-          throw err;
-        }
-      }
-    };
-
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      await sendQuestionnaireAnswer(req.user ?? null, req.body.answers);
-      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.answered);
-      return res.apiv3({}, status);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/skip:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/skip
-   *     description: Skip questionnaire
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/skip', accessTokenParser, loginRequired, validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.skipped);
-      return res.apiv3({}, status);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  /**
-   * @swagger
-   *
-   * /questionnaire/deny:
-   *   put:
-   *     tags: [Questionnaire]
-   *     security:
-   *       - bearer: []
-   *       - accessTokenInQuery: []
-   *     summary: /questionnaire/deny
-   *     description: Deny questionnaire
-   *     requestBody:
-   *       required: true
-   *       content:
-   *         application/json:
-   *           schema:
-   *             type: object
-   *     responses:
-   *       201:
-   *         description: Created
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       204:
-   *         description: No Content
-   *         content:
-   *           application/json:
-   *             schema:
-   *               type: object
-   *       404:
-   *         description: Not Found
-   */
-  router.put('/deny', accessTokenParser, loginRequired, validators.skipDeny, async(req: AuthorizedRequest, res: ApiV3Response) => {
-    const errors = validationResult(req);
-    if (!errors.isEmpty()) {
-      return res.status(400).json({ errors: errors.array() });
-    }
-
-    try {
-      const status = await changeAnswerStatus(req.user, req.body.questionnaireOrderId, StatusType.denied);
-      return res.apiv3({}, status);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  return router;
-
-};

+ 0 - 515
apps/app/src/features/questionnaire/server/service/questionnaire-cron.integ.ts

@@ -1,515 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType, GrowiWikiType } from '@growi/core';
-// eslint-disable-next-line no-restricted-imports
-import axios from 'axios';
-import mongoose from 'mongoose';
-
-import { configManager } from '~/server/service/config-manager';
-
-import { AttachmentMethodType } from '../../../../interfaces/attachment';
-import type {
-  IProactiveQuestionnaireAnswer, IProactiveQuestionnaireAnswerLegacy,
-} from '../../interfaces/proactive-questionnaire-answer';
-import type { IQuestionnaireAnswer, IQuestionnaireAnswerLegacy } from '../../interfaces/questionnaire-answer';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import ProactiveQuestionnaireAnswer from '../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-
-import questionnaireCronService from './questionnaire-cron';
-
-// TODO: use actual user model after ~/server/models/user.js becomes importable in vitest
-// ref: https://github.com/vitest-dev/vitest/issues/846
-const userSchema = new mongoose.Schema({
-  name: { type: String },
-  username: { type: String, required: true, unique: true },
-  email: { type: String, unique: true, sparse: true },
-}, {
-  timestamps: true,
-});
-const User = mongoose.model('User', userSchema);
-
-describe('QuestionnaireCronService', () => {
-  const mockResponse = {
-    data: {
-      questionnaireOrders: [
-        // saved in db、not finished (user.types is updated from the time it was saved)
-        {
-          _id: '63a8354837e7aa378e16f0b1',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2022-12-11',
-          showUntil: '2100-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'GROWI は使いやすいですか?',
-                en_US: 'Is GROWI easy to use?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['admin', 'general'],
-            },
-            growi: {
-              types: ['cloud', 'private-cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-01',
-          updatedAt: '2022-12-01',
-          __v: 0,
-        },
-        // not saved, not finished
-        {
-          _id: '63a8354837e7aa378e16f0b2',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2021-12-11',
-          showUntil: '2100-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'アンケート機能は正常動作していますか?',
-                en_US: 'Is this questionnaire functioning properly?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-02',
-          updatedAt: '2022-12-02',
-          __v: 0,
-        },
-        // not saved, finished
-        {
-          _id: '63a8354837e7aa378e16f0b3',
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: '2021-12-11',
-          showUntil: '2021-12-12',
-          questions: [
-            {
-              type: 'points',
-              text: {
-                ja_JP: 'これはいい質問ですか?',
-                en_US: 'Is this a good question?',
-              },
-            },
-          ],
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-            },
-          },
-          createdAt: '2022-12-03',
-          updatedAt: '2022-12-03',
-          __v: 0,
-        },
-      ],
-    },
-  };
-
-  beforeAll(async() => {
-    await configManager.loadConfigs();
-    await configManager.updateConfig('app:questionnaireCronMaxHoursUntilRequest', 0);
-    await User.create({
-      name: 'Example for Questionnaire Service Test',
-      username: 'questionnaire cron test user',
-      email: 'questionnaireCronTestUser@example.com',
-      createdAt: '2020-01-01',
-    });
-  });
-
-  beforeEach(async() => {
-    // insert initial db data
-    await QuestionnaireOrder.insertMany([
-      {
-        _id: '63a8354837e7aa378e16f0b1',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11',
-        showUntil: '2100-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'GROWI は使いやすいですか?',
-              en_US: 'Is GROWI easy to use?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud', 'private-cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-      // finished
-      {
-        _id: '63a8354837e7aa378e16f0b4',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2020-12-11',
-        showUntil: '2021-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'ver 2.0 は 1.0 より良いですか?',
-              en_US: 'Is ver 2.0 better than 1.0?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-      // questionnaire that doesn't exist in questionnaire server
-      {
-        _id: '63a8354837e7aa378e16f0b5',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2020-12-11',
-        showUntil: '2100-12-12',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: '新しいデザインは良いですか?',
-              en_US: 'How would you rate the latest design?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-      },
-    ]);
-
-    await QuestionnaireAnswerStatus.insertMany([
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.skipped,
-      },
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.answered,
-      },
-      {
-        user: new mongoose.Types.ObjectId(),
-        questionnaireOrderId: '63a8354837e7aa378e16f0b1',
-        status: StatusType.not_answered,
-      },
-    ]);
-
-    const validQuestionnaireAnswer: IQuestionnaireAnswer = {
-      answers: [{
-        question: '63c6da88143e531d95346188',
-        value: '1',
-      }],
-      answeredAt: new Date(),
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        additionalInfo: {
-          installedAt: new Date('2000-01-01'),
-          installedAtByOldestUser: new Date('2020-01-01'),
-          currentUsersCount: 100,
-          currentActiveUsersCount: 50,
-          attachmentType: AttachmentMethodType.aws,
-        },
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      questionnaireOrder: '63a8354837e7aa378e16f0b1',
-    };
-
-    const validQuestionnaireAnswerLegacy: IQuestionnaireAnswerLegacy = {
-      answers: [{
-        question: '63c6da88143e531d95346188',
-        value: '1',
-      }],
-      answeredAt: new Date(),
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        appSiteUrlHashed: 'hashed',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        installedAt: new Date('2000-01-01'),
-        installedAtByOldestUser: new Date('2020-01-01'),
-        currentUsersCount: 100,
-        currentActiveUsersCount: 50,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        attachmentType: AttachmentMethodType.aws,
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      questionnaireOrder: '63a8354837e7aa378e16f0b1',
-    };
-
-    await QuestionnaireAnswer.insertMany([
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswer,
-      validQuestionnaireAnswerLegacy,
-      validQuestionnaireAnswerLegacy,
-    ]);
-
-    const validProactiveQuestionnaireAnswer: IProactiveQuestionnaireAnswer = {
-      satisfaction: 1,
-      commentText: 'answer text',
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        additionalInfo: {
-          installedAt: new Date('2000-01-01'),
-          installedAtByOldestUser: new Date('2020-01-01'),
-          currentUsersCount: 100,
-          currentActiveUsersCount: 50,
-          attachmentType: AttachmentMethodType.aws,
-        },
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      answeredAt: new Date(),
-    };
-    const validProactiveQuestionnaireAnswerLegacy: IProactiveQuestionnaireAnswerLegacy = {
-      satisfaction: 1,
-      commentText: 'answer text',
-      growiInfo: {
-        version: '1.0',
-        appSiteUrl: 'https://example.com',
-        appSiteUrlHashed: 'hashed',
-        serviceInstanceId: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
-        type: GrowiServiceType.cloud,
-        wikiType: GrowiWikiType.open,
-        deploymentType: GrowiDeploymentType.others,
-        osInfo: {
-          type: 'Linux',
-          platform: 'linux',
-          arch: 'x64',
-          totalmem: 8589934592,
-        },
-        // legacy properties
-        installedAt: new Date('2000-01-01'),
-        installedAtByOldestUser: new Date('2020-01-01'),
-        currentUsersCount: 100,
-        currentActiveUsersCount: 50,
-        attachmentType: AttachmentMethodType.aws,
-      },
-      userInfo: {
-        userIdHash: '542bcc3bc5bc61b840017a18',
-        type: 'general',
-        userCreatedAt: new Date(),
-      },
-      answeredAt: new Date(),
-    };
-
-    await ProactiveQuestionnaireAnswer.insertMany([
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswer,
-      validProactiveQuestionnaireAnswerLegacy,
-      validProactiveQuestionnaireAnswerLegacy,
-    ]);
-
-    questionnaireCronService.startCron();
-
-    vi.spyOn(axios, 'get').mockResolvedValue(mockResponse);
-    vi.spyOn(axios, 'post').mockResolvedValue({ data: { result: 'success' } });
-  });
-
-  afterAll(() => {
-    questionnaireCronService.stopCron(); // vitest will not finish until cronjob stops
-  });
-
-  test('Job execution should save(update) quesionnaire orders, delete outdated ones, update skipped answer statuses, and delete resent answers', async() => {
-    // testing the cronjob from schedule has untrivial overhead, so test job execution in place
-    await questionnaireCronService.executeJob();
-
-    const savedOrders = await QuestionnaireOrder.find()
-      .select('-condition._id -questions._id -questions.createdAt -questions.updatedAt')
-      .sort({ _id: 1 });
-
-    expect(JSON.parse(JSON.stringify(savedOrders))).toEqual([
-      {
-        _id: '63a8354837e7aa378e16f0b1',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11T00:00:00.000Z',
-        showUntil: '2100-12-12T00:00:00.000Z',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'GROWI は使いやすいですか?',
-              en_US: 'Is GROWI easy to use?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['admin', 'general'],
-          },
-          growi: {
-            types: ['cloud', 'private-cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-        createdAt: '2022-12-01T00:00:00.000Z',
-        updatedAt: '2022-12-01T00:00:00.000Z',
-        __v: 0,
-      },
-      {
-        _id: '63a8354837e7aa378e16f0b2',
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2021-12-11T00:00:00.000Z',
-        showUntil: '2100-12-12T00:00:00.000Z',
-        questions: [
-          {
-            type: 'points',
-            text: {
-              ja_JP: 'アンケート機能は正常動作していますか?',
-              en_US: 'Is this questionnaire functioning properly?',
-            },
-          },
-        ],
-        condition: {
-          user: {
-            types: ['general'],
-          },
-          growi: {
-            types: ['cloud'],
-            versionRegExps: ['2\\.0\\.[0-9]', '1\\.9\\.[0-9]'],
-          },
-        },
-        createdAt: '2022-12-02T00:00:00.000Z',
-        updatedAt: '2022-12-02T00:00:00.000Z',
-        __v: 0,
-      },
-    ]);
-
-    expect((await QuestionnaireAnswerStatus.find({ status: StatusType.not_answered })).length).toEqual(2);
-    expect((await QuestionnaireAnswer.find()).length).toEqual(0);
-    expect((await ProactiveQuestionnaireAnswer.find()).length).toEqual(0);
-  });
-});

+ 0 - 106
apps/app/src/features/questionnaire/server/service/questionnaire-cron.ts

@@ -1,106 +0,0 @@
-import axiosRetry from 'axios-retry';
-
-import { configManager } from '~/server/service/config-manager';
-import CronService from '~/server/service/cron';
-import { getRandomIntInRange } from '~/utils/rand';
-
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-import ProactiveQuestionnaireAnswer from '../models/proactive-questionnaire-answer';
-import QuestionnaireAnswer from '../models/questionnaire-answer';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-import { convertToLegacyFormat } from '../util/convert-to-legacy-format';
-
-const axios = require('axios').default;
-
-axiosRetry(axios, { retries: 3 });
-
-/**
- * Manages cronjob which
- *  1. fetches QuestionnaireOrders from questionnaire server
- *  2. updates QuestionnaireOrder collection to contain only the ones that exist in the fetched list and is not finished (doesn't have to be started)
- *  3. changes QuestionnaireAnswerStatuses which are 'skipped' to 'not_answered'
- *  4. resend QuestionnaireAnswers & ProactiveQuestionnaireAnswers which failed to reach questionnaire server
- */
-class QuestionnaireCronService extends CronService {
-
-  sleep = (msec: number): Promise<void> => new Promise(resolve => setTimeout(resolve, msec));
-
-  override getCronSchedule(): string {
-    return configManager.getConfig('app:questionnaireCronSchedule');
-  }
-
-  override async executeJob(): Promise<void> {
-    // sleep for a random amount to scatter request time from GROWI apps to questionnaire server
-    await this.sleepBeforeJob();
-
-    const questionnaireServerOrigin = configManager.getConfig('app:questionnaireServerOrigin');
-    const isAppSiteUrlHashed = configManager.getConfig('questionnaire:isAppSiteUrlHashed');
-
-    const fetchQuestionnaireOrders = async(): Promise<IQuestionnaireOrder[]> => {
-      const response = await axios.get(`${questionnaireServerOrigin}/questionnaire-order/index`);
-      return response.data.questionnaireOrders;
-    };
-
-    const saveUnfinishedOrders = async(questionnaireOrders: IQuestionnaireOrder[]) => {
-      const currentDate = new Date(Date.now());
-      const unfinishedOrders = questionnaireOrders.filter(order => new Date(order.showUntil) > currentDate);
-      await QuestionnaireOrder.insertMany(unfinishedOrders);
-    };
-
-    const changeSkippedAnswerStatusToNotAnswered = async() => {
-      await QuestionnaireAnswerStatus.updateMany(
-        { status: StatusType.skipped },
-        { status: StatusType.not_answered },
-      );
-    };
-
-    const resendQuestionnaireAnswers = async() => {
-      const questionnaireAnswers = await QuestionnaireAnswer.find()
-        .select('-_id -answers._id  -growiInfo._id -userInfo._id')
-        .lean();
-      const proactiveQuestionnaireAnswers = await ProactiveQuestionnaireAnswer.find()
-        .select('-_id -growiInfo._id -userInfo._id')
-        .lean();
-
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/batch`, {
-        // convert to legacy format
-        questionnaireAnswers: questionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
-      })
-        .then(async() => {
-          await QuestionnaireAnswer.deleteMany();
-        });
-      axios.post(`${questionnaireServerOrigin}/questionnaire-answer/proactive/batch`, {
-        // convert to legacy format
-        proactiveQuestionnaireAnswers: proactiveQuestionnaireAnswers.map(answer => convertToLegacyFormat(answer, isAppSiteUrlHashed)),
-      })
-        .then(async() => {
-          await ProactiveQuestionnaireAnswer.deleteMany();
-        });
-    };
-
-    const questionnaireOrders: IQuestionnaireOrder[] = await fetchQuestionnaireOrders();
-
-    resendQuestionnaireAnswers();
-
-    // reset QuestionnaireOrder collection and save unfinished ones that exist on questionnaire server
-    await QuestionnaireOrder.deleteMany();
-    await saveUnfinishedOrders(questionnaireOrders);
-
-    await changeSkippedAnswerStatusToNotAnswered();
-  }
-
-  private async sleepBeforeJob() {
-    const maxHoursUntilRequest = configManager.getConfig('app:questionnaireCronMaxHoursUntilRequest');
-    const maxSecondsUntilRequest = maxHoursUntilRequest * 60 * 60;
-
-    const secToSleep = getRandomIntInRange(0, maxSecondsUntilRequest);
-    await this.sleep(secToSleep * 1000);
-  }
-
-}
-
-const questionnaireCronService = new QuestionnaireCronService();
-
-export default questionnaireCronService;

+ 0 - 301
apps/app/src/features/questionnaire/server/service/questionnaire.integ.ts

@@ -1,301 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { mock } from 'vitest-mock-extended';
-
-import pkg from '^/package.json';
-
-
-import type UserEvent from '~/server/events/user';
-import { configManager } from '~/server/service/config-manager';
-
-import type Crowi from '../../../../server/crowi';
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import { UserType } from '../../interfaces/user-info';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import QuestionnaireOrder from '../models/questionnaire-order';
-
-import QuestionnaireService from './questionnaire';
-
-
-describe('QuestionnaireService', () => {
-  const appVersion = pkg.version;
-
-  let questionnaireService: QuestionnaireService;
-
-  let User;
-  let user;
-
-  beforeAll(async() => {
-
-    await configManager.loadConfigs();
-
-    const crowiMock = mock<Crowi>({
-      version: appVersion,
-      event: vi.fn().mockImplementation((eventName) => {
-        if (eventName === 'user') {
-          return mock<UserEvent>({
-            on: vi.fn(),
-          });
-        }
-      }),
-    });
-    const userModelFactory = (await import('~/server/models/user')).default;
-    User = userModelFactory(crowiMock);
-
-    await User.deleteMany({}); // clear users
-    user = await User.create({
-      name: 'Example for Questionnaire Service Test',
-      username: 'questionnaire test user',
-      email: 'questionnaireTestUser@example.com',
-      password: 'usertestpass',
-      createdAt: '2000-01-01',
-    });
-
-    questionnaireService = new QuestionnaireService(crowiMock);
-  });
-
-  describe('getUserInfo', () => {
-    test('Should get correct user info when user given', () => {
-      const userInfo = questionnaireService.getUserInfo(user, 'growiurlhashfortest');
-      expect(userInfo).not.toBeNull();
-      assert(userInfo != null);
-
-      expect(userInfo.type).equal(UserType.general);
-      assert(userInfo.type === UserType.general);
-
-      expect(userInfo.userIdHash).toBeTruthy();
-      expect(userInfo.userIdHash).not.toBe(user._id);
-
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      delete (userInfo as any).userIdHash;
-
-      expect(userInfo).toEqual({ type: 'general', userCreatedAt: new Date('2000-01-01') });
-    });
-
-    test('Should get correct user info when user is null', () => {
-      const userInfo = questionnaireService.getUserInfo(null, '');
-      expect(userInfo).toEqual({ type: 'guest' });
-    });
-  });
-
-  describe('getQuestionnaireOrdersToShow', () => {
-    let doc1;
-    let doc2;
-    let doc3;
-    let doc4;
-    let doc5;
-    let doc6;
-    let doc7;
-    let doc8;
-    let doc9;
-    let doc10;
-    let doc11;
-    let doc12;
-
-    beforeAll(async() => {
-      const questionnaireToBeShown = {
-        shortTitle: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        title: {
-          ja_JP: 'GROWI に関するアンケート',
-          en_US: 'Questions about GROWI',
-        },
-        showFrom: '2022-12-11',
-        showUntil: '2100-12-12',
-        condition: {
-          user: {
-            types: ['general'],
-            daysSinceCreation: {
-              moreThanOrEqualTo: 365,
-              lessThanOrEqualTo: 365 * 1000,
-            },
-          },
-          growi: {
-            types: ['on-premise'],
-            versionRegExps: [appVersion],
-          },
-        },
-        createdAt: '2023-01-01',
-        updatedAt: '2023-01-01',
-      };
-
-      // insert initial db data
-      doc1 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert finished data
-      doc2 = await QuestionnaireOrder.create({
-        ...questionnaireToBeShown,
-        showFrom: '2020-12-11',
-        showUntil: '2021-12-12',
-      });
-      // insert data for admin or guest
-      doc3 = await QuestionnaireOrder.create({
-        ...questionnaireToBeShown,
-        condition: {
-          user: {
-            types: ['admin', 'guest'],
-          },
-          growi: {
-            types: ['on-premise'],
-            versionRegExps: [appVersion],
-          },
-        },
-      });
-      // insert answered data
-      doc4 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert skipped data
-      doc5 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert denied data
-      doc6 = await QuestionnaireOrder.create(questionnaireToBeShown);
-      // insert data for different growi type
-      doc7 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['cloud'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for different growi version
-      doc8 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: ['1.0.0-alpha'],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for less than or equal to a year
-      doc9 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                lessThanOrEqualTo: 365,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for more than or equal to 1000 years
-      doc10 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 365 * 1000,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-      // insert data for users that used GROWI for more than a month and less than 6 months
-      doc11 = await QuestionnaireOrder.create(
-        {
-          ...questionnaireToBeShown,
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 30,
-                lessThanOrEqualTo: 30 * 6,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-        },
-      );
-
-      await QuestionnaireAnswerStatus.insertMany([
-        {
-          user: user._id,
-          questionnaireOrderId: doc4._id,
-          status: StatusType.answered,
-        },
-        {
-          user: user._id,
-          questionnaireOrderId: doc5._id,
-          status: StatusType.skipped,
-        },
-        {
-          user: user._id,
-          questionnaireOrderId: doc6._id,
-          status: StatusType.skipped,
-        },
-      ]);
-    });
-
-    test('Should get questionnaire orders to show', async() => {
-      const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
-        type: 'on-premise',
-        version: appVersion,
-      });
-      const userInfo = questionnaireService.getUserInfo(user, 'appSiteUrlHashed');
-
-      const questionnaireOrderDocuments = await questionnaireService.getQuestionnaireOrdersToShow(userInfo, growiInfo, user._id);
-
-      expect(questionnaireOrderDocuments[0].toObject()).toMatchObject(
-        {
-          __v: 0,
-          shortTitle: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          title: {
-            ja_JP: 'GROWI に関するアンケート',
-            en_US: 'Questions about GROWI',
-          },
-          showFrom: new Date('2022-12-11'),
-          showUntil: new Date('2100-12-12'),
-          questions: [],
-          condition: {
-            user: {
-              types: ['general'],
-              daysSinceCreation: {
-                moreThanOrEqualTo: 365,
-                lessThanOrEqualTo: 365 * 1000,
-              },
-            },
-            growi: {
-              types: ['on-premise'],
-              versionRegExps: [appVersion],
-            },
-          },
-          createdAt: new Date('2023-01-01'),
-          updatedAt: new Date('2023-01-01'),
-        },
-      );
-
-    });
-
-  });
-
-});

+ 0 - 74
apps/app/src/features/questionnaire/server/service/questionnaire.ts

@@ -1,74 +0,0 @@
-import crypto from 'crypto';
-
-import type { IUserHasId } from '@growi/core';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type Crowi from '~/server/crowi';
-import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import loggerFactory from '~/utils/logger';
-
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import { StatusType } from '../../interfaces/questionnaire-answer-status';
-import { type IUserInfo, UserType } from '../../interfaces/user-info';
-import QuestionnaireAnswerStatus from '../models/questionnaire-answer-status';
-import type { QuestionnaireOrderDocument } from '../models/questionnaire-order';
-import QuestionnaireOrder from '../models/questionnaire-order';
-import { isShowableCondition } from '../util/condition';
-
-
-const logger = loggerFactory('growi:service:questionnaire');
-
-class QuestionnaireService {
-
-  crowi: Crowi;
-
-  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(crowi: Crowi) {
-    this.crowi = crowi;
-  }
-
-  getUserInfo(user: IUserHasId | null, appSiteUrlHashed: string): IUserInfo {
-    if (user != null) {
-      const hasher = crypto.createHmac('sha256', appSiteUrlHashed);
-      hasher.update(user._id.toString());
-
-      return {
-        userIdHash: hasher.digest('hex'),
-        type: user.admin ? UserType.admin : UserType.general,
-        userCreatedAt: user.createdAt,
-      };
-    }
-
-    return { type: UserType.guest };
-  }
-
-  async getQuestionnaireOrdersToShow(
-      userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>, userId: ObjectIdLike | null,
-  ): Promise<QuestionnaireOrderDocument[]> {
-    const currentDate = new Date();
-
-    let questionnaireOrders = await QuestionnaireOrder.find({
-      showUntil: {
-        $gte: currentDate,
-      },
-    });
-
-    if (userId != null) {
-      const statuses = await QuestionnaireAnswerStatus.find({ userId, questionnaireOrderId: { $in: questionnaireOrders.map(d => d._id) } });
-
-      questionnaireOrders = questionnaireOrders.filter((order) => {
-        const status = statuses.find(s => s.questionnaireOrderId.toString() === order._id.toString());
-
-        return !status || status?.status === StatusType.not_answered;
-      });
-    }
-
-    return questionnaireOrders
-      .filter((order) => {
-        return isShowableCondition(order, userInfo, growiInfo);
-      });
-  }
-
-}
-
-export default QuestionnaireService;

+ 0 - 70
apps/app/src/features/questionnaire/server/util/condition.ts

@@ -1,70 +0,0 @@
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-
-import type { ICondition } from '../../interfaces/condition';
-import type { IGrowiAppAdditionalInfo } from '../../interfaces/growi-app-info';
-import type { IQuestionnaireOrder } from '../../interfaces/questionnaire-order';
-import type { IUserInfo } from '../../interfaces/user-info';
-import { UserType } from '../../interfaces/user-info';
-
-
-const checkUserInfo = (condition: ICondition, userInfo: IUserInfo): boolean => {
-  const { user: { types, daysSinceCreation } } = condition;
-
-  if (!types.includes(userInfo.type)) {
-    return false;
-  }
-
-  // Check if "time passed since user creation" is between specified range
-  if (userInfo.type !== UserType.guest) {
-    const createdAt = userInfo.userCreatedAt;
-    const moreThanOrEqualTo = daysSinceCreation?.moreThanOrEqualTo;
-    const lessThanOrEqualTo = daysSinceCreation?.lessThanOrEqualTo;
-    const currentDate = new Date();
-
-    const isValidLeftThreshold = (() => {
-      if (moreThanOrEqualTo == null) {
-        return true;
-      }
-      const leftThreshold = new Date(createdAt.getTime() + 60 * 1000 * 60 * 24 * moreThanOrEqualTo);
-      return leftThreshold <= currentDate;
-    })();
-    const isValidRightThreshold = (() => {
-      if (lessThanOrEqualTo == null) {
-        return true;
-      }
-      const rightThreshold = new Date(createdAt.getTime() + 60 * 1000 * 60 * 24 * lessThanOrEqualTo);
-      return currentDate <= rightThreshold;
-    })();
-
-    return isValidLeftThreshold && isValidRightThreshold;
-  }
-
-  return true;
-};
-
-const checkGrowiInfo = (condition: ICondition, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
-  const { growi: { types, versionRegExps } } = condition;
-
-  if (!types.includes(growiInfo.type)) {
-    return false;
-  }
-
-  if (!versionRegExps.some(rs => new RegExp(rs).test(growiInfo.version))) {
-    return false;
-  }
-
-  return true;
-};
-
-export const isShowableCondition = (order: IQuestionnaireOrder, userInfo: IUserInfo, growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo>): boolean => {
-  const { condition } = order;
-
-  if (!checkUserInfo(condition, userInfo)) {
-    return false;
-  }
-  if (!checkGrowiInfo(condition, growiInfo)) {
-    return false;
-  }
-
-  return true;
-};

+ 0 - 128
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.spec.ts

@@ -1,128 +0,0 @@
-import { GrowiDeploymentType, GrowiServiceType } from '@growi/core/dist/consts';
-import type { IGrowiInfo } from '@growi/core/dist/interfaces';
-import { GrowiWikiType } from '@growi/core/dist/interfaces';
-import {
-  describe, test, expect,
-} from 'vitest';
-import { mock } from 'vitest-mock-extended';
-
-import { AttachmentMethodType } from '../../../../interfaces/attachment';
-import type { IGrowiAppAdditionalInfo, IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
-
-import { convertToLegacyFormat } from './convert-to-legacy-format';
-
-describe('convertToLegacyFormat', () => {
-  test('should return same object when input is already in legacy format', () => {
-    const growiInfoLegacy: IGrowiAppInfoLegacy = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-
-      // legacy properties
-      installedAt: new Date(),
-      installedAtByOldestUser: new Date(),
-      currentUsersCount: 1,
-      currentActiveUsersCount: 1,
-      attachmentType: AttachmentMethodType.local,
-    };
-
-    const legacyData = {
-      someData: 'test',
-      growiInfo: growiInfoLegacy,
-    };
-
-    const result = convertToLegacyFormat(legacyData);
-    expect(result).toStrictEqual(legacyData);
-  });
-
-  test('should convert new format to legacy format', () => {
-    const installedAt = new Date();
-    const installedAtByOldestUser = new Date();
-
-    const growiInfo: IGrowiInfo<IGrowiAppAdditionalInfo> = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-      additionalInfo: {
-        installedAt,
-        installedAtByOldestUser,
-        currentUsersCount: 1,
-        currentActiveUsersCount: 1,
-        attachmentType: AttachmentMethodType.local,
-      },
-    };
-    const newFormatData = {
-      someData: 'test',
-      growiInfo,
-    };
-
-    const growiInfoLegacy: IGrowiAppInfoLegacy = {
-      version: '1.0.0',
-      appSiteUrl: 'https://example.com',
-      appSiteUrlHashed: '100680ad546ce6a577f42f52df33b4cfdca756859e664b8d7de329b150d09ce9',
-      serviceInstanceId: 'service-instance-id',
-      type: GrowiServiceType.cloud,
-      wikiType: GrowiWikiType.open,
-      deploymentType: GrowiDeploymentType.others,
-      osInfo: {
-        type: 'Linux',
-        platform: 'linux',
-        arch: 'x64',
-        totalmem: 8589934592,
-      },
-
-      // legacy properties
-      installedAt,
-      installedAtByOldestUser,
-      currentUsersCount: 1,
-      currentActiveUsersCount: 1,
-      attachmentType: AttachmentMethodType.local,
-    };
-    const expected = {
-      someData: 'test',
-      growiInfo: growiInfoLegacy,
-    };
-
-    const result = convertToLegacyFormat(newFormatData);
-    expect(result).toStrictEqual(expected);
-  });
-
-  test('should convert new format and omit appSiteUrl', () => {
-    // arrange
-    const growiInfo = mock<IGrowiInfo<IGrowiAppAdditionalInfo>>({
-      appSiteUrl: 'https://example.com',
-      additionalInfo: {
-        installedAt: new Date(),
-        installedAtByOldestUser: new Date(),
-        currentUsersCount: 1,
-        currentActiveUsersCount: 1,
-        attachmentType: AttachmentMethodType.local,
-      },
-    });
-
-    // act
-    const result = convertToLegacyFormat({ growiInfo }, true);
-
-    // assert
-    expect(result.growiInfo.appSiteUrl).toBeUndefined();
-  });
-});

+ 0 - 40
apps/app/src/features/questionnaire/server/util/convert-to-legacy-format.ts

@@ -1,40 +0,0 @@
-import assert from 'assert';
-import crypto from 'crypto';
-
-import type { IGrowiAppInfoLegacy } from '../../interfaces/growi-app-info';
-
-
-type IHasGrowiAppInfoLegacy<T> = T & {
-  growiInfo: IGrowiAppInfoLegacy;
-};
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-function isLegacy<T extends { growiInfo: any }>(data: T): data is IHasGrowiAppInfoLegacy<T> {
-  return !('additionalInfo' in data.growiInfo);
-}
-
-export function getSiteUrlHashed(siteUrl: string): string {
-  const hasher = crypto.createHash('sha256');
-  hasher.update(siteUrl);
-  return hasher.digest('hex');
-}
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function convertToLegacyFormat<T extends { growiInfo: any }>(questionnaireAnswer: T, isAppSiteUrlHashed = false): IHasGrowiAppInfoLegacy<T> {
-  if (isLegacy(questionnaireAnswer)) {
-    return questionnaireAnswer;
-  }
-
-  const { additionalInfo, appSiteUrl, ...rest } = questionnaireAnswer.growiInfo;
-  assert(additionalInfo != null);
-
-  return {
-    ...questionnaireAnswer,
-    growiInfo: {
-      appSiteUrl: isAppSiteUrlHashed ? undefined : appSiteUrl,
-      appSiteUrlHashed: getSiteUrlHashed(appSiteUrl),
-      ...rest,
-      ...additionalInfo,
-    },
-  };
-}

+ 0 - 3
apps/app/src/interfaces/activity.ts

@@ -78,7 +78,6 @@ const ACTION_ADMIN_MAIL_SMTP_UPDATE = 'ADMIN_MAIL_SMTP_UPDATE';
 const ACTION_ADMIN_MAIL_SES_UPDATE = 'ADMIN_MAIL_SES_UPDATE';
 const ACTION_ADMIN_MAIL_TEST_SUBMIT = 'ADMIN_MAIL_TEST_SUBMIT';
 const ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE = 'ADMIN_FILE_UPLOAD_CONFIG_UPDATE';
-const ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE = 'ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE';
 const ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE = 'ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE';
 const ACTION_ADMIN_MAINTENANCEMODE_ENABLED = 'ADMIN_MAINTENANCEMODE_ENABLED';
 const ACTION_ADMIN_MAINTENANCEMODE_DISABLED = 'ADMIN_MAINTENANCEMODE_DISABLED';
@@ -259,7 +258,6 @@ export const SupportedAction = {
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
-  ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE,
   ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,
@@ -454,7 +452,6 @@ export const LargeActionGroup = {
   ACTION_ADMIN_MAIL_SES_UPDATE,
   ACTION_ADMIN_MAIL_TEST_SUBMIT,
   ACTION_ADMIN_FILE_UPLOAD_CONFIG_UPDATE,
-  ACTION_ADMIN_QUESTIONNAIRE_SETTINGS_UPDATE,
   ACTION_ADMIN_PAGE_BULK_EXPORT_SETTINGS_UPDATE,
   ACTION_ADMIN_MAINTENANCEMODE_ENABLED,
   ACTION_ADMIN_MAINTENANCEMODE_DISABLED,

+ 0 - 1
apps/app/src/interfaces/res/admin/app-settings.ts

@@ -55,7 +55,6 @@ export type IResAppSettings = {
 
   isEnabledPlugins: boolean,
 
-  isQuestionnaireEnabled: boolean,
   isAppSiteUrlHashed: boolean,
 
   isMaintenanceMode: boolean,

+ 0 - 2
apps/app/src/pages/[[...path]].page.tsx

@@ -91,7 +91,6 @@ const TemplateModal = dynamic(() => import('~/client/components/TemplateModal').
 const LinkEditModal = dynamic(() => import('~/client/components/PageEditor/LinkEditModal').then(mod => mod.LinkEditModal), { ssr: false });
 const TagEditModal = dynamic(() => import('~/client/components/PageTags/TagEditModal').then(mod => mod.TagEditModal), { ssr: false });
 const ConflictDiffModal = dynamic(() => import('~/client/components/PageEditor/ConflictDiffModal').then(mod => mod.ConflictDiffModal), { ssr: false });
-const QuestionnaireModalManager = dynamic(() => import('~/features/questionnaire/client/components/QuestionnaireModalManager'), { ssr: false });
 
 const EditablePageEffects = dynamic(() => import('~/client/components/Page/EditablePageEffects').then(mod => mod.EditablePageEffects), { ssr: false });
 
@@ -426,7 +425,6 @@ Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
       <DescendantsPageListModal />
       <DrawioModal />
       <HandsontableModal />
-      <QuestionnaireModalManager />
       <TemplateModal />
       <LinkEditModal />
       <TagEditModal />

+ 1 - 0
apps/app/src/pages/utils/commons.ts

@@ -115,6 +115,7 @@ export const langMap: LangMap = {
   [Lang.en_US]: 'en-US',
   [Lang.zh_CN]: 'zh-CN',
   [Lang.fr_FR]: 'fr-FR',
+  [Lang.ko_KR]: 'ko-KR',
 } as const;
 
 // use this function to translate content

+ 0 - 13
apps/app/src/server/crowi/index.js

@@ -17,8 +17,6 @@ import instanciatePageBulkExportJobCleanUpCronService, {
   pageBulkExportJobCleanUpCronService,
 } from '~/features/page-bulk-export/server/service/page-bulk-export-job-clean-up-cron';
 import instanciatePageBulkExportJobCronService from '~/features/page-bulk-export/server/service/page-bulk-export-job-cron';
-import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
-import questionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
 import { getGrowiVersion } from '~/utils/growi-version';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
@@ -95,9 +93,6 @@ class Crowi {
   /** @type {PassportService} */
   passportService;
 
-  /** @type {QuestionnaireService} */
-  questionnaireService;
-
   /** @type {import('../service/rest-qiita-API')} */
   restQiitaAPIService;
 
@@ -147,7 +142,6 @@ class Crowi {
     this.inAppNotificationService = null;
     this.activityService = null;
     this.commentService = null;
-    this.questionnaireService = null;
     this.openaiThreadDeletionCronService = null;
     this.openaiVectorStoreFileDeletionCronService = null;
 
@@ -212,7 +206,6 @@ Crowi.prototype.init = async function() {
     this.setupActivityService(),
     this.setupCommentService(),
     this.setupSyncPageStatusService(),
-    this.setupQuestionnaireService(),
     this.setUpCustomize(), // depends on pluginService
   ]);
 
@@ -362,8 +355,6 @@ Crowi.prototype.setupSocketIoService = async function() {
 };
 
 Crowi.prototype.setupCron = function() {
-  questionnaireCronService.startCron();
-
   instanciatePageBulkExportJobCronService(this);
   checkPageBulkExportJobInProgressCronService.startCron();
 
@@ -373,10 +364,6 @@ Crowi.prototype.setupCron = function() {
   startOpenaiCronIfEnabled();
 };
 
-Crowi.prototype.setupQuestionnaireService = function() {
-  this.questionnaireService = new QuestionnaireService(this);
-};
-
 Crowi.prototype.getSlack = function() {
   return this.slack;
 };

+ 0 - 4
apps/app/src/server/crowi/setup-models.ts

@@ -44,10 +44,6 @@ export const setupIndependentModels = async(): Promise<void> => {
     import('~/features/external-user-group/server/models/external-user-group-relation'),
     import('~/features/external-user-group/server/models/external-user-group'),
     import('~/features/growi-plugin/server/models'),
-    import('~/features/questionnaire/server/models/proactive-questionnaire-answer'),
-    import('~/features/questionnaire/server/models/questionnaire-answer-status'),
-    import('~/features/questionnaire/server/models/questionnaire-answer'),
-    import('~/features/questionnaire/server/models/questionnaire-order'),
     import('../models/activity'),
     import('../models/attachment'),
     import('../models/bookmark-folder'),

+ 0 - 6
apps/app/src/server/models/user.js

@@ -81,7 +81,6 @@ const factory = (crowi) => {
     admin: { type: Boolean, default: 0, index: true },
     readOnly: { type: Boolean, default: 0 },
     isInvitationEmailSended: { type: Boolean, default: false },
-    isQuestionnaireEnabled: { type: Boolean, default: true },
   }, {
     timestamps: true,
     toObject: {
@@ -763,11 +762,6 @@ const factory = (crowi) => {
     return { users, totalCount };
   };
 
-  userSchema.methods.updateIsQuestionnaireEnabled = async function(value) {
-    this.isQuestionnaireEnabled = value;
-    return this.save();
-  };
-
   class UserUpperLimitException {
 
     constructor() {

Неке датотеке нису приказане због велике количине промена