/** * 황제찹쌀꽈배기 — 공통 스크립트 * * 빌드 도구를 쓰지 않으므로 이 파일은 브라우저에 그대로 전달된다. * ES2020 문법까지만 사용한다. */ (function () { "use strict"; /** * 모바일 내비게이션 토글. * * CSS 기본값이 '보임'이므로, 스크립트가 살아 있을 때만 접는다. * aria-expanded 와 data-open 을 항상 같이 갱신해 * 화면 상태와 스크린 리더가 읽는 상태를 일치시킨다. */ function initNavToggle() { const toggle = document.querySelector(".nav-toggle"); const nav = document.querySelector(".nav"); if (!toggle || !nav) return; const label = toggle.querySelector(".sr-only"); function setOpen(isOpen) { nav.dataset.open = String(isOpen); toggle.setAttribute("aria-expanded", String(isOpen)); if (label) { label.textContent = isOpen ? "메뉴 닫기" : "메뉴 열기"; } } // 초기 상태를 닫힘으로 맞춘다. setOpen(false); toggle.addEventListener("click", function () { const isOpen = toggle.getAttribute("aria-expanded") === "true"; setOpen(!isOpen); }); // 메뉴 항목을 누르면 이동과 동시에 닫는다. nav.addEventListener("click", function (event) { if (event.target.closest("a")) { setOpen(false); } }); // Esc 로 닫고 포커스를 토글 버튼으로 되돌린다. document.addEventListener("keydown", function (event) { if (event.key !== "Escape") return; if (toggle.getAttribute("aria-expanded") !== "true") return; setOpen(false); toggle.focus(); }); } /** * 스크롤에 맞춰 요소를 아래에서 위로 등장시킨다. * * body 에 .js-reveal 을 붙인 뒤에야 CSS 가 요소를 숨기므로, * 이 함수가 실행되지 않으면 콘텐츠는 그냥 보인 채로 남는다. * 한 번 나타난 요소는 다시 감추지 않는다(unobserve). */ function initReveal() { const targets = document.querySelectorAll("[data-reveal]"); if (!targets.length) return; const prefersReducedMotion = window.matchMedia( "(prefers-reduced-motion: reduce)" ).matches; // 관찰자를 못 쓰거나 모션을 줄이는 설정이면 연출 없이 그대로 둔다. if (prefersReducedMotion || !("IntersectionObserver" in window)) return; document.body.classList.add("js-reveal"); const observer = new IntersectionObserver( function (entries) { entries.forEach(function (entry) { if (!entry.isIntersecting) return; entry.target.classList.add("is-visible"); observer.unobserve(entry.target); }); }, { // 요소가 화면 아래 12% 지점까지 올라왔을 때 발동시킨다. rootMargin: "0px 0px -12% 0px", threshold: 0, } ); targets.forEach(function (el) { observer.observe(el); }); } /** * 푸터 저작권 연도를 자동으로 채운다. * 마크업에는 폴백 값이 들어 있어 스크립트가 없어도 빈칸이 보이지 않는다. */ function initCurrentYear() { const year = String(new Date().getFullYear()); document.querySelectorAll("[data-current-year]").forEach(function (el) { el.textContent = year; }); } /** * 창업상담 폼. * * 제출하면 fast_consult 모듈(form action)로 접수되어 * 관리자 [고객센터 > 창업상담] 목록에 쌓인다. */ function initContactForm() { const form = document.querySelector(".contact-form"); if (!form) return; const errorBox = form.querySelector(".contact-form__error"); /** 첫 번째로 비어 있는 필수 항목을 찾는다. */ function findFirstInvalid() { const required = form.querySelectorAll("[required]"); for (const field of required) { const empty = field.type === "checkbox" ? !field.checked : !field.value.trim(); if (empty) return field; } return null; } function showError(field) { const label = form.querySelector('label[for="' + field.id + '"]'); const name = label ? label.textContent.replace("*", "").trim() : "필수 항목"; errorBox.textContent = name + " 항목을 입력해 주세요."; errorBox.hidden = false; field.setAttribute("aria-invalid", "true"); field.focus(); } function clearError() { errorBox.hidden = true; errorBox.textContent = ""; form.querySelectorAll('[aria-invalid="true"]').forEach(function (el) { el.removeAttribute("aria-invalid"); }); } function showMessage(text) { errorBox.textContent = text; errorBox.hidden = false; } form.addEventListener("submit", function (event) { event.preventDefault(); clearError(); const invalid = findFirstInvalid(); if (invalid) { showError(invalid); return; } const button = form.querySelector('button[type="submit"]'); button.disabled = true; fetch(form.action, { method: "POST", headers: { Accept: "application/json" }, body: new FormData(form), }) .then(function (res) { return res.json(); }) .then(function (data) { if (data.result === "OK") { form.reset(); alert("상담 신청이 접수되었습니다. 담당자가 확인 후 연락드리겠습니다."); } else if (data.result === "CHECK_HP") { const tel = form.querySelector("#f-tel"); showMessage("연락처를 확인해 주세요. 숫자만 9~12자리로 입력해 주세요."); tel.setAttribute("aria-invalid", "true"); tel.focus(); } else { showMessage("접수에 실패했습니다. 잠시 후 다시 시도해 주세요."); } }) .catch(function () { showMessage("접수에 실패했습니다. 잠시 후 다시 시도하거나 전화로 문의해 주세요."); }) .finally(function () { button.disabled = false; }); }); // 입력을 다시 시작하면 오류 안내를 지운다. form.addEventListener("input", clearError); } /** * 팝업 링크(.js-popup) — 별도 팝업 창으로 연다. * * 마크업의 target="_blank" 덕에 JS 가 없어도 새 탭으로 열린다. * JS 가 있으면 클릭을 가로채 data-popup="가로x세로" 크기의 팝업 창으로 띄운다. * 상표등록증(이미지)·개인정보 처리방침이 이 방식을 쓴다. */ function initPopupLinks() { document.querySelectorAll(".js-popup").forEach(function (link) { link.addEventListener("click", function (event) { // 기본 이동(target=_blank 새 탭)을 항상 막고 팝업만 연다. event.preventDefault(); const size = (link.dataset.popup || "820x900").split("x"); const w = parseInt(size[0], 10) || 820; const h = parseInt(size[1], 10) || 900; // 주의: 옵션에 noopener 를 넣으면 window.open 이 null 을 반환해 // 폴백이 오작동한다. 여기서는 열린 창의 opener 만 끊어 준다. const win = window.open(link.href, "hjPopup", "width=" + w + ",height=" + h); if (win) { win.opener = null; } else { // 팝업이 차단된 경우에만 새 탭으로 연다. window.open(link.href, "_blank"); } }); }); } initNavToggle(); initReveal(); initCurrentYear(); initContactForm(); initPopupLinks(); })();