Discourse 한국어 번역과 Gitea 배포 구성 완료

일반 화면과 관리자 화면 및 활성 플러그인의 최종 한국어 번역을 반영했다.

기능별 검수 원본과 보드 화면 패치 원본을 저장소에 포함했다. yarn test와 yarn deploy 및 yarn verify:deployment 명령을 추가했다.

활성 플러그인 29개의 번역 키 6,005개에서 누락 0개를 확인했다. PC와 390픽셀 모바일 화면을 확인했다. 운영 app 컨테이너에 재배포하고 HTTP 200 응답을 확인했다.

데이터베이스 구조 변경과 데이터 이전은 없다. 사용자 요청이 없어 백업은 만들지 않았다.
This commit is contained in:
2026-09-27 03:28:42 +00:00
parent cc5042a0a6
commit e0cd349f58
24 changed files with 16518 additions and 25 deletions
+13
View File
@@ -42,3 +42,16 @@
- 앞선 화면별 검사에서는 신고 창과 검색과 글 작성기와 오류 안내와 태그 입력의 한국어 표시를 확인했다. 4차 300개 묶음은 YAML 구조와 후보 수와 운영 응답을 검사했으나 화면별 문맥 검수는 후속 작업으로 남겼다. - 앞선 화면별 검사에서는 신고 창과 검색과 글 작성기와 오류 안내와 태그 입력의 한국어 표시를 확인했다. 4차 300개 묶음은 YAML 구조와 후보 수와 운영 응답을 검사했으나 화면별 문맥 검수는 후속 작업으로 남겼다.
- 현재 SSH 접속 출발지는 Fail2ban의 SSH 차단 제외 목록에 반영했다. 실제 주소는 기록에 남기지 않았다. - 현재 SSH 접속 출발지는 Fail2ban의 SSH 차단 제외 목록에 반영했다. 실제 주소는 기록에 남기지 않았다.
- 이번 기록 시점에는 사용자 지시에 따라 새 백업을 만들지 않았다. 다음 작업은 일반 화면 번역을 300개 단위로 이어서 적용하고 화면별 문맥을 확인하는 것이다. - 이번 기록 시점에는 사용자 지시에 따라 새 백업을 만들지 않았다. 다음 작업은 일반 화면 번역을 300개 단위로 이어서 적용하고 화면별 문맥을 확인하는 것이다.
## 2026-09-27 한국어 번역 완료와 Gitea 배포 준비
- 활성 일반 화면과 관리자 화면 및 활성 플러그인의 번역 누락을 모두 정리했다.
- 활성 플러그인 29개의 번역 키 6,005개를 검사했으며 누락은 0개다.
- 전체 활성 번역에서 원문과 값이 같은 216개는 브랜드명과 형식 문자열 및 주소 예시처럼 유지해야 하는 기술 값이다.
- 업무 흐름과 보드와 RSS 가져오기와 외부 채팅 연동 및 GitHub 연동과 자동화 화면을 확인했다.
- PC와 390픽셀 모바일 화면에서 일반 화면과 관리자 화면의 가로 넘침이 없음을 확인했다. 브라우저 오류도 없었다.
- 보드 설정에 노출되던 `Boards enabled`를 `보드 사용`으로 바꿨다.
- `yarn test`와 `yarn deploy` 및 `yarn verify:deployment` 명령을 추가해 Gitea 저장소에서 검사와 운영 배포를 수행할 수 있게 했다.
- 운영 `app` 컨테이너만 갱신했으며 포럼의 HTTP 200 응답을 확인했다.
- 데이터베이스 구조 변경과 데이터 이전은 없다. 사용자 요청이 없어 백업은 진행하지 않았다.
+55 -8
View File
@@ -1,18 +1,65 @@
# ITOSM 관리자 한국어 보완 # Discourse 한국어 보완
Discourse 관리자 설정 화면에서 내부 설정 키를 영어 문장으로 표시하는 항목을 자연스러운 한국어 이름으로 바꾼다. `forum.itosm.co.kr`에서 사용하는 Discourse 한국어 번역과 관리자 설정 이름 보완 플러그인이다. 일반 화면과 관리자 화면 및 활성 플러그인의 한국어 누락을 보완한다.
- 한국어 화면에서만 동작한다. ## 구성
- 등록하지 않은 설정은 Discourse 기본 이름을 그대로 사용한다.
- 데이터베이스와 사이트 설정값은 변경하지 않는다. - `plugin.rb`: 관리자 설정 이름과 플러그인 표시 문구 보완
- Discourse 기본 번역 구조가 설정 이름을 지원하게 되면 공식 번역으로 옮길 수 있다. - `config/locales/client.ko.yml`: 브라우저 화면 번역
- `config/locales/server.ko.yml`: 서버 문구와 설정 설명 번역
- `config/locales/*-quality.ko.yml`: 기능별 검수 원본
- `patches/`: 별도 플러그인에 반영한 화면 수정 원본
- `docs/`: 작업 기록과 Trilium 대응 문서
## 작성자 ## 작성자
이창현 (Lee Chang Hyun) <syskiller@naver.com> 이창현 (Lee Chang Hyun) <syskiller@naver.com>
## 프로젝트 기록 ## 검사
저장소 루트에서 다음 명령을 실행한다.
```bash
yarn test
```
Ruby 문법과 모든 한국어 YAML 파일의 구조를 검사한다.
## 운영 배포
Ubuntu 서버에서 저장소를 최신 상태로 만든 뒤 다음 명령을 실행한다.
```bash
yarn deploy
```
기본 배포 대상은 `app` 컨테이너와 `https://forum.itosm.co.kr`이다. 다른 환경에서는 아래처럼 지정한다.
```bash
DISCOURSE_CONTAINER=app DISCOURSE_URL=https://forum.itosm.co.kr yarn deploy
```
배포 명령은 검사 통과 후 실행 파일만 `itosm-admin-korean` 플러그인 경로에 복사한다. 이후 대상 컨테이너만 다시 시작하고 HTTP 200 응답을 확인한다. 백업은 자동으로 만들지 않는다. 백업이 필요한 작업은 별도 승인을 받은 뒤 먼저 진행한다.
배포하지 않고 운영 상태만 확인하려면 다음 명령을 사용한다.
```bash
yarn verify:deployment
```
## Gitea에서 새 서버로 설치
서버의 Git 인증 설정을 사용해 저장소를 받은 뒤 검사와 배포를 실행한다. 인증 정보는 저장소나 명령에 넣지 않는다.
```bash
git clone https://gitea.itosm.co.kr/syskiller/discourse-korean-localization.git
cd discourse-korean-localization
corepack yarn test
corepack yarn deploy
```
## 기록
- `PROJECT_MEMORY.md`: 운영 결정과 적용 범위 - `PROJECT_MEMORY.md`: 운영 결정과 적용 범위
- `수정기록.txt`: 날짜별 변경과 검사 및 복구 기준 - `수정기록.txt`: 날짜별 변경과 검사 결과
- `docs/`: Trilium 대응 문서와 상세 운영 기록 - `docs/`: Trilium 대응 문서와 상세 운영 기록
@@ -0,0 +1,18 @@
ko:
discourse_automation:
scriptables:
ai_tool_action:
title: "AI 도구 실행"
description: "자동화가 시작되면 지정한 AI 도구를 실행합니다."
llm_agent_triage:
title: "AI 에이전트로 게시물 분류"
description: "지정한 AI 에이전트로 게시물에 응답합니다."
llm_triage:
title: "AI로 게시물 분류"
description: "언어 모델을 사용해 게시물을 분류합니다."
llm_report:
title: "AI 정기 보고서"
description: "언어 모델을 사용해 정기 보고서를 만듭니다."
llm_tagger:
title: "AI로 글 태그 지정"
description: "AI를 사용해 글에 태그를 자동으로 추가합니다."
@@ -0,0 +1,13 @@
ko:
discourse_automation:
scriptables:
recurring_data_explorer_result_pm:
title: "데이터 탐색기 결과를 개인 메시지로 전송"
description: "데이터 탐색기 보고서를 지정한 사용자에게 정기적으로 보냅니다."
recurring_data_explorer_result_topic:
title: "데이터 탐색기 결과를 글에 정기 게시"
description: "데이터 탐색기 보고서를 지정한 글에 정기적으로 게시합니다."
send_slack_message:
title: "Slack 메시지 보내기"
send_chat_integration_message:
title: "외부 채팅 메시지 보내기"
@@ -0,0 +1,151 @@
ko:
site_settings:
discourse_automation_enabled: "자동화 플러그인을 사용합니다."
discourse_automation_max_recursion_depth: "자동화가 다른 자동화를 연속으로 실행할 수 있는 최대 단계입니다."
discourse_automation:
triggerables:
post_flag_created:
title: "게시물 신고 접수"
description: "게시물이 신고되면 자동화를 실행합니다."
user_badge_granted:
title: "사용자 배지 지급"
doc: "사용자에게 배지가 지급되면 자동화를 실행합니다."
user_first_logged_in:
title: "사용자 첫 로그인"
doc: "사용자가 처음 로그인하면 자동화를 실행합니다."
stalled_topic:
title: "응답이 멈춘 글"
doc: "글 작성자가 지정한 기간 동안 새 댓글을 남기지 않으면 자동화를 실행합니다. 대상 글이 지나치게 많아지지 않도록 태그나 카테고리로 범위를 제한하는 것이 좋습니다. 안전을 위해 한 번에 처리하는 글은 250개로 제한됩니다."
api_call:
title: "API 호출"
description: "POST 요청을 받으면 자동화를 실행합니다."
doc: "전역 API 키를 사용해 `/automations/:id/trigger.json` 주소로 POST 요청을 보내야 합니다."
category_created_edited:
title: "카테고리 생성 또는 수정"
description: "카테고리가 생성되거나 수정되면 자동화를 실행합니다."
recurring:
title: "정기 실행"
invalid_interval: "실행 간격은 0보다 커야 합니다."
point_in_time:
title: "지정 시각"
description: "지정한 날짜와 시각이 되면 자동화를 실행합니다."
user_added_to_group:
title: "사용자가 그룹에 추가됨"
description: "사용자가 지정한 그룹에 추가되면 자동화를 실행합니다."
user_removed_from_group:
title: "사용자가 그룹에서 제거됨"
description: "사용자가 지정한 그룹에서 제거되면 자동화를 실행합니다."
user_promoted:
title: "사용자 신뢰 등급 상승"
description: "사용자의 신뢰 등급이 올라가면 자동화를 실행합니다."
transition_placeholder: "%{from_level_name}에서 %{to_level_name}(으)로"
stalled_wiki:
title: "오랫동안 수정되지 않은 위키"
description: "위키가 지정한 기간 동안 수정되지 않으면 자동화를 실행합니다."
post_created_edited:
title: "게시물 작성 또는 수정"
description: "조건에 맞는 게시물이 작성되거나 수정되면 자동화를 실행합니다."
pm_created:
title: "개인 메시지 작성"
description: "조건에 맞는 개인 메시지가 작성되면 자동화를 실행합니다."
topic_tags_changed:
title: "글 태그 변경"
description: "글의 태그가 변경되면 자동화를 실행합니다."
topic:
title: "특정 글"
description: "지정한 글에서만 자동화 스크립트를 사용합니다. 게시물 내용을 검사하는 자동화에 적합합니다."
topic_closed:
title: "글 닫힘"
description: "글이 닫히면 자동화를 실행합니다."
after_post_cook:
title: "게시물 표시용 변환 완료"
description: "게시물 내용이 화면 표시용 형식으로 변환되면 자동화를 실행합니다."
user_updated:
title: "사용자 정보 변경"
description: "사용자 정보가 변경되면 자동화를 실행합니다."
scriptables:
email_on_flagged_post:
title: "신고된 게시물 이메일 알림"
description: "게시물이 신고될 때 지정한 내용으로 이메일을 보냅니다."
post:
title: "게시물 작성"
description: "지정한 글에 게시물을 작성합니다."
topic:
title: "글 작성"
description: "지정한 사용자 이름으로 새 글을 작성합니다."
flag_post_on_words:
title: "금지 단어가 포함된 게시물 신고"
description: "게시물에 지정한 단어가 포함되면 신고합니다."
flag_message: "게시물에 허용되지 않은 단어가 포함되어 있습니다: %{words}"
doc: "지정한 단어가 모두 포함된 게시물을 신고하고 숨깁니다. 쉼표로 여러 단어를 나누면 모든 단어가 함께 있어야 조건에 맞습니다."
zapier_webhook:
title: "Zapier 웹훅"
description: "실행 조건의 데이터를 본문에 담아 Zap을 호출합니다."
auto_responder:
title: "자동 답변"
description: "지정한 키워드가 포함된 게시물에 자동으로 답글을 작성합니다."
auto_tag_topic:
title: "글에 태그 자동 추가"
description: "조건에 맞는 글에 지정한 태그를 자동으로 추가합니다."
pin_topic:
title: "글 고정"
description: "지정한 날짜에 선택한 글을 고정합니다."
banner_topic:
title: "글을 배너로 표시"
description: "선택한 글을 사이트 배너로 표시합니다."
gift_exchange:
title: "선물 교환"
description: "그룹 사용자를 익명으로 연결해 서로 선물을 보낼 수 있게 합니다."
doc: "선물 교환에는 사용자가 3명 이상인 그룹이 필요합니다. 지정한 날짜에 각 사용자를 선물을 주는 사람과 받는 사람에게 연결합니다."
remove_upload_markup_from_deleted_posts:
title: "삭제된 게시물의 첨부 표시 제거"
description: "삭제된 게시물에서 첨부 파일 표시를 제거한 수정본을 시스템 사용자 이름으로 남깁니다. 첨부 파일 정리 작업과 함께 사용하면 삭제된 게시물의 첨부 파일이 영구 삭제됩니다. 주의해서 사용하세요."
edit_reason: "자동화가 삭제된 게시물의 첨부 파일 표시를 제거했습니다."
send_pms:
title: "개인 메시지 보내기"
description: "사용자에게 개인 메시지를 보내며 필요하면 전송을 지연할 수 있습니다."
doc: "한 사용자에게 여러 개인 메시지를 보낼 수 있습니다. 메시지마다 지연 시간을 지정할 수 있습니다."
suspend_user_by_email:
title: "이메일 주소로 사용자 이용 정지"
description: "이메일 주소를 기준으로 사용자 계정을 자동 이용 정지합니다."
user_global_notice:
title: "사용자 전체 공지"
description: "특정 사용자에게 사이트 전체 공지를 표시합니다."
user_group_membership_through_badge:
title: "배지에 따라 그룹 소속 변경"
description: "지급된 배지를 기준으로 사용자 그룹 소속을 변경합니다."
topic_required_words:
title: "글 필수 단어"
description: "글에 필요한 단어 목록을 정합니다. 각 게시물에는 목록의 단어가 하나 이상 있어야 합니다."
doc: "대상 글을 지정해야 합니다. 게시물을 작성하거나 수정할 때 필수 단어가 하나 이상 포함됐는지 검사합니다."
errors:
must_include_word: "게시물에는 다음 단어 중 하나 이상이 있어야 합니다: %{words}"
close_topic:
title: "글 닫기"
description: "필요하면 안내 메시지를 남긴 뒤 글을 닫습니다."
append_last_edited_by:
title: "마지막 수정자 표시"
description: "게시물 본문 아래에 마지막 수정자 정보를 표시합니다."
text: "마지막 수정: @%{username} %{date_time}"
append_last_checked_by:
title: "마지막 확인자 표시"
description: "게시물 본문 아래에 마지막 확인자 정보를 표시합니다."
text: "마지막 확인: @%{username} %{date_time}"
summary: "문서 확인"
details: "확인할 문서: "
button_text: "확인 완료"
add_user_to_group_through_custom_field:
title: "사용자 입력 항목에 따라 그룹에 추가"
description: "로그인할 때 또는 정기 점검 때 사용자 입력 항목을 확인해 그룹에 자동으로 추가합니다."
group_category_notification_default:
title: "그룹의 카테고리 알림 기본값"
description: "그룹 구성원에게 적용할 카테고리 기본 알림 수준을 설정합니다."
send_chat_message:
title: "채팅 메시지 보내기"
description: "선택한 채널에 지정한 채팅 메시지를 보냅니다."
random_assign:
title: "무작위 할당"
description: "글을 지정한 그룹 구성원에게 무작위로 할당합니다."
set_topic_timer:
title: "글 타이머 설정"
description: "선택한 글에 글 타이머를 설정합니다."
+218
View File
@@ -0,0 +1,218 @@
ko:
js:
discourse_workflows:
node_unavailable:
requires_boards: "보드 기능을 켜야 합니다"
nodes:
"action:create_board_column": "보드 열 만들기"
"action:create_board": "보드 만들기"
"trigger:card_moved": "카드 이동"
node_descriptions:
"action:create_board_column": "아이콘과 색상 및 태그와 기본 정렬 방식을 지정해 보드에 열을 만듭니다"
"action:create_board": "접근 권한과 태그 및 카테고리 조건을 지정해 보드를 만듭니다"
"trigger:card_moved": "보드에서 카드가 다른 열로 이동하면 실행됩니다"
create_board_column:
board_id: "보드"
board_id_description: "보드를 선택하거나 앞 단계에서 받은 보드 번호를 사용하세요."
title: "제목"
icon: "아이콘"
color: "색상"
tag_name: "태그"
default_sort: "기본 정렬"
default_sort_priority: "우선순위"
default_sort_recency: "최근순"
actor_username: "실행 사용자"
create_board:
name: "이름"
slug: "주소 이름"
slug_description: "비워 두면 보드 이름으로 주소를 만듭니다."
tag_names: "태그"
category_ids: "카테고리"
category_ids_description: "보드의 글 카드가 속할 수 있는 카테고리를 선택하세요."
actor_username: "실행 사용자"
acl: "보드 접근 권한"
acl_description: "보드를 보고 편집하고 관리할 사용자를 선택하세요."
card_moved:
board_id: "보드"
board_id_description: "이 실행 조건을 적용할 보드를 선택하세요. 비워 두면 모든 보드에 적용됩니다."
board_id_placeholder: "모든 보드"
boards:
filter_boards: "보드 검색"
filter_boards_no_results: "검색 조건에 맞는 보드가 없습니다"
status:
all: "전체"
open: "사용 중"
archived: "보관됨"
topic_footer:
add_to_board: "보드에 추가"
already_added: "이미 추가됨"
no_boards: "사용할 수 있는 보드가 없습니다"
topic_pill:
title: "이 글은 %{board} 보드의 %{column} 열에 있습니다"
title_multiple_columns:
one: "이 글은 %{board} 보드의 열 %{count}개에 있습니다"
other: "이 글은 %{board} 보드의 열 %{count}개에 있습니다"
multiple:
one: "보드 %{count}개"
other: "보드 %{count}개"
multiple_title: "이 글은 여러 보드에 있습니다"
board:
added_topic_to_board: "글을 '%{boardName}' 보드의 '%{columnName}' 열에 추가했습니다"
removed_topic_from_board: "글을 '%{boardName}' 보드의 '%{columnName}' 열에서 제거했습니다"
empty_board: "이 보드에는 아직 열이 없습니다"
empty_board_cta: "첫 번째 열 추가"
no_cards: "이 열에는 카드가 없습니다"
move_confirm: "'%{topic_title}' 카드를 '%{column_title}' 열로 옮기시겠습니까?"
move_failed: "카드를 옮기지 못했습니다"
add_card: "카드 추가"
add_topic_as_card: "글을 카드로 추가"
topic_search_placeholder: "글을 검색하거나 글 주소를 붙여 넣으세요"
card_title_placeholder: "카드 제목이나 글 주소를 입력하세요"
controls: "보드 메뉴"
all_boards: "모든 보드"
fullscreen: "전체 화면"
exit_fullscreen: "전체 화면 종료"
add_column: "열 추가"
edit_column: "열 편집"
clear_column: "열 비우기"
confirm_clear_column:
one: "'%{column_title}' 열에서 카드 %{count}개를 제거하시겠습니까?"
other: "'%{column_title}' 열에서 카드 %{count}개를 제거하시겠습니까?"
delete_column: "열 삭제"
move_left: "왼쪽으로 이동"
move_right: "오른쪽으로 이동"
column_controls: "열 메뉴"
board_settings: "보드 설정"
confirm_delete_column: "이 열을 삭제하시겠습니까? 열의 모든 카드는 삭제되며 복구할 수 없습니다. 글 카드를 삭제해도 원본 글은 삭제되지 않습니다."
confirm_delete_column_with_cards: "이 열을 삭제하시겠습니까? 열의 카드 %{count}개는 삭제되며 복구할 수 없습니다. 글 카드를 삭제해도 원본 글은 삭제되지 않습니다."
column_settings_title: "열 설정"
new_column_title: "새 열"
new_board: "새 보드"
archive_board: "보드 보관"
unarchive_board: "보드 보관 해제"
unarchive_board_menu: "보드 보관 해제"
confirm_archive: "이 보드를 보관하시겠습니까? <a href=\"%{boards_url}\">보드 목록</a>에서 '보관됨'을 선택하면 다시 볼 수 있습니다."
confirm_unarchive: "이 보드의 보관을 해제하시겠습니까?"
archived_successfully: "보드를 보관했습니다"
unarchived_successfully: "보드 보관을 해제했습니다"
unarchive_slug_required: "원래 보드 주소가 이미 사용 중입니다. 보관을 해제하려면 새 주소 이름을 선택하세요."
archived_tooltip: "보관된 보드입니다. 카드와 열을 변경할 수 없습니다."
delete_board: "보드 삭제"
remove_card: "제거"
new_topic: "새 글"
card_detail: "카드 상세"
title: "제목"
title_placeholder: "카드 제목을 입력하세요"
notes: "메모"
notes_placeholder: "메모를 입력하세요"
tags: "태그"
assigned_to: "담당자"
view_topic: "글 보기"
topic_load_error: "글 내용을 불러오지 못했습니다"
topic_reply_count:
one: "댓글 %{count}개"
other: "댓글 %{count}개"
topic_last_reply: "마지막 작성자 %{username}"
assign: "담당 지정"
assign_card: "카드 담당 지정"
reassign: "담당 변경"
unassign: "담당 해제"
unassign_from_topic: "글에서 @%{username}님의 담당을 해제"
unassign_from_post: "댓글 #%{post_number}에서 @%{username}님의 담당을 해제"
unassign_group_from_topic: "글에서 %{group_name} 그룹의 담당을 해제"
edit_assignments: "담당자 편집"
constraint_fix_title: "글이 보드 조건에 맞지 않습니다"
constraint_fix_description: "이 글을 보드에 넣으려면 다음 내용을 변경해야 합니다"
constraint_fix_category: "카테고리 변경"
constraint_fix_tags: "보드 태그 추가"
constraint_fix_confirm: "변경하고 이동"
constraint_tooltip: "지정한 카테고리와 태그 조건에 맞는 글만 이 보드에 추가할 수 있습니다."
created_by: "작성자"
show_older_cards: "이전 카드 %{count}개 보기"
errors:
category_unavailable: "대상 카테고리를 사용할 수 없습니다. 글을 만들기 전에 보드나 열 설정을 바꾸세요."
acl_update_failed: "보드 권한을 변경하지 못했습니다."
acl_create_failed: "보드 권한을 만들지 못했습니다."
manage:
board: "보드"
title: "보드"
description: "열을 사용해 작업 보드를 만들고 관리합니다."
new: "새 보드"
save: "보드 저장"
delete: "보드 삭제"
confirm_delete: "이 보드를 삭제하시겠습니까? 삭제한 뒤에는 되돌릴 수 없습니다."
name: "이름"
name_placeholder: "보드 제목을 입력하세요"
slug: "주소 이름"
constrain_board_by: "보드 조건"
constraint_categories: "카테고리"
constraint_tags: "태그"
constraint_categories_and_tags: "카테고리와 태그"
constraint_help: "다음 조건 중 하나 이상에 맞는 글만 보드에 추가할 수 있습니다."
board_categories_constraint: "카테고리 조건"
board_tags_constraint: "태그 조건"
advanced_settings: "고급 설정"
card_style_description: "상세 카드에는 최근 활동일과 글의 마지막 댓글 같은 추가 정보가 표시됩니다."
card_style: "카드 모양"
card_style_detailed: "상세"
card_style_simple: "간단히"
show_tags: "태그 표시"
show_topic_thumbnail: "글 미리보기 그림 표시"
require_confirmation: "카드를 옮길 때 확인"
board_access: "보드 접근 권한"
board_access_description: "여기에 추가한 그룹이나 사용자만 보드를 보거나 관리하고 편집할 수 있습니다."
board_access_requires_manager: "보드를 관리할 그룹이나 사용자를 한 명 이상 선택하세요."
board_access_permission_manager: "관리자"
board_access_permission_manager_description: "보드 설정과 열을 관리하며 카드를 추가하고 제거하고 편집할 수 있습니다"
board_access_permission_editor_description: "카드를 추가하고 제거하고 편집할 수 있습니다"
board_access_permission_viewer_description: "모든 카드를 볼 수 있지만 옮기거나 편집할 수 없습니다"
constraint_warning:
one: "주의: 조건에 맞지 않게 되는 카드 %{count}개가 제거됩니다"
other: "주의: 조건에 맞지 않게 되는 카드 %{count}개가 제거됩니다"
get_started: "아직 보드가 없습니다. 첫 번째 보드를 만들어 보세요."
no_columns: "설정된 열이 없습니다"
empty_title: "아직 보드가 없습니다"
restricted_access: "접근이 제한된 보드입니다"
column_count:
one: "열 %{count}개"
other: "열 %{count}개"
all_boards: "모든 보드"
view_board: "보드 보기"
columns:
title: "열"
column_number: "열 %{number}"
add: "열 추가"
remove: "제거"
column_title: "제목"
column_title_placeholder: "열 제목을 입력하세요. 비워 두면 태그 이름을 사용합니다."
column_title_required: "열 제목이나 태그 이름이 필요합니다"
icon: "아이콘"
color: "색상"
default_sort: "기본 정렬"
default_sort_priority: "우선순위"
default_sort_recency: "최근순"
tag: "태그"
tag_help: "이 태그가 있는 글은 이 열에 표시됩니다. 글을 이 열로 옮기면 태그가 적용됩니다."
move_to_category: "이동할 카테고리"
move_to_assigned: "이동할 때 담당 지정"
move_to_assigned_none: "변경하지 않음"
move_to_assigned_unassign: "담당 해제"
move_to_assigned_user: "사용자나 그룹에 지정"
move_to_status: "이동할 때 상태 변경"
move_to_status_none: "변경하지 않음"
move_to_status_open: "열림"
move_to_status_closed: "닫힘"
show_advanced: "고급 옵션 보기"
hide_advanced: "고급 옵션 숨기기"
keyboard_shortcuts_help:
boards:
title: "보드"
navigate_boards_columns: "보드와 열 이동"
navigate_cards: "카드 사이 이동"
move_card_column: "선택한 카드를 옆 열로 이동"
move_card_position: "직접 정렬하는 열에서 선택한 카드의 위치 변경"
admin_js:
admin:
site_settings:
categories:
boards: "보드"
+378
View File
@@ -0,0 +1,378 @@
ko:
admin_js:
admin:
site_settings:
categories:
discourse_rss_polling: "RSS 자동 수집"
chat_integration: "외부 채팅 연동"
js:
rss_polling:
title: "RSS 자동 수집"
admin:
logs:
staff_actions:
actions:
create_rss_polling_feed: "RSS 피드 등록"
update_rss_polling_feed: "RSS 피드 수정"
destroy_rss_polling_feed: "RSS 피드 삭제"
enable_rss_polling_feed: "RSS 피드 사용"
disable_rss_polling_feed: "RSS 피드 사용 중지"
poll_rss_polling_feed: "RSS 피드 지금 수집"
rss_polling:
feed_settings: "피드 설정"
discourse_settings: "포럼 등록 설정"
author: "작성자"
feed_url: "피드 주소"
feed_url_must_be_http: "http:// 또는 https://로 시작하는 올바른 피드 주소를 입력하세요."
discourse_category: "등록할 카테고리"
discourse_tags: "등록할 태그"
required_tags_hint: "이 카테고리에 필요한 태그가 있습니다. 다음 태그 중 하나를 추가하지 않으면 글을 가져올 수 없습니다."
required_tag_error: "'%{tag_group}' 그룹의 태그를 %{count}개 이상 추가하세요. 추가하지 않으면 글을 가져올 수 없습니다."
feed_category_filter: "피드 카테고리 필터"
feed_category_filter_description: "피드 카테고리에 이 문구가 포함된 항목만 가져옵니다. 대소문자는 구분하지 않습니다. 비워 두면 모두 가져옵니다."
showing: "전체 %{total}개 중 앞의 %{count}개를 표시합니다."
feeds:
title: "피드"
nav_description: "포럼으로 가져올 RSS와 Atom 피드를 관리합니다."
description: "정기적으로 확인해 포럼 글로 등록할 RSS와 Atom 피드를 설정합니다."
add: "피드 추가"
add_header: "피드 추가"
edit_header: "피드 편집"
empty: "등록된 피드가 없습니다."
edit: "편집"
delete: "삭제"
enable: "피드 사용"
disable: "피드 사용 중지"
back: "피드 목록으로"
save: "저장"
confirm_delete: "이 피드를 삭제하시겠습니까?"
delete_success: "피드를 삭제했습니다"
save_success: "피드를 저장했습니다"
enable_success: "피드를 사용하도록 설정했습니다"
disable_success: "피드 사용을 중지했습니다"
test:
button: "피드 시험"
title: "가져오기 미리보기"
empty: "이 피드에서 가져올 항목을 찾지 못했습니다."
dismiss: "닫기"
would_import: "가져올 항목"
summary:
would_import: "%{count}개 가져오기 가능"
already_imported: "%{count}개 이미 가져옴"
skipped: "%{count}개 제외됨"
already_imported: "이미 가져온 항목입니다. 수집하면 <a href='%{topic_url}' target='_blank' rel='noopener noreferrer'>기존 글</a>을 갱신합니다."
already_imported_plain: "이미 가져온 항목입니다. 수집하면 기존 글을 갱신합니다."
skip_reasons:
missing_content: "내용이 없어 제외했습니다."
missing_title: "제목이 없어 제외했습니다."
invalid_url: "가져올 수 있는 주소가 없어 제외했습니다."
category_filter_mismatch: "피드 카테고리가 조건과 맞지 않아 제외했습니다: %{categories}"
category_filter_mismatch_none: "피드 카테고리가 없어 제외했습니다."
errors:
fetch_failed: "피드를 불러오지 못했습니다. 주소가 올바르고 접속 가능한지 확인하세요."
parse_failed: "응답이 올바른 RSS 또는 Atom 피드 형식이 아닙니다."
invalid_feed: "피드 형식이 잘못되어 읽을 수 없습니다."
blank_feed_url: "시험할 피드 주소를 입력하세요."
category_deleted: "글을 등록할 카테고리가 삭제됐습니다. 피드를 편집해 새 카테고리를 선택하세요."
unknown: "피드를 읽지 못했습니다. 일시적인 장애이거나 예상하지 못한 응답일 수 있습니다. 피드 시험과 서버 기록을 확인하세요."
status:
enabled: "사용 중"
disabled: "사용 중지"
disabled_note: "예약 수집을 실행하지 않습니다"
history:
recent: "최근 수집 기록"
show_older: "이전 기록 %{count}개 보기"
poll_now: "지금 수집"
poll_now_dirty: "수집하기 전에 변경 사항을 저장하세요."
poll_now_disabled: "수집하려면 피드를 사용하도록 설정하세요."
poll_confirm: "지금 피드를 확인해 새 글을 가져오거나 기존 글을 갱신하시겠습니까?"
poll_started: "수집을 시작했습니다. 잠시 후 최근 실행 결과가 표시됩니다."
empty: "아직 이 피드를 수집한 기록이 없습니다."
no_items: "이번 수집에서 처리한 항목이 없습니다."
no_changes: "변경 없음"
imported_count: "%{count}개 가져옴"
updated_count: "%{count}개 갱신"
skipped_count: "%{count}개 제외"
failed_count: "%{count}개 실패"
imported: "새 <a href='%{topic_url}' target='_blank' rel='noopener noreferrer'>글</a>로 가져왔습니다."
imported_plain: "새 글로 가져왔습니다."
updated: "<a href='%{topic_url}' target='_blank' rel='noopener noreferrer'>기존 글</a>을 갱신했습니다."
updated_plain: "기존 글을 갱신했습니다."
failed: "가져오지 못했습니다."
failed_reasons:
import_rejected: "가져올 수 있는 주소가 없어 등록하지 못했습니다."
import_error: "가져오지 못했습니다: %{error}"
chat_integration:
menu_title: "외부 채팅 연동"
settings: "설정"
nav:
providers: "연결 서비스"
providers_description: "외부 채팅 서비스를 연결하고 관리합니다"
add_provider: "서비스 연결"
view_provider_settings: "서비스 설정 보기"
more_providers: "더 보기"
confirm_setup_provider: "%{provider} 서비스를 사용하도록 설정하시겠습니까? 나중에 설정에서 다시 끌 수 있습니다."
setup_provider_modal:
title: "%{provider} 연결 설정"
setup_instructions: "%{provider} 연결에 필요한 정보를 입력하세요. %{additionalInstructions}"
confirm_setup: "연결 설정"
success: "%{provider} 연결 설정을 마쳤습니다"
slack:
instructions: "접근 토큰이나 보내는 웹훅 주소 중 하나가 필요합니다."
at_least_one_required: "접근 토큰이나 보내는 웹훅 주소를 입력하세요."
access_token:
title: "접근 토큰"
description: "Slack 봇 OAuth 접근 토큰"
outbound_webhook_url:
title: "보내는 웹훅 주소"
description: "Slack 보내는 웹훅 주소"
telegram:
instructions: "저장할 때 접근 토큰이 올바른지 확인합니다."
access_token:
title: "접근 토큰"
description: "BotFather에서 발급한 Telegram 봇 접근 토큰"
empty_state:
title: "외부 채팅 서비스를 연결하세요"
body: "포럼 알림을 팀 채팅으로 보낼 수 있습니다. 연결할 서비스를 선택하세요."
channels_title: "알림 채널"
channels_description: "%{provider}에서 알림을 보낼 채널을 설정합니다."
no_channels: "설정된 채널이 없습니다."
channel_actions: "채널 작업"
add_first_channel: "첫 %{provider} 채널 추가"
channels_with_errors: "이 서비스의 일부 채널에서 최근 알림 전송이 실패했습니다. 오류 아이콘을 눌러 내용을 확인하세요."
channel_exception: "이 채널로 알림을 보내던 중 알 수 없는 오류가 발생했습니다."
group_mention_template: "다음 그룹을 언급한 글: @%{name}"
group_message_template: "다음 그룹에 보낸 메시지: @%{name}"
choose_group: "그룹 선택"
all_categories: "모든 카테고리"
all_tags: "모든 태그"
create_rule: "알림 규칙 만들기"
add_channel: "채널 추가"
delete_channel: "삭제"
test_channel: "시험 전송"
edit_channel: "편집"
channel_delete_confirm: "이 채널을 삭제하시겠습니까? 연결된 알림 규칙도 모두 삭제됩니다."
test_modal:
title: "시험 메시지 보내기"
topic: "시험할 글"
send: "시험 메시지 보내기"
close: "닫기"
error: "메시지를 보내던 중 알 수 없는 오류가 발생했습니다. 자세한 내용은 서버 기록을 확인하세요."
success: "메시지를 보냈습니다"
type:
normal: "일반 알림"
group_message: "그룹 메시지"
group_mention: "그룹 언급"
filter:
mute: "알림 끄기"
follow: "첫 글만"
watch: "모든 글과 댓글"
thread: "모든 글과 연결된 댓글"
rule_table:
filter: "알림 수준"
category: "카테고리"
tags: "태그"
edit_rule: "편집"
delete_rule: "삭제"
edit_channel_modal:
title: "채널 편집"
save: "채널 저장"
cancel: "취소"
provider: "연결 서비스"
channel_validation:
ok: "사용 가능"
fail: "형식이 올바르지 않음"
edit_rule_modal:
edit_title: "알림 규칙 편집"
create_title: "알림 규칙 만들기"
save: "규칙 저장"
cancel: "취소"
provider: "연결 서비스"
type: "알림 종류"
channel: "채널"
filter: "알림 수준"
category: "카테고리"
group: "그룹"
tags: "태그"
instructions:
type: "그룹 메시지나 그룹 언급 알림을 받으려면 알림 종류를 변경하세요."
filter: "알림 수준을 정합니다. 알림 끄기 규칙은 다른 규칙보다 우선합니다."
category: "지정한 카테고리의 글에만 이 규칙을 적용합니다."
group: "지정한 그룹을 언급한 글에 이 규칙을 적용합니다."
tags: "태그를 지정하면 이 태그 중 하나 이상이 있는 글에만 규칙을 적용합니다."
provider:
slack:
title: "Slack"
param:
identifier:
title: "채널"
help: "예: #channel 또는 @username"
errors:
at_least_one_required: "접근 토큰이나 보내는 웹훅 주소를 입력하세요."
invalid_webhook_url: "보내는 웹훅 주소는 https://hooks.slack.com/services/... 형식이어야 합니다."
auth_error: "인증 토큰이 올바르지 않습니다"
action_prohibited: "봇에 이 채널로 글을 보낼 권한이 없습니다"
channel_not_found: "Slack에서 지정한 채널을 찾을 수 없습니다"
telegram:
title: "Telegram"
param:
name:
title: "이름"
help: "채널을 구분할 이름입니다. Telegram 연결에는 사용하지 않습니다."
chat_id:
title: "채팅 번호"
help: "봇이 알려 준 숫자나 @channelname 형식의 방송 채널 이름을 입력하세요."
errors:
access_token_required: "봇 접근 토큰이 필요합니다."
webhook_setup_failed: "Telegram 웹훅을 등록하지 못했습니다. 토큰을 확인한 뒤 다시 시도하세요."
channel_not_found: "Telegram에서 지정한 채널을 찾을 수 없습니다"
forbidden: "봇에 이 채널로 글을 보낼 권한이 없습니다"
invalid_api_base_url: "Telegram API 기본 주소가 올바른 HTTPS 주소가 아닙니다."
api_base_url_blocked: "Telegram API 기본 주소로 보내는 요청이 차단됐습니다. 사설망 주소는 허용된 내부 호스트 설정에 추가해야 합니다."
discord:
title: "Discord"
param:
name:
title: "이름"
help: "채널을 구분할 이름입니다. Discord 연결에는 사용하지 않습니다."
webhook_url:
title: "웹훅 주소"
help: "Discord 서버 설정에서 만든 웹훅 주소를 입력하세요."
guilded:
title: "Guilded"
param:
name:
title: "이름"
help: "채널을 구분할 이름입니다. Guilded 연결에는 사용하지 않습니다."
webhook_url:
title: "웹훅 주소"
help: "Guilded 서버 설정에서 만든 웹훅 주소를 입력하세요."
mattermost:
title: "Mattermost"
param:
identifier:
title: "채널"
help: "예: #channel 또는 @username"
errors:
channel_not_found: "Mattermost에서 지정한 채널을 찾을 수 없습니다"
matrix:
title: "Matrix"
param:
name:
title: "이름"
help: "채널을 구분할 이름입니다. Matrix 연결에는 사용하지 않습니다."
room_id:
title: "대화방 번호"
help: "대화방의 비공개 식별자를 입력하세요. 예: !abcdefghijkl"
errors:
unknown_token: "접근 토큰이 올바르지 않습니다"
unknown_room: "대화방 번호가 올바르지 않습니다"
zulip:
title: "Zulip"
param:
stream:
title: "스트림"
help: "메시지를 보낼 Zulip 스트림 이름을 입력하세요. 예: general"
subject:
title: "주제"
help: "봇이 보내는 메시지에 붙일 주제를 입력하세요."
errors:
does_not_exist: "Zulip에서 지정한 스트림을 찾을 수 없습니다"
rocketchat:
title: "Rocket.Chat"
param:
identifier:
title: "채널"
help: "예: #channel 또는 @username"
errors:
invalid_channel: "Rocket.Chat에서 지정한 채널을 찾을 수 없습니다"
gitter:
title: "Gitter"
param:
name:
title: "이름"
help: "Gitter 대화방 이름을 입력하세요. 예: gitterHQ/services"
webhook_url:
title: "웹훅 주소"
help: "Gitter 대화방에서 연동을 만들 때 발급된 주소를 입력하세요."
groupme:
title: "GroupMe"
param:
groupme_instance_name:
title: "GroupMe 연결 이름"
help: "사이트 설정에 등록된 GroupMe 연결 이름을 입력하세요. 모든 연결에 보내려면 all을 사용합니다."
errors:
not_found: "메시지를 보낼 경로를 찾지 못했습니다. 사이트 설정의 봇 번호를 확인하세요."
teams:
title: "Microsoft Teams"
param:
name:
title: "이름"
help: "Teams 채널 이름을 입력하세요. 예: discourse"
webhook_url:
title: "웹훅 주소"
help: "받는 웹훅을 만들 때 발급된 주소를 입력하세요."
errors:
invalid_channel: "Microsoft Teams에서 지정한 채널을 찾을 수 없습니다"
powerautomate:
title: "Microsoft Power Automate"
param:
name:
title: "이름"
help: "관리자 화면에서 채널을 구분할 이름입니다."
webhook_url:
title: "웹훅 주소"
help: "받는 웹훅을 만들 때 발급된 주소를 입력하세요."
errors:
invalid_webhook: "웹훅 주소가 올바르지 않습니다."
webex:
title: "Webex Teams"
param:
name:
title: "이름"
help: "Webex 공간 이름을 입력하세요. 예: discourse"
webhook_url:
title: "웹훅 주소"
help: "받는 웹훅을 만들 때 발급된 주소를 입력하세요."
errors:
invalid_channel: "Webex에서 지정한 채널을 찾을 수 없습니다"
google:
title: "Google Chat"
param:
name:
title: "이름"
help: "관리자 화면에서 채널을 구분할 이름입니다."
webhook_url:
title: "웹훅 주소"
help: "새 웹훅을 만들 때 발급된 주소를 입력하세요."
discourse_automation:
scriptables:
send_slack_message:
title: "Slack 메시지 보내기"
fields:
message:
label: "메시지"
description: "글 이름에는 ${TOPIC}을 사용하고 주소에는 ${URL}을 사용하세요. 제거된 태그에는 ${REMOVED_TAGS}를 사용하고 추가된 태그에는 ${ADDED_TAGS}를 사용하세요. 기본 문구에는 ${ADDED_AND_REMOVED}를 사용합니다. 실행 조건이 글 태그 변경일 때만 사용할 수 있습니다."
url:
label: "주소"
channel:
label: "채널"
send_chat_integration_message:
title: "외부 채팅 메시지 보내기"
fields:
channel_name:
label: "채널 이름"
description: "외부 채팅 연동 설정에서 채널 이름을 확인할 수 있습니다"
provider:
label: "연결 서비스"
discourse_workflows:
node_unavailable:
requires_chat_integration: "외부 채팅 연동 기능을 켜야 합니다"
nodes:
"action:send_chat_integration_message": "외부 채팅 메시지 보내기"
node_descriptions:
"action:send_chat_integration_message": "Slack과 Discord 및 Telegram 같은 외부 채팅 채널로 알림을 보냅니다"
send_chat_integration_message:
channel_id: "채널"
configure: "설정"
message: "메시지"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,16 @@
ko:
js:
github:
pr_status:
draft: "초안"
open: "열림"
approved: "승인됨"
merged: "병합됨"
closed: "닫힘"
admin_js:
admin:
site_settings:
categories:
discourse_github: "GitHub 연동"
replace_github_link:
edit_reason: "GitHub 링크를 고정 링크로 바꿨습니다."
@@ -0,0 +1,57 @@
ko:
site_settings:
enable_discourse_github_plugin: "GitHub 연동 플러그인을 사용합니다."
github_linkback_enabled: "포럼에서 언급한 GitHub 문제와 병합 요청에 이 포럼 글의 링크를 댓글로 남깁니다."
github_linkback_projects: "연결 댓글을 남길 GitHub 저장소 목록입니다. `user/repository` 형식으로 입력합니다."
github_linkback_access_token: "연결 댓글을 작성하고 배지용 커밋과 기여 수를 확인할 GitHub 사용자의 유효한 접근 토큰입니다. 토큰 발급 방법은 <a href=\"https://meta.discourse.org/t/99895\">안내 문서</a>를 참고하세요."
github_linkback_maximum_links: "게시물 하나에서 만들 수 있는 GitHub 연결 댓글의 최대 수입니다. 링크 수가 이 값을 넘으면 연결 댓글을 만들지 않습니다."
github_linkback_ignored_categories: "선택한 카테고리의 글에서는 GitHub 연결 댓글을 만들지 않습니다."
github_permalinks_enabled: "GitHub 파일 링크를 특정 커밋을 가리키는 고정 링크로 자동 변환합니다."
github_pr_status_enabled: "GitHub 병합 요청의 상태를 링크 미리보기에 표시합니다. 초안과 열림 및 승인됨과 병합됨과 닫힘 상태를 지원합니다."
github_webhook_secret: "GitHub 웹훅 서명을 확인할 비밀 키입니다. GitHub 웹훅 설정에도 같은 값을 입력해야 합니다."
github_permalinks_exclude: "고정 링크 변환에서 제외할 파일이나 디렉터리입니다. 파일 이름 또는 `user/repository/선택-디렉터리/파일명` 형식의 전체 경로를 입력합니다."
github_badges_enabled: "GitHub 기여 배지를 사용합니다."
github_badges_repos: "기여와 커밋을 확인할 GitHub 저장소 주소 목록입니다."
github_silver_badge_min_commits: "GitHub 은색 배지를 받는 데 필요한 최소 커밋 수입니다."
github_gold_badge_min_commits: "GitHub 금색 배지를 받는 데 필요한 최소 커밋 수입니다."
errors:
invalid_badge_repo: "GitHub 주소 또는 `github_user/repository_name` 형식의 저장소 이름을 입력해야 합니다."
invalid_badge_repo_value: "'%{repo}'은 올바른 GitHub 주소나 저장소 이름이 아닙니다. `github_user/repository_name` 형식으로 입력하세요."
invalid_github_linkback_access_token: "지정한 배지 저장소에 접근할 수 있는 유효한 GitHub 연결 토큰을 입력해야 합니다."
invalid_github_linkback_access_token_for_repo: "GitHub 연결 토큰으로 '%{repo}' 저장소에 접근할 수 없습니다. 토큰과 저장소 이름 및 접근 권한을 확인하세요."
github_linkback:
commit_template: |-
이 커밋은 **%{title}**에서 언급되었습니다. 관련 내용은 다음 글에서 확인할 수 있습니다.
%{post_url}
pr_template: |-
이 병합 요청은 **%{title}**에서 언급되었습니다. 관련 내용은 다음 글에서 확인할 수 있습니다.
%{post_url}
issue_template: |-
이 문제는 **%{title}**에서 언급되었습니다. 관련 내용은 다음 글에서 확인할 수 있습니다.
%{post_url}
github_commits_populator:
errors:
repository_identifier_invalid_pm_title: "GitHub 연동 플러그인 설정을 확인하세요"
repository_identifier_invalid_pm: |-
GitHub 연동 플러그인에 지정된 저장소 이름이 올바르지 않습니다: %{repo_name}
저장소는 `user/repo` 형식으로 입력해야 합니다.
<a href="%{base_path}/admin/site_settings/category/plugins?filter=GitHub">GitHub 연동 설정</a>에서 이름을 수정한 뒤 `github_badges_enabled`를 다시 켜기 전까지 배지를 지급하지 않습니다.
repository_not_found_pm_title: "GitHub 저장소를 찾을 수 없습니다"
repository_not_found_pm: |-
GitHub 연동 플러그인에 지정된 저장소를 찾을 수 없습니다: %{repo_name}
<a href="%{base_path}/admin/site_settings/category/plugins?filter=GitHub">GitHub 연동 설정</a>에서 저장소 이름과 접근 권한을 확인한 뒤 `github_badges_enabled`를 다시 켜세요.
invalid_octokit_credentials_pm_title: "GitHub 인증 정보를 확인하세요"
invalid_octokit_credentials_pm: |-
GitHub 연동 플러그인의 인증 정보가 올바르지 않습니다. 문제가 해결될 때까지 GitHub 배지 기능과 커밋 수집을 중지했습니다.
<a href="%{base_path}/admin/site_settings/category/plugins?filter=GitHub">GitHub 연동 설정</a>에서 연결 토큰이 올바른지 확인하세요. 토큰에는 설정한 모든 저장소를 읽을 권한이 있어야 합니다.
discourse_automation:
scriptables:
send_chat_message:
title: "채팅 메시지 보내기"
File diff suppressed because it is too large Load Diff
+111
View File
@@ -0,0 +1,111 @@
ko:
js:
discourse_workflows:
title: "업무 흐름"
new_workflow: "새 업무 흐름"
default_workflow_name: "내 업무 흐름"
edit: "편집"
save: "저장"
unpublished: "게시 안 됨"
keep_editing: "계속 편집"
workflow_name: "업무 흐름 이름"
last_execution_never: "실행 기록 없음"
tabs:
workflow: "업무 흐름"
executions: "실행 기록"
settings: "설정"
versions: "버전"
data_tables: "데이터 표"
templates: "서식"
credentials: "연결 정보"
variables: "변수"
all_workflows: "모든 업무 흐름"
topic:
operation: "작업"
operations:
create: "글 만들기"
get: "글 가져오기"
list: "글 목록 가져오기"
close: "글 닫기"
archive: "글 보관"
bump: "글을 최신 목록 위로 올리기"
set_custom_fields: "사용자 지정 항목 설정"
topic_id: "글 번호"
silent: "알림 없이 올리기"
silent_description: "새 활동 항목을 만들지 않고 글을 최신 목록 위로 올립니다."
raw: "본문"
raw_description: "첫 번째 글의 본문"
category_id: "카테고리"
query: "검색 조건"
executions:
title: "실행 기록"
workflow: "업무 흐름"
view_executions: "실행 기록 보기"
back_to_editor: "편집 화면으로 돌아가기"
status: "상태"
started_at: "시작 시각"
finished_at: "완료 시각"
run_time: "실행 시간"
expand: "상세 보기"
collapse: "접기"
details: "상세"
no_executions: "아직 실행 기록이 없습니다."
empty_title: "%{username}님의 업무 흐름은 아직 실행되지 않았습니다"
empty_description: "업무 흐름이 실행되면 활동을 확인하고 문제를 찾을 수 있도록 실행 기록이 여기에 표시됩니다."
delete_selected: "선택한 기록 삭제"
select: "선택"
cancel_select: "선택 취소"
show: "보기"
export: "내보내기"
input: "입력"
output: "출력"
logs: "기록"
total_time: "전체 시간:"
statuses:
pending: "대기 중"
running: "실행 중"
success: "완료"
error: "오류"
skipped: "건너뜀"
filtered: "조건에서 제외됨"
waiting: "대기 중"
kept: "유지됨"
rejected: "거부됨"
rate_limited: "실행 횟수 제한"
kinds:
trigger: "실행 조건"
condition: "조건"
action: "작업"
flow: "흐름"
stats:
executions: "실행 횟수"
failures: "실패 횟수"
failure_rate: "실패율"
avg_run_time: "평균 실행 시간"
period: "최근 7일"
credentials:
title: "연결 정보"
description: "외부 서비스 연결에 사용하는 인증 정보를 암호화해 보관합니다"
add: "연결 정보 추가"
edit: "연결 정보 편집"
name: "이름"
name_placeholder: "연결 정보 이름"
type: "종류"
created: "만든 시각"
no_credentials: "등록된 연결 정보가 없습니다."
delete_confirm: "'%{name}' 연결 정보를 삭제하시겠습니까?"
empty_title: "%{username}님의 연결 정보를 설정해 보세요"
empty_description: "업무 흐름에서 사용할 외부 서비스 인증 정보를 암호화해 저장합니다."
add_first: "첫 연결 정보 추가"
set_up_credential: "연결 정보 설정"
select_type: "종류 선택"
versions:
title: "버전"
version: "버전"
author: "작성자"
changed_at: "변경 시각"
current: "현재"
live: "사용 중"
revert: "이 버전으로 되돌리기"
admin:
title: "업무 흐름"
+235
View File
@@ -0,0 +1,235 @@
ko:
js:
discourse_workflows:
ai:
continue: "계속"
progress_title: "진행 상황"
add_node:
categories:
discourse_triggers: "포럼 실행 조건"
triggers: "실행 조건"
discourse_actions: "포럼 작업"
ai: "AI"
data: "데이터 처리"
utilities: "도구"
flow: "흐름 제어"
human_review: "담당자 확인"
configurator:
saving: "저장 중"
output_context: "출력"
schema_item_count:
one: "항목 %{count}개"
other: "항목 %{count}개"
nodes:
"trigger:topic_closed": "글 닫힘"
"trigger:post_created": "게시물 작성됨"
"trigger:post_edited": "게시물 수정됨"
"trigger:post_moved": "게시물 이동됨"
"trigger:post_destroyed": "게시물 삭제됨"
"trigger:post_recovered": "게시물 복구됨"
"trigger:topic_created": "글 작성됨"
"trigger:topic_category_changed": "글 카테고리 변경됨"
"trigger:topic_tag_changed": "글 태그 변경됨"
"action:topic_tags": "글 태그 변경"
"action:topic": "글 작업"
"action:post": "게시물 작업"
"action:flag_post": "게시물 신고"
"trigger:stale_topic": "오래된 글 확인"
"action:limit": "항목 수 제한"
"action:template": "서식 적용"
"trigger:topic_admin_button": "글 관리 버튼"
"action:topic_category": "글 카테고리 변경"
"action:topic_pin": "글 고정"
"action:topic_moderation": "글 관리"
node_descriptions:
"trigger:post_edited": "글의 첫 게시물이 수정되고 표시용 본문 변환을 마치면 실행됩니다"
"trigger:post_destroyed": "게시물이 삭제되면 실행됩니다. 실행 사용자는 작성자가 아니라 삭제한 사용자입니다"
"trigger:post_recovered": "삭제한 게시물이 복구되면 실행됩니다. 실행 사용자는 작성자가 아니라 복구한 사용자입니다"
"action:topic": "글을 만들고 가져오고 검색하며 닫거나 보관하고 사용자 지정 항목을 설정합니다"
"action:post": "게시물을 만들고 편집하고 삭제하고 복구하고 가져오거나 검색합니다"
"action:flag_post": "게시물을 스팸으로 신고하거나 검토 대기열에 추가합니다. 필요하면 게시물을 숨기거나 삭제하고 작성자를 이용 정지할 수 있습니다"
"trigger:stale_topic": "정해진 기간 동안 활동이 없는 글을 찾으면 실행됩니다"
"trigger:topic_admin_button": "글 관리 메뉴에 이 업무 흐름을 실행하는 버튼을 추가합니다"
"trigger:post_button": "게시물 작업 메뉴에 이 업무 흐름을 실행하는 버튼을 추가합니다"
"action:topic_category": "글의 카테고리를 설정하거나 지웁니다"
"action:topic_moderation": "글에 관리 작업을 수행합니다"
post_button:
post_number: "게시물 번호"
post_number_description: "각 글에서 이 번호의 게시물에만 버튼을 표시합니다. 모든 게시물에 표시하려면 비워 두세요."
post_number_placeholder: "모든 게시물"
position_anchor_description: "대상 버튼이 숨겨지거나 더 보기 메뉴로 이동된 경우에는 버튼 정렬 순서에만 영향을 줍니다."
position_anchor_flag: "신고"
position_anchor_edit: "편집"
position_anchor_bookmark: "북마크"
modal:
body: "본문"
label: "버튼 문구"
label_placeholder: "승인"
value_placeholder: "approve"
if:
conditions: "조건"
operators:
true: "참"
false: "거짓"
topic_tags:
topic_id: "글 번호"
actor_username: "실행 사용자"
tag_group:
actor_username: "실행 사용자"
topic_category:
topic_id: "글 번호"
category_id: "카테고리"
actor_username: "실행 사용자"
topic_pin:
operations:
add: "고정"
pin_types:
pin: "카테고리 상단 고정"
banner: "배너로 표시"
topic_id: "글 번호"
actor_username: "실행 사용자"
topic_moderation:
operations:
unlist_topic: "글 목록에서 숨기기"
topic_id: "글 번호"
actor_username: "실행 사용자"
group:
actor_username: "실행 사용자"
user:
updates: "변경할 항목"
bio_raw: "소개"
actor_username: "실행 사용자"
badge:
actor_username: "실행 사용자"
send_personal_message:
raw: "본문"
raw_description: "개인 메시지 본문"
post:
operations:
create: "게시물 작성"
edit: "게시물 편집"
get: "게시물 가져오기"
list: "게시물 검색"
delete: "게시물 삭제"
recover: "게시물 복구"
topic_id: "글 번호"
raw: "본문"
raw_description: "게시물 원문"
reply_to_post_number: "답글을 달 게시물 번호"
reply_to_post_number_description: "글 안의 특정 게시물에 답글을 작성합니다."
whisper: "관리자 전용 댓글로 작성"
whisper_description: "관리자만 볼 수 있는 댓글로 작성합니다. 작성자에게 관리자 전용 댓글 작성 권한이 있어야 합니다."
author_username: "작성자"
include_raw: "원문 포함"
include_cooked: "표시용 본문 포함"
body_character_limit_description: "원문과 표시용 본문에서 가져올 최대 글자 수입니다. 앞부분과 뒷부분을 절반씩 유지합니다. 제한하지 않으려면 0을 입력하세요."
query: "검색 조건"
query_description: "PostsFilter 문법으로 게시물을 검색합니다."
query_placeholder: "카테고리와 태그 및 작성자와 키워드 같은 조건으로 게시물을 검색합니다."
groups: "그룹"
post_types:
regular: "일반 게시물만"
all: "표시되는 모든 게시물"
first: "첫 게시물만"
moderator_action: "운영자 작업 기록만"
small_action: "간단한 작업 기록만"
reply: "댓글만"
whisper: "관리자 전용 댓글만"
statuses:
listed: "목록에 표시"
public: "공개"
noreplies: "댓글 없음"
order: "정렬"
limit: "최대 결과 수"
actor_username: "실행 사용자"
flag_post:
flag_types:
review_delete: "검토 대기열에 추가하고 게시물 삭제"
review_delete_silence: "검토 대기열에 추가하고 게시물을 삭제한 뒤 사용자 이용 정지"
spam_silence: "스팸으로 신고하고 게시물을 숨긴 뒤 사용자 이용 정지"
actor_username: "실행 사용자"
flag_user:
actor_username: "실행 사용자"
topic:
key: "키"
limit: "최대 결과 수"
actor_username: "실행 사용자"
set_fields:
assignments: "변경할 항목"
reference_pill:
source:
trigger: "실행 조건"
item_index: "항목 순서"
node_preview:
items:
one: "항목 %{count}개"
other: "항목 %{count}개"
nested_items:
one: "[항목 %{count}개]"
other: "[항목 %{count}개]"
property_engine:
assignment_types:
string: "문자열"
boolean: "참 또는 거짓"
fields:
changed_fields_bio: "소개"
group_ids: "그룹"
max_items_reached:
one: "최대 %{count}개 항목"
other: "최대 %{count}개 항목"
webhook:
authentication: "인증"
credential: "연결 정보"
http_request:
authentication: "인증"
authentication_bearer_token: "전달 토큰"
credential: "연결 정보"
key_placeholder: "키"
content_type_raw: "원문"
workflow_call:
fields: "항목"
split_out:
field_placeholder: "items"
all_other_fields: "나머지 모든 항목"
sort:
order: "정렬 방향"
summarize:
fields_to_split_by: "묶을 기준 항목"
add_fields_to_summarize: "계산 항목 추가"
merge:
resolve_clash: "같은 항목 이름이 있을 때"
log:
key: "키"
template:
template: "서식"
form:
field_label: "항목 이름"
authentication: "접근 권한"
production_url: "실제 사용 주소"
submit: "제출"
data_table_node:
resource: "대상"
resources:
row: "행"
operations:
insert: "추가"
get: "가져오기"
filter: "조건"
limit: "최대 결과 수"
executions:
items: "항목"
variables:
key: "키"
creator: "작성자"
data_tables:
column_types:
string: "문자열"
boolean: "참 또는 거짓"
canvas:
clipboard_text: "Discourse 업무 흐름에서 복사한 단계입니다. 다른 업무 흐름 편집기에 붙여 넣어 복제할 수 있습니다."
export_nodes: "내보내기"
pin_data:
save: "저장"
post_moved:
category_ids_description: "이 카테고리로 이동된 게시물에 대해서만 실행됩니다. 모든 카테고리에 적용하려면 비워 두세요."
tag_names_description: "이 태그 중 하나 이상이 있는 글로 이동된 게시물에 대해서만 실행됩니다."
@@ -1,5 +1,8 @@
# 사용자 화면 한글화 진행 기록 # 사용자 화면 한글화 진행 기록
> 완료 확정 · 2026-09-27
> 후속 번역과 플러그인 검수를 마쳤다. 활성 일반 화면과 관리자 화면의 번역 누락은 0개다. 아래 내용은 2026-09-25 당시의 진행 상태를 보존한 기록이다. 최종 결과와 배포 절차는 `2026-09-27-localization-complete-and-deployment.md`에서 확인한다.
- 작업일: 2026-09-25 - 작업일: 2026-09-25
- 담당: 이창현 - 담당: 이창현
- 대상: `https://forum.itosm.co.kr` - 대상: `https://forum.itosm.co.kr`
@@ -0,0 +1,36 @@
# Discourse 한국어 번역 완료와 Gitea 배포 준비
## 작업 결과
- 활성 일반 화면 번역 누락 0개
- 활성 관리자 화면 번역 누락 0개
- 활성 플러그인 29개의 번역 키 6,005개 중 누락 0개
- 원문과 값이 같은 항목 216개는 브랜드명과 형식 문자열 및 주소 예시로 확인
- PC 화면과 390픽셀 모바일 화면에서 가로 넘침 없음
- 브라우저 오류 0건
- 운영 포럼 HTTP 200 확인
## 최종 화면 보완
보드 설정의 조건 안내에 내부 설정명 `Boards enabled`가 노출되는 문제를 확인했다. 관리자 설정 이름 대응표에 `보드 사용`을 추가해 운영 화면에서 한국어로 표시되는 것을 확인했다.
## Gitea 배포 구조
저장소에 전체 번역 파일과 관리자 보완 코드 및 기능별 검수 원본을 보관한다. `yarn test`는 Ruby와 YAML 구조를 검사한다. `yarn deploy`는 검사 통과 후 실행에 필요한 파일만 `app` 컨테이너에 복사하고 해당 컨테이너만 다시 시작한다. 마지막으로 포럼의 HTTP 200 응답을 확인한다.
자동 백업은 넣지 않았다. 백업은 사용자가 요청한 작업에서만 별도로 진행한다.
## 운영 반영
| 항목 | 결과 |
|---|---|
| 운영 플러그인 | `/var/www/discourse/plugins/itosm-admin-korean` |
| 대상 컨테이너 | `app` |
| 데이터베이스 구조 변경 | 없음 |
| 데이터 이전 | 없음 |
| 운영 응답 | HTTP 200 |
| 백업 | 요청 없음으로 진행하지 않음 |
## 검증 범위
업무 흐름과 보드와 RSS 가져오기와 외부 채팅 연동 및 GitHub 연동과 자동화 화면을 PC에서 확인했다. 일반 최신 글과 설치된 플러그인 화면은 PC와 모바일에서 확인했다. 모든 확인 화면에서 가로 넘침이 없었다.
+1
View File
@@ -13,6 +13,7 @@ Trilium의 `Ubuntu 서버 운영` 바로 아래에 만든 전용 분류다. 포
| 한국어 번역 기반 작업 · 2026-09-20 | `dZobqUl4a5ZH` | 사용자 화면과 관리자 화면의 초기 번역 기반 기록 | | 한국어 번역 기반 작업 · 2026-09-20 | `dZobqUl4a5ZH` | 사용자 화면과 관리자 화면의 초기 번역 기반 기록 |
| 관리자 한글화 운영 기록 · 2026-09-20 | `N0v5eGIKqa28` | 설정 이름 보완 플러그인과 운영 반영 및 검증 기록 | | 관리자 한글화 운영 기록 · 2026-09-20 | `N0v5eGIKqa28` | 설정 이름 보완 플러그인과 운영 반영 및 검증 기록 |
| 사용자 화면 한글화 진행 기록 · 2026-09-25 | `m6Epi7pHAylp` | 네 차례 번역 묶음과 운영 반영 및 남은 분량 기록 | | 사용자 화면 한글화 진행 기록 · 2026-09-25 | `m6Epi7pHAylp` | 네 차례 번역 묶음과 운영 반영 및 남은 분량 기록 |
| Discourse 한국어 번역 완료와 Gitea 배포 준비 · 2026-09-27 | `xmOe7b06fKOo` | 최종 번역 검사와 모바일 확인 및 Gitea 배포 절차 |
## 작성 기준 ## 작성 기준
+4 -2
View File
@@ -9,14 +9,16 @@
"scripts": { "scripts": {
"test": "yarn check:ruby && yarn check:yaml", "test": "yarn check:ruby && yarn check:yaml",
"check:ruby": "ruby -c plugin.rb", "check:ruby": "ruby -c plugin.rb",
"check:yaml": "ruby -e 'require %q(yaml); YAML.load_file(%q(config/locales/server.ko.yml)); puts %q(YAML_OK)'" "check:yaml": "ruby scripts/validate_locales.rb",
"deploy": "bash scripts/deploy.sh",
"verify:deployment": "bash scripts/verify_deployment.sh"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gitea.itosm.co.kr/syskiller/discourse-korean-localization.git" "url": "https://gitea.itosm.co.kr/syskiller/discourse-korean-localization.git"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "이창현 (Lee Chang Hyun) <syskiller@naver.com>",
"license": "ISC", "license": "ISC",
"private": true, "private": true,
"packageManager": "yarn@4.0.0-rc.40" "packageManager": "yarn@4.0.0-rc.40"
File diff suppressed because it is too large Load Diff
+527
View File
@@ -0,0 +1,527 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DMenu from "discourse/float-kit/components/d-menu";
import { eq } from "discourse/truth-helpers";
import DButton from "discourse/ui-kit/d-button";
import DDropdownMenu from "discourse/ui-kit/d-dropdown-menu";
import dIcon from "discourse/ui-kit/helpers/d-icon";
import dDragAndDropAutoScroll from "discourse/ui-kit/modifiers/d-drag-and-drop-auto-scroll";
import dDragAndDropTarget from "discourse/ui-kit/modifiers/d-drag-and-drop-target";
import { i18n } from "discourse-i18n";
import { isRecencyColumn } from "../lib/boards-card-ordering";
import {
columnColorVariable,
hasColumnColor,
} from "../lib/boards-column-helpers";
import { animateCardReorder, captureCardRects } from "../lib/boards-motion";
import BoardsCard from "./boards-card";
import BoardsAddTopicAsCardModal from "./modal/boards-add-topic-as-card";
import BoardsCardDetailModal from "./modal/boards-card-detail";
const RECENCY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
export function recencyTimestamp(card) {
const value = Date.parse(card?.recency_at || "");
return Number.isFinite(value) ? value : 0;
}
export function recencyDropIndicatorInsertBefore(
cardsContainer,
cardElements,
draggedCardId
) {
return (
cardElements.find((cardEl) => {
const elCardId = parseInt(cardEl.dataset.cardId, 10);
return elCardId !== draggedCardId;
}) || cardsContainer.querySelector(".discourse-boards-column__show-all")
);
}
export function shouldAnimateDropIndicatorPlacement({
hadIndicator,
columnId,
fromColumnId,
hasPlacedIndicator,
}) {
return hadIndicator || columnId !== fromColumnId || hasPlacedIndicator;
}
export default class BoardsColumn extends Component {
@service modal;
@tracked showAllCards = false;
get cardCount() {
return this.args.column.cards?.length || 0;
}
get visibleCards() {
const cards = this.args.column.cards || [];
if (!this.isRecencySorted || this.showAllCards) {
return cards;
}
const cutoff = Date.now() - RECENCY_WINDOW_MS;
return cards.filter(
(card) =>
card.id === this.args.linkedCardId || recencyTimestamp(card) >= cutoff
);
}
get hiddenCardCount() {
if (!this.isRecencySorted || this.showAllCards) {
return 0;
}
return this.cardCount - this.visibleCards.length;
}
get isRecencySorted() {
return isRecencyColumn(this.args.column);
}
get columnTags() {
const allColumns = this.args.allColumns || [];
return allColumns.map((col) => col.tag_name).filter(Boolean);
}
get columnIndex() {
const allColumns = this.args.allColumns || [];
return allColumns.findIndex((col) => col.id === this.args.column.id);
}
get lastColumnIndex() {
return (this.args.allColumns?.length || 0) - 1;
}
@action
showAllOlderCards() {
this.showAllCards = true;
}
@action
async startAddCard(closeMenu) {
await closeMenu();
this.modal.show(BoardsCardDetailModal, {
model: {
card: {},
isNew: true,
board: this.args.board,
onCreateCard: (data) =>
this.args.onAddCard({ ...data, columnId: this.args.column.id }),
},
});
}
@action
async addTopicAsCard(closeMenu) {
await closeMenu();
this.modal.show(BoardsAddTopicAsCardModal, {
model: {
onAddTopicAsCard: (data) =>
this.args.onAddCard({ ...data, columnId: this.args.column.id }),
},
});
}
@action
editColumn(closeMenu) {
closeMenu();
this.args.onEditColumn(this.args.column.id);
}
@action
moveLeft(closeMenu) {
closeMenu();
this.args.onMoveColumn(this.args.column.id, -1);
}
@action
moveRight(closeMenu) {
closeMenu();
this.args.onMoveColumn(this.args.column.id, 1);
}
@action
clearColumn(closeMenu) {
closeMenu();
this.args.onClearColumn(this.args.column.id);
}
@action
deleteColumn(closeMenu) {
closeMenu();
this.args.onDeleteColumn(this.args.column);
}
@action
canDropCard({ source }) {
// Recency columns sort themselves, so reordering within one is meaningless.
return !(
this.isRecencySorted && source.data.fromColumnId === this.args.column.id
);
}
@action
dragOver({ element, location }) {
const dragData = this.args.dragData;
if (!dragData) {
return;
}
element.classList.add("discourse-boards-column--drag-target");
const cardsContainer = element.querySelector(
".discourse-boards-column__cards"
);
if (!cardsContainer) {
return;
}
const { clientY } = location.current.input;
let indicator = cardsContainer.querySelector(
".discourse-boards-column__drop-indicator"
);
const hadIndicator = !!indicator;
if (!indicator) {
indicator = document.createElement("div");
indicator.className = "discourse-boards-column__drop-indicator";
}
indicator.style.height = `${dragData.cardHeight}px`;
const cardElements = [
...cardsContainer.querySelectorAll(".discourse-boards-card"),
];
let insertBefore = null;
if (this.isRecencySorted) {
insertBefore = recencyDropIndicatorInsertBefore(
cardsContainer,
cardElements,
dragData.cardId
);
} else {
for (const cardEl of cardElements) {
const elCardId = parseInt(cardEl.dataset.cardId, 10);
if (elCardId === dragData.cardId) {
continue;
}
const rect = cardEl.getBoundingClientRect();
if (clientY <= rect.top + rect.height / 2) {
insertBefore = cardEl;
break;
}
}
}
const emptyMsg = cardsContainer.querySelector(
".discourse-boards-column__empty"
);
if (emptyMsg) {
emptyMsg.hidden = true;
}
if (
this.#indicatorMatchesPosition(cardsContainer, indicator, insertBefore)
) {
return;
}
const shouldAnimate = shouldAnimateDropIndicatorPlacement({
hadIndicator,
columnId: this.args.column.id,
fromColumnId: dragData.fromColumnId,
hasPlacedIndicator: dragData.hasPlacedIndicator,
});
const previousRects = shouldAnimate
? captureCardRects(cardsContainer, {
skipCardIds: [dragData.cardId],
})
: null;
indicator.classList.remove(
"discourse-boards-column__drop-indicator--source"
);
if (insertBefore) {
cardsContainer.insertBefore(indicator, insertBefore);
} else {
cardsContainer.appendChild(indicator);
}
if (shouldAnimate) {
animateCardReorder(cardsContainer, previousRects, {
skipCardIds: [dragData.cardId],
});
}
dragData.hasPlacedIndicator = true;
}
@action
dragLeave({ element }) {
element.classList.remove("discourse-boards-column--drag-target");
this.removeDropIndicator(element, { animate: true });
}
@action
drop({ element, location }) {
element.classList.remove("discourse-boards-column--drag-target");
const dragData = this.args.dragData;
if (!dragData) {
this.removeDropIndicator(element, { animate: false });
return;
}
const cardsContainer = element.querySelector(
".discourse-boards-column__cards"
);
const { clientY } = location.current.input;
let afterCardId = null;
if (cardsContainer && !this.isRecencySorted) {
const cardElements = [
...cardsContainer.querySelectorAll(".discourse-boards-card"),
];
for (const cardEl of cardElements) {
const elCardId = parseInt(cardEl.dataset.cardId, 10);
if (elCardId === dragData.cardId) {
continue;
}
const rect = cardEl.getBoundingClientRect();
if (clientY > rect.top + rect.height / 2) {
afterCardId = elCardId;
}
}
}
// The board viewer removes the placeholder once the move settles.
this.args.onDrop(
dragData.cardId,
this.args.column.id,
afterCardId,
dragData.fromColumnId
);
}
removeDropIndicator(columnEl, { animate = false } = {}) {
const cardsContainer = columnEl.querySelector(
".discourse-boards-column__cards"
);
const indicator = columnEl.querySelector(
".discourse-boards-column__drop-indicator"
);
const dragData = this.args.dragData;
if (!indicator) {
columnEl
.querySelector(".discourse-boards-column__empty")
?.removeAttribute("hidden");
return;
}
const previousRects =
animate && cardsContainer
? captureCardRects(cardsContainer, {
skipCardIds: dragData ? [dragData.cardId] : [],
})
: null;
indicator.remove();
const emptyMsg = columnEl.querySelector(".discourse-boards-column__empty");
if (emptyMsg) {
emptyMsg.hidden = false;
}
if (cardsContainer && previousRects) {
animateCardReorder(cardsContainer, previousRects, {
skipCardIds: dragData ? [dragData.cardId] : [],
});
}
}
#indicatorMatchesPosition(cardsContainer, indicator, insertBefore) {
if (indicator.parentElement !== cardsContainer) {
return false;
}
if (insertBefore) {
return indicator.nextElementSibling === insertBefore;
}
return indicator === cardsContainer.lastElementChild;
}
<template>
<div
class="discourse-boards-column
{{if
(hasColumnColor @column.color)
'discourse-boards-column--has-color'
}}"
data-column-id={{@column.id}}
data-default-sort={{@column.default_sort}}
style={{columnColorVariable @column.color}}
{{dDragAndDropTarget
accepts="boards-card"
canDrop=this.canDropCard
indicator=false
onDrag=this.dragOver
onDragEnter=this.dragOver
onDragLeave=this.dragLeave
onDrop=this.drop
}}
>
<div class="discourse-boards-column__header">
<span class="discourse-boards-column__header-content">
<span class="discourse-boards-column__title">
{{#if @column.icon}}{{dIcon @column.icon}}{{/if}}
{{@column.fancyTitle}}
</span>
<span class="discourse-boards-column__count">
{{this.cardCount}}
</span>
</span>
{{#if @board.canManage}}
<DMenu
@icon="ellipsis"
@identifier="boards-column-controls"
@title="열 메뉴"
@triggerClass="btn-flat btn-small discourse-boards-column__menu-trigger"
>
<:content as |args|>
<DDropdownMenu as |dropdown|>
<dropdown.item>
<DButton
class="btn-transparent discourse-boards-column__menu-edit"
@action={{fn this.editColumn args.close}}
@icon="pencil"
@label="boards.board.edit_column"
/>
</dropdown.item>
<dropdown.item>
<DButton
class="btn-transparent discourse-boards-column__menu-move-left"
@action={{fn this.moveLeft args.close}}
@disabled={{eq this.columnIndex 0}}
@icon="arrow-left"
@label="boards.board.move_left"
/>
</dropdown.item>
<dropdown.item>
<DButton
class="btn-transparent discourse-boards-column__menu-move-right"
@action={{fn this.moveRight args.close}}
@disabled={{eq this.columnIndex this.lastColumnIndex}}
@icon="arrow-right"
@label="boards.board.move_right"
/>
</dropdown.item>
<dropdown.item>
<DButton
class="btn-transparent discourse-boards-column__menu-clear"
@action={{fn this.clearColumn args.close}}
@disabled={{eq this.cardCount 0}}
@icon="xmark"
@label="boards.board.clear_column"
/>
</dropdown.item>
<dropdown.item>
<DButton
class="btn-transparent btn-danger discourse-boards-column__menu-delete"
@action={{fn this.deleteColumn args.close}}
@icon="trash-can"
@label="boards.board.delete_column"
/>
</dropdown.item>
</DDropdownMenu>
</:content>
</DMenu>
{{/if}}
</div>
<div
class="discourse-boards-column__cards"
{{dDragAndDropAutoScroll types="boards-card"}}
>
{{#if this.visibleCards.length}}
{{#each this.visibleCards key="id" as |card|}}
<BoardsCard
@allSameCategory={{@allSameCategory}}
@board={{@board}}
@card={{card}}
@columnColor={{@column.color}}
@columnIcon={{@column.icon}}
@columnTags={{this.columnTags}}
@columnTitle={{@column.fancyTitle}}
@isDropHighlighted={{eq @dropHighlightCardId card.id}}
@isLinkHighlighted={{eq @linkHighlightCardId card.id}}
@isPendingDrop={{eq @pendingDropCardId card.id}}
@onDeleteCard={{@onDeleteCard}}
@onDragEnd={{@onDragEnd}}
@onDragStart={{@onDragStart}}
@onPromoteToTopic={{fn @onPromoteToTopic card.id}}
@onRefreshBoard={{@onRefreshBoard}}
@onUpdateCard={{@onUpdateCard}}
/>
{{/each}}
{{else if (eq this.cardCount 0)}}
<div class="discourse-boards-column__empty">
{{i18n "boards.board.no_cards"}}
</div>
{{/if}}
{{#if this.hiddenCardCount}}
<button
class="discourse-boards-column__show-all"
type="button"
{{on "click" this.showAllOlderCards}}
>
{{i18n "boards.board.show_older_cards" count=this.hiddenCardCount}}
</button>
{{/if}}
</div>
{{#if @board.canWrite}}
<div class="discourse-boards-column__footer">
<DMenu
@icon="plus"
@identifier="boards-column-add"
@label={{i18n "boards.board.add_card"}}
@triggerClass="discourse-boards-column__add-btn"
>
<:content as |args|>
<DDropdownMenu as |dropdown|>
<dropdown.item>
<DButton
class="btn-transparent"
@action={{fn this.startAddCard args.close}}
@icon="plus"
@label="boards.board.add_card"
/>
</dropdown.item>
<dropdown.item>
<DButton
class="btn-transparent"
@action={{fn this.addTopicAsCard args.close}}
@icon="link"
@label="boards.board.add_topic_as_card"
/>
</dropdown.item>
</DDropdownMenu>
</:content>
</DMenu>
</div>
{{/if}}
</div>
</template>
}
+1
View File
@@ -1197,6 +1197,7 @@ module ::ItosmAdminKorean
welcome_banner_text_color: "환영 배너 글자색", welcome_banner_text_color: "환영 배너 글자색",
welcome_banner_location: "환영 배너 위치", welcome_banner_location: "환영 배너 위치",
welcome_banner_page_visibility: "환영 배너 표시 화면", welcome_banner_page_visibility: "환영 배너 표시 화면",
boards_enabled: "보드 사용",
boards_manage_board_allowed_groups: "보드 관리 허용 그룹", boards_manage_board_allowed_groups: "보드 관리 허용 그룹",
chat_enabled: "채팅 사용", chat_enabled: "채팅 사용",
enable_public_channels: "공개 채널 사용", enable_public_channels: "공개 채널 사용",
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
container="${DISCOURSE_CONTAINER:-app}"
plugin_path="/var/www/discourse/plugins/itosm-admin-korean"
cd "$root_dir"
corepack yarn test
docker inspect "$container" >/dev/null
docker exec "$container" mkdir -p "$plugin_path/config/locales" "$plugin_path/assets"
docker cp plugin.rb "$container:$plugin_path/plugin.rb"
docker cp config/locales/client.ko.yml "$container:$plugin_path/config/locales/client.ko.yml"
docker cp config/locales/server.ko.yml "$container:$plugin_path/config/locales/server.ko.yml"
docker cp assets/. "$container:$plugin_path/assets/"
docker restart "$container" >/dev/null
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if DISCOURSE_CONTAINER="$container" bash scripts/verify_deployment.sh; then
echo "Discourse 한국어 보완 배포 완료"
exit 0
fi
sleep 2
done
echo "컨테이너 재시작 뒤 서비스가 정상 상태로 돌아오지 않았습니다." >&2
exit 1
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
require "yaml"
paths = Dir.glob(File.expand_path("../config/locales/*.yml", __dir__)).sort
abort "검사할 한국어 번역 파일이 없습니다." if paths.empty?
paths.each do |path|
data = YAML.safe_load_file(path, aliases: true)
abort "한국어 최상위 키가 없습니다: #{path}" unless data.is_a?(Hash) && data.key?("ko")
end
puts "한국어 YAML #{paths.length}개 검사 통과"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
container="${DISCOURSE_CONTAINER:-app}"
site_url="${DISCOURSE_URL:-https://forum.itosm.co.kr}"
plugin_path="/var/www/discourse/plugins/itosm-admin-korean"
docker inspect "$container" >/dev/null
docker exec "$container" test -f "$plugin_path/plugin.rb"
docker exec "$container" test -f "$plugin_path/config/locales/client.ko.yml"
docker exec "$container" test -f "$plugin_path/config/locales/server.ko.yml"
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "$site_url")"
if [[ "$status" != "200" ]]; then
echo "포럼 응답 확인 실패: HTTP $status" >&2
exit 1
fi
echo "운영 플러그인 파일 확인 완료"
echo "포럼 응답 확인 완료: HTTP 200"
+12
View File
@@ -40,3 +40,15 @@ Discourse 한국어 보완 수정 기록
- 제한 사항: 4차 300개 묶음은 화면별 문맥 검수가 남아 있다. 다음 묶음부터 300개 단위로 번역과 문맥 검수를 이어 간다. - 제한 사항: 4차 300개 묶음은 화면별 문맥 검수가 남아 있다. 다음 묶음부터 300개 단위로 번역과 문맥 검수를 이어 간다.
- SSH 보호: 현재 접속 출발지를 Fail2ban SSH 차단 제외 목록에 반영하고 새 SSH 연결을 확인했다. 실제 주소는 기록하지 않았다. - SSH 보호: 현재 접속 출발지를 Fail2ban SSH 차단 제외 목록에 반영하고 새 SSH 연결을 확인했다. 실제 주소는 기록하지 않았다.
- 백업: 사용자 지시에 따라 이번 기록 시점에는 만들지 않았다. - 백업: 사용자 지시에 따라 이번 기록 시점에는 만들지 않았다.
## 2026-09-27 한국어 번역 완료와 Gitea 배포 준비
- 작업 대상: https://forum.itosm.co.kr
- 변경 이유: 일반 화면과 관리자 화면 및 활성 플러그인에 남은 번역을 마무리하고 Gitea 저장소에서 다시 배포할 수 있게 정리했다.
- 변경 내용: 활성 번역 누락을 0개로 정리했다. 보드 설정에 노출되던 `Boards enabled`를 `보드 사용`으로 바꿨다. 기능별 검수 원본과 보드 화면 패치 원본을 저장소에 포함했다.
- 배포 기능: `yarn test`와 `yarn deploy` 및 `yarn verify:deployment` 명령을 추가했다. 배포 명령은 검사 통과 후 실행 파일만 운영 플러그인 경로에 복사하고 `app` 컨테이너만 다시 시작한 뒤 HTTP 200 응답을 확인한다.
- 검사: 활성 플러그인 29개의 번역 키 6,005개 중 누락 0개를 확인했다. 전체 활성 일반 화면과 관리자 화면도 누락 0개다. 원문 동일 항목 216개는 브랜드명과 형식 문자열 및 주소 예시로 확인했다.
- 화면 확인: 업무 흐름과 보드와 RSS 가져오기와 외부 채팅 연동 및 GitHub 연동과 자동화 화면을 PC에서 확인했다. 일반 최신 글과 설치된 플러그인 화면은 PC와 390픽셀 모바일에서 확인했다. 가로 넘침과 브라우저 오류는 없었다.
- 데이터 변경: 데이터베이스 구조 변경과 데이터 이전은 없다.
- 운영 반영: 운영 `app` 컨테이너만 다시 시작했다. 포럼 HTTP 200 응답을 확인했다.
- 백업: 사용자 요청이 없어 진행하지 않았다.