diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 442c51cb..1f4d4dcc 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -1202,38 +1202,35 @@ export const EditModal: React.FC = ({ className }) => { toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다."); } - // V2Repeater 디테일 데이터 저장 (모달 닫기 전에 실행) - try { - const repeaterSavePromise = new Promise((resolve) => { - const fallbackTimeout = setTimeout(resolve, 5000); - const handler = () => { - clearTimeout(fallbackTimeout); - window.removeEventListener("repeaterSaveComplete", handler); - resolve(); - }; - window.addEventListener("repeaterSaveComplete", handler); - }); + // V2Repeater 디테일 데이터 저장 (등록된 인스턴스가 있을 때만) + const hasRepeaterForInsert = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0; + if (hasRepeaterForInsert) { + try { + const repeaterSavePromise = new Promise((resolve) => { + const fallbackTimeout = setTimeout(resolve, 5000); + const handler = () => { + clearTimeout(fallbackTimeout); + window.removeEventListener("repeaterSaveComplete", handler); + resolve(); + }; + window.addEventListener("repeaterSaveComplete", handler); + }); - console.log("🟢 [EditModal] INSERT 후 repeaterSave 이벤트 발행:", { - parentId: masterRecordId, - tableName: screenData.screenInfo.tableName, - }); + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: masterRecordId, + tableName: screenData.screenInfo.tableName, + mainFormData: formData, + masterRecordId, + }, + }), + ); - window.dispatchEvent( - new CustomEvent("repeaterSave", { - detail: { - parentId: masterRecordId, - tableName: screenData.screenInfo.tableName, - mainFormData: formData, - masterRecordId, - }, - }), - ); - - await repeaterSavePromise; - console.log("✅ [EditModal] INSERT 후 repeaterSave 완료"); - } catch (repeaterError) { - console.error("❌ [EditModal] repeaterSave 오류:", repeaterError); + await repeaterSavePromise; + } catch (repeaterError) { + console.error("❌ [EditModal] repeaterSave 오류:", repeaterError); + } } handleClose(); @@ -1332,38 +1329,35 @@ export const EditModal: React.FC = ({ className }) => { toast.warning("저장은 완료되었으나 연결된 제어 실행 중 오류가 발생했습니다."); } - // V2Repeater 디테일 데이터 저장 (모달 닫기 전에 실행) - try { - const repeaterSavePromise = new Promise((resolve) => { - const fallbackTimeout = setTimeout(resolve, 5000); - const handler = () => { - clearTimeout(fallbackTimeout); - window.removeEventListener("repeaterSaveComplete", handler); - resolve(); - }; - window.addEventListener("repeaterSaveComplete", handler); - }); + // V2Repeater 디테일 데이터 저장 (등록된 인스턴스가 있을 때만) + const hasRepeaterForUpdate = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0; + if (hasRepeaterForUpdate) { + try { + const repeaterSavePromise = new Promise((resolve) => { + const fallbackTimeout = setTimeout(resolve, 5000); + const handler = () => { + clearTimeout(fallbackTimeout); + window.removeEventListener("repeaterSaveComplete", handler); + resolve(); + }; + window.addEventListener("repeaterSaveComplete", handler); + }); - console.log("🟢 [EditModal] UPDATE 후 repeaterSave 이벤트 발행:", { - parentId: recordId, - tableName: screenData.screenInfo.tableName, - }); + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: recordId, + tableName: screenData.screenInfo.tableName, + mainFormData: formData, + masterRecordId: recordId, + }, + }), + ); - window.dispatchEvent( - new CustomEvent("repeaterSave", { - detail: { - parentId: recordId, - tableName: screenData.screenInfo.tableName, - mainFormData: formData, - masterRecordId: recordId, - }, - }), - ); - - await repeaterSavePromise; - console.log("✅ [EditModal] UPDATE 후 repeaterSave 완료"); - } catch (repeaterError) { - console.error("❌ [EditModal] repeaterSave 오류:", repeaterError); + await repeaterSavePromise; + } catch (repeaterError) { + console.error("❌ [EditModal] repeaterSave 오류:", repeaterError); + } } // 리피터 저장 완료 후 메인 테이블 새로고침 diff --git a/frontend/components/v2/V2Repeater.tsx b/frontend/components/v2/V2Repeater.tsx index 8b769b56..b60617e6 100644 --- a/frontend/components/v2/V2Repeater.tsx +++ b/frontend/components/v2/V2Repeater.tsx @@ -50,9 +50,6 @@ export const V2Repeater: React.FC = ({ formData: parentFormData, ...restProps }) => { - // ScreenModal에서 전달된 groupedData (모달 간 데이터 전달용) - const groupedData = (restProps as any).groupedData || (restProps as any)._groupedData; - // componentId 결정: 직접 전달 또는 component 객체에서 추출 const effectiveComponentId = componentId || (restProps as any).component?.id; @@ -214,21 +211,20 @@ export const V2Repeater: React.FC = ({ const isModalMode = config.renderMode === "modal"; // 전역 리피터 등록 - // 🆕 useCustomTable이 설정된 경우 mainTableName 사용 (실제 저장될 테이블) + // tableName이 비어있어도 반드시 등록 (repeaterSave 이벤트 발행 가드에 필요) useEffect(() => { const targetTableName = config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; + const registrationKey = targetTableName || "__v2_repeater_same_table__"; - if (targetTableName) { - if (!window.__v2RepeaterInstances) { - window.__v2RepeaterInstances = new Set(); - } - window.__v2RepeaterInstances.add(targetTableName); + if (!window.__v2RepeaterInstances) { + window.__v2RepeaterInstances = new Set(); } + window.__v2RepeaterInstances.add(registrationKey); return () => { - if (targetTableName && window.__v2RepeaterInstances) { - window.__v2RepeaterInstances.delete(targetTableName); + if (window.__v2RepeaterInstances) { + window.__v2RepeaterInstances.delete(registrationKey); } }; }, [config.useCustomTable, config.mainTableName, config.dataSource?.tableName]); @@ -968,90 +964,8 @@ export const V2Repeater: React.FC = ({ [], ); - // 모달에서 전달된 groupedData를 초기 행 데이터로 변환 (컬럼 매핑 포함) - const groupedDataProcessedRef = useRef(false); - 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(); - 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; - 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]); + // V2Repeater는 자체 데이터 관리 (아이템 선택 모달, useCustomTable 로딩, DataReceiver)를 사용. + // EditModal의 groupedData는 메인 테이블 레코드이므로 V2Repeater에서는 사용하지 않음. // parentSequence 컬럼의 부모 필드 값이 변경되면 행 데이터 갱신 useEffect(() => { diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 88eaf946..85532c36 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -546,10 +546,12 @@ export const DynamicComponentRenderer: React.FC = let currentValue; if (componentType === "modal-repeater-table" || componentType === "repeat-screen-modal" || - componentType === "selected-items-detail-input" || - componentType === "v2-repeater") { + componentType === "selected-items-detail-input") { // EditModal/ScreenModal에서 전달된 groupedData가 있으면 우선 사용 currentValue = props.groupedData || formData?.[fieldName] || []; + } else if (componentType === "v2-repeater") { + // V2Repeater는 자체 데이터 관리 (groupedData는 메인 테이블 레코드이므로 사용하지 않음) + currentValue = formData?.[fieldName] || []; } else { currentValue = formData?.[fieldName] || ""; } diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 390404ce..7f8514ab 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -1893,29 +1893,34 @@ export class ButtonActionExecutor { mainFormDataKeys: Object.keys(mainFormData), }); - // V2Repeater 저장 완료를 기다리기 위한 Promise - const repeaterSavePromise = new Promise((resolve) => { - const fallbackTimeout = setTimeout(resolve, 5000); - const handler = () => { - clearTimeout(fallbackTimeout); - window.removeEventListener("repeaterSaveComplete", handler); - resolve(); - }; - window.addEventListener("repeaterSaveComplete", handler); - }); + // V2Repeater가 등록된 경우에만 저장 완료를 기다림 + // @ts-ignore + const hasActiveRepeaters = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0; - window.dispatchEvent( - new CustomEvent("repeaterSave", { - detail: { - parentId: savedId, - tableName: context.tableName, - mainFormData, - masterRecordId: savedId, - }, - }), - ); + if (hasActiveRepeaters) { + const repeaterSavePromise = new Promise((resolve) => { + const fallbackTimeout = setTimeout(resolve, 5000); + const handler = () => { + clearTimeout(fallbackTimeout); + window.removeEventListener("repeaterSaveComplete", handler); + resolve(); + }; + window.addEventListener("repeaterSaveComplete", handler); + }); - await repeaterSavePromise; + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: savedId, + tableName: context.tableName, + mainFormData, + masterRecordId: savedId, + }, + }), + ); + + await repeaterSavePromise; + } // 테이블과 플로우 새로고침 (모달 닫기 전에 실행) context.onRefresh?.(); @@ -1951,29 +1956,33 @@ export class ButtonActionExecutor { formDataKeys: Object.keys(formData), }); - const repeaterSavePromise = new Promise((resolve) => { - const fallbackTimeout = setTimeout(resolve, 5000); - const handler = () => { - clearTimeout(fallbackTimeout); - window.removeEventListener("repeaterSaveComplete", handler); - resolve(); - }; - window.addEventListener("repeaterSaveComplete", handler); - }); + // @ts-ignore + const hasActiveRepeaters = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0; - window.dispatchEvent( - new CustomEvent("repeaterSave", { - detail: { - parentId: savedId, - tableName: context.tableName, - mainFormData: formData, - masterRecordId: savedId, - }, - }), - ); + if (hasActiveRepeaters) { + const repeaterSavePromise = new Promise((resolve) => { + const fallbackTimeout = setTimeout(resolve, 5000); + const handler = () => { + clearTimeout(fallbackTimeout); + window.removeEventListener("repeaterSaveComplete", handler); + resolve(); + }; + window.addEventListener("repeaterSaveComplete", handler); + }); - await repeaterSavePromise; - console.log("✅ [dispatchRepeaterSave] repeaterSave 완료"); + window.dispatchEvent( + new CustomEvent("repeaterSave", { + detail: { + parentId: savedId, + tableName: context.tableName, + mainFormData: formData, + masterRecordId: savedId, + }, + }), + ); + + await repeaterSavePromise; + } } /**