V2 이벤트 시스템 통합 및 데이터 전달 인터페이스 구현: UnifiedRepeater 컴포넌트에 데이터 제공 및 수신 인터페이스를 추가하여 다른 컴포넌트와의 데이터 연동을 개선하였습니다. 또한, AggregationWidgetComponent와 RepeatContainerComponent에서 V2 표준 이벤트를 구독하여 데이터 변경 이벤트를 효율적으로 처리하도록 수정하였습니다. 이를 통해 컴포넌트 간의 데이터 흐름과 사용자 경험을 향상시켰습니다.

This commit is contained in:
juseok2
2026-01-29 23:20:23 +09:00
parent 8cdb8a3047
commit 3803b7dce1
16 changed files with 4654 additions and 85 deletions
@@ -8,6 +8,11 @@
* - modal: 엔티티 선택 (FK 저장) + 추가 입력 컬럼
*
* RepeaterTable 및 ItemSelectionModal 재사용
*
* 데이터 전달 인터페이스:
* - DataProvidable: 선택된 데이터 제공
* - DataReceivable: 외부에서 데이터 수신
* - repeaterDataChange 이벤트 발행
*/
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
@@ -28,6 +33,13 @@ import { RepeaterTable } from "@/lib/registry/components/modal-repeater-table/Re
import { ItemSelectionModal } from "@/lib/registry/components/modal-repeater-table/ItemSelectionModal";
import { RepeaterColumnConfig } from "@/lib/registry/components/modal-repeater-table/types";
// 데이터 전달 인터페이스
import type { DataProvidable, DataReceivable, DataReceiverConfig } from "@/types/data-transfer";
import { useScreenContextOptional } from "@/contexts/ScreenContext";
// V2 이벤트 시스템
import { V2_EVENTS, dispatchV2Event } from "@/types/component-events";
// 전역 UnifiedRepeater 등록 (buttonActions에서 사용)
declare global {
interface Window {
@@ -55,6 +67,9 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
[propConfig],
);
// ScreenContext (데이터 전달 인터페이스 등록용)
const screenContext = useScreenContextOptional();
// 상태
const [data, setData] = useState<any[]>(initialData || []);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
@@ -101,6 +116,123 @@ export const UnifiedRepeater: React.FC<UnifiedRepeaterProps> = ({
};
}, [config.dataSource?.tableName]);
// ============================================================
// DataProvidable 인터페이스 구현
// 다른 컴포넌트에서 이 리피터의 데이터를 가져갈 수 있게 함
// ============================================================
const dataProvider: DataProvidable = useMemo(() => ({
componentId: parentId || config.fieldName || "unified-repeater",
componentType: "unified-repeater",
// 선택된 행 데이터 반환
getSelectedData: () => {
return Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean);
},
// 전체 데이터 반환
getAllData: () => {
return [...data];
},
// 선택 초기화
clearSelection: () => {
setSelectedRows(new Set());
},
}), [parentId, config.fieldName, data, selectedRows]);
// ============================================================
// DataReceivable 인터페이스 구현
// 외부에서 이 리피터로 데이터를 전달받을 수 있게 함
// ============================================================
const dataReceiver: DataReceivable = useMemo(() => ({
componentId: parentId || config.fieldName || "unified-repeater",
componentType: "repeater",
// 데이터 수신 (append, replace, merge 모드 지원)
receiveData: async (incomingData: any[], receiverConfig: DataReceiverConfig) => {
if (!incomingData || incomingData.length === 0) return;
// 매핑 규칙 적용
const mappedData = incomingData.map((item, index) => {
const newRow: any = { _id: `received_${Date.now()}_${index}` };
if (receiverConfig.mappingRules && receiverConfig.mappingRules.length > 0) {
receiverConfig.mappingRules.forEach((rule) => {
const sourceValue = item[rule.sourceField];
newRow[rule.targetField] = sourceValue !== undefined ? sourceValue : rule.defaultValue;
});
} else {
// 매핑 규칙 없으면 그대로 복사
Object.assign(newRow, item);
}
return newRow;
});
// 모드에 따라 데이터 처리
switch (receiverConfig.mode) {
case "replace":
setData(mappedData);
onDataChange?.(mappedData);
break;
case "merge":
// 중복 제거 후 병합 (id 또는 _id 기준)
const existingIds = new Set(data.map((row) => row.id || row._id));
const newItems = mappedData.filter((row) => !existingIds.has(row.id || row._id));
const mergedData = [...data, ...newItems];
setData(mergedData);
onDataChange?.(mergedData);
break;
case "append":
default:
const appendedData = [...data, ...mappedData];
setData(appendedData);
onDataChange?.(appendedData);
break;
}
},
// 현재 데이터 반환
getData: () => {
return [...data];
},
}), [parentId, config.fieldName, data, onDataChange]);
// ============================================================
// ScreenContext에 DataProvider/DataReceiver 등록
// ============================================================
useEffect(() => {
if (screenContext && (parentId || config.fieldName)) {
const componentId = parentId || config.fieldName || "unified-repeater";
screenContext.registerDataProvider(componentId, dataProvider);
screenContext.registerDataReceiver(componentId, dataReceiver);
return () => {
screenContext.unregisterDataProvider(componentId);
screenContext.unregisterDataReceiver(componentId);
};
}
}, [screenContext, parentId, config.fieldName, dataProvider, dataReceiver]);
// ============================================================
// repeaterDataChange 이벤트 발행
// 데이터 변경 시 다른 컴포넌트(aggregation-widget 등)에 알림
// ============================================================
const prevDataLengthRef = useRef(data.length);
useEffect(() => {
// 데이터가 변경되었을 때만 이벤트 발행
if (typeof window !== "undefined" && data.length !== prevDataLengthRef.current) {
dispatchV2Event(V2_EVENTS.REPEATER_DATA_CHANGE, {
componentId: parentId || config.fieldName || "unified-repeater",
tableName: config.dataSource?.tableName || "",
data: data,
selectedData: Array.from(selectedRows).map((idx) => data[idx]).filter(Boolean),
});
prevDataLengthRef.current = data.length;
}
}, [data, selectedRows, parentId, config.fieldName, config.dataSource?.tableName]);
// 저장 이벤트 리스너
useEffect(() => {
const handleSaveEvent = async (event: CustomEvent) => {