2 Commits
Author SHA1 Message Date
syskiller 5bad207526 포럼 한국어 번역 플러그인을 복구하고 배포 검사를 보강
비어 있던 운영 플러그인 경로를 전용 서비스 원본으로 복구했다.
음성 입장 안내와 주변 메뉴 및 관리자 번역을 실제 화면에서 확인했다.
관리자 주소용 내부 이름을 보존하고 보드 메뉴 두 곳의 제목을 복구했다.

검사: Ruby 문법과 YAML 11개 통과. 활성 번역 6004개와 음성 번역 561개 누락 0개.
일반 화면과 관리자 번역 및 보드 자산의 제공 해시를 확인했다.
PC와 390픽셀 모바일 안내창 및 관리자 메뉴 주소를 확인했다.
운영: app 컨테이너만 갱신. DB 구조 변경과 데이터 이전 없음.
백업: /var/discourse/shared/standalone/backups/forum-korean-20261015-1150
설정 백업은 별도 비밀 디렉터리의 권한 600 파일로 보관했다.
제한: DB 복원 시험과 다음 컨테이너 재생성은 이번 검사에 포함하지 않았다.
Trilium: Ubuntu 서버 운영 아래 Discourse 한국어 배포에 기록했다.
2026-10-14 22:44:52 +00:00
syskiller ce3a2acee7 ITOSM 피아노 학습실 포럼 구성 기록
전용 카테고리와 안내 글 및 공개 권한과 모바일 검사 결과를 정리했다. 운영에는 카테고리와 주제 및 글쓰기 양식만 반영했다. 데이터베이스 구조 변경과 컨테이너 재시작은 없었다. 사용자 요청이 없어 백업은 진행하지 않았다. Trilium 기록은 fb6Wi8BIcf0U에 저장했다.
2026-10-04 12:07:29 +00:00
14 changed files with 315 additions and 25 deletions
+24
View File
@@ -55,3 +55,27 @@
- `yarn test`와 `yarn deploy` 및 `yarn verify:deployment` 명령을 추가해 Gitea 저장소에서 검사와 운영 배포를 수행할 수 있게 했다.
- 운영 `app` 컨테이너만 갱신했으며 포럼의 HTTP 200 응답을 확인했다.
- 데이터베이스 구조 변경과 데이터 이전은 없다. 사용자 요청이 없어 백업은 진행하지 않았다.
## 2026-10-04 ITOSM 피아노 학습실 포럼 구성
- 정식 서비스 이름을 `ITOSM 피아노 학습실`로 확인하고 `https://learn.itosm.co.kr`에 연결되는 전용 포럼 공간을 만들었다.
- 상위 카테고리와 `시작 안내와 사용법` 및 `곡 공유와 연습 자료`와 `질문과 문제 해결` 및 `오류 신고와 개선 제안` 하위 카테고리를 구성했다.
- 서비스 소개와 처음 시작하는 방법 및 MIDI와 악보 공유 안내와 난이도별 연습 정보와 질문 및 오류 작성 안내를 등록했다.
- 자료 공유 양식에 원본 출처와 저작권 상태 및 이용 허가와 재배포 가능 여부를 필수 확인 항목으로 넣었다.
- 다섯 개 카테고리는 비회원이 볼 수 있다. 회원은 글을 작성할 수 있다. 안내 글 여섯 개도 비회원 공개 상태다.
- 서비스와 포럼 및 안내 글 여섯 개가 모두 HTTP 200으로 응답했다. 390×844 모바일 화면에서는 가로 넘침이 없었다.
- 임시 이름은 제목과 본문 및 글쓰기 양식에서 모두 제거했다. `NoteRain` 표기도 사용하지 않았다.
- 기존 포럼 설정과 게시물은 삭제하지 않았다. 데이터베이스 구조 변경과 데이터 이전도 없다.
- 사용자 요청이 없어 백업을 만들지 않았다. 컨테이너 재시작도 하지 않았다.
- 실제 MIDI 장치를 이용한 서비스 동작 검사는 피아노 학습 서비스 자체의 장치 검증 작업으로 남는다.
## 2026-10-15 운영 서버 한국어 번역 복구
- 이전된 서버의 한국어 보완 플러그인 경로가 비어 있음을 확인했다. 전용 원본을 복구하고 포럼 컨테이너만 재시작했다.
- 음성 대화방 입장 안내와 주변 메뉴 및 관리자 번역을 복구했다. 활성 플러그인 고유 번역 6,004개와 음성 기능 561개를 검사했다. 누락은 0개다.
- 관리자 주소에 쓰이는 내부 플러그인 이름은 보존하도록 수정했다. 보드 메뉴 두 곳의 제목도 복구했다.
- 실제 웹 번역과 보드 자산 해시 대조를 배포 검사에 추가했다. PC와 모바일의 안내창 및 관리자 메뉴 주소를 확인했다.
- DB 구조 변경과 데이터 이전은 없다. 기존 글꼴과 게시물을 보존했다.
- 백업: `/var/discourse/shared/standalone/backups/forum-korean-20261015-1150`
- 상세 범위와 복구 방법: [한국어 복구 기록](docs/2026-10-15-localization-recovery.md)
+3 -1
View File
@@ -39,7 +39,9 @@ yarn deploy
DISCOURSE_CONTAINER=app DISCOURSE_URL=https://forum.itosm.co.kr yarn deploy
```
배포 명령은 검사 통과 후 실행 파일만 `itosm-admin-korean` 플러그인 경로에 복사한다. 이후 대상 컨테이너만 다시 시작하고 HTTP 200 응답을 확인한다. 백업은 자동으로 만들지 않는다. 백업이 필요한 작업은 별도 승인을 받은 뒤 먼저 진행한다.
배포는 영구 원본인 `/home/syskiller/discourse-korean-localization`에서 실행한다. 다른 설치 위치를 사용하는 경우 `DISCOURSE_PLUGIN_SOURCE`를 지정한다. 컨테이너 생성 설정에도 같은 원본을 `/var/www/discourse/plugins/itosm-korean-localization`로 연결해야 한다. 이전 `itosm-admin-korean` 플러그인과 중복 설치하지 않는다.
배포 명령은 검사 통과 후 실행 파일을 복사한다. 별도 Rails 부팅에서 플러그인 로딩과 음성 대화 안내 및 활성 플러그인의 번역 누락을 검사한 뒤 `app`만 재시작한다. 마지막으로 사용자 화면과 관리자 화면에서 실제 제공하는 번역 묶음의 해시까지 대조한다. HTTP 200만으로 성공 처리하지 않는다. 원본과 설정 및 필요한 데이터는 배포 전에 작업별로 백업한다.
배포하지 않고 운영 상태만 확인하려면 다음 명령을 사용한다.
+63
View File
@@ -0,0 +1,63 @@
# ITOSM 피아노 학습실 포럼 구성
## 적용 목적
`https://learn.itosm.co.kr`에서 운영하는 ITOSM 피아노 학습실의 사용법과 연습 자료 및 질문과 오류를 목적별로 나눠 관리한다. 이 기록은 Discourse 운영 소유 문서다. 게임 프로젝트 저장소와 분리한다.
## 공개 주소
- 서비스: https://learn.itosm.co.kr
- 포럼: https://forum.itosm.co.kr/c/itosm-piano-study/24
- 소개와 바로가기: https://forum.itosm.co.kr/t/itosm/52
- 처음 시작하는 방법: https://forum.itosm.co.kr/t/itosm/53
- MIDI와 악보 및 연습 자료 공유 안내: https://forum.itosm.co.kr/t/midi/54
- 난이도별 연습 정보 작성 예시: https://forum.itosm.co.kr/t/topic/55
- 질문과 문제 해결 글 작성 안내: https://forum.itosm.co.kr/t/topic/56
- 오류 신고와 개선 제안 작성 안내: https://forum.itosm.co.kr/t/topic/57
## 카테고리 구성
| ID | 이름 | 슬러그 | 공개 범위 |
|---:|---|---|---|
| 24 | ITOSM 피아노 학습실 | `itosm-piano-study` | 비회원 읽기와 회원 작성 |
| 25 | 시작 안내와 사용법 | `piano-start` | 비회원 읽기와 회원 작성 |
| 26 | 곡 공유와 연습 자료 | `piano-library` | 비회원 읽기와 회원 작성 |
| 27 | 질문과 문제 해결 | `piano-help` | 비회원 읽기와 회원 작성 |
| 28 | 오류 신고와 개선 제안 | `piano-feedback` | 비회원 읽기와 회원 작성 |
## 자료 공유 기준
곡 공유와 연습 자료의 글쓰기 양식에는 다음 항목을 넣었다.
- 곡 이름과 작곡가
- 편곡자 또는 MIDI 제작자
- 원본을 확인할 수 있는 출처 주소
- 저작권 상태와 이용 허가 및 재배포 가능 여부
- ITOSM 피아노 학습실에서 확인한 내용
- 추천 난이도와 연습 구간 및 목표 속도
직접 만든 자료와 이용 허가를 받은 자료 및 퍼블릭 도메인과 허용 조건이 분명한 자료만 첨부하도록 안내했다. 권리를 확인할 수 없는 파일은 첨부하지 않고 공식 출처 주소만 공유한다.
## 검사 결과
- 서비스와 포럼 및 안내 글 여섯 개가 모두 HTTP 200으로 응답했다.
- 다섯 개 카테고리와 안내 글은 비회원이 볼 수 있다.
- 회원은 다섯 개 카테고리에 새 글을 작성할 수 있다.
- PC 화면에서 정식 서비스 이름과 카테고리 및 안내 글 목록을 확인했다.
- 390×844 모바일 화면에서 가로 넘침이 없었다.
- 모바일 화면에서 카테고리 이름과 저작권 및 출처 안내가 표시됐다.
- 정식 이름은 `ITOSM 피아노 학습실`로 통일했다. 임시 이름은 제목과 본문 및 글쓰기 양식에서 0건이다.
- `NoteRain` 표기는 사용하지 않았다.
- 기존 포럼 설정과 게시물은 삭제하지 않았다.
## 운영 변경 범위
- 데이터베이스 구조 변경: 없음
- 데이터 이전: 없음
- 콘텐츠 변경: 카테고리와 주제와 게시물 및 글쓰기 양식
- 컨테이너 재시작: 없음
- 백업: 사용자 요청이 없어 진행하지 않음
## 남은 제한
실제 MIDI 장치를 이용한 서비스 동작 검사는 피아노 학습 서비스 자체의 장치 검증 작업으로 남는다. 이번 작업에서는 포럼의 공개 링크와 권한 및 PC와 모바일 표시를 확인했다.
File diff suppressed because one or more lines are too long
+45
View File
@@ -0,0 +1,45 @@
# 포럼 한국어 번역 복구
2026-10-15 · 이창현
## 원인과 수정
보조 서버의 `app` 컨테이너에는 한국어 보완 플러그인 연결 경로만 남아 있었다. 연결된 호스트 디렉터리가 비어 있어 플러그인을 불러오지 못했다. 데이터베이스에 따로 저장된 번역 530개는 남아 있어 일부 화면만 영어로 보였다. 음성 대화방 입장 안내의 한국어 원문은 전용 저장소에 이미 있었다.
전용 저장소 원본을 `/home/syskiller/discourse-korean-localization`에 복구했다. 게임 프로젝트 경로에 새 파일이나 바로가기를 만들지 않았다. 실행 플러그인 이름과 경로는 `itosm-korean-localization`로 맞췄다. 컨테이너 생성 설정의 연결 경로도 새 원본으로 바꿨다. 현재 컨테이너에는 실행 파일을 복사하고 재시작했다. 새 연결 설정은 다음 컨테이너 재생성 때 사용된다.
관리자 메뉴 검수 중 플러그인 내부 이름까지 번역하면 주소가 잘못 만들어지는 문제를 확인했다. 내부 이름은 유지하고 표시 이름만 번역하도록 고쳤다. 보드 메뉴 두 곳에 번역 키가 그대로 표시되는 문제도 기존 한국어 제목으로 복구했다. 보드 자산을 컴파일하고 서버가 새 파일을 제공하는지 확인했다. 컨테이너 재빌드에는 같은 제목 보완을 적용하는 단계도 추가했다.
## 검사
| 항목 | 결과 |
|---|---|
| Ruby 문법과 한국어 YAML 11개 | 통과 |
| 활성 플러그인 번역 | 고유 키 6,004개 검사. 누락 0개 |
| 음성 대화 번역 | 561개 검사. 누락 0개 |
| 일반 코어 문구 | 사용자 문구 누락 없음. 문자 치환과 서수 처리 등 기반 기능 8개는 번역 대상에서 구분 |
| 실제 제공 자산 | 일반 화면과 관리자 화면 및 보드 자산의 내용 해시 일치 |
| 배포 실패 검출 | 재시작 전 예전 웹 번역을 제공할 때 검사 실패 확인 |
| PC 화면 | 입장 안내의 제목과 본문 및 확인란과 버튼 한국어 표시 확인 |
| 모바일 화면 | 390×844에서 같은 안내 확인. 가로 넘침 없음 |
| 관리자 화면 | 대시보드와 보드 설정 및 한국어 메뉴 확인. 보드 경로 정상 |
원문과 같은 값 중 제품명과 기술 식별자 및 서식 문자열은 유지했다. 음성 통화에 실제 참여하거나 마이크 권한을 요청하지 않았다. 게임 서버는 변경하거나 재시작하지 않았다. 앞서 추가한 원티드 산스와 기존 글꼴 선택지도 유지했다.
## 배포 검사 개선
`scripts/verify_deployment.sh`는 원본 파일 해시와 Rails에서의 플러그인 로딩 및 주요 안내 문구와 활성 플러그인의 번역 누락을 확인한다. 이후 실제 웹 서버가 제공하는 번역과 보드 자산을 대조한다. 파일 존재와 HTTP 200만으로 완료 처리하지 않는다.
`scripts/deploy.sh`는 지정된 영구 원본 경로에서만 실행한다. 재시작 전에 번역을 검사하고 대상 포럼 컨테이너만 재시작한다. 보드 원본 구조가 달라졌다면 제목 보완 단계에서 중단한다.
## 백업과 복구
- 일반 백업: `/var/discourse/shared/standalone/backups/forum-korean-20261015-1150`
- DB 덤프: `discourse-before.dump` 21,728,860바이트. 복원 목록 3,527줄 확인.
- 원본: `source-before.tar`, 보드 원본 두 파일과 `boards-generated-before`
- 비밀 설정 백업: `/home/syskiller/.local/share/discourse-backups/forum-korean-20261015-1150/app.yml` · 권한 600
- DB 구조 변경과 데이터 이전 없음. 일반 글과 계정 및 설정을 변경하지 않음.
문제 발생 시 변경 전 `app.yml`을 복원하고 새 플러그인 디렉터리를 로딩 대상에서 빼면 변경 전 상태로 돌아간다. 보드 원본과 생성 자산은 위 백업으로 복원한다. 이 절차는 번역이 빠진 이전 상태로 돌아가는 장애 대응용이다. DB 덤프는 별도 복원 시험을 하지 않았다.
검증 범위는 현재 활성 번역과 확인한 화면이다. 비활성 플러그인을 새로 켜거나 제품을 업데이트할 때는 번역 검사를 다시 실행한다.
+3
View File
@@ -14,7 +14,10 @@ Trilium의 `Ubuntu 서버 운영` 바로 아래에 만든 전용 분류다. 포
| 관리자 한글화 운영 기록 · 2026-09-20 | `N0v5eGIKqa28` | 설정 이름 보완 플러그인과 운영 반영 및 검증 기록 |
| 사용자 화면 한글화 진행 기록 · 2026-09-25 | `m6Epi7pHAylp` | 네 차례 번역 묶음과 운영 반영 및 남은 분량 기록 |
| Discourse 한국어 번역 완료와 Gitea 배포 준비 · 2026-09-27 | `xmOe7b06fKOo` | 최종 번역 검사와 모바일 확인 및 Gitea 배포 절차 |
| ITOSM 피아노 학습실 포럼 구성 · 2026-10-04 | `fb6Wi8BIcf0U` | 피아노 학습 서비스 전용 카테고리와 안내 글 및 공개 권한과 모바일 검사 기록 |
## 작성 기준
관련성이 분명한 기존 분류가 없으면 이 폴더 안에 새 문서를 만든다. 다른 프로젝트 기록이나 일반 서버 기록에 섞어 넣지 않는다. 문서는 마이그레이션 기록과 같은 수준으로 표지와 색상과 카드와 표와 제목 단계를 사용한다. 모바일 폭에서도 내용이 겹치거나 잘리지 않게 확인한다.
- [포럼 한국어 번역 복구 · 2026-10-15](2026-10-15-localization-recovery.md) · Trilium `SUFRvrm6MX4L`
+1 -1
View File
@@ -8,7 +8,7 @@
},
"scripts": {
"test": "yarn check:ruby && yarn check:yaml",
"check:ruby": "ruby -c plugin.rb",
"check:ruby": "ruby -c plugin.rb && ruby -c scripts/verify_runtime.rb && ruby -c scripts/verify_web.rb && ruby -c scripts/patch_boards.rb",
"check:yaml": "ruby scripts/validate_locales.rb",
"deploy": "bash scripts/deploy.sh",
"verify:deployment": "bash scripts/verify_deployment.sh"
+6 -7
View File
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# name: itosm-admin-korean
# about: Discourse 관리자 설정 이름을 자연스러운 한국어로 표시합니다.
# version: 0.1.0
# name: itosm-korean-localization
# about: Discourse 일반 화면과 관리자 화면의 한국어 번역을 보완합니다.
# version: 0.1.1
# authors: 이창현
# url: https://gitea.itosm.co.kr/syskiller/discourse-korean-localization
@@ -50,6 +50,7 @@ module ::ItosmAdminKorean
"discourse-zendesk-plugin" => "Zendesk 연동",
"footnote" => "각주",
"itosm-admin-korean" => "관리자 한국어 번역",
"itosm-korean-localization" => "한국어 번역 보완",
"poll" => "설문",
"spoiler-alert" => "내용 숨기기",
"styleguide" => "스타일 가이드",
@@ -1897,10 +1898,8 @@ module ::ItosmAdminKorean
end
module AdminPluginNames
def name
ItosmAdminKorean::PLUGIN_NAMES[object.directory_name] || super
end
# name은 관리자 주소를 구성하는 식별자이므로 번역하지 않습니다.
# 표시 이름만 바꿔야 한국어 메뉴를 눌러도 올바른 플러그인으로 이동합니다.
def humanized_name
ItosmAdminKorean::PLUGIN_NAMES[object.directory_name] || super
end
Executable → Regular
+21 -4
View File
@@ -3,7 +3,14 @@ set -euo pipefail
root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
container="${DISCOURSE_CONTAINER:-app}"
plugin_path="/var/www/discourse/plugins/itosm-admin-korean"
plugin_path="/var/www/discourse/plugins/itosm-korean-localization"
source_path="${DISCOURSE_PLUGIN_SOURCE:-/home/syskiller/discourse-korean-localization}"
# 컨테이너만 고치면 재생성할 때 예전 번역으로 돌아갈 수 있으므로 영구 원본에서만 배포합니다.
if [[ "$root_dir" != "$(realpath "$source_path")" ]]; then
echo "영구 원본 경로에서 배포해야 합니다: $source_path" >&2
exit 1
fi
cd "$root_dir"
corepack yarn test
@@ -14,14 +21,24 @@ 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 exec -i "$container" ruby < scripts/patch_boards.rb
docker exec -u discourse -e RAILS_ENV=production -w /var/www/discourse "$container" \
bundle exec rake assets:precompile:build_plugins
docker exec -u discourse -e RAILS_ENV=production -e SKIP_EMBER_CLI_COMPILE=1 \
-e DONT_PRECOMPILE_CSS=1 -w /var/www/discourse "$container" bundle exec rake assets:precompile
# HTTP 응답만 검사하면 번역 플러그인이 빠져도 배포 성공으로 처리됩니다.
# 웹 프로세스를 재시작하기 전에 별도 Rails 부팅으로 번역을 검사합니다.
DISCOURSE_CONTAINER="$container" bash scripts/verify_deployment.sh --runtime-only
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
for attempt in {1..30}; do
if curl --silent --fail "${DISCOURSE_URL:-https://forum.itosm.co.kr}" >/dev/null; then
DISCOURSE_CONTAINER="$container" bash scripts/verify_deployment.sh
echo "Discourse 한국어 보완 배포 완료"
exit 0
fi
sleep 2
sleep 3
done
echo "컨테이너 재시작 뒤 서비스가 정상 상태로 돌아오지 않았습니다." >&2
+20
View File
@@ -0,0 +1,20 @@
# frozen_string_literal: true
# 번역 키를 그대로 출력하는 보드 메뉴 제목 두 곳만 바꿉니다.
# 원본 구조가 바뀌면 전체 파일을 덮어쓰지 않고 중단해 다른 수정 사항을 보존합니다.
root = ENV.fetch("DISCOURSE_ROOT", "/var/www/discourse")
targets = {
"boards-board-viewer.gjs" => ["boards.board.controls", "보드 메뉴"],
"boards-column.gjs" => ["boards.board.column_controls", "열 메뉴"],
}
targets.each do |file, (key, title)|
path = File.join(root, "plugins/boards/assets/javascripts/discourse/components", file)
next unless File.exist?(path)
original = File.read(path)
needle = "@title=\"#{key}\""
replacement = "@title=\"#{title}\""
next if original.include?(replacement)
abort "보드 원본 구조가 달라졌습니다: #{file}" unless original.scan(needle).size == 1
File.write(path, original.sub(needle, replacement))
end
puts "보드 메뉴 한국어 제목 확인 완료"
Executable → Regular
+18 -12
View File
@@ -1,20 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
container="${DISCOURSE_CONTAINER:-app}"
site_url="${DISCOURSE_URL:-https://forum.itosm.co.kr}"
plugin_path="/var/www/discourse/plugins/itosm-admin-korean"
plugin_path="/var/www/discourse/plugins/itosm-korean-localization"
report="$(mktemp)"
trap 'rm -f "$report"' EXIT
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"
# 파일 존재뿐 아니라 저장소 원본과 실행 파일이 일치하는지 검사합니다.
for file in plugin.rb config/locales/client.ko.yml config/locales/server.ko.yml; do
expected="$(sha256sum "$root_dir/$file" | cut -d ' ' -f 1)"
actual="$(docker exec "$container" sha256sum "$plugin_path/$file" | cut -d ' ' -f 1)"
[[ "$expected" == "$actual" ]] || { echo "운영 파일 불일치: $file" >&2; exit 1; }
done
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "$site_url")"
if [[ "$status" != "200" ]]; then
echo "포럼 응답 확인 실패: HTTP $status" >&2
exit 1
docker exec -i -u discourse -e RAILS_ENV=production -w /var/www/discourse \
"$container" bundle exec rails runner - < "$root_dir/scripts/verify_runtime.rb" > "$report"
if [[ "${1:-}" != "--runtime-only" ]]; then
# 별도 Rails 검사와 실제 웹 프로세스가 제공하는 번역 묶음을 함께 대조합니다.
# 재시작을 빠뜨려 예전 영어 번역을 계속 제공하는 경우도 실패로 처리합니다.
ruby "$root_dir/scripts/verify_web.rb" "$site_url" "$report"
fi
echo "운영 플러그인 파일 확인 완료"
echo "포럼 응답 확인 완료: HTTP 200"
echo "운영 파일과 한국어 번역 검사 통과"
+64
View File
@@ -0,0 +1,64 @@
# frozen_string_literal: true
require "json"
require "digest"
require "yaml"
abort "한국어 보완 플러그인을 불러오지 못했습니다." unless Discourse.plugins.any? { |plugin| plugin.name == "itosm-korean-localization" }
abort "이전 한국어 플러그인이 중복 설치되어 있습니다." if Discourse.plugins.any? { |plugin| plugin.name == "itosm-admin-korean" }
I18n.with_locale(:ko) do
boards = Discourse.plugins.find { |plugin| plugin.name == "boards" }
if boards
serialized = AdminPluginSerializer.new(boards, root: false).as_json
abort "플러그인 내부 이름이 번역되어 관리자 링크가 손상됩니다." unless serialized[:name] == "boards"
abort "보드 표시 이름 번역 누락" unless serialized[:humanized_name] == "보드"
end
end
expected = {
"title" => "대화방에 참여하기 전에 확인해 주세요.",
"join" => "대화방 참여",
"cancel" => "취소",
"dont_show_again" => "다시 표시하지 않기",
}
expected.each do |key, value|
actual = I18n.t("js.voice.mesh_privacy_warning.#{key}", locale: :ko, fallback: false)
abort "음성 대화방 번역 불일치: #{key}" unless actual == value
end
body = I18n.t("js.voice.mesh_privacy_warning.body", locale: :ko, fallback: false)
abort "음성 대화방 개인정보 안내 번역 누락" unless body.include?("다른 참여자가 내 IP 주소를 확인할 수 있습니다.")
def flatten_translations(hash, prefix = "", result = {})
hash.each do |key, value|
path = prefix.empty? ? key.to_s : "#{prefix}.#{key}"
value.is_a?(Hash) ? flatten_translations(value, path, result) : result[path] = value
end
result
end
korean = flatten_translations(I18n.backend.send(:translations).fetch(:ko))
TranslationOverride.where(locale: "ko").pluck(:translation_key, :value).each { |key, value| korean[key] = value }
active_keys = Discourse.plugins.select(&:enabled?).flat_map do |plugin|
Dir.glob(Rails.root.join("plugins", plugin.directory_name, "config/locales/*.en.yml")).flat_map do |path|
flatten_translations(YAML.safe_load_file(path, aliases: true).fetch("en", {})).keys
end
end.uniq
# 한국어는 단수와 복수에 같은 표현을 사용하므로 other가 있으면 one도 충족합니다.
missing = active_keys.reject do |key|
korean.key?(key) || (key.end_with?(".one") && korean.key?(key.sub(/\.one$/, ".other")))
end
abort "활성 플러그인 한국어 번역 누락 #{missing.size}개: #{missing.first(10).join(', ')}" unless missing.empty?
# 영어 대체 표시를 정상 번역으로 오인하지 않도록 한국어 묶음의 내용 해시를 반환합니다.
bundles = %w[main admin].to_h do |bundle|
content = ExtraLocalesController.bundle_js(bundle, locale: :ko)
[bundle, { url: ExtraLocalesController.url(bundle, locale: :ko), sha256: Digest::SHA256.hexdigest(content) }]
end
if Discourse.plugins.any? { |plugin| plugin.name == "boards" }
filename = Plugin::JsManager.read_manifest("boards").dig("main", "fileName")
board_js = File.read(Rails.root.join("app/assets/generated/boards/js/plugins", filename))
abort "보드 메뉴 제목 컴파일 누락" unless board_js.include?("보드 메뉴") && board_js.include?("열 메뉴")
bundles["boards"] = {url: "/assets/js/plugins/#{filename}", sha256: Digest::SHA256.hexdigest(board_js)}
end
puts JSON.generate(bundles: bundles, active_plugin_keys: active_keys.size, missing: missing.size)
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
require "json"
require "digest"
require "net/http"
require "uri"
base, report_path = ARGV
report = JSON.parse(File.read(report_path))
site = URI(base)
home = Net::HTTP.get_response(site)
abort "포럼 응답 확인 실패: HTTP #{home.code}" unless home.is_a?(Net::HTTPSuccess)
report.fetch("bundles").each do |bundle, expected|
uri = URI.join(base, expected.fetch("url"))
abort "예상하지 않은 번역 서버 주소" unless uri.host == site.host
response = Net::HTTP.get_response(uri)
abort "#{bundle} 번역 요청 실패: HTTP #{response.code}" unless response.is_a?(Net::HTTPSuccess)
abort "#{bundle} 웹 번역이 실행 원본과 다릅니다." unless Digest::SHA256.hexdigest(response.body) == expected.fetch("sha256")
end
puts "포럼 HTTP 200과 실제 사용자 화면 및 관리자 화면 번역 묶음 확인 완료"
+25
View File
@@ -52,3 +52,28 @@ Discourse 한국어 보완 수정 기록
- 데이터 변경: 데이터베이스 구조 변경과 데이터 이전은 없다.
- 운영 반영: 운영 `app` 컨테이너만 다시 시작했다. 포럼 HTTP 200 응답을 확인했다.
- 백업: 사용자 요청이 없어 진행하지 않았다.
## 2026-10-04 ITOSM 피아노 학습실 포럼 구성
- 작업 대상: https://forum.itosm.co.kr 및 https://learn.itosm.co.kr
- 변경 이유: 피아노 학습 서비스의 사용법과 연습 자료 및 질문과 오류를 목적별로 나눠 관리하기 위해 진행했다.
- 변경 내용: `ITOSM 피아노 학습실` 상위 카테고리와 네 개의 하위 카테고리를 만들었다. 서비스 소개와 처음 시작하는 방법 및 자료 공유와 난이도별 연습 정보와 질문 및 오류 안내 글을 등록했다.
- 저작권 안내: MIDI와 악보 및 연습 자료를 공유할 때 제작자와 원본 출처와 저작권 상태 및 이용 허가와 재배포 가능 여부를 적도록 글쓰기 양식과 고정 안내에 반영했다.
- 권한: 모든 새 카테고리와 안내 글은 비회원이 볼 수 있다. 회원은 새 글을 작성할 수 있다.
- 검사: 서비스와 포럼 및 안내 글 여섯 개의 HTTP 200 응답을 확인했다. PC 화면에서 카테고리와 글 목록을 확인했다. 390×844 모바일 화면에서 가로 넘침이 없고 저작권 안내가 표시되는지 확인했다.
- 이름 정리: 정식 이름을 `ITOSM 피아노 학습실`로 통일했다. 잘못 사용한 임시 이름은 카테고리와 제목과 본문 및 글쓰기 양식에서 제거했다. `NoteRain` 표기도 사용하지 않았다.
- 데이터 변경: 카테고리와 주제 및 게시물과 글쓰기 양식만 추가하거나 수정했다. 데이터베이스 구조 변경과 데이터 이전은 없다.
- 운영 반영: 운영 `app` 컨테이너의 Rails 작업으로 즉시 반영했다. 컨테이너는 다시 시작하지 않았다.
- 보존 사항: 기존 포럼 설정과 카테고리와 게시물은 삭제하지 않았다.
- 백업: 사용자 요청이 없어 진행하지 않았다.
- 남은 제한: 실제 MIDI 장치를 이용한 서비스 동작 검사는 별도 장치 검증 작업으로 남는다.
## 2026-10-15 운영 서버 한국어 번역 복구
- 이전된 서버의 한국어 보완 플러그인 경로가 비어 있음을 확인했다. 전용 원본을 복구하고 포럼 컨테이너만 재시작했다.
- 음성 대화방 입장 안내와 주변 메뉴 및 관리자 번역을 복구했다. 활성 플러그인 고유 번역 6,004개와 음성 기능 561개를 검사했다. 누락은 0개다.
- 관리자 주소에 쓰이는 내부 플러그인 이름은 보존하도록 수정했다. 보드 메뉴 두 곳의 제목도 복구했다.
- 실제 웹 번역과 보드 자산 해시 대조를 배포 검사에 추가했다. PC와 모바일의 안내창 및 관리자 메뉴 주소를 확인했다.
- DB 구조 변경과 데이터 이전은 없다. 기존 글꼴과 게시물을 보존했다.
- 백업: `/var/discourse/shared/standalone/backups/forum-korean-20261015-1150`
- 상세 범위와 복구 방법: [한국어 복구 기록](docs/2026-10-15-localization-recovery.md)