/** * intro-animation.js * 인트로 페이지 애니메이션 함수들 * 공통 모듈 활용 (ErrorHandler, DOMUtils, Utils, AnimationUtils) */ // 전역 의존성 (폴백 포함) const _domUtils = typeof DOMUtils !== 'undefined' ? DOMUtils : null; const _errorHandler = typeof ErrorHandler !== 'undefined' ? ErrorHandler : null; const _utils = typeof Utils !== 'undefined' ? Utils : null; const _animationUtils = typeof AnimationUtils !== 'undefined' ? AnimationUtils : null; /** * 에러 처리 헬퍼 * @private */ function _handleError(error, context, additionalInfo = {}) { if (_errorHandler) { _errorHandler.handle(error, { context: `IntroAnimation.${context}`, component: 'IntroAnimation', ...additionalInfo }, false); } else { console.error(`[IntroAnimation] ${context}:`, error, additionalInfo); } } /** * Section 1: 이름 타이핑 애니메이션 */ function typeName() { try { // 안전성 검사 if (typeof INTRO_CONFIG === 'undefined' || typeof INTRO_STATE === 'undefined') { _handleError(new Error('INTRO_CONFIG or INTRO_STATE is not defined'), 'typeName'); return; } const typedNameEl = _domUtils?.$("#typedName") || document.getElementById("typedName"); const cursorEl = _domUtils?.$("#cursor") || document.getElementById("cursor"); if (!typedNameEl) { _handleError(new Error('typedName element not found'), 'typeName'); return; } if (!INTRO_CONFIG.fullName || typeof INTRO_CONFIG.fullName !== 'string' || INTRO_CONFIG.fullName.length === 0) { _handleError(new Error('INTRO_CONFIG.fullName is empty or invalid'), 'typeName'); return; } if (INTRO_STATE.typedIndex < INTRO_CONFIG.fullName.length) { const currentText = INTRO_CONFIG.fullName.slice(0, INTRO_STATE.typedIndex + 1); typedNameEl.textContent = currentText; INTRO_STATE.typedIndex++; const delay = INTRO_CONFIG.typingSpeed || 100; if (_utils && _utils.delay) { _utils.delay(delay).then(() => typeName()); } else { setTimeout(typeName, delay); } } else { if (cursorEl) { if (_domUtils && _domUtils.setStyles) { _domUtils.setStyles(cursorEl, { opacity: '0' }); } else { cursorEl.style.opacity = "0"; } } const delay = 600; if (_utils && _utils.delay) { _utils.delay(delay).then(() => animateWelcome()); } else { setTimeout(animateWelcome, delay); } } } catch (error) { _handleError(error, 'typeName'); } } /** * Section 1: 환영 메시지 애니메이션 */ function animateWelcome() { try { if (typeof INTRO_CONFIG === 'undefined' || typeof INTRO_STATE === 'undefined') { _handleError(new Error('INTRO_CONFIG or INTRO_STATE is not defined'), 'animateWelcome'); return; } const container = _domUtils?.$("#welcome") || document.getElementById("welcome"); if (!container) { _handleError(new Error('welcome element not found'), 'animateWelcome'); return; } if (!INTRO_CONFIG.welcomeText || typeof INTRO_CONFIG.welcomeText !== 'string') { _handleError(new Error('INTRO_CONFIG.welcomeText is empty or invalid'), 'animateWelcome'); return; } const tempDiv = _domUtils?.createElement('div') || document.createElement("div"); tempDiv.innerHTML = INTRO_CONFIG.welcomeText; let html = ""; tempDiv.childNodes.forEach((node) => { try { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent || ''; html += text .split("") .map((c) => { const escaped = c === " " ? " " : (c === "<" ? "<" : c === ">" ? ">" : c === "&" ? "&" : c); return `${escaped}`; }) .join(""); } else if (node.nodeType === Node.ELEMENT_NODE) { const innerText = node.textContent || ''; const tagName = node.tagName.toLowerCase(); const escapedText = innerText .split("") .map((c) => { const escaped = c === " " ? " " : (c === "<" ? "<" : c === ">" ? ">" : c === "&" ? "&" : c); return `${escaped}`; }) .join(""); html += `<${tagName}>${escapedText}`; } } catch (error) { _handleError(error, 'animateWelcome.processNode', { node }); } }); container.innerHTML = html; const spans = container.querySelectorAll("span"); const charSpeed = INTRO_CONFIG.welcomeCharSpeed || 50; spans.forEach((char, i) => { const delay = i * charSpeed; if (_utils && _utils.delay) { _utils.delay(delay).then(() => { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(char, 'show'); } else { char.classList.add("show"); } }); } else { setTimeout(() => { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(char, 'show'); } else { char.classList.add("show"); } }, delay); } }); // 애니메이션 완료 후 자동 스크롤 타이머 시작 const fullNameLength = INTRO_CONFIG.fullName ? INTRO_CONFIG.fullName.length : 0; const totalDelay = fullNameLength * charSpeed + 1000; if (_utils && _utils.delay) { _utils.delay(totalDelay).then(() => startAutoScrollTimer()); } else { setTimeout(() => startAutoScrollTimer(), totalDelay); } } catch (error) { _handleError(error, 'animateWelcome'); } } /** * Section 2: 업데이트 텍스트 + 카드 애니메이션 * - 모바일(≤992px): section2Text 애니메이션 → 페이드아웃 → card-list 표시 * - 데스크탑(>992px): section2Text + card-list 동시 표시 */ function animateSection2() { try { if (typeof INTRO_STATE === 'undefined') { _handleError(new Error('INTRO_STATE is not defined'), 'animateSection2'); return; } const isMobile = window.innerWidth <= 992; const lines = ["line1", "line2", "line3"]; const cards = ["card1", "card2", "card3", "card4"]; const lineDelay = 1200; const cardDelay = isMobile ? 750 : 500; const totalLineDelay = lines.length * lineDelay + 300; const delayFn = (_utils && _utils.delay) ? (ms) => _utils.delay(ms) : (ms) => new Promise(resolve => setTimeout(resolve, ms)); // 라인 순차 애니메이션 lines.forEach((id, i) => { const element = _domUtils?.$(`#${id}`) || document.getElementById(id); if (!element) { console.warn(`[IntroAnimation] Element #${id} not found`); return; } delayFn(i * lineDelay).then(() => { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(element, 'show'); } else { element.classList.add("show"); } }); }); // 카드 순차 애니메이션 헬퍼 const showCards = () => { cards.forEach((id, i) => { const cardElement = _domUtils?.$(`#${id}`) || document.getElementById(id); if (!cardElement) { console.warn(`[IntroAnimation] Element #${id} not found`); return; } delayFn(i * cardDelay).then(() => { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(cardElement, 'show'); } else { cardElement.classList.add("show"); } if (i === 3) { INTRO_STATE.section2AnimDone = true; startAutoScrollTimer(); } }); }); }; delayFn(totalLineDelay).then(() => { if (isMobile) { // 모바일: section2Text 페이드아웃 → cardsContainer 표시 → 카드 애니메이션 const section2Text = _domUtils?.$("#section2Text") || document.getElementById("section2Text"); const cardsContainer = _domUtils?.$("#cardsContainer") || document.getElementById("cardsContainer"); if (section2Text) { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(section2Text, 'fade-out'); } else { section2Text.classList.add("fade-out"); } } delayFn(800).then(() => { if (section2Text) { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(section2Text, 'hidden'); _domUtils.removeClasses(section2Text, 'fade-out'); } else { section2Text.classList.add("hidden"); section2Text.classList.remove("fade-out"); } } // 카드 표시 시 스크롤 인디케이터 숨기기 const scrollIndicator = _domUtils?.$("#scrollIndicator") || document.getElementById("scrollIndicator"); if (scrollIndicator) { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(scrollIndicator, 'hidden'); } else { scrollIndicator.classList.add("hidden"); } } if (cardsContainer) { if (_domUtils && _domUtils.setStyles) { _domUtils.setStyles(cardsContainer, { display: 'flex' }); } else { cardsContainer.style.display = "flex"; } } showCards(); }); } else { // 데스크탑: 바로 카드 표시 showCards(); } }); } catch (error) { _handleError(error, 'animateSection2'); } } /** * Section 2: 애니메이션 리셋 */ function resetSection2() { try { if (typeof INTRO_STATE === 'undefined') { _handleError(new Error('INTRO_STATE is not defined'), 'resetSection2'); return; } const lines = ["line1", "line2", "line3"]; const cards = ["card1", "card2", "card3", "card4"]; lines.forEach((id) => { const element = _domUtils?.$(`#${id}`) || document.getElementById(id); if (element) { if (_domUtils && _domUtils.removeClasses) { _domUtils.removeClasses(element, 'show'); } else { element.classList.remove("show"); } } }); cards.forEach((id) => { const element = _domUtils?.$(`#${id}`) || document.getElementById(id); if (element) { if (_domUtils && _domUtils.removeClasses) { _domUtils.removeClasses(element, 'show'); } else { element.classList.remove("show"); } } }); INTRO_STATE.section2AnimDone = false; } catch (error) { _handleError(error, 'resetSection2'); } } /** * Section 3: CTA 애니메이션 */ function animateSection3() { try { const ctaBtn = _domUtils?.$("#ctaBtn") || document.getElementById("ctaBtn"); const ctaLine1 = _domUtils?.$("#ctaLine1") || document.getElementById("ctaLine1"); const ctaLine2 = _domUtils?.$("#ctaLine2") || document.getElementById("ctaLine2"); const addShowClass = (element, delay) => { if (!element) { console.warn(`[IntroAnimation] Element not found`); return; } if (_utils && _utils.delay) { _utils.delay(delay).then(() => { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(element, 'show'); } else { element.classList.add("show"); } }); } else { setTimeout(() => { if (_domUtils && _domUtils.addClasses) { _domUtils.addClasses(element, 'show'); } else { element.classList.add("show"); } }, delay); } }; addShowClass(ctaBtn, 100); addShowClass(ctaLine1, 300); addShowClass(ctaLine2, 500); // 모바일: 마스크 중앙에서 전체 화면으로 자동 확장 (CTA 섹션 진입 후 약 1초 뒤) if (window.innerWidth <= 992) { const maskContainer = _domUtils?.$("#maskContainer") || document.getElementById("maskContainer"); if (maskContainer && typeof INTRO_STATE !== "undefined") { if (INTRO_STATE.mobileMaskExpandTimer) { clearTimeout(INTRO_STATE.mobileMaskExpandTimer); INTRO_STATE.mobileMaskExpandTimer = null; } const mobileMaskDelayMs = 1500; const runExpand = () => { INTRO_STATE.mobileMaskExpandTimer = null; if (INTRO_STATE.currentSection === 2) { maskContainer.classList.add("expand"); } }; INTRO_STATE.mobileMaskExpandTimer = setTimeout(runExpand, mobileMaskDelayMs); } } } catch (error) { _handleError(error, 'animateSection3'); } } /** * Section 3: 애니메이션 리셋 */ function resetSection3() { try { const ctaLine1 = _domUtils?.$("#ctaLine1") || document.getElementById("ctaLine1"); const ctaLine2 = _domUtils?.$("#ctaLine2") || document.getElementById("ctaLine2"); const ctaBtn = _domUtils?.$("#ctaBtn") || document.getElementById("ctaBtn"); const removeShowClass = (element) => { if (element) { if (_domUtils && _domUtils.removeClasses) { _domUtils.removeClasses(element, 'show'); } else { element.classList.remove("show"); } } }; removeShowClass(ctaLine1); removeShowClass(ctaLine2); removeShowClass(ctaBtn); // 모바일: 마스크 확장 리셋 · 진행 중인 자동 확장 타이머 취소 if (typeof INTRO_STATE !== "undefined" && INTRO_STATE.mobileMaskExpandTimer) { clearTimeout(INTRO_STATE.mobileMaskExpandTimer); INTRO_STATE.mobileMaskExpandTimer = null; } const maskContainer = _domUtils?.$("#maskContainer") || document.getElementById("maskContainer"); if (maskContainer) { maskContainer.classList.remove("expand"); } } catch (error) { _handleError(error, 'resetSection3'); } } /** * 자동 스크롤 타이머 시작 */ function startAutoScrollTimer() { try { if (typeof INTRO_STATE === 'undefined') { _handleError(new Error('INTRO_STATE is not defined'), 'startAutoScrollTimer'); return; } if (typeof handleScrollDown !== 'function') { _handleError(new Error('handleScrollDown function is not defined'), 'startAutoScrollTimer'); return; } // 기존 타이머 정리 if (INTRO_STATE.autoScrollTimer) { clearTimeout(INTRO_STATE.autoScrollTimer); INTRO_STATE.autoScrollTimer = null; } // 섹션 0: 1500ms, 섹션 1(업데이트+카드): 6000ms, 그 외: 3000ms const delay = INTRO_STATE.currentSection === 0 ? 1500 : INTRO_STATE.currentSection === 1 ? 6000 : 3000; if (_utils && _utils.delay) { _utils.delay(delay).then(() => { // 마지막 상호작용 후 설정된 시간이 지났는지 확인 const now = Date.now(); const lastInteraction = INTRO_STATE.lastInteractionTime || 0; if (now - lastInteraction >= delay) { try { handleScrollDown(); } catch (error) { _handleError(error, 'startAutoScrollTimer.handleScrollDown'); } } }); } else { INTRO_STATE.autoScrollTimer = setTimeout(() => { try { // 마지막 상호작용 후 설정된 시간이 지났는지 확인 const now = Date.now(); const lastInteraction = INTRO_STATE.lastInteractionTime || 0; if (now - lastInteraction >= delay) { handleScrollDown(); } } catch (error) { _handleError(error, 'startAutoScrollTimer.handleScrollDown'); } }, delay); } } catch (error) { _handleError(error, 'startAutoScrollTimer'); } } /** * 사용자 상호작용 감지 - 자동 스크롤 타이머 리셋 */ function resetAutoScrollTimer() { try { if (typeof INTRO_STATE === 'undefined') { _handleError(new Error('INTRO_STATE is not defined'), 'resetAutoScrollTimer'); return; } INTRO_STATE.lastInteractionTime = Date.now(); startAutoScrollTimer(); } catch (error) { _handleError(error, 'resetAutoScrollTimer'); } }