// v-reveal 디렉티브 — 요소가 뷰포트에 들어오면 등장 애니메이션 트리거 // 사용법: // v-reveal → 기본(아래→위 fade-up) // v-reveal:scale → 확대 등장 // v-reveal:fade → 투명도만 // v-reveal="{ delay: 120 }" → 등장 지연(ms) — 스태거용 // v-reveal:up="{ delay: i*90 }" → 타입 + 지연 조합 // // 디렉티브는 서버/클라이언트 양쪽에 등록(미등록 시 SSR 렌더 크래시)하되, // IntersectionObserver 등 실제 동작은 클라이언트에서만 수행한다. // getSSRProps 를 제공해 SSR 단계에서 안전하게 통과시킨다. type RevealType = 'up' | 'scale' | 'fade' interface RevealValue { type?: RevealType delay?: number } export default defineNuxtPlugin((nuxtApp) => { let observer: IntersectionObserver | null = null let reduceMotion = false if (import.meta.client) { reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { entry.target.classList.add('is-revealed') observer?.unobserve(entry.target) } } }, // 요소가 화면 하단에서 약간 올라온 시점에 트리거 { threshold: 0.15, rootMargin: '0px 0px -8% 0px' }, ) } nuxtApp.vueApp.directive('reveal', { // SSR: 추가 속성 없이 통과 (디렉티브 미등록으로 인한 크래시 방지) getSSRProps: () => ({}), mounted(el, binding) { // 서버에서는 호출되지 않음. 모션 최소화 선호 시 애니메이션 없이 그대로 노출. if (reduceMotion || !observer) return const value = binding.value const type: RevealType = (binding.arg as RevealType) || (typeof value === 'object' && value?.type) || 'up' const delay = typeof value === 'number' ? value : value?.delay ?? 0 el.classList.add('reveal', `reveal-${type}`) if (delay) el.style.setProperty('--reveal-delay', `${delay}ms`) observer.observe(el) }, unmounted(el) { observer?.unobserve(el) }, }) })