다국어 시스템 가이드 작성
This commit is contained in:
parent
7bb7f1621f
commit
bc24cccdf1
|
|
@ -0,0 +1,970 @@
|
|||
# 다국어 관리 시스템 가이드
|
||||
|
||||
## 📋 목차
|
||||
|
||||
1. [시스템 개요](#시스템-개요)
|
||||
2. [아키텍처 구조](#아키텍처-구조)
|
||||
3. [데이터베이스 구조](#데이터베이스-구조)
|
||||
4. [핵심 컴포넌트](#핵심-컴포넌트)
|
||||
5. [사용법 가이드](#사용법-가이드)
|
||||
6. [페이지 적용 방법](#페이지-적용-방법)
|
||||
7. [새로운 다국어 키 추가](#새로운-다국어-키-추가)
|
||||
8. [문제 해결](#문제-해결)
|
||||
9. [모범 사례](#모범-사례)
|
||||
|
||||
## 🎯 시스템 개요
|
||||
|
||||
### 다국어 시스템이란?
|
||||
|
||||
현재 ERP 시스템은 한국어와 영어를 지원하는 다국어 시스템을 구축하고 있습니다. 사용자가 설정한 언어에 따라 UI의 모든 텍스트가 자동으로 번역되어 표시됩니다.
|
||||
|
||||
### 지원 언어
|
||||
|
||||
- **한국어 (KR)**: 기본 언어
|
||||
- **영어 (EN)**: 사용자 설정 가능
|
||||
|
||||
### 주요 특징
|
||||
|
||||
- **실시간 번역**: 페이지 로드 시 즉시 번역 적용
|
||||
- **배치 처리**: 여러 키를 한 번에 조회하여 성능 최적화
|
||||
- **캐싱 시스템**: 번역 결과를 메모리에 캐싱하여 중복 요청 방지
|
||||
- **폴백 시스템**: 번역 실패 시 기본 텍스트 표시
|
||||
|
||||
## 🏗️ 아키텍처 구조
|
||||
|
||||
### 전체 구조도
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Frontend │ │ Backend │ │ Database │
|
||||
│ │ │ │ │ │
|
||||
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
|
||||
│ │ useMultiLang│ │ │ │ Multilang │ │ │ │ multilang │ │
|
||||
│ │ Hook │ │ │ │ Controller │ │ │ │ Table │ │
|
||||
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
|
||||
│ │ Multilang │ │ │ │ Multilang │ │ │ │ lang_keys │ │
|
||||
│ │ Utils │ │ │ │ Service │ │ │ │ Table │ │
|
||||
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │
|
||||
│ │ UI │ │ │ │ Batch API │ │ │ │ translations│ │
|
||||
│ │ Components │ │ │ │ Endpoint │ │ │ │ Table │ │
|
||||
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### 데이터 흐름
|
||||
|
||||
1. **사용자 언어 설정**: `useMultiLang` 훅에서 사용자 언어 확인
|
||||
2. **다국어 키 정의**: 컴포넌트에서 필요한 다국어 키 상수 정의
|
||||
3. **배치 요청**: 여러 키를 한 번에 백엔드로 전송
|
||||
4. **번역 조회**: 백엔드에서 데이터베이스에서 번역 데이터 조회
|
||||
5. **결과 반환**: 번역된 텍스트를 프론트엔드로 반환
|
||||
6. **UI 업데이트**: 번역된 텍스트로 컴포넌트 렌더링
|
||||
|
||||
## 🗄️ 데이터베이스 구조
|
||||
|
||||
### 실제 테이블 구조
|
||||
|
||||
#### 1. language_master (언어 마스터)
|
||||
|
||||
```sql
|
||||
CREATE TABLE public.language_master (
|
||||
lang_code varchar(10) NOT NULL PRIMARY KEY, -- 언어 코드 (KR, EN)
|
||||
lang_name varchar(50) NOT NULL, -- 언어명 (Korean, English)
|
||||
lang_native varchar(50) NOT NULL, -- 원어명 (한국어, English)
|
||||
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
||||
sort_order int4 DEFAULT 0, -- 정렬 순서
|
||||
created_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by varchar(50),
|
||||
updated_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by varchar(50)
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. multi_lang_key_master (다국어 키 마스터)
|
||||
|
||||
```sql
|
||||
CREATE TABLE public.multi_lang_key_master (
|
||||
key_id serial4 NOT NULL PRIMARY KEY, -- 키 ID (자동 증가)
|
||||
company_code varchar(20) DEFAULT '*' NOT NULL, -- 회사 코드 (* = 공통)
|
||||
lang_key varchar(100) NOT NULL, -- 다국어 키 (예: button.add)
|
||||
description text, -- 키 설명
|
||||
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
||||
menu_name varchar(50), -- 메뉴명 (사용하지 않음)
|
||||
created_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by varchar(50),
|
||||
updated_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by varchar(50),
|
||||
CONSTRAINT uk_lang_key_company UNIQUE (company_code, lang_key)
|
||||
);
|
||||
```
|
||||
|
||||
#### 3. multi_lang_text (다국어 텍스트)
|
||||
|
||||
```sql
|
||||
CREATE TABLE public.multi_lang_text (
|
||||
text_id serial4 NOT NULL PRIMARY KEY, -- 텍스트 ID (자동 증가)
|
||||
key_id int4 NOT NULL, -- 키 마스터의 key_id
|
||||
lang_code varchar(10) NOT NULL, -- 언어 코드 (KR, EN)
|
||||
lang_text text NOT NULL, -- 번역된 텍스트
|
||||
is_active bpchar(1) DEFAULT 'Y', -- 활성화 여부
|
||||
created_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by varchar(50),
|
||||
updated_date timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by varchar(50),
|
||||
CONSTRAINT multi_lang_text_key_id_lang_code_key UNIQUE (key_id, lang_code),
|
||||
CONSTRAINT multi_lang_text_key_id_fkey FOREIGN KEY (key_id)
|
||||
REFERENCES multi_lang_key_master(key_id) ON DELETE CASCADE,
|
||||
CONSTRAINT multi_lang_text_lang_code_fkey FOREIGN KEY (lang_code)
|
||||
REFERENCES language_master(lang_code)
|
||||
);
|
||||
```
|
||||
|
||||
### 테이블 관계도
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ language_master │ │multi_lang_key_ │ │ multi_lang_text │
|
||||
│ │ │ master │ │ │
|
||||
│ - lang_code │ │ - key_id │ │ - text_id │
|
||||
│ - lang_name │ │ - company_code │ │ - lang_key │
|
||||
│ - lang_native │ │ - description │ │ - key_id │
|
||||
│ - is_active │ │ - is_active │ │ - lang_code │
|
||||
│ - sort_order │ │ - created_date │ │ - lang_text │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
│ │ │
|
||||
└───────────────────────┼───────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 외래키 관계 │
|
||||
│ │
|
||||
│ lang_code │
|
||||
│ key_id │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 데이터 저장 예시
|
||||
|
||||
#### 1. 언어 마스터 데이터
|
||||
|
||||
```sql
|
||||
INSERT INTO language_master (lang_code, lang_name, lang_native) VALUES
|
||||
('KR', 'Korean', '한국어'),
|
||||
('EN', 'English', 'English');
|
||||
```
|
||||
|
||||
#### 2. 다국어 키 등록
|
||||
|
||||
```sql
|
||||
INSERT INTO multi_lang_key_master (lang_key, company_code, description) VALUES
|
||||
('button.add', '*', '추가 버튼'),
|
||||
('button.edit', '*', '수정 버튼'),
|
||||
('menu.title', '*', '메뉴 제목');
|
||||
```
|
||||
|
||||
#### 3. 번역 텍스트 등록
|
||||
|
||||
```sql
|
||||
-- 한국어 번역
|
||||
INSERT INTO multi_lang_text (key_id, lang_code, lang_text)
|
||||
SELECT km.key_id, 'KR',
|
||||
CASE km.lang_key
|
||||
WHEN 'button.add' THEN '추가'
|
||||
WHEN 'button.edit' THEN '수정'
|
||||
WHEN 'menu.title' THEN '메뉴 관리'
|
||||
END
|
||||
FROM multi_lang_key_master km
|
||||
WHERE km.lang_key IN ('button.add', 'button.edit', 'menu.title')
|
||||
AND km.company_code = '*';
|
||||
|
||||
-- 영어 번역
|
||||
INSERT INTO multi_lang_text (key_id, lang_code, lang_text)
|
||||
SELECT km.key_id, 'EN',
|
||||
CASE km.lang_key
|
||||
WHEN 'button.add' THEN 'Add'
|
||||
WHEN 'button.edit' THEN 'Edit'
|
||||
WHEN 'menu.title' THEN 'Menu Management'
|
||||
END
|
||||
FROM multi_lang_key_master km
|
||||
WHERE km.lang_key IN ('button.add', 'button.edit', 'menu.title')
|
||||
AND km.company_code = '*';
|
||||
```
|
||||
|
||||
## 🔧 핵심 컴포넌트
|
||||
|
||||
### 1. useMultiLang 훅
|
||||
|
||||
```typescript
|
||||
// frontend/hooks/useMultiLang.ts
|
||||
export const useMultiLang = (options: { companyCode?: string } = {}) => {
|
||||
const [userLang, setUserLang] = useState<string | null>(null);
|
||||
|
||||
// 사용자 언어 조회 및 설정
|
||||
// 전역 언어 상태 동기화
|
||||
// 언어 변경 처리
|
||||
|
||||
return {
|
||||
userLang, // 현재 사용자 언어 (KR, EN)
|
||||
getText, // 다국어 텍스트 조회 함수
|
||||
changeLang, // 언어 변경 함수
|
||||
companyCode, // 회사 코드
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**주요 기능:**
|
||||
|
||||
- 사용자 언어 설정 및 조회
|
||||
- 전역 언어 상태 관리
|
||||
- 언어 변경 시 자동 동기화
|
||||
|
||||
**사용 예시:**
|
||||
|
||||
```typescript
|
||||
const { userLang, getText, changeLang } = useMultiLang({ companyCode: "*" });
|
||||
|
||||
// 언어 변경
|
||||
await changeLang("EN");
|
||||
|
||||
// 다국어 텍스트 조회
|
||||
const text = await getText("menu.management", "button.add");
|
||||
```
|
||||
|
||||
### 2. Multilang Utils
|
||||
|
||||
```typescript
|
||||
// frontend/lib/utils/multilang.ts
|
||||
export const MENU_MANAGEMENT_KEYS = {
|
||||
TITLE: "menu.management.title",
|
||||
BUTTON_ADD: "button.add",
|
||||
BUTTON_EDIT: "button.edit",
|
||||
// ... 더 많은 키들
|
||||
} as const;
|
||||
|
||||
// 배치 조회 함수
|
||||
export async function getMultilangText(
|
||||
key: string,
|
||||
companyCode: string = "*",
|
||||
menuCode: string = "MENU_MANAGEMENT",
|
||||
userLang: string = "KR"
|
||||
): Promise<string>;
|
||||
|
||||
// 동기 조회 함수 (캐시에서만)
|
||||
export function getMultilangTextSync(
|
||||
key: string,
|
||||
companyCode: string = "*",
|
||||
menuCode: string = "MENU_MANAGEMENT",
|
||||
userLang: string = "KR"
|
||||
): string;
|
||||
```
|
||||
|
||||
**주요 기능:**
|
||||
|
||||
- 다국어 키 상수 정의
|
||||
- 배치 처리로 성능 최적화
|
||||
- 캐싱 시스템으로 중복 요청 방지
|
||||
- 동기/비동기 조회 지원
|
||||
|
||||
### 3. Backend API
|
||||
|
||||
```typescript
|
||||
// backend/src/controllers/multilangController.ts
|
||||
@Post("/batch")
|
||||
async getBatchTranslations(
|
||||
@Body() body: { langKeys: string[] },
|
||||
@Query() query: { companyCode: string; menuCode: string; userLang: string }
|
||||
): Promise<ApiResponse<Record<string, string>>>
|
||||
```
|
||||
|
||||
**API 엔드포인트:**
|
||||
|
||||
- **POST** `/multilang/batch` - 여러 키를 한 번에 조회
|
||||
- **GET** `/multilang/:key` - 개별 키 조회 (사용하지 않음)
|
||||
|
||||
**요청 파라미터:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
langKeys: ["button.add", "button.edit", "menu.title"],
|
||||
companyCode: "*", // 회사 코드 (* = 전체)
|
||||
menuCode: "MENU_MANAGEMENT", // 메뉴 코드
|
||||
userLang: "KR" // 사용자 언어
|
||||
}
|
||||
```
|
||||
|
||||
**응답 형식:**
|
||||
|
||||
```typescript
|
||||
{
|
||||
success: true,
|
||||
data: {
|
||||
"button.add": "추가",
|
||||
"button.edit": "수정",
|
||||
"menu.title": "메뉴 관리"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📖 사용법 가이드
|
||||
|
||||
### 1. 기본 사용법
|
||||
|
||||
#### 단계별 가이드
|
||||
|
||||
1. **다국어 키 상수 정의**
|
||||
2. **컴포넌트에서 다국어 텍스트 로드**
|
||||
3. **UI에 번역된 텍스트 적용**
|
||||
|
||||
#### 예시 코드
|
||||
|
||||
```typescript
|
||||
// 1. 다국어 키 상수 정의
|
||||
const MENU_KEYS = ["menu.title", "button.add", "button.edit"];
|
||||
|
||||
// 2. 다국어 텍스트 로드
|
||||
const [uiTexts, setUiTexts] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const loadTexts = async () => {
|
||||
const response = await apiClient.post("/multilang/batch", {
|
||||
langKeys: MENU_KEYS,
|
||||
companyCode: "*",
|
||||
menuCode: "MENU_MANAGEMENT",
|
||||
userLang: userLang,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
setUiTexts(response.data.data);
|
||||
}
|
||||
};
|
||||
|
||||
if (userLang) {
|
||||
loadTexts();
|
||||
}
|
||||
}, [userLang]);
|
||||
|
||||
// 3. UI에 적용
|
||||
const getText = (key: string) => uiTexts[key] || key;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{getText("menu.title")}</h1>
|
||||
<button>{getText("button.add")}</button>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### 2. 고급 사용법
|
||||
|
||||
#### 배치 처리 최적화
|
||||
|
||||
```typescript
|
||||
// 여러 키를 한 번에 조회하여 API 호출 최소화
|
||||
const LANG_KEYS = [
|
||||
"form.title",
|
||||
"form.name.label",
|
||||
"form.name.placeholder",
|
||||
"form.submit.button",
|
||||
];
|
||||
|
||||
// 한 번의 API 호출로 모든 키 조회
|
||||
const translations = await getMultilangTextBatch(LANG_KEYS);
|
||||
```
|
||||
|
||||
#### 폴백 시스템
|
||||
|
||||
```typescript
|
||||
const getText = (key: string, fallback?: string) => {
|
||||
// 1. 번역된 텍스트 우선
|
||||
if (uiTexts[key]) return uiTexts[key];
|
||||
|
||||
// 2. 폴백 텍스트 사용
|
||||
if (fallback) return fallback;
|
||||
|
||||
// 3. 키 자체 반환 (디버깅용)
|
||||
return key;
|
||||
};
|
||||
```
|
||||
|
||||
## 🚀 페이지 적용 방법
|
||||
|
||||
### 1. 새로운 페이지에 다국어 적용하기
|
||||
|
||||
#### Step 1: 다국어 키 정의
|
||||
|
||||
```typescript
|
||||
// constants/pageKeys.ts
|
||||
export const PAGE_KEYS = {
|
||||
// 페이지 제목
|
||||
TITLE: "page.title",
|
||||
DESCRIPTION: "page.description",
|
||||
|
||||
// 폼 요소
|
||||
FORM_NAME: "form.name",
|
||||
FORM_EMAIL: "form.email",
|
||||
FORM_SUBMIT: "form.submit",
|
||||
|
||||
// 메시지
|
||||
SUCCESS_MESSAGE: "message.success",
|
||||
ERROR_MESSAGE: "message.error",
|
||||
} as const;
|
||||
```
|
||||
|
||||
#### Step 2: 컴포넌트에서 다국어 텍스트 로드
|
||||
|
||||
```typescript
|
||||
// components/MyPage.tsx
|
||||
import { PAGE_KEYS } from "@/constants/pageKeys";
|
||||
|
||||
export const MyPage: React.FC = () => {
|
||||
const { userLang } = useMultiLang();
|
||||
const [uiTexts, setUiTexts] = useState<Record<string, string>>({});
|
||||
|
||||
// 다국어 텍스트 로드
|
||||
useEffect(() => {
|
||||
const loadTexts = async () => {
|
||||
if (!userLang) return;
|
||||
|
||||
const response = await apiClient.post("/multilang/batch", {
|
||||
langKeys: Object.values(PAGE_KEYS),
|
||||
companyCode: "*",
|
||||
menuCode: "MY_PAGE",
|
||||
userLang: userLang,
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
setUiTexts(response.data.data);
|
||||
}
|
||||
};
|
||||
|
||||
loadTexts();
|
||||
}, [userLang]);
|
||||
|
||||
// 텍스트 가져오기 함수
|
||||
const getText = (key: string) => uiTexts[key] || key;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{getText(PAGE_KEYS.TITLE)}</h1>
|
||||
<p>{getText(PAGE_KEYS.DESCRIPTION)}</p>
|
||||
|
||||
<form>
|
||||
<label>{getText(PAGE_KEYS.FORM_NAME)}</label>
|
||||
<input placeholder={getText(PAGE_KEYS.FORM_NAME)} />
|
||||
|
||||
<button type="submit">{getText(PAGE_KEYS.FORM_SUBMIT)}</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### Step 3: 데이터베이스에 번역 데이터 등록
|
||||
|
||||
```sql
|
||||
-- 1. 다국어 키 마스터에 키 등록
|
||||
INSERT INTO multi_lang_key_master (
|
||||
lang_key,
|
||||
company_code,
|
||||
description,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
) VALUES
|
||||
('page.title', '*', '페이지 제목', 'Y', now(), 'system'),
|
||||
('page.description', '*', '페이지 설명', 'Y', now(), 'system'),
|
||||
('form.name', '*', '이름', 'Y', now(), 'system'),
|
||||
('form.email', '*', '이메일', 'Y', now(), 'system'),
|
||||
('form.submit', '*', '제출', 'Y', now(), 'system');
|
||||
|
||||
-- 2. 한국어 번역 텍스트 등록
|
||||
INSERT INTO multi_lang_text (
|
||||
key_id,
|
||||
lang_code,
|
||||
lang_text,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
)
|
||||
SELECT
|
||||
km.key_id,
|
||||
'KR',
|
||||
CASE km.lang_key
|
||||
WHEN 'page.title' THEN '내 페이지'
|
||||
WHEN 'page.description' THEN '이것은 내 페이지입니다'
|
||||
WHEN 'form.name' THEN '이름'
|
||||
WHEN 'form.email' THEN '이메일'
|
||||
WHEN 'form.submit' THEN '제출'
|
||||
END,
|
||||
'Y',
|
||||
now(),
|
||||
'system'
|
||||
FROM multi_lang_key_master km
|
||||
WHERE km.lang_key IN ('page.title', 'page.description', 'form.name', 'form.email', 'form.submit')
|
||||
AND km.company_code = '*';
|
||||
|
||||
-- 3. 영어 번역 텍스트 등록
|
||||
INSERT INTO multi_lang_text (
|
||||
key_id,
|
||||
lang_code,
|
||||
lang_text,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
)
|
||||
SELECT
|
||||
km.key_id,
|
||||
'EN',
|
||||
CASE km.lang_key
|
||||
WHEN 'page.title' THEN 'My Page'
|
||||
WHEN 'page.description' THEN 'This is my page'
|
||||
WHEN 'form.name' THEN 'Name'
|
||||
WHEN 'form.email' THEN 'Email'
|
||||
WHEN 'form.submit' THEN 'Submit'
|
||||
END,
|
||||
'Y',
|
||||
now(),
|
||||
'system'
|
||||
FROM multi_lang_key_master km
|
||||
WHERE km.lang_key IN ('page.title', 'page.description', 'form.name', 'form.email', 'form.submit')
|
||||
AND km.company_code = '*';
|
||||
```
|
||||
|
||||
### 2. 기존 페이지에 다국어 적용하기
|
||||
|
||||
#### Step 1: 하드코딩된 텍스트 찾기
|
||||
|
||||
```typescript
|
||||
// 변경 전
|
||||
return (
|
||||
<div>
|
||||
<h1>메뉴 관리</h1>
|
||||
<button>추가</button>
|
||||
<button>수정</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 변경 후
|
||||
return (
|
||||
<div>
|
||||
<h1>{getText("menu.management.title")}</h1>
|
||||
<button>{getText("button.add")}</button>
|
||||
<button>{getText("button.edit")}</button>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
#### Step 2: 다국어 키 상수 추가
|
||||
|
||||
```typescript
|
||||
// 기존 키에 새로운 키 추가
|
||||
export const EXISTING_KEYS = [
|
||||
// 기존 키들...
|
||||
"menu.management.title",
|
||||
"button.add",
|
||||
"button.edit",
|
||||
];
|
||||
```
|
||||
|
||||
#### Step 3: 번역 데이터 등록
|
||||
|
||||
```sql
|
||||
-- 1. 다국어 키 마스터에 키 등록
|
||||
INSERT INTO multi_lang_key_master (
|
||||
lang_key,
|
||||
company_code,
|
||||
description,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
) VALUES
|
||||
('menu.management.title', '*', '메뉴 관리 제목', 'Y', now(), 'system'),
|
||||
('button.add', '*', '추가 버튼', 'Y', now(), 'system'),
|
||||
('button.edit', '*', '수정 버튼', 'Y', now(), 'system');
|
||||
|
||||
-- 2. 한국어 번역 텍스트 등록
|
||||
INSERT INTO multi_lang_text (
|
||||
key_id,
|
||||
lang_code,
|
||||
lang_text,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
)
|
||||
SELECT
|
||||
km.key_id,
|
||||
'KR',
|
||||
CASE km.lang_key
|
||||
WHEN 'menu.management.title' THEN '메뉴 관리'
|
||||
WHEN 'button.add' THEN '추가'
|
||||
WHEN 'button.edit' THEN '수정'
|
||||
END,
|
||||
'Y',
|
||||
now(),
|
||||
'system'
|
||||
FROM multi_lang_key_master km
|
||||
WHERE km.lang_key IN ('menu.management.title', 'button.add', 'button.edit')
|
||||
AND km.company_code = '*';
|
||||
|
||||
-- 3. 영어 번역 텍스트 등록
|
||||
INSERT INTO multi_lang_text (
|
||||
key_id,
|
||||
lang_code,
|
||||
lang_text,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
)
|
||||
SELECT
|
||||
km.key_id,
|
||||
'EN',
|
||||
CASE km.lang_key
|
||||
WHEN 'menu.management.title' THEN 'Menu Management'
|
||||
WHEN 'button.add' THEN 'Add'
|
||||
WHEN 'button.edit' THEN 'Edit'
|
||||
END,
|
||||
'Y',
|
||||
now(),
|
||||
'system'
|
||||
FROM multi_lang_key_master km
|
||||
WHERE km.lang_key IN ('menu.management.title', 'button.add', 'button.edit')
|
||||
AND km.company_code = '*';
|
||||
```
|
||||
|
||||
## ➕ 새로운 다국어 키 추가
|
||||
|
||||
### 1. 프론트엔드에서 키 추가
|
||||
|
||||
#### Step 1: 키 상수 정의
|
||||
|
||||
```typescript
|
||||
// constants/multilang.ts
|
||||
export const NEW_FEATURE_KEYS = {
|
||||
// 새로운 기능 관련 키들
|
||||
FEATURE_TITLE: "new.feature.title",
|
||||
FEATURE_DESCRIPTION: "new.feature.description",
|
||||
FEATURE_BUTTON: "new.feature.button",
|
||||
} as const;
|
||||
```
|
||||
|
||||
#### Step 2: 컴포넌트에서 사용
|
||||
|
||||
```typescript
|
||||
import { NEW_FEATURE_KEYS } from "@/constants/multilang";
|
||||
|
||||
const MyComponent = () => {
|
||||
const getText = (key: string) => uiTexts[key] || key;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{getText(NEW_FEATURE_KEYS.FEATURE_TITLE)}</h2>
|
||||
<p>{getText(NEW_FEATURE_KEYS.FEATURE_DESCRIPTION)}</p>
|
||||
<button>{getText(NEW_FEATURE_KEYS.FEATURE_BUTTON)}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 백엔드에서 번역 데이터 등록
|
||||
|
||||
#### Step 1: 다국어 키 등록
|
||||
|
||||
```sql
|
||||
-- multi_lang_key_master 테이블에 새로운 키 등록
|
||||
INSERT INTO multi_lang_key_master (
|
||||
lang_key,
|
||||
company_code,
|
||||
description,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
) VALUES
|
||||
('new.feature.title', '*', '새로운 기능 제목', 'Y', now(), 'system'),
|
||||
('new.feature.description', '*', '새로운 기능 설명', 'Y', now(), 'system'),
|
||||
('new.feature.button', '*', '새로운 기능 버튼', 'Y', now(), 'system');
|
||||
```
|
||||
|
||||
#### Step 2: 번역 텍스트 등록
|
||||
|
||||
```sql
|
||||
-- 한국어 번역 텍스트 등록
|
||||
INSERT INTO multi_lang_text (
|
||||
key_id,
|
||||
lang_code,
|
||||
lang_text,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
)
|
||||
SELECT
|
||||
km.key_id,
|
||||
'KR',
|
||||
CASE km.lang_key
|
||||
WHEN 'new.feature.title' THEN '새로운 기능'
|
||||
WHEN 'new.feature.description' THEN '이것은 새로운 기능입니다'
|
||||
WHEN 'new.feature.button' THEN '시작하기'
|
||||
END,
|
||||
'Y',
|
||||
now(),
|
||||
'system'
|
||||
FROM multi_lang_key_master km
|
||||
WHERE km.lang_key IN ('new.feature.title', 'new.feature.description', 'new.feature.button')
|
||||
AND km.company_code = '*';
|
||||
|
||||
-- 영어 번역 텍스트 등록
|
||||
INSERT INTO multi_lang_text (
|
||||
key_id,
|
||||
lang_code,
|
||||
lang_text,
|
||||
is_active,
|
||||
created_date,
|
||||
created_by
|
||||
)
|
||||
SELECT
|
||||
km.key_id,
|
||||
'EN',
|
||||
CASE km.lang_key
|
||||
WHEN 'new.feature.title' THEN 'New Feature'
|
||||
WHEN 'new.feature.description' THEN 'This is a new feature'
|
||||
WHEN 'new.feature.button' THEN 'Get Started'
|
||||
END,
|
||||
'Y',
|
||||
now(),
|
||||
'system'
|
||||
FROM multi_lang_key_master km
|
||||
WHERE km.lang_key IN ('new.feature.title', 'new.feature.description', 'new.feature.button')
|
||||
AND km.company_code = '*';
|
||||
```
|
||||
|
||||
## 🔍 문제 해결
|
||||
|
||||
### 1. 일반적인 문제들
|
||||
|
||||
#### 문제: 키가 그대로 표시됨
|
||||
|
||||
```typescript
|
||||
// 문제 상황
|
||||
<h1>menu.management.title</h1>;
|
||||
|
||||
// 원인: 번역 데이터가 로드되지 않음
|
||||
// 해결: 다국어 텍스트 로드 확인
|
||||
console.log("uiTexts:", uiTexts);
|
||||
console.log("userLang:", userLang);
|
||||
```
|
||||
|
||||
#### 문제: 일부만 번역됨
|
||||
|
||||
```typescript
|
||||
// 원인: 일부 키만 번역 데이터에 등록됨
|
||||
// 해결: 모든 키에 대한 번역 데이터 확인
|
||||
SELECT km.lang_key, t.lang_code, t.lang_text
|
||||
FROM multi_lang_key_master km
|
||||
LEFT JOIN multi_lang_text t ON km.key_id = t.key_id
|
||||
WHERE km.lang_key IN ('key1', 'key2', 'key3')
|
||||
AND km.company_code = '*';
|
||||
```
|
||||
|
||||
#### 문제: 언어 변경이 반영되지 않음
|
||||
|
||||
```typescript
|
||||
// 원인: useMultiLang 훅의 의존성 배열 문제
|
||||
// 해결: userLang 변경 시 다국어 텍스트 재로드
|
||||
useEffect(() => {
|
||||
if (userLang) {
|
||||
loadTexts();
|
||||
}
|
||||
}, [userLang]); // userLang 의존성 추가
|
||||
```
|
||||
|
||||
### 2. 디버깅 방법
|
||||
|
||||
#### 콘솔 로그 확인
|
||||
|
||||
```typescript
|
||||
// 다국어 텍스트 로드 과정 추적
|
||||
console.log("🌐 다국어 텍스트 로드 시작:", {
|
||||
userLang,
|
||||
keys: MENU_KEYS,
|
||||
uiTextsCount: Object.keys(uiTexts).length,
|
||||
});
|
||||
|
||||
// API 응답 확인
|
||||
console.log("📡 API 응답:", response.data);
|
||||
|
||||
// 최종 상태 확인
|
||||
console.log("✅ 최종 uiTexts:", uiTexts);
|
||||
```
|
||||
|
||||
#### 네트워크 탭 확인
|
||||
|
||||
- **API 호출**: `/multilang/batch` 엔드포인트 호출 확인
|
||||
- **요청 파라미터**: langKeys, companyCode, menuCode, userLang 확인
|
||||
- **응답 데이터**: 번역된 텍스트가 올바르게 반환되는지 확인
|
||||
|
||||
#### 데이터베이스 직접 확인
|
||||
|
||||
```sql
|
||||
-- 특정 키의 번역 데이터 확인
|
||||
SELECT km.lang_key, t.lang_code, t.lang_text, km.description
|
||||
FROM multi_lang_key_master km
|
||||
LEFT JOIN multi_lang_text t ON km.key_id = t.key_id
|
||||
WHERE km.lang_key = 'button.add'
|
||||
AND km.company_code = '*'
|
||||
AND t.lang_code = 'KR';
|
||||
|
||||
-- 특정 회사의 모든 번역 데이터 확인
|
||||
SELECT km.lang_key, t.lang_code, t.lang_text, km.description
|
||||
FROM multi_lang_key_master km
|
||||
LEFT JOIN multi_lang_text t ON km.key_id = t.key_id
|
||||
WHERE km.company_code = '*'
|
||||
AND t.lang_code = 'KR';
|
||||
```
|
||||
|
||||
## 📚 모범 사례
|
||||
|
||||
### 1. 키 명명 규칙
|
||||
|
||||
#### 계층 구조 사용
|
||||
|
||||
```typescript
|
||||
// 좋은 예시
|
||||
export const KEYS = {
|
||||
// 페이지 레벨
|
||||
PAGE_TITLE: "page.title",
|
||||
PAGE_DESCRIPTION: "page.description",
|
||||
|
||||
// 폼 레벨
|
||||
FORM_TITLE: "form.title",
|
||||
FORM_NAME: "form.name",
|
||||
FORM_EMAIL: "form.email",
|
||||
|
||||
// 버튼 레벨
|
||||
BUTTON_SUBMIT: "button.submit",
|
||||
BUTTON_CANCEL: "button.cancel",
|
||||
|
||||
// 메시지 레벨
|
||||
MESSAGE_SUCCESS: "message.success",
|
||||
MESSAGE_ERROR: "message.error",
|
||||
};
|
||||
```
|
||||
|
||||
#### 일관된 접두사 사용
|
||||
|
||||
```typescript
|
||||
// 일관된 접두사로 그룹화
|
||||
export const KEYS = {
|
||||
// 공통 UI 요소
|
||||
COMMON_LOADING: "common.loading",
|
||||
COMMON_ERROR: "common.error",
|
||||
COMMON_SUCCESS: "common.success",
|
||||
|
||||
// 특정 기능
|
||||
FEATURE_TITLE: "feature.title",
|
||||
FEATURE_DESCRIPTION: "feature.description",
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 성능 최적화
|
||||
|
||||
#### 배치 처리 사용
|
||||
|
||||
```typescript
|
||||
// ❌ 나쁜 예시: 개별 요청
|
||||
const text1 = await getText("key1");
|
||||
const text2 = await getText("key2");
|
||||
const text3 = await getText("key3");
|
||||
|
||||
// ✅ 좋은 예시: 배치 요청
|
||||
const keys = ["key1", "key2", "key3"];
|
||||
const translations = await getBatchTexts(keys);
|
||||
```
|
||||
|
||||
#### 캐싱 활용
|
||||
|
||||
```typescript
|
||||
// 번역 결과를 상태에 저장하여 재사용
|
||||
const [uiTexts, setUiTexts] = useState<Record<string, string>>({});
|
||||
|
||||
// 한 번 로드한 후 재사용
|
||||
useEffect(() => {
|
||||
if (Object.keys(uiTexts).length === 0) {
|
||||
loadTexts();
|
||||
}
|
||||
}, []);
|
||||
```
|
||||
|
||||
### 3. 에러 처리
|
||||
|
||||
#### 폴백 시스템 구현
|
||||
|
||||
```typescript
|
||||
const getText = (key: string, fallback?: string) => {
|
||||
// 1. 번역된 텍스트 우선
|
||||
if (uiTexts[key]) return uiTexts[key];
|
||||
|
||||
// 2. 폴백 텍스트 사용
|
||||
if (fallback) return fallback;
|
||||
|
||||
// 3. 키 자체 반환 (디버깅용)
|
||||
console.warn(`번역 키를 찾을 수 없음: ${key}`);
|
||||
return key;
|
||||
};
|
||||
```
|
||||
|
||||
#### 로딩 상태 관리
|
||||
|
||||
```typescript
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadTexts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await apiClient.post("/multilang/batch", { ... });
|
||||
|
||||
if (response.data.success) {
|
||||
setUiTexts(response.data.data);
|
||||
} else {
|
||||
setError(response.data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('다국어 텍스트 로드 실패');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 📝 요약
|
||||
|
||||
### 핵심 포인트
|
||||
|
||||
1. **useMultiLang 훅**을 사용하여 사용자 언어 설정 관리
|
||||
2. **배치 API**를 사용하여 여러 키를 한 번에 조회
|
||||
3. **키 상수**를 정의하여 일관성 유지
|
||||
4. **폴백 시스템**을 구현하여 번역 실패 시에도 UI 표시
|
||||
5. **캐싱**을 활용하여 성능 최적화
|
||||
|
||||
### 개발 워크플로우
|
||||
|
||||
1. 다국어 키 상수 정의
|
||||
2. 컴포넌트에서 다국어 텍스트 로드
|
||||
3. UI에 번역된 텍스트 적용
|
||||
4. 데이터베이스에 번역 데이터 등록
|
||||
5. 테스트 및 디버깅
|
||||
|
||||
### 주의사항
|
||||
|
||||
- 키 명명 규칙을 일관되게 유지
|
||||
- 배치 처리를 사용하여 API 호출 최소화
|
||||
- 폴백 시스템을 구현하여 사용자 경험 보장
|
||||
- 성능을 고려한 캐싱 전략 수립
|
||||
|
||||
이 가이드를 따라하면 새로운 개발자도 쉽게 다국어 시스템을 이해하고 적용할 수 있을 것입니다.
|
||||
Loading…
Reference in New Issue