Files
sori.studio/plugins/site-page-transition.client.js
zenn 4c8f6e44f6 페이지 전환 깜빡임 완화 및 레이아웃 통합 (v1.10.50)
- 게시물 상세를 default 레이아웃으로 통합해 홈↔게시물 레이아웃 교체 제거
- CSS 전환 베일·스크롤 정리 플러그인으로 이동 중 깜빡임 완화

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 14:28:24 +09:00

158 lines
3.7 KiB
JavaScript

import {
consumePendingScrollRestore,
scrollDocumentTo,
scrollDocumentToTop,
shouldUseSiteRouteVeil
} from '../lib/site-route-transition.js'
const SITE_ROUTE_VEIL_CLASS = 'site-page-veil'
/**
* 공개 페이지 이동 시 전환 베일을 표시하고, 베일 뒤에서 스크롤을 정리한 뒤 새 화면을 드러낸다.
*/
export default defineNuxtPlugin((nuxtApp) => {
const router = useRouter()
const isSitePageVeiled = useState('site-page-veil', () => false)
/** @type {boolean} */
let activeVeilNavigation = false
/**
* 전환 베일을 즉시 표시한다.
* @returns {void}
*/
const showRouteVeil = () => {
if (document.documentElement.classList.contains(SITE_ROUTE_VEIL_CLASS)) {
return
}
activeVeilNavigation = true
isSitePageVeiled.value = true
document.documentElement.classList.add(SITE_ROUTE_VEIL_CLASS)
}
/**
* 전환 베일을 즉시 숨긴다.
* @returns {void}
*/
const hideRouteVeil = () => {
activeVeilNavigation = false
isSitePageVeiled.value = false
document.documentElement.classList.remove(SITE_ROUTE_VEIL_CLASS)
}
/**
* 내부 링크 클릭이 라우터보다 먼저 베일을 덮도록 한다.
* @param {MouseEvent} event - 클릭 이벤트
* @returns {void}
*/
const handleInternalLinkCapture = (event) => {
if (event.defaultPrevented || event.button !== 0) {
return
}
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
return
}
const target = event.target
if (!(target instanceof Element)) {
return
}
const anchor = target.closest('a')
if (!anchor || anchor.target === '_blank' || anchor.hasAttribute('download')) {
return
}
const href = anchor.getAttribute('href')
if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) {
return
}
let url = null
try {
url = new URL(anchor.href, window.location.origin)
} catch {
return
}
if (url.origin !== window.location.origin) {
return
}
const currentPath = router.currentRoute.value.path
if (!shouldUseSiteRouteVeil(url.pathname) && !shouldUseSiteRouteVeil(currentPath)) {
return
}
if (url.pathname === currentPath && !url.hash) {
return
}
showRouteVeil()
}
/**
* 페이지 렌더 완료 후 스크롤을 맞추고 베일을 해제한다.
* @returns {Promise<void>}
*/
const finishRouteTransition = async () => {
if (!activeVeilNavigation) {
return
}
const route = router.currentRoute.value
const restore = consumePendingScrollRestore()
if (restore) {
scrollDocumentTo(restore)
} else if (!route.hash) {
scrollDocumentToTop()
}
await new Promise((resolve) => requestAnimationFrame(resolve))
await new Promise((resolve) => requestAnimationFrame(resolve))
if (restore) {
scrollDocumentTo(restore)
} else if (!route.hash) {
scrollDocumentToTop()
}
await new Promise((resolve) => setTimeout(resolve, 48))
hideRouteVeil()
}
if (import.meta.client && 'scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual'
}
document.addEventListener('click', handleInternalLinkCapture, true)
router.beforeEach((to, from) => {
if (to.fullPath === from.fullPath) {
return
}
if (!shouldUseSiteRouteVeil(to.path) && !shouldUseSiteRouteVeil(from.path)) {
return
}
showRouteVeil()
})
router.onError(() => {
hideRouteVeil()
})
nuxtApp.hook('page:finish', () => {
finishRouteTransition()
})
nuxtApp.hook('app:error', () => {
hideRouteVeil()
})
})