/** * 모달 관련 기본 클래스 * @module ModalBase */ class ModalBase { constructor(config = {}) { this.config = { closeOnEscape: true, closeOnBackdrop: true, showCloseButton: true, animation: "fade", animationDuration: 300, backdrop: true, keyboard: true, ...config, }; this.modal = null; this.backdrop = null; this.isOpen = false; this.onOpen = config.onOpen || null; this.onClose = config.onClose || null; } /** * 모달 생성 * @param {Object} options - 옵션 * @returns {HTMLElement} */ create(options = {}) { const { id = `modal-${Date.now()}`, className = "", content = "", header = null, footer = null, } = options; // 모달 컨테이너 this.modal = document.createElement("div"); this.modal.id = id; this.modal.className = `modal ${className}`; this.modal.setAttribute("role", "dialog"); this.modal.setAttribute("aria-modal", "true"); this.modal.style.cssText = ` display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1000; overflow: auto; `; // 백드롭 if (this.config.backdrop) { this.backdrop = document.createElement("div"); this.backdrop.className = "modal-backdrop"; this.backdrop.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: -1; `; this.modal.appendChild(this.backdrop); } // 모달 다이얼로그 const dialog = document.createElement("div"); dialog.className = "modal-dialog"; dialog.style.cssText = ` position: relative; margin: 50px auto; max-width: 600px; background: white; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); `; // 모달 컨텐츠 const modalContent = document.createElement("div"); modalContent.className = "modal-content"; // 헤더 if (header !== null) { const modalHeader = document.createElement("div"); modalHeader.className = "modal-header"; modalHeader.style.cssText = ` padding: 20px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; `; if (typeof header === "string") { modalHeader.innerHTML = header; } else { modalHeader.appendChild(header); } // 닫기 버튼 if (this.config.showCloseButton) { const closeBtn = document.createElement("button"); closeBtn.className = "modal-close"; closeBtn.innerHTML = "×"; closeBtn.style.cssText = ` background: none; border: none; font-size: 28px; cursor: pointer; color: #999; `; closeBtn.onclick = () => this.close(); modalHeader.appendChild(closeBtn); } modalContent.appendChild(modalHeader); } // 바디 const modalBody = document.createElement("div"); modalBody.className = "modal-body"; modalBody.style.cssText = ` padding: 20px; `; if (typeof content === "string") { modalBody.innerHTML = content; } else { modalBody.appendChild(content); } modalContent.appendChild(modalBody); // 푸터 if (footer !== null) { const modalFooter = document.createElement("div"); modalFooter.className = "modal-footer"; modalFooter.style.cssText = ` padding: 20px; border-top: 1px solid #e0e0e0; display: flex; justify-content: flex-end; gap: 10px; `; if (typeof footer === "string") { modalFooter.innerHTML = footer; } else { modalFooter.appendChild(footer); } modalContent.appendChild(modalFooter); } dialog.appendChild(modalContent); this.modal.appendChild(dialog); // 이벤트 리스너 등록 this._setupEventListeners(); return this.modal; } /** * 이벤트 리스너 설정 * @private */ _setupEventListeners() { // 백드롭 클릭 if (this.config.closeOnBackdrop && this.backdrop) { this.backdrop.onclick = () => this.close(); } // ESC 키 if (this.config.closeOnEscape) { this._escapeHandler = (e) => { if (e.key === "Escape" && this.isOpen) { this.close(); } }; document.addEventListener("keydown", this._escapeHandler); } // 모달 외부 클릭 if (this.config.closeOnBackdrop) { this.modal.onclick = (e) => { if (e.target === this.modal) { this.close(); } }; } } /** * 모달 열기 * @param {Object} data - 전달할 데이터 * @returns {Promise} */ async open(data = null) { if (this.isOpen) return; if (!this.modal) { console.error("Modal not created"); return; } // DOM에 추가 if (!this.modal.parentElement) { document.body.appendChild(this.modal); } // onOpen 콜백 if (this.onOpen) { await this.onOpen(data); } // 애니메이션 this.modal.style.display = "block"; await this._animate("in"); this.isOpen = true; // body 스크롤 방지 document.body.style.overflow = "hidden"; return this; } /** * 모달 닫기 * @returns {Promise} */ async close() { if (!this.isOpen) return; // 애니메이션 await this._animate("out"); this.modal.style.display = "none"; this.isOpen = false; // body 스크롤 복원 document.body.style.overflow = ""; // onClose 콜백 if (this.onClose) { await this.onClose(); } return this; } /** * 모달 토글 * @param {Object} data - 전달할 데이터 */ toggle(data = null) { if (this.isOpen) { this.close(); } else { this.open(data); } } /** * 애니메이션 처리 * @private */ async _animate(direction) { const dialog = this.modal.querySelector(".modal-dialog"); const { animation, animationDuration } = this.config; if (animation === "fade") { if (direction === "in") { this.modal.style.opacity = "0"; await this._delay(10); this.modal.style.transition = `opacity ${animationDuration}ms`; this.modal.style.opacity = "1"; await this._delay(animationDuration); } else { this.modal.style.transition = `opacity ${animationDuration}ms`; this.modal.style.opacity = "0"; await this._delay(animationDuration); } } else if (animation === "slide") { if (direction === "in") { dialog.style.transform = "translateY(-50px)"; dialog.style.opacity = "0"; await this._delay(10); dialog.style.transition = `transform ${animationDuration}ms, opacity ${animationDuration}ms`; dialog.style.transform = "translateY(0)"; dialog.style.opacity = "1"; await this._delay(animationDuration); } else { dialog.style.transition = `transform ${animationDuration}ms, opacity ${animationDuration}ms`; dialog.style.transform = "translateY(-50px)"; dialog.style.opacity = "0"; await this._delay(animationDuration); } } else if (animation === "zoom") { if (direction === "in") { dialog.style.transform = "scale(0.7)"; dialog.style.opacity = "0"; await this._delay(10); dialog.style.transition = `transform ${animationDuration}ms, opacity ${animationDuration}ms`; dialog.style.transform = "scale(1)"; dialog.style.opacity = "1"; await this._delay(animationDuration); } else { dialog.style.transition = `transform ${animationDuration}ms, opacity ${animationDuration}ms`; dialog.style.transform = "scale(0.7)"; dialog.style.opacity = "0"; await this._delay(animationDuration); } } // transition 초기화 this.modal.style.transition = ""; if (dialog) { dialog.style.transition = ""; } } /** * 딜레이 * @private */ _delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * 모달 파괴 */ destroy() { if (this.isOpen) { this.close(); } // 이벤트 리스너 제거 if (this._escapeHandler) { document.removeEventListener("keydown", this._escapeHandler); } // DOM에서 제거 if (this.modal && this.modal.parentElement) { this.modal.parentElement.removeChild(this.modal); } this.modal = null; this.backdrop = null; } /** * 모달 컨텐츠 업데이트 * @param {string|Element} content - 새 컨텐츠 */ updateContent(content) { const modalBody = this.modal.querySelector(".modal-body"); if (modalBody) { if (typeof content === "string") { modalBody.innerHTML = content; } else { modalBody.innerHTML = ""; modalBody.appendChild(content); } } } /** * 모달 헤더 업데이트 * @param {string|Element} header - 새 헤더 */ updateHeader(header) { const modalHeader = this.modal.querySelector(".modal-header"); if (modalHeader) { if (typeof header === "string") { modalHeader.innerHTML = header; } else { modalHeader.innerHTML = ""; modalHeader.appendChild(header); } // 닫기 버튼 재추가 if (this.config.showCloseButton) { const closeBtn = document.createElement("button"); closeBtn.className = "modal-close"; closeBtn.innerHTML = "×"; closeBtn.style.cssText = ` background: none; border: none; font-size: 28px; cursor: pointer; color: #999; `; closeBtn.onclick = () => this.close(); modalHeader.appendChild(closeBtn); } } } } /** * 확인 모달 (Confirm Dialog) */ class ConfirmModal extends ModalBase { constructor(config = {}) { super(config); this.promise = null; } /** * 확인 모달 표시 * @param {Object} options - 옵션 * @returns {Promise} */ show(options = {}) { const { title = "확인", message = "계속하시겠습니까?", confirmText = "확인", cancelText = "취소", confirmClass = "btn-primary", cancelClass = "btn-secondary", } = options; return new Promise((resolve) => { // 헤더 const header = document.createElement("div"); header.innerHTML = `

${title}

`; // 컨텐츠 const content = document.createElement("div"); content.innerHTML = message; // 푸터 const footer = document.createElement("div"); const cancelBtn = document.createElement("button"); cancelBtn.className = `btn ${cancelClass}`; cancelBtn.textContent = cancelText; cancelBtn.onclick = () => { this.close(); resolve(false); }; const confirmBtn = document.createElement("button"); confirmBtn.className = `btn ${confirmClass}`; confirmBtn.textContent = confirmText; confirmBtn.onclick = () => { this.close(); resolve(true); }; footer.appendChild(cancelBtn); footer.appendChild(confirmBtn); // 모달 생성 및 열기 this.create({ header, content, footer }); this.open(); }); } } /** * 알림 모달 (Alert Dialog) */ class AlertModal extends ModalBase { /** * 알림 모달 표시 * @param {Object} options - 옵션 * @returns {Promise} */ show(options = {}) { const { title = "알림", message = "", confirmText = "확인", confirmClass = "btn-primary", } = options; return new Promise((resolve) => { // 헤더 const header = document.createElement("div"); header.innerHTML = `

${title}

`; // 컨텐츠 const content = document.createElement("div"); content.innerHTML = message; // 푸터 const footer = document.createElement("div"); const confirmBtn = document.createElement("button"); confirmBtn.className = `btn ${confirmClass}`; confirmBtn.textContent = confirmText; confirmBtn.onclick = () => { this.close(); resolve(); }; footer.appendChild(confirmBtn); // 모달 생성 및 열기 this.create({ header, content, footer }); this.open(); }); } } // ES6 모듈 내보내기 if (typeof module !== "undefined" && module.exports) { module.exports = { ModalBase, ConfirmModal, AlertModal }; }