개발관련

Unity 게임 번역기 개발기 #5: RPG 메이커? 그것도 된다

삽질연구소 소장 2025. 10. 18. 09:57

새 기능 시작

RPG 메이커 게임도 번역이 하고 싶어졌다.

RPG Maker MV/MZ 구조 파악

RPG 메이커는 Unity랑 완전히 다르다.

RPGMaker_Game/
├── www/
│   └── data/
│       ├── Map001.json
│       ├── Map002.json
│       ├── CommonEvents.json
│       ├── Actors.json
│       └── ...

JSON이다. 그냥 평문 JSON.

Unity처럼 Bundle 파싱 필요 없다. 그냥 읽으면 된다.

import json

with open('www/data/Map001.json', 'r', encoding='utf-8') as f:
    data = json.load(f)

간단하다. 너무 간단해서 의심스럽다.

RPG Maker JSON 구조

{
  "id": 1,
  "name": "Map001",
  "events": [
    {
      "id": 1,
      "pages": [
        {
          "list": [
            {"code": 401, "parameters": ["こんにちは"]},
            {"code": 401, "parameters": ["元気ですか?"]},
            {"code": 102, "parameters": [["はい", "いいえ"]]}
          ]
        }
      ]
    }
  ]
}
  • code: 401 = 대사
  • code: 102 = 선택지
  • code: 355 = 스크립트

코드 번호로 구분. 심플하다.

추출 로직 (Unity보다 쉽다)

# core/rpgmaker_extractor.py

class RPGMakerExtractor:
    def extract_from_json(self, json_path):
        with open(json_path, 'r', encoding='utf-8') as f:
            data = json.load(f)

        entries = []

        # Map 파일
        if 'events' in data:
            for event in data['events']:
                if not event:
                    continue
                for page in event['pages']:
                    for cmd in page['list']:
                        # 401 = 대사, 405 = 스크롤 텍스트
                        if cmd['code'] in [401, 405]:
                            text = cmd['parameters'][0]
                            if self._is_japanese(text):
                                entries.append({
                                    'file': json_path.name,
                                    'original': text,
                                    'code': cmd['code']
                                })

                        # 102 = 선택지
                        elif cmd['code'] == 102:
                            for choice in cmd['parameters'][0]:
                                if self._is_japanese(choice):
                                    entries.append({
                                        'file': json_path.name,
                                        'original': choice,
                                        'code': 102
                                    })

        # CommonEvents, Actors, Items 등
        # ... 비슷한 로직 ...

        return entries

Unity보다 훨씬 간단하다. Bundle 파싱도 없고, TypeTree도 없다.

문제 발생: Excel 가져오기 폭사

번역 완료. Excel 내보내기 성공.

Excel 수정 후 가져오기 클릭.

Traceback (most recent call last):
  File "gui/handlers/excel_handler.py", line 316
    for entry in data.get('entries', []):
AttributeError: 'list' object has no attribute 'get'

터졌다.

원인 파악 (포맷 차이)

Unity 포맷 (dict):

{
    "entries": [
        {"text": "Hello", "translated": "안녕"},
        {"text": "World", "translated": "세계"}
    ]
}

RPG Maker 포맷 (list):

[
    {"id": 1, "original": "こんにちは", "translated": "안녕하세요"},
    {"id": 2, "original": "元気ですか", "translated": "잘 지내세요"}
]

JSON 최상위가 dict vs list.

코드는 Unity dict만 가정했다. RPG Maker list는 처리 못 한다.

수정: 포맷 감지 로직

# gui/handlers/excel_handler.py

def _apply_excel_to_json(self, json_path, updates_map):
    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    modified = False

    # 포맷 감지
    if isinstance(data, list):
        # RPG Maker 포맷 (list)
        for entry in data:
            original_text = entry.get('original', '')
            if original_text in updates_map:
                entry['translated'] = updates_map[original_text]
                modified = True

    elif isinstance(data, dict) and 'entries' in data:
        # Unity 포맷 (dict)
        for entry in data.get('entries', []):
            original_text = entry.get('text', '')
            if original_text in updates_map:
                entry['translated'] = updates_map[original_text]
                modified = True

    if modified:
        with open(json_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

    return modified

isinstance로 타입 체크. list면 RPG Maker, dict면 Unity.

Excel 가져오기 재시도. 성공.

번역 검수가 불편하다

Excel 워크플로우:

  1. 번역 완료
  2. Excel 내보내기
  3. Excel 열기
  4. 수정
  5. Excel 저장
  6. Excel 가져오기

6단계나 된다.

사용자: "Excel 열지 않고 프로그램에서 바로 검수하면 안 돼요?"

좋은 아이디어다.

번역 검수 뷰어 구현

요구사항

  • 페이징 (50개씩)
  • 검색 (원문/번역/수정본)
  • 테이블에서 직접 수정
  • 수정 사항 하이라이트
  • 저장 버튼 한 번에 적용

구현

# gui/widgets/translation_viewer.py

class TranslationViewerWidget(QWidget):
    entry_modified = pyqtSignal(int, str)  # (index, modified_text)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.entries = []
        self.filtered_entries = []
        self.current_page = 0
        self.items_per_page = 50
        self.modified_entries = {}  # {index: modified_text}

        self.init_ui()

    def init_ui(self):
        # 검색 영역
        self.search_input = QLineEdit()
        self.search_input.setPlaceholderText("원문 또는 번역문 검색...")

        self.search_type_combo = QComboBox()
        self.search_type_combo.addItems(["전체", "원문만", "번역만", "수정본만"])

        # 테이블
        self.table = QTableWidget()
        self.table.setColumnCount(5)
        self.table.setHorizontalHeaderLabels([
            "번호", "파일", "원문", "AI 번역", "수정본"
        ])

        # 수정본 컬럼만 편집 가능
        self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)

        # ... UI 구성 ...

페이징 로직

def update_table(self):
    # 현재 페이지 항목
    start_idx = self.current_page * self.items_per_page
    end_idx = min(start_idx + self.items_per_page, len(self.filtered_entries))
    page_entries = self.filtered_entries[start_idx:end_idx]

    self.table.setRowCount(len(page_entries))

    for row, entry in enumerate(page_entries):
        global_idx = self.entries.index(entry)

        # 원문
        item_original = QTableWidgetItem(entry['original'])
        item_original.setFlags(item_original.flags() & ~Qt.ItemFlag.ItemIsEditable)
        self.table.setItem(row, 2, item_original)

        # AI 번역
        item_translated = QTableWidgetItem(entry['translated'])
        item_translated.setFlags(item_translated.flags() & ~Qt.ItemFlag.ItemIsEditable)
        self.table.setItem(row, 3, item_translated)

        # 수정본 (편집 가능)
        modified = self.modified_entries.get(global_idx, '')
        item_modified = QTableWidgetItem(modified)
        item_modified.setFlags(item_modified.flags() | Qt.ItemFlag.ItemIsEditable)

        # 수정된 항목 하이라이트
        if modified:
            item_modified.setBackground(QColor(200, 255, 200))  # 연두색
            item_modified.setFont(QFont("맑은 고딕", 9, QFont.Weight.Bold))
        else:
            item_modified.setBackground(QColor(255, 255, 200))  # 노란색 (편집 대기)

        self.table.setItem(row, 4, item_modified)

검색 기능

def on_search(self):
    search_text = self.search_input.text().strip().lower()
    search_type = self.search_type_combo.currentText()

    if not search_text:
        self.filtered_entries = self.entries.copy()
    else:
        self.filtered_entries = []

        for entry in self.entries:
            original = entry.get('original', '').lower()
            translated = entry.get('translated', '').lower()

            idx = self.entries.index(entry)
            modified = self.modified_entries.get(idx, '').lower()

            # 검색 타입별 필터링
            match = False
            if search_type == "전체":
                match = (search_text in original or
                        search_text in translated or
                        search_text in modified)
            elif search_type == "원문만":
                match = search_text in original
            elif search_type == "번역만":
                match = search_text in translated
            elif search_type == "수정본만":
                match = search_text in modified

            if match:
                self.filtered_entries.append(entry)

    self.current_page = 0
    self.update_view()

UI 통합

# gui/ui/tab_builder.py

def create_excel_tab(self):
    layout = QVBoxLayout()

    # 버튼 영역
    btn_layout = QHBoxLayout()

    btn_load = QPushButton("📂 번역 결과 불러오기")
    btn_load.clicked.connect(self.load_translation_for_review)
    btn_layout.addWidget(btn_load)

    btn_export = QPushButton("📤 Excel 내보내기")
    btn_export.clicked.connect(self.export_excel)
    btn_layout.addWidget(btn_export)

    btn_import = QPushButton("📥 수정본 Excel 가져오기")
    btn_import.clicked.connect(self.import_excel_to_viewer)
    btn_layout.addWidget(btn_import)

    btn_save = QPushButton("💾 수정 사항 저장")
    btn_save.clicked.connect(self.save_viewer_modifications)
    btn_layout.addWidget(btn_save)

    layout.addLayout(btn_layout)

    # 뷰어 위젯
    self.translation_viewer = TranslationViewerWidget()
    layout.addWidget(self.translation_viewer)

    return layout

워크플로우 개선

Before (Excel 필수)

번역 → Excel 내보내기 → Excel 열기 → 수정 → 저장 → 가져오기
6단계

After (뷰어 사용)

번역 → 뷰어에서 바로 수정 → 저장
3단계

50% 단계 감소.

사용자는 여전히 Excel로도 작업 가능. 선택권 제공.

실제 사용 사례

RPG Maker 게임 번역

  1. 게임 폴더 선택 (www/data/ 자동 감지)
  2. 번역 시작 (Claude API)
  3. 뷰어에서 검수
    • "魔法" 검색 → 관련 대사 전부 확인
    • 고유명사 일관성 체크
    • 어색한 번역 수정
  4. 저장 버튼 클릭
  5. 게임 실행 → 한글 나옴

총 소요 시간: 10분

Unity + Naninovel 게임

이전과 동일하게 작동. 포맷 감지가 자동이라 신경 안 써도 됨.

성과

지원 게임 엔진

✅ Unity (일반)
✅ Unity + Naninovel
✅ RPG Maker MV
✅ RPG Maker MZ

워크플로우

✅ 뷰어 내 검수 (NEW)
✅ Excel 검수 (기존)
✅ 검색/필터 (NEW)
✅ 페이징 50개씩 (NEW)

코드 변경

+ gui/widgets/translation_viewer.py (379줄)
+ gui/widgets/__init__.py (5줄)
~ gui/handlers/excel_handler.py (+포맷 감지 로직)
~ gui/ui/tab_builder.py (뷰어 통합)

배운 것

  1. 포맷 가정하지 말기
    Unity는 dict, RPG Maker는 list. isinstance로 체크.
  2. 게으름이 기능을 만든다
    "Excel 말고 프로그램에서 확인하자" → 뷰어 탄생.
  3. 선택권을 주자
    Excel 워크플로우 제거 안 함. 뷰어 추가만.
    사용자가 선택. 강요 안 함.
  4. 페이징은 필수
    5,000개 항목 한 번에? 프로그램 죽음. 50개씩 나누기.
  5. 검색은 생산성
    "魔法" 검색 → 고유명사 일관성 한 번에 체크 가능.