2026-03-03 15:13:16 +09:00

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;
}
});