버전 업데이트에 대비한 번역 비교 도구와 유지 절차 추가
새 원문과 이전 한국어를 키별로 비교해 추가와 변경 및 삭제를 구분한다. 배포 파일 변경 비교와 기준 해시 및 후속 작업 문서를 추가했다. 자동 검사 4개와 UI 문구 1496개 비교 및 기존 배포 파일 492개 무결성 검사를 통과했다. 운영 앱과 DB는 변경하지 않았으며 기존 배포 커밋 eddda22를 보존한다. 코드와 도움말 이식 및 새 버전 실행 검증은 대상 버전 작업 때 수행한다.
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# Trilium 한국어판 작업 기준
|
||||||
|
|
||||||
|
## 2026-09-20 추가 번역 준비
|
||||||
|
|
||||||
|
대상 저장소는 `syskiller/triliumnext-korean-patch`다. 운영 기준은 0.95.0이며 배포판은 2.0.0이다. 최신 버전으로 이식한 상태는 아니다.
|
||||||
|
|
||||||
|
다음 작업은 `docs/TRANSLATION-MAINTENANCE.ko.md`에서 시작한다. `localization/baseline.json`에 기존 UI 번역 기준과 해시를 기록했다. 새 원문의 추가와 변경 및 삭제를 비교하는 도구와 배포 파일 변경을 비교하는 도구를 추가했다.
|
||||||
|
|
||||||
|
기존 UI 사전 자체 비교 결과는 재사용 후보 1482개와 원문 동일 검토 항목 14개다. 원문 동일 항목은 고유 명칭일 수 있으므로 미번역으로 단정하지 않는다. 서버 사전의 이전 영문은 별도로 확보해야 한다.
|
||||||
|
|
||||||
|
기존 이미지 유지와 한국어 설명 보완 정책을 유지한다. 코드 식별자와 URL은 원문을 유지한다. 직접 수정한 번들은 다른 버전에 복사하지 않는다. 운영 코드와 DB 변경 및 데이터 이전은 없다. 이번 작업은 도구와 문서 추가이므로 운영 데이터 백업을 새로 만들지 않는다. 기존 배포 커밋은 `eddda22`다.
|
||||||
@@ -76,6 +76,8 @@ bash install.sh
|
|||||||
|
|
||||||
## 검증과 재생성
|
## 검증과 재생성
|
||||||
|
|
||||||
|
다음 버전의 추가 번역은 [버전 업데이트와 추가 번역 절차](docs/TRANSLATION-MAINTENANCE.ko.md)를 따릅니다. 기존 한국어와 새 원문을 비교하는 도구 및 코드와 도움말 파일의 변경 확인 도구를 제공합니다. 현재 배포판의 지원 버전은 그대로 0.95.0입니다.
|
||||||
|
|
||||||
검증 결과는 [배포 검증 기록](docs/DEPLOYMENT-TESTS.ko.md)에 있습니다. 배포 파일은 Docker 빌드 중 SHA-256 검사와 JavaScript 구문 검사를 통과해야 합니다. 개발 환경에 Node.js가 있다면 다음 명령으로도 확인할 수 있습니다.
|
검증 결과는 [배포 검증 기록](docs/DEPLOYMENT-TESTS.ko.md)에 있습니다. 배포 파일은 Docker 빌드 중 SHA-256 검사와 JavaScript 구문 검사를 통과해야 합니다. 개발 환경에 Node.js가 있다면 다음 명령으로도 확인할 수 있습니다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 버전 업데이트와 추가 번역
|
||||||
|
|
||||||
|
이 저장소는 Trilium 한국어 배포판 전용이다. 게임 저장소나 게임 문서에 작업 기록을 넣지 않는다.
|
||||||
|
|
||||||
|
현재 운영 기준은 Trilium 0.95.0과 한국어 배포판 2.0.0이다. 다음 버전 번역을 이어 갈 준비를 위한 도구이며 최신 Trilium 지원을 완료한 것은 아니다. 기존 배포 파일과 운영 데이터는 이 도구로 변경하지 않는다.
|
||||||
|
|
||||||
|
## 이어서 작업할 자료
|
||||||
|
|
||||||
|
| 대상 | 기준 자료 | 다음 버전에서 할 일 |
|
||||||
|
|---|---|---|
|
||||||
|
| UI 번역 | `localization/baseline.json`의 영문과 한국어 파일 | 키와 원문을 비교한 뒤 새 문구와 변경된 문구 번역 |
|
||||||
|
| 서버 번역 | `release/app/assets/translations/ko/server.json` | 같은 버전의 공식 영문을 확보한 뒤 비교 도구에 명시적으로 전달 |
|
||||||
|
| 직접 수정한 코드 | `release/app/main.cjs`와 `release/app/public/src` | 새 버전 원본 소스에서 해당 기능을 찾아 번역 키로 수정 |
|
||||||
|
| 도움말 | `release/app/assets/doc_notes/en` | 원본 문서의 변경 내용과 내부 링크를 대조 |
|
||||||
|
| 예제와 PDF | `tools/demo-source`와 `tools/build-demo-archive.py` | 새 예제 구조와 노트 연결을 확인한 뒤 새 설치 환경에서 검사 |
|
||||||
|
| 이미지 | 도움말 이미지와 한국어 설명 | 원본 이미지를 유지하고 설명을 보완하는 기존 정책 유지 |
|
||||||
|
| 변경 이력 | `updates/2026-09-05` | 과거 작업의 참고 자료로 보존 |
|
||||||
|
|
||||||
|
`tools/build-release.mjs`는 0.95.0용 과거 작업 결과를 다시 모으는 도구다. 새 버전 이식 도구로 사용하지 않는다. 예제 생성 도구도 기존 노트 구조에 맞춰져 있으므로 새 버전에 바로 적용하지 않는다.
|
||||||
|
|
||||||
|
## 1. 원본과 작업 기준 확보
|
||||||
|
|
||||||
|
운영과 분리된 작업 폴더에서 대상 버전의 공식 소스와 배포 이미지를 확보한다. 버전과 커밋 및 이미지 다이제스트를 기록한다. 작업 브랜치는 `translation/대상버전` 형태로 구분한다. 기존 데이터와 비밀 파일은 저장소에 넣지 않는다.
|
||||||
|
|
||||||
|
영문 파일은 반드시 대상 버전에서 가져온다. 현재 저장소에 이전 서버 영문 사전은 없으므로 한국어 파일을 영문 기준으로 간주하면 안 된다. UI 기준은 `localization/baseline.json`에 경로와 SHA-256을 고정했다.
|
||||||
|
|
||||||
|
## 2. UI 문구 비교
|
||||||
|
|
||||||
|
Node.js 20 이상에서 저장소 루트를 기준으로 실행한다. 출력 폴더는 아직 존재하지 않는 경로를 지정한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/translation-upgrade.mjs 대상버전 /작업/새버전/translation.en.json /작업/번역검토
|
||||||
|
```
|
||||||
|
|
||||||
|
다른 사전이나 이후 버전의 검토 완료 자료를 기준으로 삼을 때는 이전 영문과 한국어를 함께 지정한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/translation-upgrade.mjs 대상버전 /작업/새영문.json /작업/서버검토 /작업/이전영문.json /작업/이전한국어.json
|
||||||
|
```
|
||||||
|
|
||||||
|
`review.json`에는 키 경로와 새 원문 및 이전 원문과 한국어 후보가 함께 들어간다.
|
||||||
|
|
||||||
|
| 상태 | 처리 |
|
||||||
|
|---|---|
|
||||||
|
| `reusable` | 원문과 자리표시자가 같아 재사용 후보로 분류됨. 화면 문맥은 확인해야 함 |
|
||||||
|
| `added` | 새 키. 한국어를 새로 작성 |
|
||||||
|
| `source-changed` | 원문이 바뀜. 이전 한국어를 참고해 다시 번역 |
|
||||||
|
| `missing` | 한국어가 없거나 빈 문자열임 |
|
||||||
|
| `placeholder-mismatch` | 변수 자리표시자가 다름. 적용 전 수정 |
|
||||||
|
| `review-identical` | 원문과 번역이 같음. 고유 명칭인지 미번역인지 확인 |
|
||||||
|
| `removed` | 새 사전에서 없어짐. 이전 기록에만 보존 |
|
||||||
|
|
||||||
|
이 도구는 i18next 중괄호 변수와 템플릿 변수 및 일반 printf 자리표시자를 비교한다. 모든 메시지 문법이나 HTML 및 복수형 의미를 검증하지는 않는다. 새 원문과 한국어를 검토한 뒤 대상 버전의 번역 파일에 반영한다. JSON 키를 바꾸거나 원문이 바뀐 문구에 이전 한국어를 일괄 덮어쓰지 않는다.
|
||||||
|
|
||||||
|
## 3. 코드와 도움말 수정 대상 비교
|
||||||
|
|
||||||
|
공식 이전 이미지와 새 이미지에서 앱 파일을 각각 별도 폴더로 추출한다. 두 폴더 모두 `main.cjs`와 `assets` 등이 바로 아래에 있어야 한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node tools/translation-files.mjs /작업/이전원본앱 /작업/새원본앱 /작업/파일비교.json
|
||||||
|
```
|
||||||
|
|
||||||
|
현재 배포판의 수정 대상 492개 경로를 비교한다. 변경되거나 이동한 파일은 수동 검토 대상으로 삼는다. 한국어판에만 있는 파일도 별도로 표시한다. 새 버전에 추가된 다른 파일은 이 목록에 없으므로 공식 소스의 버전 간 Git diff도 함께 확인한다.
|
||||||
|
|
||||||
|
빌드된 `main.cjs`를 새 버전에 덮어쓰지 않는다. 바뀐 기능을 원본 소스에서 찾아 번역 기능을 연결하고 다시 빌드한다. 공식 기여를 준비할 때도 UI 사전과 번역 기능 추가 및 도움말 변경을 나누어 검토한다.
|
||||||
|
|
||||||
|
## 4. 검증과 배포
|
||||||
|
|
||||||
|
1. 자연스러운 한국어와 변수 및 링크를 검사한다. 문장을 잇기 위한 쉼표를 사용하지 않는다.
|
||||||
|
2. 대상 버전 소스의 빌드와 관련 검사를 수행한다.
|
||||||
|
3. 별도 Docker 환경에서 신규 설치와 한국어 선택 및 기본 예제를 확인한다.
|
||||||
|
4. 운영 데이터의 백업 사본으로 업데이트와 첨부 파일 및 검색을 확인한다.
|
||||||
|
5. 실패 시 이전 이미지와 해당 버전 데이터 백업으로 복원할 수 있는지 검사한다.
|
||||||
|
6. 기반 이미지와 배포 버전 및 생성 도구를 새 버전에 맞춰 수정한다. 파일 해시 검증과 실제 화면 확인을 마친 뒤 배포한다.
|
||||||
|
|
||||||
|
## 5. 다음 작업을 위한 기록
|
||||||
|
|
||||||
|
검토를 마친 새 영문과 한국어 사전의 경로와 해시를 새 기준으로 기록한다. 이전 기준은 Git 기록에 보존한다. `PROJECT_MEMORY.md`와 `수정기록.txt`에 버전과 변경 내용 및 검사 결과와 배포 여부를 남긴다. 번역 검토 자료는 비밀값이 없는지 확인한 뒤 해당 버전 폴더에 보관한다. 운영 노트를 기록할 때는 Trilium의 `Ubuntu 서버 운영` 아래 기존 Trilium 분류를 찾아 사용한다.
|
||||||
|
|
||||||
|
이번 준비 작업은 운영 앱과 데이터베이스를 변경하지 않는다. 새 버전 자동 배포나 자동 번역을 예약하지 않는다.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"appVersion": "0.95.0",
|
||||||
|
"koreanSha256": "e1d34275426dabf9aabf2f9b3ec880157946975f9f3f5a9035cf412ce02c9114",
|
||||||
|
"english": "updates/2026-09-05/work/trilium/scoped/translation.en.json",
|
||||||
|
"englishSha256": "3a08afa9a2215e53245466bc2ca84e5c2ff24edbfce0ee25577ff680cdb332ce",
|
||||||
|
"korean": "release/app/public/translations/ko/translation.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { compare } from './translation-upgrade.mjs';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawnSync } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
test('추가와 변경 및 삭제를 구분하고 기존 번역을 검토 후보로 보존한다', () => {
|
||||||
|
const result = compare({ same: 'Hello', changed: 'Old', gone: 'Gone' }, { same: '안녕하세요', changed: '이전', gone: '삭제' }, { same: 'Hello', changed: 'New', added: 'Add' });
|
||||||
|
assert.deepEqual(result.map(x => x.status), ['reusable', 'source-changed', 'added', 'removed']);
|
||||||
|
assert.equal(result[1].korean, '이전');
|
||||||
|
});
|
||||||
|
test('자리표시자 누락과 미번역을 자동 재사용에서 제외한다', () => {
|
||||||
|
const en = { a: 'Hi {{name}}', b: 'Name', c: 'Value', d: 'Brand' };
|
||||||
|
assert.deepEqual(compare(en, { a: '안녕', b: '', d: 'Brand' }, en).map(x => x.status), ['placeholder-mismatch', 'missing', 'missing', 'review-identical']);
|
||||||
|
});
|
||||||
|
test('점이 포함된 키와 중첩 키 및 배열을 구분한다', () => {
|
||||||
|
const en = { 'a.b': 'One', a: { b: 'Two' }, list: ['Three'] };
|
||||||
|
const ko = { 'a.b': '하나', a: { b: '둘' }, list: ['셋'] };
|
||||||
|
assert.equal(compare(en, ko, en).filter(x => x.status === 'reusable').length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('파일 비교는 변경과 삭제 및 추가를 구분하고 기존 보고서를 덮어쓰지 않는다', () => {
|
||||||
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'trilium-translation-'));
|
||||||
|
try {
|
||||||
|
const old = path.join(temp, 'old'), next = path.join(temp, 'next');
|
||||||
|
fs.mkdirSync(old); fs.mkdirSync(next);
|
||||||
|
fs.writeFileSync(path.join(old, 'main.cjs'), 'old');
|
||||||
|
fs.writeFileSync(path.join(next, 'main.cjs'), 'next');
|
||||||
|
fs.mkdirSync(path.join(old, 'assets/db'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(old, 'assets/db/demo.zip'), 'old-demo');
|
||||||
|
fs.mkdirSync(path.join(next, 'public/src'), { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(next, 'public/src/ko.js'), 'next-ko');
|
||||||
|
const output = path.join(temp, 'report.json');
|
||||||
|
const tool = fileURLToPath(new URL('./translation-files.mjs', import.meta.url));
|
||||||
|
const run = () => spawnSync(process.execPath, [tool, old, next, output], { encoding: 'utf8' });
|
||||||
|
assert.equal(run().status, 0);
|
||||||
|
const saved = fs.readFileSync(output, 'utf8');
|
||||||
|
const entries = JSON.parse(saved).files;
|
||||||
|
assert.equal(entries.find(x => x.file === 'main.cjs').status, 'upstream-changed');
|
||||||
|
assert.equal(entries.find(x => x.file === 'assets/db/demo.zip').status, 'removed-or-moved');
|
||||||
|
assert.equal(entries.find(x => x.file === 'public/src/ko.js').status, 'new-upstream-path');
|
||||||
|
assert.notEqual(run().status, 0);
|
||||||
|
assert.equal(fs.readFileSync(output, 'utf8'), saved);
|
||||||
|
} finally {
|
||||||
|
// 이 검사에서 만든 임시 폴더만 정리한다. 사용자 자료 경로는 받지 않는다.
|
||||||
|
fs.rmSync(temp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const [oldRoot, nextRoot, output] = process.argv.slice(2);
|
||||||
|
if (!oldRoot || !nextRoot || !output) throw new Error('사용법: node tools/translation-files.mjs 이전원본앱폴더 새원본앱폴더 새보고서JSON');
|
||||||
|
for (const dir of [oldRoot, nextRoot]) if (!fs.statSync(dir).isDirectory()) throw new Error(`앱 폴더가 아닙니다: ${dir}`);
|
||||||
|
const digest = file => fs.existsSync(file) && fs.statSync(file).isFile() ? crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') : null;
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'release/manifest.json'), 'utf8'));
|
||||||
|
// 배포판의 수정 대상만 검사한다. 새로 추가된 전체 파일은 원본 소스의 Git 비교로 별도 확인해야 한다.
|
||||||
|
const files = Object.keys(manifest.files).map(file => {
|
||||||
|
const before = digest(path.join(oldRoot, file)), after = digest(path.join(nextRoot, file));
|
||||||
|
const status = !before ? (after ? 'new-upstream-path' : 'local-only') : !after ? 'removed-or-moved' : before === after ? 'upstream-unchanged' : 'upstream-changed';
|
||||||
|
return { file, status, previousSha256: before, nextSha256: after, koreanSha256: manifest.files[file].sha256 };
|
||||||
|
});
|
||||||
|
fs.writeFileSync(output, JSON.stringify({ baselineVersion: manifest.appVersion, warning: '같은 경로의 변경 여부만 확인합니다. 번역 패치 적용 가능 여부와 실행 호환성을 보장하지 않습니다.', files }, null, 2) + '\n', { flag: 'wx' });
|
||||||
|
console.log(`수정 대상 ${files.length}개 비교 완료`);
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const read = p => JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||||
|
const hash = p => crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
||||||
|
// 경로를 배열로 보관하므로 점이 들어간 번역 키도 다른 키와 혼동하지 않는다.
|
||||||
|
export function flatten(value, parts = [], result = new Map()) {
|
||||||
|
if (typeof value === 'string') result.set(JSON.stringify(parts), value);
|
||||||
|
else if (value && typeof value === 'object') {
|
||||||
|
for (const [key, child] of Object.entries(value)) flatten(child, [...parts, key], result);
|
||||||
|
} else throw new Error(`문자열이 아닌 번역 값: ${JSON.stringify(parts)}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const tokens = text => (text.match(/\{\{[^}]+\}\}|\$\{[^}]+\}|%(?:\d+\$)?[sdif]/g) || []).sort();
|
||||||
|
export function compare(previous, korean, next) {
|
||||||
|
const before = flatten(previous), translated = flatten(korean), after = flatten(next);
|
||||||
|
const entries = [];
|
||||||
|
for (const [key, source] of after) {
|
||||||
|
const old = before.get(key), ko = translated.get(key);
|
||||||
|
let status = old === undefined ? 'added' : old !== source ? 'source-changed' : !ko ? 'missing' : ko === source ? 'review-identical' : 'reusable';
|
||||||
|
if (status === 'reusable' && JSON.stringify(tokens(source)) !== JSON.stringify(tokens(ko))) status = 'placeholder-mismatch';
|
||||||
|
entries.push({ path: JSON.parse(key), source, previousSource: old ?? null, korean: ko ?? null, status });
|
||||||
|
}
|
||||||
|
for (const [key, source] of before) if (!after.has(key)) entries.push({ path: JSON.parse(key), previousSource: source, korean: translated.get(key) ?? null, status: 'removed' });
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
const [version, nextFile, output, previousFile, koreanFile] = process.argv.slice(2);
|
||||||
|
if (!version || !nextFile || !output) throw new Error('사용법: node tools/translation-upgrade.mjs 대상버전 새영문JSON 새출력폴더 [이전영문JSON 이전한국어JSON]');
|
||||||
|
if (!!previousFile !== !!koreanFile) throw new Error('이전 영문과 한국어 파일은 함께 지정해야 합니다.');
|
||||||
|
const baseline = read(path.join(root, 'localization/baseline.json'));
|
||||||
|
const oldPath = previousFile || path.join(root, baseline.english);
|
||||||
|
const koPath = koreanFile || path.join(root, baseline.korean);
|
||||||
|
if (!previousFile && (hash(oldPath) !== baseline.englishSha256 || hash(koPath) !== baseline.koreanSha256)) throw new Error('기준 번역 파일이 변경됐습니다. 기준 메타데이터를 검토하세요.');
|
||||||
|
const entries = compare(read(oldPath), read(koPath), read(nextFile));
|
||||||
|
const counts = {};
|
||||||
|
for (const entry of entries) counts[entry.status] = (counts[entry.status] || 0) + 1;
|
||||||
|
// 기존 검토 내용이 사라지지 않도록 출력 폴더는 새 경로만 허용한다.
|
||||||
|
fs.mkdirSync(output, { recursive: false });
|
||||||
|
const metadata = { targetVersion: version, baselineVersion: previousFile ? '사용자 지정 기준' : baseline.appVersion, sourceSha256: hash(nextFile), previousSha256: hash(oldPath), koreanSha256: hash(koPath), counts };
|
||||||
|
fs.writeFileSync(path.join(output, 'review.json'), JSON.stringify({ ...metadata, entries }, null, 2) + '\n');
|
||||||
|
fs.writeFileSync(path.join(output, 'SUMMARY.ko.md'), `# ${version} 번역 비교\n\n${Object.entries(counts).map(([key, count]) => `- ${key}: ${count}`).join('\n')}\n\nreview.json의 한국어는 검토용 후보입니다. 원문이 바뀐 항목은 기존 번역을 그대로 적용하지 마세요. 이 도구는 배포 파일과 운영 데이터를 변경하지 않습니다.\n`);
|
||||||
|
console.log(JSON.stringify(metadata, null, 2));
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
2026-09-20 버전별 추가 번역 준비
|
||||||
|
|
||||||
|
변경 이유: 다음 Trilium 버전에서 기존 번역을 다시 찾는 시간을 줄이고 원문 변경을 놓치지 않기 위해 준비했다.
|
||||||
|
주요 파일: tools/translation-upgrade.mjs, tools/translation-files.mjs, tools/test-translation-upgrade.mjs, localization/baseline.json, docs/TRANSLATION-MAINTENANCE.ko.md, PROJECT_MEMORY.md, README.md
|
||||||
|
검사: 번역 추가와 변경 및 삭제 분류 검사 통과. 자리표시자 불일치와 미번역 분류 검사 통과. 점이 포함된 키와 중첩 키 및 배열 구분 검사 통과. 실제 UI 기준 1496개 비교 완료.
|
||||||
|
운영 반영: 운영 앱과 DB 변경 없음. 데이터 이전 없음. 배포 도구와 번역 절차만 추가.
|
||||||
|
복구 기준: 기존 배포 커밋 eddda22와 release/manifest.json 유지. 운영 데이터를 수정하지 않아 신규 운영 백업 없음.
|
||||||
|
제한: 코드와 도움말은 수동 검토가 필요하다. 최신 버전 지원과 공식 프로젝트 기여 완료를 뜻하지 않는다.
|
||||||
Reference in New Issue
Block a user