Files
gtsit/public/assets/js/site.js
2026-08-10 21:19:10 +09:00

316 lines
11 KiB
JavaScript

(() => {
const toggle = document.querySelector('.menu-toggle');
const navigation = document.querySelector('.site-navigation');
if (!toggle || !navigation) return;
const close = () => {
navigation.classList.remove('is-open');
toggle.setAttribute('aria-expanded', 'false');
toggle.querySelector('.sr-only').textContent = '메뉴 열기';
};
toggle.addEventListener('click', () => {
const open = !navigation.classList.contains('is-open');
navigation.classList.toggle('is-open', open);
toggle.setAttribute('aria-expanded', String(open));
toggle.querySelector('.sr-only').textContent = open ? '메뉴 닫기' : '메뉴 열기';
});
navigation.addEventListener('click', (event) => {
if (event.target.closest('a')) close();
});
window.addEventListener('resize', () => {
if (window.innerWidth > 960) close();
});
})();
(() => {
const slider = document.querySelector('[data-hero-slider]');
if (!slider) return;
const slides = [...slider.querySelectorAll('.hero-slide')];
const dots = [...slider.querySelectorAll('[data-slider-dot]')];
const previous = slider.querySelector('[data-slider-prev]');
const next = slider.querySelector('[data-slider-next]');
const pause = slider.querySelector('[data-slider-pause]');
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let current = 0;
let timer = null;
let isPaused = false;
let isInteracting = false;
const show = (index) => {
current = (index + slides.length) % slides.length;
slides.forEach((slide, slideIndex) => {
const isActive = slideIndex === current;
slide.classList.toggle('is-active', isActive);
slide.setAttribute('aria-hidden', String(!isActive));
});
dots.forEach((dot, dotIndex) => {
const isActive = dotIndex === current;
dot.classList.toggle('is-active', isActive);
if (isActive) dot.setAttribute('aria-current', 'true');
else dot.removeAttribute('aria-current');
});
};
const stop = () => {
if (timer !== null) window.clearInterval(timer);
timer = null;
};
const start = () => {
stop();
if (isPaused || isInteracting || reducedMotion.matches || document.hidden) return;
timer = window.setInterval(() => show(current + 1), 5000);
};
const move = (direction) => {
show(current + direction);
start();
};
previous?.addEventListener('click', () => move(-1));
next?.addEventListener('click', () => move(1));
dots.forEach((dot) => dot.addEventListener('click', () => {
show(Number(dot.dataset.sliderDot));
start();
}));
pause?.addEventListener('click', () => {
isPaused = !isPaused;
pause.setAttribute('aria-pressed', String(isPaused));
pause.textContent = isPaused ? '자동재생 시작' : '자동재생 일시정지';
start();
});
slider.addEventListener('mouseenter', () => {
isInteracting = true;
stop();
});
slider.addEventListener('mouseleave', () => {
isInteracting = false;
start();
});
slider.addEventListener('focusin', () => {
isInteracting = true;
stop();
});
slider.addEventListener('focusout', () => {
isInteracting = false;
start();
});
document.addEventListener('visibilitychange', start);
reducedMotion.addEventListener('change', start);
start();
})();
(() => {
const phoneInput = document.querySelector('[data-phone-input]');
const emailField = document.querySelector('[data-email-field]');
if (phoneInput) {
const formatPhone = (value) => {
let numbers = value.replace(/\D/g, '');
if (numbers.startsWith('02')) {
numbers = numbers.slice(0, 10);
if (numbers.length <= 2) return numbers;
if (numbers.length <= 5) return `${numbers.slice(0, 2)}-${numbers.slice(2)}`;
if (numbers.length <= 9) return `${numbers.slice(0, 2)}-${numbers.slice(2, 5)}-${numbers.slice(5)}`;
return `${numbers.slice(0, 2)}-${numbers.slice(2, 6)}-${numbers.slice(6)}`;
}
if (/^1\d{3}/.test(numbers)) {
numbers = numbers.slice(0, 8);
return numbers.length <= 4 ? numbers : `${numbers.slice(0, 4)}-${numbers.slice(4)}`;
}
if (/^050\d/.test(numbers)) {
numbers = numbers.slice(0, 12);
if (numbers.length <= 4) return numbers;
if (numbers.length <= 8) return `${numbers.slice(0, 4)}-${numbers.slice(4)}`;
return `${numbers.slice(0, 4)}-${numbers.slice(4, 8)}-${numbers.slice(8)}`;
}
numbers = numbers.slice(0, 11);
if (numbers.length <= 3) return numbers;
if (numbers.length <= 6) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
if (numbers.length <= 10) return `${numbers.slice(0, 3)}-${numbers.slice(3, 6)}-${numbers.slice(6)}`;
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7)}`;
};
const applyPhoneFormat = () => {
phoneInput.value = formatPhone(phoneInput.value);
};
phoneInput.addEventListener('input', applyPhoneFormat);
applyPhoneFormat();
}
document.querySelectorAll('[data-pin-input]').forEach((pinInput) => {
const applyPinFormat = () => {
pinInput.value = pinInput.value.replace(/\D/g, '').slice(0, 4);
};
pinInput.addEventListener('input', applyPinFormat);
applyPinFormat();
});
if (!emailField) return;
const template = emailField.querySelector('[data-email-template]');
const localInput = emailField.querySelector('[data-email-local]');
const domainSelect = emailField.querySelector('[data-email-domain]');
const directRow = emailField.querySelector('[data-email-direct]');
const fullInput = emailField.querySelector('[data-email-full]');
const returnButton = emailField.querySelector('[data-email-return]');
if (!template || !localInput || !domainSelect || !directRow || !fullInput || !returnButton) return;
const domains = [...domainSelect.options].map((option) => option.value).filter((value) => value !== 'direct');
const splitEmail = (email) => {
const separator = email.lastIndexOf('@');
if (separator <= 0) return null;
return { local: email.slice(0, separator), domain: email.slice(separator + 1).toLowerCase() };
};
const syncEmail = () => {
fullInput.value = localInput.value && domainSelect.value !== 'direct'
? `${localInput.value}@${domainSelect.value}`
: '';
};
const showTemplate = () => {
const parsed = splitEmail(fullInput.value);
localInput.value = parsed?.local || fullInput.value;
domainSelect.value = parsed && domains.includes(parsed.domain) ? parsed.domain : 'naver.com';
template.hidden = false;
directRow.hidden = true;
returnButton.hidden = true;
syncEmail();
};
const showDirect = () => {
const local = localInput.value.trim();
fullInput.value = local ? `${local}@` : '';
template.hidden = true;
directRow.hidden = false;
returnButton.hidden = false;
fullInput.focus();
};
localInput.addEventListener('input', syncEmail);
domainSelect.addEventListener('change', () => {
if (domainSelect.value === 'direct') showDirect();
else syncEmail();
});
returnButton.addEventListener('click', () => {
showTemplate();
localInput.focus();
});
emailField.closest('form')?.addEventListener('submit', () => {
if (!template.hidden) syncEmail();
});
const initialEmail = splitEmail(fullInput.value);
if (!fullInput.value || (initialEmail && domains.includes(initialEmail.domain))) showTemplate();
else returnButton.hidden = false;
})();
(() => {
const mapFrame = document.querySelector('[data-naver-map]');
if (!mapFrame) return;
const mapElement = mapFrame.querySelector('.naver-map');
const statusElement = mapFrame.querySelector('[data-naver-map-status]');
const fullscreenButton = mapFrame.querySelector('[data-naver-map-fullscreen]');
const externalLink = mapFrame.parentElement?.querySelector('[data-naver-map-external]');
const clientId = mapFrame.dataset.clientId;
const latitude = Number(mapFrame.dataset.latitude);
const longitude = Number(mapFrame.dataset.longitude);
const label = mapFrame.dataset.label;
let settled = false;
const setState = (state, message = '') => {
settled = state !== 'loading';
mapFrame.dataset.mapState = state;
mapFrame.setAttribute('aria-busy', String(state === 'loading'));
if (fullscreenButton) fullscreenButton.hidden = state !== 'ready';
if (statusElement && message) statusElement.textContent = message;
};
const fail = () => {
setState('error', '지도를 불러오지 못했습니다. 아래 네이버 지도 링크를 이용해 주세요.');
};
const renderMap = () => {
if (!mapElement || !window.naver?.maps || !Number.isFinite(latitude) || !Number.isFinite(longitude)) {
fail();
return;
}
const position = new window.naver.maps.LatLng(latitude, longitude);
const map = new window.naver.maps.Map(mapElement, {
center: position,
zoom: 17,
minZoom: 10,
zoomControl: true,
zoomControlOptions: {
position: window.naver.maps.Position.RIGHT_CENTER,
},
});
new window.naver.maps.Marker({
map,
position,
title: label,
});
const refreshMapSize = () => {
window.setTimeout(() => {
window.naver.maps.Event.trigger(map, 'resize');
map.setCenter(position);
}, 0);
};
document.addEventListener('fullscreenchange', () => {
const isFullscreen = document.fullscreenElement === mapFrame;
mapFrame.classList.toggle('is-fullscreen', isFullscreen);
if (fullscreenButton) {
fullscreenButton.setAttribute('aria-label', isFullscreen ? '전체 화면 종료' : '지도를 전체 화면으로 보기');
fullscreenButton.title = isFullscreen ? '전체 화면 종료' : '전체 화면';
}
refreshMapSize();
});
if (fullscreenButton && !mapFrame.requestFullscreen) {
fullscreenButton.setAttribute('aria-label', '네이버 지도에서 크게 보기');
fullscreenButton.title = '네이버 지도에서 열기';
}
fullscreenButton?.addEventListener('click', async () => {
if (document.fullscreenElement === mapFrame) {
await document.exitFullscreen();
} else if (mapFrame.requestFullscreen) {
await mapFrame.requestFullscreen();
} else if (externalLink) {
window.open(externalLink.href, '_blank', 'noopener');
}
});
setState('ready', '지도를 불러왔습니다.');
};
if (!clientId) {
fail();
return;
}
setState('loading');
window.navermap_authFailure = fail;
const script = document.createElement('script');
const parameters = new URLSearchParams({
ncpKeyId: clientId,
});
script.src = `https://oapi.map.naver.com/openapi/v3/maps.js?${parameters}`;
script.async = true;
script.onload = renderMap;
script.onerror = fail;
document.head.append(script);
window.setTimeout(() => {
if (!settled) fail();
}, 12000);
})();