refactor: Improve V2Repeater integration and event handling
- Updated the EditModal component to check for registered V2Repeater instances before saving detail data, enhancing the reliability of the repeater save process. - Simplified the V2Repeater component by removing unnecessary groupedData handling, ensuring it manages its own data effectively. - Enhanced the DynamicComponentRenderer to correctly handle V2Repeater's data management, improving overall component behavior. - Refactored button actions to wait for V2Repeater save completion only when active repeaters are present, optimizing performance and user experience.
This commit is contained in:
parent
1b7163ee1a
commit
0f52c3adc2
|
|
@ -1202,7 +1202,9 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
|
toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// V2Repeater 디테일 데이터 저장 (모달 닫기 전에 실행)
|
// V2Repeater 디테일 데이터 저장 (등록된 인스턴스가 있을 때만)
|
||||||
|
const hasRepeaterForInsert = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;
|
||||||
|
if (hasRepeaterForInsert) {
|
||||||
try {
|
try {
|
||||||
const repeaterSavePromise = new Promise<void>((resolve) => {
|
const repeaterSavePromise = new Promise<void>((resolve) => {
|
||||||
const fallbackTimeout = setTimeout(resolve, 5000);
|
const fallbackTimeout = setTimeout(resolve, 5000);
|
||||||
|
|
@ -1214,11 +1216,6 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
window.addEventListener("repeaterSaveComplete", handler);
|
window.addEventListener("repeaterSaveComplete", handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("🟢 [EditModal] INSERT 후 repeaterSave 이벤트 발행:", {
|
|
||||||
parentId: masterRecordId,
|
|
||||||
tableName: screenData.screenInfo.tableName,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("repeaterSave", {
|
new CustomEvent("repeaterSave", {
|
||||||
detail: {
|
detail: {
|
||||||
|
|
@ -1231,10 +1228,10 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
await repeaterSavePromise;
|
await repeaterSavePromise;
|
||||||
console.log("✅ [EditModal] INSERT 후 repeaterSave 완료");
|
|
||||||
} catch (repeaterError) {
|
} catch (repeaterError) {
|
||||||
console.error("❌ [EditModal] repeaterSave 오류:", repeaterError);
|
console.error("❌ [EditModal] repeaterSave 오류:", repeaterError);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1332,7 +1329,9 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
|
toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// V2Repeater 디테일 데이터 저장 (모달 닫기 전에 실행)
|
// V2Repeater 디테일 데이터 저장 (등록된 인스턴스가 있을 때만)
|
||||||
|
const hasRepeaterForUpdate = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;
|
||||||
|
if (hasRepeaterForUpdate) {
|
||||||
try {
|
try {
|
||||||
const repeaterSavePromise = new Promise<void>((resolve) => {
|
const repeaterSavePromise = new Promise<void>((resolve) => {
|
||||||
const fallbackTimeout = setTimeout(resolve, 5000);
|
const fallbackTimeout = setTimeout(resolve, 5000);
|
||||||
|
|
@ -1344,11 +1343,6 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
window.addEventListener("repeaterSaveComplete", handler);
|
window.addEventListener("repeaterSaveComplete", handler);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("🟢 [EditModal] UPDATE 후 repeaterSave 이벤트 발행:", {
|
|
||||||
parentId: recordId,
|
|
||||||
tableName: screenData.screenInfo.tableName,
|
|
||||||
});
|
|
||||||
|
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent("repeaterSave", {
|
new CustomEvent("repeaterSave", {
|
||||||
detail: {
|
detail: {
|
||||||
|
|
@ -1361,10 +1355,10 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
await repeaterSavePromise;
|
await repeaterSavePromise;
|
||||||
console.log("✅ [EditModal] UPDATE 후 repeaterSave 완료");
|
|
||||||
} catch (repeaterError) {
|
} catch (repeaterError) {
|
||||||
console.error("❌ [EditModal] repeaterSave 오류:", repeaterError);
|
console.error("❌ [EditModal] repeaterSave 오류:", repeaterError);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 리피터 저장 완료 후 메인 테이블 새로고침
|
// 리피터 저장 완료 후 메인 테이블 새로고침
|
||||||
if (modalState.onSave) {
|
if (modalState.onSave) {
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,6 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
|
||||||
formData: parentFormData,
|
formData: parentFormData,
|
||||||
...restProps
|
...restProps
|
||||||
}) => {
|
}) => {
|
||||||
// ScreenModal에서 전달된 groupedData (모달 간 데이터 전달용)
|
|
||||||
const groupedData = (restProps as any).groupedData || (restProps as any)._groupedData;
|
|
||||||
|
|
||||||
// componentId 결정: 직접 전달 또는 component 객체에서 추출
|
// componentId 결정: 직접 전달 또는 component 객체에서 추출
|
||||||
const effectiveComponentId = componentId || (restProps as any).component?.id;
|
const effectiveComponentId = componentId || (restProps as any).component?.id;
|
||||||
|
|
||||||
|
|
@ -214,21 +211,20 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
|
||||||
const isModalMode = config.renderMode === "modal";
|
const isModalMode = config.renderMode === "modal";
|
||||||
|
|
||||||
// 전역 리피터 등록
|
// 전역 리피터 등록
|
||||||
// 🆕 useCustomTable이 설정된 경우 mainTableName 사용 (실제 저장될 테이블)
|
// tableName이 비어있어도 반드시 등록 (repeaterSave 이벤트 발행 가드에 필요)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const targetTableName =
|
const targetTableName =
|
||||||
config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
|
config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
|
||||||
|
const registrationKey = targetTableName || "__v2_repeater_same_table__";
|
||||||
|
|
||||||
if (targetTableName) {
|
|
||||||
if (!window.__v2RepeaterInstances) {
|
if (!window.__v2RepeaterInstances) {
|
||||||
window.__v2RepeaterInstances = new Set();
|
window.__v2RepeaterInstances = new Set();
|
||||||
}
|
}
|
||||||
window.__v2RepeaterInstances.add(targetTableName);
|
window.__v2RepeaterInstances.add(registrationKey);
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (targetTableName && window.__v2RepeaterInstances) {
|
if (window.__v2RepeaterInstances) {
|
||||||
window.__v2RepeaterInstances.delete(targetTableName);
|
window.__v2RepeaterInstances.delete(registrationKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [config.useCustomTable, config.mainTableName, config.dataSource?.tableName]);
|
}, [config.useCustomTable, config.mainTableName, config.dataSource?.tableName]);
|
||||||
|
|
@ -968,90 +964,8 @@ export const V2Repeater: React.FC<V2RepeaterProps> = ({
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 모달에서 전달된 groupedData를 초기 행 데이터로 변환 (컬럼 매핑 포함)
|
// V2Repeater는 자체 데이터 관리 (아이템 선택 모달, useCustomTable 로딩, DataReceiver)를 사용.
|
||||||
const groupedDataProcessedRef = useRef(false);
|
// EditModal의 groupedData는 메인 테이블 레코드이므로 V2Repeater에서는 사용하지 않음.
|
||||||
useEffect(() => {
|
|
||||||
if (!groupedData || !Array.isArray(groupedData) || groupedData.length === 0) return;
|
|
||||||
if (groupedDataProcessedRef.current) return;
|
|
||||||
|
|
||||||
groupedDataProcessedRef.current = true;
|
|
||||||
|
|
||||||
const newRows = groupedData.map((item: any, index: number) => {
|
|
||||||
const row: any = { _id: `grouped_${Date.now()}_${index}` };
|
|
||||||
|
|
||||||
for (const col of config.columns) {
|
|
||||||
let sourceValue = item[(col as any).sourceKey || col.key];
|
|
||||||
|
|
||||||
// 카테고리 코드 → 라벨 변환 (접두사 무관, categoryLabelMap 기반)
|
|
||||||
if (typeof sourceValue === "string" && categoryLabelMap[sourceValue]) {
|
|
||||||
sourceValue = categoryLabelMap[sourceValue];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (col.isSourceDisplay) {
|
|
||||||
row[col.key] = sourceValue ?? "";
|
|
||||||
row[`_display_${col.key}`] = sourceValue ?? "";
|
|
||||||
} else if (col.autoFill && col.autoFill.type !== "none") {
|
|
||||||
const autoValue = generateAutoFillValueSync(col, index, parentFormData);
|
|
||||||
if (autoValue !== undefined) {
|
|
||||||
row[col.key] = autoValue;
|
|
||||||
} else {
|
|
||||||
row[col.key] = "";
|
|
||||||
}
|
|
||||||
} else if (sourceValue !== undefined) {
|
|
||||||
row[col.key] = sourceValue;
|
|
||||||
} else {
|
|
||||||
row[col.key] = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return row;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 카테고리 컬럼의 코드 → 라벨 변환 (접두사 무관)
|
|
||||||
const categoryColSet = new Set(allCategoryColumns);
|
|
||||||
const codesToResolve = new Set<string>();
|
|
||||||
for (const row of newRows) {
|
|
||||||
for (const col of config.columns) {
|
|
||||||
const val = row[col.key] || row[`_display_${col.key}`];
|
|
||||||
if (typeof val === "string" && val && (categoryColSet.has(col.key) || col.autoFill?.type === "fromMainForm")) {
|
|
||||||
if (!categoryLabelMap[val]) {
|
|
||||||
codesToResolve.add(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codesToResolve.size > 0) {
|
|
||||||
apiClient.post("/table-categories/labels-by-codes", {
|
|
||||||
valueCodes: Array.from(codesToResolve),
|
|
||||||
}).then((resp) => {
|
|
||||||
if (resp.data?.success && resp.data.data) {
|
|
||||||
const labelData = resp.data.data as Record<string, string>;
|
|
||||||
setCategoryLabelMap((prev) => ({ ...prev, ...labelData }));
|
|
||||||
const convertedRows = newRows.map((row) => {
|
|
||||||
const updated = { ...row };
|
|
||||||
for (const col of config.columns) {
|
|
||||||
const val = updated[col.key];
|
|
||||||
if (typeof val === "string" && labelData[val]) {
|
|
||||||
updated[col.key] = labelData[val];
|
|
||||||
}
|
|
||||||
const dispKey = `_display_${col.key}`;
|
|
||||||
const dispVal = updated[dispKey];
|
|
||||||
if (typeof dispVal === "string" && labelData[dispVal]) {
|
|
||||||
updated[dispKey] = labelData[dispVal];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
setData(convertedRows);
|
|
||||||
onDataChange?.(convertedRows);
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(newRows);
|
|
||||||
onDataChange?.(newRows);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [groupedData, config.columns, generateAutoFillValueSync]);
|
|
||||||
|
|
||||||
// parentSequence 컬럼의 부모 필드 값이 변경되면 행 데이터 갱신
|
// parentSequence 컬럼의 부모 필드 값이 변경되면 행 데이터 갱신
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -546,10 +546,12 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
let currentValue;
|
let currentValue;
|
||||||
if (componentType === "modal-repeater-table" ||
|
if (componentType === "modal-repeater-table" ||
|
||||||
componentType === "repeat-screen-modal" ||
|
componentType === "repeat-screen-modal" ||
|
||||||
componentType === "selected-items-detail-input" ||
|
componentType === "selected-items-detail-input") {
|
||||||
componentType === "v2-repeater") {
|
|
||||||
// EditModal/ScreenModal에서 전달된 groupedData가 있으면 우선 사용
|
// EditModal/ScreenModal에서 전달된 groupedData가 있으면 우선 사용
|
||||||
currentValue = props.groupedData || formData?.[fieldName] || [];
|
currentValue = props.groupedData || formData?.[fieldName] || [];
|
||||||
|
} else if (componentType === "v2-repeater") {
|
||||||
|
// V2Repeater는 자체 데이터 관리 (groupedData는 메인 테이블 레코드이므로 사용하지 않음)
|
||||||
|
currentValue = formData?.[fieldName] || [];
|
||||||
} else {
|
} else {
|
||||||
currentValue = formData?.[fieldName] || "";
|
currentValue = formData?.[fieldName] || "";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1893,7 +1893,11 @@ export class ButtonActionExecutor {
|
||||||
mainFormDataKeys: Object.keys(mainFormData),
|
mainFormDataKeys: Object.keys(mainFormData),
|
||||||
});
|
});
|
||||||
|
|
||||||
// V2Repeater 저장 완료를 기다리기 위한 Promise
|
// V2Repeater가 등록된 경우에만 저장 완료를 기다림
|
||||||
|
// @ts-ignore
|
||||||
|
const hasActiveRepeaters = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;
|
||||||
|
|
||||||
|
if (hasActiveRepeaters) {
|
||||||
const repeaterSavePromise = new Promise<void>((resolve) => {
|
const repeaterSavePromise = new Promise<void>((resolve) => {
|
||||||
const fallbackTimeout = setTimeout(resolve, 5000);
|
const fallbackTimeout = setTimeout(resolve, 5000);
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
|
|
@ -1916,6 +1920,7 @@ export class ButtonActionExecutor {
|
||||||
);
|
);
|
||||||
|
|
||||||
await repeaterSavePromise;
|
await repeaterSavePromise;
|
||||||
|
}
|
||||||
|
|
||||||
// 테이블과 플로우 새로고침 (모달 닫기 전에 실행)
|
// 테이블과 플로우 새로고침 (모달 닫기 전에 실행)
|
||||||
context.onRefresh?.();
|
context.onRefresh?.();
|
||||||
|
|
@ -1951,6 +1956,10 @@ export class ButtonActionExecutor {
|
||||||
formDataKeys: Object.keys(formData),
|
formDataKeys: Object.keys(formData),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const hasActiveRepeaters = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;
|
||||||
|
|
||||||
|
if (hasActiveRepeaters) {
|
||||||
const repeaterSavePromise = new Promise<void>((resolve) => {
|
const repeaterSavePromise = new Promise<void>((resolve) => {
|
||||||
const fallbackTimeout = setTimeout(resolve, 5000);
|
const fallbackTimeout = setTimeout(resolve, 5000);
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
|
|
@ -1973,7 +1982,7 @@ export class ButtonActionExecutor {
|
||||||
);
|
);
|
||||||
|
|
||||||
await repeaterSavePromise;
|
await repeaterSavePromise;
|
||||||
console.log("✅ [dispatchRepeaterSave] repeaterSave 완료");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue