Files
invyone/backend-spring/src/main/java/com/erp/service/ScreenGroupService.java
T
DDD1542 2f398ae0b3 chore: 제어모드 IDE 작업 + v2/legacy 레지스트리 컴포넌트 폐기
- 제어모드 IDE: ControlCardPanel, control/ide/* (Canvas/LeftRail/RightRail/PanZoomStage/V3RuleNode 등), schemas, lib/api/control
- 레지스트리 정리: aggregation-widget, status-count, section-card/paper, table-list(legacy/v2), tabs-widget 폐기 → table/_shared/ 로 통합
- InvLegacyButtonConfigPanel cp 마이그레이션
- canonical data view cleanup 후속 노트
2026-05-19 21:31:03 +09:00

1458 lines
76 KiB
Java

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<String, Integer> 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<String, Object> getScreenGroups(Map<String, Object> 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<Map<String, Object>> groups = sqlSession.selectList(NS + "selectScreenGroups", params);
// screens 조립 (별도 쿼리)
if (!groups.isEmpty()) {
List<Object> groupIds = groups.stream()
.map(g -> g.get("id"))
.collect(Collectors.toList());
Map<String, Object> screenParams = new HashMap<>();
screenParams.put("group_ids", groupIds);
List<Map<String, Object>> allScreens = sqlSession.selectList(NS + "selectGroupScreensByGroupIds", screenParams);
Map<Object, List<Map<String, Object>>> byGroup = allScreens.stream()
.collect(Collectors.groupingBy(s -> s.get("group_id")));
for (Map<String, Object> g : groups) {
g.put("screens", byGroup.getOrDefault(g.get("id"), Collections.emptyList()));
}
}
Map<String, Object> 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<String, Object> getScreenGroup(Map<String, Object> params) {
Map<String, Object> group = sqlSession.selectOne(NS + "selectScreenGroupById", params);
if (group == null) return null;
List<Object> groupIds = Collections.singletonList(group.get("id"));
Map<String, Object> sp = new HashMap<>();
sp.put("group_ids", groupIds);
List<Map<String, Object>> screens = sqlSession.selectList(NS + "selectGroupScreensByGroupIds", sp);
group.put("screens", screens);
return group;
}
@Transactional
public Map<String, Object> createScreenGroup(Map<String, Object> params) {
// 부모 그룹 계층 계산
Object parentGroupId = params.get("parent_group_id");
int groupLevel = 0;
String parentHierarchyPath = "";
if (parentGroupId != null) {
Map<String, Object> pp = new HashMap<>();
pp.put("parent_group_id", parentGroupId);
Map<String, Object> 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<String, Object> hp = new HashMap<>();
hp.put("id", newId);
hp.put("hierarchy_path", hierarchyPath);
sqlSession.update(NS + "updateScreenGroupHierarchyPath", hp);
Map<String, Object> sp = new HashMap<>();
sp.put("id", newId);
return sqlSession.selectOne(NS + "selectScreenGroupById", sp);
}
@Transactional
public Map<String, Object> updateScreenGroup(Map<String, Object> 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<String, Object> pp = new HashMap<>();
pp.put("parent_group_id", parentGroupId);
Map<String, Object> 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<String, Object> sp = new HashMap<>();
sp.put("id", id);
return sqlSession.selectOne(NS + "selectScreenGroupById", sp);
}
@Transactional
public void deleteScreenGroup(Map<String, Object> 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<String, Object> 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<String, Object> cp = new HashMap<>();
cp.put("id", id);
cp.put("target_company_code", targetCompanyCode);
List<Map<String, Object>> children = sqlSession.selectList(NS + "selectAllChildGroupIds", cp);
List<Object> groupIds = children.stream().map(c -> c.get("id")).collect(Collectors.toList());
if (!groupIds.isEmpty()) {
// 연결된 메뉴 조회 및 삭제
Map<String, Object> mp = new HashMap<>();
mp.put("group_ids", groupIds);
mp.put("target_company_code", targetCompanyCode);
List<Map<String, Object>> menus = sqlSession.selectList(NS + "selectMenusByGroupIds", mp);
List<Object> menuObjids = menus.stream().map(m -> m.get("objid")).collect(Collectors.toList());
if (!menuObjids.isEmpty()) {
Map<String, Object> 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<String, Object> rp = new HashMap<>();
rp.put("id", id);
int isRoot = sqlSession.selectOne(NS + "isRootGroupById", rp);
if (isRoot > 0) {
Map<String, Object> 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<String, Object> addScreenToGroup(Map<String, Object> params) {
sqlSession.insert(NS + "insertGroupScreen", params);
// 삽입 후 조회
List<Object> ids = Collections.singletonList(params.get("group_id"));
Map<String, Object> sp = new HashMap<>();
sp.put("group_ids", ids);
List<Map<String, Object>> 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<String, Object> updateScreenInGroup(Map<String, Object> params) {
int rows = sqlSession.update(NS + "updateGroupScreen", params);
if (rows == 0) throw new NoSuchElementException("연결을 찾을 수 없거나 권한이 없습니다.");
return params;
}
@Transactional
public void removeScreenFromGroup(Map<String, Object> params) {
int rows = sqlSession.delete(NS + "deleteGroupScreen", params);
if (rows == 0) throw new NoSuchElementException("연결을 찾을 수 없거나 권한이 없습니다.");
}
// ══════════════════════════════════════════════════════════════
// Field Joins
// ══════════════════════════════════════════════════════════════
public List<Map<String, Object>> getFieldJoins(Map<String, Object> params) {
return sqlSession.selectList(NS + "selectFieldJoins", params);
}
@Transactional
public Map<String, Object> createFieldJoin(Map<String, Object> params) {
sqlSession.insert(NS + "insertFieldJoin", params);
return params;
}
@Transactional
public Map<String, Object> updateFieldJoin(Map<String, Object> params) {
int rows = sqlSession.update(NS + "updateFieldJoin", params);
if (rows == 0) throw new NoSuchElementException("필드 조인을 찾을 수 없거나 권한이 없습니다.");
return params;
}
@Transactional
public void deleteFieldJoin(Map<String, Object> params) {
int rows = sqlSession.delete(NS + "deleteFieldJoin", params);
if (rows == 0) throw new NoSuchElementException("필드 조인을 찾을 수 없거나 권한이 없습니다.");
}
// ══════════════════════════════════════════════════════════════
// Data Flows
// ══════════════════════════════════════════════════════════════
public List<Map<String, Object>> getDataFlows(Map<String, Object> params) {
return sqlSession.selectList(NS + "selectDataFlows", params);
}
@Transactional
public Map<String, Object> createDataFlow(Map<String, Object> params) {
// data_mapping을 JSON 문자열로 변환
convertToJsonString(params, "data_mapping");
sqlSession.insert(NS + "insertDataFlow", params);
return params;
}
@Transactional
public Map<String, Object> updateDataFlow(Map<String, Object> 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<String, Object> params) {
int rows = sqlSession.delete(NS + "deleteDataFlow", params);
if (rows == 0) throw new NoSuchElementException("데이터 흐름을 찾을 수 없거나 권한이 없습니다.");
}
// ══════════════════════════════════════════════════════════════
// Table Relations
// ══════════════════════════════════════════════════════════════
public List<Map<String, Object>> getTableRelations(Map<String, Object> params) {
return sqlSession.selectList(NS + "selectTableRelations", params);
}
@Transactional
public Map<String, Object> createTableRelation(Map<String, Object> params) {
sqlSession.insert(NS + "insertTableRelation", params);
return params;
}
@Transactional
public Map<String, Object> updateTableRelation(Map<String, Object> params) {
int rows = sqlSession.update(NS + "updateTableRelation", params);
if (rows == 0) throw new NoSuchElementException("화면-테이블 관계를 찾을 수 없거나 권한이 없습니다.");
return params;
}
@Transactional
public void deleteTableRelation(Map<String, Object> params) {
int rows = sqlSession.delete(NS + "deleteTableRelation", params);
if (rows == 0) throw new NoSuchElementException("화면-테이블 관계를 찾을 수 없거나 권한이 없습니다.");
}
// ══════════════════════════════════════════════════════════════
// Layout Summary
// ══════════════════════════════════════════════════════════════
public Map<String, Object> getScreenLayoutSummary(Map<String, Object> params) {
List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectLayoutComponents", params);
Map<String, Integer> widgetCounts = new LinkedHashMap<>();
List<String> labels = new ArrayList<>();
List<Map<String, Object>> fields = new ArrayList<>();
for (Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> getMultipleScreenLayoutSummary(List<Integer> screenIds) {
if (screenIds == null || screenIds.isEmpty()) return new LinkedHashMap<>();
Map<String, Object> params = new HashMap<>();
params.put("screen_ids", screenIds);
List<Map<String, Object>> rows = sqlSession.selectList(NS + "selectMultipleLayoutComponents", params);
// 화면별 summary 초기화
Map<Integer, Map<String, Object>> summaryMap = new LinkedHashMap<>();
for (Integer sid : screenIds) {
Map<String, Object> s = new LinkedHashMap<>();
s.put("screen_id", sid);
s.put("screen_type", "form");
s.put("widget_counts", new LinkedHashMap<String, Integer>());
s.put("total_components", 0);
s.put("layout_items", new ArrayList<Map<String, Object>>());
s.put("canvas_width", 0);
s.put("canvas_height", 0);
summaryMap.put(sid, s);
}
for (Map<String, Object> row : rows) {
int sid = toInt(row.get("screen_id"));
Map<String, Object> 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<String, Integer> wc = (Map<String, Integer>) summary.get("widget_counts");
wc.merge(componentKind, 1, Integer::sum);
summary.put("total_components", toInt(summary.get("total_components")) + 1);
Map<String, Object> 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<Map<String, Object>> layoutItems = (List<Map<String, Object>>) 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<String, Integer> wc = (Map<String, Integer>) 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<String, Object> result = new LinkedHashMap<>();
summaryMap.forEach((k, v) -> result.put(String.valueOf(k), v));
return result;
}
// ══════════════════════════════════════════════════════════════
// Sub Tables
// ══════════════════════════════════════════════════════════════
public Map<String, Object> getScreenSubTables(List<Integer> screenIds) {
if (screenIds == null || screenIds.isEmpty()) return new LinkedHashMap<>();
Map<String, Object> p = new HashMap<>();
p.put("screen_ids", screenIds);
// ── 1. 컴포넌트 config 기반 서브 테이블 수집 ─────────────────
List<Map<String, Object>> compRows = sqlSession.selectList(NS + "selectSubTableComponentConfigs", p);
// column label lookup 수집
List<Map<String, Object>> columnPairs = new ArrayList<>();
compRows.forEach(row -> {
Object fms = row.get("field_mappings");
if (fms instanceof List) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> fieldMappings = (List<Map<String, Object>>) 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<String, String> colLabelMap = new HashMap<>();
if (!columnPairs.isEmpty()) {
Map<String, Object> lp = new HashMap<>();
lp.put("pairs", columnPairs);
sqlSession.<Map<String, Object>>selectList(NS + "selectColumnLabelsByPairs", lp)
.forEach(r -> colLabelMap.put(r.get("table_name") + "." + r.get("column_name"),
(String) r.get("column_label")));
}
// screenSubTables 조립
Map<Integer, Map<String, Object>> 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<Map<String, Object>> subTables = (List<Map<String, Object>>)
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<Map<String, Object>> fieldMappings = buildFieldMappings(row, subTable, mainTable, colLabelMap);
Map<String, Object> 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.<Map<String, Object>>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<Map<String, Object>> subTables = (List<Map<String, Object>>)
screenSubTables.get(sid).get("sub_tables");
Map<String, Object> existing = subTables.stream()
.filter(st -> refTable.equals(st.get("table_name"))).findFirst().orElse(null);
Map<String, Object> 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<Map<String, Object>> fms = (List<Map<String, Object>>) 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<String, Object> stEntry = new LinkedHashMap<>();
stEntry.put("table_name", refTable);
stEntry.put("component_type", "column_reference");
stEntry.put("relation_type", "reference");
List<Map<String, Object>> fms = new ArrayList<>();
fms.add(mapping);
stEntry.put("field_mappings", fms);
subTables.add(stEntry);
}
});
// ── 3. parentDataMapping ───────────────────────────────────
sqlSession.<Map<String, Object>>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<Map<String, Object>> subTables = (List<Map<String, Object>>)
screenSubTables.get(sid).get("sub_tables");
@SuppressWarnings("unchecked")
List<Map<String, Object>> mappings = (List<Map<String, Object>>) pdm;
mappings.forEach(mapping -> {
String sourceTable = (String) mapping.get("source_table");
if (sourceTable == null || sourceTable.equals(mainTable)) return;
Map<String, Object> 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<String, Object> existing = subTables.stream()
.filter(st -> sourceTable.equals(st.get("table_name"))).findFirst().orElse(null);
if (existing != null) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> fms = (List<Map<String, Object>>) 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<String, Object> stEntry = new LinkedHashMap<>();
stEntry.put("table_name", sourceTable);
stEntry.put("component_type", compType);
stEntry.put("relation_type", "parent_mapping");
List<Map<String, Object>> fms = new ArrayList<>();
fms.add(newMapping);
stEntry.put("field_mappings", fms);
subTables.add(stEntry);
}
});
});
// ── 4. rightPanel.relation ─────────────────────────────────
List<Map<String, Object>> rpRows = sqlSession.selectList(NS + "selectRightPanelRelations", p);
// rightPanel columns에서 dot-notation 참조 테이블 수집
Map<String, Set<String>> 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<Map<String, Object>> columns = (List<Map<String, Object>>) 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<String, Object> rel = (Map<String, Object>) 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<Map<String, Object>> subTables = (List<Map<String, Object>>)
screenSubTables.get(sid).get("sub_tables");
String key = sid + "_" + subTable;
List<String> joinedTables = rpJoinedTables.containsKey(key)
? new ArrayList<>(rpJoinedTables.get(key)) : new ArrayList<>();
List<Map<String, Object>> fieldMappings = new ArrayList<>();
if (relation instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> rel = (Map<String, Object>) relation;
if (rel.get("source_field") != null && rel.get("target_field") != null) {
Map<String, Object> 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<Map<String, Object>> rfms = (List<Map<String, Object>>) rel.get("field_mappings");
rfms.forEach(rfm -> {
Map<String, Object> 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<String, Object> existing = subTables.stream()
.filter(st -> subTableFinal.equals(st.get("table_name"))).findFirst().orElse(null);
if (existing != null) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> fms = (List<Map<String, Object>>)
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<String, Object> 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<String, Object> rel = (Map<String, Object>) 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<String> subTableNamesSet = new HashSet<>();
Set<String> refTableNamesSet = new HashSet<>();
screenSubTables.values().forEach(sd -> {
@SuppressWarnings("unchecked")
List<Map<String, Object>> sts = (List<Map<String, Object>>) sd.get("sub_tables");
sts.forEach(st -> {
Object jt = st.get("joined_tables");
if (jt instanceof List) {
@SuppressWarnings("unchecked")
List<String> joinedList = (List<String>) jt;
subTableNamesSet.add((String) st.get("table_name"));
refTableNamesSet.addAll(joinedList);
}
});
});
if (!subTableNamesSet.isEmpty() && !refTableNamesSet.isEmpty()) {
Map<String, Object> fkp = new HashMap<>();
fkp.put("sub_table_names", new ArrayList<>(subTableNamesSet));
fkp.put("ref_table_names", new ArrayList<>(refTableNamesSet));
Map<String, List<Map<String, Object>>> joinColRefs = new HashMap<>();
sqlSession.<Map<String, Object>>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<String, Object> 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<Map<String, Object>> sts = (List<Map<String, Object>>) sd.get("sub_tables");
sts.forEach(st -> {
List<Map<String, Object>> 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.<Map<String, Object>>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<Map<String, Object>> sts = (List<Map<String, Object>>)
screenSubTables.get(sid).get("sub_tables");
if (sts.stream().noneMatch(st -> subTable.equals(st.get("table_name")))) {
Map<String, Object> 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<String, Object> 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.<Map<String, Object>>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<Map<String, Object>> sts = (List<Map<String, Object>>)
screenSubTables.get(sid).get("sub_tables");
if (sts.stream().noneMatch(st -> subTable.equals(st.get("table_name")))) {
Map<String, Object> 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<String, Object> 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.<Map<String, Object>>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<String, Object> sd = screenSubTables.get(sid);
@SuppressWarnings("unchecked")
List<Map<String, Object>> saveTables = (List<Map<String, Object>>)
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<String, Object> 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<String> globalMainTables = sqlSession.<Map<String, Object>>selectList(NS + "selectGlobalMainTables", p).stream()
.map(r -> (String) r.get("main_table"))
.filter(t -> t != null && !t.isEmpty())
.collect(Collectors.toList());
Map<String, Object> result = new LinkedHashMap<>();
result.put("data", screenSubTables);
result.put("global_main_tables", globalMainTables);
return result;
}
// ══════════════════════════════════════════════════════════════
// POP Groups
// ══════════════════════════════════════════════════════════════
public List<Map<String, Object>> getPopScreenGroups(Map<String, Object> params) {
List<Map<String, Object>> groups = sqlSession.selectList(NS + "selectPopScreenGroups", params);
if (!groups.isEmpty()) {
List<Object> groupIds = groups.stream().map(g -> g.get("id")).collect(Collectors.toList());
Map<String, Object> sp = new HashMap<>();
sp.put("group_ids", groupIds);
List<Map<String, Object>> allScreens = sqlSession.selectList(NS + "selectPopGroupScreens", sp);
Map<Object, List<Map<String, Object>>> 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<String, Object> createPopScreenGroup(Map<String, Object> 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<String, Object> pp = new HashMap<>();
pp.put("parent_group_id", parentGroupId);
Map<String, Object> 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<String, Object> sp = new HashMap<>();
sp.put("id", params.get("id"));
return sqlSession.selectOne(NS + "selectScreenGroupById", sp);
}
@Transactional
public Map<String, Object> updatePopScreenGroup(Map<String, Object> params) {
Map<String, Object> 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<String, Object> sp = new HashMap<>();
sp.put("id", params.get("id"));
return sqlSession.selectOne(NS + "selectScreenGroupById", sp);
}
@Transactional
public void deletePopScreenGroup(Map<String, Object> params) {
Map<String, Object> existing = sqlSession.selectOne(NS + "selectScreenGroupForUpdate", params);
if (existing == null) {
Map<String, Object> 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<String, Object> 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<String, Object> ensurePopRootGroup(Map<String, Object> params) {
String companyCode = (String) params.get("company_code");
Map<String, Object> 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<String, Object> syncScreenGroupsToMenu(String companyCode, String userId) {
int created = 0, linked = 0, skipped = 0;
List<String> errors = new ArrayList<>();
List<Map<String, Object>> details = new ArrayList<>();
try {
Map<String, Object> p = new HashMap<>();
p.put("company_code", companyCode);
List<Map<String, Object>> groups = sqlSession.selectList(NS + "selectScreenGroupsForSync", p);
List<Map<String, Object>> existingMenus = sqlSession.selectList(NS + "selectExistingMenusForSync", p);
// path/name → menu 매핑 (screen_group_id 없는 것만)
Map<String, Map<String, Object>> menuByPath = new LinkedHashMap<>();
Map<String, Map<String, Object>> menuByName = new LinkedHashMap<>();
Set<Long> 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<String, Object> rootMenu = sqlSession.selectOne(NS + "selectUserMenuRoot", p);
long userMenuRootObjid;
if (rootMenu != null) {
userMenuRootObjid = toLong(rootMenu.get("objid"));
} else {
long rootObjid = System.currentTimeMillis();
Map<String, Object> 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<Integer, Long> groupToMenuMap = new LinkedHashMap<>();
Map<Integer, String> groupIdToName = new LinkedHashMap<>();
Set<Integer> 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<String, Object> 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<String, Object> 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<String, Object> matchedMenu = menuByPath.getOrDefault(pathKey, menuByName.get(gNameLower));
if (matchedMenu != null) {
long mObjid = toLong(matchedMenu.get("objid"));
Map<String, Object> up = new HashMap<>(); up.put("menu_objid", mObjid); up.put("id", gid);
sqlSession.update(NS + "updateScreenGroupMenuObjid", up);
Map<String, Object> up2 = new HashMap<>(); up2.put("group_id", gid); up2.put("objid", mObjid);
sqlSession.update(NS + "updateMenuScreenGroupId", up2);
// URL 업데이트
Map<String, Object> dp = new HashMap<>(); dp.put("group_id", gid); dp.put("company_code", companyCode);
Map<String, Object> defaultScreen = sqlSession.selectOne(NS + "selectDefaultScreenForGroup", dp);
if (defaultScreen != null) {
Map<String, Object> 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<String, Object> seqp = new HashMap<>();
seqp.put("parent_objid", parentMenuObjid);
seqp.put("company_code", companyCode);
int seq = sqlSession.selectOne(NS + "getNextMenuSeqUnderParent", seqp);
Map<String, Object> dp = new HashMap<>(); dp.put("group_id", gid); dp.put("company_code", companyCode);
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> syncMenuToScreenGroups(String companyCode, String userId) {
int created = 0, linked = 0, skipped = 0;
List<String> errors = new ArrayList<>();
List<Map<String, Object>> details = new ArrayList<>();
try {
Map<String, Object> p = new HashMap<>();
p.put("company_code", companyCode);
// 회사명 조회
Map<String, Object> companyRow = sqlSession.selectOne(NS + "selectCompanyName", p);
String companyName = companyRow != null ? toStr(companyRow.get("company_name")) : companyCode;
List<Map<String, Object>> menus = sqlSession.selectList(NS + "selectMenusForSync", p);
List<Map<String, Object>> groups = sqlSession.selectList(NS + "selectGroupsForSync", p);
// path/name → group 매핑 (menu_objid 없는 것만)
Map<String, Map<String, Object>> groupByPath = new LinkedHashMap<>();
Map<String, Map<String, Object>> groupByName = new LinkedHashMap<>();
Set<Integer> 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<String, Object> 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<String, Object> 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<String, Object> hp = new HashMap<>();
hp.put("id", companyFolderId);
hp.put("hierarchy_path", "/" + companyFolderId + "/");
sqlSession.update(NS + "updateGroupHierarchyPathById", hp);
}
Map<Long, Integer> menuToGroupMap = new LinkedHashMap<>();
menuToGroupMap.put(0L, companyFolderId);
for (Map<String, Object> 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<String, Object> matchedGroup = groupByPath.getOrDefault(pathKey, groupByName.get(mNameLower));
if (matchedGroup != null) {
int gid = toInt(matchedGroup.get("id"));
Map<String, Object> up = new HashMap<>(); up.put("menu_objid", mObjid); up.put("id", gid);
sqlSession.update(NS + "updateScreenGroupForMenuSync", up);
Map<String, Object> 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<String, Object> pp = new HashMap<>();
pp.put("parent_group_id", parentGid);
Map<String, Object> 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<String, Object> 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<String, Object> hpu = new HashMap<>();
hpu.put("id", newGid);
hpu.put("hierarchy_path", hp);
sqlSession.update(NS + "updateGroupHierarchyPathById", hpu);
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> getSyncStatus(String companyCode) {
Map<String, Object> p = new HashMap<>();
p.put("company_code", companyCode);
List<Map<String, Object>> groupStats = sqlSession.selectList(NS + "selectSyncStatusGroups", p);
List<Map<String, Object>> menuStats = sqlSession.selectList(NS + "selectSyncStatusMenus", p);
Map<String, Object> status = new LinkedHashMap<>();
status.put("company_code", companyCode);
status.put("groups", groupStats);
status.put("menus", menuStats);
return status;
}
// ══════════════════════════════════════════════════════════════
// Sync: All Companies
// ══════════════════════════════════════════════════════════════
public Map<String, Object> syncAllCompanies(String userId) {
List<Map<String, Object>> companies = sqlSession.selectList(NS + "selectAllCompanyCodes", null);
List<Map<String, Object>> results = new ArrayList<>();
int successCount = 0, failedCount = 0;
for (Map<String, Object> company : companies) {
String code = (String) company.get("company_code");
try {
Map<String, Object> 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<String, Object> r = new LinkedHashMap<>();
r.put("company_code", code); r.put("success", false);
r.put("error", e.getMessage());
results.add(r);
}
}
Map<String, Object> 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<Integer, Map<String, Object>> map, int sid,
String screenName, String mainTable) {
if (!map.containsKey(sid)) {
Map<String, Object> 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<String, Object>>());
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<Map<String, Object>> buildFieldMappings(Map<String, Object> row, String subTable,
String mainTable, Map<String, String> colLabelMap) {
Object fmsObj = row.get("field_mappings");
Object colsObj = row.get("columns_config");
List<Map<String, Object>> result = new ArrayList<>();
if (fmsObj instanceof List) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> fms = (List<Map<String, Object>>) 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<String, Object> 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<Map<String, Object>> cols = (List<Map<String, Object>>) colsObj;
cols.forEach(col -> {
Object mapping = col.get("mapping");
if (mapping instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> mp = (Map<String, Object>) mapping;
if ("source".equals(mp.get("type")) && mp.get("source_field") != null) {
Map<String, Object> 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<String, Object> pairOf(String tableName, String columnName) {
Map<String, Object> m = new HashMap<>();
m.put("table_name", tableName);
m.put("column_name", columnName);
return m;
}
private Map<String, Object> detail(String action, String name, Object sourceId,
Object targetId, String reason) {
Map<String, Object> 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<String, Object> 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() : "";
}
}