- 폰트: Pretendard/Noto Sans KR → 나눔스퀘어(NanumSquare)로 전면 교체 - CSS 변수 체계화: font-weight, z-index, duration, border-radius 등 토큰 추가 - 이모지 → Font Awesome 아이콘 전면 교체 (전 페이지) - 푸터 3단 그리드 구조 개편 (회사정보/연락처/오시는 길) - CTA 섹션 다크 시네마틱 디자인 적용 - 컨택트 페이지 카드 hover 효과 및 위치 섹션 개선 - 가격 금액 색상 원래 오렌지로 복원 - 다크모드 글래스모피즘 일관성 강화 - 이미지 및 프로필 리소스 업데이트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
513 lines
17 KiB
JavaScript
513 lines
17 KiB
JavaScript
// ========================================
|
|
// FAQ 페이지 전용 JavaScript
|
|
// ========================================
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initFAQ();
|
|
initSearch();
|
|
initCategories();
|
|
initAnimations();
|
|
initEmailForm();
|
|
});
|
|
|
|
// FAQ 기능 초기화
|
|
function initFAQ() {
|
|
const faqItems = document.querySelectorAll('.faq-item');
|
|
|
|
faqItems.forEach(item => {
|
|
const question = item.querySelector('.faq-question');
|
|
const toggle = item.querySelector('.faq-toggle');
|
|
const answer = item.querySelector('.faq-answer');
|
|
|
|
if (question && toggle && answer) {
|
|
question.addEventListener('click', () => toggleFAQ(item));
|
|
|
|
// 키보드 접근성
|
|
question.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
toggleFAQ(item);
|
|
}
|
|
});
|
|
|
|
// 포커스 가능하도록 설정
|
|
question.setAttribute('tabindex', '0');
|
|
question.setAttribute('role', 'button');
|
|
question.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
}
|
|
|
|
// FAQ 아이템 토글
|
|
function toggleFAQ(item) {
|
|
const isActive = item.classList.contains('active');
|
|
const answer = item.querySelector('.faq-answer');
|
|
const question = item.querySelector('.faq-question');
|
|
|
|
if (isActive) {
|
|
// 닫기
|
|
item.classList.remove('active');
|
|
question.setAttribute('aria-expanded', 'false');
|
|
answer.style.maxHeight = '0';
|
|
} else {
|
|
// 다른 모든 FAQ 닫기 (아코디언 효과)
|
|
document.querySelectorAll('.faq-item.active').forEach(activeItem => {
|
|
if (activeItem !== item) {
|
|
activeItem.classList.remove('active');
|
|
activeItem.querySelector('.faq-question').setAttribute('aria-expanded', 'false');
|
|
activeItem.querySelector('.faq-answer').style.maxHeight = '0';
|
|
}
|
|
});
|
|
|
|
// 현재 FAQ 열기
|
|
item.classList.add('active');
|
|
question.setAttribute('aria-expanded', 'true');
|
|
|
|
// 정확한 높이 계산을 위해 잠시 보이게 한 후 측정
|
|
answer.style.maxHeight = 'none';
|
|
answer.style.overflow = 'visible';
|
|
const height = answer.scrollHeight;
|
|
answer.style.maxHeight = '0';
|
|
answer.style.overflow = 'hidden';
|
|
|
|
// 애니메이션을 위해 약간의 지연 후 높이 설정
|
|
setTimeout(() => {
|
|
answer.style.maxHeight = (height + 50) + 'px';
|
|
}, 10);
|
|
|
|
// 스크롤 애니메이션
|
|
setTimeout(() => {
|
|
item.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'nearest'
|
|
});
|
|
}, 100);
|
|
}
|
|
}
|
|
|
|
// 검색 기능 초기화
|
|
function initSearch() {
|
|
const searchInput = document.getElementById('faqSearch');
|
|
const searchBtn = document.querySelector('.search-btn');
|
|
const suggestions = document.getElementById('searchSuggestions');
|
|
|
|
if (searchInput) {
|
|
let searchTimeout;
|
|
|
|
searchInput.addEventListener('input', function() {
|
|
clearTimeout(searchTimeout);
|
|
searchTimeout = setTimeout(() => {
|
|
handleSearch(this.value.trim());
|
|
updateSearchSuggestions(this.value.trim());
|
|
}, 300);
|
|
});
|
|
|
|
searchInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
handleSearch(this.value.trim());
|
|
hideSuggestions();
|
|
}
|
|
});
|
|
|
|
// 검색 버튼 클릭
|
|
if (searchBtn) {
|
|
searchBtn.addEventListener('click', () => {
|
|
handleSearch(searchInput.value.trim());
|
|
hideSuggestions();
|
|
});
|
|
}
|
|
|
|
// 클릭 외부 영역 시 제안 사항 숨기기
|
|
document.addEventListener('click', function(e) {
|
|
if (!searchInput.contains(e.target) && !suggestions?.contains(e.target)) {
|
|
hideSuggestions();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 검색 처리
|
|
function handleSearch(query) {
|
|
const faqItems = document.querySelectorAll('.faq-item');
|
|
const noResults = document.querySelector('.no-results');
|
|
let hasResults = false;
|
|
|
|
if (!query) {
|
|
// 검색어가 없으면 모든 항목 표시
|
|
faqItems.forEach(item => {
|
|
item.classList.remove('hidden');
|
|
clearSearchHighlight(item);
|
|
});
|
|
hideNoResults();
|
|
return;
|
|
}
|
|
|
|
const searchRegex = new RegExp(query, 'gi');
|
|
|
|
faqItems.forEach(item => {
|
|
const question = item.querySelector('.faq-question h3');
|
|
const answer = item.querySelector('.faq-answer');
|
|
const questionText = question.textContent;
|
|
const answerText = answer.textContent;
|
|
|
|
// 검색어 매칭 확인
|
|
const questionMatch = searchRegex.test(questionText);
|
|
const answerMatch = searchRegex.test(answerText);
|
|
|
|
if (questionMatch || answerMatch) {
|
|
item.classList.remove('hidden');
|
|
hasResults = true;
|
|
|
|
// 검색어 하이라이트
|
|
highlightSearchTerm(item, query);
|
|
|
|
// 답변에 매칭되는 경우 자동으로 열기
|
|
if (answerMatch && !questionMatch) {
|
|
toggleFAQ(item);
|
|
}
|
|
} else {
|
|
item.classList.add('hidden');
|
|
clearSearchHighlight(item);
|
|
}
|
|
});
|
|
|
|
// 검색 결과가 없는 경우
|
|
if (!hasResults) {
|
|
showNoResults(query);
|
|
} else {
|
|
hideNoResults();
|
|
}
|
|
}
|
|
|
|
// 검색어 하이라이트 (XSS-safe: DOM API 사용)
|
|
function highlightSearchTerm(item, term) {
|
|
const question = item.querySelector('.faq-question h3');
|
|
const answer = item.querySelector('.faq-answer');
|
|
|
|
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const highlightRegex = new RegExp(`(${escapedTerm})`, 'gi');
|
|
|
|
// 질문 하이라이트
|
|
const originalQuestionText = question.dataset.originalText || question.textContent;
|
|
question.dataset.originalText = originalQuestionText;
|
|
highlightTextContent(question, originalQuestionText, highlightRegex);
|
|
|
|
// 답변 하이라이트
|
|
const answerElements = answer.querySelectorAll('p, li');
|
|
answerElements.forEach(el => {
|
|
const originalText = el.dataset.originalText || el.textContent;
|
|
el.dataset.originalText = originalText;
|
|
highlightTextContent(el, originalText, highlightRegex);
|
|
});
|
|
}
|
|
|
|
// XSS-safe 텍스트 하이라이트 (DOM API로 안전하게 생성)
|
|
function highlightTextContent(element, text, regex) {
|
|
element.textContent = '';
|
|
let lastIndex = 0;
|
|
let match;
|
|
regex.lastIndex = 0;
|
|
|
|
while ((match = regex.exec(text)) !== null) {
|
|
// 매칭 전 텍스트 추가
|
|
if (match.index > lastIndex) {
|
|
element.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
|
|
}
|
|
// 하이라이트 span 추가
|
|
const span = document.createElement('span');
|
|
span.className = 'search-highlight';
|
|
span.textContent = match[1];
|
|
element.appendChild(span);
|
|
lastIndex = regex.lastIndex;
|
|
}
|
|
// 나머지 텍스트 추가
|
|
if (lastIndex < text.length) {
|
|
element.appendChild(document.createTextNode(text.slice(lastIndex)));
|
|
}
|
|
}
|
|
|
|
// 검색 하이라이트 제거
|
|
function clearSearchHighlight(item) {
|
|
const question = item.querySelector('.faq-question h3');
|
|
const answer = item.querySelector('.faq-answer');
|
|
|
|
// 질문 하이라이트 제거
|
|
if (question.dataset.originalText) {
|
|
question.textContent = question.dataset.originalText;
|
|
delete question.dataset.originalText;
|
|
}
|
|
|
|
// 답변 하이라이트 제거
|
|
const answerElements = answer.querySelectorAll('p, li');
|
|
answerElements.forEach(el => {
|
|
if (el.dataset.originalText) {
|
|
el.textContent = el.dataset.originalText;
|
|
delete el.dataset.originalText;
|
|
}
|
|
});
|
|
}
|
|
|
|
// 검색 제안사항 업데이트
|
|
function updateSearchSuggestions(query) {
|
|
const suggestions = document.getElementById('searchSuggestions');
|
|
if (!suggestions || !query || query.length < 2) {
|
|
hideSuggestions();
|
|
return;
|
|
}
|
|
|
|
// 미리 정의된 검색 키워드
|
|
const searchKeywords = [
|
|
'예약', '취소', '환불', '요금', '가격', '결제',
|
|
'장비', '모션캡쳐', '시간', '인원', '준비물',
|
|
'데이터', '파일', '형식', '스트리밍', '버튜버',
|
|
'주차', '위치', '견학', '투어', '방역', '코로나'
|
|
];
|
|
|
|
const matchingKeywords = searchKeywords.filter(keyword =>
|
|
keyword.includes(query) || query.includes(keyword)
|
|
);
|
|
|
|
if (matchingKeywords.length > 0) {
|
|
suggestions.textContent = '';
|
|
matchingKeywords.slice(0, 5).forEach(keyword => {
|
|
const div = document.createElement('div');
|
|
div.className = 'suggestion-item';
|
|
div.textContent = keyword;
|
|
div.addEventListener('click', () => selectSuggestion(keyword));
|
|
suggestions.appendChild(div);
|
|
});
|
|
suggestions.classList.add('active');
|
|
} else {
|
|
hideSuggestions();
|
|
}
|
|
}
|
|
|
|
// 제안사항 선택
|
|
function selectSuggestion(keyword) {
|
|
const searchInput = document.getElementById('faqSearch');
|
|
if (searchInput) {
|
|
searchInput.value = keyword;
|
|
handleSearch(keyword);
|
|
}
|
|
hideSuggestions();
|
|
}
|
|
|
|
// 제안사항 숨기기
|
|
function hideSuggestions() {
|
|
const suggestions = document.getElementById('searchSuggestions');
|
|
if (suggestions) {
|
|
suggestions.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
// 검색 결과 없음 표시
|
|
function showNoResults(query) {
|
|
const safeQuery = query.replace(/[<>&"]/g, c => ({'<':'<','>':'>','&':'&','"':'"'})[c]);
|
|
let noResults = document.querySelector('.no-results');
|
|
if (!noResults) {
|
|
noResults = document.createElement('div');
|
|
noResults.className = 'no-results';
|
|
noResults.innerHTML = `
|
|
<div class="no-results-icon">🔍</div>
|
|
<h3>검색 결과가 없습니다</h3>
|
|
<p><strong>"${safeQuery}"</strong>와 관련된 질문을 찾을 수 없습니다.</p>
|
|
<p>다른 키워드로 검색해보시거나 <a href="contact.html">직접 문의</a>해 주세요.</p>
|
|
`;
|
|
document.querySelector('.faq-list').appendChild(noResults);
|
|
} else {
|
|
noResults.querySelector('p strong').textContent = `"${query}"`;
|
|
}
|
|
noResults.classList.add('active');
|
|
}
|
|
|
|
// 검색 결과 없음 숨기기
|
|
function hideNoResults() {
|
|
const noResults = document.querySelector('.no-results');
|
|
if (noResults) {
|
|
noResults.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
// 카테고리 필터 초기화
|
|
function initCategories() {
|
|
const categoryBtns = document.querySelectorAll('.category-btn');
|
|
|
|
categoryBtns.forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
const category = this.dataset.category;
|
|
|
|
// 활성 버튼 업데이트
|
|
categoryBtns.forEach(b => {
|
|
b.classList.remove('active');
|
|
b.setAttribute('aria-pressed', 'false');
|
|
});
|
|
this.classList.add('active');
|
|
this.setAttribute('aria-pressed', 'true');
|
|
|
|
// FAQ 필터링
|
|
filterByCategory(category);
|
|
|
|
// 검색 초기화
|
|
const searchInput = document.getElementById('faqSearch');
|
|
if (searchInput) {
|
|
searchInput.value = '';
|
|
}
|
|
hideSuggestions();
|
|
hideNoResults();
|
|
});
|
|
});
|
|
}
|
|
|
|
// 카테고리별 필터링
|
|
function filterByCategory(category) {
|
|
const faqItems = document.querySelectorAll('.faq-item');
|
|
|
|
faqItems.forEach(item => {
|
|
// 모든 FAQ 닫기
|
|
item.classList.remove('active');
|
|
item.querySelector('.faq-answer').style.maxHeight = '0';
|
|
item.querySelector('.faq-question').setAttribute('aria-expanded', 'false');
|
|
|
|
// 검색 하이라이트 제거
|
|
clearSearchHighlight(item);
|
|
|
|
if (category === 'all' || item.dataset.category === category) {
|
|
item.classList.remove('hidden');
|
|
} else {
|
|
item.classList.add('hidden');
|
|
}
|
|
});
|
|
|
|
// 페이지 상단으로 스크롤
|
|
document.querySelector('.faq-list').scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
|
|
// 애니메이션 초기화
|
|
function initAnimations() {
|
|
// Intersection Observer를 사용한 스크롤 애니메이션
|
|
const observerOptions = {
|
|
threshold: 0.1,
|
|
rootMargin: '50px 0px'
|
|
};
|
|
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry, index) => {
|
|
if (entry.isIntersecting) {
|
|
setTimeout(() => {
|
|
entry.target.style.opacity = '1';
|
|
entry.target.style.transform = 'translateY(0)';
|
|
}, index * 100);
|
|
observer.unobserve(entry.target);
|
|
}
|
|
});
|
|
}, observerOptions);
|
|
|
|
// FAQ 아이템들에 초기 스타일 적용 및 관찰 시작
|
|
const faqItems = document.querySelectorAll('.faq-item');
|
|
faqItems.forEach(item => {
|
|
item.style.opacity = '0';
|
|
item.style.transform = 'translateY(30px)';
|
|
item.style.transition = 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)';
|
|
observer.observe(item);
|
|
});
|
|
}
|
|
|
|
// URL 해시로 특정 FAQ 열기
|
|
function openFAQByHash() {
|
|
const hash = window.location.hash.substring(1);
|
|
if (!hash) return;
|
|
// CSS selector injection 방지
|
|
const safeHash = CSS.escape ? CSS.escape(hash) : hash.replace(/[^\w-]/g, '');
|
|
const faqItem = document.querySelector(`[data-id="${safeHash}"]`);
|
|
if (faqItem) {
|
|
toggleFAQ(faqItem);
|
|
faqItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
}
|
|
}
|
|
|
|
// 페이지 로드 시 해시 확인
|
|
window.addEventListener('load', openFAQByHash);
|
|
|
|
// FAQ 공유 기능 (선택사항)
|
|
function shareFAQ(faqId) {
|
|
const url = `${window.location.origin}${window.location.pathname}#${faqId}`;
|
|
|
|
if (navigator.share) {
|
|
navigator.share({
|
|
title: 'FAQ - 밍글 스튜디오',
|
|
url: url
|
|
});
|
|
} else {
|
|
// 클립보드에 복사 (폴백 포함)
|
|
const copyToClipboard = (text) => {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
return navigator.clipboard.writeText(text);
|
|
}
|
|
// 구형 브라우저 폴백
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = text;
|
|
textarea.style.position = 'fixed';
|
|
textarea.style.opacity = '0';
|
|
document.body.appendChild(textarea);
|
|
textarea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textarea);
|
|
return Promise.resolve();
|
|
};
|
|
|
|
copyToClipboard(url).then(() => {
|
|
if (window.commonUtils && window.commonUtils.showNotification) {
|
|
window.commonUtils.showNotification('링크가 클립보드에 복사되었습니다.', 'success');
|
|
}
|
|
}).catch(() => {
|
|
if (window.commonUtils && window.commonUtils.showNotification) {
|
|
window.commonUtils.showNotification('링크 복사에 실패했습니다.', 'error');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// 이메일 폼 초기화
|
|
function initEmailForm() {
|
|
const emailBtn = document.getElementById('showEmailFormFAQ');
|
|
const emailForm = document.getElementById('emailFormFAQ');
|
|
|
|
if (emailBtn && emailForm) {
|
|
emailBtn.addEventListener('click', function() {
|
|
if (emailForm.style.display === 'none' || !emailForm.style.display) {
|
|
emailForm.style.display = 'block';
|
|
emailBtn.textContent = '📧 양식 숨기기';
|
|
|
|
// FAQ 답변 높이 재계산
|
|
const faqItem = emailForm.closest('.faq-item');
|
|
const answer = faqItem.querySelector('.faq-answer');
|
|
if (faqItem.classList.contains('active')) {
|
|
// 높이 재계산을 위해 잠시 auto로 설정
|
|
answer.style.maxHeight = 'none';
|
|
const newHeight = answer.scrollHeight;
|
|
answer.style.maxHeight = (newHeight + 100) + 'px'; // 여유분 추가
|
|
}
|
|
|
|
setTimeout(() => {
|
|
emailForm.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}, 100);
|
|
} else {
|
|
emailForm.style.display = 'none';
|
|
emailBtn.innerHTML = '<span>📧</span> 이메일 문의하기';
|
|
|
|
// FAQ 답변 높이 재계산
|
|
const faqItem = emailForm.closest('.faq-item');
|
|
const answer = faqItem.querySelector('.faq-answer');
|
|
if (faqItem.classList.contains('active')) {
|
|
answer.style.maxHeight = 'none';
|
|
const newHeight = answer.scrollHeight;
|
|
answer.style.maxHeight = (newHeight + 50) + 'px';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
} |