/**
* 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}${tagName}>`;
}
} 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');
}
}