feature/screen-management #306
|
|
@ -53,3 +53,4 @@ export default router;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,3 +49,4 @@ export default router;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -65,3 +65,4 @@ export default router;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -53,3 +53,4 @@ export default router;
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface MenuCopyResult {
|
|||
copiedNumberingRules: number;
|
||||
copiedCategoryMappings: number;
|
||||
copiedTableTypeColumns: number; // 테이블 타입관리 입력타입 설정
|
||||
copiedCascadingRelations: number; // 연쇄관계 설정
|
||||
menuIdMap: Record<number, number>;
|
||||
screenIdMap: Record<number, number>;
|
||||
flowIdMap: Record<number, number>;
|
||||
|
|
@ -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<string, string>();
|
||||
|
||||
const menuObjids = menus.map((m) => m.objid);
|
||||
|
||||
// 메뉴 ID 맵을 먼저 생성 (채번 규칙 복사에 필요)
|
||||
// 메뉴 ID 맵을 먼저 생성 (일관된 ID 사용을 위해)
|
||||
const tempMenuIdMap = new Map<number, number>();
|
||||
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<number, number>,
|
||||
userId: string,
|
||||
client: PoolClient
|
||||
client: PoolClient,
|
||||
preAllocatedMenuIdMap?: Map<number, number> // 미리 할당된 메뉴 ID 맵 (옵션 데이터 복사에 사용된 경우)
|
||||
): Promise<Map<number, number>> {
|
||||
const menuIdMap = new Map<number, number>();
|
||||
|
||||
|
|
@ -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<number, number>,
|
||||
userId: string,
|
||||
client: PoolClient
|
||||
): Promise<number> {
|
||||
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<number, number>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -585,3 +585,4 @@ const result = await executeNodeFlow(flowId, {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -358,3 +358,4 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -344,3 +344,4 @@ const getComponentValue = (componentId: string) => {
|
|||
4. **연쇄 저장**: 한 번의 클릭으로 여러 테이블에 저장
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
테이블 타입관리 입력타입 설정 복사
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="copyCascadingRelation"
|
||||
checked={copyCascadingRelation}
|
||||
onCheckedChange={(checked) => setCopyCascadingRelation(checked as boolean)}
|
||||
disabled={copying}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="copyCascadingRelation"
|
||||
className="text-xs cursor-pointer"
|
||||
>
|
||||
연쇄관계 설정 복사
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -410,6 +427,12 @@ export function MenuCopyDialog({
|
|||
<span className="font-medium">{result.copiedTableTypeColumns}개</span>
|
||||
</div>
|
||||
)}
|
||||
{(result.copiedCascadingRelations ?? 0) > 0 && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">연쇄관계:</span>{" "}
|
||||
<span className="font-medium">{result.copiedCascadingRelations}개</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -138,3 +138,4 @@ export const useActiveTabOptional = () => {
|
|||
};
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -195,3 +195,4 @@ export function applyAutoFillToFormData(
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ApiResponse<MenuCopyResult>> => {
|
||||
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<number, number>;
|
||||
screenIdMap: Record<number, number>;
|
||||
flowIdMap: Record<number, number>;
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ export function UniversalFormModalComponent({
|
|||
// 설정에 정의된 필드 columnName 목록 수집
|
||||
const configuredFields = new Set<string>();
|
||||
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<string, any> = {};
|
||||
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({
|
|||
<CardContent>
|
||||
<div className="grid gap-4" style={{ gridTemplateColumns: `repeat(12, 1fr)` }}>
|
||||
{/* 일반 필드 렌더링 */}
|
||||
{section.fields.map((field) =>
|
||||
{(section.fields || []).map((field) =>
|
||||
renderFieldWithColumns(
|
||||
field,
|
||||
formData[field.columnName],
|
||||
|
|
@ -1582,7 +1582,7 @@ export function UniversalFormModalComponent({
|
|||
<CardContent>
|
||||
<div className="grid gap-4" style={{ gridTemplateColumns: `repeat(12, 1fr)` }}>
|
||||
{/* 일반 필드 렌더링 */}
|
||||
{section.fields.map((field) =>
|
||||
{(section.fields || []).map((field) =>
|
||||
renderFieldWithColumns(
|
||||
field,
|
||||
formData[field.columnName],
|
||||
|
|
@ -1719,7 +1719,7 @@ export function UniversalFormModalComponent({
|
|||
</div>
|
||||
<CollapsibleContent>
|
||||
<div className="grid gap-3 px-3 pb-3" style={{ gridTemplateColumns: "repeat(12, 1fr)" }}>
|
||||
{group.fields.map((field) =>
|
||||
{(group.fields || []).map((field) =>
|
||||
renderFieldWithColumns(
|
||||
field,
|
||||
formData[field.columnName],
|
||||
|
|
@ -1763,7 +1763,7 @@ export function UniversalFormModalComponent({
|
|||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-3" style={{ gridTemplateColumns: "repeat(12, 1fr)" }}>
|
||||
{group.fields.map((field) =>
|
||||
{(group.fields || []).map((field) =>
|
||||
renderFieldWithColumns(
|
||||
field,
|
||||
formData[field.columnName],
|
||||
|
|
@ -1819,7 +1819,7 @@ export function UniversalFormModalComponent({
|
|||
</div>
|
||||
<div className="grid gap-4" style={{ gridTemplateColumns: `repeat(12, 1fr)` }}>
|
||||
{/* 일반 필드 렌더링 */}
|
||||
{section.fields.map((field) =>
|
||||
{(section.fields || []).map((field) =>
|
||||
renderFieldWithColumns(
|
||||
field,
|
||||
item[field.columnName],
|
||||
|
|
@ -1898,7 +1898,7 @@ export function UniversalFormModalComponent({
|
|||
<div className="text-muted-foreground text-center">
|
||||
<p className="font-medium">{config.modal.title || "범용 폼 모달"}</p>
|
||||
<p className="mt-1 text-xs">
|
||||
{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)}개 필드
|
||||
</p>
|
||||
<p className="mt-1 text-xs">저장 테이블: {config.saveConfig.tableName || "(미설정)"}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1687,3 +1687,4 @@ const 출고등록_설정: ScreenSplitPanel = {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -534,3 +534,4 @@ const { data: config } = await getScreenSplitPanel(screenId);
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -521,3 +521,4 @@ function ScreenViewPage() {
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue