class UiDialog { constructor(rootId = "uiDialog") { this.root = document.getElementById(rootId); if (!this.root) return; // stacking context 문제 방지: 무조건 body 직속으로 이동 try { if (this.root.parentElement !== document.body) { document.body.appendChild(this.root); } } catch (e) {} this.titleEl = this.root.querySelector("#uiDialogTitle"); this.msgEl = this.root.querySelector("#uiDialogMessage"); this.okBtn = this.root.querySelector("#uiDialogOk"); this.cancelBtn = this.root.querySelector("#uiDialogCancel"); this._resolver = null; this._type = "alert"; this._lastFocus = null; this._closePolicy = { backdrop: false, x: false, esc: false, enter: true }; // close triggers this.root.addEventListener("click", (e) => { if (!e.target?.dataset?.uidialogClose) return; const isBackdrop = !!e.target.closest(".ui-dialog__backdrop"); const isX = !!e.target.closest(".ui-dialog__x"); if (isBackdrop && !this._closePolicy?.backdrop) return; if (isX && !this._closePolicy?.x) return; this._resolve(false); }); // keyboard document.addEventListener("keydown", (e) => { if (!this.isOpen()) return; if (e.key === "Escape") { if (this._closePolicy?.esc) this._resolve(false); return; } if (e.key === "Enter") { if (this._closePolicy?.enter !== false) this._resolve(true); return; } }); // 버튼 이벤트(존재할 때만) this.okBtn?.addEventListener("click", () => this._resolve(true)); this.cancelBtn?.addEventListener("click", () => this._resolve(false)); } // 현재 페이지 최상단 z-index 탐색 (모달 겹침 대응) static getTopZIndex() { let maxZ = 0; const nodes = document.querySelectorAll("body *"); for (const el of nodes) { const cs = getComputedStyle(el); if (cs.position === "fixed" || cs.position === "absolute" || cs.position === "sticky") { const z = parseInt(cs.zIndex, 10); if (!Number.isNaN(z)) maxZ = Math.max(maxZ, z); } } return maxZ; } isOpen() { return !!this.root && this.root.classList.contains("is-open"); } alert(message, options = {}) { return this._open("alert", message, options); } confirm(message, options = {}) { return this._open("confirm", message, options); } _open(type, message, options) { if (!this.root) return Promise.resolve(false); const { title = type === "confirm" ? "확인" : "알림", okText = "확인", cancelText = "취소", dangerous = false, // 기본: 밖 클릭/닫기(X)/ESC로 닫기 금지 closeOnBackdrop = false, closeOnX = false, closeOnEsc = false, closeOnEnter = true, } = options; // 항상 최상단: DOM 마지막 + z-index 최상단 try { document.body.appendChild(this.root); const topZ = UiDialog.getTopZIndex(); this.root.style.zIndex = String(Math.max(300000, topZ + 10)); // 300000 안전 마진 } catch (e) {} this._closePolicy = { backdrop: !!closeOnBackdrop, x: !!closeOnX, esc: !!closeOnEsc, enter: closeOnEnter !== false, }; this._type = type; this._lastFocus = document.activeElement; // 요소가 없으면 조용히 실패(페이지별 차이 방어) if (this.titleEl) this.titleEl.textContent = title; if (this.msgEl) this.msgEl.textContent = message ?? ""; if (this.okBtn) this.okBtn.textContent = okText; if (this.cancelBtn) this.cancelBtn.textContent = cancelText; // alert면 cancel 숨김 if (this.cancelBtn) { this.cancelBtn.style.display = (type === "alert") ? "none" : ""; } // danger 스타일 if (this.okBtn) { this.okBtn.classList.toggle("ui-dialog__btn--danger", !!dangerous); } // open this.root.classList.add("is-open"); this.root.setAttribute("aria-hidden", "false"); document.documentElement.style.overflow = "hidden"; // focus setTimeout(() => { try { this.okBtn?.focus?.(); } catch (e) {} }, 0); return new Promise((resolve) => { this._resolver = resolve; }); } _resolve(ok) { if (!this.isOpen()) return; // close this.root.classList.remove("is-open"); this.root.setAttribute("aria-hidden", "true"); document.documentElement.style.overflow = ""; const r = this._resolver; this._resolver = null; // focus restore try { this._lastFocus?.focus?.(); } catch (e) {} if (typeof r === "function") r(!!ok); } } // 전역으로 노출 (모든 페이지에서 바로 호출) window.uiDialog = new UiDialog(); // ====================================================== // Global showMsg / clearMsg (공통 사용) // ====================================================== (function () { let cachedHelpEl = null; function getHelpEl(opt = {}) { if (opt.helpId) { const el = document.getElementById(opt.helpId); if (el) return el; } if (cachedHelpEl && document.contains(cachedHelpEl)) return cachedHelpEl; cachedHelpEl = document.getElementById("reg_phone_help"); return cachedHelpEl; } window.showMsg = async function (msg, opt = {}) { const d = Object.assign({ type: "alert", title: "알림", okText: "확인", cancelText: "취소", dangerous: false, redirect: "", helpId: "", // 닫기 정책 기본값(너가 말한 “다른 공간 클릭해도 닫히지 않게”와 일치) closeOnBackdrop: false, closeOnX: false, closeOnEsc: false, closeOnEnter: true, }, opt || {}); if (window.uiDialog && typeof window.uiDialog[d.type] === "function") { const ok = await window.uiDialog[d.type](msg || "", d); if (d.redirect && ok) { window.location.href = d.redirect; } return d.type === "confirm" ? !!ok : true; } const helpEl = getHelpEl(d); if (helpEl) { helpEl.style.display = "block"; helpEl.textContent = msg || ""; return true; } alert(msg || ""); return true; }; window.clearMsg = function (helpId = "") { const el = helpId ? document.getElementById(helpId) : getHelpEl({}); if (!el) return; el.style.display = "none"; el.textContent = ""; }; })(); // ====================================================== // 서버 flash 자동 실행 (기존 유지) // ====================================================== document.addEventListener("DOMContentLoaded", async () => { const flashEl = document.getElementById("uiDialogFlash"); if (!flashEl || !window.uiDialog) return; let payload = null; try { payload = JSON.parse(flashEl.textContent || "{}"); } catch (e) {} if (!payload) return; const type = payload.type || "alert"; const ok = await window.uiDialog[type](payload.message || "", payload); if (type === "confirm") { if (ok && payload.redirect) window.location.href = payload.redirect; } else { if (payload.redirect) window.location.href = payload.redirect; } });