/** * V2 EventBus - 타입 안전한 이벤트 버스 * * 특징: * - 타입 안전한 이벤트 발행/구독 * - 에러 격리 (하나의 핸들러 실패가 다른 핸들러에 영향 없음) * - 병렬/순차 실행 지원 * - 디버그 모드 지원 * - 구독 자동 정리 (컴포넌트 언마운트 시) */ import { V2_EVENTS, V2EventName, V2EventPayloadMap, V2EventHandler, V2Unsubscribe, } from "./types"; interface SubscriberInfo { id: string; handler: V2EventHandler; componentId?: string; once: boolean; } interface EmitOptions { /** 병렬 실행 여부 (기본값: true) */ parallel?: boolean; /** 타임아웃 (ms, 기본값: 5000) */ timeout?: number; /** 실패 시 재시도 횟수 (기본값: 0) */ retryCount?: number; } interface EmitResult { success: boolean; handlerCount: number; errors: Array<{ subscriberId: string; error: Error }>; } class V2EventBus { private subscribers: Map>> = new Map(); private subscriberIdCounter = 0; /** 디버그 모드 활성화 시 모든 이벤트 로깅 */ public debug = false; /** 디버그용 로거 */ private log(message: string, ...args: any[]) { if (this.debug) { console.log(`[V2EventBus] ${message}`, ...args); } } /** 에러 로거 */ private logError(message: string, error: any) { console.error(`[V2EventBus] ${message}`, error); } /** * 이벤트 구독 * * @param eventName - 이벤트 이름 * @param handler - 이벤트 핸들러 * @param options - 구독 옵션 * @returns 구독 해제 함수 * * @example * ```typescript * const unsubscribe = v2EventBus.subscribe( * V2_EVENTS.TABLE_REFRESH, * (payload) => { * console.log("테이블 새로고침:", payload.tableName); * }, * { componentId: "my-table" } * ); * * // 컴포넌트 언마운트 시 * unsubscribe(); * ``` */ subscribe( eventName: T, handler: V2EventHandler, options?: { componentId?: string; once?: boolean } ): V2Unsubscribe { const subscriberId = `sub_${++this.subscriberIdCounter}`; if (!this.subscribers.has(eventName)) { this.subscribers.set(eventName, new Map()); } const eventSubscribers = this.subscribers.get(eventName)!; eventSubscribers.set(subscriberId, { id: subscriberId, handler, componentId: options?.componentId, once: options?.once ?? false, }); this.log( `구독 등록: ${eventName} (${subscriberId})`, options?.componentId ? `컴포넌트: ${options.componentId}` : "" ); // 구독 해제 함수 반환 return () => { this.unsubscribe(eventName, subscriberId); }; } /** * 일회성 이벤트 구독 (한 번 실행 후 자동 해제) */ once( eventName: T, handler: V2EventHandler, options?: { componentId?: string } ): V2Unsubscribe { return this.subscribe(eventName, handler, { ...options, once: true }); } /** * 이벤트 구독 해제 */ private unsubscribe(eventName: V2EventName, subscriberId: string): void { const eventSubscribers = this.subscribers.get(eventName); if (eventSubscribers) { eventSubscribers.delete(subscriberId); this.log(`구독 해제: ${eventName} (${subscriberId})`); // 구독자가 없으면 Map 정리 if (eventSubscribers.size === 0) { this.subscribers.delete(eventName); } } } /** * 특정 컴포넌트의 모든 구독 해제 * * @param componentId - 컴포넌트 ID * * @example * ```typescript * // useEffect cleanup에서 사용 * useEffect(() => { * return () => { * v2EventBus.unsubscribeByComponent("my-table-component"); * }; * }, []); * ``` */ unsubscribeByComponent(componentId: string): void { let unsubscribedCount = 0; this.subscribers.forEach((eventSubscribers, eventName) => { eventSubscribers.forEach((subscriber, subscriberId) => { if (subscriber.componentId === componentId) { eventSubscribers.delete(subscriberId); unsubscribedCount++; } }); // 구독자가 없으면 Map 정리 if (eventSubscribers.size === 0) { this.subscribers.delete(eventName); } }); this.log( `컴포넌트 구독 해제: ${componentId} (${unsubscribedCount}개 해제)` ); } /** * 이벤트 발행 * * @param eventName - 이벤트 이름 * @param payload - 이벤트 데이터 * @param options - 발행 옵션 * @returns 발행 결과 * * @example * ```typescript * await v2EventBus.emit(V2_EVENTS.TABLE_REFRESH, { * tableName: "item_info", * target: "single", * }); * ``` */ async emit( eventName: T, payload: V2EventPayloadMap[T], options: EmitOptions = {} ): Promise { const { parallel = true, timeout = 5000, retryCount = 0 } = options; const eventSubscribers = this.subscribers.get(eventName); if (!eventSubscribers || eventSubscribers.size === 0) { this.log(`이벤트 발행 (구독자 없음): ${eventName}`); return { success: true, handlerCount: 0, errors: [] }; } this.log(`이벤트 발행: ${eventName} → ${eventSubscribers.size}개 구독자`); const errors: Array<{ subscriberId: string; error: Error }> = []; const subscribersToRemove: string[] = []; // 핸들러 실행 함수 const executeHandler = async ( subscriber: SubscriberInfo ): Promise => { const executeWithRetry = async (retriesLeft: number): Promise => { try { // 타임아웃 적용 const timeoutPromise = new Promise((_, reject) => { setTimeout( () => reject(new Error(`Handler timeout after ${timeout}ms`)), timeout ); }); const handlerPromise = Promise.resolve(subscriber.handler(payload)); await Promise.race([handlerPromise, timeoutPromise]); if (subscriber.once) { subscribersToRemove.push(subscriber.id); } } catch (error) { if (retriesLeft > 0) { this.log( `핸들러 재시도: ${subscriber.id} (남은 횟수: ${retriesLeft})` ); await executeWithRetry(retriesLeft - 1); } else { const err = error instanceof Error ? error : new Error(String(error)); this.logError(`핸들러 실행 실패: ${subscriber.id}`, err); errors.push({ subscriberId: subscriber.id, error: err }); } } }; await executeWithRetry(retryCount); }; // 병렬 또는 순차 실행 const subscriberArray = Array.from(eventSubscribers.values()); if (parallel) { // 병렬 실행 (Promise.allSettled로 에러 격리) await Promise.allSettled( subscriberArray.map((subscriber) => executeHandler(subscriber)) ); } else { // 순차 실행 for (const subscriber of subscriberArray) { await executeHandler(subscriber); } } // 일회성 구독자 정리 subscribersToRemove.forEach((id) => { eventSubscribers.delete(id); }); if (eventSubscribers.size === 0) { this.subscribers.delete(eventName); } const success = errors.length === 0; if (errors.length > 0) { this.log( `이벤트 완료: ${eventName} (성공: ${subscriberArray.length - errors.length}, 실패: ${errors.length})` ); } return { success, handlerCount: subscriberArray.length, errors, }; } /** * 동기적 이벤트 발행 (결과 대기 없음) * * 빠른 발행이 필요하고 결과를 기다릴 필요 없을 때 사용 */ emitSync( eventName: T, payload: V2EventPayloadMap[T] ): void { this.emit(eventName, payload).catch((error) => { this.logError(`동기 이벤트 발행 실패: ${eventName}`, error); }); } /** * 이벤트 구독자 수 조회 */ getSubscriberCount(eventName: V2EventName): number { return this.subscribers.get(eventName)?.size ?? 0; } /** * 모든 구독 해제 (테스트용) */ clear(): void { this.subscribers.clear(); this.log("모든 구독 해제됨"); } /** * 현재 구독 상태 출력 (디버그용) */ printState(): void { console.log("=== V2EventBus 상태 ==="); this.subscribers.forEach((subscribers, eventName) => { console.log(`${eventName}: ${subscribers.size}개 구독자`); subscribers.forEach((sub) => { console.log(` - ${sub.id} (컴포넌트: ${sub.componentId ?? "없음"})`); }); }); console.log("======================"); } } // 싱글톤 인스턴스 생성 및 내보내기 export const v2EventBus = new V2EventBus(); // 개발 환경에서 window에 노출 (디버깅용) if (typeof window !== "undefined" && process.env.NODE_ENV === "development") { (window as any).__v2EventBus = v2EventBus; } // 클래스도 내보내기 (테스트용) export { V2EventBus };