package com.erp.service; import com.erp.common.BaseService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; @Service @Slf4j public class MultilangService extends BaseService { private static final String NS = "multilang."; public List> getLanguages() { return sqlSession.selectList(NS + "getMultilangLanguageList", new HashMap<>()); } public Map createLanguage(Map params) { String langCode = str(params.get("lang_code")); Map check = Map.of("lang_code", langCode); if (sqlSession.selectOne(NS + "getMultilangLanguageInfo", check) != null) { throw new IllegalArgumentException("이미 존재하는 언어 코드입니다: " + langCode); } if (params.get("is_active") == null) params.put("is_active", "Y"); if (params.get("sort_order") == null) params.put("sort_order", 0); sqlSession.insert(NS + "insertMultilangLanguage", params); return sqlSession.selectOne(NS + "getMultilangLanguageInfo", check); } public Map updateLanguage(String langCode, Map params) { params.put("lang_code", langCode); Map check = Map.of("lang_code", langCode); if (sqlSession.selectOne(NS + "getMultilangLanguageInfo", check) == null) { throw new IllegalArgumentException("언어를 찾을 수 없습니다: " + langCode); } sqlSession.update(NS + "updateMultilangLanguage", params); return sqlSession.selectOne(NS + "getMultilangLanguageInfo", check); } public String toggleLanguage(String langCode) { Map check = Map.of("lang_code", langCode); Map current = sqlSession.selectOne(NS + "getMultilangLanguageInfo", check); if (current == null) { throw new IllegalArgumentException("언어를 찾을 수 없습니다: " + langCode); } String newStatus = "Y".equals(current.get("is_active")) ? "N" : "Y"; Map params = new HashMap<>(); params.put("lang_code", langCode); params.put("new_status", newStatus); params.put("updated_by", "system"); sqlSession.update(NS + "updateMultilangLanguageStatus", params); return "Y".equals(newStatus) ? "활성화" : "비활성화"; } @Transactional public void deleteLanguage(String langCode) { Map check = Map.of("lang_code", langCode); if (sqlSession.selectOne(NS + "getMultilangLanguageInfo", check) == null) { throw new IllegalArgumentException("언어를 찾을 수 없습니다: " + langCode); } Map params = Map.of("lang_code", langCode); sqlSession.delete(NS + "deleteMultilangTextByLangCode", params); sqlSession.delete(NS + "deleteMultilangLanguage", params); } public List> getLangKeys(Map params) { if (params.containsKey("company_code")) { params.put("filter_company_code", params.get("company_code")); } return sqlSession.selectList(NS + "getMultilangKeyList", params); } public List> getLangTexts(int keyId) { return sqlSession.selectList(NS + "getMultilangTextList", Map.of("key_id", keyId)); } public int createLangKey(Map params) { String companyCode = str(params.get("company_code")); String langKey = str(params.get("lang_key")); Map dupCheck = Map.of("company_code", companyCode, "lang_key", langKey); if (sqlSession.selectOne(NS + "getMultilangKeyByCompanyAndKey", dupCheck) != null) { throw new IllegalArgumentException("동일한 회사에 이미 존재하는 언어키입니다: " + langKey); } if (params.get("is_active") == null) params.put("is_active", "Y"); sqlSession.insert(NS + "insertMultilangKey", params); return toInt(params.get("key_id")); } public void updateLangKey(int keyId, Map params) { params.put("key_id", keyId); if (sqlSession.selectOne(NS + "getMultilangKeyInfo", Map.of("key_id", keyId)) == null) { throw new IllegalArgumentException("다국어 키를 찾을 수 없습니다: " + keyId); } String companyCode = str(params.get("company_code")); String langKey = str(params.get("lang_key")); if (!companyCode.isEmpty() && !langKey.isEmpty()) { Map dupCheck = Map.of("company_code", companyCode, "lang_key", langKey, "key_id", keyId); if (sqlSession.selectOne(NS + "getMultilangKeyByCompanyAndKeyExclude", dupCheck) != null) { throw new IllegalArgumentException("동일한 회사에 이미 존재하는 언어키입니다: " + langKey); } } sqlSession.update(NS + "updateMultilangKey", params); } @Transactional public void deleteLangKey(int keyId) { if (sqlSession.selectOne(NS + "getMultilangKeyInfo", Map.of("key_id", keyId)) == null) { throw new IllegalArgumentException("다국어 키를 찾을 수 없습니다: " + keyId); } Map params = Map.of("key_id", keyId); sqlSession.delete(NS + "deleteMultilangTextByKeyId", params); sqlSession.delete(NS + "deleteMultilangKey", params); } public String toggleLangKey(int keyId) { Map current = sqlSession.selectOne(NS + "getMultilangKeyInfo", Map.of("key_id", keyId)); if (current == null) { throw new IllegalArgumentException("다국어 키를 찾을 수 없습니다: " + keyId); } String newStatus = "Y".equals(current.get("is_active")) ? "N" : "Y"; Map params = new HashMap<>(); params.put("key_id", keyId); params.put("new_status", newStatus); params.put("updated_by", "system"); sqlSession.update(NS + "updateMultilangKeyStatus", params); return "Y".equals(newStatus) ? "활성화" : "비활성화"; } @Transactional public void saveLangTexts(int keyId, List> texts) { if (sqlSession.selectOne(NS + "getMultilangKeyInfo", Map.of("key_id", keyId)) == null) { throw new IllegalArgumentException("다국어 키를 찾을 수 없습니다: " + keyId); } sqlSession.delete(NS + "deleteMultilangTextByKeyId", Map.of("key_id", keyId)); for (Map text : texts) { Map params = new HashMap<>(text); params.put("key_id", keyId); if (params.get("is_active") == null) params.put("is_active", "Y"); sqlSession.insert(NS + "insertMultilangText", params); } } public String getUserText(String companyCode, String menuCode, String langKey, String userLang) { Map params = Map.of( "company_code", companyCode, "menu_code", menuCode, "lang_key", langKey, "user_lang", userLang); Map result = sqlSession.selectOne(NS + "getMultilangUserText", params); return result != null ? str(result.get("lang_text")) : langKey; } public String getLangText(String companyCode, String langKey, String langCode) { Map params = Map.of( "company_code", companyCode, "lang_key", langKey, "lang_code", langCode); Map result = sqlSession.selectOne(NS + "getMultilangSingleText", params); return result != null ? str(result.get("lang_text")) : langKey; } public Map getBatchTranslations(String companyCode, String menuCode, String userLang, List langKeys) { if (langKeys == null || langKeys.isEmpty()) return new LinkedHashMap<>(); Map params = new HashMap<>(); params.put("company_code", companyCode); params.put("menu_code", menuCode); params.put("user_lang", userLang); params.put("lang_keys", langKeys); List> translations = sqlSession.selectList(NS + "getMultilangBatchTranslationList", params); Map result = new LinkedHashMap<>(); Set processed = new HashSet<>(); for (Map t : translations) { String key = str(t.get("lang_key")); if (langKeys.contains(key) && !processed.contains(key)) { result.put(key, str(t.get("lang_text"))); processed.add(key); } } List missingKeys = langKeys.stream().filter(k -> !processed.contains(k)).toList(); if (!missingKeys.isEmpty() && !"KR".equals(userLang)) { Map fallbackParams = new HashMap<>(); fallbackParams.put("company_code", companyCode); fallbackParams.put("missing_keys", missingKeys); List> fallback = sqlSession.selectList(NS + "getMultilangFallbackTranslationList", fallbackParams); Set fbProcessed = new HashSet<>(); for (Map t : fallback) { String key = str(t.get("lang_key")); if (!result.containsKey(key) && !fbProcessed.contains(key)) { result.put(key, str(t.get("lang_text"))); fbProcessed.add(key); } } } for (String key : langKeys) result.putIfAbsent(key, key); return result; } public List> getCategories() { List> flat = sqlSession.selectList(NS + "getMultilangCategoryList", new HashMap<>()); return buildCategoryTree(flat); } public Map getCategoryById(int categoryId) { return sqlSession.selectOne(NS + "getMultilangCategoryInfo", Map.of("category_id", categoryId)); } public List> getCategoryPath(int categoryId) { return sqlSession.selectList(NS + "getMultilangCategoryPath", Map.of("category_id", categoryId)); } private List> buildCategoryTree(List> flat) { Map> byId = new LinkedHashMap<>(); for (Map cat : flat) { Map node = new LinkedHashMap<>(cat); node.put("children", new ArrayList<>()); byId.put(toInt(cat.get("category_id")), node); } List> roots = new ArrayList<>(); for (Map node : byId.values()) { Object parentIdObj = node.get("parent_id"); if (parentIdObj == null) { roots.add(node); } else { int parentId = toInt(parentIdObj); Map parent = byId.get(parentId); if (parent != null) { @SuppressWarnings("unchecked") List> children = (List>) parent.get("children"); children.add(node); } else { roots.add(node); } } } return roots; } @Transactional public int generateKey(Map params) { int categoryId = toInt(params.get("category_id")); String keyMeaning = str(params.get("key_meaning")); String companyCode = str(params.get("company_code")); List> categoryPath = getCategoryPath(categoryId); if (categoryPath.isEmpty()) throw new IllegalArgumentException("존재하지 않는 카테고리입니다"); String langKey = buildLangKey(categoryPath, keyMeaning); if (sqlSession.selectOne(NS + "getMultilangKeyByCompanyAndKey", Map.of("company_code", companyCode, "lang_key", langKey)) != null) { throw new IllegalArgumentException("이미 존재하는 키입니다: " + langKey); } Map insertParams = new HashMap<>(); insertParams.put("company_code", companyCode); insertParams.put("lang_key", langKey); insertParams.put("category_id", categoryId); insertParams.put("key_meaning", keyMeaning); insertParams.put("usage_note", params.get("usage_note")); insertParams.put("description", params.get("usage_note")); insertParams.put("created_by", params.getOrDefault("created_by", "system")); sqlSession.insert(NS + "insertMultilangKeyWithCategory", insertParams); int keyId = toInt(insertParams.get("key_id")); @SuppressWarnings("unchecked") List> texts = (List>) params.get("texts"); if (texts != null) { for (Map text : texts) { Map tp = new HashMap<>(text); tp.put("key_id", keyId); if (tp.get("is_active") == null) tp.put("is_active", "Y"); if (tp.get("created_by") == null) tp.put("created_by", params.getOrDefault("created_by", "system")); if (tp.get("updated_by") == null) tp.put("updated_by", params.getOrDefault("created_by", "system")); sqlSession.insert(NS + "insertMultilangText", tp); } } return keyId; } public Map previewKey(int categoryId, String keyMeaning, String companyCode) { List> categoryPath = getCategoryPath(categoryId); if (categoryPath.isEmpty()) throw new IllegalArgumentException("존재하지 않는 카테고리입니다"); String langKey = buildLangKey(categoryPath, keyMeaning); Map commonKey = sqlSession.selectOne(NS + "getMultilangKeyByCompanyAndKey", Map.of("company_code", "*", "lang_key", langKey)); Map companyKey = sqlSession.selectOne(NS + "getMultilangKeyByCompanyAndKey", Map.of("company_code", companyCode, "lang_key", langKey)); Map result = new LinkedHashMap<>(); result.put("lang_key", langKey); result.put("exists", companyKey != null); result.put("is_override", commonKey != null && companyKey == null); if (commonKey != null) result.put("base_key_id", toInt(commonKey.get("key_id"))); return result; } @Transactional public int createOverrideKey(Map params) { int baseKeyId = toInt(params.get("base_key_id")); String companyCode = str(params.get("company_code")); Map baseKey = sqlSession.selectOne(NS + "getMultilangBaseKeyInfo", Map.of("key_id", baseKeyId)); if (baseKey == null) throw new IllegalArgumentException("원본 키를 찾을 수 없습니다"); if (!"*".equals(str(baseKey.get("company_code")))) throw new IllegalArgumentException("공통 키(*)만 오버라이드 할 수 있습니다"); String langKey = str(baseKey.get("lang_key")); if (sqlSession.selectOne(NS + "getMultilangKeyByCompanyAndKey", Map.of("company_code", companyCode, "lang_key", langKey)) != null) { throw new IllegalArgumentException("이미 해당 회사의 오버라이드 키가 존재합니다"); } Map insertParams = new HashMap<>(); insertParams.put("company_code", companyCode); insertParams.put("lang_key", langKey); insertParams.put("category_id", baseKey.get("category_id")); insertParams.put("key_meaning", baseKey.get("key_meaning")); insertParams.put("base_key_id", baseKeyId); insertParams.put("created_by", params.getOrDefault("created_by", "system")); sqlSession.insert(NS + "insertMultilangOverrideKey", insertParams); int keyId = toInt(insertParams.get("key_id")); @SuppressWarnings("unchecked") List> texts = (List>) params.get("texts"); if (texts != null) { for (Map text : texts) { Map tp = new HashMap<>(text); tp.put("key_id", keyId); if (tp.get("is_active") == null) tp.put("is_active", "Y"); if (tp.get("created_by") == null) tp.put("created_by", params.getOrDefault("created_by", "system")); if (tp.get("updated_by") == null) tp.put("updated_by", params.getOrDefault("created_by", "system")); sqlSession.insert(NS + "insertMultilangText", tp); } } return keyId; } public List> getOverrideKeys(String companyCode) { return sqlSession.selectList(NS + "getMultilangOverrideKeyList", Map.of("company_code", companyCode)); } @Transactional public List> generateScreenLabelKeys(Map params) { int screenId = toInt(params.get("screen_id")); Map screenInfo = sqlSession.selectOne(NS + "getMultilangScreenCompanyCode", Map.of("screen_id", screenId)); String companyCode = (screenInfo != null && !str(screenInfo.get("company_code")).isEmpty()) ? str(screenInfo.get("company_code")) : str(params.getOrDefault("company_code", "*")); String companyName; if ("*".equals(companyCode)) { companyName = "공통"; } else { Map companyInfo = sqlSession.selectOne(NS + "getMultilangCompanyName", Map.of("company_code", companyCode)); companyName = (companyInfo != null) ? str(companyInfo.get("company_name")) : companyCode; } List groupPath = getScreenGroupPath(screenId); int categoryId = ensureScreenGroupCategory(companyCode, companyName, groupPath); List> categoryPath = getCategoryPath(categoryId); List keyPrefixParts = new ArrayList<>(); for (Map c : categoryPath) keyPrefixParts.add(str(c.get("key_prefix"))); @SuppressWarnings("unchecked") List> labels = (List>) params.get("labels"); List> results = new ArrayList<>(); for (Map labelInfo : labels) { String label = str(labelInfo.get("label")); String componentId = str(labelInfo.get("component_id")); String keyMeaning = labelToKeyMeaning(label); List parts = new ArrayList<>(keyPrefixParts); parts.add(keyMeaning); String langKey = String.join(".", parts); Map existing = sqlSession.selectOne(NS + "getMultilangKeyByCompanyAndKey", Map.of("company_code", companyCode, "lang_key", langKey)); int keyId; if (existing != null) { keyId = toInt(existing.get("key_id")); } else { Map insertParams = new HashMap<>(); insertParams.put("company_code", companyCode); insertParams.put("lang_key", langKey); insertParams.put("description", "화면 " + screenId + "의 " + str(labelInfo.getOrDefault("type", "라벨")) + ": " + label); insertParams.put("is_active", "Y"); insertParams.put("category_id", categoryId); insertParams.put("key_meaning", keyMeaning); sqlSession.insert(NS + "insertMultilangKeyWithCategory", insertParams); keyId = toInt(insertParams.get("key_id")); sqlSession.update(NS + "upsertMultilangText", Map.of("key_id", keyId, "lang_code", "KR", "lang_text", label)); } Map entry = new LinkedHashMap<>(); entry.put("component_id", componentId); entry.put("key_id", keyId); entry.put("lang_key", langKey); results.add(entry); } return results; } private List getScreenGroupPath(int screenId) { Map groupRow = sqlSession.selectOne(NS + "getMultilangScreenGroupId", Map.of("screen_id", screenId)); if (groupRow == null) return Collections.emptyList(); int groupId = toInt(groupRow.get("group_id")); List> groups = sqlSession.selectList(NS + "getMultilangScreenGroupPath", Map.of("group_id", groupId)); List path = new ArrayList<>(); for (Map g : groups) path.add(str(g.get("group_name"))); return path; } private int ensureScreenGroupCategory(String companyCode, String companyName, List groupPath) { if (groupPath.isEmpty()) return ensureCompanyCategory(companyCode, companyName); int parentId = ensureCompanyCategory(companyCode, companyName); int currentLevel = 3; for (String groupName : groupPath) { Map existing = sqlSession.selectOne(NS + "getMultilangCategoryByNameAndParent", Map.of("category_name", groupName, "parent_id", parentId)); if (existing != null) { parentId = toInt(existing.get("category_id")); } else { Map insertParams = new HashMap<>(); insertParams.put("category_code", (companyCode + "_GROUP_" + groupName).replace("\\s+", "_")); insertParams.put("category_name", groupName); insertParams.put("parent_id", parentId); insertParams.put("level", currentLevel); insertParams.put("key_prefix", groupName.toLowerCase().replace("\\s+", "_")); insertParams.put("description", groupName + " 화면 그룹의 다국어"); insertParams.put("sort_order", 0); sqlSession.insert(NS + "insertMultilangCategory", insertParams); parentId = toInt(insertParams.get("category_id")); } currentLevel++; } return parentId; } private int ensureCompanyCategory(String companyCode, String companyName) { int screenRootId = ensureScreenRootCategory(); Map existing = sqlSession.selectOne(NS + "getMultilangCategoryByCodeAndParent", Map.of("category_code", companyCode, "parent_id", screenRootId)); if (existing != null) return toInt(existing.get("category_id")); String displayName = "*".equals(companyCode) ? "공통" : companyName; String keyPrefix = "*".equals(companyCode) ? "common" : companyCode.toLowerCase(); Map insertParams = new HashMap<>(); insertParams.put("category_code", companyCode); insertParams.put("category_name", displayName); insertParams.put("parent_id", screenRootId); insertParams.put("level", 2); insertParams.put("key_prefix", keyPrefix); insertParams.put("description", displayName + " 회사의 화면 다국어"); insertParams.put("sort_order", "*".equals(companyCode) ? 0 : 10); sqlSession.insert(NS + "insertMultilangCategory", insertParams); return toInt(insertParams.get("category_id")); } private int ensureScreenRootCategory() { Map existing = sqlSession.selectOne(NS + "getMultilangRootCategoryByCode", Map.of("category_code", "screen")); if (existing != null) return toInt(existing.get("category_id")); Map insertParams = new HashMap<>(); insertParams.put("category_code", "screen"); insertParams.put("category_name", "화면"); insertParams.put("parent_id", null); insertParams.put("level", 1); insertParams.put("key_prefix", "screen"); insertParams.put("description", "화면 디자이너에서 자동 생성된 다국어 키"); insertParams.put("sort_order", 100); sqlSession.insert(NS + "insertMultilangCategory", insertParams); return toInt(insertParams.get("category_id")); } private String buildLangKey(List> categoryPath, String keyMeaning) { List parts = new ArrayList<>(); for (Map c : categoryPath) parts.add(str(c.get("key_prefix"))); parts.add(keyMeaning); return String.join(".", parts); } private String labelToKeyMeaning(String label) { if (label.matches("^[a-z][a-z0-9_]*$")) return label; if (label.matches("^[A-Za-z][A-Za-z0-9 ]*$")) return label.toLowerCase().replace(" ", "_"); return label.replaceAll("[^\\w가-힣\\s]", "").replaceAll("\\s+", "_").toLowerCase(); } private String str(Object o) { return o == null ? "" : o.toString(); } private int toInt(Object o) { if (o == null) return 0; if (o instanceof Number) return ((Number) o).intValue(); try { return Integer.parseInt(o.toString()); } catch (NumberFormatException e) { return 0; } } }