AI 번역 (범용 · 용어집 우선)
작성 시각: 2026-10-11 범위: 포털 공용 번역 기능 — 텍스트박스·웹편집기 본문을 LLM 으로 다른 언어로 옮기고, 승인 용어집을 우선 적용해 별도 사본을 저장한다.
1. 왜 이렇게 하는가
- 사용자는 프로세스를 코드가 아니라 위치(NODE No.) 와 익숙한 용어로 인식한다. 기계 번역이 회사 용어를 제멋대로 옮기면 문서가 곧바로 틀린 말이 된다.
- 그래서 번역은 모델의 일반 번역이 아니라 사전이 정한 번역어를 우선한다. 무엇이 옳은지는 공유 용어집(정본: PortalConsole/DirectoryService 원장)이 정한다.
- 언어별 컬럼(
nameEn·descriptionEn)을 손으로 채우던 방식을 대체한다 — 언어를 늘리면 컬럼이 아니라 행이 는다.
2. 구성 (책임 분리)
| 계층 | 위치 | 책임 |
|---|---|---|
| 번역 엔진 | @hohyun94/portal-ai (packages/ai-core/src/translate.ts) | 프롬프트 생성(용어집 주입·HTML 마크업 보존), 응답 검증(용어 반영·구조 손실 경고). 모델·포털을 모른다 |
| 번역 UI | @hohyun94/portal-foundation (packages/ui-react/src/translation.tsx) | PortalTranslateControl — "번역 → 미리보기 → 저장" 한 흐름. 서버 호출은 호출부가 소유 |
| 번역 테이블 | ProcessCenter drizzle (translations) | 어느 포털·엔티티·필드·언어든 한 형식으로 저장. 키는 (content_key, field, locale) |
| 서버 | server/services/translationService.ts · server/routers/translation.ts | 승인 용어집 로드 → 엔진 호출 → 저장(upsert) · 조회 |
| 화면 | client/src/components/ProcessManagementPanel.tsx (수정 폼) · ProcessWorkspace.tsx (조회) · DesignQualityPanel.tsx (설계 품질) | 필드별 번역 컨트롤 · locale 별 본문 선택 · 관리자 화면 NODE No. 표기 |
3. 데이터 모델 (translations)
text
id · contentKey(portalId:entityType:entityId) · field · locale · content · sourceLocale
sourceContentHash(원문 해시 — 낡은 번역 감지) · glossarySnapshot(적용한 용어집 JSON) · status
createdBy · updatedBy · createdAt · updatedAt -- UNIQUE(contentKey, field, locale)content_key규약(process-center:process_node:12)은 용어 연결·추출 큐가 쓰는 것과 같다.- 마이그레이션:
drizzle/0035_translations.sql(MySQL)·drizzle/postgres-migrations/0026_translations.sql(PostgreSQL). 기존nameEn·descriptionEn을 한 번 백필한다(멱등).
4. 번역 규칙 (엔진)
- 용어집을 못 읽어도 번역은 계속한다: 원장 조회가 거부(포털 API Key 에
glossary:readscope 가 없어 HTTP 403insufficient_scope)되거나 일시 장애면 빈 용어집으로 일반 번역하고 미리보기에 경고를 남긴다(loadGlossarySafely). 사전 문제로 기능 전체가 멈추지 않게 하는 경계다.- 고치는 방법: PortalConsole 관리자 → 포털 API Key 에서 ProcessCenter 키의 scope 에
glossary:read를 추가(또는 키 재발급 후PORTAL_CONSOLE_API_KEY갱신).
- 고치는 방법: PortalConsole 관리자 → 포털 API Key 에서 ProcessCenter 키의 scope 에
- 용어집 우선: 본문에 실제로 나온 승인 용어만 골라 "이 표기는 반드시 이 번역어" 를 시스템 프롬프트에 주입한다. 응답이 그 번역어를 담았는지 검증하고, 빠졌으면 경고로 남긴다(사람이 미리보기에서 본다).
- HTML 보존: 웹편집기 본문은 태그·속성·구조를 그대로 두고 텍스트 노드만 번역한다(
href·src·data-asset-id·코드 값 불변). 태그가 크게 줄면 "구조 손실" 경고. - 지시 언어: 한국어 원문은 한국어로, 그 밖은 영어로 지시한다.
- 지시 문구는 소스 언어 기준이며, 대상 언어는 프롬프트에 이름으로 들어간다(ko → "한국어", en → "영어", zh → "중국어" …).
5. 저장·조회 규칙 (테이블이 정본)
- 저장: 사람이 미리보기에서 확정한 번역만 저장한다(
translation.save→ upsert). 재번역하면 덮어쓴다.- 미리보기에서 번역문을 직접 고칠 수 있다 — 저장되는 값은 AI 원본이 아니라 사람이 고친 문장이다. 리치 본문(
detailContent·manualContent)은 웹편집기로 편집해 원시 HTML 을 손대지 않는다. - 함께 남기는 것: 원문 해시(
sourceContentHash— 마크업을 지운 텍스트의 sha256 앞 40자)와 적용한 용어집(glossarySnapshot).
- 미리보기에서 번역문을 직접 고칠 수 있다 — 저장되는 값은 AI 원본이 아니라 사람이 고친 문장이다. 리치 본문(
- 조회: 프로세스 목록 응답(
process.list)이 번역 테이블에서 언어본을 채운다.nameEn·descriptionEn응답 필드는 그대로 유지하되 값의 출처가 테이블이 된다(레거시 컬럼은 아직 번역이 없는 행의 폴백).- 리치 본문은
detailContentEn·manualContentEn로 내려주고, 조회 화면이 locale 에 따라 고른다. translationStale: string[]로 낡은 번역 필드를 함께 내려준다.
- 수동 입력 중단: 수정 폼의
NAME / EN입력은 제거했다 — 영어 이름은 번역으로만 만든다. - 설명(description): 수정 폼에서 편집하고(
DESCRIPTION / KO+ 번역 컨트롤), 언어본은 번역으로 만든다. 조회 화면은 설명 칸을 쓰지 않는다(기존 설계). - 관리자 화면 어휘: 설계 품질 패널·상위 항목 선택도 코드가 아니라 NODE No. 로 프로세스를 가리킨다(사용자 화면과 같은 어휘).
5-1. 낡은 번역 (원문 변경 감지)
- 저장 때 남긴 원문 해시와 현재 원문 해시가 다르면 그 필드는 낡았다.
- 화면: 조회 화면은 영어로 볼 때 해당 섹션 제목 옆에
영어 번역 낡음배지(translation-stale-*)를, 수정 폼은 번역 컨트롤 위에 다시 번역 안내(translate-stale-*)를 띄운다. - 노드 저장 직후:
translation.stale로 낡은 필드를 확인해 "원문이 바뀐 영어 번역이 N건 있습니다" 토스트로 알린다. 자동 재번역은 하지 않는다 — 사람이 미리보기로 확인해야 한다.
5-2. 미리보기 캐시
- 저장하지 않는 미리보기(번역)는 같은 원문 + 대상 언어 + 용어집이면 30분간 결과를 재사용한다(모델 호출 절약).
- 캐시 키에 용어집 서명이 들어가므로, 용어집이 바뀌면 자동으로 다시 번역한다.
clearTranslationCache()로 일괄 무효화할 수 있다. - 같은 본문이 여러 노드에 있으면 일괄 재번역에서도 한 번만 모델을 부른다.
5-3. 낡은 번역 일괄 재번역 (관리자 도구)
- 관리자 화면의
낡은 번역 재번역버튼(translation.retranslateStale)이 낡은 번역을 한도(기본 20, 최대 50) 안에서 다시 번역해 바로 저장한다. - 사람이 미리보기로 확정하는 원칙의 예외가 아니라 관리자 도구다 — 원문만 바뀌고 방치된 번역을 되살린다.
- 한 건이 실패해도 나머지는 계속 처리하고, 결과는
{ processed, failed, skipped }로 요약해 토스트로 알린다.
6. 다국어 확장 경로
- 지금은 ko↔en 이다. 중국어·베트남어를 더하려면 ① 대상 언어를
translations.locale에 행으로 추가(스키마 변경 없음) ② 중앙 용어집 원장에 언어쌍 추가(sourceLocale·targetLocale) ③SUPPORTED_LOCALES확장. - 테이블·엔진·UI 는 언어를 나열하지 않으므로 스키마 변경 없이 언어가 늘어난다.
7. API
| 라우트 | 권한 | 설명 |
|---|---|---|
translation.translate | 관리자 | 필드 본문을 번역(저장 안 함) — 결과·적용 용어집·경고 반환. 같은 입력은 캐시 재사용 |
translation.save | 관리자 | 확정한 번역본 upsert — 원문 텍스트(sourceText)로 해시를 남기고, 적용 용어집을 스냅샷으로 저장 |
translation.list | 로그인 | 노드 하나의 번역본 전부 |
translation.stale | 로그인 | 원문이 바뀌어 다시 번역해야 하는 필드 목록 |
translation.retranslateStale | 관리자 | 낡은 번역 일괄 재번역(한도 1~50) → { processed, failed, skipped } |
8. 테스트
- 엔진:
packages/ai-core/src/translate.test.ts— 용어집 선별·주입·검증, HTML 보존/손실 경고, 빈 본문. - UI:
packages/ui-react/src/translation.contract.test.tsx— 번역→미리보기→저장, 실패 표시, HTML 렌더. - 서버:
server/translationService.test.ts— 용어집 로드, AI 비활성 폴백, 미리보기 캐시, HTML 필드 지시, 원문 해시(마크업 무관), 저장 시 해시·용어집 스냅샷, 목록 병합·낡음 판정, 낡은 필드 조회.