Initial commit: 교육 프로젝트 배포
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
/**
|
||||
* 모달 관련 기본 클래스
|
||||
* @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<boolean>}
|
||||
*/
|
||||
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 = `<h3 style="margin: 0;">${title}</h3>`;
|
||||
|
||||
// 컨텐츠
|
||||
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 = `<h3 style="margin: 0;">${title}</h3>`;
|
||||
|
||||
// 컨텐츠
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user