feat: GTSIT 사이트 구축
This commit is contained in:
315
public/assets/js/site.js
Normal file
315
public/assets/js/site.js
Normal file
@@ -0,0 +1,315 @@
|
||||
(() => {
|
||||
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);
|
||||
})();
|
||||
Reference in New Issue
Block a user