feat: 화면 관리 및 메뉴 동기화 기능 개선
- 화면 그룹 컨트롤러 기능 확장 - 메뉴 복사 서비스 개선 - 메뉴-화면 동기화 서비스 추가 - 번호 규칙 서비스 개선 - 화면 관리 서비스 확장 - CopyScreenModal 기능 개선 - DataFlowPanel, FieldJoinPanel 수정
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user