diff --git a/backend-node/src/routes/cascadingAutoFillRoutes.ts b/backend-node/src/routes/cascadingAutoFillRoutes.ts index 92cd1bbc..92036080 100644 --- a/backend-node/src/routes/cascadingAutoFillRoutes.ts +++ b/backend-node/src/routes/cascadingAutoFillRoutes.ts @@ -53,3 +53,4 @@ export default router; + diff --git a/backend-node/src/routes/cascadingConditionRoutes.ts b/backend-node/src/routes/cascadingConditionRoutes.ts index 5745511b..ed11d3d1 100644 --- a/backend-node/src/routes/cascadingConditionRoutes.ts +++ b/backend-node/src/routes/cascadingConditionRoutes.ts @@ -49,3 +49,4 @@ export default router; + diff --git a/backend-node/src/routes/cascadingHierarchyRoutes.ts b/backend-node/src/routes/cascadingHierarchyRoutes.ts index 92da4019..d74929cb 100644 --- a/backend-node/src/routes/cascadingHierarchyRoutes.ts +++ b/backend-node/src/routes/cascadingHierarchyRoutes.ts @@ -65,3 +65,4 @@ export default router; + diff --git a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts index 451fe973..ce2fbcac 100644 --- a/backend-node/src/routes/cascadingMutualExclusionRoutes.ts +++ b/backend-node/src/routes/cascadingMutualExclusionRoutes.ts @@ -53,3 +53,4 @@ export default router; + diff --git a/backend-node/src/services/menuCopyService.ts b/backend-node/src/services/menuCopyService.ts index b5266377..683d71ba 100644 --- a/backend-node/src/services/menuCopyService.ts +++ b/backend-node/src/services/menuCopyService.ts @@ -15,6 +15,7 @@ export interface MenuCopyResult { copiedNumberingRules: number; copiedCategoryMappings: number; copiedTableTypeColumns: number; // 테이블 타입관리 입력타입 설정 + copiedCascadingRelations: number; // 연쇄관계 설정 menuIdMap: Record; screenIdMap: Record; flowIdMap: Record; @@ -29,6 +30,7 @@ export interface AdditionalCopyOptions { copyNumberingRules?: boolean; copyCategoryMapping?: boolean; copyTableTypeColumns?: boolean; // 테이블 타입관리 입력타입 설정 + copyCascadingRelation?: boolean; // 연쇄관계 설정 } /** @@ -754,28 +756,44 @@ export class MenuCopyService { client ); - // === 2.5단계: 채번 규칙 복사 (화면 복사 전에 실행하여 참조 업데이트 가능) === + // 변수 초기화 let copiedCodeCategories = 0; let copiedCodes = 0; let copiedNumberingRules = 0; let copiedCategoryMappings = 0; let copiedTableTypeColumns = 0; + let copiedCascadingRelations = 0; let numberingRuleIdMap = new Map(); const menuObjids = menus.map((m) => m.objid); - // 메뉴 ID 맵을 먼저 생성 (채번 규칙 복사에 필요) + // 메뉴 ID 맵을 먼저 생성 (일관된 ID 사용을 위해) const tempMenuIdMap = new Map(); let tempObjId = await this.getNextMenuObjid(client); for (const menu of menus) { tempMenuIdMap.set(menu.objid, tempObjId++); } + // === 3단계: 메뉴 복사 (외래키 의존성 해결을 위해 먼저 실행) === + // 채번 규칙, 코드 카테고리 등이 menu_info를 참조하므로 메뉴를 먼저 생성 + logger.info("\n📂 [3단계] 메뉴 복사 (외래키 선행 조건)"); + const menuIdMap = await this.copyMenus( + menus, + sourceMenuObjid, + sourceCompanyCode, + targetCompanyCode, + new Map(), // screenIdMap은 아직 없음 (나중에 할당에서 처리) + userId, + client, + tempMenuIdMap + ); + + // === 4단계: 채번 규칙 복사 (메뉴 복사 후, 화면 복사 전) === if (additionalCopyOptions?.copyNumberingRules) { - logger.info("\n📦 [2.5단계] 채번 규칙 복사 (화면 복사 전)"); + logger.info("\n📦 [4단계] 채번 규칙 복사"); const ruleResult = await this.copyNumberingRulesWithMap( menuObjids, - tempMenuIdMap, + menuIdMap, // 실제 생성된 메뉴 ID 사용 targetCompanyCode, userId, client @@ -784,8 +802,46 @@ export class MenuCopyService { numberingRuleIdMap = ruleResult.ruleIdMap; } - // === 3단계: 화면 복사 === - logger.info("\n📄 [3단계] 화면 복사"); + // === 4.1단계: 코드 카테고리 + 코드 복사 === + if (additionalCopyOptions?.copyCodeCategory) { + logger.info("\n📦 [4.1단계] 코드 카테고리 + 코드 복사"); + const codeResult = await this.copyCodeCategoriesAndCodes( + menuObjids, + menuIdMap, + targetCompanyCode, + userId, + client + ); + copiedCodeCategories = codeResult.copiedCategories; + copiedCodes = codeResult.copiedCodes; + } + + // === 4.2단계: 카테고리 매핑 + 값 복사 === + if (additionalCopyOptions?.copyCategoryMapping) { + logger.info("\n📦 [4.2단계] 카테고리 매핑 + 값 복사"); + copiedCategoryMappings = await this.copyCategoryMappingsAndValues( + menuObjids, + menuIdMap, + targetCompanyCode, + userId, + client + ); + } + + // === 4.3단계: 연쇄관계 복사 === + if (additionalCopyOptions?.copyCascadingRelation) { + logger.info("\n📦 [4.3단계] 연쇄관계 복사"); + copiedCascadingRelations = await this.copyCascadingRelations( + sourceCompanyCode, + targetCompanyCode, + menuIdMap, + userId, + client + ); + } + + // === 5단계: 화면 복사 === + logger.info("\n📄 [5단계] 화면 복사"); const screenIdMap = await this.copyScreens( screenIds, targetCompanyCode, @@ -796,20 +852,8 @@ export class MenuCopyService { numberingRuleIdMap ); - // === 4단계: 메뉴 복사 === - logger.info("\n📂 [4단계] 메뉴 복사"); - const menuIdMap = await this.copyMenus( - menus, - sourceMenuObjid, // 원본 최상위 메뉴 ID 전달 - sourceCompanyCode, - targetCompanyCode, - screenIdMap, - userId, - client - ); - - // === 5단계: 화면-메뉴 할당 === - logger.info("\n🔗 [5단계] 화면-메뉴 할당"); + // === 6단계: 화면-메뉴 할당 === + logger.info("\n🔗 [6단계] 화면-메뉴 할당"); await this.createScreenMenuAssignments( menus, menuIdMap, @@ -818,44 +862,15 @@ export class MenuCopyService { client ); - // === 6단계: 추가 복사 옵션 처리 (코드 카테고리, 카테고리 매핑) === - if (additionalCopyOptions) { - // 6-1. 코드 카테고리 + 코드 복사 - if (additionalCopyOptions.copyCodeCategory) { - logger.info("\n📦 [6-1단계] 코드 카테고리 + 코드 복사"); - const codeResult = await this.copyCodeCategoriesAndCodes( - menuObjids, - menuIdMap, - targetCompanyCode, - userId, - client - ); - copiedCodeCategories = codeResult.copiedCategories; - copiedCodes = codeResult.copiedCodes; - } - - // 6-2. 카테고리 매핑 + 값 복사 - if (additionalCopyOptions.copyCategoryMapping) { - logger.info("\n📦 [6-2단계] 카테고리 매핑 + 값 복사"); - copiedCategoryMappings = await this.copyCategoryMappingsAndValues( - menuObjids, - menuIdMap, - targetCompanyCode, - userId, - client - ); - } - - // 6-3. 테이블 타입관리 입력타입 설정 복사 - if (additionalCopyOptions.copyTableTypeColumns) { - logger.info("\n📦 [6-3단계] 테이블 타입 설정 복사"); - copiedTableTypeColumns = await this.copyTableTypeColumns( - Array.from(screenIdMap.keys()), // 원본 화면 IDs - sourceCompanyCode, - targetCompanyCode, - client - ); - } + // === 7단계: 테이블 타입 설정 복사 === + if (additionalCopyOptions?.copyTableTypeColumns) { + logger.info("\n📦 [7단계] 테이블 타입 설정 복사"); + copiedTableTypeColumns = await this.copyTableTypeColumns( + Array.from(screenIdMap.keys()), + sourceCompanyCode, + targetCompanyCode, + client + ); } // 커밋 @@ -872,6 +887,7 @@ export class MenuCopyService { copiedNumberingRules, copiedCategoryMappings, copiedTableTypeColumns, + copiedCascadingRelations, menuIdMap: Object.fromEntries(menuIdMap), screenIdMap: Object.fromEntries(screenIdMap), flowIdMap: Object.fromEntries(flowIdMap), @@ -889,6 +905,7 @@ export class MenuCopyService { - 채번규칙: ${copiedNumberingRules}개 - 카테고리 매핑: ${copiedCategoryMappings}개 - 테이블 타입 설정: ${copiedTableTypeColumns}개 + - 연쇄관계: ${copiedCascadingRelations}개 ============================================ `); @@ -1569,7 +1586,8 @@ export class MenuCopyService { targetCompanyCode: string, screenIdMap: Map, userId: string, - client: PoolClient + client: PoolClient, + preAllocatedMenuIdMap?: Map // 미리 할당된 메뉴 ID 맵 (옵션 데이터 복사에 사용된 경우) ): Promise> { const menuIdMap = new Map(); @@ -1676,7 +1694,8 @@ export class MenuCopyService { } // === 신규 메뉴 복사 === - const newObjId = await this.getNextMenuObjid(client); + // 미리 할당된 ID가 있으면 사용, 없으면 새로 생성 + const newObjId = preAllocatedMenuIdMap?.get(menu.objid) ?? await this.getNextMenuObjid(client); // source_menu_objid 저장: 모든 복사된 메뉴에 원본 ID 저장 (추적용) const sourceMenuObjid = menu.objid; @@ -2219,4 +2238,184 @@ export class MenuCopyService { return copiedCount; } + /** + * 연쇄관계 복사 + * - category_value_cascading_group + category_value_cascading_mapping + * - cascading_relation (테이블 기반) + */ + private async copyCascadingRelations( + sourceCompanyCode: string, + targetCompanyCode: string, + menuIdMap: Map, + userId: string, + client: PoolClient + ): Promise { + logger.info(`📋 연쇄관계 복사 시작`); + let copiedCount = 0; + + // === 1. category_value_cascading_group 복사 === + const groupsResult = await client.query( + `SELECT * FROM category_value_cascading_group + WHERE company_code = $1 AND is_active = 'Y'`, + [sourceCompanyCode] + ); + + logger.info(` 카테고리 값 연쇄 그룹: ${groupsResult.rows.length}개`); + + // group_id 매핑 (매핑 복사 시 사용) + const groupIdMap = new Map(); + + for (const group of groupsResult.rows) { + // 대상 회사에 같은 relation_code가 있는지 확인 + const existing = await client.query( + `SELECT group_id FROM category_value_cascading_group + WHERE relation_code = $1 AND company_code = $2`, + [group.relation_code, targetCompanyCode] + ); + + if (existing.rows.length > 0) { + // 이미 존재하면 스킵 (기존 설정 유지) + groupIdMap.set(group.group_id, existing.rows[0].group_id); + logger.info(` ↳ ${group.relation_name}: 이미 존재 (스킵)`); + continue; + } + + // menu_objid 재매핑 + const newParentMenuObjid = group.parent_menu_objid + ? menuIdMap.get(Number(group.parent_menu_objid)) || null + : null; + const newChildMenuObjid = group.child_menu_objid + ? menuIdMap.get(Number(group.child_menu_objid)) || null + : null; + + // 새로 삽입 + const insertResult = await client.query( + `INSERT INTO category_value_cascading_group ( + relation_code, relation_name, description, + parent_table_name, parent_column_name, parent_menu_objid, + child_table_name, child_column_name, child_menu_objid, + clear_on_parent_change, show_group_label, + empty_parent_message, no_options_message, + company_code, is_active, created_by, created_date + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, NOW()) + RETURNING group_id`, + [ + group.relation_code, + group.relation_name, + group.description, + group.parent_table_name, + group.parent_column_name, + newParentMenuObjid, + group.child_table_name, + group.child_column_name, + newChildMenuObjid, + group.clear_on_parent_change, + group.show_group_label, + group.empty_parent_message, + group.no_options_message, + targetCompanyCode, + "Y", + userId, + ] + ); + + const newGroupId = insertResult.rows[0].group_id; + groupIdMap.set(group.group_id, newGroupId); + logger.info(` ↳ ${group.relation_name}: 신규 추가 (ID: ${newGroupId})`); + copiedCount++; + + // 해당 그룹의 매핑 복사 + const mappingsResult = await client.query( + `SELECT * FROM category_value_cascading_mapping + WHERE group_id = $1 AND company_code = $2`, + [group.group_id, sourceCompanyCode] + ); + + for (const mapping of mappingsResult.rows) { + await client.query( + `INSERT INTO category_value_cascading_mapping ( + group_id, parent_value_code, parent_value_label, + child_value_code, child_value_label, display_order, + company_code, is_active, created_date + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())`, + [ + newGroupId, + mapping.parent_value_code, + mapping.parent_value_label, + mapping.child_value_code, + mapping.child_value_label, + mapping.display_order, + targetCompanyCode, + "Y", + ] + ); + } + + if (mappingsResult.rows.length > 0) { + logger.info(` ↳ 매핑 ${mappingsResult.rows.length}개 복사`); + } + } + + // === 2. cascading_relation 복사 (테이블 기반) === + const relationsResult = await client.query( + `SELECT * FROM cascading_relation + WHERE company_code = $1 AND is_active = 'Y'`, + [sourceCompanyCode] + ); + + logger.info(` 기본 연쇄관계: ${relationsResult.rows.length}개`); + + for (const relation of relationsResult.rows) { + // 대상 회사에 같은 relation_code가 있는지 확인 + const existing = await client.query( + `SELECT relation_id FROM cascading_relation + WHERE relation_code = $1 AND company_code = $2`, + [relation.relation_code, targetCompanyCode] + ); + + if (existing.rows.length > 0) { + logger.info(` ↳ ${relation.relation_name}: 이미 존재 (스킵)`); + continue; + } + + // 새로 삽입 + await client.query( + `INSERT INTO cascading_relation ( + relation_code, relation_name, description, + parent_table, parent_value_column, parent_label_column, + child_table, child_filter_column, child_value_column, child_label_column, + child_order_column, child_order_direction, + empty_parent_message, no_options_message, loading_message, + clear_on_parent_change, company_code, is_active, created_by, created_date + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, NOW())`, + [ + relation.relation_code, + relation.relation_name, + relation.description, + relation.parent_table, + relation.parent_value_column, + relation.parent_label_column, + relation.child_table, + relation.child_filter_column, + relation.child_value_column, + relation.child_label_column, + relation.child_order_column, + relation.child_order_direction, + relation.empty_parent_message, + relation.no_options_message, + relation.loading_message, + relation.clear_on_parent_change, + targetCompanyCode, + "Y", + userId, + ] + ); + logger.info(` ↳ ${relation.relation_name}: 신규 추가`); + copiedCount++; + } + + logger.info(`✅ 연쇄관계 복사 완료: ${copiedCount}개`); + return copiedCount; + } + } diff --git a/docs/노드플로우_개선사항.md b/docs/노드플로우_개선사항.md index b19c7092..c2c44be0 100644 --- a/docs/노드플로우_개선사항.md +++ b/docs/노드플로우_개선사항.md @@ -585,3 +585,4 @@ const result = await executeNodeFlow(flowId, { + diff --git a/docs/메일발송_기능_사용_가이드.md b/docs/메일발송_기능_사용_가이드.md index f0805640..4ffb7655 100644 --- a/docs/메일발송_기능_사용_가이드.md +++ b/docs/메일발송_기능_사용_가이드.md @@ -358,3 +358,4 @@ + diff --git a/docs/즉시저장_버튼_액션_구현_계획서.md b/docs/즉시저장_버튼_액션_구현_계획서.md index 69e34f5a..1de42fb2 100644 --- a/docs/즉시저장_버튼_액션_구현_계획서.md +++ b/docs/즉시저장_버튼_액션_구현_계획서.md @@ -344,3 +344,4 @@ const getComponentValue = (componentId: string) => { 4. **연쇄 저장**: 한 번의 클릭으로 여러 테이블에 저장 + diff --git a/frontend/components/admin/MenuCopyDialog.tsx b/frontend/components/admin/MenuCopyDialog.tsx index 88d29de6..c33e726b 100644 --- a/frontend/components/admin/MenuCopyDialog.tsx +++ b/frontend/components/admin/MenuCopyDialog.tsx @@ -61,6 +61,7 @@ export function MenuCopyDialog({ const [copyNumberingRules, setCopyNumberingRules] = useState(false); const [copyCategoryMapping, setCopyCategoryMapping] = useState(false); const [copyTableTypeColumns, setCopyTableTypeColumns] = useState(false); + const [copyCascadingRelation, setCopyCascadingRelation] = useState(false); // 회사 목록 로드 useEffect(() => { @@ -76,6 +77,7 @@ export function MenuCopyDialog({ setCopyNumberingRules(false); setCopyCategoryMapping(false); setCopyTableTypeColumns(false); + setCopyCascadingRelation(false); } }, [open]); @@ -128,6 +130,7 @@ export function MenuCopyDialog({ copyNumberingRules, copyCategoryMapping, copyTableTypeColumns, + copyCascadingRelation, }; const response = await menuApi.copyMenu( @@ -344,6 +347,20 @@ export function MenuCopyDialog({ 테이블 타입관리 입력타입 설정 복사 +
+ setCopyCascadingRelation(checked as boolean)} + disabled={copying} + /> + +
)} @@ -410,6 +427,12 @@ export function MenuCopyDialog({ {result.copiedTableTypeColumns}개 )} + {(result.copiedCascadingRelations ?? 0) > 0 && ( +
+ 연쇄관계:{" "} + {result.copiedCascadingRelations}개 +
+ )} )} diff --git a/frontend/contexts/ActiveTabContext.tsx b/frontend/contexts/ActiveTabContext.tsx index 9a18a44d..340cf26f 100644 --- a/frontend/contexts/ActiveTabContext.tsx +++ b/frontend/contexts/ActiveTabContext.tsx @@ -138,3 +138,4 @@ export const useActiveTabOptional = () => { }; + diff --git a/frontend/hooks/useAutoFill.ts b/frontend/hooks/useAutoFill.ts index a1a2f711..ba81e49e 100644 --- a/frontend/hooks/useAutoFill.ts +++ b/frontend/hooks/useAutoFill.ts @@ -195,3 +195,4 @@ export function applyAutoFillToFormData( + diff --git a/frontend/lib/api/menu.ts b/frontend/lib/api/menu.ts index 5119b7e4..82ab39ac 100644 --- a/frontend/lib/api/menu.ts +++ b/frontend/lib/api/menu.ts @@ -163,7 +163,7 @@ export const menuApi = { } }, - // 메뉴 복사 + // 메뉴 복사 (타임아웃 5분 - 대량 데이터 처리) copyMenu: async ( menuObjid: number, targetCompanyCode: string, @@ -176,6 +176,7 @@ export const menuApi = { copyNumberingRules?: boolean; copyCategoryMapping?: boolean; copyTableTypeColumns?: boolean; + copyCascadingRelation?: boolean; } ): Promise> => { try { @@ -185,11 +186,24 @@ export const menuApi = { targetCompanyCode, screenNameConfig, additionalCopyOptions + }, + { + timeout: 300000, // 5분 (메뉴 복사는 많은 데이터를 처리하므로 긴 타임아웃 필요) } ); return response.data; } catch (error: any) { console.error("❌ 메뉴 복사 실패:", error); + + // 타임아웃 에러 구분 처리 + if (error.code === "ECONNABORTED" || error.message?.includes("timeout")) { + return { + success: false, + message: "메뉴 복사 요청 시간이 초과되었습니다. 백엔드에서 작업이 완료되었을 수 있으니 잠시 후 확인해주세요.", + errorCode: "MENU_COPY_TIMEOUT", + }; + } + return { success: false, message: error.response?.data?.message || "메뉴 복사 중 오류가 발생했습니다", @@ -211,6 +225,7 @@ export interface MenuCopyResult { copiedNumberingRules?: number; copiedCategoryMappings?: number; copiedTableTypeColumns?: number; + copiedCascadingRelations?: number; menuIdMap: Record; screenIdMap: Record; flowIdMap: Record; diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 68a1553c..03c2efb8 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -269,7 +269,7 @@ export function UniversalFormModalComponent({ // 설정에 정의된 필드 columnName 목록 수집 const configuredFields = new Set(); config.sections.forEach((section) => { - section.fields.forEach((field) => { + (section.fields || []).forEach((field) => { if (field.columnName) { configuredFields.add(field.columnName); } @@ -319,7 +319,7 @@ export function UniversalFormModalComponent({ // 모든 섹션의 필드에서 linkedFieldGroup.sourceTable 수집 config.sections.forEach((section) => { - section.fields.forEach((field) => { + (section.fields || []).forEach((field) => { if (field.linkedFieldGroup?.enabled && field.linkedFieldGroup?.sourceTable) { tablesToLoad.add(field.linkedFieldGroup.sourceTable); } @@ -374,7 +374,7 @@ export function UniversalFormModalComponent({ newRepeatSections[section.id] = items; } else { // 일반 섹션 필드 초기화 - for (const field of section.fields) { + for (const field of section.fields || []) { // 기본값 설정 let value = field.defaultValue ?? ""; @@ -405,7 +405,7 @@ export function UniversalFormModalComponent({ console.log(`[initializeForm] 옵셔널 그룹 자동 활성화: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`); // 활성화된 그룹의 필드값도 초기화 - for (const field of group.fields) { + for (const field of group.fields || []) { let value = field.defaultValue ?? ""; const parentField = field.parentFieldName || field.columnName; if (effectiveInitialData[parentField] !== undefined) { @@ -448,7 +448,7 @@ export function UniversalFormModalComponent({ _index: index, }; - for (const field of section.fields) { + for (const field of section.fields || []) { item[field.columnName] = field.defaultValue ?? ""; } @@ -481,7 +481,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable) continue; - for (const field of section.fields) { + for (const field of section.fields || []) { if ( field.numberingRule?.enabled && field.numberingRule?.generateOnOpen && @@ -653,7 +653,7 @@ export function UniversalFormModalComponent({ } // 옵셔널 필드 그룹 필드 값 초기화 - group.fields.forEach((field) => { + (group.fields || []).forEach((field) => { handleFieldChange(field.columnName, field.defaultValue || ""); }); }, [config, handleFieldChange]); @@ -783,7 +783,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable) continue; // 반복 섹션은 별도 검증 - for (const field of section.fields) { + for (const field of section.fields || []) { if (field.required && !field.hidden && !field.numberingRule?.hidden) { const value = formData[field.columnName]; if (value === undefined || value === null || value === "") { @@ -809,7 +809,7 @@ export function UniversalFormModalComponent({ // 저장 시점 채번규칙 처리 (generateOnSave만 처리) for (const section of config.sections) { - for (const field of section.fields) { + for (const field of section.fields || []) { if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) { const response = await allocateNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { @@ -840,7 +840,7 @@ export function UniversalFormModalComponent({ // 공통 필드가 설정되지 않은 경우, 기본정보 섹션의 모든 필드를 공통 필드로 사용 if (commonFields.length === 0) { const nonRepeatableSections = config.sections.filter((s) => !s.repeatable); - commonFields = nonRepeatableSections.flatMap((s) => s.fields.map((f) => f.columnName)); + commonFields = nonRepeatableSections.flatMap((s) => (s.fields || []).map((f) => f.columnName)); } // 반복 섹션 ID가 설정되지 않은 경우, 첫 번째 반복 섹션 사용 @@ -886,7 +886,7 @@ export function UniversalFormModalComponent({ // 반복 섹션의 필드 값 추가 const repeatSection = config.sections.find((s) => s.id === repeatSectionId); - repeatSection?.fields.forEach((field) => { + (repeatSection?.fields || []).forEach((field) => { if (item[field.columnName] !== undefined) { subRow[field.columnName] = item[field.columnName]; } @@ -903,7 +903,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable) continue; - for (const field of section.fields) { + for (const field of section.fields || []) { if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // generateOnSave 또는 generateOnOpen 모두 저장 시 실제 순번 할당 const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen; @@ -952,7 +952,7 @@ export function UniversalFormModalComponent({ const mainData: Record = {}; config.sections.forEach((section) => { if (section.repeatable) return; // 반복 섹션은 제외 - section.fields.forEach((field) => { + (section.fields || []).forEach((field) => { const value = formData[field.columnName]; if (value !== undefined && value !== null && value !== "") { mainData[field.columnName] = value; @@ -964,7 +964,7 @@ export function UniversalFormModalComponent({ for (const section of config.sections) { if (section.repeatable) continue; - for (const field of section.fields) { + for (const field of section.fields || []) { // 채번규칙이 활성화된 필드 처리 if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // 신규 생성이거나 값이 없는 경우에만 채번 @@ -1055,7 +1055,7 @@ export function UniversalFormModalComponent({ else { config.sections.forEach((section) => { if (section.repeatable) return; - const matchingField = section.fields.find((f) => f.columnName === mapping.targetColumn); + const matchingField = (section.fields || []).find((f) => f.columnName === mapping.targetColumn); if (matchingField && mainData[matchingField.columnName] !== undefined) { mainFieldMappings!.push({ formField: matchingField.columnName, @@ -1560,7 +1560,7 @@ export function UniversalFormModalComponent({
{/* 일반 필드 렌더링 */} - {section.fields.map((field) => + {(section.fields || []).map((field) => renderFieldWithColumns( field, formData[field.columnName], @@ -1582,7 +1582,7 @@ export function UniversalFormModalComponent({
{/* 일반 필드 렌더링 */} - {section.fields.map((field) => + {(section.fields || []).map((field) => renderFieldWithColumns( field, formData[field.columnName], @@ -1719,7 +1719,7 @@ export function UniversalFormModalComponent({
- {group.fields.map((field) => + {(group.fields || []).map((field) => renderFieldWithColumns( field, formData[field.columnName], @@ -1763,7 +1763,7 @@ export function UniversalFormModalComponent({
- {group.fields.map((field) => + {(group.fields || []).map((field) => renderFieldWithColumns( field, formData[field.columnName], @@ -1819,7 +1819,7 @@ export function UniversalFormModalComponent({
{/* 일반 필드 렌더링 */} - {section.fields.map((field) => + {(section.fields || []).map((field) => renderFieldWithColumns( field, item[field.columnName], @@ -1898,7 +1898,7 @@ export function UniversalFormModalComponent({

{config.modal.title || "범용 폼 모달"}

- {config.sections.length}개 섹션 |{config.sections.reduce((acc, s) => acc + s.fields.length, 0)}개 필드 + {config.sections.length}개 섹션 |{config.sections.reduce((acc, s) => acc + (s.fields?.length || 0), 0)}개 필드

저장 테이블: {config.saveConfig.tableName || "(미설정)"}

diff --git a/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md b/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md index 275272ce..f962f9c3 100644 --- a/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md +++ b/화면_임베딩_및_데이터_전달_시스템_구현_계획서.md @@ -1687,3 +1687,4 @@ const 출고등록_설정: ScreenSplitPanel = { + diff --git a/화면_임베딩_시스템_Phase1-4_구현_완료.md b/화면_임베딩_시스템_Phase1-4_구현_완료.md index f88df210..a480f7ec 100644 --- a/화면_임베딩_시스템_Phase1-4_구현_완료.md +++ b/화면_임베딩_시스템_Phase1-4_구현_완료.md @@ -534,3 +534,4 @@ const { data: config } = await getScreenSplitPanel(screenId); + diff --git a/화면_임베딩_시스템_충돌_분석_보고서.md b/화면_임베딩_시스템_충돌_분석_보고서.md index 0f74ba8d..eb56a747 100644 --- a/화면_임베딩_시스템_충돌_분석_보고서.md +++ b/화면_임베딩_시스템_충돌_분석_보고서.md @@ -521,3 +521,4 @@ function ScreenViewPage() { +