191 lines
5.6 KiB
TypeScript
191 lines
5.6 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|