우측 패널 일괄삭제 기능
This commit is contained in:
@@ -257,18 +257,31 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
|
||||
groupFields.forEach((field: any) => {
|
||||
let fieldValue = record[field.name];
|
||||
if (fieldValue !== undefined && fieldValue !== null) {
|
||||
// 🔧 날짜 타입이면 YYYY-MM-DD 형식으로 변환 (타임존 제거)
|
||||
if (field.type === "date" || field.type === "datetime") {
|
||||
const dateStr = String(fieldValue);
|
||||
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (match) {
|
||||
const [, year, month, day] = match;
|
||||
fieldValue = `${year}-${month}-${day}`; // ISO 형식 유지 (시간 제거)
|
||||
}
|
||||
|
||||
// 🔧 값이 없으면 기본값 사용 (false, 0, "" 등 falsy 값도 유효한 값으로 처리)
|
||||
if (fieldValue === undefined || fieldValue === null) {
|
||||
// 기본값이 있으면 사용, 없으면 필드 타입에 따라 기본값 설정
|
||||
if (field.defaultValue !== undefined) {
|
||||
fieldValue = field.defaultValue;
|
||||
} else if (field.type === "checkbox") {
|
||||
fieldValue = false; // checkbox는 기본값 false
|
||||
} else {
|
||||
// 다른 타입은 null로 유지 (필수 필드가 아니면 표시 안 됨)
|
||||
return;
|
||||
}
|
||||
entryData[field.name] = fieldValue;
|
||||
}
|
||||
|
||||
// 🔧 날짜 타입이면 YYYY-MM-DD 형식으로 변환 (타임존 제거)
|
||||
if (field.type === "date" || field.type === "datetime") {
|
||||
const dateStr = String(fieldValue);
|
||||
const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (match) {
|
||||
const [, year, month, day] = match;
|
||||
fieldValue = `${year}-${month}-${day}`; // ISO 형식 유지 (시간 제거)
|
||||
}
|
||||
}
|
||||
|
||||
entryData[field.name] = fieldValue;
|
||||
});
|
||||
|
||||
// 🔑 모든 필드 값을 합쳐서 고유 키 생성 (중복 제거 기준)
|
||||
@@ -347,6 +360,59 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [modalData, component.id, componentConfig.fieldGroups, formData]); // formData 의존성 추가
|
||||
|
||||
// 🆕 Cartesian Product 생성 함수 (items에서 모든 그룹의 조합을 생성)
|
||||
const generateCartesianProduct = useCallback((itemsList: ItemData[]): Record<string, any>[] => {
|
||||
const allRecords: Record<string, any>[] = [];
|
||||
const groups = componentConfig.fieldGroups || [];
|
||||
const additionalFields = componentConfig.additionalFields || [];
|
||||
|
||||
itemsList.forEach((item) => {
|
||||
// 각 그룹의 엔트리 배열들을 준비
|
||||
const groupEntriesArrays: GroupEntry[][] = groups.map(group => item.fieldGroups[group.id] || []);
|
||||
|
||||
// Cartesian Product 재귀 함수
|
||||
const cartesian = (arrays: GroupEntry[][], currentIndex: number, currentCombination: Record<string, any>) => {
|
||||
if (currentIndex === arrays.length) {
|
||||
// 모든 그룹을 순회했으면 조합 완성
|
||||
allRecords.push({ ...currentCombination });
|
||||
return;
|
||||
}
|
||||
|
||||
const currentGroupEntries = arrays[currentIndex];
|
||||
if (currentGroupEntries.length === 0) {
|
||||
// 현재 그룹에 데이터가 없으면 빈 조합으로 다음 그룹 진행
|
||||
cartesian(arrays, currentIndex + 1, currentCombination);
|
||||
return;
|
||||
}
|
||||
|
||||
// 현재 그룹의 각 엔트리마다 재귀
|
||||
currentGroupEntries.forEach(entry => {
|
||||
const newCombination = { ...currentCombination };
|
||||
|
||||
// 현재 그룹의 필드들을 조합에 추가
|
||||
const groupFields = additionalFields.filter(f => f.groupId === groups[currentIndex].id);
|
||||
groupFields.forEach(field => {
|
||||
if (entry[field.name] !== undefined) {
|
||||
newCombination[field.name] = entry[field.name];
|
||||
}
|
||||
});
|
||||
|
||||
cartesian(arrays, currentIndex + 1, newCombination);
|
||||
});
|
||||
};
|
||||
|
||||
// 재귀 시작
|
||||
cartesian(groupEntriesArrays, 0, {});
|
||||
});
|
||||
|
||||
console.log("🔀 [generateCartesianProduct] 생성된 레코드:", {
|
||||
count: allRecords.length,
|
||||
records: allRecords,
|
||||
});
|
||||
|
||||
return allRecords;
|
||||
}, [componentConfig.fieldGroups, componentConfig.additionalFields]);
|
||||
|
||||
// 🆕 저장 요청 시에만 데이터 전달 (이벤트 리스너 방식)
|
||||
useEffect(() => {
|
||||
const handleSaveRequest = async (event: Event) => {
|
||||
@@ -377,17 +443,40 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
// 🔄 수정 모드: UPSERT API 사용
|
||||
try {
|
||||
console.log("🔄 [SelectedItemsDetailInput] UPSERT 모드로 저장 시작");
|
||||
console.log("📋 [SelectedItemsDetailInput] componentConfig:", {
|
||||
targetTable: componentConfig.targetTable,
|
||||
parentDataMapping: componentConfig.parentDataMapping,
|
||||
fieldGroups: componentConfig.fieldGroups,
|
||||
additionalFields: componentConfig.additionalFields,
|
||||
});
|
||||
|
||||
// 부모 키 추출 (parentDataMapping에서)
|
||||
const parentKeys: Record<string, any> = {};
|
||||
|
||||
// formData 또는 items[0].originalData에서 부모 데이터 가져오기
|
||||
const sourceData = formData || items[0]?.originalData || {};
|
||||
// formData가 배열이면 첫 번째 항목 사용
|
||||
let sourceData: any = formData;
|
||||
if (Array.isArray(formData) && formData.length > 0) {
|
||||
sourceData = formData[0];
|
||||
} else if (!formData) {
|
||||
sourceData = items[0]?.originalData || {};
|
||||
}
|
||||
|
||||
console.log("📦 [SelectedItemsDetailInput] 부모 데이터 소스:", {
|
||||
formDataType: Array.isArray(formData) ? "배열" : typeof formData,
|
||||
sourceData,
|
||||
sourceDataKeys: Object.keys(sourceData),
|
||||
parentDataMapping: componentConfig.parentDataMapping,
|
||||
});
|
||||
|
||||
console.log("🔍 [SelectedItemsDetailInput] sourceData 전체 내용 (JSON):", JSON.stringify(sourceData, null, 2));
|
||||
|
||||
componentConfig.parentDataMapping.forEach((mapping) => {
|
||||
const value = sourceData[mapping.sourceField];
|
||||
if (value !== undefined && value !== null) {
|
||||
parentKeys[mapping.targetField] = value;
|
||||
} else {
|
||||
console.warn(`⚠️ [SelectedItemsDetailInput] 부모 키 누락: ${mapping.sourceField} → ${mapping.targetField}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -402,10 +491,28 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
records,
|
||||
});
|
||||
|
||||
// targetTable 검증
|
||||
if (!componentConfig.targetTable) {
|
||||
console.error("❌ [SelectedItemsDetailInput] targetTable이 설정되지 않았습니다!");
|
||||
window.dispatchEvent(new CustomEvent("formSaveError", {
|
||||
detail: { message: "대상 테이블이 설정되지 않았습니다." },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("🎯 [SelectedItemsDetailInput] targetTable:", componentConfig.targetTable);
|
||||
console.log("📡 [SelectedItemsDetailInput] UPSERT API 호출 직전:", {
|
||||
tableName: componentConfig.targetTable,
|
||||
tableNameType: typeof componentConfig.targetTable,
|
||||
tableNameLength: componentConfig.targetTable?.length,
|
||||
parentKeys,
|
||||
recordsCount: records.length,
|
||||
});
|
||||
|
||||
// UPSERT API 호출
|
||||
const { dataApi } = await import("@/lib/api/data");
|
||||
const result = await dataApi.upsertGroupedRecords(
|
||||
componentConfig.targetTable || "",
|
||||
componentConfig.targetTable,
|
||||
parentKeys,
|
||||
records
|
||||
);
|
||||
@@ -469,7 +576,7 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
return () => {
|
||||
window.removeEventListener("beforeFormSave", handleSaveRequest as EventListener);
|
||||
};
|
||||
}, [items, component.id, onFormDataChange, componentConfig, formData]);
|
||||
}, [items, component.id, onFormDataChange, componentConfig, formData, generateCartesianProduct]);
|
||||
|
||||
// 스타일 계산
|
||||
const componentStyle: React.CSSProperties = {
|
||||
@@ -1027,6 +1134,15 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
|
||||
// 값이 있는 경우, 형식에 맞게 표시
|
||||
let formattedValue = fieldValue;
|
||||
|
||||
// 🔧 자동 날짜 감지 (format 설정 없어도 ISO 날짜 자동 변환)
|
||||
const strValue = String(fieldValue);
|
||||
const isoDateMatch = strValue.match(/^(\d{4})-(\d{2})-(\d{2})(T|\s|$)/);
|
||||
if (isoDateMatch && !displayItem.format) {
|
||||
const [, year, month, day] = isoDateMatch;
|
||||
formattedValue = `${year}.${month}.${day}`;
|
||||
}
|
||||
|
||||
switch (displayItem.format) {
|
||||
case "currency":
|
||||
// 천 단위 구분
|
||||
@@ -1075,9 +1191,19 @@ export const SelectedItemsDetailInputComponent: React.FC<SelectedItemsDetailInpu
|
||||
break;
|
||||
}
|
||||
|
||||
// 🔧 마지막 안전장치: formattedValue가 여전히 ISO 형식이면 한번 더 변환
|
||||
let finalValue = formattedValue;
|
||||
if (typeof formattedValue === 'string') {
|
||||
const isoCheck = formattedValue.match(/^(\d{4})-(\d{2})-(\d{2})(T|\s|$)/);
|
||||
if (isoCheck) {
|
||||
const [, year, month, day] = isoCheck;
|
||||
finalValue = `${year}.${month}.${day}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span key={displayItem.id} className={styleClasses} style={inlineStyle}>
|
||||
{displayItem.label}{formattedValue}
|
||||
{displayItem.label}{finalValue}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user