/** * useConnectionResolver - 런타임 컴포넌트 연결 해석기 * * PopViewerWithModals에서 사용. * layout.dataFlow.connections를 읽고, 소스 컴포넌트의 __comp_output__ 이벤트를 * 타겟 컴포넌트의 __comp_input__ 이벤트로 자동 변환/중계한다. * * 이벤트 규칙: * 소스: __comp_output__${sourceComponentId}__${outputKey} * 타겟: __comp_input__${targetComponentId}__${inputKey} * * _auto 모드: * sourceOutput="_auto"인 연결은 소스/타겟의 connectionMeta를 비교하여 * key가 같고 category="event"인 쌍을 양방향으로 자동 라우팅한다. * (정방향: 소스->타겟, 역방향: 타겟->소스) */ import { useEffect, useRef } from "react"; import { usePopEvent } from "./usePopEvent"; import type { PopDataConnection } from "@/components/pop/designer/types/pop-layout"; import { PopComponentRegistry, } from "@/lib/registry/PopComponentRegistry"; interface UseConnectionResolverOptions { screenId: string; connections: PopDataConnection[]; componentTypes?: Map; } interface AutoMatchPair { sourceKey: string; targetKey: string; isFilter: boolean; } /** * 소스/타겟의 connectionMeta에서 자동 매칭 가능한 쌍을 찾는다. * 규칙 1: category="event"이고 key가 동일한 쌍 (이벤트 매칭) * 규칙 2: 소스 type="filter_value" + 타겟 type="filter_value" (필터 매칭) */ function getAutoMatchPairs( sourceType: string, targetType: string ): AutoMatchPair[] { const sourceDef = PopComponentRegistry.getComponent(sourceType); const targetDef = PopComponentRegistry.getComponent(targetType); if (!sourceDef?.connectionMeta?.sendable || !targetDef?.connectionMeta?.receivable) { return []; } const pairs: AutoMatchPair[] = []; for (const s of sourceDef.connectionMeta.sendable) { for (const r of targetDef.connectionMeta.receivable) { if (s.category === "event" && r.category === "event" && s.key === r.key) { pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: false }); } if (s.type === "filter_value" && r.type === "filter_value") { pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: true }); } } } return pairs; } export function useConnectionResolver({ screenId, connections, componentTypes, }: UseConnectionResolverOptions): void { const { publish, subscribe } = usePopEvent(screenId); const connectionsRef = useRef(connections); connectionsRef.current = connections; const componentTypesRef = useRef(componentTypes); componentTypesRef.current = componentTypes; useEffect(() => { if (!connections || connections.length === 0) return; const unsubscribers: (() => void)[] = []; for (const conn of connections) { const isAutoMode = conn.sourceOutput === "_auto" || !conn.sourceOutput; if (isAutoMode && componentTypesRef.current) { const sourceType = componentTypesRef.current.get(conn.sourceComponent); const targetType = componentTypesRef.current.get(conn.targetComponent); if (!sourceType || !targetType) continue; // 정방향: 소스 sendable -> 타겟 receivable const forwardPairs = getAutoMatchPairs(sourceType, targetType); for (const pair of forwardPairs) { const sourceEvent = `__comp_output__${conn.sourceComponent}__${pair.sourceKey}`; const targetEvent = `__comp_input__${conn.targetComponent}__${pair.targetKey}`; const unsub = subscribe(sourceEvent, (payload: unknown) => { if (pair.isFilter) { const data = payload as Record | null; const fieldName = data?.fieldName as string | undefined; const filterColumns = data?.filterColumns as string[] | undefined; const filterMode = (data?.filterMode as string) || "contains"; publish(targetEvent, { value: payload, filterConfig: fieldName ? { targetColumn: fieldName, targetColumns: filterColumns?.length ? filterColumns : [fieldName], filterMode } : conn.filterConfig, _connectionId: conn.id, }); } else { publish(targetEvent, { value: payload, _connectionId: conn.id, }); } }); unsubscribers.push(unsub); } // 역방향: 타겟 sendable -> 소스 receivable const reversePairs = getAutoMatchPairs(targetType, sourceType); for (const pair of reversePairs) { const sourceEvent = `__comp_output__${conn.targetComponent}__${pair.sourceKey}`; const targetEvent = `__comp_input__${conn.sourceComponent}__${pair.targetKey}`; const unsub = subscribe(sourceEvent, (payload: unknown) => { publish(targetEvent, { value: payload, _connectionId: conn.id, }); }); unsubscribers.push(unsub); } } else { const sourceEvent = `__comp_output__${conn.sourceComponent}__${conn.sourceOutput || conn.sourceField}`; const unsub = subscribe(sourceEvent, (payload: unknown) => { const targetEvent = `__comp_input__${conn.targetComponent}__${conn.targetInput || conn.targetField}`; let resolvedFilterConfig = conn.filterConfig; if (!resolvedFilterConfig) { const data = payload as Record | null; const fieldName = data?.fieldName as string | undefined; const filterColumns = data?.filterColumns as string[] | undefined; if (fieldName) { const filterMode = (data?.filterMode as string) || "contains"; resolvedFilterConfig = { targetColumn: fieldName, targetColumns: filterColumns?.length ? filterColumns : [fieldName], filterMode: filterMode as "equals" | "contains" | "starts_with" | "range" }; } } publish(targetEvent, { value: payload, filterConfig: resolvedFilterConfig, _connectionId: conn.id, }); }); unsubscribers.push(unsub); } } return () => { for (const unsub of unsubscribers) { unsub(); } }; }, [screenId, connections, subscribe, publish]); }