import { animate, stagger } from 'motion-v' import type { DOMKeyframesDefinition, AnimationOptions } from 'motion-v' export default defineNuxtPlugin(nuxtApp => { // 전역 상태 관리 const animatedItems = new Set() const sectionObservers = new Map() const sectionItems = new Map>() // 섹션의 motion-item들을 애니메이션 const animateSectionItems = (section: Element) => { const items = sectionItems.get(section) if (!items || items.size === 0) return const newItems = Array.from(items).filter(item => !animatedItems.has(item)) if (newItems.length === 0) return // 애니메이션 실행 newItems.forEach(item => animatedItems.add(item)) animate( newItems, { opacity: 1, y: 0 } as DOMKeyframesDefinition, { delay: stagger(0.2), duration: 0.5, easing: [0.22, 1, 0.36, 1], } as AnimationOptions ) } // 섹션에 IntersectionObserver 등록 const observeSection = (section: Element) => { if (sectionObservers.has(section)) return const observer = new IntersectionObserver( entries => { entries.forEach(entry => { if (entry.isIntersecting) { animateSectionItems(section) } }) }, { threshold: 0.2 } ) observer.observe(section) sectionObservers.set(section, observer) } // 섹션이 viewport에 있는지 확인 const isSectionInViewport = (section: Element): boolean => { const rect = section.getBoundingClientRect() const windowHeight = window.innerHeight || document.documentElement.clientHeight // threshold 0.2를 고려한 체크 const visibleHeight = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0) const sectionHeight = rect.height return visibleHeight / sectionHeight >= 0.2 } // v-motion-stagger 디렉티브 nuxtApp.vueApp.directive('motion-stagger', { mounted(el: HTMLElement) { // 초기 스타일 설정 (애니메이션 전) el.style.opacity = '0' el.style.transform = 'translateY(30px)' // 디버깅: 요소 등록 확인 if (import.meta.dev) { // eslint-disable-next-line no-console console.log('[motion-stagger] Element mounted:', el.tagName) } // 가장 가까운 section 찾기 const section = el.closest('section') if (!section) { if (import.meta.dev) { // eslint-disable-next-line no-console console.warn('[motion-stagger] No section found for element:', el) } return } // 섹션의 아이템 목록에 추가 if (!sectionItems.has(section)) { sectionItems.set(section, new Set()) observeSection(section) if (import.meta.dev) { // eslint-disable-next-line no-console console.log('[motion-stagger] New section registered') } } sectionItems.get(section)!.add(el) // 이미 viewport에 있는 경우 즉시 체크 (여러 번 체크) const inViewport = isSectionInViewport(section) if (import.meta.dev) { // eslint-disable-next-line no-console console.log('[motion-stagger] Section in viewport:', inViewport) } if (inViewport) { // 즉시 실행 requestAnimationFrame(() => { animateSectionItems(section) }) // 추가로 100ms 후 재확인 (비동기 렌더링 대응) setTimeout(() => { if (isSectionInViewport(section)) { animateSectionItems(section) } }, 100) // 추가로 300ms 후 재확인 (느린 비동기 데이터 대응) setTimeout(() => { if (isSectionInViewport(section)) { animateSectionItems(section) } }, 300) } }, unmounted(el: Element) { const section = el.closest('section') if (!section) return // 아이템 제거 const items = sectionItems.get(section) if (items) { items.delete(el) // 섹션에 아이템이 없으면 observer 정리 if (items.size === 0) { const observer = sectionObservers.get(section) if (observer) { observer.disconnect() sectionObservers.delete(section) } sectionItems.delete(section) } } animatedItems.delete(el) }, }) // 페이지 이동 시 정리 if (import.meta.client) { nuxtApp.hook('page:finish', () => { // 현재 페이지에 없는 섹션들 정리 const currentSections = new Set( Array.from(document.querySelectorAll('section')) ) sectionObservers.forEach((observer, section) => { if (!currentSections.has(section)) { observer.disconnect() sectionObservers.delete(section) sectionItems.delete(section) } }) }) } })