엔티티조인 읽기전용 컬럼 추가

This commit is contained in:
kjs
2026-01-15 10:39:23 +09:00
parent 71af4dfc6b
commit 19dbe59e3a
8 changed files with 1011 additions and 591 deletions

View File

@@ -0,0 +1,699 @@
---
description: 화면 컴포넌트 개발 시 필수 가이드 - 엔티티 조인, 폼 데이터, 다국어 지원
alwaysApply: false
---
# 화면 컴포넌트 개발 가이드
새로운 화면 컴포넌트를 개발할 때 반드시 따라야 하는 핵심 원칙과 패턴을 설명합니다.
이 가이드는 컴포넌트가 시스템의 핵심 기능(엔티티 조인, 다국어, 폼 데이터 관리 등)과
올바르게 통합되도록 하는 방법을 설명합니다.
---
## 목차
1. [엔티티 조인 컬럼 활용 (필수)](#1-엔티티-조인-컬럼-활용-필수)
2. [폼 데이터 관리](#2-폼-데이터-관리)
3. [다국어 지원](#3-다국어-지원)
4. [컬럼 설정 패널 구현](#4-컬럼-설정-패널-구현)
5. [체크리스트](#5-체크리스트)
---
## 1. 엔티티 조인 컬럼 활용 (필수)
### 핵심 원칙
**화면을 새로 만들어서 화면 안에 넣는 방식을 사용하지 않습니다.**
대신, 현재 화면의 메인 테이블을 기준으로 테이블 타입관리의 엔티티 관계를 불러와서
조인되어 있는 컬럼들을 모두 사용 가능하게 해야 합니다.
### API 사용법
```typescript
import { entityJoinApi } from "@/lib/api/entityJoin";
// 테이블의 엔티티 조인 컬럼 정보 가져오기
const result = await entityJoinApi.getEntityJoinColumns(tableName);
// 응답 구조
{
tableName: string;
joinTables: Array<{
tableName: string; // 조인 테이블명 (예: item_info)
currentDisplayColumn: string; // 현재 표시 컬럼
availableColumns: Array<{
// 사용 가능한 컬럼들
columnName: string;
columnLabel: string;
dataType: string;
description?: string;
}>;
}>;
availableColumns: Array<{
// 플랫한 구조의 전체 사용 가능 컬럼
tableName: string;
columnName: string;
columnLabel: string;
dataType: string;
joinAlias: string; // 예: item_code_item_name
suggestedLabel: string; // 예: 품목명
}>;
summary: {
totalJoinTables: number;
totalAvailableColumns: number;
}
}
```
### 컬럼 선택 UI 구현
ConfigPanel에서 엔티티 조인 컬럼을 표시하는 표준 패턴입니다.
```typescript
// 상태 정의
const [entityJoinColumns, setEntityJoinColumns] = useState<{
availableColumns: Array<{
tableName: string;
columnName: string;
columnLabel: string;
dataType: string;
joinAlias: string;
suggestedLabel: string;
}>;
joinTables: Array<{
tableName: string;
currentDisplayColumn: string;
availableColumns: Array<{
columnName: string;
columnLabel: string;
dataType: string;
description?: string;
}>;
}>;
}>({ availableColumns: [], joinTables: [] });
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
// 엔티티 조인 컬럼 로드
useEffect(() => {
const fetchEntityJoinColumns = async () => {
const tableName = config.selectedTable || screenTableName;
if (!tableName) {
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
return;
}
setLoadingEntityJoins(true);
try {
const result = await entityJoinApi.getEntityJoinColumns(tableName);
setEntityJoinColumns({
availableColumns: result.availableColumns || [],
joinTables: result.joinTables || [],
});
} catch (error) {
console.error("엔티티 조인 컬럼 조회 오류:", error);
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
} finally {
setLoadingEntityJoins(false);
}
};
fetchEntityJoinColumns();
}, [config.selectedTable, screenTableName]);
```
### 컬럼 선택 UI 렌더링
```tsx
{
/* 엔티티 조인 컬럼 섹션 */
}
{
entityJoinColumns.joinTables.length > 0 && (
<div className="space-y-2 mt-4">
<Label className="flex items-center gap-2 text-xs text-muted-foreground">
<Link2 className="h-3 w-3" />
엔티티 조인 컬럼
</Label>
{entityJoinColumns.joinTables.map((joinTable) => (
<div key={joinTable.tableName} className="border rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
<Badge variant="secondary" className="text-xs">
{joinTable.tableName}
</Badge>
<span className="text-xs text-muted-foreground">
({joinTable.availableColumns.length})
</span>
</div>
<div className="grid grid-cols-2 gap-2">
{joinTable.availableColumns.map((col) => {
// "테이블명.컬럼명" 형식으로 컬럼 이름 생성
const fullColumnName = `${joinTable.tableName}.${col.columnName}`;
const isSelected = config.columns?.some(
(c) => c.columnName === fullColumnName
);
return (
<div
key={col.columnName}
className={cn(
"flex items-center gap-2 p-2 border rounded cursor-pointer",
isSelected
? "bg-primary/10 border-primary"
: "hover:bg-muted"
)}
onClick={() => {
if (isSelected) {
removeColumn(fullColumnName);
} else {
addEntityJoinColumn(joinTable.tableName, col);
}
}}
>
<Checkbox checked={isSelected} />
<div className="flex-1 min-w-0">
<div className="text-sm truncate">{col.columnLabel}</div>
<div className="text-xs text-muted-foreground truncate">
{col.columnName}
</div>
</div>
</div>
);
})}
</div>
</div>
))}
</div>
);
}
```
### 엔티티 조인 컬럼 추가 함수
```typescript
const addEntityJoinColumn = (tableName: string, column: any) => {
const fullColumnName = `${tableName}.${column.columnName}`;
const newColumn: ColumnConfig = {
columnName: fullColumnName,
displayName: column.columnLabel || column.columnName,
visible: true,
sortable: true,
searchable: true,
align: "left",
format: "text",
order: config.columns?.length || 0,
isEntityJoin: true, // 엔티티 조인 컬럼 표시
entityJoinTable: tableName,
entityJoinColumn: column.columnName,
};
onChange({
...config,
columns: [...(config.columns || []), newColumn],
});
};
```
### 데이터 조회 시 엔티티 조인 활용
```typescript
// 엔티티 조인이 포함된 데이터 조회
const response = await entityJoinApi.getTableDataWithJoins(tableName, {
page: 1,
size: 10,
enableEntityJoin: true,
// 추가 조인 컬럼 지정 (화면 설정에서 선택한 컬럼들)
additionalJoinColumns: config.columns
?.filter((col) => col.isEntityJoin)
?.map((col) => ({
sourceTable: col.entityJoinTable!,
sourceColumn: col.entityJoinColumn!,
joinAlias: col.columnName,
})),
});
```
### 셀 값 추출 헬퍼
엔티티 조인 컬럼의 값을 데이터에서 추출하는 헬퍼 함수입니다.
```typescript
const getEntityJoinValue = (item: any, columnName: string): any => {
// 직접 매칭 시도
if (item[columnName] !== undefined) {
return item[columnName];
}
// "테이블명.컬럼명" 형식인 경우
if (columnName.includes(".")) {
const [tableName, fieldName] = columnName.split(".");
// 1. 소스 컬럼 추론 (item_info → item_code)
const inferredSourceColumn = tableName
.replace("_info", "_code")
.replace("_mng", "_id");
// 2. 정확한 키 매핑: 소스컬럼_필드명
const exactKey = `${inferredSourceColumn}_${fieldName}`;
if (item[exactKey] !== undefined) {
return item[exactKey];
}
// 3. item_id 패턴 시도
const idPatternKey = `${tableName.replace("_info", "_id")}_${fieldName}`;
if (item[idPatternKey] !== undefined) {
return item[idPatternKey];
}
// 4. 단순 필드명으로 시도
if (item[fieldName] !== undefined) {
return item[fieldName];
}
}
return undefined;
};
```
---
## 2. 폼 데이터 관리
### 통합 폼 시스템 (UnifiedFormContext)
새 컴포넌트는 통합 폼 시스템을 사용해야 합니다.
```typescript
import { useFormCompatibility } from "@/hooks/useFormCompatibility";
const MyComponent = ({ onFormDataChange, formData, ...props }) => {
// 호환성 브릿지 사용
const { getValue, setValue, submit } = useFormCompatibility({
legacyOnFormDataChange: onFormDataChange,
});
// 값 읽기
const currentValue = getValue("fieldName");
// 값 설정 (모든 시스템에 전파됨)
const handleChange = (value: any) => {
setValue("fieldName", value);
};
// 저장
const handleSave = async () => {
const result = await submit({
tableName: "my_table",
mode: "insert",
});
};
};
```
### 레거시 컴포넌트와의 호환성
기존 `beforeFormSave` 이벤트를 사용하는 컴포넌트(리피터 등)와 호환됩니다.
```typescript
import { useBeforeFormSave } from "@/hooks/useFormCompatibility";
const MyRepeaterComponent = ({ value, columnName }) => {
// beforeFormSave 이벤트에서 데이터 수집
useEffect(() => {
const handleSaveRequest = (event: CustomEvent) => {
if (event.detail && columnName) {
event.detail.formData[columnName] = value;
}
};
window.addEventListener("beforeFormSave", handleSaveRequest);
return () =>
window.removeEventListener("beforeFormSave", handleSaveRequest);
}, [value, columnName]);
};
```
### onChange 핸들러 패턴
컴포넌트에서 값이 변경될 때 사용하는 표준 패턴입니다.
```typescript
// 기본 패턴 (권장)
const handleChange = useCallback(
(value: any) => {
// 1. UnifiedFormContext가 있으면 사용
if (unifiedContext) {
unifiedContext.setValue(fieldName, value);
}
// 2. ScreenContext가 있으면 사용
if (screenContext?.updateFormData) {
screenContext.updateFormData(fieldName, value);
}
// 3. 레거시 콜백이 있으면 호출
if (onFormDataChange) {
onFormDataChange(fieldName, value);
}
},
[fieldName, unifiedContext, screenContext, onFormDataChange]
);
```
---
## 3. 다국어 지원
### 타입 정의 시 다국어 필드 추가
텍스트가 표시되는 **모든 속성**에 `langKeyId`와 `langKey` 필드를 추가합니다.
```typescript
interface MyComponentConfig {
// 기본 텍스트
title?: string;
titleLangKeyId?: number;
titleLangKey?: string;
// 컬럼 배열
columns?: Array<{
name: string;
label: string;
langKeyId?: number;
langKey?: string;
}>;
}
```
### 라벨 추출 로직 등록
파일: `frontend/lib/utils/multilangLabelExtractor.ts`
```typescript
// extractMultilangLabels 함수에 추가
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 && Array.isArray(config.columns)) {
config.columns.forEach((col, index) => {
addLabel({
id: `${comp.id}_col_${index}`,
componentId: `${comp.id}_col_${index}`,
label: col.label || col.name,
type: "column",
parentType: "my-new-component",
parentLabel: config.title || "컴포넌트",
langKeyId: col.langKeyId,
langKey: col.langKey,
});
});
}
}
```
### 매핑 적용 로직 등록
```typescript
// applyMultilangMappings 함수에 추가
if (comp.componentType === "my-new-component") {
const config = comp.componentConfig as MyComponentConfig;
// 제목 매핑
const titleMapping = mappingMap.get(`${comp.id}_title`);
if (titleMapping) {
updated.componentConfig = {
...updated.componentConfig,
titleLangKeyId: titleMapping.keyId,
titleLangKey: titleMapping.langKey,
};
}
// 컬럼 매핑
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,
};
}
}
```
### 번역 표시 로직
```typescript
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
const MyComponent = ({ component }) => {
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,
}));
return (
<div>
<h2>{displayTitle}</h2>
<table>
<thead>
<tr>
{translatedColumns?.map((col, idx) => (
<th key={idx}>{col.displayLabel}</th>
))}
</tr>
</thead>
</table>
</div>
);
};
```
### ScreenMultiLangContext에 키 수집 로직 추가
파일: `frontend/contexts/ScreenMultiLangContext.tsx`
```typescript
// collectLangKeys 함수에 추가
if (comp.componentType === "my-new-component") {
const config = comp.componentConfig;
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);
}
});
}
}
```
---
## 4. 컬럼 설정 패널 구현
### 필수 구조
모든 테이블/목록 기반 컴포넌트의 설정 패널은 다음 구조를 따릅니다:
```typescript
interface ConfigPanelProps {
config: MyComponentConfig;
onChange: (config: Partial<MyComponentConfig>) => void;
screenTableName?: string; // 화면에 연결된 테이블명
tableColumns?: any[]; // 테이블 컬럼 정보
}
export const MyComponentConfigPanel: React.FC<ConfigPanelProps> = ({
config,
onChange,
screenTableName,
tableColumns,
}) => {
// 1. 기본 테이블 컬럼 상태
const [availableColumns, setAvailableColumns] = useState<Array<{
columnName: string;
dataType: string;
label?: string;
input_type?: string;
}>>([]);
// 2. 엔티티 조인 컬럼 상태 (필수!)
const [entityJoinColumns, setEntityJoinColumns] = useState<{
availableColumns: Array<{...}>;
joinTables: Array<{...}>;
}>({ availableColumns: [], joinTables: [] });
// 3. 로딩 상태
const [loadingEntityJoins, setLoadingEntityJoins] = useState(false);
// 4. 화면 테이블명이 있으면 자동 설정
useEffect(() => {
if (screenTableName && !config.selectedTable) {
onChange({
...config,
selectedTable: screenTableName,
columns: config.columns || [],
});
}
}, [screenTableName]);
// 5. 기본 컬럼 로드
useEffect(() => {
// tableColumns prop 또는 API에서 로드
}, [config.selectedTable, screenTableName, tableColumns]);
// 6. 엔티티 조인 컬럼 로드 (필수!)
useEffect(() => {
const fetchEntityJoinColumns = async () => {
const tableName = config.selectedTable || screenTableName;
if (!tableName) {
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
return;
}
setLoadingEntityJoins(true);
try {
const result = await entityJoinApi.getEntityJoinColumns(tableName);
setEntityJoinColumns({
availableColumns: result.availableColumns || [],
joinTables: result.joinTables || [],
});
} catch (error) {
setEntityJoinColumns({ availableColumns: [], joinTables: [] });
} finally {
setLoadingEntityJoins(false);
}
};
fetchEntityJoinColumns();
}, [config.selectedTable, screenTableName]);
// 7. UI 렌더링
return (
<div className="space-y-4">
{/* 기본 테이블 컬럼 */}
<div>
<Label>표시할 컬럼 선택</Label>
{/* 기본 컬럼 체크박스들 */}
</div>
{/* 엔티티 조인 컬럼 (필수!) */}
{entityJoinColumns.joinTables.length > 0 && (
<div>
<Label className="flex items-center gap-2">
<Link2 className="h-3 w-3" />
엔티티 조인 컬럼
</Label>
{/* 조인 테이블별 컬럼 선택 UI */}
</div>
)}
</div>
);
};
```
---
## 5. 체크리스트
새 컴포넌트 개발 시 다음 항목을 확인하세요:
### 엔티티 조인 (필수)
- [ ] `entityJoinApi.getEntityJoinColumns()` 호출하여 조인 컬럼 로드
- [ ] 설정 패널에 "엔티티 조인 컬럼" 섹션 추가
- [ ] 조인 컬럼 선택 시 `tableName.columnName` 형식으로 저장
- [ ] 데이터 조회 시 `getTableDataWithJoins()` 사용
- [ ] 셀 값 추출 시 `getEntityJoinValue()` 헬퍼 사용
### 폼 데이터 관리
- [ ] `useFormCompatibility` 훅 사용
- [ ] 값 변경 시 `setValue()` 호출
- [ ] 리피터 컴포넌트는 `beforeFormSave` 이벤트 처리
### 다국어 지원
- [ ] 타입 정의에 `langKeyId`, `langKey` 필드 추가
- [ ] `extractMultilangLabels` 함수에 라벨 추출 로직 추가
- [ ] `applyMultilangMappings` 함수에 매핑 적용 로직 추가
- [ ] `collectLangKeys` 함수에 키 수집 로직 추가
- [ ] 컴포넌트에서 `useScreenMultiLang` 훅으로 번역 표시
### 설정 패널
- [ ] `screenTableName` prop 처리
- [ ] `tableColumns` prop 처리
- [ ] 엔티티 조인 컬럼 로드 및 표시
- [ ] 컬럼 추가/제거/순서변경 기능
---
## 관련 파일 목록
| 파일 | 역할 |
| ---------------------------------------------------- | --------------------- |
| `frontend/lib/api/entityJoin.ts` | 엔티티 조인 API |
| `frontend/hooks/useFormCompatibility.ts` | 폼 호환성 브릿지 |
| `frontend/components/unified/UnifiedFormContext.tsx` | 통합 폼 Context |
| `frontend/lib/utils/multilangLabelExtractor.ts` | 다국어 라벨 추출/매핑 |
| `frontend/contexts/ScreenMultiLangContext.tsx` | 다국어 번역 Context |
---
## 참고: TableListConfigPanel 예시
`frontend/lib/registry/components/table-list/TableListConfigPanel.tsx` 파일에서
엔티티 조인 컬럼을 어떻게 표시하는지 참고하세요.
주요 패턴:
1. `entityJoinApi.getEntityJoinColumns(tableName)` 호출
2. `joinTables` 배열을 순회하며 각 조인 테이블의 컬럼 표시
3. `tableName.columnName` 형식으로 컬럼명 생성
4. `isEntityJoin: true` 플래그로 일반 컬럼과 구분

View File

@@ -1,559 +1,40 @@
# 다국어 지원 컴포넌트 개발 가이드
새로운 화면 컴포넌트를 개발할 때 반드시 다국어 시스템을 고려해야 합니다.
이 가이드는 컴포넌트가 다국어 자동 생성 및 매핑 시스템과 호환되도록 하는 방법을 설명합니다.
---
description: (Deprecated) 이 파일은 component-development-guide.mdc로 통합되었습니다.
alwaysApply: false
---
## 1. 타입 정의 시 다국어 필드 추가
# 다국어 지원 컴포넌트 개발 가이드 (Deprecated)
### 기본 원칙
> **이 문서는 더 이상 사용되지 않습니다.**
>
> 새로운 통합 가이드를 참조하세요: `component-development-guide.mdc`
텍스트가 표시되는 **모든 속성**에 `langKeyId`와 `langKey` 필드를 함께 정의해야 합니다.
다국어 지원을 포함한 모든 컴포넌트 개발 가이드가 다음 파일로 통합되었습니다:
### 단일 텍스트 속성
**[component-development-guide.mdc](.cursor/rules/component-development-guide.mdc)**
```typescript
interface MyComponentConfig {
// 기본 텍스트
title?: string;
// 다국어 키 (필수 추가)
titleLangKeyId?: number;
titleLangKey?: string;
통합된 가이드에는 다음 내용이 포함되어 있습니다:
// 라벨
label?: string;
labelLangKeyId?: number;
labelLangKey?: string;
1. **엔티티 조인 컬럼 활용 (필수)**
// 플레이스홀더
placeholder?: string;
placeholderLangKeyId?: number;
placeholderLangKey?: string;
}
```
- 화면을 새로 만들어 임베딩하는 방식 대신 엔티티 관계 활용
- `entityJoinApi.getEntityJoinColumns()` 사용법
- 설정 패널에서 조인 컬럼 표시 패턴
### 배열/목록 속성 (컬럼, 탭 등)
2. **폼 데이터 관리**
```typescript
interface ColumnConfig {
name: string;
label: string;
// 다국어 키 (필수 추가)
langKeyId?: number;
langKey?: string;
// 기타 속성
width?: number;
align?: "left" | "center" | "right";
}
- `useFormCompatibility` 훅 사용법
- 레거시 `beforeFormSave` 이벤트 호환성
interface TabConfig {
id: string;
label: string;
// 다국어 키 (필수 추가)
langKeyId?: number;
langKey?: string;
// 탭 제목도 별도로
title?: string;
titleLangKeyId?: number;
titleLangKey?: string;
}
3. **다국어 지원**
interface MyComponentConfig {
columns?: ColumnConfig[];
tabs?: TabConfig[];
}
```
- 타입 정의 시 `langKeyId`, `langKey` 필드 추가
- 라벨 추출/매핑 로직
- 번역 표시 로직
### 버튼 컴포넌트
4. **컬럼 설정 패널 구현**
```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. **테스트**: 다국어 생성 → 다국어 설정 → 언어 변경 순서로 테스트
5. **체크리스트**
- 새 컴포넌트 개발 시 확인 항목