diff --git a/docs/다국어_시스템_가이드.md b/docs/다국어_시스템_가이드.md new file mode 100644 index 00000000..e83096a9 --- /dev/null +++ b/docs/다국어_시스템_가이드.md @@ -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(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; + +// 동기 조회 함수 (캐시에서만) +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>> +``` + +**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>({}); + +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 ( +
+

{getText("menu.title")}

+ +
+); +``` + +### 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>({}); + + // 다국어 텍스트 로드 + 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 ( +
+

{getText(PAGE_KEYS.TITLE)}

+

{getText(PAGE_KEYS.DESCRIPTION)}

+ +
+ + + + +
+
+ ); +}; +``` + +#### 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 ( +
+

메뉴 관리

+ + +
+); + +// 변경 후 +return ( +
+

{getText("menu.management.title")}

+ + +
+); +``` + +#### 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 ( +
+

{getText(NEW_FEATURE_KEYS.FEATURE_TITLE)}

+

{getText(NEW_FEATURE_KEYS.FEATURE_DESCRIPTION)}

+ +
+ ); +}; +``` + +### 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 +// 문제 상황 +

menu.management.title

; + +// 원인: 번역 데이터가 로드되지 않음 +// 해결: 다국어 텍스트 로드 확인 +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>({}); + +// 한 번 로드한 후 재사용 +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(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 호출 최소화 +- 폴백 시스템을 구현하여 사용자 경험 보장 +- 성능을 고려한 캐싱 전략 수립 + +이 가이드를 따라하면 새로운 개발자도 쉽게 다국어 시스템을 이해하고 적용할 수 있을 것입니다.