feat: 부모 데이터 매핑 기능 구현 (선택항목 상세입력 컴포넌트)
- 여러 테이블(거래처, 품목 등)에서 데이터를 가져와 자동 매핑 가능 - 각 매핑마다 소스 테이블, 원본 필드, 저장 필드를 독립적으로 설정 - 검색 가능한 Combobox로 테이블 및 컬럼 선택 UX 개선 - 소스 테이블 선택 시 해당 테이블의 컬럼 자동 로드 - 라벨, 컬럼명, 데이터 타입으로 검색 가능 - 세로 레이아웃으로 가독성 향상 기술적 변경사항: - ParentDataMapping 인터페이스 추가 (sourceTable, sourceField, targetField) - buttonActions.ts의 handleBatchSave에서 소스 테이블 기반 데이터 소스 자동 판단 - tableManagementApi.getColumnList() 사용하여 테이블 컬럼 동적 로드 - Command + Popover 조합으로 검색 가능한 Select 구현 - 각 매핑별 독립적인 컬럼 상태 관리 (mappingSourceColumns)
This commit is contained in:
@@ -208,10 +208,17 @@ export class ButtonActionExecutor {
|
||||
console.log("💾 [handleSave] 저장 시작:", { formData, tableName, screenId });
|
||||
|
||||
// 🆕 저장 전 이벤트 발생 (SelectedItemsDetailInput 등에서 최신 데이터 수집)
|
||||
window.dispatchEvent(new CustomEvent("beforeFormSave"));
|
||||
// context.formData를 이벤트 detail에 포함하여 직접 수정 가능하게 함
|
||||
window.dispatchEvent(new CustomEvent("beforeFormSave", {
|
||||
detail: {
|
||||
formData: context.formData
|
||||
}
|
||||
}));
|
||||
|
||||
// 약간의 대기 시간을 주어 이벤트 핸들러가 formData를 업데이트할 수 있도록 함
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
console.log("📦 [handleSave] beforeFormSave 이벤트 후 formData:", context.formData);
|
||||
|
||||
// 🆕 SelectedItemsDetailInput 배치 저장 처리 (fieldGroups 구조)
|
||||
console.log("🔍 [handleSave] formData 구조 확인:", {
|
||||
@@ -508,7 +515,14 @@ export class ButtonActionExecutor {
|
||||
context: ButtonActionContext,
|
||||
selectedItemsKeys: string[]
|
||||
): Promise<boolean> {
|
||||
const { formData, tableName, screenId } = context;
|
||||
const { formData, tableName, screenId, selectedRowsData, originalData } = context;
|
||||
|
||||
console.log(`🔍 [handleBatchSave] context 확인:`, {
|
||||
hasSelectedRowsData: !!selectedRowsData,
|
||||
selectedRowsCount: selectedRowsData?.length || 0,
|
||||
hasOriginalData: !!originalData,
|
||||
originalDataKeys: originalData ? Object.keys(originalData) : [],
|
||||
});
|
||||
|
||||
if (!tableName || !screenId) {
|
||||
toast.error("저장에 필요한 정보가 부족합니다. (테이블명 또는 화면ID 누락)");
|
||||
@@ -520,6 +534,15 @@ export class ButtonActionExecutor {
|
||||
let failCount = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
// 🆕 부모 화면 데이터 준비 (parentDataMapping용)
|
||||
// selectedRowsData 또는 originalData를 parentData로 사용
|
||||
const parentData = selectedRowsData?.[0] || originalData || {};
|
||||
|
||||
console.log(`🔍 [handleBatchSave] 부모 데이터:`, {
|
||||
hasParentData: Object.keys(parentData).length > 0,
|
||||
parentDataKeys: Object.keys(parentData),
|
||||
});
|
||||
|
||||
// 각 SelectedItemsDetailInput 컴포넌트의 데이터 처리
|
||||
for (const key of selectedItemsKeys) {
|
||||
// 🆕 새로운 데이터 구조: ItemData[] with fieldGroups
|
||||
@@ -531,22 +554,152 @@ export class ButtonActionExecutor {
|
||||
|
||||
console.log(`📦 [handleBatchSave] ${key} 처리 중 (${items.length}개 품목)`);
|
||||
|
||||
// 각 품목의 모든 그룹의 모든 항목을 개별 저장
|
||||
// 🆕 이 컴포넌트의 parentDataMapping 설정 가져오기
|
||||
// TODO: 실제로는 componentConfig에서 가져와야 함
|
||||
// 현재는 selectedItemsKeys[0]을 사용하여 임시로 가져옴
|
||||
const componentConfig = (context as any).componentConfigs?.[key];
|
||||
const parentDataMapping = componentConfig?.parentDataMapping || [];
|
||||
|
||||
console.log(`🔍 [handleBatchSave] parentDataMapping 설정:`, {
|
||||
hasMapping: parentDataMapping.length > 0,
|
||||
mappings: parentDataMapping
|
||||
});
|
||||
|
||||
// 🆕 각 품목의 그룹 간 조합(카티션 곱) 생성
|
||||
for (const item of items) {
|
||||
const allGroupEntries = Object.values(item.fieldGroups).flat();
|
||||
console.log(`🔍 [handleBatchSave] 품목 처리: ${item.id} (${allGroupEntries.length}개 입력 항목)`);
|
||||
const groupKeys = Object.keys(item.fieldGroups);
|
||||
console.log(`🔍 [handleBatchSave] 품목 처리: ${item.id} (${groupKeys.length}개 그룹)`);
|
||||
|
||||
// 모든 그룹의 모든 항목을 개별 레코드로 저장
|
||||
for (const entry of allGroupEntries) {
|
||||
// 각 그룹의 항목 배열 가져오기
|
||||
const groupArrays = groupKeys.map(groupKey => ({
|
||||
groupKey,
|
||||
entries: item.fieldGroups[groupKey] || []
|
||||
}));
|
||||
|
||||
console.log(`📊 [handleBatchSave] 그룹별 항목 수:`,
|
||||
groupArrays.map(g => `${g.groupKey}: ${g.entries.length}개`).join(", ")
|
||||
);
|
||||
|
||||
// 카티션 곱 계산 함수
|
||||
const cartesianProduct = (arrays: any[][]): any[][] => {
|
||||
if (arrays.length === 0) return [[]];
|
||||
if (arrays.length === 1) return arrays[0].map(item => [item]);
|
||||
|
||||
const [first, ...rest] = arrays;
|
||||
const restProduct = cartesianProduct(rest);
|
||||
|
||||
return first.flatMap(item =>
|
||||
restProduct.map(combination => [item, ...combination])
|
||||
);
|
||||
};
|
||||
|
||||
// 모든 그룹의 카티션 곱 생성
|
||||
const entryArrays = groupArrays.map(g => g.entries);
|
||||
const combinations = cartesianProduct(entryArrays);
|
||||
|
||||
console.log(`🔢 [handleBatchSave] 생성된 조합 수: ${combinations.length}개`);
|
||||
|
||||
// 각 조합을 개별 레코드로 저장
|
||||
for (let i = 0; i < combinations.length; i++) {
|
||||
const combination = combinations[i];
|
||||
try {
|
||||
// 원본 데이터 + 입력 데이터 병합
|
||||
const mergedData = {
|
||||
...item.originalData,
|
||||
...entry,
|
||||
};
|
||||
// 🆕 부모 데이터 매핑 적용
|
||||
const mappedData: any = {};
|
||||
|
||||
// id 필드 제거 (entry.id는 임시 ID이므로)
|
||||
delete mergedData.id;
|
||||
// 1. parentDataMapping 설정이 있으면 적용
|
||||
if (parentDataMapping.length > 0) {
|
||||
console.log(` 🔗 [parentDataMapping] 매핑 시작 (${parentDataMapping.length}개 매핑)`);
|
||||
|
||||
for (const mapping of parentDataMapping) {
|
||||
// sourceTable을 기준으로 데이터 소스 결정
|
||||
let sourceData: any;
|
||||
|
||||
// 🔍 sourceTable과 실제 데이터 테이블 비교
|
||||
// - parentData는 이전 화면 데이터 (예: 거래처 테이블)
|
||||
// - item.originalData는 선택된 항목 데이터 (예: 품목 테이블)
|
||||
|
||||
// 원본 데이터 테이블명 확인 (sourceTable이 config에 명시되어 있음)
|
||||
const sourceTableName = mapping.sourceTable;
|
||||
|
||||
// 현재 선택된 항목의 테이블 = config.sourceTable
|
||||
const selectedItemTable = componentConfig?.sourceTable;
|
||||
|
||||
if (sourceTableName === selectedItemTable) {
|
||||
// 선택된 항목 데이터 사용
|
||||
sourceData = item.originalData;
|
||||
console.log(` 📦 소스: 선택된 항목 데이터 (${sourceTableName})`);
|
||||
} else {
|
||||
// 이전 화면 데이터 사용
|
||||
sourceData = parentData;
|
||||
console.log(` 👤 소스: 이전 화면 데이터 (${sourceTableName})`);
|
||||
}
|
||||
|
||||
const sourceValue = sourceData[mapping.sourceField];
|
||||
|
||||
if (sourceValue !== undefined && sourceValue !== null) {
|
||||
mappedData[mapping.targetField] = sourceValue;
|
||||
console.log(` ✅ [${sourceTableName}] ${mapping.sourceField} → ${mapping.targetField}: ${sourceValue}`);
|
||||
} else if (mapping.defaultValue !== undefined) {
|
||||
mappedData[mapping.targetField] = mapping.defaultValue;
|
||||
console.log(` ⚠️ [${sourceTableName}] ${mapping.sourceField} 없음, 기본값 사용 → ${mapping.targetField}: ${mapping.defaultValue}`);
|
||||
} else {
|
||||
console.log(` ⚠️ [${sourceTableName}] ${mapping.sourceField} 없음, 건너뜀`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 🔧 parentDataMapping 설정이 없는 경우 기본 매핑 (하위 호환성)
|
||||
console.log(` ⚠️ [parentDataMapping] 설정 없음, 기본 매핑 적용`);
|
||||
|
||||
// 기본 item_id 매핑 (item.originalData의 id)
|
||||
if (item.originalData.id) {
|
||||
mappedData.item_id = item.originalData.id;
|
||||
console.log(` ✅ [기본] item_id 매핑: ${item.originalData.id}`);
|
||||
}
|
||||
|
||||
// 기본 customer_id 매핑 (parentData의 id 또는 customer_id)
|
||||
if (parentData.id || parentData.customer_id) {
|
||||
mappedData.customer_id = parentData.customer_id || parentData.id;
|
||||
console.log(` ✅ [기본] customer_id 매핑: ${mappedData.customer_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 공통 필드 복사 (company_code, currency_code 등)
|
||||
if (item.originalData.company_code && !mappedData.company_code) {
|
||||
mappedData.company_code = item.originalData.company_code;
|
||||
}
|
||||
if (item.originalData.currency_code && !mappedData.currency_code) {
|
||||
mappedData.currency_code = item.originalData.currency_code;
|
||||
}
|
||||
|
||||
// 원본 데이터로 시작 (매핑된 데이터 사용)
|
||||
let mergedData = { ...mappedData };
|
||||
|
||||
console.log(`🔍 [handleBatchSave] 조합 ${i + 1}/${combinations.length} 병합 시작:`, {
|
||||
originalDataKeys: Object.keys(item.originalData),
|
||||
mappedDataKeys: Object.keys(mappedData),
|
||||
combinationLength: combination.length
|
||||
});
|
||||
|
||||
// 각 그룹의 항목 데이터를 순차적으로 병합
|
||||
for (let j = 0; j < combination.length; j++) {
|
||||
const entry = combination[j];
|
||||
const { id, ...entryData } = entry; // id 제외
|
||||
|
||||
console.log(` 🔸 그룹 ${j + 1} 데이터 병합:`, entryData);
|
||||
|
||||
mergedData = { ...mergedData, ...entryData };
|
||||
}
|
||||
|
||||
console.log(`📝 [handleBatchSave] 조합 ${i + 1}/${combinations.length} 최종 데이터:`, mergedData);
|
||||
|
||||
// 🆕 조합 저장 시 id 필드 제거 (각 조합이 독립된 새 레코드가 되도록)
|
||||
// originalData의 id는 원본 품목의 ID이므로, 새로운 customer_item_mapping 레코드 생성 시 제거 필요
|
||||
const { id: _removedId, ...dataWithoutId } = mergedData;
|
||||
|
||||
console.log(`🔧 [handleBatchSave] 조합 ${i + 1}/${combinations.length} id 제거됨:`, {
|
||||
removedId: _removedId,
|
||||
hasId: 'id' in dataWithoutId
|
||||
});
|
||||
|
||||
// 사용자 정보 추가
|
||||
if (!context.userId) {
|
||||
@@ -557,16 +710,17 @@ export class ButtonActionExecutor {
|
||||
const companyCodeValue = context.companyCode || "";
|
||||
|
||||
const dataWithUserInfo = {
|
||||
...mergedData,
|
||||
writer: mergedData.writer || writerValue,
|
||||
...dataWithoutId, // id가 제거된 데이터 사용
|
||||
writer: dataWithoutId.writer || writerValue,
|
||||
created_by: writerValue,
|
||||
updated_by: writerValue,
|
||||
company_code: mergedData.company_code || companyCodeValue,
|
||||
company_code: dataWithoutId.company_code || companyCodeValue,
|
||||
};
|
||||
|
||||
console.log(`💾 [handleBatchSave] 입력 항목 저장:`, {
|
||||
console.log(`💾 [handleBatchSave] 조합 ${i + 1}/${combinations.length} 저장 요청:`, {
|
||||
itemId: item.id,
|
||||
entryId: entry.id,
|
||||
combinationIndex: i + 1,
|
||||
totalCombinations: combinations.length,
|
||||
data: dataWithUserInfo
|
||||
});
|
||||
|
||||
@@ -580,16 +734,19 @@ export class ButtonActionExecutor {
|
||||
|
||||
if (saveResult.success) {
|
||||
successCount++;
|
||||
console.log(`✅ [handleBatchSave] 입력 항목 저장 성공: ${item.id} > ${entry.id}`);
|
||||
console.log(`✅ [handleBatchSave] 조합 ${i + 1}/${combinations.length} 저장 성공!`, {
|
||||
savedId: saveResult.data?.id,
|
||||
itemId: item.id
|
||||
});
|
||||
} else {
|
||||
failCount++;
|
||||
errors.push(`품목 ${item.id} > 항목 ${entry.id}: ${saveResult.message}`);
|
||||
console.error(`❌ [handleBatchSave] 입력 항목 저장 실패: ${item.id} > ${entry.id}`, saveResult.message);
|
||||
errors.push(`품목 ${item.id} > 조합 ${i + 1}: ${saveResult.message}`);
|
||||
console.error(`❌ [handleBatchSave] 조합 ${i + 1}/${combinations.length} 저장 실패:`, saveResult.message);
|
||||
}
|
||||
} catch (error: any) {
|
||||
failCount++;
|
||||
errors.push(`품목 ${item.id} > 항목 ${entry.id}: ${error.message}`);
|
||||
console.error(`❌ [handleBatchSave] 입력 항목 저장 오류: ${item.id} > ${entry.id}`, error);
|
||||
errors.push(`품목 ${item.id} > 조합 ${i + 1}: ${error.message}`);
|
||||
console.error(`❌ [handleBatchSave] 조합 ${i + 1}/${combinations.length} 저장 오류:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user