다국어 버튼 자동매핑

This commit is contained in:
kjs 2026-01-14 13:08:44 +09:00
parent c26b346054
commit f9575d7b5f
2 changed files with 521 additions and 120 deletions

View File

@ -1460,115 +1460,44 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
setIsGeneratingMultilang(true);
try {
// 모든 컴포넌트에서 라벨 정보 추출
const labels: Array<{ componentId: string; label: string; type?: string }> = [];
const addedLabels = new Set<string>(); // 중복 방지
// 공통 유틸 사용하여 라벨 추출
const { extractMultilangLabels, extractTableNames, applyMultilangMappings } = await import(
"@/lib/utils/multilangLabelExtractor"
);
const { apiClient } = await import("@/lib/api/client");
const addLabel = (componentId: string, label: string, type: string) => {
const key = `${label}_${type}`;
if (label && label.trim() && !addedLabels.has(key)) {
addedLabels.add(key);
labels.push({ componentId, label: label.trim(), type });
// 테이블별 컬럼 라벨 로드
const tableNames = extractTableNames(layout.components);
const columnLabelMap: Record<string, Record<string, string>> = {};
for (const tableName of tableNames) {
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
if (response.data?.success && response.data?.data) {
const columns = response.data.data.columns || response.data.data;
if (Array.isArray(columns)) {
columnLabelMap[tableName] = {};
columns.forEach((col: any) => {
const colName = col.columnName || col.column_name || col.name;
const colLabel = col.displayName || col.columnLabel || col.column_label || colName;
if (colName) {
columnLabelMap[tableName][colName] = colLabel;
}
});
}
}
} catch (error) {
console.error(`컬럼 라벨 조회 실패 (${tableName}):`, error);
}
};
}
const extractLabels = (components: ComponentData[]) => {
components.forEach((comp) => {
const anyComp = comp as any;
const config = anyComp.componentConfig;
// 1. 기본 라벨 추출
if (anyComp.label && typeof anyComp.label === "string") {
addLabel(comp.id, anyComp.label, "label");
}
// 2. 제목 추출 (컨테이너, 카드 등)
if (anyComp.title && typeof anyComp.title === "string") {
addLabel(comp.id, anyComp.title, "title");
}
// 3. 버튼 텍스트 추출
if (config?.text && typeof config.text === "string") {
addLabel(comp.id, config.text, "button");
}
// 4. placeholder 추출
if (anyComp.placeholder && typeof anyComp.placeholder === "string") {
addLabel(comp.id, anyComp.placeholder, "placeholder");
}
// 5. 테이블 컬럼 헤더 추출 (table-list, split-panel-layout 등)
if (config?.columns && Array.isArray(config.columns)) {
config.columns.forEach((col: any, index: number) => {
if (col.displayName && typeof col.displayName === "string") {
addLabel(`${comp.id}_col_${index}`, col.displayName, "column");
}
});
}
// 6. 분할 패널 - 좌측/우측 패널 컬럼 추출
if (config?.leftPanel?.columns && Array.isArray(config.leftPanel.columns)) {
config.leftPanel.columns.forEach((col: any, index: number) => {
if (col.displayName && typeof col.displayName === "string") {
addLabel(`${comp.id}_left_col_${index}`, col.displayName, "column");
}
});
}
if (config?.rightPanel?.columns && Array.isArray(config.rightPanel.columns)) {
config.rightPanel.columns.forEach((col: any, index: number) => {
if (col.displayName && typeof col.displayName === "string") {
addLabel(`${comp.id}_right_col_${index}`, col.displayName, "column");
}
});
}
// 7. 검색 필터 필드 추출
if (config?.filter?.filters && Array.isArray(config.filter.filters)) {
config.filter.filters.forEach((filter: any, index: number) => {
if (filter.label && typeof filter.label === "string") {
addLabel(`${comp.id}_filter_${index}`, filter.label, "filter");
}
});
}
// 8. 폼 필드 라벨 추출 (input-form 등)
if (config?.fields && Array.isArray(config.fields)) {
config.fields.forEach((field: any, index: number) => {
if (field.label && typeof field.label === "string") {
addLabel(`${comp.id}_field_${index}`, field.label, "field");
}
if (field.placeholder && typeof field.placeholder === "string") {
addLabel(`${comp.id}_field_ph_${index}`, field.placeholder, "placeholder");
}
});
}
// 9. 탭 라벨 추출
if (config?.tabs && Array.isArray(config.tabs)) {
config.tabs.forEach((tab: any, index: number) => {
if (tab.label && typeof tab.label === "string") {
addLabel(`${comp.id}_tab_${index}`, tab.label, "tab");
}
});
}
// 10. 액션 버튼 추출
if (config?.actions?.actions && Array.isArray(config.actions.actions)) {
config.actions.actions.forEach((action: any, index: number) => {
if (action.label && typeof action.label === "string") {
addLabel(`${comp.id}_action_${index}`, action.label, "action");
}
});
}
// 자식 컴포넌트 재귀 탐색
if (anyComp.children && Array.isArray(anyComp.children)) {
extractLabels(anyComp.children);
}
});
};
extractLabels(layout.components);
// 라벨 추출 (다국어 설정과 동일한 로직)
const extractedLabels = extractMultilangLabels(layout.components, columnLabelMap);
const labels = extractedLabels.map((l) => ({
componentId: l.componentId,
label: l.label,
type: l.type,
}));
if (labels.length === 0) {
toast.info("다국어로 변환할 라벨이 없습니다.");
@ -1576,13 +1505,6 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
return;
}
console.log("🌐 다국어 자동 생성 요청:", {
screenId: selectedScreen.screenId,
menuObjid,
labelsCount: labels.length,
labels: labels.slice(0, 5), // 처음 5개만 로그
});
// API 호출
const { generateScreenLabelKeys } = await import("@/lib/api/multilang");
const response = await generateScreenLabelKeys({
@ -1592,13 +1514,21 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
});
if (response.success && response.data) {
toast.success(`${response.data.length}개의 다국어 키가 생성되었습니다.`);
console.log("✅ 다국어 키 생성 완료:", response.data);
// 자동 매핑 적용
const updatedComponents = applyMultilangMappings(layout.components, response.data);
// 레이아웃 업데이트
setLayout((prev) => ({
...prev,
components: updatedComponents,
}));
toast.success(`${response.data.length}개의 다국어 키가 생성되고 컴포넌트에 매핑되었습니다.`);
} else {
toast.error(response.error?.details || "다국어 키 생성에 실패했습니다.");
}
} catch (error) {
console.error("❌ 다국어 생성 실패:", error);
console.error("다국어 생성 실패:", error);
toast.error("다국어 키 생성 중 오류가 발생했습니다.");
} finally {
setIsGeneratingMultilang(false);
@ -5165,10 +5095,35 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
isOpen={showMultilangSettingsModal}
onClose={() => setShowMultilangSettingsModal(false)}
components={layout.components}
onSave={(updates) => {
// TODO: 컴포넌트에 langKeyId 저장 로직 구현
console.log("다국어 설정 저장:", updates);
toast.success(`${updates.length}개 항목의 다국어 설정이 저장되었습니다.`);
onSave={async (updates) => {
if (updates.length === 0) {
toast.info("저장할 변경사항이 없습니다.");
return;
}
try {
// 공통 유틸 사용하여 매핑 적용
const { applyMultilangMappings } = await import("@/lib/utils/multilangLabelExtractor");
// 매핑 형식 변환
const mappings = updates.map((u) => ({
componentId: u.componentId,
keyId: u.langKeyId,
langKey: u.langKey,
}));
// 레이아웃 업데이트
const updatedComponents = applyMultilangMappings(layout.components, mappings);
setLayout((prev) => ({
...prev,
components: updatedComponents,
}));
toast.success(`${updates.length}개 항목의 다국어 설정이 저장되었습니다.`);
} catch (error) {
console.error("다국어 설정 저장 실패:", error);
toast.error("다국어 설정 저장 중 오류가 발생했습니다.");
}
}}
/>
</div>

View File

@ -0,0 +1,446 @@
/**
*
* .
*/
import { ComponentData } from "@/types/screen";
// 추출된 라벨 타입
export interface ExtractedLabel {
id: string;
componentId: string;
label: string;
type: "label" | "title" | "button" | "placeholder" | "column" | "filter" | "field" | "tab" | "action";
parentType?: string;
parentLabel?: string;
langKeyId?: number;
langKey?: string;
}
// 입력 폼 컴포넌트인지 확인
const INPUT_COMPONENT_TYPES = new Set([
"text-field",
"number-field",
"date-field",
"datetime-field",
"select-field",
"checkbox-field",
"radio-field",
"textarea-field",
"file-field",
"email-field",
"tel-field",
"password-field",
"entity-field",
"code-field",
"category-field",
"input-field",
"widget",
]);
const isInputComponent = (comp: any): boolean => {
const compType = comp.componentType || comp.type;
if (INPUT_COMPONENT_TYPES.has(compType)) return true;
if (compType === "widget" && comp.widgetType) return true;
if (comp.inputType || comp.webType) return true;
return false;
};
// 컬럼 라벨 맵 타입
export type ColumnLabelMap = Record<string, Record<string, string>>;
/**
*
* @param components
* @param columnLabelMap ()
* @returns
*/
export function extractMultilangLabels(
components: ComponentData[],
columnLabelMap: ColumnLabelMap = {}
): ExtractedLabel[] {
const labels: ExtractedLabel[] = [];
const addedLabels = new Set<string>();
const addLabel = (
componentId: string,
label: string,
type: ExtractedLabel["type"],
parentType?: string,
parentLabel?: string,
langKeyId?: number,
langKey?: string
) => {
const key = `${componentId}_${type}_${label}`;
if (label && label.trim() && !addedLabels.has(key)) {
addedLabels.add(key);
labels.push({
id: key,
componentId,
label: label.trim(),
type,
parentType,
parentLabel,
langKeyId,
langKey,
});
}
};
const extractFromComponent = (comp: ComponentData, parentType?: string, parentLabel?: string) => {
const anyComp = comp as any;
const config = anyComp.componentConfig;
const compType = anyComp.componentType || anyComp.type;
const compLabel = anyComp.label || anyComp.title || compType;
// 1. 기본 라벨 - 입력 폼 컴포넌트인 경우에만 추출
if (isInputComponent(anyComp)) {
if (anyComp.label && typeof anyComp.label === "string") {
addLabel(comp.id, anyComp.label, "label", parentType, parentLabel, anyComp.langKeyId, anyComp.langKey);
}
}
// 2. 제목
if (anyComp.title && typeof anyComp.title === "string") {
addLabel(comp.id, anyComp.title, "title", parentType, parentLabel);
}
// 3. 버튼 텍스트
if (config?.text && typeof config.text === "string") {
addLabel(comp.id, config.text, "button", parentType, parentLabel);
}
// 4. placeholder
if (anyComp.placeholder && typeof anyComp.placeholder === "string") {
addLabel(comp.id, anyComp.placeholder, "placeholder", parentType, parentLabel);
}
// 5. 테이블 컬럼 - columnLabelMap에서 한글 라벨 조회
const tableName = config?.selectedTable || config?.tableName || config?.table || anyComp.tableName;
if (config?.columns && Array.isArray(config.columns)) {
config.columns.forEach((col: any, index: number) => {
const colName = col.columnName || col.field || col.name;
// columnLabelMap에서 한글 라벨 조회, 없으면 displayName 사용
const colLabel = columnLabelMap[tableName]?.[colName] || col.displayName || col.label || colName;
if (colLabel && typeof colLabel === "string") {
addLabel(
`${comp.id}_col_${index}`,
colLabel,
"column",
compType,
compLabel,
col.langKeyId,
col.langKey
);
}
});
}
// 6. 분할 패널 컬럼 - columnLabelMap에서 한글 라벨 조회
const leftTableName = config?.leftPanel?.selectedTable || config?.leftPanel?.tableName || tableName;
if (config?.leftPanel?.columns && Array.isArray(config.leftPanel.columns)) {
config.leftPanel.columns.forEach((col: any, index: number) => {
const colName = col.columnName || col.field || col.name;
const colLabel = columnLabelMap[leftTableName]?.[colName] || col.displayName || col.label || colName;
if (colLabel && typeof colLabel === "string") {
addLabel(
`${comp.id}_left_col_${index}`,
colLabel,
"column",
compType,
`${compLabel} (좌측)`,
col.langKeyId,
col.langKey
);
}
});
}
const rightTableName = config?.rightPanel?.selectedTable || config?.rightPanel?.tableName || tableName;
if (config?.rightPanel?.columns && Array.isArray(config.rightPanel.columns)) {
config.rightPanel.columns.forEach((col: any, index: number) => {
const colName = col.columnName || col.field || col.name;
const colLabel = columnLabelMap[rightTableName]?.[colName] || col.displayName || col.label || colName;
if (colLabel && typeof colLabel === "string") {
addLabel(
`${comp.id}_right_col_${index}`,
colLabel,
"column",
compType,
`${compLabel} (우측)`,
col.langKeyId,
col.langKey
);
}
});
}
// 7. 검색 필터
if (config?.filter?.filters && Array.isArray(config.filter.filters)) {
config.filter.filters.forEach((filter: any, index: number) => {
if (filter.label && typeof filter.label === "string") {
addLabel(
`${comp.id}_filter_${index}`,
filter.label,
"filter",
compType,
compLabel,
filter.langKeyId,
filter.langKey
);
}
});
}
// 8. 폼 필드
if (config?.fields && Array.isArray(config.fields)) {
config.fields.forEach((field: any, index: number) => {
if (field.label && typeof field.label === "string") {
addLabel(
`${comp.id}_field_${index}`,
field.label,
"field",
compType,
compLabel,
field.langKeyId,
field.langKey
);
}
});
}
// 9. 탭
if (config?.tabs && Array.isArray(config.tabs)) {
config.tabs.forEach((tab: any, index: number) => {
if (tab.label && typeof tab.label === "string") {
addLabel(
`${comp.id}_tab_${index}`,
tab.label,
"tab",
compType,
compLabel,
tab.langKeyId,
tab.langKey
);
}
});
}
// 10. 액션 버튼
if (config?.actions?.actions && Array.isArray(config.actions.actions)) {
config.actions.actions.forEach((action: any, index: number) => {
if (action.label && typeof action.label === "string") {
addLabel(
`${comp.id}_action_${index}`,
action.label,
"action",
compType,
compLabel,
action.langKeyId,
action.langKey
);
}
});
}
// 자식 컴포넌트 재귀 탐색
if (anyComp.children && Array.isArray(anyComp.children)) {
anyComp.children.forEach((child: ComponentData) => {
extractFromComponent(child, compType, compLabel);
});
}
};
components.forEach((comp) => extractFromComponent(comp));
return labels;
}
/**
*
* @param components
* @returns Set
*/
export function extractTableNames(components: ComponentData[]): Set<string> {
const tableNames = new Set<string>();
const extractTableName = (comp: any) => {
const config = comp.componentConfig;
// 1. 컴포넌트 직접 tableName
if (comp.tableName) tableNames.add(comp.tableName);
// 2. componentConfig 직접 tableName
if (config?.tableName) tableNames.add(config.tableName);
// 3. 테이블 리스트 컴포넌트 - selectedTable (주요!)
if (config?.selectedTable) tableNames.add(config.selectedTable);
// 4. 테이블 리스트 컴포넌트 - table 속성
if (config?.table) tableNames.add(config.table);
// 5. 분할 패널의 leftPanel/rightPanel
if (config?.leftPanel?.tableName) tableNames.add(config.leftPanel.tableName);
if (config?.rightPanel?.tableName) tableNames.add(config.rightPanel.tableName);
if (config?.leftPanel?.table) tableNames.add(config.leftPanel.table);
if (config?.rightPanel?.table) tableNames.add(config.rightPanel.table);
if (config?.leftPanel?.selectedTable) tableNames.add(config.leftPanel.selectedTable);
if (config?.rightPanel?.selectedTable) tableNames.add(config.rightPanel.selectedTable);
// 6. 검색 필터의 tableName
if (config?.filter?.tableName) tableNames.add(config.filter.tableName);
// 7. properties 안의 tableName
if (comp.properties?.tableName) tableNames.add(comp.properties.tableName);
if (comp.properties?.selectedTable) tableNames.add(comp.properties.selectedTable);
// 자식 컴포넌트 탐색
if (comp.children && Array.isArray(comp.children)) {
comp.children.forEach(extractTableName);
}
};
components.forEach(extractTableName);
return tableNames;
}
/**
*
* @param components
* @param mappings [{componentId, keyId, langKey}]
* @returns
*/
export function applyMultilangMappings(
components: ComponentData[],
mappings: Array<{ componentId: string; keyId: number; langKey: string }>
): ComponentData[] {
// 매핑을 빠르게 찾기 위한 맵 생성
const mappingMap = new Map(mappings.map((m) => [m.componentId, m]));
const updateComponent = (comp: ComponentData): ComponentData => {
const anyComp = comp as any;
const config = anyComp.componentConfig;
let updated = { ...comp } as any;
// 기본 컴포넌트 라벨 매핑 확인
const labelMapping = mappingMap.get(comp.id);
if (labelMapping) {
updated.langKeyId = labelMapping.keyId;
updated.langKey = labelMapping.langKey;
// 버튼 컴포넌트의 경우 componentConfig에도 매핑
if (config?.text) {
updated.componentConfig = {
...updated.componentConfig,
langKeyId: labelMapping.keyId,
langKey: labelMapping.langKey,
};
}
}
// 컬럼 매핑
if (config?.columns && Array.isArray(config.columns)) {
const updatedColumns = config.columns.map((col: any, index: number) => {
const colMapping = mappingMap.get(`${comp.id}_col_${index}`);
if (colMapping) {
return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey };
}
return col;
});
updated.componentConfig = { ...config, columns: updatedColumns };
}
// 분할 패널 좌측 컬럼 매핑
if (config?.leftPanel?.columns && Array.isArray(config.leftPanel.columns)) {
const updatedLeftColumns = config.leftPanel.columns.map((col: any, index: number) => {
const colMapping = mappingMap.get(`${comp.id}_left_col_${index}`);
if (colMapping) {
return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey };
}
return col;
});
updated.componentConfig = {
...updated.componentConfig,
leftPanel: { ...config.leftPanel, columns: updatedLeftColumns },
};
}
// 분할 패널 우측 컬럼 매핑
if (config?.rightPanel?.columns && Array.isArray(config.rightPanel.columns)) {
const updatedRightColumns = config.rightPanel.columns.map((col: any, index: number) => {
const colMapping = mappingMap.get(`${comp.id}_right_col_${index}`);
if (colMapping) {
return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey };
}
return col;
});
updated.componentConfig = {
...updated.componentConfig,
rightPanel: { ...config.rightPanel, columns: updatedRightColumns },
};
}
// 필터 매핑
if (config?.filter?.filters && Array.isArray(config.filter.filters)) {
const updatedFilters = config.filter.filters.map((filter: any, index: number) => {
const filterMapping = mappingMap.get(`${comp.id}_filter_${index}`);
if (filterMapping) {
return { ...filter, langKeyId: filterMapping.keyId, langKey: filterMapping.langKey };
}
return filter;
});
updated.componentConfig = {
...updated.componentConfig,
filter: { ...config.filter, filters: updatedFilters },
};
}
// 폼 필드 매핑
if (config?.fields && Array.isArray(config.fields)) {
const updatedFields = config.fields.map((field: any, index: number) => {
const fieldMapping = mappingMap.get(`${comp.id}_field_${index}`);
if (fieldMapping) {
return { ...field, langKeyId: fieldMapping.keyId, langKey: fieldMapping.langKey };
}
return field;
});
updated.componentConfig = { ...updated.componentConfig, fields: updatedFields };
}
// 탭 매핑
if (config?.tabs && Array.isArray(config.tabs)) {
const updatedTabs = config.tabs.map((tab: any, index: number) => {
const tabMapping = mappingMap.get(`${comp.id}_tab_${index}`);
if (tabMapping) {
return { ...tab, langKeyId: tabMapping.keyId, langKey: tabMapping.langKey };
}
return tab;
});
updated.componentConfig = { ...updated.componentConfig, tabs: updatedTabs };
}
// 액션 버튼 매핑
if (config?.actions?.actions && Array.isArray(config.actions.actions)) {
const updatedActions = config.actions.actions.map((action: any, index: number) => {
const actionMapping = mappingMap.get(`${comp.id}_action_${index}`);
if (actionMapping) {
return { ...action, langKeyId: actionMapping.keyId, langKey: actionMapping.langKey };
}
return action;
});
updated.componentConfig = {
...updated.componentConfig,
actions: { ...config.actions, actions: updatedActions },
};
}
// 자식 컴포넌트 재귀 처리
if (anyComp.children && Array.isArray(anyComp.children)) {
updated.children = anyComp.children.map((child: ComponentData) => updateComponent(child));
}
return updated;
};
return components.map(updateComponent);
}