우측 패널 일괄삭제 기능

This commit is contained in:
kjs
2025-11-20 11:58:43 +09:00
parent c3f58feef7
commit e3b78309fa
7 changed files with 587 additions and 105 deletions

View File

@@ -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>
);
}