/** * usePopEvent - POP 컴포넌트 간 이벤트 통신 훅 * * 같은 화면(screenId) 안에서만 동작하는 이벤트 버스. * 다른 screenId 간에는 완전히 격리됨. * * 주요 기능: * - publish/subscribe: 일회성 이벤트 (거래처 선택됨, 저장 완료 등) * - getSharedData/setSharedData: 지속성 상태 (버튼 클릭 시 다른 컴포넌트 값 수집용) * * 사용 패턴: * ```typescript * const { publish, subscribe, getSharedData, setSharedData } = usePopEvent("S001"); * * // 이벤트 구독 (반드시 useEffect 안에서, cleanup 필수) * useEffect(() => { * const unsub = subscribe("supplier-selected", (payload) => { * console.log(payload.supplierId); * }); * return unsub; * }, []); * * // 이벤트 발행 * publish("supplier-selected", { supplierId: "SUP-001" }); * * // 공유 데이터 저장/조회 * setSharedData("selectedSupplier", { id: "SUP-001" }); * const supplier = getSharedData("selectedSupplier"); * ``` */ import { useCallback, useRef } from "react"; // ===== 타입 정의 ===== /** 이벤트 콜백 함수 타입 */ type EventCallback = (payload: unknown) => void; /** 화면별 이벤트 리스너 맵: eventName -> Set */ type ListenerMap = Map>; /** 화면별 공유 데이터 맵: key -> value */ type SharedDataMap = Map; // ===== 전역 저장소 (React 외부, 모듈 스코프) ===== // SSR 환경에서 서버/클라이언트 간 공유 방지 /** screenId별 이벤트 리스너 저장소 */ const screenBuses: Map = typeof window !== "undefined" ? new Map() : new Map(); /** screenId별 공유 데이터 저장소 */ const sharedDataStore: Map = typeof window !== "undefined" ? new Map() : new Map(); // ===== 내부 헬퍼 ===== /** 해당 screenId의 리스너 맵 가져오기 (없으면 생성) */ function getListenerMap(screenId: string): ListenerMap { let map = screenBuses.get(screenId); if (!map) { map = new Map(); screenBuses.set(screenId, map); } return map; } /** 해당 screenId의 공유 데이터 맵 가져오기 (없으면 생성) */ function getSharedMap(screenId: string): SharedDataMap { let map = sharedDataStore.get(screenId); if (!map) { map = new Map(); sharedDataStore.set(screenId, map); } return map; } // ===== 외부 API: 화면 정리 ===== /** * 화면 언마운트 시 해당 screenId의 모든 리스너 + 공유 데이터 정리 * 메모리 누수 방지용. 뷰어 또는 PopRenderer에서 화면 전환 시 호출. */ export function cleanupScreen(screenId: string): void { screenBuses.delete(screenId); sharedDataStore.delete(screenId); } // ===== 메인 훅 ===== /** * POP 컴포넌트 간 이벤트 통신 훅 * * @param screenId - 화면 ID (같은 screenId 안에서만 통신) * @returns publish, subscribe, getSharedData, setSharedData */ export function usePopEvent(screenId: string) { // screenId를 ref로 저장 (콜백 안정성) const screenIdRef = useRef(screenId); screenIdRef.current = screenId; /** * 이벤트 발행 * 해당 screenId + eventName에 등록된 모든 콜백에 payload 전달 */ const publish = useCallback( (eventName: string, payload?: unknown): void => { const listeners = getListenerMap(screenIdRef.current); const callbacks = listeners.get(eventName); if (!callbacks || callbacks.size === 0) return; // Set을 배열로 복사 후 순회 (순회 중 unsubscribe 안전) const callbackArray = Array.from(callbacks); for (const cb of callbackArray) { try { cb(payload); } catch (err) { // 개별 콜백 에러가 다른 콜백 실행을 막지 않음 console.error( `[usePopEvent] 콜백 에러 (screen: ${screenIdRef.current}, event: ${eventName}):`, err ); } } }, [] ); /** * 이벤트 구독 * * 주의: 반드시 useEffect 안에서 호출하고, 반환값(unsubscribe)을 cleanup에서 호출할 것. * * @returns unsubscribe 함수 */ const subscribe = useCallback( (eventName: string, callback: EventCallback): (() => void) => { const listeners = getListenerMap(screenIdRef.current); let callbacks = listeners.get(eventName); if (!callbacks) { callbacks = new Set(); listeners.set(eventName, callbacks); } callbacks.add(callback); // unsubscribe 함수 반환 const capturedScreenId = screenIdRef.current; return () => { const map = screenBuses.get(capturedScreenId); if (!map) return; const cbs = map.get(eventName); if (!cbs) return; cbs.delete(callback); // 빈 Set 정리 if (cbs.size === 0) { map.delete(eventName); } }; }, [] ); /** * 공유 데이터 조회 * 다른 컴포넌트가 setSharedData로 저장한 값을 가져옴 */ const getSharedData = useCallback( (key: string): T | undefined => { const shared = sharedDataStore.get(screenIdRef.current); if (!shared) return undefined; return shared.get(key) as T | undefined; }, [] ); /** * 공유 데이터 저장 * 같은 screenId의 다른 컴포넌트가 getSharedData로 읽을 수 있음 */ const setSharedData = useCallback( (key: string, value: unknown): void => { const shared = getSharedMap(screenIdRef.current); shared.set(key, value); }, [] ); return { publish, subscribe, getSharedData, setSharedData } as const; }