Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* 이벤트 관리 모듈 (EventManager.js)
|
||||
* ========================================
|
||||
* 이벤트 리스너를 중앙에서 관리합니다.
|
||||
* 리스너 ID를 저장해두면 나중에 off()로 제거 가능 (메모리 누수 방지).
|
||||
*
|
||||
* [초보자용] 전역 변수 eventManager 로 사용:
|
||||
* eventManager.on(element, 'click', handler)
|
||||
* eventManager.delegate(parent, 'click', '.btn', handler)
|
||||
*
|
||||
* @module EventManager
|
||||
*/
|
||||
class EventManager {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
this.delegatedListeners = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 등록
|
||||
* @param {Element|Window|Document} target - 대상 요소
|
||||
* @param {string} eventType - 이벤트 타입
|
||||
* @param {Function} handler - 핸들러 함수
|
||||
* @param {Object} options - 이벤트 옵션
|
||||
* @returns {string} 리스너 ID
|
||||
*/
|
||||
on(target, eventType, handler, options = {}) {
|
||||
if (!target || typeof handler !== 'function') {
|
||||
console.warn('[EventManager] Invalid target or handler');
|
||||
return null;
|
||||
}
|
||||
|
||||
const listenerId = this._generateId();
|
||||
const wrappedHandler = this._wrapHandler(handler, listenerId);
|
||||
|
||||
target.addEventListener(eventType, wrappedHandler, options);
|
||||
|
||||
if (!this.listeners.has(target)) {
|
||||
this.listeners.set(target, new Map());
|
||||
}
|
||||
this.listeners.get(target).set(listenerId, {
|
||||
eventType,
|
||||
handler: wrappedHandler,
|
||||
originalHandler: handler,
|
||||
options,
|
||||
});
|
||||
|
||||
return listenerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 제거
|
||||
* @param {Element|Window|Document} target - 대상 요소
|
||||
* @param {string} listenerId - 리스너 ID
|
||||
*/
|
||||
off(target, listenerId) {
|
||||
if (!target || !listenerId) return;
|
||||
|
||||
const targetListeners = this.listeners.get(target);
|
||||
if (!targetListeners) return;
|
||||
|
||||
const listener = targetListeners.get(listenerId);
|
||||
if (!listener) return;
|
||||
|
||||
target.removeEventListener(listener.eventType, listener.handler, listener.options);
|
||||
targetListeners.delete(listenerId);
|
||||
|
||||
if (targetListeners.size === 0) {
|
||||
this.listeners.delete(target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 위임 등록
|
||||
* @param {Element|Window|Document} parent - 부모 요소
|
||||
* @param {string} eventType - 이벤트 타입
|
||||
* @param {string} selector - 자식 선택자
|
||||
* @param {Function} handler - 핸들러 함수
|
||||
* @param {Object} options - 이벤트 옵션
|
||||
* @returns {string} 리스너 ID
|
||||
*/
|
||||
delegate(parent, eventType, selector, handler, options = {}) {
|
||||
if (!parent || typeof handler !== 'function') {
|
||||
console.warn('[EventManager] Invalid parent or handler');
|
||||
return null;
|
||||
}
|
||||
|
||||
const listenerId = this._generateId();
|
||||
const wrappedHandler = (event) => {
|
||||
const target = event.target.closest(selector);
|
||||
if (target && parent.contains(target)) {
|
||||
handler.call(target, event);
|
||||
}
|
||||
};
|
||||
|
||||
parent.addEventListener(eventType, wrappedHandler, options);
|
||||
|
||||
if (!this.delegatedListeners.has(parent)) {
|
||||
this.delegatedListeners.set(parent, new Map());
|
||||
}
|
||||
this.delegatedListeners.get(parent).set(listenerId, {
|
||||
eventType,
|
||||
selector,
|
||||
handler: wrappedHandler,
|
||||
originalHandler: handler,
|
||||
options,
|
||||
});
|
||||
|
||||
return listenerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 위임 제거
|
||||
* @param {Element|Window|Document} parent - 부모 요소
|
||||
* @param {string} listenerId - 리스너 ID
|
||||
*/
|
||||
undelegate(parent, listenerId) {
|
||||
if (!parent || !listenerId) return;
|
||||
|
||||
const delegatedListeners = this.delegatedListeners.get(parent);
|
||||
if (!delegatedListeners) return;
|
||||
|
||||
const listener = delegatedListeners.get(listenerId);
|
||||
if (!listener) return;
|
||||
|
||||
parent.removeEventListener(listener.eventType, listener.handler, listener.options);
|
||||
delegatedListeners.delete(listenerId);
|
||||
|
||||
if (delegatedListeners.size === 0) {
|
||||
this.delegatedListeners.delete(parent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 요소의 모든 리스너 제거
|
||||
* @param {Element|Window|Document} target - 대상 요소
|
||||
*/
|
||||
removeAll(target) {
|
||||
// 일반 리스너 제거
|
||||
const targetListeners = this.listeners.get(target);
|
||||
if (targetListeners) {
|
||||
targetListeners.forEach((listener, listenerId) => {
|
||||
target.removeEventListener(listener.eventType, listener.handler, listener.options);
|
||||
});
|
||||
this.listeners.delete(target);
|
||||
}
|
||||
|
||||
// 위임 리스너 제거
|
||||
const delegatedListeners = this.delegatedListeners.get(target);
|
||||
if (delegatedListeners) {
|
||||
delegatedListeners.forEach((listener, listenerId) => {
|
||||
target.removeEventListener(listener.eventType, listener.handler, listener.options);
|
||||
});
|
||||
this.delegatedListeners.delete(target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 리스너 제거
|
||||
*/
|
||||
removeAllListeners() {
|
||||
// 일반 리스너 제거
|
||||
this.listeners.forEach((targetListeners, target) => {
|
||||
targetListeners.forEach((listener) => {
|
||||
target.removeEventListener(listener.eventType, listener.handler, listener.options);
|
||||
});
|
||||
});
|
||||
this.listeners.clear();
|
||||
|
||||
// 위임 리스너 제거
|
||||
this.delegatedListeners.forEach((delegatedListeners, parent) => {
|
||||
delegatedListeners.forEach((listener) => {
|
||||
parent.removeEventListener(listener.eventType, listener.handler, listener.options);
|
||||
});
|
||||
});
|
||||
this.delegatedListeners.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 한 번만 실행되는 이벤트 리스너
|
||||
* @param {Element|Window|Document} target - 대상 요소
|
||||
* @param {string} eventType - 이벤트 타입
|
||||
* @param {Function} handler - 핸들러 함수
|
||||
* @param {Object} options - 이벤트 옵션
|
||||
* @returns {string} 리스너 ID
|
||||
*/
|
||||
once(target, eventType, handler, options = {}) {
|
||||
const listenerId = this._generateId();
|
||||
const wrappedHandler = (event) => {
|
||||
handler(event);
|
||||
this.off(target, listenerId);
|
||||
};
|
||||
|
||||
return this.on(target, eventType, wrappedHandler, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 발생 (커스텀 이벤트)
|
||||
* @param {Element|Window|Document} target - 대상 요소
|
||||
* @param {string} eventType - 이벤트 타입
|
||||
* @param {Object} detail - 이벤트 데이터
|
||||
*/
|
||||
emit(target, eventType, detail = {}) {
|
||||
if (!target) return;
|
||||
|
||||
const event = new CustomEvent(eventType, {
|
||||
detail,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
target.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 핸들러 래핑 (에러 처리 포함)
|
||||
* @private
|
||||
*/
|
||||
_wrapHandler(handler, listenerId) {
|
||||
return (event) => {
|
||||
try {
|
||||
handler(event);
|
||||
} catch (error) {
|
||||
console.error(`[EventManager] Error in event handler (${listenerId}):`, error);
|
||||
if (typeof ErrorHandler !== 'undefined') {
|
||||
ErrorHandler.handle(error, { context: 'EventManager', listenerId });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 고유 ID 생성
|
||||
* @private
|
||||
*/
|
||||
_generateId() {
|
||||
return `listener_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록된 리스너 정보 가져오기
|
||||
* @returns {Object}
|
||||
*/
|
||||
getListenersInfo() {
|
||||
const info = {
|
||||
regular: {},
|
||||
delegated: {},
|
||||
};
|
||||
|
||||
this.listeners.forEach((targetListeners, target) => {
|
||||
const targetKey = target === window ? 'window' :
|
||||
target === document ? 'document' :
|
||||
target.id || target.className || 'unknown';
|
||||
info.regular[targetKey] = Array.from(targetListeners.keys());
|
||||
});
|
||||
|
||||
this.delegatedListeners.forEach((delegatedListeners, parent) => {
|
||||
const parentKey = parent === window ? 'window' :
|
||||
parent === document ? 'document' :
|
||||
parent.id || parent.className || 'unknown';
|
||||
info.delegated[parentKey] = Array.from(delegatedListeners.keys());
|
||||
});
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전역 EventManager 인스턴스 (싱글톤)
|
||||
*/
|
||||
const eventManager = new EventManager();
|
||||
|
||||
// ES6 모듈 내보내기
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = { EventManager, eventManager };
|
||||
}
|
||||
Reference in New Issue
Block a user