560 lines
15 KiB
Plaintext
560 lines
15 KiB
Plaintext
# 다국어 지원 컴포넌트 개발 가이드
|
|
|
|
새로운 화면 컴포넌트를 개발할 때 반드시 다국어 시스템을 고려해야 합니다.
|
|
이 가이드는 컴포넌트가 다국어 자동 생성 및 매핑 시스템과 호환되도록 하는 방법을 설명합니다.
|
|
|
|
---
|
|
|
|
## 1. 타입 정의 시 다국어 필드 추가
|
|
|
|
### 기본 원칙
|
|
|
|
텍스트가 표시되는 **모든 속성**에 `langKeyId`와 `langKey` 필드를 함께 정의해야 합니다.
|
|
|
|
### 단일 텍스트 속성
|
|
|
|
```typescript
|
|
interface MyComponentConfig {
|
|
// 기본 텍스트
|
|
title?: string;
|
|
// 다국어 키 (필수 추가)
|
|
titleLangKeyId?: number;
|
|
titleLangKey?: string;
|
|
|
|
// 라벨
|
|
label?: string;
|
|
labelLangKeyId?: number;
|
|
labelLangKey?: string;
|
|
|
|
// 플레이스홀더
|
|
placeholder?: string;
|
|
placeholderLangKeyId?: number;
|
|
placeholderLangKey?: string;
|
|
}
|
|
```
|
|
|
|
### 배열/목록 속성 (컬럼, 탭 등)
|
|
|
|
```typescript
|
|
interface ColumnConfig {
|
|
name: string;
|
|
label: string;
|
|
// 다국어 키 (필수 추가)
|
|
langKeyId?: number;
|
|
langKey?: string;
|
|
// 기타 속성
|
|
width?: number;
|
|
align?: "left" | "center" | "right";
|
|
}
|
|
|
|
interface TabConfig {
|
|
id: string;
|
|
label: string;
|
|
// 다국어 키 (필수 추가)
|
|
langKeyId?: number;
|
|
langKey?: string;
|
|
// 탭 제목도 별도로
|
|
title?: string;
|
|
titleLangKeyId?: number;
|
|
titleLangKey?: string;
|
|
}
|
|
|
|
interface MyComponentConfig {
|
|
columns?: ColumnConfig[];
|
|
tabs?: TabConfig[];
|
|
}
|
|
```
|
|
|
|
### 버튼 컴포넌트
|
|
|
|
```typescript
|
|
interface ButtonComponentConfig {
|
|
text?: string;
|
|
// 다국어 키 (필수 추가)
|
|
langKeyId?: number;
|
|
langKey?: string;
|
|
}
|
|
```
|
|
|
|
### 실제 예시: 분할 패널
|
|
|
|
```typescript
|
|
interface SplitPanelLayoutConfig {
|
|
leftPanel?: {
|
|
title?: string;
|
|
langKeyId?: number; // 좌측 패널 제목 다국어
|
|
langKey?: string;
|
|
columns?: Array<{
|
|
name: string;
|
|
label: string;
|
|
langKeyId?: number; // 각 컬럼 다국어
|
|
langKey?: string;
|
|
}>;
|
|
};
|
|
rightPanel?: {
|
|
title?: string;
|
|
langKeyId?: number; // 우측 패널 제목 다국어
|
|
langKey?: string;
|
|
columns?: Array<{
|
|
name: string;
|
|
label: string;
|
|
langKeyId?: number;
|
|
langKey?: string;
|
|
}>;
|
|
additionalTabs?: Array<{
|
|
label: string;
|
|
langKeyId?: number; // 탭 라벨 다국어
|
|
langKey?: string;
|
|
title?: string;
|
|
titleLangKeyId?: number; // 탭 제목 다국어
|
|
titleLangKey?: string;
|
|
columns?: Array<{
|
|
name: string;
|
|
label: string;
|
|
langKeyId?: number;
|
|
langKey?: string;
|
|
}>;
|
|
}>;
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 라벨 추출 로직 등록
|
|
|
|
### 파일 위치
|
|
|
|
`frontend/lib/utils/multilangLabelExtractor.ts`
|
|
|
|
### `extractMultilangLabels` 함수에 추가
|
|
|
|
새 컴포넌트의 라벨을 추출하는 로직을 추가해야 합니다.
|
|
|
|
```typescript
|
|
// 새 컴포넌트 타입 체크
|
|
if (comp.componentType === "my-new-component") {
|
|
const config = comp.componentConfig as MyComponentConfig;
|
|
|
|
// 1. 제목 추출
|
|
if (config?.title) {
|
|
addLabel({
|
|
id: `${comp.id}_title`,
|
|
componentId: `${comp.id}_title`,
|
|
label: config.title,
|
|
type: "title",
|
|
parentType: "my-new-component",
|
|
parentLabel: config.title,
|
|
langKeyId: config.titleLangKeyId,
|
|
langKey: config.titleLangKey,
|
|
});
|
|
}
|
|
|
|
// 2. 컬럼 추출
|
|
if (config?.columns && Array.isArray(config.columns)) {
|
|
config.columns.forEach((col, index) => {
|
|
const colLabel = col.label || col.name;
|
|
addLabel({
|
|
id: `${comp.id}_col_${index}`,
|
|
componentId: `${comp.id}_col_${index}`,
|
|
label: colLabel,
|
|
type: "column",
|
|
parentType: "my-new-component",
|
|
parentLabel: config.title || "새 컴포넌트",
|
|
langKeyId: col.langKeyId,
|
|
langKey: col.langKey,
|
|
});
|
|
});
|
|
}
|
|
|
|
// 3. 버튼 텍스트 추출 (버튼 컴포넌트인 경우)
|
|
if (config?.text) {
|
|
addLabel({
|
|
id: `${comp.id}_button`,
|
|
componentId: `${comp.id}_button`,
|
|
label: config.text,
|
|
type: "button",
|
|
parentType: "my-new-component",
|
|
parentLabel: config.text,
|
|
langKeyId: config.langKeyId,
|
|
langKey: config.langKey,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### 추출해야 할 라벨 타입
|
|
|
|
| 타입 | 설명 | 예시 |
|
|
| ------------- | ------------------ | ------------------------ |
|
|
| `title` | 컴포넌트/패널 제목 | 분할패널 제목, 카드 제목 |
|
|
| `label` | 입력 필드 라벨 | 텍스트 입력 라벨 |
|
|
| `button` | 버튼 텍스트 | 저장, 취소, 삭제 |
|
|
| `column` | 테이블 컬럼 헤더 | 품목명, 수량, 금액 |
|
|
| `tab` | 탭 라벨 | 기본정보, 상세정보 |
|
|
| `filter` | 검색 필터 라벨 | 검색어, 기간 |
|
|
| `placeholder` | 플레이스홀더 | "검색어를 입력하세요" |
|
|
| `action` | 액션 버튼/링크 | 수정, 삭제, 상세보기 |
|
|
|
|
---
|
|
|
|
## 3. 매핑 적용 로직 등록
|
|
|
|
### 파일 위치
|
|
|
|
`frontend/lib/utils/multilangLabelExtractor.ts`
|
|
|
|
### `applyMultilangMappings` 함수에 추가
|
|
|
|
다국어 키가 선택되면 컴포넌트에 `langKeyId`와 `langKey`를 저장하는 로직을 추가합니다.
|
|
|
|
```typescript
|
|
// 새 컴포넌트 매핑 적용
|
|
if (comp.componentType === "my-new-component") {
|
|
const config = comp.componentConfig as MyComponentConfig;
|
|
|
|
// 1. 제목 매핑
|
|
const titleMapping = mappingMap.get(`${comp.id}_title`);
|
|
if (titleMapping) {
|
|
updated.componentConfig = {
|
|
...updated.componentConfig,
|
|
titleLangKeyId: titleMapping.keyId,
|
|
titleLangKey: titleMapping.langKey,
|
|
};
|
|
}
|
|
|
|
// 2. 컬럼 매핑
|
|
if (config?.columns && Array.isArray(config.columns)) {
|
|
const updatedColumns = config.columns.map((col, index) => {
|
|
const colMapping = mappingMap.get(`${comp.id}_col_${index}`);
|
|
if (colMapping) {
|
|
return {
|
|
...col,
|
|
langKeyId: colMapping.keyId,
|
|
langKey: colMapping.langKey,
|
|
};
|
|
}
|
|
return col;
|
|
});
|
|
updated.componentConfig = {
|
|
...updated.componentConfig,
|
|
columns: updatedColumns,
|
|
};
|
|
}
|
|
|
|
// 3. 버튼 매핑 (버튼 컴포넌트인 경우)
|
|
const buttonMapping = mappingMap.get(`${comp.id}_button`);
|
|
if (buttonMapping) {
|
|
updated.componentConfig = {
|
|
...updated.componentConfig,
|
|
langKeyId: buttonMapping.keyId,
|
|
langKey: buttonMapping.langKey,
|
|
};
|
|
}
|
|
}
|
|
```
|
|
|
|
### 주의사항
|
|
|
|
- **객체 참조 유지**: 매핑 시 기존 `updated.componentConfig`를 기반으로 업데이트해야 합니다.
|
|
- **중첩 구조**: 중첩된 객체(예: `leftPanel.columns`)는 상위 객체부터 순서대로 업데이트합니다.
|
|
|
|
```typescript
|
|
// 잘못된 방법 - 이전 업데이트 덮어쓰기
|
|
updated.componentConfig = { ...config, langKeyId: mapping.keyId }; // ❌
|
|
updated.componentConfig = { ...config, columns: updatedColumns }; // langKeyId 사라짐!
|
|
|
|
// 올바른 방법 - 이전 업데이트 유지
|
|
updated.componentConfig = {
|
|
...updated.componentConfig,
|
|
langKeyId: mapping.keyId,
|
|
}; // ✅
|
|
updated.componentConfig = {
|
|
...updated.componentConfig,
|
|
columns: updatedColumns,
|
|
}; // ✅
|
|
```
|
|
|
|
---
|
|
|
|
## 4. 번역 표시 로직 구현
|
|
|
|
### 파일 위치
|
|
|
|
새 컴포넌트 파일 (예: `frontend/lib/registry/components/my-component/MyComponent.tsx`)
|
|
|
|
### Context 사용
|
|
|
|
```typescript
|
|
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
|
|
|
const MyComponent = ({ component }: Props) => {
|
|
const { getTranslatedText } = useScreenMultiLang();
|
|
const config = component.componentConfig;
|
|
|
|
// 제목 번역
|
|
const displayTitle = config?.titleLangKey
|
|
? getTranslatedText(config.titleLangKey, config.title || "")
|
|
: config?.title || "";
|
|
|
|
// 컬럼 헤더 번역
|
|
const translatedColumns = config?.columns?.map((col) => ({
|
|
...col,
|
|
displayLabel: col.langKey
|
|
? getTranslatedText(col.langKey, col.label)
|
|
: col.label,
|
|
}));
|
|
|
|
// 버튼 텍스트 번역
|
|
const buttonText = config?.langKey
|
|
? getTranslatedText(config.langKey, config.text || "")
|
|
: config?.text || "";
|
|
|
|
return (
|
|
<div>
|
|
<h2>{displayTitle}</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
{translatedColumns?.map((col, idx) => (
|
|
<th key={idx}>{col.displayLabel}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
</table>
|
|
<button>{buttonText}</button>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### getTranslatedText 함수
|
|
|
|
```typescript
|
|
// 첫 번째 인자: langKey (다국어 키)
|
|
// 두 번째 인자: fallback (키가 없거나 번역이 없을 때 기본값)
|
|
const text = getTranslatedText(
|
|
"screen.company_1.Sales.OrderList.품목명",
|
|
"품목명"
|
|
);
|
|
```
|
|
|
|
### 주의사항
|
|
|
|
- `langKey`가 없으면 원본 텍스트를 표시합니다.
|
|
- `useScreenMultiLang`은 반드시 `ScreenMultiLangProvider` 내부에서 사용해야 합니다.
|
|
- 화면 페이지(`/screens/[screenId]/page.tsx`)에서 이미 Provider로 감싸져 있습니다.
|
|
|
|
---
|
|
|
|
## 5. ScreenMultiLangContext에 키 수집 로직 추가
|
|
|
|
### 파일 위치
|
|
|
|
`frontend/contexts/ScreenMultiLangContext.tsx`
|
|
|
|
### `collectLangKeys` 함수에 추가
|
|
|
|
번역을 미리 로드하기 위해 컴포넌트에서 사용하는 모든 `langKey`를 수집해야 합니다.
|
|
|
|
```typescript
|
|
const collectLangKeys = (comps: ComponentData[]): Set<string> => {
|
|
const keys = new Set<string>();
|
|
|
|
const processComponent = (comp: ComponentData) => {
|
|
const config = comp.componentConfig;
|
|
|
|
// 새 컴포넌트의 langKey 수집
|
|
if (comp.componentType === "my-new-component") {
|
|
// 제목
|
|
if (config?.titleLangKey) {
|
|
keys.add(config.titleLangKey);
|
|
}
|
|
|
|
// 컬럼
|
|
if (config?.columns && Array.isArray(config.columns)) {
|
|
config.columns.forEach((col: any) => {
|
|
if (col.langKey) {
|
|
keys.add(col.langKey);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 버튼
|
|
if (config?.langKey) {
|
|
keys.add(config.langKey);
|
|
}
|
|
}
|
|
|
|
// 자식 컴포넌트 재귀 처리
|
|
if (comp.children && Array.isArray(comp.children)) {
|
|
comp.children.forEach(processComponent);
|
|
}
|
|
};
|
|
|
|
comps.forEach(processComponent);
|
|
return keys;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 6. MultilangSettingsModal에 표시 로직 추가
|
|
|
|
### 파일 위치
|
|
|
|
`frontend/components/screen/modals/MultilangSettingsModal.tsx`
|
|
|
|
### `extractLabelsFromComponents` 함수에 추가
|
|
|
|
다국어 설정 모달에서 새 컴포넌트의 라벨이 표시되도록 합니다.
|
|
|
|
```typescript
|
|
// 새 컴포넌트 라벨 추출
|
|
if (comp.componentType === "my-new-component") {
|
|
const config = comp.componentConfig as MyComponentConfig;
|
|
|
|
// 제목
|
|
if (config?.title) {
|
|
addLabel({
|
|
id: `${comp.id}_title`,
|
|
componentId: `${comp.id}_title`,
|
|
label: config.title,
|
|
type: "title",
|
|
parentType: "my-new-component",
|
|
parentLabel: config.title,
|
|
langKeyId: config.titleLangKeyId,
|
|
langKey: config.titleLangKey,
|
|
});
|
|
}
|
|
|
|
// 컬럼
|
|
if (config?.columns) {
|
|
config.columns.forEach((col, index) => {
|
|
// columnLabelMap에서 라벨 가져오기 (테이블 컬럼인 경우)
|
|
const tableName = config.tableName;
|
|
const displayLabel =
|
|
tableName && columnLabelMap[tableName]?.[col.name]
|
|
? columnLabelMap[tableName][col.name]
|
|
: col.label || col.name;
|
|
|
|
addLabel({
|
|
id: `${comp.id}_col_${index}`,
|
|
componentId: `${comp.id}_col_${index}`,
|
|
label: displayLabel,
|
|
type: "column",
|
|
parentType: "my-new-component",
|
|
parentLabel: config.title || "새 컴포넌트",
|
|
langKeyId: col.langKeyId,
|
|
langKey: col.langKey,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 테이블명 추출 (테이블 사용 컴포넌트인 경우)
|
|
|
|
### 파일 위치
|
|
|
|
`frontend/lib/utils/multilangLabelExtractor.ts`
|
|
|
|
### `extractTableNames` 함수에 추가
|
|
|
|
컴포넌트가 테이블을 사용하는 경우, 테이블명을 추출해야 컬럼 라벨을 가져올 수 있습니다.
|
|
|
|
```typescript
|
|
const extractTableNames = (comps: ComponentData[]): Set<string> => {
|
|
const tableNames = new Set<string>();
|
|
|
|
const processComponent = (comp: ComponentData) => {
|
|
const config = comp.componentConfig;
|
|
|
|
// 새 컴포넌트의 테이블명 추출
|
|
if (comp.componentType === "my-new-component") {
|
|
if (config?.tableName) {
|
|
tableNames.add(config.tableName);
|
|
}
|
|
if (config?.selectedTable) {
|
|
tableNames.add(config.selectedTable);
|
|
}
|
|
}
|
|
|
|
// 자식 컴포넌트 재귀 처리
|
|
if (comp.children && Array.isArray(comp.children)) {
|
|
comp.children.forEach(processComponent);
|
|
}
|
|
};
|
|
|
|
comps.forEach(processComponent);
|
|
return tableNames;
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 8. 체크리스트
|
|
|
|
새 컴포넌트 개발 시 다음 항목을 확인하세요:
|
|
|
|
### 타입 정의
|
|
|
|
- [ ] 모든 텍스트 속성에 `langKeyId`, `langKey` 필드 추가
|
|
- [ ] 배열 속성(columns, tabs 등)의 각 항목에도 다국어 필드 추가
|
|
|
|
### 라벨 추출 (multilangLabelExtractor.ts)
|
|
|
|
- [ ] `extractMultilangLabels` 함수에 라벨 추출 로직 추가
|
|
- [ ] `extractTableNames` 함수에 테이블명 추출 로직 추가 (해당되는 경우)
|
|
|
|
### 매핑 적용 (multilangLabelExtractor.ts)
|
|
|
|
- [ ] `applyMultilangMappings` 함수에 매핑 적용 로직 추가
|
|
|
|
### 번역 표시 (컴포넌트 파일)
|
|
|
|
- [ ] `useScreenMultiLang` 훅 사용
|
|
- [ ] `getTranslatedText`로 텍스트 번역 적용
|
|
|
|
### 키 수집 (ScreenMultiLangContext.tsx)
|
|
|
|
- [ ] `collectLangKeys` 함수에 langKey 수집 로직 추가
|
|
|
|
### 설정 모달 (MultilangSettingsModal.tsx)
|
|
|
|
- [ ] `extractLabelsFromComponents`에 라벨 표시 로직 추가
|
|
|
|
---
|
|
|
|
## 9. 관련 파일 목록
|
|
|
|
| 파일 | 역할 |
|
|
| -------------------------------------------------------------- | ----------------------- |
|
|
| `frontend/lib/utils/multilangLabelExtractor.ts` | 라벨 추출 및 매핑 적용 |
|
|
| `frontend/contexts/ScreenMultiLangContext.tsx` | 번역 Context 및 키 수집 |
|
|
| `frontend/components/screen/modals/MultilangSettingsModal.tsx` | 다국어 설정 UI |
|
|
| `frontend/components/screen/ScreenDesigner.tsx` | 다국어 생성 버튼 처리 |
|
|
| `backend-node/src/services/multilangService.ts` | 다국어 키 생성 서비스 |
|
|
|
|
---
|
|
|
|
## 10. 주의사항
|
|
|
|
1. **componentId 형식 일관성**: 라벨 추출과 매핑 적용에서 동일한 ID 형식 사용
|
|
|
|
- 제목: `${comp.id}_title`
|
|
- 컬럼: `${comp.id}_col_${index}`
|
|
- 버튼: `${comp.id}_button`
|
|
|
|
2. **중첩 구조 주의**: 분할패널처럼 중첩된 구조는 경로를 명확히 지정
|
|
|
|
- `${comp.id}_left_title`, `${comp.id}_right_col_${index}`
|
|
|
|
3. **기존 값 보존**: 매핑 적용 시 `updated.componentConfig`를 기반으로 업데이트
|
|
|
|
4. **라벨 타입 구분**: 입력 폼의 `label`과 다른 컴포넌트의 `label`을 구분하여 처리
|
|
|
|
5. **테스트**: 다국어 생성 → 다국어 설정 → 언어 변경 순서로 테스트
|