"use client"; /** * UnifiedRepeater 컴포넌트 * * 렌더링 모드: * - inline: 현재 테이블 컬럼 직접 입력 * - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼 * * RepeaterTable 및 ItemSelectionModal 재사용 */ import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Plus } from "lucide-react"; import { cn } from "@/lib/utils"; import { UnifiedRepeaterConfig, UnifiedRepeaterProps, RepeaterColumnConfig as UnifiedColumnConfig, DEFAULT_REPEATER_CONFIG, } from "@/types/unified-repeater"; import { apiClient } from "@/lib/api/client"; import { allocateNumberingCode } from "@/lib/api/numberingRule"; // modal-repeater-table 컴포넌트 재사용 import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/RepeaterTable"; import { ItemSelectionModal } from "@/lib/registry/components/modal-repeater-table/ItemSelectionModal"; import { RepeaterColumnConfig } from "@/lib/registry/components/modal-repeater-table/types"; // 전역 UnifiedRepeater 등록 (buttonActions에서 사용) declare global { interface Window { __unifiedRepeaterInstances?: Set; } } export const UnifiedRepeater: React.FC = ({ config: propConfig, parentId, data: initialData, onDataChange, onRowClick, className, }) => { // 설정 병합 const config: UnifiedRepeaterConfig = useMemo( () => ({ ...DEFAULT_REPEATER_CONFIG, ...propConfig, dataSource: { ...DEFAULT_REPEATER_CONFIG.dataSource, ...propConfig.dataSource }, features: { ...DEFAULT_REPEATER_CONFIG.features, ...propConfig.features }, modal: { ...DEFAULT_REPEATER_CONFIG.modal, ...propConfig.modal }, }), [propConfig], ); // 상태 const [data, setData] = useState(initialData || []); const [selectedRows, setSelectedRows] = useState>(new Set()); const [modalOpen, setModalOpen] = useState(false); // 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 트리거 const [autoWidthTrigger, setAutoWidthTrigger] = useState(0); // 소스 테이블 컬럼 라벨 매핑 const [sourceColumnLabels, setSourceColumnLabels] = useState>({}); // 🆕 소스 테이블의 카테고리 타입 컬럼 목록 const [sourceCategoryColumns, setSourceCategoryColumns] = useState([]); // 🆕 카테고리 코드 → 라벨 매핑 (RepeaterTable 표시용) const [categoryLabelMap, setCategoryLabelMap] = useState>({}); // 현재 테이블 컬럼 정보 (inputType 매핑용) const [currentTableColumnInfo, setCurrentTableColumnInfo] = useState>({}); // 동적 데이터 소스 상태 const [activeDataSources, setActiveDataSources] = useState>({}); // 🆕 최신 엔티티 참조 정보 (column_labels에서 조회) const [resolvedSourceTable, setResolvedSourceTable] = useState(""); const [resolvedReferenceKey, setResolvedReferenceKey] = useState("id"); const isModalMode = config.renderMode === "modal"; // 전역 리피터 등록 useEffect(() => { const tableName = config.dataSource?.tableName; if (tableName) { if (!window.__unifiedRepeaterInstances) { window.__unifiedRepeaterInstances = new Set(); } window.__unifiedRepeaterInstances.add(tableName); } return () => { if (tableName && window.__unifiedRepeaterInstances) { window.__unifiedRepeaterInstances.delete(tableName); } }; }, [config.dataSource?.tableName]); // 저장 이벤트 리스너 useEffect(() => { const handleSaveEvent = async (event: CustomEvent) => { // 🆕 mainTableName이 설정된 경우 우선 사용, 없으면 dataSource.tableName 사용 const tableName = config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName; const eventParentId = event.detail?.parentId; const mainFormData = event.detail?.mainFormData; // 🆕 마스터 테이블에서 생성된 ID (FK 연결용) const masterRecordId = event.detail?.masterRecordId || mainFormData?.id; if (!tableName || data.length === 0) { console.log("📋 UnifiedRepeater 저장 스킵:", { tableName, dataLength: data.length }); return; } console.log("📋 UnifiedRepeater 저장 시작:", { tableName, useCustomTable: config.useCustomTable, mainTableName: config.mainTableName, foreignKeyColumn: config.foreignKeyColumn, masterRecordId, dataLength: data.length }); try { // 테이블 유효 컬럼 조회 let validColumns: Set = new Set(); try { const columnsResponse = await apiClient.get(`/table-management/tables/${tableName}/columns`); const columns = columnsResponse.data?.data?.columns || columnsResponse.data?.columns || columnsResponse.data || []; validColumns = new Set(columns.map((col: any) => col.columnName || col.column_name || col.name)); } catch { console.warn("테이블 컬럼 정보 조회 실패"); } for (let i = 0; i < data.length; i++) { const row = data[i]; // 내부 필드 제거 const cleanRow = Object.fromEntries(Object.entries(row).filter(([key]) => !key.startsWith("_"))); // 메인 폼 데이터 병합 (커스텀 테이블 사용 시에는 메인 폼 데이터 병합 안함) let mergedData: Record; if (config.useCustomTable && config.mainTableName) { // 커스텀 테이블: 리피터 데이터만 저장 mergedData = { ...cleanRow }; // 🆕 FK 자동 연결 if (config.foreignKeyColumn && masterRecordId) { mergedData[config.foreignKeyColumn] = masterRecordId; console.log(`📎 FK 자동 연결: ${config.foreignKeyColumn} = ${masterRecordId}`); } } else { // 기존 방식: 메인 폼 데이터 병합 const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {}; mergedData = { ...mainFormDataWithoutId, ...cleanRow, }; } // 유효하지 않은 컬럼 제거 const filteredData: Record = {}; for (const [key, value] of Object.entries(mergedData)) { if (validColumns.size === 0 || validColumns.has(key)) { filteredData[key] = value; } } await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData); } console.log("✅ UnifiedRepeater 저장 완료:", data.length, "건 →", tableName); } catch (error) { console.error("❌ UnifiedRepeater 저장 실패:", error); throw error; } }; window.addEventListener("repeaterSave" as any, handleSaveEvent); return () => { window.removeEventListener("repeaterSave" as any, handleSaveEvent); }; }, [data, config.dataSource?.tableName, config.useCustomTable, config.mainTableName, config.foreignKeyColumn, parentId]); // 현재 테이블 컬럼 정보 로드 useEffect(() => { const loadCurrentTableColumnInfo = async () => { const tableName = config.dataSource?.tableName; if (!tableName) return; try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); const columns = response.data?.data?.columns || response.data?.columns || response.data || []; const columnMap: Record = {}; columns.forEach((col: any) => { const name = col.columnName || col.column_name || col.name; columnMap[name] = { inputType: col.inputType || col.input_type || col.webType || "text", displayName: col.displayName || col.display_name || col.label || name, detailSettings: col.detailSettings || col.detail_settings, }; }); setCurrentTableColumnInfo(columnMap); } catch (error) { console.error("컬럼 정보 로드 실패:", error); } }; loadCurrentTableColumnInfo(); }, [config.dataSource?.tableName]); // 🆕 FK 컬럼 기반으로 최신 참조 테이블 정보 조회 (column_labels에서) useEffect(() => { const resolveEntityReference = async () => { const tableName = config.dataSource?.tableName; const foreignKey = config.dataSource?.foreignKey; if (!isModalMode || !tableName || !foreignKey) { // config에 저장된 값을 기본값으로 사용 setResolvedSourceTable(config.dataSource?.sourceTable || ""); setResolvedReferenceKey(config.dataSource?.referenceKey || "id"); return; } try { // 현재 테이블의 컬럼 정보에서 FK 컬럼의 참조 테이블 조회 const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); const columns = response.data?.data?.columns || response.data?.columns || response.data || []; const fkColumn = columns.find((col: any) => (col.columnName || col.column_name || col.name) === foreignKey); if (fkColumn) { // column_labels의 reference_table 사용 (항상 최신값) const refTable = fkColumn.detailSettings?.referenceTable || fkColumn.reference_table || fkColumn.referenceTable || config.dataSource?.sourceTable || ""; const refKey = fkColumn.detailSettings?.referenceColumn || fkColumn.reference_column || fkColumn.referenceColumn || config.dataSource?.referenceKey || "id"; console.log("🔄 [UnifiedRepeater] 엔티티 참조 정보 조회:", { foreignKey, resolvedSourceTable: refTable, resolvedReferenceKey: refKey, configSourceTable: config.dataSource?.sourceTable, }); setResolvedSourceTable(refTable); setResolvedReferenceKey(refKey); } else { // FK 컬럼을 찾지 못한 경우 config 값 사용 setResolvedSourceTable(config.dataSource?.sourceTable || ""); setResolvedReferenceKey(config.dataSource?.referenceKey || "id"); } } catch (error) { console.error("엔티티 참조 정보 조회 실패:", error); // 오류 시 config 값 사용 setResolvedSourceTable(config.dataSource?.sourceTable || ""); setResolvedReferenceKey(config.dataSource?.referenceKey || "id"); } }; resolveEntityReference(); }, [ config.dataSource?.tableName, config.dataSource?.foreignKey, config.dataSource?.sourceTable, config.dataSource?.referenceKey, isModalMode, ]); // 소스 테이블 컬럼 라벨 로드 (modal 모드) - resolvedSourceTable 사용 // 🆕 카테고리 타입 컬럼도 함께 감지 useEffect(() => { const loadSourceColumnLabels = async () => { if (!isModalMode || !resolvedSourceTable) return; try { const response = await apiClient.get(`/table-management/tables/${resolvedSourceTable}/columns`); const columns = response.data?.data?.columns || response.data?.columns || response.data || []; const labels: Record = {}; const categoryCols: string[] = []; columns.forEach((col: any) => { const name = col.columnName || col.column_name || col.name; labels[name] = col.displayName || col.display_name || col.label || name; // 🆕 카테고리 타입 컬럼 감지 const inputType = col.inputType || col.input_type || ""; if (inputType === "category") { categoryCols.push(name); } }); setSourceColumnLabels(labels); setSourceCategoryColumns(categoryCols); } catch (error) { console.error("소스 컬럼 라벨 로드 실패:", error); } }; loadSourceColumnLabels(); }, [resolvedSourceTable, isModalMode]); // UnifiedColumnConfig → RepeaterColumnConfig 변환 // 🆕 모든 컬럼을 columns 배열의 순서대로 처리 (isSourceDisplay 플래그로 구분) const repeaterColumns: RepeaterColumnConfig[] = useMemo(() => { return config.columns .filter((col: UnifiedColumnConfig) => col.visible !== false) .map((col: UnifiedColumnConfig): RepeaterColumnConfig => { const colInfo = currentTableColumnInfo[col.key]; const inputType = col.inputType || colInfo?.inputType || "text"; // 소스 표시 컬럼인 경우 (모달 모드에서 읽기 전용) if (col.isSourceDisplay) { const label = col.title || sourceColumnLabels[col.key] || col.key; return { field: `_display_${col.key}`, label, type: "text", editable: false, calculated: true, width: col.width === "auto" ? undefined : col.width, }; } // 일반 입력 컬럼 let type: "text" | "number" | "date" | "select" | "category" = "text"; if (inputType === "number" || inputType === "decimal") type = "number"; else if (inputType === "date" || inputType === "datetime") type = "date"; else if (inputType === "code") type = "select"; else if (inputType === "category") type = "category"; // 🆕 카테고리 타입 // 🆕 카테고리 참조 ID 가져오기 (tableName.columnName 형식) // category 타입인 경우 현재 테이블명과 컬럼명을 조합 let categoryRef: string | undefined; if (inputType === "category") { // 🆕 소스 표시 컬럼이면 소스 테이블 사용, 아니면 타겟 테이블 사용 const tableName = col.isSourceDisplay ? resolvedSourceTable : config.dataSource?.tableName; if (tableName) { categoryRef = `${tableName}.${col.key}`; } } return { field: col.key, label: col.title || colInfo?.displayName || col.key, type, editable: col.editable !== false, width: col.width === "auto" ? undefined : col.width, required: false, categoryRef, // 🆕 카테고리 참조 ID 전달 hidden: col.hidden, // 🆕 히든 처리 autoFill: col.autoFill, // 🆕 자동 입력 설정 }; }); }, [config.columns, sourceColumnLabels, currentTableColumnInfo, resolvedSourceTable, config.dataSource?.tableName]); // 🆕 데이터 변경 시 카테고리 라벨 로드 (RepeaterTable 표시용) useEffect(() => { const loadCategoryLabels = async () => { if (sourceCategoryColumns.length === 0 || data.length === 0) { return; } // 데이터에서 카테고리 컬럼의 모든 고유 코드 수집 const allCodes = new Set(); for (const row of data) { for (const col of sourceCategoryColumns) { // _display_ 접두사가 있는 컬럼과 원본 컬럼 모두 확인 const val = row[`_display_${col}`] || row[col]; if (val && typeof val === "string") { const codes = val .split(",") .map((c: string) => c.trim()) .filter(Boolean); for (const code of codes) { if (!categoryLabelMap[code] && code.startsWith("CATEGORY_")) { allCodes.add(code); } } } } } if (allCodes.size === 0) { return; } try { const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: Array.from(allCodes), }); if (response.data?.success && response.data.data) { setCategoryLabelMap((prev) => ({ ...prev, ...response.data.data, })); } } catch (error) { console.error("카테고리 라벨 조회 실패:", error); } }; loadCategoryLabels(); }, [data, sourceCategoryColumns]); // 데이터 변경 핸들러 const handleDataChange = useCallback( (newData: any[]) => { setData(newData); onDataChange?.(newData); // 🆕 데이터 변경 시 자동으로 컬럼 너비 조정 setAutoWidthTrigger((prev) => prev + 1); }, [onDataChange], ); // 행 변경 핸들러 const handleRowChange = useCallback( (index: number, newRow: any) => { const newData = [...data]; newData[index] = newRow; // 🆕 handleDataChange 대신 직접 호출 (행 변경마다 너비 조정은 불필요) setData(newData); onDataChange?.(newData); }, [data, onDataChange], ); // 행 삭제 핸들러 const handleRowDelete = useCallback( (index: number) => { const newData = data.filter((_, i) => i !== index); handleDataChange(newData); // 🆕 handleDataChange 사용 // 선택 상태 업데이트 const newSelected = new Set(); selectedRows.forEach((i) => { if (i < index) newSelected.add(i); else if (i > index) newSelected.add(i - 1); }); setSelectedRows(newSelected); }, [data, selectedRows, handleDataChange], ); // 일괄 삭제 핸들러 const handleBulkDelete = useCallback(() => { const newData = data.filter((_, index) => !selectedRows.has(index)); handleDataChange(newData); // 🆕 handleDataChange 사용 setSelectedRows(new Set()); }, [data, selectedRows, handleDataChange]); // 행 추가 (inline 모드) // 🆕 자동 입력 값 생성 함수 (동기 - 채번 제외) const generateAutoFillValueSync = useCallback( (col: any, rowIndex: number, mainFormData?: Record) => { if (!col.autoFill || col.autoFill.type === "none") return undefined; const now = new Date(); switch (col.autoFill.type) { case "currentDate": return now.toISOString().split("T")[0]; // YYYY-MM-DD case "currentDateTime": return now.toISOString().slice(0, 19).replace("T", " "); // YYYY-MM-DD HH:mm:ss case "sequence": return rowIndex + 1; // 1부터 시작하는 순번 case "numbering": // 채번은 별도 비동기 처리 필요 return null; // null 반환하여 비동기 처리 필요함을 표시 case "fromMainForm": if (col.autoFill.sourceField && mainFormData) { return mainFormData[col.autoFill.sourceField]; } return ""; case "fixed": return col.autoFill.fixedValue ?? ""; default: return undefined; } }, [], ); // 🆕 채번 API 호출 (비동기) const generateNumberingCode = useCallback(async (ruleId: string): Promise => { try { const result = await allocateNumberingCode(ruleId); if (result.success && result.data?.generatedCode) { return result.data.generatedCode; } console.error("채번 실패:", result.error); return ""; } catch (error) { console.error("채번 API 호출 실패:", error); return ""; } }, []); // 🆕 행 추가 (inline 모드 또는 모달 열기) - 비동기로 변경 const handleAddRow = useCallback(async () => { if (isModalMode) { setModalOpen(true); } else { const newRow: any = { _id: `new_${Date.now()}` }; const currentRowCount = data.length; // 먼저 동기적 자동 입력 값 적용 for (const col of config.columns) { const autoValue = generateAutoFillValueSync(col, currentRowCount); if (autoValue === null && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId) { // 채번 규칙: 즉시 API 호출 newRow[col.key] = await generateNumberingCode(col.autoFill.numberingRuleId); } else if (autoValue !== undefined) { newRow[col.key] = autoValue; } else { newRow[col.key] = ""; } } const newData = [...data, newRow]; handleDataChange(newData); } }, [isModalMode, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode]); // 모달에서 항목 선택 - 비동기로 변경 const handleSelectItems = useCallback( async (items: Record[]) => { const fkColumn = config.dataSource?.foreignKey; const currentRowCount = data.length; // 채번이 필요한 컬럼 찾기 const numberingColumns = config.columns.filter( (col) => !col.isSourceDisplay && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId ); const newRows = await Promise.all( items.map(async (item, index) => { const row: any = { _id: `new_${Date.now()}_${Math.random()}` }; // FK 값 저장 (resolvedReferenceKey 사용) if (fkColumn && item[resolvedReferenceKey]) { row[fkColumn] = item[resolvedReferenceKey]; } // 모든 컬럼 처리 (순서대로) for (const col of config.columns) { if (col.isSourceDisplay) { // 소스 표시 컬럼: 소스 테이블에서 값 복사 (읽기 전용) row[`_display_${col.key}`] = item[col.key] || ""; } else { // 자동 입력 값 적용 const autoValue = generateAutoFillValueSync(col, currentRowCount + index); if (autoValue === null && col.autoFill?.type === "numbering" && col.autoFill.numberingRuleId) { // 채번 규칙: 즉시 API 호출 row[col.key] = await generateNumberingCode(col.autoFill.numberingRuleId); } else if (autoValue !== undefined) { row[col.key] = autoValue; } else if (row[col.key] === undefined) { // 입력 컬럼: 빈 값으로 초기화 row[col.key] = ""; } } } return row; }) ); const newData = [...data, ...newRows]; handleDataChange(newData); setModalOpen(false); }, [config.dataSource?.foreignKey, resolvedReferenceKey, config.columns, data, handleDataChange, generateAutoFillValueSync, generateNumberingCode], ); // 소스 컬럼 목록 (모달용) - 🆕 columns 배열에서 isSourceDisplay인 것만 필터링 const sourceColumns = useMemo(() => { return config.columns .filter((col) => col.isSourceDisplay && col.visible !== false) .map((col) => col.key) .filter((key) => key && key !== "none"); }, [config.columns]); // 🆕 beforeFormSave 이벤트에서 채번 placeholder를 실제 값으로 변환 const dataRef = useRef(data); dataRef.current = data; useEffect(() => { const handleBeforeFormSave = async (event: Event) => { const customEvent = event as CustomEvent; const formData = customEvent.detail?.formData; if (!formData || !dataRef.current.length) return; // 채번 placeholder가 있는 행들을 찾아서 실제 값으로 변환 const processedData = await Promise.all( dataRef.current.map(async (row) => { const newRow = { ...row }; for (const key of Object.keys(newRow)) { const value = newRow[key]; if (typeof value === "string" && value.startsWith("__NUMBERING_RULE__")) { // __NUMBERING_RULE__ruleId__ 형식에서 ruleId 추출 const match = value.match(/__NUMBERING_RULE__(.+)__/); if (match) { const ruleId = match[1]; try { const result = await allocateNumberingCode(ruleId); if (result.success && result.data?.generatedCode) { newRow[key] = result.data.generatedCode; } else { console.error("채번 실패:", result.error); newRow[key] = ""; // 채번 실패 시 빈 값 } } catch (error) { console.error("채번 API 호출 실패:", error); newRow[key] = ""; } } } } return newRow; }), ); // 처리된 데이터를 formData에 추가 const fieldName = config.fieldName || "repeaterData"; formData[fieldName] = processedData; }; window.addEventListener("beforeFormSave", handleBeforeFormSave); return () => { window.removeEventListener("beforeFormSave", handleBeforeFormSave); }; }, [config.fieldName]); // 🆕 데이터 전달 이벤트 리스너 (transferData 버튼 액션용) useEffect(() => { // componentDataTransfer: 특정 컴포넌트 ID로 데이터 전달 const handleComponentDataTransfer = async (event: Event) => { const customEvent = event as CustomEvent; const { targetComponentId, data: transferData, mappingRules, mode } = customEvent.detail || {}; // 이 컴포넌트가 대상인지 확인 if (targetComponentId !== parentId && targetComponentId !== config.fieldName) { return; } console.log("📥 [UnifiedRepeater] componentDataTransfer 수신:", { targetComponentId, dataCount: transferData?.length, mode, myId: parentId, }); if (!transferData || transferData.length === 0) { return; } // 데이터 매핑 처리 const mappedData = transferData.map((item: any, index: number) => { const newRow: any = { _id: `transfer_${Date.now()}_${index}` }; if (mappingRules && mappingRules.length > 0) { // 매핑 규칙이 있으면 적용 mappingRules.forEach((rule: any) => { newRow[rule.targetField] = item[rule.sourceField]; }); } else { // 매핑 규칙 없으면 그대로 복사 Object.assign(newRow, item); } return newRow; }); // mode에 따라 데이터 처리 if (mode === "replace") { handleDataChange(mappedData); } else if (mode === "merge") { // 중복 제거 후 병합 (id 기준) const existingIds = new Set(data.map((row) => row.id || row._id)); const newItems = mappedData.filter((row: any) => !existingIds.has(row.id || row._id)); handleDataChange([...data, ...newItems]); } else { // 기본: append handleDataChange([...data, ...mappedData]); } }; // splitPanelDataTransfer: 분할 패널에서 전역 이벤트로 전달 const handleSplitPanelDataTransfer = async (event: Event) => { const customEvent = event as CustomEvent; const { data: transferData, mappingRules, mode, sourcePosition } = customEvent.detail || {}; console.log("📥 [UnifiedRepeater] splitPanelDataTransfer 수신:", { dataCount: transferData?.length, mode, sourcePosition, }); if (!transferData || transferData.length === 0) { return; } // 데이터 매핑 처리 const mappedData = transferData.map((item: any, index: number) => { const newRow: any = { _id: `transfer_${Date.now()}_${index}` }; if (mappingRules && mappingRules.length > 0) { mappingRules.forEach((rule: any) => { newRow[rule.targetField] = item[rule.sourceField]; }); } else { Object.assign(newRow, item); } return newRow; }); // mode에 따라 데이터 처리 if (mode === "replace") { handleDataChange(mappedData); } else { handleDataChange([...data, ...mappedData]); } }; window.addEventListener("componentDataTransfer", handleComponentDataTransfer as EventListener); window.addEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener); return () => { window.removeEventListener("componentDataTransfer", handleComponentDataTransfer as EventListener); window.removeEventListener("splitPanelDataTransfer", handleSplitPanelDataTransfer as EventListener); }; }, [parentId, config.fieldName, data, handleDataChange]); return (
{/* 헤더 영역 */}
{data.length > 0 && `${data.length}개 항목`} {selectedRows.size > 0 && ` (${selectedRows.size}개 선택됨)`}
{selectedRows.size > 0 && ( )}
{/* Repeater 테이블 */} { setActiveDataSources((prev) => ({ ...prev, [field]: optionId })); }} selectedRows={selectedRows} onSelectionChange={setSelectedRows} equalizeWidthsTrigger={autoWidthTrigger} categoryColumns={sourceCategoryColumns} categoryLabelMap={categoryLabelMap} /> {/* 항목 선택 모달 (modal 모드) - 검색 필드는 표시 컬럼과 동일하게 자동 설정 */} {isModalMode && ( )}
); }; UnifiedRepeater.displayName = "UnifiedRepeater"; export default UnifiedRepeater;