다국어 화면에서 수정기능 구현
This commit is contained in:
parent
14f8714ea1
commit
16c9c71a23
|
|
@ -53,6 +53,14 @@ interface LangKey {
|
||||||
categoryName?: string;
|
categoryName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 번역 텍스트 타입
|
||||||
|
interface LangText {
|
||||||
|
langCode: string;
|
||||||
|
langName: string;
|
||||||
|
langNative: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 입력 가능한 폼 컴포넌트 타입 목록
|
// 입력 가능한 폼 컴포넌트 타입 목록
|
||||||
const INPUT_COMPONENT_TYPES = new Set([
|
const INPUT_COMPONENT_TYPES = new Set([
|
||||||
"text-input",
|
"text-input",
|
||||||
|
|
@ -508,7 +516,32 @@ export const MultilangSettingsModal: React.FC<MultilangSettingsModalProps> = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 분할 패널 컬럼 - columnLabelMap에서 한글 라벨 조회
|
// 6. 분할 패널 제목 및 컬럼 - columnLabelMap에서 한글 라벨 조회
|
||||||
|
// 6-1. 좌측 패널 제목
|
||||||
|
if (config?.leftPanel?.title && typeof config.leftPanel.title === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_left_title`,
|
||||||
|
config.leftPanel.title,
|
||||||
|
"title",
|
||||||
|
compType,
|
||||||
|
`${compLabel} (좌측)`,
|
||||||
|
config.leftPanel.langKeyId,
|
||||||
|
config.leftPanel.langKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 6-2. 우측 패널 제목
|
||||||
|
if (config?.rightPanel?.title && typeof config.rightPanel.title === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_right_title`,
|
||||||
|
config.rightPanel.title,
|
||||||
|
"title",
|
||||||
|
compType,
|
||||||
|
`${compLabel} (우측)`,
|
||||||
|
config.rightPanel.langKeyId,
|
||||||
|
config.rightPanel.langKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 6-3. 좌측 패널 컬럼
|
||||||
const leftTableName = config?.leftPanel?.selectedTable || config?.leftPanel?.tableName || tableName;
|
const leftTableName = config?.leftPanel?.selectedTable || config?.leftPanel?.tableName || tableName;
|
||||||
if (config?.leftPanel?.columns && Array.isArray(config.leftPanel.columns)) {
|
if (config?.leftPanel?.columns && Array.isArray(config.leftPanel.columns)) {
|
||||||
config.leftPanel.columns.forEach((col: any, index: number) => {
|
config.leftPanel.columns.forEach((col: any, index: number) => {
|
||||||
|
|
@ -546,6 +579,56 @@ export const MultilangSettingsModal: React.FC<MultilangSettingsModalProps> = ({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6-5. 추가 탭 (additionalTabs) 제목 및 컬럼 - rightPanel.additionalTabs 확인
|
||||||
|
const additionalTabs = config?.rightPanel?.additionalTabs || config?.additionalTabs;
|
||||||
|
if (additionalTabs && Array.isArray(additionalTabs)) {
|
||||||
|
additionalTabs.forEach((tab: any, tabIndex: number) => {
|
||||||
|
// 탭 라벨
|
||||||
|
if (tab.label && typeof tab.label === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_addtab_${tabIndex}_label`,
|
||||||
|
tab.label,
|
||||||
|
"tab",
|
||||||
|
compType,
|
||||||
|
compLabel,
|
||||||
|
tab.langKeyId,
|
||||||
|
tab.langKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 탭 제목
|
||||||
|
if (tab.title && typeof tab.title === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_addtab_${tabIndex}_title`,
|
||||||
|
tab.title,
|
||||||
|
"title",
|
||||||
|
compType,
|
||||||
|
`${compLabel} (탭: ${tab.label || tabIndex})`,
|
||||||
|
tab.titleLangKeyId,
|
||||||
|
tab.titleLangKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 탭 컬럼
|
||||||
|
const tabTableName = tab.tableName || tab.selectedTable || rightTableName;
|
||||||
|
if (tab.columns && Array.isArray(tab.columns)) {
|
||||||
|
tab.columns.forEach((col: any, colIndex: number) => {
|
||||||
|
const colName = col.columnName || col.field || col.name;
|
||||||
|
const colLabel = columnLabelMap[tabTableName]?.[colName] || col.displayName || col.label || colName;
|
||||||
|
if (colLabel && typeof colLabel === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_addtab_${tabIndex}_col_${colIndex}`,
|
||||||
|
colLabel,
|
||||||
|
"column",
|
||||||
|
compType,
|
||||||
|
`${compLabel} (탭: ${tab.label || tabIndex})`,
|
||||||
|
col.langKeyId,
|
||||||
|
col.langKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 7. 검색 필터
|
// 7. 검색 필터
|
||||||
if (config?.filter?.filters && Array.isArray(config.filter.filters)) {
|
if (config?.filter?.filters && Array.isArray(config.filter.filters)) {
|
||||||
config.filter.filters.forEach((filter: any, index: number) => {
|
config.filter.filters.forEach((filter: any, index: number) => {
|
||||||
|
|
@ -732,9 +815,160 @@ export const MultilangSettingsModal: React.FC<MultilangSettingsModalProps> = ({
|
||||||
const connectedCount = selectedItems.size;
|
const connectedCount = selectedItems.size;
|
||||||
const totalCount = extractedLabels.length;
|
const totalCount = extractedLabels.length;
|
||||||
|
|
||||||
|
// 선택된 라벨 항목
|
||||||
|
const [selectedLabelItem, setSelectedLabelItem] = useState<ExtractedLabel | null>(null);
|
||||||
|
|
||||||
|
// 선택된 라벨의 번역 텍스트 로드
|
||||||
|
const [isLoadingTranslations, setIsLoadingTranslations] = useState(false);
|
||||||
|
|
||||||
|
// 모든 항목의 편집된 번역을 저장하는 맵 (itemId -> langCode -> text)
|
||||||
|
const [allEditedTranslations, setAllEditedTranslations] = useState<Record<string, Record<string, string>>>({});
|
||||||
|
|
||||||
|
// 현재 선택된 항목의 편집된 번역
|
||||||
|
const currentTranslations = selectedLabelItem ? (allEditedTranslations[selectedLabelItem.id] || {}) : {};
|
||||||
|
|
||||||
|
// 저장 중 상태
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
// 활성화된 언어 목록
|
||||||
|
const [activeLanguages, setActiveLanguages] = useState<Array<{ langCode: string; langName: string; langNative: string }>>([]);
|
||||||
|
|
||||||
|
// 언어 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadLanguages = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get("/multilang/languages");
|
||||||
|
if (response.data?.success && Array.isArray(response.data.data)) {
|
||||||
|
const activeLangs = response.data.data
|
||||||
|
.filter((lang: any) => (lang.isActive || lang.is_active) === "Y")
|
||||||
|
.map((lang: any) => ({
|
||||||
|
langCode: lang.langCode || lang.lang_code,
|
||||||
|
langName: lang.langName || lang.lang_name,
|
||||||
|
langNative: lang.langNative || lang.lang_native,
|
||||||
|
}));
|
||||||
|
setActiveLanguages(activeLangs);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("언어 목록 로드 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isOpen) {
|
||||||
|
loadLanguages();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// 선택된 라벨의 번역 텍스트 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTranslations = async () => {
|
||||||
|
if (!selectedLabelItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 언어 목록이 아직 로드되지 않았으면 대기
|
||||||
|
if (activeLanguages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미 이 항목의 번역을 로드한 적이 있으면 스킵
|
||||||
|
if (allEditedTranslations[selectedLabelItem.id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectedKey = selectedItems.get(selectedLabelItem.id);
|
||||||
|
if (!connectedKey?.langKeyId) {
|
||||||
|
// 키가 연결되지 않은 경우 빈 번역으로 초기화
|
||||||
|
const defaultTranslations = activeLanguages.reduce((acc, lang) => {
|
||||||
|
acc[lang.langCode] = lang.langCode === "KR" ? selectedLabelItem.label : "";
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
setAllEditedTranslations((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[selectedLabelItem.id]: defaultTranslations,
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoadingTranslations(true);
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`/multilang/keys/${connectedKey.langKeyId}/texts`);
|
||||||
|
if (response.data?.success && response.data?.data) {
|
||||||
|
const textsMap: Record<string, string> = {};
|
||||||
|
response.data.data.forEach((t: any) => {
|
||||||
|
textsMap[t.langCode || t.lang_code] = t.langText || t.lang_text || "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 모든 활성 언어에 대해 번역 텍스트 생성
|
||||||
|
const loadedTranslations = activeLanguages.reduce((acc, lang) => {
|
||||||
|
acc[lang.langCode] = textsMap[lang.langCode] || "";
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
setAllEditedTranslations((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[selectedLabelItem.id]: loadedTranslations,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("번역 텍스트 로드 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingTranslations(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTranslations();
|
||||||
|
}, [selectedLabelItem, selectedItems, activeLanguages, allEditedTranslations]);
|
||||||
|
|
||||||
|
// 현재 항목의 번역 텍스트 변경
|
||||||
|
const handleTranslationChange = (langCode: string, text: string) => {
|
||||||
|
if (!selectedLabelItem) return;
|
||||||
|
|
||||||
|
setAllEditedTranslations((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[selectedLabelItem.id]: {
|
||||||
|
...(prev[selectedLabelItem.id] || {}),
|
||||||
|
[langCode]: text,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 모든 번역 텍스트 저장 (최종 저장 시)
|
||||||
|
const handleSaveAllTranslations = async () => {
|
||||||
|
const savePromises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
// 키가 연결된 항목들의 번역만 저장
|
||||||
|
for (const [itemId, translations] of Object.entries(allEditedTranslations)) {
|
||||||
|
const connectedKey = selectedItems.get(itemId);
|
||||||
|
if (!connectedKey?.langKeyId) continue;
|
||||||
|
|
||||||
|
const texts = activeLanguages.map((lang) => ({
|
||||||
|
langCode: lang.langCode,
|
||||||
|
langText: translations[lang.langCode] || "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
savePromises.push(
|
||||||
|
apiClient.post(`/multilang/keys/${connectedKey.langKeyId}/texts`, { texts })
|
||||||
|
.then(() => {})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`번역 저장 실패 (keyId: ${connectedKey.langKeyId}):`, error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(savePromises);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 모달 닫힐 때 선택 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setSelectedLabelItem(null);
|
||||||
|
setAllEditedTranslations({});
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||||
<DialogContent className="max-h-[85vh] max-w-3xl">
|
<DialogContent className="max-h-[85vh] max-w-5xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<Languages className="h-5 w-5" />
|
<Languages className="h-5 w-5" />
|
||||||
|
|
@ -745,117 +979,214 @@ export const MultilangSettingsModal: React.FC<MultilangSettingsModalProps> = ({
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{/* 검색 */}
|
{/* 좌우 분할 레이아웃 */}
|
||||||
<div className="relative">
|
<div className="flex gap-4" style={{ height: "500px" }}>
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
{/* 왼쪽: 라벨 목록 */}
|
||||||
<Input
|
<div className="flex w-1/2 flex-col overflow-hidden">
|
||||||
placeholder="라벨 또는 키로 검색..."
|
{/* 검색 */}
|
||||||
value={searchText}
|
<div className="relative mb-2 shrink-0">
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
className="pl-10"
|
<Input
|
||||||
/>
|
placeholder="라벨 또는 키로 검색..."
|
||||||
</div>
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 라벨 목록 */}
|
{/* 라벨 목록 */}
|
||||||
<ScrollArea className="h-[400px] rounded-md border">
|
<ScrollArea className="min-h-0 flex-1 rounded-md border">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{groupedLabels.length === 0 ? (
|
{groupedLabels.length === 0 ? (
|
||||||
<div className="flex h-32 items-center justify-center text-sm text-muted-foreground">
|
<div className="flex h-32 items-center justify-center text-sm text-muted-foreground">
|
||||||
{searchText ? "검색 결과가 없습니다." : "다국어 대상이 없습니다."}
|
{searchText ? "검색 결과가 없습니다." : "다국어 대상이 없습니다."}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
groupedLabels.map((group) => (
|
||||||
|
<div key={group.id} className="mb-2">
|
||||||
|
{/* 그룹 헤더 */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleGroup(group.id)}
|
||||||
|
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium hover:bg-muted"
|
||||||
|
>
|
||||||
|
{group.isExpanded ? (
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{group.icon}
|
||||||
|
<span>{group.label}</span>
|
||||||
|
<Badge variant="outline" className="ml-auto">
|
||||||
|
{group.items.filter((i) => selectedItems.has(i.id)).length} / {group.items.length}
|
||||||
|
</Badge>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 그룹 아이템 */}
|
||||||
|
{group.isExpanded && (
|
||||||
|
<div className="ml-6 mt-1 space-y-1">
|
||||||
|
{group.items.map((item) => {
|
||||||
|
const isConnected = selectedItems.has(item.id);
|
||||||
|
const isSelected = selectedLabelItem?.id === item.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => setSelectedLabelItem(item)}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-pointer items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors",
|
||||||
|
isSelected
|
||||||
|
? "border-primary bg-primary/10 ring-1 ring-primary"
|
||||||
|
: isConnected
|
||||||
|
? "border-green-200 bg-green-50 hover:bg-green-100"
|
||||||
|
: "border-gray-200 bg-white hover:bg-gray-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* 상태 아이콘 */}
|
||||||
|
<div className="shrink-0">
|
||||||
|
{isConnected ? (
|
||||||
|
<Check className="h-4 w-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<X className="h-4 w-4 text-gray-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 타입 배지 */}
|
||||||
|
<Badge variant="outline" className="shrink-0 text-xs">
|
||||||
|
{getTypeLabel(item.type)}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
{/* 라벨 텍스트 */}
|
||||||
|
<span className="flex-1 truncate font-medium">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</ScrollArea>
|
||||||
groupedLabels.map((group) => (
|
</div>
|
||||||
<div key={group.id} className="mb-2">
|
|
||||||
{/* 그룹 헤더 */}
|
|
||||||
<button
|
|
||||||
onClick={() => toggleGroup(group.id)}
|
|
||||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm font-medium hover:bg-muted"
|
|
||||||
>
|
|
||||||
{group.isExpanded ? (
|
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{group.icon}
|
|
||||||
<span>{group.label}</span>
|
|
||||||
<Badge variant="outline" className="ml-auto">
|
|
||||||
{group.items.filter((i) => selectedItems.has(i.id)).length} / {group.items.length}
|
|
||||||
</Badge>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* 그룹 아이템 */}
|
{/* 오른쪽: 선택된 항목 상세 및 번역 편집 */}
|
||||||
{group.isExpanded && (
|
<div className="flex w-1/2 flex-col rounded-md border bg-gray-50 p-4">
|
||||||
<div className="ml-6 mt-1 space-y-1">
|
{selectedLabelItem ? (
|
||||||
{group.items.map((item) => {
|
<>
|
||||||
const isConnected = selectedItems.has(item.id);
|
{/* 선택된 항목 정보 */}
|
||||||
const connectedKey = selectedItems.get(item.id);
|
<div className="mb-4 rounded-md border bg-white p-3">
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<Badge variant="outline">{getTypeLabel(selectedLabelItem.type)}</Badge>
|
||||||
|
<span className="font-medium">{selectedLabelItem.label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
return (
|
{/* 키 선택 */}
|
||||||
<div
|
<div className="flex items-center gap-2">
|
||||||
key={item.id}
|
<span className="text-sm text-muted-foreground">다국어 키:</span>
|
||||||
className={cn(
|
<KeySelectCombobox
|
||||||
"flex items-center gap-2 rounded-md border px-3 py-2 text-sm",
|
value={
|
||||||
isConnected ? "border-green-200 bg-green-50" : "border-gray-200 bg-white"
|
selectedItems.get(selectedLabelItem.id)?.langKeyId &&
|
||||||
)}
|
selectedItems.get(selectedLabelItem.id)?.langKey
|
||||||
>
|
? {
|
||||||
{/* 상태 아이콘 */}
|
keyId: selectedItems.get(selectedLabelItem.id)!.langKeyId!,
|
||||||
<div className="shrink-0">
|
langKey: selectedItems.get(selectedLabelItem.id)!.langKey!,
|
||||||
{isConnected ? (
|
}
|
||||||
<Check className="h-4 w-4 text-green-600" />
|
: undefined
|
||||||
) : (
|
}
|
||||||
<X className="h-4 w-4 text-gray-400" />
|
onChange={(key) => handleKeySelect(selectedLabelItem.id, key)}
|
||||||
)}
|
langKeys={langKeys}
|
||||||
|
isLoading={isLoadingKeys}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 번역 텍스트 편집 */}
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
<div className="mb-2">
|
||||||
|
<h4 className="text-sm font-medium">번역 텍스트</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoadingTranslations || activeLanguages.length === 0 ? (
|
||||||
|
<div className="flex h-32 items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : !selectedItems.get(selectedLabelItem.id)?.langKeyId ? (
|
||||||
|
<div className="flex h-32 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
먼저 다국어 키를 선택해주세요
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ScrollArea className="h-[280px]">
|
||||||
|
<div className="space-y-3 pr-4">
|
||||||
|
{activeLanguages.map((lang) => (
|
||||||
|
<div key={lang.langCode} className="rounded-md border bg-white p-3">
|
||||||
|
<div className="mb-1.5 flex items-center gap-2">
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{lang.langCode}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{lang.langNative || lang.langName}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<Input
|
||||||
{/* 타입 배지 */}
|
value={currentTranslations[lang.langCode] || ""}
|
||||||
<Badge variant="outline" className="shrink-0 text-xs">
|
onChange={(e) => handleTranslationChange(lang.langCode, e.target.value)}
|
||||||
{getTypeLabel(item.type)}
|
placeholder={`${lang.langName} 번역 입력...`}
|
||||||
</Badge>
|
className="h-9 text-sm"
|
||||||
|
|
||||||
{/* 라벨 텍스트 */}
|
|
||||||
<span className="flex-1 truncate font-medium">{item.label}</span>
|
|
||||||
|
|
||||||
{/* 키 선택 콤보박스 */}
|
|
||||||
<KeySelectCombobox
|
|
||||||
value={
|
|
||||||
connectedKey?.langKeyId && connectedKey?.langKey
|
|
||||||
? { keyId: connectedKey.langKeyId, langKey: connectedKey.langKey }
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={(key) => handleKeySelect(item.id, key)}
|
|
||||||
langKeys={langKeys}
|
|
||||||
isLoading={isLoadingKeys}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
))}
|
||||||
})}
|
</div>
|
||||||
</div>
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center text-muted-foreground">
|
||||||
|
<Languages className="mb-2 h-12 w-12 opacity-20" />
|
||||||
|
<p className="text-sm">좌측에서 항목을 선택하세요</p>
|
||||||
|
<p className="text-xs">선택한 항목의 다국어 설정을 할 수 있습니다</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2">
|
<DialogFooter className="gap-2">
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="outline" onClick={onClose}>
|
||||||
닫기
|
닫기
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
disabled={isSaving}
|
||||||
// 변경사항 저장
|
onClick={async () => {
|
||||||
const updates = Array.from(selectedItems.entries())
|
setIsSaving(true);
|
||||||
.filter(([, data]) => data.langKeyId && data.langKey)
|
try {
|
||||||
.map(([id, data]) => ({
|
// 1. 번역 텍스트 저장
|
||||||
componentId: id,
|
await handleSaveAllTranslations();
|
||||||
langKeyId: data.langKeyId!,
|
|
||||||
langKey: data.langKey!,
|
// 2. 컴포넌트 매핑 저장
|
||||||
}));
|
const updates = Array.from(selectedItems.entries())
|
||||||
onSave(updates);
|
.filter(([, data]) => data.langKeyId && data.langKey)
|
||||||
onClose();
|
.map(([id, data]) => ({
|
||||||
|
componentId: id,
|
||||||
|
langKeyId: data.langKeyId!,
|
||||||
|
langKey: data.langKey!,
|
||||||
|
}));
|
||||||
|
onSave(updates);
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("저장 실패:", error);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
저장
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-1 h-4 w-4 animate-spin" />
|
||||||
|
저장 중...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"저장"
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,47 @@ export const ScreenMultiLangProvider: React.FC<ScreenMultiLangProviderProps> = (
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 분할패널 좌측/우측 제목 langKey 수집
|
||||||
|
const config = (comp as any).componentConfig;
|
||||||
|
if (config?.leftPanel?.langKey) {
|
||||||
|
keys.push(config.leftPanel.langKey);
|
||||||
|
}
|
||||||
|
if (config?.rightPanel?.langKey) {
|
||||||
|
keys.push(config.rightPanel.langKey);
|
||||||
|
}
|
||||||
|
// 분할패널 좌측/우측 컬럼 langKey 수집
|
||||||
|
if (config?.leftPanel?.columns) {
|
||||||
|
config.leftPanel.columns.forEach((col: any) => {
|
||||||
|
if (col.langKey) {
|
||||||
|
keys.push(col.langKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (config?.rightPanel?.columns) {
|
||||||
|
config.rightPanel.columns.forEach((col: any) => {
|
||||||
|
if (col.langKey) {
|
||||||
|
keys.push(col.langKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 추가 탭 langKey 수집
|
||||||
|
if (config?.additionalTabs) {
|
||||||
|
config.additionalTabs.forEach((tab: any) => {
|
||||||
|
if (tab.langKey) {
|
||||||
|
keys.push(tab.langKey);
|
||||||
|
}
|
||||||
|
if (tab.titleLangKey) {
|
||||||
|
keys.push(tab.titleLangKey);
|
||||||
|
}
|
||||||
|
if (tab.columns) {
|
||||||
|
tab.columns.forEach((col: any) => {
|
||||||
|
if (col.langKey) {
|
||||||
|
keys.push(col.langKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
// 자식 컴포넌트 재귀 처리
|
// 자식 컴포넌트 재귀 처리
|
||||||
if ((comp as any).children) {
|
if ((comp as any).children) {
|
||||||
collectLangKeys((comp as any).children);
|
collectLangKeys((comp as any).children);
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import { useTableOptions } from "@/contexts/TableOptionsContext";
|
||||||
import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options";
|
import { TableFilter, ColumnVisibility, GroupSumConfig } from "@/types/table-options";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { useSplitPanel } from "./SplitPanelContext";
|
import { useSplitPanel } from "./SplitPanelContext";
|
||||||
|
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||||
|
|
||||||
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
export interface SplitPanelLayoutComponentProps extends ComponentRendererProps {
|
||||||
// 추가 props
|
// 추가 props
|
||||||
|
|
@ -58,6 +59,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig;
|
const componentConfig = (component.componentConfig || {}) as SplitPanelLayoutConfig;
|
||||||
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 사용 가능)
|
// 🆕 프리뷰용 회사 코드 오버라이드 (최고 관리자만 사용 가능)
|
||||||
const companyCode = (props as any).companyCode as string | undefined;
|
const companyCode = (props as any).companyCode as string | undefined;
|
||||||
|
// 🌐 다국어 컨텍스트
|
||||||
|
const { getTranslatedText } = useScreenMultiLang();
|
||||||
|
|
||||||
// 기본 설정값
|
// 기본 설정값
|
||||||
const splitRatio = componentConfig.splitRatio || 30;
|
const splitRatio = componentConfig.splitRatio || 30;
|
||||||
|
|
@ -2559,7 +2562,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<CardTitle className="text-base font-semibold">
|
<CardTitle className="text-base font-semibold">
|
||||||
{componentConfig.leftPanel?.title || "좌측 패널"}
|
{getTranslatedText(componentConfig.leftPanel?.langKey, componentConfig.leftPanel?.title || "좌측 패널")}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{!isDesignMode && componentConfig.leftPanel?.showAdd && (
|
{!isDesignMode && componentConfig.leftPanel?.showAdd && (
|
||||||
<Button size="sm" variant="outline" onClick={() => handleAddClick("left")}>
|
<Button size="sm" variant="outline" onClick={() => handleAddClick("left")}>
|
||||||
|
|
@ -2593,9 +2596,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">컬럼 1</th>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">컬럼 1</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">컬럼 2</th>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">컬럼 2</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">컬럼 3</th>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">컬럼 3</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200 bg-white">
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
|
|
@ -2644,10 +2647,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
visibleLeftColumns.length > 0
|
visibleLeftColumns.length > 0
|
||||||
? visibleLeftColumns.map((col: any) => {
|
? visibleLeftColumns.map((col: any) => {
|
||||||
const colName = typeof col === "string" ? col : col.name || col.columnName;
|
const colName = typeof col === "string" ? col : col.name || col.columnName;
|
||||||
|
const originalLabel =
|
||||||
|
leftColumnLabels[colName] || (typeof col === "object" ? col.label : null) || colName;
|
||||||
|
const colLangKey = typeof col === "object" ? col.langKey : undefined;
|
||||||
return {
|
return {
|
||||||
name: colName,
|
name: colName,
|
||||||
label:
|
label: colLangKey ? getTranslatedText(colLangKey, originalLabel) : originalLabel,
|
||||||
leftColumnLabels[colName] || (typeof col === "object" ? col.label : null) || colName,
|
|
||||||
width: typeof col === "object" ? col.width : 150,
|
width: typeof col === "object" ? col.width : 150,
|
||||||
align: (typeof col === "object" ? col.align : "left") as "left" | "center" | "right",
|
align: (typeof col === "object" ? col.align : "left") as "left" | "center" | "right",
|
||||||
format: typeof col === "object" ? col.format : undefined, // 🆕 포맷 설정 포함
|
format: typeof col === "object" ? col.format : undefined, // 🆕 포맷 설정 포함
|
||||||
|
|
@ -2679,7 +2684,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{columnsToShow.map((col, idx) => (
|
{columnsToShow.map((col, idx) => (
|
||||||
<th
|
<th
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500"
|
||||||
style={{
|
style={{
|
||||||
width: col.width ? `${col.width}px` : "auto",
|
width: col.width ? `${col.width}px` : "auto",
|
||||||
textAlign: col.align || "left",
|
textAlign: col.align || "left",
|
||||||
|
|
@ -2740,7 +2745,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{columnsToShow.map((col, idx) => (
|
{columnsToShow.map((col, idx) => (
|
||||||
<th
|
<th
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500"
|
||||||
style={{
|
style={{
|
||||||
width: col.width ? `${col.width}px` : "auto",
|
width: col.width ? `${col.width}px` : "auto",
|
||||||
textAlign: col.align || "left",
|
textAlign: col.align || "left",
|
||||||
|
|
@ -3079,10 +3084,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<CardTitle className="text-base font-semibold">
|
<CardTitle className="text-base font-semibold">
|
||||||
{activeTabIndex === 0
|
{activeTabIndex === 0
|
||||||
? componentConfig.rightPanel?.title || "우측 패널"
|
? getTranslatedText(
|
||||||
: componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.title ||
|
componentConfig.rightPanel?.langKey,
|
||||||
componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.label ||
|
componentConfig.rightPanel?.title || "우측 패널",
|
||||||
"우측 패널"}
|
)
|
||||||
|
: getTranslatedText(
|
||||||
|
componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.titleLangKey,
|
||||||
|
componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.title ||
|
||||||
|
componentConfig.rightPanel?.additionalTabs?.[activeTabIndex - 1]?.label ||
|
||||||
|
"우측 패널",
|
||||||
|
)}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{!isDesignMode && (
|
{!isDesignMode && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -3368,12 +3379,16 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
let columnsToShow: any[] = [];
|
let columnsToShow: any[] = [];
|
||||||
|
|
||||||
if (displayColumns.length > 0) {
|
if (displayColumns.length > 0) {
|
||||||
// 설정된 컬럼 사용
|
// 설정된 컬럼 사용 - 🌐 다국어 처리 추가
|
||||||
columnsToShow = displayColumns.map((col) => ({
|
columnsToShow = displayColumns.map((col) => {
|
||||||
...col,
|
const originalLabel = rightColumnLabels[col.name] || col.label || col.name;
|
||||||
label: rightColumnLabels[col.name] || col.label || col.name,
|
const colLangKey = (col as any).langKey;
|
||||||
format: col.format,
|
return {
|
||||||
}));
|
...col,
|
||||||
|
label: colLangKey ? getTranslatedText(colLangKey, originalLabel) : originalLabel,
|
||||||
|
format: col.format,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// 🆕 그룹 합산 모드이고, 키 컬럼이 표시 목록에 없으면 맨 앞에 추가
|
// 🆕 그룹 합산 모드이고, 키 컬럼이 표시 목록에 없으면 맨 앞에 추가
|
||||||
if (isGroupedMode && keyColumns.length > 0) {
|
if (isGroupedMode && keyColumns.length > 0) {
|
||||||
|
|
@ -3422,7 +3437,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{columnsToShow.map((col, idx) => (
|
{columnsToShow.map((col, idx) => (
|
||||||
<th
|
<th
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase"
|
className="px-3 py-2 text-left text-xs font-medium tracking-wider text-gray-500"
|
||||||
style={{
|
style={{
|
||||||
width: col.width ? `${col.width}px` : "auto",
|
width: col.width ? `${col.width}px` : "auto",
|
||||||
textAlign: col.align || "left",
|
textAlign: col.align || "left",
|
||||||
|
|
@ -3435,9 +3450,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
{!isDesignMode &&
|
{!isDesignMode &&
|
||||||
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
((componentConfig.rightPanel?.editButton?.enabled ?? true) ||
|
||||||
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
(componentConfig.rightPanel?.deleteButton?.enabled ?? true)) && (
|
||||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500 uppercase">
|
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">작업</th>
|
||||||
작업
|
|
||||||
</th>
|
|
||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -3752,7 +3765,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{displayEntries.map(([key, value, label]) => (
|
{displayEntries.map(([key, value, label]) => (
|
||||||
<div key={key} className="bg-card rounded-lg border p-4 shadow-sm">
|
<div key={key} className="bg-card rounded-lg border p-4 shadow-sm">
|
||||||
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase">
|
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide">
|
||||||
{label || getColumnLabel(key)}
|
{label || getColumnLabel(key)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">{String(value)}</div>
|
<div className="text-sm">{String(value)}</div>
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,13 @@ export interface AdditionalTabConfig {
|
||||||
// 탭 고유 정보
|
// 탭 고유 정보
|
||||||
tabId: string;
|
tabId: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
langKeyId?: number; // 탭 라벨 다국어 키 ID
|
||||||
|
langKey?: string; // 탭 라벨 다국어 키
|
||||||
|
|
||||||
// === 우측 패널과 동일한 설정 ===
|
// === 우측 패널과 동일한 설정 ===
|
||||||
title: string;
|
title: string;
|
||||||
|
titleLangKeyId?: number; // 탭 제목 다국어 키 ID
|
||||||
|
titleLangKey?: string; // 탭 제목 다국어 키
|
||||||
panelHeaderHeight?: number;
|
panelHeaderHeight?: number;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
dataSource?: string;
|
dataSource?: string;
|
||||||
|
|
@ -107,6 +111,8 @@ export interface SplitPanelLayoutConfig {
|
||||||
// 좌측 패널 설정
|
// 좌측 패널 설정
|
||||||
leftPanel: {
|
leftPanel: {
|
||||||
title: string;
|
title: string;
|
||||||
|
langKeyId?: number; // 다국어 키 ID
|
||||||
|
langKey?: string; // 다국어 키
|
||||||
panelHeaderHeight?: number; // 패널 상단 헤더 높이 (px)
|
panelHeaderHeight?: number; // 패널 상단 헤더 높이 (px)
|
||||||
tableName?: string; // 데이터베이스 테이블명
|
tableName?: string; // 데이터베이스 테이블명
|
||||||
dataSource?: string; // API 엔드포인트
|
dataSource?: string; // API 엔드포인트
|
||||||
|
|
@ -170,6 +176,8 @@ export interface SplitPanelLayoutConfig {
|
||||||
// 우측 패널 설정
|
// 우측 패널 설정
|
||||||
rightPanel: {
|
rightPanel: {
|
||||||
title: string;
|
title: string;
|
||||||
|
langKeyId?: number; // 다국어 키 ID
|
||||||
|
langKey?: string; // 다국어 키
|
||||||
panelHeaderHeight?: number; // 패널 상단 헤더 높이 (px)
|
panelHeaderHeight?: number; // 패널 상단 헤더 높이 (px)
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
dataSource?: string;
|
dataSource?: string;
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,32 @@ export function extractMultilangLabels(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 분할 패널 컬럼 - columnLabelMap에서 한글 라벨 조회
|
// 6. 분할 패널 제목 및 컬럼 - columnLabelMap에서 한글 라벨 조회
|
||||||
|
// 6-1. 좌측 패널 제목
|
||||||
|
if (config?.leftPanel?.title && typeof config.leftPanel.title === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_left_title`,
|
||||||
|
config.leftPanel.title,
|
||||||
|
"title",
|
||||||
|
compType,
|
||||||
|
`${compLabel} (좌측)`,
|
||||||
|
config.leftPanel.langKeyId,
|
||||||
|
config.leftPanel.langKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 6-2. 우측 패널 제목
|
||||||
|
if (config?.rightPanel?.title && typeof config.rightPanel.title === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_right_title`,
|
||||||
|
config.rightPanel.title,
|
||||||
|
"title",
|
||||||
|
compType,
|
||||||
|
`${compLabel} (우측)`,
|
||||||
|
config.rightPanel.langKeyId,
|
||||||
|
config.rightPanel.langKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 6-3. 좌측 패널 컬럼
|
||||||
const leftTableName = config?.leftPanel?.selectedTable || config?.leftPanel?.tableName || tableName;
|
const leftTableName = config?.leftPanel?.selectedTable || config?.leftPanel?.tableName || tableName;
|
||||||
if (config?.leftPanel?.columns && Array.isArray(config.leftPanel.columns)) {
|
if (config?.leftPanel?.columns && Array.isArray(config.leftPanel.columns)) {
|
||||||
config.leftPanel.columns.forEach((col: any, index: number) => {
|
config.leftPanel.columns.forEach((col: any, index: number) => {
|
||||||
|
|
@ -183,6 +208,56 @@ export function extractMultilangLabels(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 6-5. 추가 탭 (additionalTabs) 제목 및 컬럼 - rightPanel.additionalTabs 확인
|
||||||
|
const additionalTabs = config?.rightPanel?.additionalTabs || config?.additionalTabs;
|
||||||
|
if (additionalTabs && Array.isArray(additionalTabs)) {
|
||||||
|
additionalTabs.forEach((tab: any, tabIndex: number) => {
|
||||||
|
// 탭 라벨
|
||||||
|
if (tab.label && typeof tab.label === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_addtab_${tabIndex}_label`,
|
||||||
|
tab.label,
|
||||||
|
"tab",
|
||||||
|
compType,
|
||||||
|
compLabel,
|
||||||
|
tab.langKeyId,
|
||||||
|
tab.langKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 탭 제목
|
||||||
|
if (tab.title && typeof tab.title === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_addtab_${tabIndex}_title`,
|
||||||
|
tab.title,
|
||||||
|
"title",
|
||||||
|
compType,
|
||||||
|
`${compLabel} (탭: ${tab.label || tabIndex})`,
|
||||||
|
tab.titleLangKeyId,
|
||||||
|
tab.titleLangKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 탭 컬럼
|
||||||
|
const tabTableName = tab.tableName || tab.selectedTable || rightTableName;
|
||||||
|
if (tab.columns && Array.isArray(tab.columns)) {
|
||||||
|
tab.columns.forEach((col: any, colIndex: number) => {
|
||||||
|
const colName = col.columnName || col.field || col.name;
|
||||||
|
const colLabel = columnLabelMap[tabTableName]?.[colName] || col.displayName || col.label || colName;
|
||||||
|
if (colLabel && typeof colLabel === "string") {
|
||||||
|
addLabel(
|
||||||
|
`${comp.id}_addtab_${tabIndex}_col_${colIndex}`,
|
||||||
|
colLabel,
|
||||||
|
"column",
|
||||||
|
compType,
|
||||||
|
`${compLabel} (탭: ${tab.label || tabIndex})`,
|
||||||
|
col.langKeyId,
|
||||||
|
col.langKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 7. 검색 필터
|
// 7. 검색 필터
|
||||||
if (config?.filter?.filters && Array.isArray(config.filter.filters)) {
|
if (config?.filter?.filters && Array.isArray(config.filter.filters)) {
|
||||||
config.filter.filters.forEach((filter: any, index: number) => {
|
config.filter.filters.forEach((filter: any, index: number) => {
|
||||||
|
|
@ -358,9 +433,36 @@ export function applyMultilangMappings(
|
||||||
updated.componentConfig = { ...config, columns: updatedColumns };
|
updated.componentConfig = { ...config, columns: updatedColumns };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 분할 패널 좌측 컬럼 매핑
|
// 분할 패널 좌측 제목 매핑
|
||||||
if (config?.leftPanel?.columns && Array.isArray(config.leftPanel.columns)) {
|
const leftTitleMapping = mappingMap.get(`${comp.id}_left_title`);
|
||||||
const updatedLeftColumns = config.leftPanel.columns.map((col: any, index: number) => {
|
if (leftTitleMapping && config?.leftPanel?.title) {
|
||||||
|
updated.componentConfig = {
|
||||||
|
...updated.componentConfig,
|
||||||
|
leftPanel: {
|
||||||
|
...updated.componentConfig?.leftPanel,
|
||||||
|
langKeyId: leftTitleMapping.keyId,
|
||||||
|
langKey: leftTitleMapping.langKey,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 분할 패널 우측 제목 매핑
|
||||||
|
const rightTitleMapping = mappingMap.get(`${comp.id}_right_title`);
|
||||||
|
if (rightTitleMapping && config?.rightPanel?.title) {
|
||||||
|
updated.componentConfig = {
|
||||||
|
...updated.componentConfig,
|
||||||
|
rightPanel: {
|
||||||
|
...updated.componentConfig?.rightPanel,
|
||||||
|
langKeyId: rightTitleMapping.keyId,
|
||||||
|
langKey: rightTitleMapping.langKey,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 분할 패널 좌측 컬럼 매핑 (이미 업데이트된 leftPanel 사용)
|
||||||
|
const currentLeftPanel = updated.componentConfig?.leftPanel || config?.leftPanel;
|
||||||
|
if (currentLeftPanel?.columns && Array.isArray(currentLeftPanel.columns)) {
|
||||||
|
const updatedLeftColumns = currentLeftPanel.columns.map((col: any, index: number) => {
|
||||||
const colMapping = mappingMap.get(`${comp.id}_left_col_${index}`);
|
const colMapping = mappingMap.get(`${comp.id}_left_col_${index}`);
|
||||||
if (colMapping) {
|
if (colMapping) {
|
||||||
return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey };
|
return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey };
|
||||||
|
|
@ -369,13 +471,14 @@ export function applyMultilangMappings(
|
||||||
});
|
});
|
||||||
updated.componentConfig = {
|
updated.componentConfig = {
|
||||||
...updated.componentConfig,
|
...updated.componentConfig,
|
||||||
leftPanel: { ...config.leftPanel, columns: updatedLeftColumns },
|
leftPanel: { ...currentLeftPanel, columns: updatedLeftColumns },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 분할 패널 우측 컬럼 매핑
|
// 분할 패널 우측 컬럼 매핑 (이미 업데이트된 rightPanel 사용)
|
||||||
if (config?.rightPanel?.columns && Array.isArray(config.rightPanel.columns)) {
|
const currentRightPanel = updated.componentConfig?.rightPanel || config?.rightPanel;
|
||||||
const updatedRightColumns = config.rightPanel.columns.map((col: any, index: number) => {
|
if (currentRightPanel?.columns && Array.isArray(currentRightPanel.columns)) {
|
||||||
|
const updatedRightColumns = currentRightPanel.columns.map((col: any, index: number) => {
|
||||||
const colMapping = mappingMap.get(`${comp.id}_right_col_${index}`);
|
const colMapping = mappingMap.get(`${comp.id}_right_col_${index}`);
|
||||||
if (colMapping) {
|
if (colMapping) {
|
||||||
return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey };
|
return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey };
|
||||||
|
|
@ -384,7 +487,7 @@ export function applyMultilangMappings(
|
||||||
});
|
});
|
||||||
updated.componentConfig = {
|
updated.componentConfig = {
|
||||||
...updated.componentConfig,
|
...updated.componentConfig,
|
||||||
rightPanel: { ...config.rightPanel, columns: updatedRightColumns },
|
rightPanel: { ...currentRightPanel, columns: updatedRightColumns },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,6 +530,52 @@ export function applyMultilangMappings(
|
||||||
updated.componentConfig = { ...updated.componentConfig, tabs: updatedTabs };
|
updated.componentConfig = { ...updated.componentConfig, tabs: updatedTabs };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 추가 탭 (additionalTabs) 매핑 - rightPanel.additionalTabs 또는 additionalTabs 확인
|
||||||
|
const currentRightPanelForAddTabs = updated.componentConfig?.rightPanel || config?.rightPanel;
|
||||||
|
const configAdditionalTabs = currentRightPanelForAddTabs?.additionalTabs || config?.additionalTabs;
|
||||||
|
if (configAdditionalTabs && Array.isArray(configAdditionalTabs)) {
|
||||||
|
const updatedAdditionalTabs = configAdditionalTabs.map((tab: any, tabIndex: number) => {
|
||||||
|
let updatedTab = { ...tab };
|
||||||
|
|
||||||
|
// 탭 라벨 매핑
|
||||||
|
const labelMapping = mappingMap.get(`${comp.id}_addtab_${tabIndex}_label`);
|
||||||
|
if (labelMapping) {
|
||||||
|
updatedTab.langKeyId = labelMapping.keyId;
|
||||||
|
updatedTab.langKey = labelMapping.langKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 탭 제목 매핑
|
||||||
|
const titleMapping = mappingMap.get(`${comp.id}_addtab_${tabIndex}_title`);
|
||||||
|
if (titleMapping) {
|
||||||
|
updatedTab.titleLangKeyId = titleMapping.keyId;
|
||||||
|
updatedTab.titleLangKey = titleMapping.langKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 탭 컬럼 매핑
|
||||||
|
if (tab.columns && Array.isArray(tab.columns)) {
|
||||||
|
updatedTab.columns = tab.columns.map((col: any, colIndex: number) => {
|
||||||
|
const colMapping = mappingMap.get(`${comp.id}_addtab_${tabIndex}_col_${colIndex}`);
|
||||||
|
if (colMapping) {
|
||||||
|
return { ...col, langKeyId: colMapping.keyId, langKey: colMapping.langKey };
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedTab;
|
||||||
|
});
|
||||||
|
|
||||||
|
// rightPanel.additionalTabs에 저장하거나 additionalTabs에 저장
|
||||||
|
if (currentRightPanelForAddTabs?.additionalTabs) {
|
||||||
|
updated.componentConfig = {
|
||||||
|
...updated.componentConfig,
|
||||||
|
rightPanel: { ...currentRightPanelForAddTabs, additionalTabs: updatedAdditionalTabs },
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
updated.componentConfig = { ...updated.componentConfig, additionalTabs: updatedAdditionalTabs };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 액션 버튼 매핑
|
// 액션 버튼 매핑
|
||||||
if (config?.actions?.actions && Array.isArray(config.actions.actions)) {
|
if (config?.actions?.actions && Array.isArray(config.actions.actions)) {
|
||||||
const updatedActions = config.actions.actions.map((action: any, index: number) => {
|
const updatedActions = config.actions.actions.map((action: any, index: number) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue