250 lines
7.8 KiB
JavaScript
250 lines
7.8 KiB
JavaScript
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;
|
|
}
|
|
});
|