diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index e31c2eba..25fba342 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -78,6 +78,7 @@ import StyleEditor from "./StyleEditor"; import { RealtimePreview } from "./RealtimePreviewDynamic"; import FloatingPanel from "./FloatingPanel"; import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer"; +import { MultilangSettingsModal } from "./modals/MultilangSettingsModal"; import DesignerToolbar from "./DesignerToolbar"; import TablesPanel from "./panels/TablesPanel"; import { TemplatesPanel, TemplateComponent } from "./panels/TemplatesPanel"; @@ -145,6 +146,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD }); const [isSaving, setIsSaving] = useState(false); const [isGeneratingMultilang, setIsGeneratingMultilang] = useState(false); + const [showMultilangSettingsModal, setShowMultilangSettingsModal] = useState(false); // ๐Ÿ†• ํ™”๋ฉด์— ํ• ๋‹น๋œ ๋ฉ”๋‰ด OBJID const [menuObjid, setMenuObjid] = useState(undefined); @@ -4375,6 +4377,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD isSaving={isSaving} onGenerateMultilang={handleGenerateMultilang} isGeneratingMultilang={isGeneratingMultilang} + onOpenMultilangSettings={() => setShowMultilangSettingsModal(true)} /> {/* ๋ฉ”์ธ ์ปจํ…Œ์ด๋„ˆ (์ขŒ์ธก ํˆด๋ฐ” + ํŒจ๋„๋“ค + ์บ”๋ฒ„์Šค) */}
@@ -5157,6 +5160,17 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD screenId={selectedScreen.screenId} /> )} + {/* ๋‹ค๊ตญ์–ด ์„ค์ • ๋ชจ๋‹ฌ */} + setShowMultilangSettingsModal(false)} + components={layout.components} + onSave={(updates) => { + // TODO: ์ปดํฌ๋„ŒํŠธ์— langKeyId ์ €์žฅ ๋กœ์ง ๊ตฌํ˜„ + console.log("๋‹ค๊ตญ์–ด ์„ค์ • ์ €์žฅ:", updates); + toast.success(`${updates.length}๊ฐœ ํ•ญ๋ชฉ์˜ ๋‹ค๊ตญ์–ด ์„ค์ •์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`); + }} + />
diff --git a/frontend/components/screen/modals/MultilangSettingsModal.tsx b/frontend/components/screen/modals/MultilangSettingsModal.tsx new file mode 100644 index 00000000..1f9acc22 --- /dev/null +++ b/frontend/components/screen/modals/MultilangSettingsModal.tsx @@ -0,0 +1,858 @@ +"use client"; + +import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + ChevronRight, + ChevronDown, + Search, + Check, + X, + Languages, + Table2, + LayoutPanelLeft, + Type, + MousePointer, + Filter, + FormInput, + ChevronsUpDown, + Loader2, +} from "lucide-react"; +import { ComponentData } from "@/types/screen"; +import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; + +// ๋‹ค๊ตญ์–ด ํ‚ค ํƒ€์ž… +interface LangKey { + keyId: number; + langKey: string; + description?: string; + categoryName?: string; +} + +// ์ž…๋ ฅ ๊ฐ€๋Šฅํ•œ ํผ ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… ๋ชฉ๋ก +const INPUT_COMPONENT_TYPES = new Set([ + "text-input", + "number-input", + "date-input", + "datetime-input", + "select-input", + "textarea-input", + "checkbox-input", + "radio-input", + "file-input", + "email-input", + "phone-input", + "entity-input", + "code-input", + // ์œ„์ ฏ ํƒ€์ž…๋„ ํฌํ•จ + "widget", + "input", + "field", +]); + +// ์ž…๋ ฅ ๊ฐ€๋Šฅํ•œ ์›นํƒ€์ž… ๋ชฉ๋ก +const INPUT_WEB_TYPES = new Set([ + "text", + "number", + "decimal", + "date", + "datetime", + "select", + "dropdown", + "textarea", + "boolean", + "checkbox", + "radio", + "code", + "entity", + "file", + "email", + "tel", +]); + +// ์ถ”์ถœ๋œ ๋ผ๋ฒจ ํ•ญ๋ชฉ ํƒ€์ž… +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; +} + +// ๊ทธ๋ฃนํ™”๋œ ๋ผ๋ฒจ ํƒ€์ž… +interface LabelGroup { + id: string; + label: string; + type: string; + icon: React.ReactNode; + items: ExtractedLabel[]; + isExpanded: boolean; +} + +interface MultilangSettingsModalProps { + isOpen: boolean; + onClose: () => void; + components: ComponentData[]; + onSave: (updates: Array<{ componentId: string; path?: string; langKeyId: number; langKey: string }>) => void; +} + +// ํƒ€์ž…๋ณ„ ์•„์ด์ฝ˜ ๋งคํ•‘ +const getTypeIcon = (type: string) => { + switch (type) { + case "button": + return ; + case "table-list": + return ; + case "split-panel-layout": + return ; + case "filter": + return ; + case "field": + case "input": + return ; + default: + return ; + } +}; + +// ํƒ€์ž…๋ณ„ ํ•œ๊ธ€ ๋ผ๋ฒจ +const getTypeLabel = (type: string) => { + switch (type) { + case "button": + return "๋ฒ„ํŠผ"; + case "label": + return "๋ผ๋ฒจ"; + case "title": + return "์ œ๋ชฉ"; + case "column": + return "์ปฌ๋Ÿผ"; + case "filter": + return "ํ•„ํ„ฐ"; + case "field": + return "ํ•„๋“œ"; + case "placeholder": + return "ํ”Œ๋ ˆ์ด์Šคํ™€๋”"; + case "tab": + return "ํƒญ"; + case "action": + return "์•ก์…˜"; + default: + return type; + } +}; + +// ๋ผ๋ฒจ ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š” ์—†๋Š” ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… (ํ…Œ์ด๋ธ”, ๋ถ„ํ• ํŒจ๋„ ๋“ฑ) +const NON_INPUT_COMPONENT_TYPES = new Set([ + "table-list", + "split-panel-layout", + "tab-panel", + "container", + "group", + "layout", + "panel", + "card", + "accordion", + "modal", + "drawer", + "form-layout", +]); + +// ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž…๋ ฅ ํผ์ธ์ง€ ํ™•์ธ +const isInputComponent = (comp: any): boolean => { + const compType = comp.componentType || comp.type; + const webType = comp.webType || comp.componentConfig?.webType; + + // ๋ช…์‹œ์ ์œผ๋กœ ์ œ์™ธ๋˜๋Š” ์ปดํฌ๋„ŒํŠธ ํƒ€์ž… + if (NON_INPUT_COMPONENT_TYPES.has(compType)) { + return false; + } + + // ์ปดํฌ๋„ŒํŠธ ํƒ€์ž…์œผ๋กœ ํ™•์ธ + if (INPUT_COMPONENT_TYPES.has(compType)) { + return true; + } + + // ์›นํƒ€์ž…์œผ๋กœ ํ™•์ธ + if (webType && INPUT_WEB_TYPES.has(webType)) { + return true; + } + + return false; +}; + +// ํ‚ค ์„ ํƒ ์ฝค๋ณด๋ฐ•์Šค ์ปดํฌ๋„ŒํŠธ +interface KeySelectComboboxProps { + value?: { keyId: number; langKey: string }; + onChange: (value: { keyId: number; langKey: string } | null) => void; + langKeys: LangKey[]; + isLoading: boolean; +} + +const KeySelectCombobox: React.FC = ({ + value, + onChange, + langKeys, + isLoading, +}) => { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + + {/* ์—ฐ๊ฒฐ ํ•ด์ œ ์˜ต์…˜ */} + {value && ( + { + onChange(null); + setOpen(false); + }} + className="text-red-600" + > + + ์—ฐ๊ฒฐ ํ•ด์ œ + + )} + {langKeys.map((key) => ( + { + onChange({ keyId: key.keyId, langKey: key.langKey }); + setOpen(false); + }} + > + +
+ {key.langKey} + {key.description && ( + {key.description} + )} +
+
+ ))} +
+
+
+
+
+ ); +}; + +export const MultilangSettingsModal: React.FC = ({ + isOpen, + onClose, + components, + onSave, +}) => { + const [searchText, setSearchText] = useState(""); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); + const [selectedItems, setSelectedItems] = useState>(new Map()); + const [extractedLabels, setExtractedLabels] = useState([]); + const [langKeys, setLangKeys] = useState([]); + const [isLoadingKeys, setIsLoadingKeys] = useState(false); + // ํ…Œ์ด๋ธ”๋ณ„ ์ปฌ๋Ÿผ ๋ผ๋ฒจ ๋งคํ•‘ (columnName -> displayName) + const [columnLabelMap, setColumnLabelMap] = useState>>({}); + + // ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ…Œ์ด๋ธ”๋ช… ์ถ”์ถœ + const getTableNamesFromComponents = useCallback((comps: ComponentData[]): Set => { + const tableNames = new Set(); + + 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); + } + }; + + comps.forEach(extractTableName); + return tableNames; + }, []); + + // ์ด๋ฏธ ๋กœ๋“œํ•œ ํ…Œ์ด๋ธ” ์ถ”์  (๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€) + const loadedTablesRef = useRef>(new Set()); + + // ๋‹ค๊ตญ์–ด ํ‚ค ๋ชฉ๋ก ๋กœ๋“œ ๋ฐ ํ…Œ์ด๋ธ”๋ณ„ ์ปฌ๋Ÿผ ๋ผ๋ฒจ ์กฐํšŒ + useEffect(() => { + if (!isOpen) return; + + // ๋‹ค๊ตญ์–ด ํ‚ค ๋ชฉ๋ก ๋กœ๋“œ + const fetchLangKeys = async () => { + setIsLoadingKeys(true); + try { + const response = await apiClient.get("/multilang/keys"); + if (response.data?.success && response.data?.data) { + setLangKeys(response.data.data.map((k: any) => ({ + keyId: k.keyId, + langKey: k.langKey, + description: k.description, + categoryName: k.categoryName, + }))); + } + } catch (error) { + console.error("๋‹ค๊ตญ์–ด ํ‚ค ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:", error); + } finally { + setIsLoadingKeys(false); + } + }; + fetchLangKeys(); + + // ํ…Œ์ด๋ธ”๋ณ„ ์ปฌ๋Ÿผ ๋ผ๋ฒจ ์กฐํšŒ + const tableNames = getTableNamesFromComponents(components); + + // ์ด๋ฏธ ๋กœ๋“œํ•œ ํ…Œ์ด๋ธ”์€ ์ œ์™ธ + const tablesToLoad = Array.from(tableNames).filter((t) => !loadedTablesRef.current.has(t)); + + if (tablesToLoad.length > 0) { + const fetchColumnLabelsForTables = async () => { + const newLabelMap: Record> = {}; + + for (const tableName of tablesToLoad) { + 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)) { + newLabelMap[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) { + newLabelMap[tableName][colName] = colLabel; + } + }); + loadedTablesRef.current.add(tableName); + } + } + } catch (error) { + console.error(`์ปฌ๋Ÿผ ๋ผ๋ฒจ ์กฐํšŒ ์‹คํŒจ (${tableName}):`, error); + } + } + + if (Object.keys(newLabelMap).length > 0) { + setColumnLabelMap((prev) => ({ ...prev, ...newLabelMap })); + } + }; + fetchColumnLabelsForTables(); + } + }, [isOpen, components, getTableNamesFromComponents]); + + // ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ผ๋ฒจ ์ถ”์ถœ + const extractLabelsFromComponents = useCallback((comps: ComponentData[]): ExtractedLabel[] => { + const labels: ExtractedLabel[] = []; + const addedLabels = new Set(); + + 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 ์‚ฌ์šฉ + let 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); + }); + } + }; + + comps.forEach((comp) => extractFromComponent(comp)); + return labels; + }, [columnLabelMap]); + + // ์ปดํฌ๋„ŒํŠธ ๋ณ€๊ฒฝ ์‹œ ๋˜๋Š” columnLabelMap ๋กœ๋“œ ํ›„ ๋ผ๋ฒจ ์žฌ์ถ”์ถœ + useEffect(() => { + if (isOpen && Object.keys(columnLabelMap).length > 0) { + const labels = extractLabelsFromComponents(components); + setExtractedLabels(labels); + + // ๊ธฐ์กด langKeyId๊ฐ€ ์žˆ๋Š” ํ•ญ๋ชฉ๋“ค ์„ ํƒ ์ƒํƒœ๋กœ ์ดˆ๊ธฐํ™” + const initialSelected = new Map(); + labels.forEach((label) => { + if (label.langKeyId) { + initialSelected.set(label.id, { langKeyId: label.langKeyId, langKey: label.langKey }); + } + }); + setSelectedItems(initialSelected); + + // ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ชจ๋“  ๊ทธ๋ฃน ํŽผ์น˜๊ธฐ + const groupIds = new Set(); + labels.forEach((l) => groupIds.add(l.parentType || l.type)); + setExpandedGroups(groupIds); + } else if (isOpen) { + // columnLabelMap ๋กœ๋“œ ์ „์ด๋ผ๋„ ์ผ๋‹จ ์ปดํฌ๋„ŒํŠธ ๋ผ๋ฒจ์€ ํ‘œ์‹œ + const labels = extractLabelsFromComponents(components); + setExtractedLabels(labels); + + const initialSelected = new Map(); + labels.forEach((label) => { + if (label.langKeyId) { + initialSelected.set(label.id, { langKeyId: label.langKeyId, langKey: label.langKey }); + } + }); + setSelectedItems(initialSelected); + + const groupIds = new Set(); + labels.forEach((l) => groupIds.add(l.parentType || l.type)); + setExpandedGroups(groupIds); + } + }, [isOpen, components, extractLabelsFromComponents, columnLabelMap]); + + // ๋ผ๋ฒจ์„ ๋ถ€๋ชจ ํƒ€์ž…๋ณ„๋กœ ๊ทธ๋ฃนํ™” + const groupedLabels = useMemo(() => { + const groups: LabelGroup[] = []; + const groupMap = new Map(); + + extractedLabels.forEach((label) => { + const groupKey = label.parentType || label.type; + if (!groupMap.has(groupKey)) { + groupMap.set(groupKey, []); + } + groupMap.get(groupKey)!.push(label); + }); + + groupMap.forEach((items, key) => { + // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ง + const filteredItems = searchText + ? items.filter( + (item) => + item.label.toLowerCase().includes(searchText.toLowerCase()) || + item.langKey?.toLowerCase().includes(searchText.toLowerCase()) + ) + : items; + + if (filteredItems.length > 0) { + groups.push({ + id: key, + label: items[0].parentLabel || getTypeLabel(key), + type: key, + icon: getTypeIcon(key), + items: filteredItems, + isExpanded: expandedGroups.has(key), + }); + } + }); + + return groups; + }, [extractedLabels, searchText, expandedGroups]); + + // ๊ทธ๋ฃน ํŽผ์นจ/์ ‘๊ธฐ ํ† ๊ธ€ + const toggleGroup = (groupId: string) => { + setExpandedGroups((prev) => { + const next = new Set(prev); + if (next.has(groupId)) { + next.delete(groupId); + } else { + next.add(groupId); + } + return next; + }); + }; + + // ํ‚ค ์„ ํƒ ํ•ธ๋“ค๋Ÿฌ + const handleKeySelect = (itemId: string, key: { keyId: number; langKey: string } | null) => { + setSelectedItems((prev) => { + const next = new Map(prev); + if (key) { + next.set(itemId, { langKeyId: key.keyId, langKey: key.langKey }); + } else { + next.delete(itemId); + } + return next; + }); + }; + + // ์—ฐ๊ฒฐ๋œ ํ‚ค ๊ฐœ์ˆ˜ + const connectedCount = selectedItems.size; + const totalCount = extractedLabels.length; + + return ( + !open && onClose()}> + + + + + ๋‹ค๊ตญ์–ด ์„ค์ • + + {connectedCount} / {totalCount} ์—ฐ๊ฒฐ๋จ + + + + + {/* ๊ฒ€์ƒ‰ */} +
+ + setSearchText(e.target.value)} + className="pl-10" + /> +
+ + {/* ๋ผ๋ฒจ ๋ชฉ๋ก */} + +
+ {groupedLabels.length === 0 ? ( +
+ {searchText ? "๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." : "๋‹ค๊ตญ์–ด ๋Œ€์ƒ์ด ์—†์Šต๋‹ˆ๋‹ค."} +
+ ) : ( + groupedLabels.map((group) => ( +
+ {/* ๊ทธ๋ฃน ํ—ค๋” */} + + + {/* ๊ทธ๋ฃน ์•„์ดํ…œ */} + {group.isExpanded && ( +
+ {group.items.map((item) => { + const isConnected = selectedItems.has(item.id); + const connectedKey = selectedItems.get(item.id); + + return ( +
+ {/* ์ƒํƒœ ์•„์ด์ฝ˜ */} +
+ {isConnected ? ( + + ) : ( + + )} +
+ + {/* ํƒ€์ž… ๋ฐฐ์ง€ */} + + {getTypeLabel(item.type)} + + + {/* ๋ผ๋ฒจ ํ…์ŠคํŠธ */} + {item.label} + + {/* ํ‚ค ์„ ํƒ ์ฝค๋ณด๋ฐ•์Šค */} + handleKeySelect(item.id, key)} + langKeys={langKeys} + isLoading={isLoadingKeys} + /> +
+ ); + })} +
+ )} +
+ )) + )} +
+
+ + + + + +
+
+ ); +}; + +export default MultilangSettingsModal; diff --git a/frontend/components/screen/toolbar/SlimToolbar.tsx b/frontend/components/screen/toolbar/SlimToolbar.tsx index a0efdeb2..a6cbc1a6 100644 --- a/frontend/components/screen/toolbar/SlimToolbar.tsx +++ b/frontend/components/screen/toolbar/SlimToolbar.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Button } from "@/components/ui/button"; -import { Database, ArrowLeft, Save, Monitor, Smartphone, Languages } from "lucide-react"; +import { Database, ArrowLeft, Save, Monitor, Smartphone, Languages, Settings2 } from "lucide-react"; import { ScreenResolution } from "@/types/screen"; interface SlimToolbarProps { @@ -15,6 +15,7 @@ interface SlimToolbarProps { onPreview?: () => void; onGenerateMultilang?: () => void; isGeneratingMultilang?: boolean; + onOpenMultilangSettings?: () => void; } export const SlimToolbar: React.FC = ({ @@ -27,6 +28,7 @@ export const SlimToolbar: React.FC = ({ onPreview, onGenerateMultilang, isGeneratingMultilang = false, + onOpenMultilangSettings, }) => { return (
@@ -86,6 +88,17 @@ export const SlimToolbar: React.FC = ({ {isGeneratingMultilang ? "์ƒ์„ฑ ์ค‘..." : "๋‹ค๊ตญ์–ด ์ƒ์„ฑ"} )} + {onOpenMultilangSettings && ( + + )}