From 14802f507fd7041a3070f382de8ac9ca53b03231 Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 21 Nov 2025 15:27:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=B1=84=EB=B2=88=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EB=B3=B5=EC=82=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 새로운 기능: 1. 카테고리 컬럼 매핑(category_column_mapping) 복사 2. 테이블 컬럼 카테고리 값(table_column_category_values) 복사 3. 채번 규칙(numbering_rules) 복사 4. 채번 규칙 파트(numbering_rule_parts) 복사 중복 처리: - 모든 항목: 스킵(Skip) 정책 적용 - 이미 존재하는 데이터는 덮어쓰지 않고 건너뜀 - 카테고리 값: 부모-자식 관계 유지를 위해 기존 ID 매핑 저장 채번 규칙 특징: - 구조(파트)는 그대로 복사 - 순번(current_sequence)은 1부터 초기화 - rule_id는 타임스탬프 기반으로 새로 생성 (항상 고유) 복사 프로세스: - [7단계] 카테고리 설정 복사 - [8단계] 채번 규칙 복사 결과 로그: - 컬럼 매핑, 카테고리 값, 규칙, 파트 개수 표시 - 스킵된 항목 개수도 함께 표시 이제 메뉴 복사 시 카테고리와 채번 규칙도 함께 복사되어 복사한 회사에서 바로 업무를 시작할 수 있습니다. 관련 파일: - backend-node/src/services/menuCopyService.ts --- backend-node/src/services/menuCopyService.ts | 330 +++++++++++++++++++ 1 file changed, 330 insertions(+) diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index 90f49770..5551fa32 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -12,6 +12,8 @@ export interface MenuCopyResult { copiedFlows: number; copiedCategories: number; copiedCodes: number; + copiedCategorySettings: number; + copiedNumberingRules: number; menuIdMap: Record; screenIdMap: Record; flowIdMap: Record; @@ -392,6 +394,88 @@ export class MenuCopyService { return { categories, codes }; } + /** + * 카테고리 설정 수집 + */ + private async collectCategorySettings( + menuObjids: number[], + sourceCompanyCode: string, + client: PoolClient + ): Promise<{ + columnMappings: any[]; + categoryValues: any[]; + }> { + logger.info(`📂 카테고리 설정 수집 시작: ${menuObjids.length}개 메뉴`); + + const columnMappings: any[] = []; + const categoryValues: any[] = []; + + for (const menuObjid of menuObjids) { + // 카테고리 컬럼 매핑 + const mappingsResult = await client.query( + `SELECT * FROM category_column_mapping + WHERE menu_objid = $1 AND company_code = $2`, + [menuObjid, sourceCompanyCode] + ); + columnMappings.push(...mappingsResult.rows); + + // 테이블 컬럼 카테고리 값 + const valuesResult = await client.query( + `SELECT * FROM table_column_category_values + WHERE menu_objid = $1 AND company_code = $2`, + [menuObjid, sourceCompanyCode] + ); + categoryValues.push(...valuesResult.rows); + } + + logger.info( + `✅ 카테고리 설정 수집 완료: 컬럼 매핑 ${columnMappings.length}개, 카테고리 값 ${categoryValues.length}개` + ); + return { columnMappings, categoryValues }; + } + + /** + * 채번 규칙 수집 + */ + private async collectNumberingRules( + menuObjids: number[], + sourceCompanyCode: string, + client: PoolClient + ): Promise<{ + rules: any[]; + parts: any[]; + }> { + logger.info(`📋 채번 규칙 수집 시작: ${menuObjids.length}개 메뉴`); + + const rules: any[] = []; + const parts: any[] = []; + + for (const menuObjid of menuObjids) { + // 채번 규칙 + const rulesResult = await client.query( + `SELECT * FROM numbering_rules + WHERE menu_objid = $1 AND company_code = $2`, + [menuObjid, sourceCompanyCode] + ); + rules.push(...rulesResult.rows); + + // 각 규칙의 파트 + for (const rule of rulesResult.rows) { + const partsResult = await client.query( + `SELECT * FROM numbering_rule_parts + WHERE rule_id = $1 AND company_code = $2`, + [rule.rule_id, sourceCompanyCode] + ); + parts.push(...partsResult.rows); + } + } + + logger.info( + `✅ 채번 규칙 수집 완료: 규칙 ${rules.length}개, 파트 ${parts.length}개` + ); + return { rules, parts }; + } + /** * 다음 메뉴 objid 생성 */ @@ -684,6 +768,18 @@ export class MenuCopyService { client ); + const categorySettings = await this.collectCategorySettings( + menus.map((m) => m.objid), + sourceCompanyCode, + client + ); + + const numberingRules = await this.collectNumberingRules( + menus.map((m) => m.objid), + sourceCompanyCode, + client + ); + logger.info(` 📊 수집 완료: - 메뉴: ${menus.length}개 @@ -691,6 +787,8 @@ export class MenuCopyService { - 플로우: ${flowIds.size}개 - 코드 카테고리: ${codes.categories.length}개 - 코드: ${codes.codes.length}개 + - 카테고리 설정: 컬럼 매핑 ${categorySettings.columnMappings.length}개, 카테고리 값 ${categorySettings.categoryValues.length}개 + - 채번 규칙: 규칙 ${numberingRules.rules.length}개, 파트 ${numberingRules.parts.length}개 `); // === 2단계: 플로우 복사 === @@ -737,6 +835,26 @@ export class MenuCopyService { logger.info("\n📋 [6단계] 코드 복사"); await this.copyCodes(codes, menuIdMap, targetCompanyCode, userId, client); + // === 7단계: 카테고리 설정 복사 === + logger.info("\n📂 [7단계] 카테고리 설정 복사"); + await this.copyCategorySettings( + categorySettings, + menuIdMap, + targetCompanyCode, + userId, + client + ); + + // === 8단계: 채번 규칙 복사 === + logger.info("\n📋 [8단계] 채번 규칙 복사"); + await this.copyNumberingRules( + numberingRules, + menuIdMap, + targetCompanyCode, + userId, + client + ); + // 커밋 await client.query("COMMIT"); logger.info("✅ 트랜잭션 커밋 완료"); @@ -748,6 +866,11 @@ export class MenuCopyService { copiedFlows: flowIdMap.size, copiedCategories: codes.categories.length, copiedCodes: codes.codes.length, + copiedCategorySettings: + categorySettings.columnMappings.length + + categorySettings.categoryValues.length, + copiedNumberingRules: + numberingRules.rules.length + numberingRules.parts.length, menuIdMap: Object.fromEntries(menuIdMap), screenIdMap: Object.fromEntries(screenIdMap), flowIdMap: Object.fromEntries(flowIdMap), @@ -762,6 +885,8 @@ export class MenuCopyService { - 플로우: ${result.copiedFlows}개 - 코드 카테고리: ${result.copiedCategories}개 - 코드: ${result.copiedCodes}개 + - 카테고리 설정: ${result.copiedCategorySettings}개 + - 채번 규칙: ${result.copiedNumberingRules}개 ============================================ `); @@ -1440,4 +1565,209 @@ export class MenuCopyService { `✅ 코드 복사 완료: 카테고리 ${categoryCount}개 (${skippedCategories}개 스킵), 코드 ${codeCount}개 (${skippedCodes}개 스킵)` ); } + + /** + * 카테고리 설정 복사 + */ + private async copyCategorySettings( + settings: { columnMappings: any[]; categoryValues: any[] }, + menuIdMap: Map, + targetCompanyCode: string, + userId: string, + client: PoolClient + ): Promise { + logger.info(`📂 카테고리 설정 복사 중...`); + + const valueIdMap = new Map(); // 원본 value_id → 새 value_id + let mappingCount = 0; + let valueCount = 0; + + // 1) 카테고리 컬럼 매핑 복사 + for (const mapping of settings.columnMappings) { + const newMenuObjid = menuIdMap.get(mapping.menu_objid); + if (!newMenuObjid) continue; + + // 중복 체크 + const existsResult = await client.query( + `SELECT mapping_id FROM category_column_mapping + WHERE table_name = $1 AND physical_column_name = $2 AND company_code = $3`, + [mapping.table_name, mapping.physical_column_name, targetCompanyCode] + ); + + if (existsResult.rows.length > 0) { + logger.debug( + ` ⏭️ 카테고리 매핑 이미 존재: ${mapping.table_name}.${mapping.physical_column_name}` + ); + continue; + } + + await client.query( + `INSERT INTO category_column_mapping ( + table_name, logical_column_name, physical_column_name, + menu_objid, company_code, description, created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ + mapping.table_name, + mapping.logical_column_name, + mapping.physical_column_name, + newMenuObjid, + targetCompanyCode, + mapping.description, + userId, + ] + ); + + mappingCount++; + } + + // 2) 테이블 컬럼 카테고리 값 복사 (부모-자식 관계 유지) + const sortedValues = settings.categoryValues.sort( + (a, b) => a.depth - b.depth + ); + let skippedValues = 0; + + for (const value of sortedValues) { + const newMenuObjid = menuIdMap.get(value.menu_objid); + if (!newMenuObjid) continue; + + // 중복 체크 + const existsResult = await client.query( + `SELECT value_id FROM table_column_category_values + WHERE table_name = $1 AND column_name = $2 AND value_code = $3 AND company_code = $4`, + [value.table_name, value.column_name, value.value_code, targetCompanyCode] + ); + + if (existsResult.rows.length > 0) { + skippedValues++; + logger.debug( + ` ⏭️ 카테고리 값 이미 존재: ${value.table_name}.${value.column_name}.${value.value_code}` + ); + // 기존 값의 ID를 매핑에 저장 (자식 항목의 parent_id 재매핑용) + valueIdMap.set(value.value_id, existsResult.rows[0].value_id); + continue; + } + + // 부모 ID 재매핑 + let newParentValueId = null; + if (value.parent_value_id) { + newParentValueId = valueIdMap.get(value.parent_value_id) || null; + } + + const result = await client.query( + `INSERT INTO table_column_category_values ( + table_name, column_name, value_code, value_label, + value_order, parent_value_id, depth, description, + color, icon, is_active, is_default, + company_code, menu_objid, created_by + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + RETURNING value_id`, + [ + value.table_name, + value.column_name, + value.value_code, + value.value_label, + value.value_order, + newParentValueId, + value.depth, + value.description, + value.color, + value.icon, + value.is_active, + value.is_default, + targetCompanyCode, + newMenuObjid, + userId, + ] + ); + + // ID 매핑 저장 + const newValueId = result.rows[0].value_id; + valueIdMap.set(value.value_id, newValueId); + + valueCount++; + } + + logger.info( + `✅ 카테고리 설정 복사 완료: 컬럼 매핑 ${mappingCount}개, 카테고리 값 ${valueCount}개 (${skippedValues}개 스킵)` + ); + } + + /** + * 채번 규칙 복사 + */ + private async copyNumberingRules( + rules: { rules: any[]; parts: any[] }, + menuIdMap: Map, + targetCompanyCode: string, + userId: string, + client: PoolClient + ): Promise { + logger.info(`📋 채번 규칙 복사 중...`); + + const ruleIdMap = new Map(); // 원본 rule_id → 새 rule_id + let ruleCount = 0; + let partCount = 0; + + // 1) 채번 규칙 복사 + for (const rule of rules.rules) { + const newMenuObjid = menuIdMap.get(rule.menu_objid); + if (!newMenuObjid) continue; + + // 새 rule_id 생성 (타임스탬프 기반) + const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + ruleIdMap.set(rule.rule_id, newRuleId); + + await client.query( + `INSERT INTO numbering_rules ( + rule_id, rule_name, description, separator, + reset_period, current_sequence, table_name, column_name, + company_code, menu_objid, created_by, scope_type + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, + [ + newRuleId, + rule.rule_name, + rule.description, + rule.separator, + rule.reset_period, + 1, // 시퀀스 초기화 + rule.table_name, + rule.column_name, + targetCompanyCode, + newMenuObjid, + userId, + rule.scope_type, + ] + ); + + ruleCount++; + } + + // 2) 채번 규칙 파트 복사 + for (const part of rules.parts) { + const newRuleId = ruleIdMap.get(part.rule_id); + if (!newRuleId) continue; + + await client.query( + `INSERT INTO numbering_rule_parts ( + rule_id, part_order, part_type, generation_method, + auto_config, manual_config, company_code + ) VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [ + newRuleId, + part.part_order, + part.part_type, + part.generation_method, + part.auto_config, + part.manual_config, + targetCompanyCode, + ] + ); + + partCount++; + } + + logger.info( + `✅ 채번 규칙 복사 완료: 규칙 ${ruleCount}개, 파트 ${partCount}개` + ); + } }