/** * DOM 조작 유틸리티 모듈 (DOMUtils.js) * ======================================== * document.querySelector 대신 쓰는 간편 함수들입니다. * * [초보자용 사용 예] * DOMUtils.$('.my-class') → 첫 번째 요소 (querySelector) * DOMUtils.$$('.my-class') → 모든 요소 (querySelectorAll) * DOMUtils.fadeIn(el, 300) → 페이드 인 (300ms) * DOMUtils.fadeOut(el, 300) → 페이드 아웃 * DOMUtils.delegate(parent, 'click', '.btn', handler) → 동적 요소에 이벤트 위임 * DOMUtils.htmlToElement('
') → HTML 문자열 → Element * * @module DOMUtils */ class DOMUtils { /** * 요소 선택 (단일) * @param {string} selector - CSS 선택자 * @param {Element} parent - 부모 요소 * @returns {Element|null} */ static $(selector, parent = document) { return parent.querySelector(selector); } /** * 요소 선택 (다중) * @param {string} selector - CSS 선택자 * @param {Element} parent - 부모 요소 * @returns {NodeList} */ static $$(selector, parent = document) { return parent.querySelectorAll(selector); } /** * 요소 생성 * @param {string} tag - 태그명 * @param {Object} attrs - 속성 객체 * @param {string} content - 내용 * @returns {Element} */ static createElement(tag, attrs = {}, content = "") { const element = document.createElement(tag); Object.entries(attrs).forEach(([key, value]) => { if (key === "class" || key === "className") { element.className = value; } else if (key === "style" && typeof value === "object") { Object.assign(element.style, value); } else if (key.startsWith("data-")) { element.setAttribute(key, value); } else { element[key] = value; } }); if (content) { if (typeof content === "string") { element.innerHTML = content; } else if (content instanceof Node) { element.appendChild(content); } } return element; } /** * 클래스 토글 * @param {Element} element - 대상 요소 * @param {string} className - 클래스명 * @param {boolean} force - 강제 적용 여부 */ static toggleClass(element, className, force) { if (!element) return; if (force !== undefined) { element.classList.toggle(className, force); } else { element.classList.toggle(className); } } /** * 여러 클래스 추가 * @param {Element} element - 대상 요소 * @param {...string} classNames - 클래스명들 */ static addClasses(element, ...classNames) { if (!element) return; element.classList.add(...classNames); } /** * 여러 클래스 제거 * @param {Element} element - 대상 요소 * @param {...string} classNames - 클래스명들 */ static removeClasses(element, ...classNames) { if (!element) return; element.classList.remove(...classNames); } /** * 요소의 위치 정보 가져오기 * @param {Element} element - 대상 요소 * @returns {Object} */ static getPosition(element) { if (!element) return null; const rect = element.getBoundingClientRect(); return { top: rect.top, left: rect.left, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height, x: rect.x, y: rect.y, }; } /** * 요소를 퍼센트 위치로 설정 * @param {Element} element - 대상 요소 * @param {number} x - X 위치 (%) * @param {number} y - Y 위치 (%) * @param {string} transform - 추가 transform */ static setPercentPosition(element, x, y, transform = "translate(-50%, -50%)") { if (!element) return; element.style.position = "absolute"; element.style.left = `${x}%`; element.style.top = `${y}%`; if (transform) { element.style.transform = transform; } } /** * 요소 페이드 인 * @param {Element} element - 대상 요소 * @param {number} duration - 지속 시간 (ms) * @returns {Promise} */ static fadeIn(element, duration = 300) { if (!element) return Promise.resolve(); return new Promise((resolve) => { // 기존 display 값 저장 (grid, flex 등 유지) const originalDisplay = element.style.display || window.getComputedStyle(element).display; const isGridOrFlex = originalDisplay === "grid" || originalDisplay === "flex" || originalDisplay.includes("grid") || originalDisplay.includes("flex"); element.style.opacity = "0"; // grid/flex인 경우 display를 설정하지 않음 if (!isGridOrFlex) { element.style.display = "block"; } element.style.transition = `opacity ${duration}ms ease-in-out`; setTimeout(() => { element.style.opacity = "1"; }, 10); setTimeout(() => { element.style.transition = ""; // grid/flex인 경우 display 스타일 제거 if (isGridOrFlex) { element.style.display = ""; } resolve(); }, duration); }); } /** * 요소 페이드 아웃 * @param {Element} element - 대상 요소 * @param {number} duration - 지속 시간 (ms) * @returns {Promise} */ static fadeOut(element, duration = 300) { if (!element) return Promise.resolve(); return new Promise((resolve) => { element.style.opacity = "1"; element.style.transition = `opacity ${duration}ms ease-in-out`; setTimeout(() => { element.style.opacity = "0"; }, 10); setTimeout(() => { element.style.display = "none"; element.style.transition = ""; element.style.opacity = ""; // 재오픈 시 opacity 초기화 resolve(); }, duration); }); } /** * 요소 슬라이드 다운 * @param {Element} element - 대상 요소 * @param {number} duration - 지속 시간 (ms) * @returns {Promise} */ static slideDown(element, duration = 300) { if (!element) return Promise.resolve(); return new Promise((resolve) => { element.style.display = "block"; const height = element.scrollHeight; element.style.height = "0"; element.style.overflow = "hidden"; element.style.transition = `height ${duration}ms ease-in-out`; setTimeout(() => { element.style.height = `${height}px`; }, 10); setTimeout(() => { element.style.height = ""; element.style.overflow = ""; element.style.transition = ""; resolve(); }, duration); }); } /** * 요소 슬라이드 업 * @param {Element} element - 대상 요소 * @param {number} duration - 지속 시간 (ms) * @returns {Promise} */ static slideUp(element, duration = 300) { if (!element) return Promise.resolve(); return new Promise((resolve) => { const height = element.scrollHeight; element.style.height = `${height}px`; element.style.overflow = "hidden"; element.style.transition = `height ${duration}ms ease-in-out`; setTimeout(() => { element.style.height = "0"; }, 10); setTimeout(() => { element.style.display = "none"; element.style.height = ""; element.style.overflow = ""; element.style.transition = ""; resolve(); }, duration); }); } /** * 이벤트 위임 * @param {Element} parent - 부모 요소 * @param {string} eventType - 이벤트 타입 * @param {string} selector - 자식 선택자 * @param {Function} handler - 핸들러 함수 */ static delegate(parent, eventType, selector, handler) { if (!parent) return; parent.addEventListener(eventType, (event) => { const target = event.target.closest(selector); if (target && parent.contains(target)) { handler.call(target, event); } }); } /** * HTML 문자열을 요소로 변환 * @param {string} html - HTML 문자열 * @returns {Element} */ static htmlToElement(html) { const template = document.createElement("template"); template.innerHTML = html.trim(); return template.content.firstElementChild; } /** * HTML 문자열을 요소 배열로 변환 * @param {string} html - HTML 문자열 * @returns {Array} */ static htmlToElements(html) { const template = document.createElement("template"); template.innerHTML = html.trim(); return Array.from(template.content.children); } /** * 요소가 뷰포트에 있는지 확인 * @param {Element} element - 대상 요소 * @returns {boolean} */ static isInViewport(element) { if (!element) return false; const rect = element.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } /** * 부드러운 스크롤 * @param {Element|string} target - 대상 요소 또는 선택자 * @param {Object} options - 옵션 */ static smoothScroll(target, options = {}) { const element = typeof target === "string" ? this.$(target) : target; if (!element) return; const defaultOptions = { behavior: "smooth", block: "start", inline: "nearest", }; element.scrollIntoView({ ...defaultOptions, ...options }); } /** * 전체 화면 토글 * @param {Element} element - 대상 요소 */ static toggleFullscreen(element = document.documentElement) { if (!document.fullscreenElement) { element.requestFullscreen?.() || element.webkitRequestFullscreen?.() || element.msRequestFullscreen?.(); } else { document.exitFullscreen?.() || document.webkitExitFullscreen?.() || document.msExitFullscreen?.(); } } /** * 클립보드에 복사 * @param {string} text - 복사할 텍스트 * @returns {Promise} */ static async copyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch (err) { console.error("Failed to copy:", err); return false; } } /** * 요소의 스타일 가져오기 * @param {Element} element - 대상 요소 * @param {string} property - CSS 속성 * @returns {string} */ static getStyle(element, property) { if (!element) return null; return window.getComputedStyle(element).getPropertyValue(property); } /** * 여러 스타일 설정 * @param {Element} element - 대상 요소 * @param {Object} styles - 스타일 객체 */ static setStyles(element, styles) { if (!element || !styles) return; Object.assign(element.style, styles); } /** * 요소 제거 * @param {Element} element - 대상 요소 */ static remove(element) { if (element && element.parentNode) { element.parentNode.removeChild(element); } } /** * 요소 내용 비우기 * @param {Element} element - 대상 요소 */ static empty(element) { if (!element) return; while (element.firstChild) { element.removeChild(element.firstChild); } } } // ES6 모듈 내보내기 if (typeof module !== "undefined" && module.exports) { module.exports = DOMUtils; }