From 2d40fc05749ca78863262e75a3ad23d2e62c4626 Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 24 Jul 2026 16:08:09 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=AC=BC=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/map.md | 3 ++- docs/spec.md | 2 +- docs/update.md | 6 +++++ lib/site-route-transition.js | 19 +++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- plugins/site-page-transition.client.js | 7 ++---- tests/site-route-transition.test.js | 33 ++++++++++++++++++++++++++ 8 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 tests/site-route-transition.test.js diff --git a/docs/map.md b/docs/map.md index 8da5604..5cc02d3 100644 --- a/docs/map.md +++ b/docs/map.md @@ -10,7 +10,7 @@ | layouts/post.vue | 개별 게시물 — `default`와 동일, 오른쪽 컬럼 전체 높이 구분선 | | layouts/admin.vue | 관리자 전체, 320px 밝은 Ghost형 사이드바(대시보드 `/admin` 활성 링크·사이트 보기 새 창·콘텐츠 구간 여백), 우측 전체 높이 캔버스, 멤버 수 표시, 하단 사용자 드롭다운·설정, `내 프로필` 멤버 편집 이동, 게시글 `+` 새 글 진입, 글 작성/수정 화면의 전체 화면 편집 모드와 문서 스크롤 잠금, **`/admin/settings`에서는 사이드바 숨김·본문 전체 화면·설정용 문서 스크롤 잠금** | | layouts/page.vue | 고정 페이지 전체 화면 | -| app.vue | 공개 사이트 설정 기반 파비콘 head 링크, 페이지 제목이 있으면 제목만 쓰고 없으면 사이트 이름을 쓰는 title template, 브랜드 컬러 `--site-accent` 적용, 페이지 이동 로딩 진행바(`NuxtLoadingIndicator`, 브랜드 컬러)와 새 화면 렌더 후 해제되는 불투명 전역 이동 로딩 오버레이 | +| app.vue | 공개 사이트 설정 기반 파비콘 head 링크, 페이지 제목이 있으면 제목만 쓰고 없으면 사이트 이름을 쓰는 title template, 브랜드 컬러 `--site-accent` 적용, 새 화면 렌더 후 해제되는 불투명 전역 이동 로딩 오버레이 | | error.vue | 공개 404/서버 오류 화면, 홈 이동 버튼 | ## Composables @@ -66,6 +66,7 @@ | lib/analytics-traffic.js | referrer·User-Agent 기반 유입원·디바이스·검색 키워드 축약 분류 | | lib/admin-pagination.js | 관리자 번호형 페이지네이션 페이지 보정·ellipsis 항목 계산 | | lib/csp-report.js | CSP 보고 Content-Type·32KB/20건 제한 상수, payload 검증, 문서 경로·차단 origin·브라우저 계열 최소화 | +| lib/site-route-transition.js | 공개 라우트 전환 베일 사용 여부와 query/hash 이동 제외, 스크롤 복원·최상단 이동 helper | | lib/media-catalog.js | 업로드 URL·저장 키 정규화, 미디어 종류·원본/파생 역할 판별, 콘텐츠 미디어 참조 추출, PostgreSQL LIKE 리터럴 이스케이프 | | lib/social-links.js | 사이트 설정 SNS 링크 아이콘 프리셋·사용자 SVG 정리·URL 자동 보정·레거시 JSON 문자열 복구·공개 노출 목록 생성 | | server/utils/backup-storage-summary.js | 관리자 사이트 설정 백업 현황용 업로드 파일 스캔·원본/자동 생성 이미지/프로필/내보내기 파일 용량 집계 | diff --git a/docs/spec.md b/docs/spec.md index 7eb723f..1e42d4f 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -174,7 +174,7 @@ ### 공개 URL 구조 -- `/posts` - 게시물 전체 목록. 사이트 설정 POST 설정의 `postListPageSize` 값 기준으로 서버 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다. 하단 페이지네이션은 이전·다음 화살표와 페이지 번호 직접 이동을 함께 제공하며, 전체 페이지가 많으면 첫 페이지·끝 페이지·현재 페이지 주변 번호를 유지하고 중간 구간은 `...`으로 접는다. +- `/posts` - 게시물 전체 목록. 사이트 설정 POST 설정의 `postListPageSize` 값 기준으로 서버 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다. 하단 페이지네이션은 이전·다음 화살표와 페이지 번호 직접 이동을 함께 제공하며, 전체 페이지가 많으면 첫 페이지·끝 페이지·현재 페이지 주변 번호를 유지하고 중간 구간은 `...`으로 접는다. 같은 path에서 `page` query만 바뀌는 이동은 Nuxt 페이지 컴포넌트가 재사용될 수 있으므로 전역 전환 베일을 켜지 않는다. - `/post/:slug` - 개별 게시물 상세. 직접 접속·SSR과 클라이언트 내부 이동 모두 본문을 먼저 조회한 뒤 완성된 상세 화면으로 진입한다. 이동 시작 시 불투명 전환 베일을 즉시 표시하고, `page:finish` 이후 베일 뒤에서 스크롤을 최상단으로 맞춘 뒤 베일을 해제한다. 뒤로가기는 저장 스크롤 위치를 베일 뒤에서 복원한다. `history.scrollRestoration`은 `manual`로 두어 브라우저 자동 복원과 충돌하지 않게 한다. 이전·다음 글은 상세 API 응답의 `previous`·`next`로 표시하며, `previous`는 더 과거 글, `next`는 더 최신 글을 뜻한다. - `/` - 홈. 클라이언트 복귀 시에도 주요 데이터를 블로킹 조회한 뒤 전환한다. - `/sitemap.xml` - 공개 sitemap XML. 홈, 게시물 목록, 태그 목록, 공개 발행글, 공개 고정 페이지, 글이 있는 태그 URL을 요청 시점 DB 기준으로 자동 생성한다. 멤버십·비공개·초안·예약 대기 글과 `noindex` 글은 포함하지 않는다. diff --git a/docs/update.md b/docs/update.md index fae4c5b..6463047 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 이력 +## v1.11.58 + +- 공개 전환 베일 시작 조건을 정리해 `/posts?page=2`에서 `/posts?page=3`처럼 같은 path의 query만 바뀌는 페이지네이션 이동에서는 베일을 켜지 않도록 수정. +- 사이트 라우트 전환 베일 판별 helper와 단위 테스트를 추가. +- 게시물 목록 페이지네이션 클라이언트 이동 무한 로딩 회귀를 방지. + ## v1.11.57 - 공개 사이트 설정 조회를 `usePublicSiteSettings` composable로 통합해 Nuxt `site-settings-public` async data 옵션 충돌 경고를 제거. diff --git a/lib/site-route-transition.js b/lib/site-route-transition.js index c766fbf..0590b9f 100644 --- a/lib/site-route-transition.js +++ b/lib/site-route-transition.js @@ -33,6 +33,25 @@ export const shouldUseSiteRouteVeil = (path) => { return !clean.startsWith('/admin') } +/** + * 라우터 이동에서 공개 전환 베일을 시작할지 판별한다. + * 같은 페이지 컴포넌트의 query/hash 이동은 Nuxt page finish가 발생하지 않을 수 있어 베일을 쓰지 않는다. + * @param {{ fullPath?: string, path?: string }} to - 이동 대상 라우트 + * @param {{ fullPath?: string, path?: string }} from - 이전 라우트 + * @returns {boolean} 전환 베일 시작 여부 + */ +export const shouldStartSiteRouteVeilNavigation = (to, from) => { + if (to?.fullPath === from?.fullPath) { + return false + } + + if (to?.path === from?.path) { + return false + } + + return shouldUseSiteRouteVeil(to?.path) || shouldUseSiteRouteVeil(from?.path) +} + /** * 문서 스크롤을 최상단으로 이동한다. * @returns {void} diff --git a/package-lock.json b/package-lock.json index 45e2231..2cc2f8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.57", + "version": "1.11.58", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.57", + "version": "1.11.58", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 62452e1..3561e67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.57", + "version": "1.11.58", "private": true, "type": "module", "imports": { diff --git a/plugins/site-page-transition.client.js b/plugins/site-page-transition.client.js index 814361d..b9ad5c3 100644 --- a/plugins/site-page-transition.client.js +++ b/plugins/site-page-transition.client.js @@ -2,6 +2,7 @@ import { consumePendingScrollRestore, scrollDocumentTo, scrollDocumentToTop, + shouldStartSiteRouteVeilNavigation, shouldUseSiteRouteVeil } from '../lib/site-route-transition.js' @@ -132,11 +133,7 @@ export default defineNuxtPlugin((nuxtApp) => { document.addEventListener('click', handleInternalLinkCapture, true) router.beforeEach((to, from) => { - if (to.fullPath === from.fullPath) { - return - } - - if (!shouldUseSiteRouteVeil(to.path) && !shouldUseSiteRouteVeil(from.path)) { + if (!shouldStartSiteRouteVeilNavigation(to, from)) { return } diff --git a/tests/site-route-transition.test.js b/tests/site-route-transition.test.js new file mode 100644 index 0000000..dda255a --- /dev/null +++ b/tests/site-route-transition.test.js @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { + shouldStartSiteRouteVeilNavigation, + shouldUseSiteRouteVeil +} from '../lib/site-route-transition.js' + +test('공개 라우트는 전환 베일 대상이고 관리자 라우트는 제외한다', () => { + assert.equal(shouldUseSiteRouteVeil('/posts'), true) + assert.equal(shouldUseSiteRouteVeil('/post/sample'), true) + assert.equal(shouldUseSiteRouteVeil('/admin/posts'), false) +}) + +test('같은 공개 path의 query 이동은 전환 베일을 시작하지 않는다', () => { + assert.equal(shouldStartSiteRouteVeilNavigation( + { path: '/posts', fullPath: '/posts?page=3' }, + { path: '/posts', fullPath: '/posts?page=2' } + ), false) +}) + +test('서로 다른 공개 path 이동은 전환 베일을 시작한다', () => { + assert.equal(shouldStartSiteRouteVeilNavigation( + { path: '/post/sample', fullPath: '/post/sample' }, + { path: '/posts', fullPath: '/posts?page=2' } + ), true) +}) + +test('같은 라우트 이동은 전환 베일을 시작하지 않는다', () => { + assert.equal(shouldStartSiteRouteVeilNavigation( + { path: '/posts', fullPath: '/posts?page=2' }, + { path: '/posts', fullPath: '/posts?page=2' } + ), false) +})