package com.erp.service; import com.erp.common.BaseService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Slf4j public class ScreenGroupService extends BaseService { private static final String NS = "screenGroup."; /** * canonical table / legacy table-list / hidden v2-table-list 위젯 카운트 합산. * screen type inference 시 셋 모두 grid 화면으로 인식해야 한다 (frontend * isTableLikeComponentType 와 동일 정책 — 2026-05-19 canonical cleanup follow-up). */ private static int countTableLikeWidgets(Map widgetCounts) { if (widgetCounts == null) return 0; return widgetCounts.getOrDefault("table", 0) + widgetCounts.getOrDefault("table-list", 0) + widgetCounts.getOrDefault("v2-table-list", 0); } // ══════════════════════════════════════════════════════════════ // Screen Groups // ══════════════════════════════════════════════════════════════ public Map getScreenGroups(Map params) { int page = toInt(params.getOrDefault("page", 1)); int size = toInt(params.getOrDefault("size", 20)); params.put("limit", size); params.put("offset", (page - 1) * size); int total = sqlSession.selectOne(NS + "countScreenGroups", params); List> groups = sqlSession.selectList(NS + "selectScreenGroups", params); // screens 조립 (별도 쿼리) if (!groups.isEmpty()) { List groupIds = groups.stream() .map(g -> g.get("id")) .collect(Collectors.toList()); Map screenParams = new HashMap<>(); screenParams.put("group_ids", groupIds); List> allScreens = sqlSession.selectList(NS + "selectGroupScreensByGroupIds", screenParams); Map>> byGroup = allScreens.stream() .collect(Collectors.groupingBy(s -> s.get("group_id"))); for (Map g : groups) { g.put("screens", byGroup.getOrDefault(g.get("id"), Collections.emptyList())); } } Map result = new LinkedHashMap<>(); result.put("success", true); result.put("data", groups); result.put("total", total); result.put("page", page); result.put("size", size); result.put("total_pages", (int) Math.ceil((double) total / size)); return result; } public Map getScreenGroup(Map params) { Map group = sqlSession.selectOne(NS + "selectScreenGroupById", params); if (group == null) return null; List groupIds = Collections.singletonList(group.get("id")); Map sp = new HashMap<>(); sp.put("group_ids", groupIds); List> screens = sqlSession.selectList(NS + "selectGroupScreensByGroupIds", sp); group.put("screens", screens); return group; } @Transactional public Map createScreenGroup(Map params) { // 부모 그룹 계층 계산 Object parentGroupId = params.get("parent_group_id"); int groupLevel = 0; String parentHierarchyPath = ""; if (parentGroupId != null) { Map pp = new HashMap<>(); pp.put("parent_group_id", parentGroupId); Map parent = sqlSession.selectOne(NS + "selectParentGroupById", pp); if (parent != null) { groupLevel = toInt(parent.getOrDefault("group_level", 0)) + 1; parentHierarchyPath = (String) parent.getOrDefault("hierarchy_path", "/" + parentGroupId + "/"); } } params.put("group_level", groupLevel); sqlSession.insert(NS + "insertScreenGroup", params); Object newId = params.get("id"); // hierarchy_path 업데이트 String hierarchyPath; if (parentGroupId != null && !parentHierarchyPath.isEmpty()) { hierarchyPath = (parentHierarchyPath + newId + "/").replace("//", "/"); } else { hierarchyPath = "/" + newId + "/"; } Map hp = new HashMap<>(); hp.put("id", newId); hp.put("hierarchy_path", hierarchyPath); sqlSession.update(NS + "updateScreenGroupHierarchyPath", hp); Map sp = new HashMap<>(); sp.put("id", newId); return sqlSession.selectOne(NS + "selectScreenGroupById", sp); } @Transactional public Map updateScreenGroup(Map params) { Object id = params.get("id"); String userCompanyCode = (String) params.get("company_code"); Object parentGroupId = params.get("parent_group_id"); // 자기 자신을 부모로 지정하는 것 방지 if (parentGroupId != null && toInt(parentGroupId) == toInt(id)) { throw new IllegalArgumentException("자기 자신을 상위 그룹으로 지정할 수 없습니다."); } // 부모 그룹 계층 재계산 int groupLevel = 0; String hierarchyPath = "/" + id + "/"; if (parentGroupId != null) { Map pp = new HashMap<>(); pp.put("parent_group_id", parentGroupId); Map parent = sqlSession.selectOne(NS + "selectParentGroupById", pp); if (parent != null) { String parentPath = (String) parent.getOrDefault("hierarchy_path", "/" + parentGroupId + "/"); // 순환 참조 방지 if (parentPath != null && parentPath.contains("/" + id + "/")) { throw new IllegalArgumentException("하위 그룹을 상위 그룹으로 지정할 수 없습니다."); } groupLevel = toInt(parent.getOrDefault("group_level", 0)) + 1; hierarchyPath = (parentPath + id + "/").replace("//", "/"); } } params.put("group_level", groupLevel); params.put("hierarchy_path", hierarchyPath); // 최고관리자가 회사 변경하는 경우 Object targetCompanyCode = params.get("target_company_code"); int rows; if ("*".equals(userCompanyCode) && targetCompanyCode != null) { rows = sqlSession.update(NS + "updateScreenGroupWithCompany", params); } else { rows = sqlSession.update(NS + "updateScreenGroup", params); } if (rows == 0) { throw new NoSuchElementException("화면 그룹을 찾을 수 없거나 권한이 없습니다."); } Map sp = new HashMap<>(); sp.put("id", id); return sqlSession.selectOne(NS + "selectScreenGroupById", sp); } @Transactional public void deleteScreenGroup(Map params) { Object id = params.get("id"); String companyCode = (String) params.get("company_code"); boolean deleteNumberingRules = Boolean.TRUE.equals(params.get("delete_numbering_rules")); // 대상 그룹의 company_code 확인 Map target = sqlSession.selectOne(NS + "selectScreenGroupForDelete", params); if (target == null) { throw new NoSuchElementException("화면 그룹을 찾을 수 없습니다."); } String targetCompanyCode = (String) target.get("company_code"); // 권한 체크 if (!"*".equals(companyCode) && !companyCode.equals(targetCompanyCode)) { throw new SecurityException("권한이 없습니다."); } // 하위 그룹 ID 수집 (재귀) Map cp = new HashMap<>(); cp.put("id", id); cp.put("target_company_code", targetCompanyCode); List> children = sqlSession.selectList(NS + "selectAllChildGroupIds", cp); List groupIds = children.stream().map(c -> c.get("id")).collect(Collectors.toList()); if (!groupIds.isEmpty()) { // 연결된 메뉴 조회 및 삭제 Map mp = new HashMap<>(); mp.put("group_ids", groupIds); mp.put("target_company_code", targetCompanyCode); List> menus = sqlSession.selectList(NS + "selectMenusByGroupIds", mp); List menuObjids = menus.stream().map(m -> m.get("objid")).collect(Collectors.toList()); if (!menuObjids.isEmpty()) { Map delp = new HashMap<>(); delp.put("menu_objids", menuObjids); delp.put("target_company_code", targetCompanyCode); sqlSession.delete(NS + "deleteScreenMenuAssignmentsByMenuObjids", delp); sqlSession.delete(NS + "deleteMenusByGroupIds", mp); } // 채번 규칙 삭제 (최상위 그룹 + 명시 요청) if (deleteNumberingRules) { Map rp = new HashMap<>(); rp.put("id", id); int isRoot = sqlSession.selectOne(NS + "isRootGroupById", rp); if (isRoot > 0) { Map nrp = new HashMap<>(); nrp.put("target_company_code", targetCompanyCode); sqlSession.delete(NS + "deleteNumberingRuleParts", nrp); sqlSession.delete(NS + "deleteNumberingRules", nrp); } } } // 그룹 삭제 int deleted = sqlSession.delete(NS + "deleteScreenGroupById", cp); if (deleted == 0) { throw new NoSuchElementException("화면 그룹을 찾을 수 없거나 권한이 없습니다."); } } // ══════════════════════════════════════════════════════════════ // Screen Group Screens // ══════════════════════════════════════════════════════════════ @Transactional public Map addScreenToGroup(Map params) { sqlSession.insert(NS + "insertGroupScreen", params); // 삽입 후 조회 List ids = Collections.singletonList(params.get("group_id")); Map sp = new HashMap<>(); sp.put("group_ids", ids); List> screens = sqlSession.selectList(NS + "selectGroupScreensByGroupIds", sp); return screens.stream() .filter(s -> Objects.equals(s.get("screen_id"), params.get("screen_id"))) .findFirst() .orElse(params); } @Transactional public Map updateScreenInGroup(Map params) { int rows = sqlSession.update(NS + "updateGroupScreen", params); if (rows == 0) throw new NoSuchElementException("연결을 찾을 수 없거나 권한이 없습니다."); return params; } @Transactional public void removeScreenFromGroup(Map params) { int rows = sqlSession.delete(NS + "deleteGroupScreen", params); if (rows == 0) throw new NoSuchElementException("연결을 찾을 수 없거나 권한이 없습니다."); } // ══════════════════════════════════════════════════════════════ // Field Joins // ══════════════════════════════════════════════════════════════ public List> getFieldJoins(Map params) { return sqlSession.selectList(NS + "selectFieldJoins", params); } @Transactional public Map createFieldJoin(Map params) { sqlSession.insert(NS + "insertFieldJoin", params); return params; } @Transactional public Map updateFieldJoin(Map params) { int rows = sqlSession.update(NS + "updateFieldJoin", params); if (rows == 0) throw new NoSuchElementException("필드 조인을 찾을 수 없거나 권한이 없습니다."); return params; } @Transactional public void deleteFieldJoin(Map params) { int rows = sqlSession.delete(NS + "deleteFieldJoin", params); if (rows == 0) throw new NoSuchElementException("필드 조인을 찾을 수 없거나 권한이 없습니다."); } // ══════════════════════════════════════════════════════════════ // Data Flows // ══════════════════════════════════════════════════════════════ public List> getDataFlows(Map params) { return sqlSession.selectList(NS + "selectDataFlows", params); } @Transactional public Map createDataFlow(Map params) { // data_mapping을 JSON 문자열로 변환 convertToJsonString(params, "data_mapping"); sqlSession.insert(NS + "insertDataFlow", params); return params; } @Transactional public Map updateDataFlow(Map params) { convertToJsonString(params, "data_mapping"); int rows = sqlSession.update(NS + "updateDataFlow", params); if (rows == 0) throw new NoSuchElementException("데이터 흐름을 찾을 수 없거나 권한이 없습니다."); return params; } @Transactional public void deleteDataFlow(Map params) { int rows = sqlSession.delete(NS + "deleteDataFlow", params); if (rows == 0) throw new NoSuchElementException("데이터 흐름을 찾을 수 없거나 권한이 없습니다."); } // ══════════════════════════════════════════════════════════════ // Table Relations // ══════════════════════════════════════════════════════════════ public List> getTableRelations(Map params) { return sqlSession.selectList(NS + "selectTableRelations", params); } @Transactional public Map createTableRelation(Map params) { sqlSession.insert(NS + "insertTableRelation", params); return params; } @Transactional public Map updateTableRelation(Map params) { int rows = sqlSession.update(NS + "updateTableRelation", params); if (rows == 0) throw new NoSuchElementException("화면-테이블 관계를 찾을 수 없거나 권한이 없습니다."); return params; } @Transactional public void deleteTableRelation(Map params) { int rows = sqlSession.delete(NS + "deleteTableRelation", params); if (rows == 0) throw new NoSuchElementException("화면-테이블 관계를 찾을 수 없거나 권한이 없습니다."); } // ══════════════════════════════════════════════════════════════ // Layout Summary // ══════════════════════════════════════════════════════════════ public Map getScreenLayoutSummary(Map params) { List> rows = sqlSession.selectList(NS + "selectLayoutComponents", params); Map widgetCounts = new LinkedHashMap<>(); List labels = new ArrayList<>(); List> fields = new ArrayList<>(); for (Map row : rows) { String widgetType = row.get("widget_type") != null ? (String) row.get("widget_type") : "text"; widgetCounts.merge(widgetType, 1, Integer::sum); String label = (String) row.get("label"); if (label != null && !label.equals("기본 버튼")) { labels.add(label); Map field = new LinkedHashMap<>(); field.put("label", label); field.put("widget_type", widgetType); field.put("field_name", row.get("field_name")); fields.add(field); } } // 화면 타입 추론 // table-like (canonical 'table' / legacy 'table-list' / hidden 'v2-table-list') // 어느 것이든 있으면 grid 로 본다. String screenType = "form"; if (countTableLikeWidgets(widgetCounts) > 0) { screenType = "grid"; } else if (widgetCounts.getOrDefault("custom", 0) > 2) { screenType = "dashboard"; } else if (widgetCounts.size() <= 2 && widgetCounts.getOrDefault("button", 0) > 0) { screenType = "action"; } Map data = new LinkedHashMap<>(); data.put("screen_id", toInt(params.get("screen_id"))); data.put("screen_type", screenType); data.put("widget_counts", widgetCounts); data.put("total_components", rows.size()); data.put("fields", fields.subList(0, Math.min(fields.size(), 10))); data.put("labels", labels.subList(0, Math.min(labels.size(), 8))); return data; } public Map getMultipleScreenLayoutSummary(List screenIds) { if (screenIds == null || screenIds.isEmpty()) return new LinkedHashMap<>(); Map params = new HashMap<>(); params.put("screen_ids", screenIds); List> rows = sqlSession.selectList(NS + "selectMultipleLayoutComponents", params); // 화면별 summary 초기화 Map> summaryMap = new LinkedHashMap<>(); for (Integer sid : screenIds) { Map s = new LinkedHashMap<>(); s.put("screen_id", sid); s.put("screen_type", "form"); s.put("widget_counts", new LinkedHashMap()); s.put("total_components", 0); s.put("layout_items", new ArrayList>()); s.put("canvas_width", 0); s.put("canvas_height", 0); summaryMap.put(sid, s); } for (Map row : rows) { int sid = toInt(row.get("screen_id")); Map summary = summaryMap.get(sid); if (summary == null) continue; String componentKind = row.get("component_kind") != null ? (String) row.get("component_kind") : (row.get("widget_type") != null ? (String) row.get("widget_type") : "text"); String widgetType = row.get("widget_type") != null ? (String) row.get("widget_type") : "text"; @SuppressWarnings("unchecked") Map wc = (Map) summary.get("widget_counts"); wc.merge(componentKind, 1, Integer::sum); summary.put("total_components", toInt(summary.get("total_components")) + 1); Map item = new LinkedHashMap<>(); item.put("x", row.getOrDefault("position_x", 0)); item.put("y", row.getOrDefault("position_y", 0)); item.put("width", row.getOrDefault("width", 100)); item.put("height", row.getOrDefault("height", 30)); item.put("component_kind", componentKind); item.put("widget_type", widgetType); item.put("label", row.get("label")); item.put("bind_field", row.get("bind_field")); item.put("used_columns", new ArrayList<>()); item.put("join_columns", new ArrayList<>()); @SuppressWarnings("unchecked") List> layoutItems = (List>) summary.get("layout_items"); layoutItems.add(item); int rightEdge = toInt(row.getOrDefault("position_x", 0)) + toInt(row.getOrDefault("width", 100)); int bottomEdge = toInt(row.getOrDefault("position_y", 0)) + toInt(row.getOrDefault("height", 30)); if (rightEdge > toInt(summary.get("canvas_width"))) summary.put("canvas_width", rightEdge); if (bottomEdge > toInt(summary.get("canvas_height"))) summary.put("canvas_height", bottomEdge); } // 화면 타입 추론 — canonical / legacy / hidden v2 모두 grid 로 인식 summaryMap.values().forEach(summary -> { @SuppressWarnings("unchecked") Map wc = (Map) summary.get("widget_counts"); if (countTableLikeWidgets(wc) > 0) { summary.put("screen_type", "grid"); } else if (wc.getOrDefault("table-search-widget", 0) > 1) { summary.put("screen_type", "dashboard"); } else if (toInt(summary.get("total_components")) <= 5 && wc.getOrDefault("button-primary", 0) > 0) { summary.put("screen_type", "action"); } }); Map result = new LinkedHashMap<>(); summaryMap.forEach((k, v) -> result.put(String.valueOf(k), v)); return result; } // ══════════════════════════════════════════════════════════════ // Sub Tables // ══════════════════════════════════════════════════════════════ public Map getScreenSubTables(List screenIds) { if (screenIds == null || screenIds.isEmpty()) return new LinkedHashMap<>(); Map p = new HashMap<>(); p.put("screen_ids", screenIds); // ── 1. 컴포넌트 config 기반 서브 테이블 수집 ───────────────── List> compRows = sqlSession.selectList(NS + "selectSubTableComponentConfigs", p); // column label lookup 수집 List> columnPairs = new ArrayList<>(); compRows.forEach(row -> { Object fms = row.get("field_mappings"); if (fms instanceof List) { @SuppressWarnings("unchecked") List> fieldMappings = (List>) fms; String mainTable = (String) row.get("main_table"); String subTable = (String) row.get("sub_table"); fieldMappings.forEach(fm -> { if (fm.get("source_field") != null && subTable != null) { columnPairs.add(pairOf(subTable, (String) fm.get("source_field"))); } if (fm.get("target_field") != null && mainTable != null) { columnPairs.add(pairOf(mainTable, (String) fm.get("target_field"))); } }); } }); // column labels 조회 Map colLabelMap = new HashMap<>(); if (!columnPairs.isEmpty()) { Map lp = new HashMap<>(); lp.put("pairs", columnPairs); sqlSession.>selectList(NS + "selectColumnLabelsByPairs", lp) .forEach(r -> colLabelMap.put(r.get("table_name") + "." + r.get("column_name"), (String) r.get("column_label"))); } // screenSubTables 조립 Map> screenSubTables = new LinkedHashMap<>(); compRows.forEach(row -> { int sid = toInt(row.get("screen_id")); String mainTable = (String) row.get("main_table"); String subTable = (String) row.get("sub_table"); if (subTable == null || subTable.equals(mainTable)) return; ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable); @SuppressWarnings("unchecked") List> subTables = (List>) screenSubTables.get(sid).get("sub_tables"); boolean exists = subTables.stream().anyMatch(st -> subTable.equals(st.get("table_name"))); if (exists) return; String componentType = (String) row.get("component_type"); String relationType = inferRelationType(componentType); List> fieldMappings = buildFieldMappings(row, subTable, mainTable, colLabelMap); Map stEntry = new LinkedHashMap<>(); stEntry.put("table_name", subTable); stEntry.put("component_type", componentType); stEntry.put("relation_type", relationType); if (fieldMappings != null) stEntry.put("field_mappings", fieldMappings); subTables.add(stEntry); }); // ── 2. reference_table 기반 참조 서브 테이블 ────────────────── sqlSession.>selectList(NS + "selectReferenceColumns", p).forEach(row -> { int sid = toInt(row.get("screen_id")); String mainTable = (String) row.get("main_table"); String refTable = (String) row.get("reference_table"); if (refTable == null || refTable.equals(mainTable)) return; ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable); @SuppressWarnings("unchecked") List> subTables = (List>) screenSubTables.get(sid).get("sub_tables"); Map existing = subTables.stream() .filter(st -> refTable.equals(st.get("table_name"))).findFirst().orElse(null); Map mapping = new LinkedHashMap<>(); mapping.put("source_field", row.get("column_name")); mapping.put("target_field", row.getOrDefault("reference_column", "id")); mapping.put("source_display_name", row.getOrDefault("source_display_name", row.get("column_name"))); mapping.put("target_display_name", row.getOrDefault("target_display_name", row.getOrDefault("reference_column", "id"))); if (existing != null) { @SuppressWarnings("unchecked") List> fms = (List>) existing.get("field_mappings"); if (fms != null && fms.stream().noneMatch(fm -> row.get("column_name").equals(fm.get("source_field")))) { fms.add(mapping); } } else { Map stEntry = new LinkedHashMap<>(); stEntry.put("table_name", refTable); stEntry.put("component_type", "column_reference"); stEntry.put("relation_type", "reference"); List> fms = new ArrayList<>(); fms.add(mapping); stEntry.put("field_mappings", fms); subTables.add(stEntry); } }); // ── 3. parentDataMapping ─────────────────────────────────── sqlSession.>selectList(NS + "selectParentDataMappingConfigs", p).forEach(row -> { int sid = toInt(row.get("screen_id")); String mainTable = (String) row.get("main_table"); String compType = (String) row.get("component_type"); Object pdm = row.get("parent_data_mapping"); if (!(pdm instanceof List)) return; ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable); @SuppressWarnings("unchecked") List> subTables = (List>) screenSubTables.get(sid).get("sub_tables"); @SuppressWarnings("unchecked") List> mappings = (List>) pdm; mappings.forEach(mapping -> { String sourceTable = (String) mapping.get("source_table"); if (sourceTable == null || sourceTable.equals(mainTable)) return; Map newMapping = new LinkedHashMap<>(); newMapping.put("source_table", sourceTable); newMapping.put("source_field", mapping.getOrDefault("source_field", "")); newMapping.put("target_field", mapping.getOrDefault("target_field", "")); newMapping.put("source_display_name", mapping.getOrDefault("source_field", "")); newMapping.put("target_display_name", mapping.getOrDefault("target_field", "")); Map existing = subTables.stream() .filter(st -> sourceTable.equals(st.get("table_name"))).findFirst().orElse(null); if (existing != null) { @SuppressWarnings("unchecked") List> fms = (List>) existing.computeIfAbsent("field_mappings", k -> new ArrayList<>()); String sf = (String) newMapping.get("source_field"); String tf = (String) newMapping.get("target_field"); boolean exists = fms.stream().anyMatch(fm -> sf.equals(fm.get("source_field")) && tf.equals(fm.get("target_field"))); if (!exists) fms.add(newMapping); } else { Map stEntry = new LinkedHashMap<>(); stEntry.put("table_name", sourceTable); stEntry.put("component_type", compType); stEntry.put("relation_type", "parent_mapping"); List> fms = new ArrayList<>(); fms.add(newMapping); stEntry.put("field_mappings", fms); subTables.add(stEntry); } }); }); // ── 4. rightPanel.relation ───────────────────────────────── List> rpRows = sqlSession.selectList(NS + "selectRightPanelRelations", p); // rightPanel columns에서 dot-notation 참조 테이블 수집 Map> rpJoinedTables = new HashMap<>(); rpRows.forEach(row -> { Object cols = row.get("right_panel_columns"); String rpTable = (String) row.get("right_panel_table"); if (cols instanceof List && rpTable != null) { @SuppressWarnings("unchecked") List> columns = (List>) cols; int sid = toInt(row.get("screen_id")); String key = sid + "_" + rpTable; columns.forEach(col -> { String colName = (String) col.getOrDefault("name", col.getOrDefault("column_name", col.get("field"))); if (colName != null && colName.contains(".")) { rpJoinedTables.computeIfAbsent(key, k -> new HashSet<>()).add(colName.split("\\.")[0]); } }); } }); rpRows.forEach(row -> { int sid = toInt(row.get("screen_id")); String mainTable = (String) row.get("main_table"); String compType = (String) row.get("component_type"); Object relation = row.get("right_panel_relation"); String rpTable = (String) row.get("right_panel_table"); String subTable = rpTable; if (subTable == null && relation instanceof Map) { @SuppressWarnings("unchecked") Map rel = (Map) relation; subTable = (String) rel.getOrDefault("target_table", rel.get("table_name")); } if (subTable == null || subTable.equals(mainTable)) return; ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable); @SuppressWarnings("unchecked") List> subTables = (List>) screenSubTables.get(sid).get("sub_tables"); String key = sid + "_" + subTable; List joinedTables = rpJoinedTables.containsKey(key) ? new ArrayList<>(rpJoinedTables.get(key)) : new ArrayList<>(); List> fieldMappings = new ArrayList<>(); if (relation instanceof Map) { @SuppressWarnings("unchecked") Map rel = (Map) relation; if (rel.get("source_field") != null && rel.get("target_field") != null) { Map fm = new LinkedHashMap<>(); fm.put("source_field", rel.get("source_field")); fm.put("target_field", rel.get("target_field")); fm.put("source_display_name", rel.get("source_field")); fm.put("target_display_name", rel.get("target_field")); fieldMappings.add(fm); } if (rel.get("field_mappings") instanceof List) { @SuppressWarnings("unchecked") List> rfms = (List>) rel.get("field_mappings"); rfms.forEach(rfm -> { Map fm = new LinkedHashMap<>(); fm.put("source_field", rfm.getOrDefault("source_field", rfm.get("source_field"))); fm.put("target_field", rfm.getOrDefault("target_field", rfm.get("target_field"))); fm.put("source_display_name", fm.get("source_field")); fm.put("target_display_name", fm.get("target_field")); fieldMappings.add(fm); }); } } final String subTableFinal = subTable; Map existing = subTables.stream() .filter(st -> subTableFinal.equals(st.get("table_name"))).findFirst().orElse(null); if (existing != null) { @SuppressWarnings("unchecked") List> fms = (List>) existing.computeIfAbsent("field_mappings", k -> new ArrayList<>()); fieldMappings.forEach(fm -> { boolean dup = fms.stream().anyMatch(e -> Objects.equals(fm.get("source_field"), e.get("source_field")) && Objects.equals(fm.get("target_field"), e.get("target_field"))); if (!dup) fms.add(fm); }); } else { Map stEntry = new LinkedHashMap<>(); stEntry.put("table_name", subTable); stEntry.put("component_type", compType); stEntry.put("relation_type", "right_panel_relation"); if (!joinedTables.isEmpty()) stEntry.put("joined_tables", joinedTables); if (!fieldMappings.isEmpty()) stEntry.put("field_mappings", fieldMappings); if (relation instanceof Map) { @SuppressWarnings("unchecked") Map rel = (Map) relation; if (rel.get("type") != null) stEntry.put("original_relation_type", rel.get("type")); if (rel.get("foreign_key") != null) stEntry.put("foreign_key", rel.get("foreign_key")); if (rel.get("left_column") != null) stEntry.put("left_column", rel.get("left_column")); } subTables.add(stEntry); } }); // ── 5. FK 컬럼 조회 ─────────────────────────────────────── Set subTableNamesSet = new HashSet<>(); Set refTableNamesSet = new HashSet<>(); screenSubTables.values().forEach(sd -> { @SuppressWarnings("unchecked") List> sts = (List>) sd.get("sub_tables"); sts.forEach(st -> { Object jt = st.get("joined_tables"); if (jt instanceof List) { @SuppressWarnings("unchecked") List joinedList = (List) jt; subTableNamesSet.add((String) st.get("table_name")); refTableNamesSet.addAll(joinedList); } }); }); if (!subTableNamesSet.isEmpty() && !refTableNamesSet.isEmpty()) { Map fkp = new HashMap<>(); fkp.put("sub_table_names", new ArrayList<>(subTableNamesSet)); fkp.put("ref_table_names", new ArrayList<>(refTableNamesSet)); Map>> joinColRefs = new HashMap<>(); sqlSession.>selectList(NS + "selectFkColumnsForJoinedTables", fkp).forEach(row -> { String tbl = (String) row.get("table_name"); joinColRefs.computeIfAbsent(tbl, k -> new ArrayList<>()); String col = (String) row.get("column_name"); String ref = (String) row.get("reference_table"); boolean dup = joinColRefs.get(tbl).stream().anyMatch(r -> col.equals(r.get("column")) && ref.equals(r.get("ref_table"))); if (!dup) { Map ref2 = new LinkedHashMap<>(); ref2.put("column", col); ref2.put("column_label", row.getOrDefault("column_label", col)); ref2.put("ref_table", ref); ref2.put("ref_table_label", row.getOrDefault("reference_table_label", ref)); ref2.put("ref_column", row.getOrDefault("reference_column", "id")); joinColRefs.get(tbl).add(ref2); } }); screenSubTables.values().forEach(sd -> { @SuppressWarnings("unchecked") List> sts = (List>) sd.get("sub_tables"); sts.forEach(st -> { List> refs = joinColRefs.get(st.get("table_name")); if (refs != null) { st.put("join_columns", refs.stream().map(r -> r.get("column")).collect(Collectors.toList())); st.put("join_column_refs", refs); } }); }); } // ── 6. v2-repeater ──────────────────────────────────────── sqlSession.>selectList(NS + "selectV2Repeaters", p).forEach(row -> { int sid = toInt(row.get("screen_id")); String mainTable = (String) row.get("main_table"); String subTable = (String) row.get("sub_table"); String fk = (String) row.get("foreign_key"); if (subTable == null || subTable.equals(mainTable)) return; ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable); @SuppressWarnings("unchecked") List> sts = (List>) screenSubTables.get(sid).get("sub_tables"); if (sts.stream().noneMatch(st -> subTable.equals(st.get("table_name")))) { Map stEntry = new LinkedHashMap<>(); stEntry.put("table_name", subTable); stEntry.put("component_type", "v2-repeater"); stEntry.put("relation_type", "right_panel_relation"); if (fk != null) { Map fm = new LinkedHashMap<>(); fm.put("source_field", "id"); fm.put("target_field", fk); fm.put("source_display_name", "ID"); fm.put("target_display_name", fk); stEntry.put("field_mappings", Collections.singletonList(fm)); } sts.add(stEntry); } }); // ── 7. v2-bom-tree detailTable ──────────────────────────── sqlSession.>selectList(NS + "selectV2DetailTables", p).forEach(row -> { int sid = toInt(row.get("screen_id")); String mainTable = (String) row.get("main_table"); String subTable = (String) row.get("sub_table"); String fk = (String) row.get("foreign_key"); if (subTable == null || subTable.equals(mainTable)) return; ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable); @SuppressWarnings("unchecked") List> sts = (List>) screenSubTables.get(sid).get("sub_tables"); if (sts.stream().noneMatch(st -> subTable.equals(st.get("table_name")))) { Map stEntry = new LinkedHashMap<>(); stEntry.put("table_name", subTable); stEntry.put("component_type", row.getOrDefault("component_type", "v2-bom-tree")); stEntry.put("relation_type", "right_panel_relation"); if (fk != null) { Map fm = new LinkedHashMap<>(); fm.put("source_field", "id"); fm.put("target_field", fk); fm.put("source_display_name", "ID"); fm.put("target_display_name", fk); stEntry.put("field_mappings", Collections.singletonList(fm)); } sts.add(stEntry); } }); // ── 8. Save Tables ──────────────────────────────────────── sqlSession.>selectList(NS + "selectSaveTableActions", p).forEach(row -> { int sid = toInt(row.get("screen_id")); String mainTable = (String) row.get("main_table"); String actionType = (String) row.get("action_type"); String compType = (String) row.getOrDefault("component_type", "component"); String targetTable = row.get("target_table") != null ? (String) row.get("target_table") : (row.get("transfer_target_table") != null ? (String) row.get("transfer_target_table") : mainTable); if (targetTable == null) return; ensureScreenEntry(screenSubTables, sid, (String) row.get("screen_name"), mainTable); Map sd = screenSubTables.get(sid); @SuppressWarnings("unchecked") List> saveTables = (List>) sd.computeIfAbsent("save_tables", k -> new ArrayList<>()); boolean dup = saveTables.stream().anyMatch(st -> targetTable.equals(st.get("table_name")) && actionType.equals(st.get("save_type"))); if (!dup) { Map stEntry = new LinkedHashMap<>(); stEntry.put("table_name", targetTable); stEntry.put("save_type", actionType); stEntry.put("component_type", compType); stEntry.put("is_main_table", targetTable.equals(mainTable)); saveTables.add(stEntry); } }); // ── 9. 전역 메인 테이블 목록 ────────────────────────────── List globalMainTables = sqlSession.>selectList(NS + "selectGlobalMainTables", p).stream() .map(r -> (String) r.get("main_table")) .filter(t -> t != null && !t.isEmpty()) .collect(Collectors.toList()); Map result = new LinkedHashMap<>(); result.put("data", screenSubTables); result.put("global_main_tables", globalMainTables); return result; } // ══════════════════════════════════════════════════════════════ // POP Groups // ══════════════════════════════════════════════════════════════ public List> getPopScreenGroups(Map params) { List> groups = sqlSession.selectList(NS + "selectPopScreenGroups", params); if (!groups.isEmpty()) { List groupIds = groups.stream().map(g -> g.get("id")).collect(Collectors.toList()); Map sp = new HashMap<>(); sp.put("group_ids", groupIds); List> allScreens = sqlSession.selectList(NS + "selectPopGroupScreens", sp); Map>> byGroup = allScreens.stream() .collect(Collectors.groupingBy(s -> s.get("group_id"))); groups.forEach(g -> g.put("screens", byGroup.getOrDefault(g.get("id"), Collections.emptyList()))); } return groups; } @Transactional public Map createPopScreenGroup(Map params) { String userCompanyCode = (String) params.get("user_company_code"); String effectiveCompanyCode = (String) params.getOrDefault("target_company_code", userCompanyCode); if (!"*".equals(userCompanyCode) && !effectiveCompanyCode.equals(userCompanyCode)) { throw new SecurityException("다른 회사의 그룹을 생성할 권한이 없습니다."); } params.put("company_code", effectiveCompanyCode); // hierarchy_path 계산 Object parentGroupId = params.get("parent_group_id"); String hierarchyPath; if (parentGroupId != null) { Map pp = new HashMap<>(); pp.put("parent_group_id", parentGroupId); Map parent = sqlSession.selectOne(NS + "selectParentGroupById", pp); if (parent != null) { hierarchyPath = parent.get("hierarchy_path") + "/" + params.get("group_code"); } else { hierarchyPath = "POP/" + params.get("group_code"); } } else { hierarchyPath = "POP/" + params.get("group_code"); } params.put("hierarchy_path", hierarchyPath); // 중복 체크 int dupCount = sqlSession.selectOne(NS + "countGroupByCode", params); if (dupCount > 0) { throw new IllegalArgumentException("동일한 그룹코드가 이미 존재합니다."); } sqlSession.insert(NS + "insertPopScreenGroup", params); Map sp = new HashMap<>(); sp.put("id", params.get("id")); return sqlSession.selectOne(NS + "selectScreenGroupById", sp); } @Transactional public Map updatePopScreenGroup(Map params) { Map existing = sqlSession.selectOne(NS + "selectScreenGroupForUpdate", params); if (existing == null) throw new NoSuchElementException("그룹을 찾을 수 없습니다."); String hierarchyPath = (String) existing.get("hierarchy_path"); if (hierarchyPath == null || !hierarchyPath.startsWith("POP")) { throw new IllegalArgumentException("POP 그룹만 수정할 수 있습니다."); } sqlSession.update(NS + "updatePopScreenGroup", params); Map sp = new HashMap<>(); sp.put("id", params.get("id")); return sqlSession.selectOne(NS + "selectScreenGroupById", sp); } @Transactional public void deletePopScreenGroup(Map params) { Map existing = sqlSession.selectOne(NS + "selectScreenGroupForUpdate", params); if (existing == null) { Map any = sqlSession.selectOne(NS + "selectAnyScreenGroupById", params); if (any != null) { String ownerCode = (String) any.get("company_code"); throw new SecurityException("이 그룹은 " + ("*".equals(ownerCode) ? "최고관리자" : ownerCode) + " 소속이라 삭제할 수 없습니다."); } throw new NoSuchElementException("그룹을 찾을 수 없습니다."); } String hierarchyPath = (String) existing.get("hierarchy_path"); if (hierarchyPath == null || !hierarchyPath.startsWith("POP")) { throw new IllegalArgumentException("POP 그룹만 삭제할 수 있습니다."); } int childCount = sqlSession.selectOne(NS + "countChildGroupsByParentId", params); if (childCount > 0) throw new IllegalArgumentException("하위 그룹이 " + childCount + "개 있어 삭제할 수 없습니다. 하위 그룹을 먼저 삭제해주세요."); int screenCount = sqlSession.selectOne(NS + "countGroupScreensByGroupId", params); if (screenCount > 0) throw new IllegalArgumentException("그룹에 연결된 화면이 " + screenCount + "개 있어 삭제할 수 없습니다. 화면을 먼저 제거해주세요."); Map dp = new HashMap<>(); dp.put("id", params.get("id")); dp.put("target_company_code", existing.get("company_code")); sqlSession.delete(NS + "deleteScreenGroupById", dp); } public Map ensurePopRootGroup(Map params) { String companyCode = (String) params.get("company_code"); Map existing = sqlSession.selectOne(NS + "selectPopRootGroup", params); if (existing != null) return existing; if (!"*".equals(companyCode)) return null; sqlSession.insert(NS + "insertPopRootGroup", params); return sqlSession.selectOne(NS + "selectPopRootGroup", params); } // ══════════════════════════════════════════════════════════════ // Sync: Screen Groups → Menu // ══════════════════════════════════════════════════════════════ @Transactional public Map syncScreenGroupsToMenu(String companyCode, String userId) { int created = 0, linked = 0, skipped = 0; List errors = new ArrayList<>(); List> details = new ArrayList<>(); try { Map p = new HashMap<>(); p.put("company_code", companyCode); List> groups = sqlSession.selectList(NS + "selectScreenGroupsForSync", p); List> existingMenus = sqlSession.selectList(NS + "selectExistingMenusForSync", p); // path/name → menu 매핑 (screen_group_id 없는 것만) Map> menuByPath = new LinkedHashMap<>(); Map> menuByName = new LinkedHashMap<>(); Set existingMenuObjids = new HashSet<>(); existingMenus.forEach(m -> { existingMenuObjids.add(toLong(m.get("objid"))); if (m.get("screen_group_id") == null) { String mName = toStr(m.get("menu_name_kor")).trim().toLowerCase(); String pName = toStr(m.get("parent_name")).trim().toLowerCase(); String pathKey = !pName.isEmpty() ? pName + ">" + mName : mName; menuByPath.put(pathKey, m); menuByName.putIfAbsent(mName, m); } }); // 사용자 메뉴 루트 확보 Map rootMenu = sqlSession.selectOne(NS + "selectUserMenuRoot", p); long userMenuRootObjid; if (rootMenu != null) { userMenuRootObjid = toLong(rootMenu.get("objid")); } else { long rootObjid = System.currentTimeMillis(); Map rp = new HashMap<>(); rp.put("objid", rootObjid); rp.put("company_code", companyCode); rp.put("user_id", userId); sqlSession.insert(NS + "insertUserMenuRoot", rp); userMenuRootObjid = rootObjid; } // 최상위 회사 폴더 (level=0) 매핑 Map groupToMenuMap = new LinkedHashMap<>(); Map groupIdToName = new LinkedHashMap<>(); Set topLevelFolderIds = new HashSet<>(); groups.forEach(g -> groupIdToName.put(toInt(g.get("id")), toStr(g.get("group_name")).trim().toLowerCase())); groups.forEach(g -> { if (toInt(g.get("group_level")) == 0 && g.get("parent_group_id") == null) { topLevelFolderIds.add(toInt(g.get("id"))); groupToMenuMap.put(toInt(g.get("id")), userMenuRootObjid); } }); long nextObjid = System.currentTimeMillis(); for (Map group : groups) { int gid = toInt(group.get("id")); String gName = toStr(group.get("group_name")).trim(); if (topLevelFolderIds.contains(gid)) { skipped++; details.add(detail("skipped", gName, gid, null, "최상위 회사 폴더 (메뉴 생성 스킵)")); continue; } // 이미 연결된 경우 Object menuObjidObj = group.get("menu_objid"); if (menuObjidObj != null) { if (existingMenuObjids.contains(toLong(menuObjidObj))) { skipped++; details.add(detail("skipped", gName, gid, toLong(menuObjidObj), "이미 메뉴와 연결됨")); groupToMenuMap.put(gid, toLong(menuObjidObj)); continue; } else { Map cp = new HashMap<>(); cp.put("id", gid); sqlSession.update(NS + "clearScreenGroupMenuObjid", cp); } } // 경로 기반 매칭 String parentName = group.get("parent_group_id") != null ? groupIdToName.getOrDefault(toInt(group.get("parent_group_id")), "") : ""; String gNameLower = gName.toLowerCase(); String pathKey = !parentName.isEmpty() ? parentName + ">" + gNameLower : gNameLower; Map matchedMenu = menuByPath.getOrDefault(pathKey, menuByName.get(gNameLower)); if (matchedMenu != null) { long mObjid = toLong(matchedMenu.get("objid")); Map up = new HashMap<>(); up.put("menu_objid", mObjid); up.put("id", gid); sqlSession.update(NS + "updateScreenGroupMenuObjid", up); Map up2 = new HashMap<>(); up2.put("group_id", gid); up2.put("objid", mObjid); sqlSession.update(NS + "updateMenuScreenGroupId", up2); // URL 업데이트 Map dp = new HashMap<>(); dp.put("group_id", gid); dp.put("company_code", companyCode); Map defaultScreen = sqlSession.selectOne(NS + "selectDefaultScreenForGroup", dp); if (defaultScreen != null) { Map urlp = new HashMap<>(); urlp.put("menu_url", "/screens/" + defaultScreen.get("screen_id")); urlp.put("screen_code", defaultScreen.get("screen_code")); urlp.put("objid", mObjid); sqlSession.update(NS + "updateMenuUrlAndScreenCode", urlp); } groupToMenuMap.put(gid, mObjid); linked++; details.add(detail("linked", gName, gid, mObjid, null)); menuByPath.remove(pathKey); menuByName.remove(gNameLower); } else { // 새 메뉴 생성 long newObjid = nextObjid++; long parentMenuObjid = userMenuRootObjid; if (group.get("parent_group_id") != null) { int pgid = toInt(group.get("parent_group_id")); if (groupToMenuMap.containsKey(pgid)) parentMenuObjid = groupToMenuMap.get(pgid); else if (group.get("parent_menu_objid") != null && existingMenuObjids.contains(toLong(group.get("parent_menu_objid")))) { parentMenuObjid = toLong(group.get("parent_menu_objid")); } } Map seqp = new HashMap<>(); seqp.put("parent_objid", parentMenuObjid); seqp.put("company_code", companyCode); int seq = sqlSession.selectOne(NS + "getNextMenuSeqUnderParent", seqp); Map dp = new HashMap<>(); dp.put("group_id", gid); dp.put("company_code", companyCode); Map defScreen = sqlSession.selectOne(NS + "selectDefaultScreenForGroup", dp); String menuUrl = defScreen != null ? "/screens/" + defScreen.get("screen_id") : null; String screenCode = defScreen != null ? (String) defScreen.get("screen_code") : null; Map ins = new HashMap<>(); ins.put("objid", newObjid); ins.put("parent_objid", parentMenuObjid); ins.put("group_name", gName); ins.put("group_code", group.getOrDefault("group_code", gName)); ins.put("seq", seq); ins.put("company_code", companyCode); ins.put("user_id", userId); ins.put("group_id", gid); ins.put("description", group.get("description")); ins.put("menu_url", menuUrl); ins.put("screen_code", screenCode); ins.put("icon", group.get("icon")); sqlSession.insert(NS + "insertMenuForGroup", ins); Map up = new HashMap<>(); up.put("menu_objid", newObjid); up.put("id", gid); sqlSession.update(NS + "updateScreenGroupMenuObjid", up); groupToMenuMap.put(gid, newObjid); created++; details.add(detail("created", gName, gid, newObjid, null)); } } } catch (Exception e) { log.error("화면관리 → 메뉴 동기화 실패", e); errors.add(e.getMessage()); Map r = new LinkedHashMap<>(); r.put("success", false); r.put("created", created); r.put("linked", linked); r.put("skipped", skipped); r.put("errors", errors); r.put("details", details); return r; } Map r = new LinkedHashMap<>(); r.put("success", true); r.put("created", created); r.put("linked", linked); r.put("skipped", skipped); r.put("errors", errors); r.put("details", details); return r; } // ══════════════════════════════════════════════════════════════ // Sync: Menu → Screen Groups // ══════════════════════════════════════════════════════════════ @Transactional public Map syncMenuToScreenGroups(String companyCode, String userId) { int created = 0, linked = 0, skipped = 0; List errors = new ArrayList<>(); List> details = new ArrayList<>(); try { Map p = new HashMap<>(); p.put("company_code", companyCode); // 회사명 조회 Map companyRow = sqlSession.selectOne(NS + "selectCompanyName", p); String companyName = companyRow != null ? toStr(companyRow.get("company_name")) : companyCode; List> menus = sqlSession.selectList(NS + "selectMenusForSync", p); List> groups = sqlSession.selectList(NS + "selectGroupsForSync", p); // path/name → group 매핑 (menu_objid 없는 것만) Map> groupByPath = new LinkedHashMap<>(); Map> groupByName = new LinkedHashMap<>(); Set existingGroupIds = new HashSet<>(); groups.forEach(g -> { existingGroupIds.add(toInt(g.get("id"))); if (g.get("menu_objid") == null) { String gn = toStr(g.get("group_name")).trim().toLowerCase(); String pn = toStr(g.get("parent_name")).trim().toLowerCase(); String pk = !pn.isEmpty() ? pn + ">" + gn : gn; groupByPath.put(pk, g); groupByName.putIfAbsent(gn, g); } }); // 회사 폴더 확보 Map folderRow = sqlSession.selectOne(NS + "selectRootCompanyFolder", p); int companyFolderId; if (folderRow != null) { companyFolderId = toInt(folderRow.get("id")); } else { int maxOrder = sqlSession.selectOne(NS + "getMaxRootDisplayOrder", null); Map fp = new HashMap<>(); fp.put("company_name", companyName); fp.put("group_code", companyCode.toLowerCase()); fp.put("display_order", maxOrder); fp.put("company_code", companyCode); fp.put("user_id", userId); sqlSession.insert(NS + "insertCompanyFolder", fp); companyFolderId = toInt(fp.get("id")); Map hp = new HashMap<>(); hp.put("id", companyFolderId); hp.put("hierarchy_path", "/" + companyFolderId + "/"); sqlSession.update(NS + "updateGroupHierarchyPathById", hp); } Map menuToGroupMap = new LinkedHashMap<>(); menuToGroupMap.put(0L, companyFolderId); for (Map menu : menus) { long mObjid = toLong(menu.get("objid")); String mName = toStr(menu.get("menu_name_kor")).trim(); // 이미 연결된 경우 Object sgId = menu.get("screen_group_id"); if (sgId != null && existingGroupIds.contains(toInt(sgId))) { skipped++; details.add(detail("skipped", mName, mObjid, toInt(sgId), "이미 그룹과 연결됨")); menuToGroupMap.put(mObjid, toInt(sgId)); continue; } // 경로 기반 매칭 String mNameLower = mName.toLowerCase(); String pathKey = mNameLower; Map matchedGroup = groupByPath.getOrDefault(pathKey, groupByName.get(mNameLower)); if (matchedGroup != null) { int gid = toInt(matchedGroup.get("id")); Map up = new HashMap<>(); up.put("menu_objid", mObjid); up.put("id", gid); sqlSession.update(NS + "updateScreenGroupForMenuSync", up); Map up2 = new HashMap<>(); up2.put("group_id", gid); up2.put("objid", mObjid); sqlSession.update(NS + "updateMenuScreenGroupId", up2); menuToGroupMap.put(mObjid, gid); linked++; details.add(detail("linked", mName, mObjid, gid, null)); groupByPath.remove(pathKey); groupByName.remove(mNameLower); } else { // 새 그룹 생성 long parentObjid = toLong(menu.get("parent_obj_id")); int parentGid = menuToGroupMap.getOrDefault(parentObjid, companyFolderId); // 부모 그룹 level + hierarchy_path 조회 Map pp = new HashMap<>(); pp.put("parent_group_id", parentGid); Map parentGroup = sqlSession.selectOne(NS + "selectParentGroupById", pp); int parentLevel = parentGroup != null ? toInt(parentGroup.getOrDefault("group_level", 0)) : 0; String parentPath = parentGroup != null ? toStr(parentGroup.get("hierarchy_path")) : "/" + parentGid + "/"; Map ins = new HashMap<>(); ins.put("group_name", mName); ins.put("group_code", mNameLower.replaceAll("\\s+", "_")); ins.put("parent_group_id", parentGid); ins.put("group_level", parentLevel + 1); ins.put("display_order", toInt(menu.getOrDefault("seq", 0))); ins.put("company_code", companyCode); ins.put("user_id", userId); ins.put("hierarchy_path", parentPath + "0/"); ins.put("menu_objid", mObjid); ins.put("description", menu.get("menu_desc")); sqlSession.insert(NS + "insertScreenGroupForSync", ins); int newGid = toInt(ins.get("id")); String hp = (parentPath + newGid + "/").replace("//", "/"); Map hpu = new HashMap<>(); hpu.put("id", newGid); hpu.put("hierarchy_path", hp); sqlSession.update(NS + "updateGroupHierarchyPathById", hpu); Map up2 = new HashMap<>(); up2.put("group_id", newGid); up2.put("objid", mObjid); sqlSession.update(NS + "updateMenuScreenGroupId", up2); menuToGroupMap.put(mObjid, newGid); existingGroupIds.add(newGid); created++; details.add(detail("created", mName, mObjid, newGid, null)); } } } catch (Exception e) { log.error("메뉴 → 화면관리 동기화 실패", e); errors.add(e.getMessage()); Map r = new LinkedHashMap<>(); r.put("success", false); r.put("created", created); r.put("linked", linked); r.put("skipped", skipped); r.put("errors", errors); r.put("details", details); return r; } Map r = new LinkedHashMap<>(); r.put("success", true); r.put("created", created); r.put("linked", linked); r.put("skipped", skipped); r.put("errors", errors); r.put("details", details); return r; } // ══════════════════════════════════════════════════════════════ // Sync: Status // ══════════════════════════════════════════════════════════════ public Map getSyncStatus(String companyCode) { Map p = new HashMap<>(); p.put("company_code", companyCode); List> groupStats = sqlSession.selectList(NS + "selectSyncStatusGroups", p); List> menuStats = sqlSession.selectList(NS + "selectSyncStatusMenus", p); Map status = new LinkedHashMap<>(); status.put("company_code", companyCode); status.put("groups", groupStats); status.put("menus", menuStats); return status; } // ══════════════════════════════════════════════════════════════ // Sync: All Companies // ══════════════════════════════════════════════════════════════ public Map syncAllCompanies(String userId) { List> companies = sqlSession.selectList(NS + "selectAllCompanyCodes", null); List> results = new ArrayList<>(); int successCount = 0, failedCount = 0; for (Map company : companies) { String code = (String) company.get("company_code"); try { Map r = syncScreenGroupsToMenu(code, userId); r.put("company_code", code); results.add(r); if (Boolean.TRUE.equals(r.get("success"))) successCount++; else failedCount++; } catch (Exception e) { failedCount++; Map r = new LinkedHashMap<>(); r.put("company_code", code); r.put("success", false); r.put("error", e.getMessage()); results.add(r); } } Map res = new LinkedHashMap<>(); res.put("success", failedCount == 0); res.put("total_companies", companies.size()); res.put("success_count", successCount); res.put("failed_count", failedCount); res.put("results", results); return res; } // ══════════════════════════════════════════════════════════════ // Helpers // ══════════════════════════════════════════════════════════════ private void ensureScreenEntry(Map> map, int sid, String screenName, String mainTable) { if (!map.containsKey(sid)) { Map entry = new LinkedHashMap<>(); entry.put("screen_id", sid); entry.put("screen_name", screenName); entry.put("main_table", mainTable != null ? mainTable : ""); entry.put("sub_tables", new ArrayList>()); map.put(sid, entry); } } private String inferRelationType(String componentType) { if (componentType == null) return "lookup"; if (componentType.contains("autocomplete") || componentType.contains("entity-search")) return "lookup"; if (componentType.contains("modal-repeater") || componentType.contains("selected-items")) return "source"; if (componentType.contains("table")) return "join"; return "lookup"; } private List> buildFieldMappings(Map row, String subTable, String mainTable, Map colLabelMap) { Object fmsObj = row.get("field_mappings"); Object colsObj = row.get("columns_config"); List> result = new ArrayList<>(); if (fmsObj instanceof List) { @SuppressWarnings("unchecked") List> fms = (List>) fmsObj; fms.forEach(fm -> { String sf = toStr(fm.getOrDefault("source_field", fm.get("source_field"))); String tf = toStr(fm.getOrDefault("target_field", fm.get("target_field"))); Map m = new LinkedHashMap<>(); m.put("source_field", sf); m.put("target_field", tf); m.put("source_display_name", colLabelMap.getOrDefault(subTable + "." + sf, sf)); m.put("target_display_name", colLabelMap.getOrDefault(mainTable + "." + tf, tf)); result.add(m); }); } else if (colsObj instanceof List) { @SuppressWarnings("unchecked") List> cols = (List>) colsObj; cols.forEach(col -> { Object mapping = col.get("mapping"); if (mapping instanceof Map) { @SuppressWarnings("unchecked") Map mp = (Map) mapping; if ("source".equals(mp.get("type")) && mp.get("source_field") != null) { Map m = new LinkedHashMap<>(); m.put("source_field", col.getOrDefault("field", "")); m.put("target_field", mp.get("source_field")); m.put("source_display_name", col.getOrDefault("label", col.getOrDefault("field", ""))); m.put("target_display_name", mp.get("source_field")); result.add(m); } } }); } return result.isEmpty() ? null : result; } private Map pairOf(String tableName, String columnName) { Map m = new HashMap<>(); m.put("table_name", tableName); m.put("column_name", columnName); return m; } private Map detail(String action, String name, Object sourceId, Object targetId, String reason) { Map d = new LinkedHashMap<>(); d.put("action", action); d.put("source_name", name); d.put("source_id", sourceId); if (targetId != null) d.put("target_id", targetId); if (reason != null) d.put("reason", reason); return d; } private void convertToJsonString(Map params, String key) { Object val = params.get(key); if (val != null && !(val instanceof String)) { params.put(key, val.toString()); } } private int toInt(Object val) { if (val == null) return 0; if (val instanceof Number) return ((Number) val).intValue(); try { return Integer.parseInt(val.toString()); } catch (NumberFormatException e) { return 0; } } private long toLong(Object val) { if (val == null) return 0L; if (val instanceof Number) return ((Number) val).longValue(); try { return Long.parseLong(val.toString()); } catch (NumberFormatException e) { return 0L; } } private String toStr(Object val) { return val != null ? val.toString() : ""; } }