ERP-node/frontend/hooks/pop/usePopEvent.ts

191 lines
5.6 KiB
TypeScript
Raw Permalink Normal View History

/**
* 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<callback> */
type ListenerMap = Map<string, Set<EventCallback>>;
/** 화면별 공유 데이터 맵: key -> value */
type SharedDataMap = Map<string, unknown>;
// ===== 전역 저장소 (React 외부, 모듈 스코프) =====
// SSR 환경에서 서버/클라이언트 간 공유 방지
/** screenId별 이벤트 리스너 저장소 */
const screenBuses: Map<string, ListenerMap> =
typeof window !== "undefined" ? new Map() : new Map();
/** screenId별 공유 데이터 저장소 */
const sharedDataStore: Map<string, SharedDataMap> =
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(
<T = unknown>(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;
}