Initial commit: 교육 프로젝트 배포

This commit is contained in:
송대일
2026-07-01 18:32:42 +09:00
commit be6dccd120
1483 changed files with 5082202 additions and 0 deletions
+508
View File
@@ -0,0 +1,508 @@
/**
* 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 === " " ? "&nbsp;" : (c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === "&" ? "&amp;" : c);
return `<span>${escaped}</span>`;
})
.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 === " " ? "&nbsp;" : (c === "<" ? "&lt;" : c === ">" ? "&gt;" : c === "&" ? "&amp;" : c);
return `<span>${escaped}</span>`;
})
.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');
}
}
+162
View File
@@ -0,0 +1,162 @@
/**
* intro-events.js
* 이벤트 리스너 설정
*/
/**
* 모든 이벤트 리스너 등록
*/
function initEventListeners() {
// 마우스 휠 이벤트
document.addEventListener("wheel", (e) => {
resetAutoScrollTimer();
if (e.deltaY > 0) handleScrollDown();
else handleScrollUp();
});
// 터치 이벤트
let touchStartY = 0;
document.addEventListener(
"touchstart",
(e) => {
touchStartY = e.touches[0].clientY;
resetAutoScrollTimer();
},
{ passive: true }
);
document.addEventListener(
"touchend",
(e) => {
const diff = touchStartY - e.changedTouches[0].clientY;
if (Math.abs(diff) > 60) {
if (diff > 0) handleScrollDown();
else handleScrollUp();
}
},
{ passive: true }
);
// 키보드 이벤트
document.addEventListener("keydown", (e) => {
if (["ArrowDown", "PageDown", " "].includes(e.key)) {
e.preventDefault();
resetAutoScrollTimer();
handleScrollDown();
} else if (["ArrowUp", "PageUp"].includes(e.key)) {
e.preventDefault();
resetAutoScrollTimer();
handleScrollUp();
}
});
// 마우스 움직임 이벤트 (Section 3 마스크 효과)
// requestAnimationFrame을 사용하여 부드러운 업데이트 보장
let rafId = null;
let lastMouseEvent = null;
document.addEventListener("mousemove", (e) => {
if (typeof INTRO_STATE !== 'undefined' && INTRO_STATE.currentSection === 2) {
// 마지막 마우스 이벤트 저장
lastMouseEvent = e;
// 이미 요청된 애니메이션 프레임이 없으면 새로 요청
if (rafId === null) {
rafId = requestAnimationFrame(() => {
if (lastMouseEvent) {
handleMouseMoveOnSection3(lastMouseEvent);
}
rafId = null;
lastMouseEvent = null;
});
}
}
});
}
/**
* Section 3의 마우스 움직임 처리 (마스크 효과)
* @param {MouseEvent} e - 마우스 이벤트
*/
function handleMouseMoveOnSection3(e) {
try {
// 입력 검증
if (!e || typeof e.clientX !== 'number' || typeof e.clientY !== 'number') {
return;
}
const maskContainer = document.getElementById("maskContainer");
if (!maskContainer) {
return; // 요소가 없으면 조용히 종료
}
const mouseX = e.clientX;
const mouseY = e.clientY;
const mainContainer = document.getElementById("mainContainer");
if (!mainContainer) {
return;
}
const mainContainerRect = mainContainer.getBoundingClientRect();
const h1Elements = mainContainer.querySelectorAll("p");
const ctaBtn = document.querySelector(".cta-btn");
let hovering = false;
let isCtaBtnHovering = false;
// .cta-btn 위에 마우스가 있는지 먼저 체크
if (ctaBtn) {
try {
const ctaRect = ctaBtn.getBoundingClientRect();
if (
mouseX >= ctaRect.left &&
mouseX <= ctaRect.right &&
mouseY >= ctaRect.top &&
mouseY <= ctaRect.bottom
) {
hovering = true;
isCtaBtnHovering = true;
}
} catch (error) {
console.warn('[IntroEvents] CTA 버튼 체크 중 오류:', error);
}
}
// p 요소들 체크
try {
h1Elements.forEach((element) => {
try {
const h1Rect = element.getBoundingClientRect();
if (
mouseX >= h1Rect.left &&
mouseX <= h1Rect.right &&
mouseY >= h1Rect.top &&
mouseY <= h1Rect.bottom
) {
hovering = true;
}
} catch (error) {
// 개별 요소 체크 실패는 무시
}
});
} catch (error) {
console.warn('[IntroEvents] 요소 체크 중 오류:', error);
}
// .cta-btn 위에 있을 때만 세로 위치 고정, 그 외에는 마우스 위치 따라감
const targetY = isCtaBtnHovering
? mainContainerRect.top + mainContainerRect.height / 2.3
: mouseY;
// CSS 변수 설정 (안전하게)
const x = Math.max(0, Math.min(mouseX, window.innerWidth));
const y = Math.max(0, Math.min(targetY, window.innerHeight));
const targetSize = hovering ? 380 : 30;
maskContainer.style.setProperty("--x", `${x}px`);
maskContainer.style.setProperty("--y", `${y}px`);
maskContainer.style.setProperty("--size", `${targetSize}px`);
} catch (error) {
console.error('[IntroEvents] handleMouseMoveOnSection3 오류:', error);
}
}
+267
View File
@@ -0,0 +1,267 @@
/**
* intro-init.js
* 인트로 페이지 초기화
* 공통 모듈 활용 (ErrorHandler, DOMUtils, Utils, EventManager)
*/
// 전역 의존성 (폴백 포함)
const _initDomUtils = typeof DOMUtils !== 'undefined' ? DOMUtils : null;
const _initErrorHandler = typeof ErrorHandler !== 'undefined' ? ErrorHandler : null;
const _initUtils = typeof Utils !== 'undefined' ? Utils : null;
const _initEventManager = typeof eventManager !== 'undefined' ? eventManager : null;
/**
* 에러 처리 헬퍼
* @private
*/
function _handleError(error, context, additionalInfo = {}) {
if (_initErrorHandler) {
_initErrorHandler.handle(error, {
context: `IntroInit.${context}`,
component: 'IntroInit',
...additionalInfo
}, false);
} else {
console.error(`[IntroInit] ${context}:`, error, additionalInfo);
}
}
/**
* 페이지 초기 설정
*/
function initializeIntroPage() {
try {
// DOM 요소 선택
const cardsContainer = _initDomUtils?.$("#cardsContainer") || document.getElementById("cardsContainer");
const ctaBtn = _initDomUtils?.$("#ctaBtn") || document.getElementById("ctaBtn");
const maskContainer = _initDomUtils?.$("#maskContainer") || document.getElementById("maskContainer");
const typedNameEl = _initDomUtils?.$("#typedName") || document.getElementById("typedName");
const cursorEl = _initDomUtils?.$("#cursor") || document.getElementById("cursor");
// 초기 요소 숨김 처리
if (cardsContainer) {
if (_initDomUtils && _initDomUtils.setStyles) {
_initDomUtils.setStyles(cardsContainer, { display: 'none' });
} else {
cardsContainer.style.display = "none";
}
} else {
console.warn('[IntroInit] cardsContainer 요소를 찾을 수 없습니다.');
}
if (ctaBtn) {
if (_initDomUtils && _initDomUtils.setStyles) {
_initDomUtils.setStyles(ctaBtn, { display: 'none' });
} else {
ctaBtn.style.display = "none";
}
} else {
console.warn('[IntroInit] ctaBtn 요소를 찾을 수 없습니다.');
}
if (maskContainer) {
maskContainer.style.setProperty("--size", "30px");
} else {
console.warn('[IntroInit] maskContainer 요소를 찾을 수 없습니다.');
}
// 이벤트 리스너 등록
if (typeof initEventListeners === 'function') {
try {
initEventListeners();
} catch (error) {
_handleError(error, 'initializeIntroPage.initEventListeners');
}
} else {
console.warn('[IntroInit] initEventListeners 함수를 찾을 수 없습니다.');
}
// typedIndex 초기화 및 typedName 요소 초기화
if (typeof INTRO_STATE !== 'undefined') {
INTRO_STATE.typedIndex = 0;
} else {
console.warn('[IntroInit] INTRO_STATE가 정의되지 않았습니다.');
}
if (typedNameEl) {
typedNameEl.textContent = "";
} else {
console.warn('[IntroInit] typedName 요소를 찾을 수 없습니다.');
}
if (cursorEl) {
if (_initDomUtils && _initDomUtils.setStyles) {
_initDomUtils.setStyles(cursorEl, { opacity: '1' });
} else {
cursorEl.style.opacity = "1";
}
} else {
console.warn('[IntroInit] cursor 요소를 찾을 수 없습니다.');
}
// 타이핑 애니메이션 시작 (INTRO_CONFIG가 준비된 후)
const animationDelay = 800;
const delayFn = _initUtils && _initUtils.delay ? _initUtils.delay : (ms) => new Promise(resolve => setTimeout(resolve, ms));
delayFn(animationDelay).then(() => {
try {
if (typeof INTRO_CONFIG !== 'undefined' && typeof INTRO_STATE !== 'undefined' && INTRO_CONFIG.fullName) {
// text-ani 섹션 표시 (CSS에서 display: none이므로 animating 클래스 추가)
const textAniSection = _initDomUtils?.$('.text-ani') || document.querySelector('.text-ani');
if (textAniSection) {
if (_initDomUtils && _initDomUtils.addClasses) {
_initDomUtils.addClasses(textAniSection, 'animating');
} else {
textAniSection.classList.add('animating');
}
}
INTRO_STATE.typedIndex = 0;
if (typeof typeName === 'function') {
typeName();
} else {
console.warn('[IntroInit] typeName 함수를 찾을 수 없습니다.');
}
} else {
console.warn('[IntroInit] INTRO_CONFIG 또는 INTRO_STATE가 준비되지 않았거나 fullName이 없습니다.');
}
} catch (error) {
_handleError(error, 'initializeIntroPage.animationStart');
}
});
} catch (error) {
_handleError(error, 'initializeIntroPage');
}
}
// DOM이 로드되면 초기화 실행
function setupInitialization() {
try {
if (document.readyState === "loading") {
// EventManager를 사용하여 이벤트 등록 (폴백 포함)
if (_initEventManager) {
_initEventManager.once(document, "DOMContentLoaded", initializeIntroPage);
} else {
document.addEventListener("DOMContentLoaded", initializeIntroPage, { once: true });
}
} else {
// DOMContentLoaded 이벤트가 이미 발생한 경우
initializeIntroPage();
}
} catch (error) {
_handleError(error, 'setupInitialization');
// 폴백: 에러가 발생해도 초기화 시도
if (document.readyState !== "loading") {
initializeIntroPage();
}
}
}
setupInitialization();
/**
* 외부에서 사용자 이름을 설정하고 다시 시작하는 함수
* @param {string} name - 사용자 이름
*/
function restartIntroWithName(name) {
try {
// 입력 검증
if (typeof name !== 'string' || name.trim() === '') {
_handleError(new Error('유효하지 않은 사용자 이름'), 'restartIntroWithName', { name });
return;
}
if (typeof INTRO_STATE === 'undefined') {
_handleError(new Error('INTRO_STATE가 정의되지 않았습니다'), 'restartIntroWithName');
return;
}
// 이름 설정
if (typeof setUserName === 'function') {
try {
setUserName(name);
} catch (error) {
_handleError(error, 'restartIntroWithName.setUserName');
}
} else {
console.warn('[IntroInit] setUserName 함수를 찾을 수 없습니다.');
}
// 상태 초기화
INTRO_STATE.currentSection = 0;
INTRO_STATE.typedIndex = 0;
INTRO_STATE.isScrolling = false;
INTRO_STATE.isAnimating = false;
INTRO_STATE.section2AnimDone = false;
clearTimeout(INTRO_STATE.autoScrollTimer);
INTRO_STATE.lastInteractionTime = Date.now();
// DOM 요소 선택
const typedNameEl = _initDomUtils?.$("#typedName") || document.getElementById("typedName");
const cursorEl = _initDomUtils?.$("#cursor") || document.getElementById("cursor");
const welcomeEl = _initDomUtils?.$("#welcome") || document.getElementById("welcome");
const textAniSection = _initDomUtils?.$('.text-ani') || document.querySelector('.text-ani');
// 화면 리셋
if (typedNameEl) {
typedNameEl.textContent = "";
} else {
console.warn('[IntroInit] typedName 요소를 찾을 수 없습니다.');
}
if (cursorEl) {
if (_initDomUtils && _initDomUtils.setStyles) {
_initDomUtils.setStyles(cursorEl, { opacity: '1' });
} else {
cursorEl.style.opacity = "1";
}
} else {
console.warn('[IntroInit] cursor 요소를 찾을 수 없습니다.');
}
if (welcomeEl) {
welcomeEl.innerHTML = "";
} else {
console.warn('[IntroInit] welcome 요소를 찾을 수 없습니다.');
}
// text-ani 섹션 표시
if (textAniSection) {
if (_initDomUtils && _initDomUtils.addClasses) {
_initDomUtils.addClasses(textAniSection, 'animating');
} else {
textAniSection.classList.add('animating');
}
}
// 섹션 1로 이동
if (typeof goToSection === 'function') {
try {
goToSection(0);
} catch (error) {
_handleError(error, 'restartIntroWithName.goToSection');
}
} else {
console.warn('[IntroInit] goToSection 함수를 찾을 수 없습니다.');
}
// 타이핑 재시작
const animationDelay = 800;
const delayFn = _initUtils && _initUtils.delay ? _initUtils.delay : (ms) => new Promise(resolve => setTimeout(resolve, ms));
delayFn(animationDelay).then(() => {
try {
if (typeof typeName === 'function') {
typeName();
} else {
console.warn('[IntroInit] typeName 함수를 찾을 수 없습니다.');
}
} catch (error) {
_handleError(error, 'restartIntroWithName.typeName');
}
});
} catch (error) {
_handleError(error, 'restartIntroWithName');
}
}
+279
View File
@@ -0,0 +1,279 @@
/**
* intro-section.js
* 섹션 전환 및 스크롤 처리 로직
* 공통 모듈 활용 (ErrorHandler, DOMUtils, AnimationUtils, Utils)
*/
// 전역 의존성 (폴백 포함)
const _sectionDomUtils = typeof DOMUtils !== 'undefined' ? DOMUtils : null;
const _sectionErrorHandler = typeof ErrorHandler !== 'undefined' ? ErrorHandler : null;
const _sectionUtils = typeof Utils !== 'undefined' ? Utils : null;
const _sectionAnimationUtils = typeof AnimationUtils !== 'undefined' ? AnimationUtils : null;
/**
* 에러 처리 헬퍼
* @private
*/
function _handleError(error, context, additionalInfo = {}) {
if (_sectionErrorHandler) {
_sectionErrorHandler.handle(error, {
context: `IntroSection.${context}`,
component: 'IntroSection',
...additionalInfo
}, false);
} else {
console.error(`[IntroSection] ${context}:`, error, additionalInfo);
}
}
/**
* 특정 섹션으로 이동
* @param {number} index - 이동할 섹션 인덱스 (0, 1, 2)
*/
function goToSection(index) {
try {
// 입력 검증
if (typeof index !== 'number' || index < 0 || index > 2) {
_handleError(new Error(`유효하지 않은 섹션 인덱스: ${index}`), 'goToSection');
return;
}
if (typeof INTRO_STATE === 'undefined') {
_handleError(new Error('INTRO_STATE가 정의되지 않았습니다'), 'goToSection');
return;
}
if (INTRO_STATE.isAnimating) return;
INTRO_STATE.isAnimating = true;
// 자동 스크롤 타이머 정지
clearTimeout(INTRO_STATE.autoScrollTimer);
// DOM 요소 선택
const section1Text = _sectionDomUtils?.$("#section1Text") || document.getElementById("section1Text");
const section2Text = _sectionDomUtils?.$("#section2Text") || document.getElementById("section2Text");
const section3Text = _sectionDomUtils?.$("#section3Text") || document.getElementById("section3Text");
const cardsContainer = _sectionDomUtils?.$("#cardsContainer") || document.getElementById("cardsContainer");
const ctaBtn = _sectionDomUtils?.$("#ctaBtn") || document.getElementById("ctaBtn");
const scrollIndicator = _sectionDomUtils?.$("#scrollIndicator") || document.getElementById("scrollIndicator");
// 요소 존재 확인
if (!section1Text || !section2Text || !section3Text || !cardsContainer || !ctaBtn || !scrollIndicator) {
_handleError(new Error('필수 DOM 요소를 찾을 수 없습니다'), 'goToSection');
INTRO_STATE.isAnimating = false;
return;
}
// 현재 섹션 페이드 아웃
const currentSection = INTRO_STATE.currentSection;
if (currentSection === 0) {
if (_sectionDomUtils && _sectionDomUtils.addClasses) {
_sectionDomUtils.addClasses(section1Text, 'fade-out');
} else {
section1Text.classList.add("fade-out");
}
} else if (currentSection === 1) {
if (_sectionDomUtils && _sectionDomUtils.addClasses) {
_sectionDomUtils.addClasses(section2Text, 'fade-out');
} else {
section2Text.classList.add("fade-out");
}
// 카드 숨기기
["card1", "card2", "card3", "card4"].forEach((id) => {
const card = _sectionDomUtils?.$(`#${id}`) || document.getElementById(id);
if (card) {
if (_sectionDomUtils && _sectionDomUtils.removeClasses) {
_sectionDomUtils.removeClasses(card, 'show');
} else {
card.classList.remove("show");
}
}
});
} else if (currentSection === 2) {
if (_sectionDomUtils && _sectionDomUtils.addClasses) {
_sectionDomUtils.addClasses(section3Text, 'fade-out');
} else {
section3Text.classList.add("fade-out");
}
if (_sectionDomUtils && _sectionDomUtils.removeClasses) {
_sectionDomUtils.removeClasses(ctaBtn, 'show');
} else {
ctaBtn.classList.remove("show");
}
}
// 전환 애니메이션 지연
const transitionDelay = 400;
const delayFn = _sectionUtils && _sectionUtils.delay ? _sectionUtils.delay : (ms) => new Promise(resolve => setTimeout(resolve, ms));
delayFn(transitionDelay).then(() => {
try {
// 모든 섹션 숨기기
if (_sectionDomUtils && _sectionDomUtils.addClasses) {
_sectionDomUtils.addClasses(section1Text, 'hidden');
_sectionDomUtils.addClasses(section2Text, 'hidden');
_sectionDomUtils.addClasses(section3Text, 'hidden');
} else {
section1Text.classList.add("hidden");
section2Text.classList.add("hidden");
section3Text.classList.add("hidden");
}
if (_sectionDomUtils && _sectionDomUtils.removeClasses) {
_sectionDomUtils.removeClasses(scrollIndicator, 'sec2');
_sectionDomUtils.removeClasses(section1Text, 'fade-out');
_sectionDomUtils.removeClasses(section2Text, 'fade-out');
_sectionDomUtils.removeClasses(section3Text, 'fade-out');
} else {
scrollIndicator.classList.remove("sec2");
section1Text.classList.remove("fade-out");
section2Text.classList.remove("fade-out");
section3Text.classList.remove("fade-out");
}
// 목표 섹션 표시
if (index === 0) {
if (_sectionDomUtils && _sectionDomUtils.removeClasses) {
_sectionDomUtils.removeClasses(section1Text, 'hidden');
_sectionDomUtils.removeClasses(scrollIndicator, 'hidden');
} else {
section1Text.classList.remove("hidden");
scrollIndicator.classList.remove("hidden");
}
if (_sectionDomUtils && _sectionDomUtils.setStyles) {
_sectionDomUtils.setStyles(cardsContainer, { display: 'none' });
_sectionDomUtils.setStyles(ctaBtn, { display: 'none' });
} else {
cardsContainer.style.display = "none";
ctaBtn.style.display = "none";
}
} else if (index === 1) {
if (_sectionDomUtils && _sectionDomUtils.addClasses) {
_sectionDomUtils.addClasses(scrollIndicator, 'sec2');
_sectionDomUtils.removeClasses(section2Text, 'hidden');
_sectionDomUtils.removeClasses(scrollIndicator, 'hidden');
} else {
scrollIndicator.classList.add("sec2");
section2Text.classList.remove("hidden");
scrollIndicator.classList.remove("hidden");
}
// 모바일(≤992px): section2Text 먼저 보여준 뒤 animateSection2에서 카드 표시
const isMobile = window.innerWidth <= 992;
if (_sectionDomUtils && _sectionDomUtils.setStyles) {
_sectionDomUtils.setStyles(cardsContainer, { display: isMobile ? 'none' : 'flex' });
_sectionDomUtils.setStyles(ctaBtn, { display: 'none' });
} else {
cardsContainer.style.display = isMobile ? "none" : "flex";
ctaBtn.style.display = "none";
}
resetSection2();
setTimeout(animateSection2, 200);
} else if (index === 2) {
if (_sectionDomUtils && _sectionDomUtils.removeClasses) {
_sectionDomUtils.removeClasses(section3Text, 'hidden');
_sectionDomUtils.addClasses(scrollIndicator, 'hidden');
_sectionDomUtils.removeClasses(scrollIndicator, 'sec2');
} else {
section3Text.classList.remove("hidden");
scrollIndicator.classList.add("hidden");
scrollIndicator.classList.remove("sec2");
}
if (_sectionDomUtils && _sectionDomUtils.setStyles) {
_sectionDomUtils.setStyles(cardsContainer, { display: 'none' });
_sectionDomUtils.setStyles(ctaBtn, { display: 'block' });
} else {
cardsContainer.style.display = "none";
ctaBtn.style.display = "block";
}
resetSection3();
setTimeout(animateSection3, 200);
}
INTRO_STATE.currentSection = index;
// 애니메이션 완료 플래그 리셋
const resetDelay = 500;
delayFn(resetDelay).then(() => {
INTRO_STATE.isAnimating = false;
});
} catch (error) {
_handleError(error, 'goToSection.transition');
INTRO_STATE.isAnimating = false;
}
});
} catch (error) {
_handleError(error, 'goToSection');
if (typeof INTRO_STATE !== 'undefined') {
INTRO_STATE.isAnimating = false;
}
}
}
/**
* 다음 섹션으로 스크롤
*/
function handleScrollDown() {
try {
if (typeof INTRO_STATE === 'undefined') {
_handleError(new Error('INTRO_STATE가 정의되지 않았습니다'), 'handleScrollDown');
return;
}
if (INTRO_STATE.isScrolling || INTRO_STATE.isAnimating) return;
const delayFn = _sectionUtils && _sectionUtils.delay ? _sectionUtils.delay : (ms) => new Promise(resolve => setTimeout(resolve, ms));
const scrollCooldown = 700;
if (INTRO_STATE.currentSection === 0) {
INTRO_STATE.isScrolling = true;
delayFn(scrollCooldown).then(() => {
INTRO_STATE.isScrolling = false;
});
goToSection(1);
} else if (INTRO_STATE.currentSection === 1 && INTRO_STATE.section2AnimDone) {
INTRO_STATE.isScrolling = true;
delayFn(scrollCooldown).then(() => {
INTRO_STATE.isScrolling = false;
});
goToSection(2);
}
} catch (error) {
_handleError(error, 'handleScrollDown');
}
}
/**
* 이전 섹션으로 스크롤
*/
function handleScrollUp() {
try {
if (typeof INTRO_STATE === 'undefined') {
_handleError(new Error('INTRO_STATE가 정의되지 않았습니다'), 'handleScrollUp');
return;
}
if (INTRO_STATE.isScrolling || INTRO_STATE.isAnimating) return;
INTRO_STATE.isScrolling = true;
const delayFn = _sectionUtils && _sectionUtils.delay ? _sectionUtils.delay : (ms) => new Promise(resolve => setTimeout(resolve, ms));
const scrollCooldown = 700;
delayFn(scrollCooldown).then(() => {
INTRO_STATE.isScrolling = false;
});
if (INTRO_STATE.currentSection === 2) {
goToSection(1);
} else if (INTRO_STATE.currentSection === 1) {
goToSection(0);
}
} catch (error) {
_handleError(error, 'handleScrollUp');
}
}