feat: 화면 관리 및 메뉴 동기화 기능 개선

- 화면 그룹 컨트롤러 기능 확장
- 메뉴 복사 서비스 개선
- 메뉴-화면 동기화 서비스 추가
- 번호 규칙 서비스 개선
- 화면 관리 서비스 확장
- CopyScreenModal 기능 개선
- DataFlowPanel, FieldJoinPanel 수정
This commit is contained in:
DDD1542
2026-01-21 11:53:51 +09:00
parent 40a226ca30
commit ad8b1791bc
15 changed files with 3895 additions and 136 deletions

View File

@@ -16,6 +16,8 @@ export interface MenuCopyResult {
copiedCategoryMappings: number;
copiedTableTypeColumns: number; // 테이블 타입관리 입력타입 설정
copiedCascadingRelations: number; // 연쇄관계 설정
copiedNodeFlows: number; // 노드 플로우 (제어관리)
copiedDataflowDiagrams: number; // 데이터플로우 다이어그램 (버튼 제어)
menuIdMap: Record<number, number>;
screenIdMap: Record<number, number>;
flowIdMap: Record<number, number>;
@@ -983,6 +985,14 @@ export class MenuCopyService {
client
);
// === 2.1단계: 노드 플로우 복사는 화면 복사에서 처리 ===
// (screenManagementService.ts의 copyScreen에서 처리)
const copiedNodeFlows = 0;
// === 2.2단계: 데이터플로우 다이어그램 복사는 화면 복사에서 처리 ===
// (screenManagementService.ts의 copyScreen에서 처리)
const copiedDataflowDiagrams = 0;
// 변수 초기화
let copiedCodeCategories = 0;
let copiedCodes = 0;
@@ -1132,6 +1142,8 @@ export class MenuCopyService {
copiedCategoryMappings,
copiedTableTypeColumns,
copiedCascadingRelations,
copiedNodeFlows,
copiedDataflowDiagrams,
menuIdMap: Object.fromEntries(menuIdMap),
screenIdMap: Object.fromEntries(screenIdMap),
flowIdMap: Object.fromEntries(flowIdMap),
@@ -1144,6 +1156,8 @@ export class MenuCopyService {
- 메뉴: ${result.copiedMenus}
- 화면: ${result.copiedScreens}
- 플로우: ${result.copiedFlows}
- 노드 플로우(제어관리): ${copiedNodeFlows}
- 데이터플로우 다이어그램(버튼 제어): ${copiedDataflowDiagrams}
- 코드 카테고리: ${copiedCodeCategories}
- 코드: ${copiedCodes}
- 채번규칙: ${copiedNumberingRules}
@@ -2556,33 +2570,34 @@ export class MenuCopyService {
}
// 4. 배치 INSERT로 채번 규칙 복사
if (rulesToCopy.length > 0) {
const ruleValues = rulesToCopy
// menu 스코프인데 menu_objid 매핑이 없는 규칙은 제외 (연결 없이 복제하지 않음)
const validRulesToCopy = rulesToCopy.filter((r) => {
if (r.scope_type === "menu") {
const newMenuObjid = menuIdMap.get(r.menu_objid);
if (newMenuObjid === undefined) {
logger.info(` ⏭️ 채번규칙 "${r.rule_name}" 건너뜀: 메뉴 연결 없음 (원본 menu_objid: ${r.menu_objid})`);
// ruleIdMap에서도 제거
ruleIdMap.delete(r.rule_id);
return false; // 복제 대상에서 제외
}
}
return true;
});
if (validRulesToCopy.length > 0) {
const ruleValues = validRulesToCopy
.map(
(_, i) =>
`($${i * 13 + 1}, $${i * 13 + 2}, $${i * 13 + 3}, $${i * 13 + 4}, $${i * 13 + 5}, $${i * 13 + 6}, $${i * 13 + 7}, $${i * 13 + 8}, $${i * 13 + 9}, NOW(), $${i * 13 + 10}, $${i * 13 + 11}, $${i * 13 + 12}, $${i * 13 + 13})`
)
.join(", ");
const ruleParams = rulesToCopy.flatMap((r) => {
const ruleParams = validRulesToCopy.flatMap((r) => {
const newMenuObjid = menuIdMap.get(r.menu_objid);
// scope_type = 'menu'인 경우 menu_objid가 반드시 필요함 (check 제약조건)
// menuIdMap에 없으면 원본 menu_objid가 복사된 메뉴 범위 밖이므로
// scope_type을 'table'로 변경하거나, 매핑이 없으면 null 처리
// menu 스코프인 경우 반드시 menu_objid가 있음 (위에서 필터링됨)
const finalMenuObjid = newMenuObjid !== undefined ? newMenuObjid : null;
// scope_type 결정 로직:
// 1. menu 스코프인데 menu_objid 매핑이 없는 경우
// - table_name이 있으면 'table' 스코프로 변경
// - table_name이 없으면 'global' 스코프로 변경
// 2. 그 외에는 원본 scope_type 유지
let finalScopeType = r.scope_type;
if (r.scope_type === "menu" && finalMenuObjid === null) {
if (r.table_name) {
finalScopeType = "table"; // table_name이 있으면 table 스코프
} else {
finalScopeType = "global"; // table_name도 없으면 global 스코프
}
}
// scope_type은 원본 유지 (menu 스코프는 반드시 menu_objid가 있으므로)
const finalScopeType = r.scope_type;
return [
r.newRuleId,
@@ -2610,8 +2625,8 @@ export class MenuCopyService {
ruleParams
);
copiedCount = rulesToCopy.length;
logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사`);
copiedCount = validRulesToCopy.length;
logger.info(` ✅ 채번 규칙 ${copiedCount}개 복사 (${rulesToCopy.length - validRulesToCopy.length}개 건너뜀)`);
}
// 4-1. 기존 채번 규칙의 menu_objid 업데이트 (새 메뉴와 연결) - 배치 처리
@@ -3324,4 +3339,175 @@ export class MenuCopyService {
logger.info(`✅ 연쇄관계 복사 완료: ${copiedCount}`);
return copiedCount;
}
/**
* 노드 플로우 복사 (node_flows 테이블 - 제어관리에서 사용)
* - 원본 회사의 모든 node_flows를 대상 회사로 복사
* - 대상 회사에 같은 이름의 노드 플로우가 있으면 재사용
* - 없으면 새로 복사 (flow_data 포함)
* - 원본 ID → 새 ID 매핑 반환 (버튼의 flowId, selectedDiagramId 매핑용)
*/
private async copyNodeFlows(
sourceCompanyCode: string,
targetCompanyCode: string,
client: PoolClient
): Promise<{ copiedCount: number; nodeFlowIdMap: Map<number, number> }> {
logger.info(`📋 노드 플로우(제어관리) 복사 시작`);
const nodeFlowIdMap = new Map<number, number>();
let copiedCount = 0;
// 1. 원본 회사의 모든 node_flows 조회
const sourceFlowsResult = await client.query(
`SELECT * FROM node_flows WHERE company_code = $1`,
[sourceCompanyCode]
);
if (sourceFlowsResult.rows.length === 0) {
logger.info(` 📭 원본 회사에 노드 플로우 없음`);
return { copiedCount: 0, nodeFlowIdMap };
}
logger.info(` 📋 원본 노드 플로우: ${sourceFlowsResult.rows.length}`);
// 2. 대상 회사의 기존 노드 플로우 조회 (이름 기준)
const existingFlowsResult = await client.query(
`SELECT flow_id, flow_name FROM node_flows WHERE company_code = $1`,
[targetCompanyCode]
);
const existingFlowsByName = new Map<string, number>(
existingFlowsResult.rows.map((f) => [f.flow_name, f.flow_id])
);
// 3. 복사할 플로우 필터링 + 기존 플로우 매핑
const flowsToCopy: any[] = [];
for (const flow of sourceFlowsResult.rows) {
const existingId = existingFlowsByName.get(flow.flow_name);
if (existingId) {
// 기존 플로우 재사용 - ID 매핑 추가
nodeFlowIdMap.set(flow.flow_id, existingId);
logger.info(` ♻️ 기존 노드 플로우 재사용: ${flow.flow_name} (${flow.flow_id}${existingId})`);
} else {
flowsToCopy.push(flow);
}
}
if (flowsToCopy.length === 0) {
logger.info(` 📭 모든 노드 플로우가 이미 존재함 (매핑 ${nodeFlowIdMap.size}개)`);
return { copiedCount: 0, nodeFlowIdMap };
}
logger.info(` 🔄 복사할 노드 플로우: ${flowsToCopy.length}`);
// 4. 개별 INSERT (RETURNING으로 새 ID 획득)
for (const flow of flowsToCopy) {
const insertResult = await client.query(
`INSERT INTO node_flows (flow_name, flow_description, flow_data, company_code)
VALUES ($1, $2, $3, $4)
RETURNING flow_id`,
[
flow.flow_name,
flow.flow_description,
JSON.stringify(flow.flow_data),
targetCompanyCode,
]
);
const newFlowId = insertResult.rows[0].flow_id;
nodeFlowIdMap.set(flow.flow_id, newFlowId);
logger.info(` 노드 플로우 복사: ${flow.flow_name} (${flow.flow_id}${newFlowId})`);
copiedCount++;
}
logger.info(` ✅ 노드 플로우 복사 완료: ${copiedCount}개, 매핑 ${nodeFlowIdMap.size}`);
return { copiedCount, nodeFlowIdMap };
}
/**
* 데이터플로우 다이어그램 복사 (dataflow_diagrams 테이블 - 버튼 제어 설정에서 사용)
* - 원본 회사의 모든 dataflow_diagrams를 대상 회사로 복사
* - 대상 회사에 같은 이름의 다이어그램이 있으면 재사용
* - 없으면 새로 복사 (relationships, node_positions, control, plan, category 포함)
* - 원본 ID → 새 ID 매핑 반환
*/
private async copyDataflowDiagrams(
sourceCompanyCode: string,
targetCompanyCode: string,
userId: string,
client: PoolClient
): Promise<{ copiedCount: number; diagramIdMap: Map<number, number> }> {
logger.info(`📋 데이터플로우 다이어그램(버튼 제어) 복사 시작`);
const diagramIdMap = new Map<number, number>();
let copiedCount = 0;
// 1. 원본 회사의 모든 dataflow_diagrams 조회
const sourceDiagramsResult = await client.query(
`SELECT * FROM dataflow_diagrams WHERE company_code = $1`,
[sourceCompanyCode]
);
if (sourceDiagramsResult.rows.length === 0) {
logger.info(` 📭 원본 회사에 데이터플로우 다이어그램 없음`);
return { copiedCount: 0, diagramIdMap };
}
logger.info(` 📋 원본 데이터플로우 다이어그램: ${sourceDiagramsResult.rows.length}`);
// 2. 대상 회사의 기존 다이어그램 조회 (이름 기준)
const existingDiagramsResult = await client.query(
`SELECT diagram_id, diagram_name FROM dataflow_diagrams WHERE company_code = $1`,
[targetCompanyCode]
);
const existingDiagramsByName = new Map<string, number>(
existingDiagramsResult.rows.map((d) => [d.diagram_name, d.diagram_id])
);
// 3. 복사할 다이어그램 필터링 + 기존 다이어그램 매핑
const diagramsToCopy: any[] = [];
for (const diagram of sourceDiagramsResult.rows) {
const existingId = existingDiagramsByName.get(diagram.diagram_name);
if (existingId) {
// 기존 다이어그램 재사용 - ID 매핑 추가
diagramIdMap.set(diagram.diagram_id, existingId);
logger.info(` ♻️ 기존 다이어그램 재사용: ${diagram.diagram_name} (${diagram.diagram_id}${existingId})`);
} else {
diagramsToCopy.push(diagram);
}
}
if (diagramsToCopy.length === 0) {
logger.info(` 📭 모든 다이어그램이 이미 존재함 (매핑 ${diagramIdMap.size}개)`);
return { copiedCount: 0, diagramIdMap };
}
logger.info(` 🔄 복사할 다이어그램: ${diagramsToCopy.length}`);
// 4. 개별 INSERT (RETURNING으로 새 ID 획득)
for (const diagram of diagramsToCopy) {
const insertResult = await client.query(
`INSERT INTO dataflow_diagrams (diagram_name, relationships, company_code, created_by, node_positions, control, plan, category)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING diagram_id`,
[
diagram.diagram_name,
JSON.stringify(diagram.relationships),
targetCompanyCode,
userId,
diagram.node_positions ? JSON.stringify(diagram.node_positions) : null,
diagram.control ? JSON.stringify(diagram.control) : null,
diagram.plan ? JSON.stringify(diagram.plan) : null,
diagram.category ? JSON.stringify(diagram.category) : null,
]
);
const newDiagramId = insertResult.rows[0].diagram_id;
diagramIdMap.set(diagram.diagram_id, newDiagramId);
logger.info(` 다이어그램 복사: ${diagram.diagram_name} (${diagram.diagram_id}${newDiagramId})`);
copiedCount++;
}
logger.info(` ✅ 데이터플로우 다이어그램 복사 완료: ${copiedCount}개, 매핑 ${diagramIdMap.size}`);
return { copiedCount, diagramIdMap };
}
}