diff --git a/backend-node/src/controllers/multilangController.ts b/backend-node/src/controllers/multilangController.ts index e88ec912..c77b50d7 100644 --- a/backend-node/src/controllers/multilangController.ts +++ b/backend-node/src/controllers/multilangController.ts @@ -190,7 +190,7 @@ export const getLangKeys = async ( res: Response ): Promise => { try { - const { companyCode, menuCode, keyType, searchText } = req.query; + const { companyCode, menuCode, keyType, searchText, categoryId } = req.query; logger.info("다국어 키 목록 조회 요청", { query: req.query, user: req.user, @@ -202,6 +202,7 @@ export const getLangKeys = async ( menuCode: menuCode as string, keyType: keyType as string, searchText: searchText as string, + categoryId: categoryId ? parseInt(categoryId as string, 10) : undefined, }); const response: ApiResponse = { diff --git a/backend-node/src/services/multilangService.ts b/backend-node/src/services/multilangService.ts index 2e624fef..2944644e 100644 --- a/backend-node/src/services/multilangService.ts +++ b/backend-node/src/services/multilangService.ts @@ -696,6 +696,21 @@ export class MultiLangService { values.push(params.menuCode); } + // 카테고리 필터 (하위 카테고리 포함) + if (params.categoryId) { + whereConditions.push(`category_id IN ( + WITH RECURSIVE category_tree AS ( + SELECT category_id FROM multi_lang_category WHERE category_id = $${paramIndex} + UNION ALL + SELECT c.category_id FROM multi_lang_category c + INNER JOIN category_tree ct ON c.parent_id = ct.category_id + ) + SELECT category_id FROM category_tree + )`); + values.push(params.categoryId); + paramIndex++; + } + // 검색 조건 (OR) if (params.searchText) { whereConditions.push( @@ -717,12 +732,13 @@ export class MultiLangService { lang_key: string; description: string | null; is_active: string | null; + category_id: number | null; created_date: Date | null; created_by: string | null; updated_date: Date | null; updated_by: string | null; }>( - `SELECT key_id, company_code, usage_note, lang_key, description, is_active, + `SELECT key_id, company_code, usage_note, lang_key, description, is_active, category_id, created_date, created_by, updated_date, updated_by FROM multi_lang_key_master ${whereClause} @@ -737,6 +753,7 @@ export class MultiLangService { langKey: key.lang_key, description: key.description || undefined, isActive: key.is_active || "Y", + categoryId: key.category_id || undefined, createdDate: key.created_date || undefined, createdBy: key.created_by || undefined, updatedDate: key.updated_date || undefined, diff --git a/frontend/app/(main)/admin/systemMng/i18nList/page.tsx b/frontend/app/(main)/admin/systemMng/i18nList/page.tsx index 22ca2ae9..79264d72 100644 --- a/frontend/app/(main)/admin/systemMng/i18nList/page.tsx +++ b/frontend/app/(main)/admin/systemMng/i18nList/page.tsx @@ -35,6 +35,7 @@ interface LangKey { langKey: string; description: string; isActive: string; + categoryId?: number; } interface LangText { @@ -102,9 +103,14 @@ export default function I18nPage() { }; // 다국어 키 목록 조회 - const fetchLangKeys = async () => { + const fetchLangKeys = async (categoryId?: number | null) => { try { - const response = await apiClient.get("/multilang/keys"); + const params = new URLSearchParams(); + if (categoryId) { + params.append("categoryId", categoryId.toString()); + } + const url = `/multilang/keys${params.toString() ? `?${params.toString()}` : ""}`; + const response = await apiClient.get(url); const data = response.data; if (data.success) { setLangKeys(data.data); @@ -481,6 +487,13 @@ export default function I18nPage() { initializeData(); }, []); + // 카테고리 변경 시 키 목록 다시 조회 + useEffect(() => { + if (!loading) { + fetchLangKeys(selectedCategory?.categoryId); + } + }, [selectedCategory?.categoryId]); + const columns = [ { id: "select", @@ -879,7 +892,7 @@ export default function I18nPage() { companyCode={user?.companyCode || ""} isSuperAdmin={user?.companyCode === "*"} onSuccess={() => { - fetchLangKeys(); + fetchLangKeys(selectedCategory?.categoryId); }} /> diff --git a/frontend/components/admin/multilang/CategoryTree.tsx b/frontend/components/admin/multilang/CategoryTree.tsx index 66d047a6..2e1238cf 100644 --- a/frontend/components/admin/multilang/CategoryTree.tsx +++ b/frontend/components/admin/multilang/CategoryTree.tsx @@ -26,7 +26,8 @@ function CategoryNode({ onSelectCategory, onDoubleClickCategory, }: CategoryNodeProps) { - const [isExpanded, setIsExpanded] = useState(true); + // 기본값: 접힌 상태로 시작 + const [isExpanded, setIsExpanded] = useState(false); const hasChildren = category.children && category.children.length > 0; const isSelected = selectedCategoryId === category.categoryId; diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 2c7031f0..e31c2eba 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -1460,44 +1460,102 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD try { // 모든 컴포넌트에서 라벨 정보 추출 const labels: Array<{ componentId: string; label: string; type?: string }> = []; + const addedLabels = new Set(); // 중복 방지 + + 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 extractLabels = (components: ComponentData[]) => { components.forEach((comp) => { const anyComp = comp as any; + const config = anyComp.componentConfig; - // 라벨 추출 - if (anyComp.label && typeof anyComp.label === "string" && anyComp.label.trim()) { - labels.push({ - componentId: comp.id, - label: anyComp.label.trim(), - type: "label", - }); + // 1. 기본 라벨 추출 + if (anyComp.label && typeof anyComp.label === "string") { + addLabel(comp.id, anyComp.label, "label"); } - // 제목 추출 (컨테이너, 카드 등) - if (anyComp.title && typeof anyComp.title === "string" && anyComp.title.trim()) { - labels.push({ - componentId: comp.id, - label: anyComp.title.trim(), - type: "title", - }); + // 2. 제목 추출 (컨테이너, 카드 등) + if (anyComp.title && typeof anyComp.title === "string") { + addLabel(comp.id, anyComp.title, "title"); } - // 버튼 텍스트 추출 - if (anyComp.componentConfig?.text && typeof anyComp.componentConfig.text === "string") { - labels.push({ - componentId: comp.id, - label: anyComp.componentConfig.text.trim(), - type: "button", - }); + // 3. 버튼 텍스트 추출 + if (config?.text && typeof config.text === "string") { + addLabel(comp.id, config.text, "button"); } - // placeholder 추출 - if (anyComp.placeholder && typeof anyComp.placeholder === "string" && anyComp.placeholder.trim()) { - labels.push({ - componentId: comp.id, - label: anyComp.placeholder.trim(), - type: "placeholder", + // 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"); + } }); }