From cb5cc68ecf264e8a44b007835d31d777a411e0dd Mon Sep 17 00:00:00 2001 From: zenn Date: Mon, 6 Jul 2026 11:51:09 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20=ED=8A=90=20=EC=99=84?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/router.options.js | 16 +++++++++ docs/deploy.md | 2 +- docs/history.md | 6 ++++ docs/map.md | 2 ++ docs/spec.md | 2 +- docs/update.md | 6 ++++ package-lock.json | 4 +-- package.json | 2 +- plugins/site-route-scroll.client.js | 54 +++++++++++++++++++++++++++++ 9 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 app/router.options.js create mode 100644 plugins/site-route-scroll.client.js diff --git a/app/router.options.js b/app/router.options.js new file mode 100644 index 0000000..b859f64 --- /dev/null +++ b/app/router.options.js @@ -0,0 +1,16 @@ +/** + * Vue Router 기본 스크롤 이동을 비활성화한다. + * + * 기본 동작은 클라이언트 페이지 이동 중 이전 화면이 먼저 최상단으로 + * 이동하는 순간을 만들 수 있어, 실제 스크롤 처리는 클라이언트 플러그인에서 + * 라우트 전환 이후 새 화면 기준으로만 수행한다. + */ +export default { + /** + * 라우터 내장 스크롤 처리를 끈다. + * @returns {false} 기본 스크롤 처리 비활성화 + */ + scrollBehavior() { + return false + } +} diff --git a/docs/deploy.md b/docs/deploy.md index d018f82..432e171 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,6 +1,6 @@ # 배포 가이드 -> 로컬 기준 v1.10.43에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. +> 로컬 기준 v1.10.44에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다. ## 빌드 유형 diff --git a/docs/history.md b/docs/history.md index 7b28f41..b79348a 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,11 @@ # 의사결정 이력 +## 2026-07-06 v1.10.44 — 스크롤은 라우트 전환 후 새 화면에서만 적용한다 + +게시물 상세를 즉시 전환 방식으로 바꿔도 Vue Router 기본 스크롤 처리가 남아 있으면, 새 화면이 그려지기 전에 이전 화면이 먼저 최상단으로 이동하는 순간이 보일 수 있다. 사용자는 이를 페이지가 넘어가기 전에 화면이 깜빡이는 현상으로 느끼므로, 라우터 기본 `scrollBehavior`를 끄고 클라이언트 플러그인에서 라우트 확정 후 렌더 프레임을 기다린 다음 새 화면 기준으로만 스크롤을 적용한다. + +이 방식은 기존 스켈레톤 전환 구조를 유지하면서도 스크롤 제어 책임을 분리한다. 해시 링크는 대상 요소가 존재하면 해당 위치로 이동하고, 일반 이동은 새 화면 최상단으로 정리한다. + ## 2026-07-06 v1.10.43 — 게시물 이동은 새 화면을 먼저 보여 주고 내부에서 로딩한다 전체 화면 오버레이는 이전 화면의 스크롤 튐을 숨기는 데는 효과적이지만, 문서형 사이트에서는 클릭 후 새 페이지가 바로 열린다는 감각이 더 중요하다. 따라서 게시물 상세의 클라이언트 내부 이동은 라우트를 먼저 전환하고, 아직 상세 데이터가 준비되지 않았을 때는 이전 글을 보여 주지 않고 새 상세 화면 안에서 제목·메타·이미지·본문 자리 스켈레톤을 표시한다. diff --git a/docs/map.md b/docs/map.md index 956b522..f48c955 100644 --- a/docs/map.md +++ b/docs/map.md @@ -11,6 +11,7 @@ | layouts/admin.vue | 관리자 전체, 320px 밝은 Ghost형 사이드바(대시보드 `/admin` 활성 링크·사이트 보기 새 창·콘텐츠 구간 여백), 우측 전체 높이 캔버스, 멤버 수 표시, 하단 사용자 드롭다운·설정, `내 프로필` 멤버 편집 이동, 게시글 `+` 새 글 진입, 글 작성/수정 화면의 전체 화면 편집 모드와 문서 스크롤 잠금, **`/admin/settings`에서는 사이드바 숨김·본문 전체 화면·설정용 문서 스크롤 잠금** | | layouts/page.vue | 고정 페이지 전체 화면 | | app.vue | 공개 사이트 설정 기반 파비콘 head 링크, 페이지 제목이 있으면 제목만 쓰고 없으면 사이트 이름을 쓰는 title template, 브랜드 컬러 `--site-accent` 적용, 페이지 이동 로딩 진행바(`NuxtLoadingIndicator`, 브랜드 컬러) | +| app/router.options.js | Vue Router 기본 스크롤 이동 비활성화, 클라이언트 플러그인이 새 화면 기준으로 스크롤 적용 | | error.vue | 공개 404/서버 오류 화면, 홈 이동 버튼 | ## Composables @@ -362,6 +363,7 @@ | plugins/site-analytics.client.js | 공개 라우트 pageview·heartbeat·read 클라이언트 전송 | | plugins/bfcache-page-restore.client.js | 뒤로가기 캐시(bfcache) 복원 시 라우터 경로와 실제 URL 어긋남을 감지해 새로고침(HTML 문서 모드 페이지 이동 후 뒤로가기 잔상 방지) | | plugins/navigation-lock.client.js | 페이지 이동 중 `html.site-navigating` 클래스로 링크·버튼 클릭을 잠가 중복 클릭 차단(안전 타임아웃 포함) | +| plugins/site-route-scroll.client.js | 라우트 전환 완료 후 새 화면에서 최상단·해시 위치 스크롤 적용(이동 전 이전 화면 스크롤 튐 방지) | ## 데이터베이스 diff --git a/docs/spec.md b/docs/spec.md index 4eb5cb9..ad3c013 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -144,7 +144,7 @@ ### 공개 URL 구조 - `/posts` - 게시물 전체 목록. 사이트 설정 POST 설정의 `postListPageSize` 값 기준으로 서버 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다. 하단 페이지네이션은 이전·다음 화살표와 페이지 번호 직접 이동을 함께 제공하며, 전체 페이지가 많으면 첫 페이지·끝 페이지·현재 페이지 주변 번호를 유지하고 중간 구간은 `...`으로 접는다. -- `/post/:slug` - 개별 게시물 상세. 직접 접속·SSR은 본문을 먼저 조회해 SEO 메타를 완성하고, 클라이언트 내부 이동은 새 상세 화면으로 즉시 전환한 뒤 데이터 준비 전에는 제목·메타·이미지·본문 자리 스켈레톤을 표시한다. 이동 중에는 `NuxtLoadingIndicator` 상단 진행바와 중복 클릭 잠금으로 피드백을 제공한다. 이전·다음 글은 상세 API 응답의 `previous`·`next`로 표시하며, `previous`는 더 과거 글, `next`는 더 최신 글을 뜻한다. +- `/post/:slug` - 개별 게시물 상세. 직접 접속·SSR은 본문을 먼저 조회해 SEO 메타를 완성하고, 클라이언트 내부 이동은 새 상세 화면으로 즉시 전환한 뒤 데이터 준비 전에는 제목·메타·이미지·본문 자리 스켈레톤을 표시한다. 이동 중에는 `NuxtLoadingIndicator` 상단 진행바와 중복 클릭 잠금으로 피드백을 제공한다. Vue Router 기본 스크롤 처리는 끄고 라우트 전환 후 새 화면 기준으로 최상단 또는 해시 위치를 적용해 이동 전 이전 화면이 먼저 위로 튀지 않게 한다. 이전·다음 글은 상세 API 응답의 `previous`·`next`로 표시하며, `previous`는 더 과거 글, `next`는 더 최신 글을 뜻한다. - `/` - 홈. 클라이언트 복귀 시에도 주요 데이터를 블로킹 조회한 뒤 전환한다. - `/sitemap.xml` - 공개 sitemap XML. 홈, 게시물 목록, 태그 목록, 공개 발행글, 공개 고정 페이지, 글이 있는 태그 URL을 요청 시점 DB 기준으로 자동 생성한다. 멤버십·비공개·초안·예약 대기 글과 `noindex` 글은 포함하지 않는다. - `/robots.txt` - 크롤러 안내 파일. 관리자·API·회원 설정·로그인 경로를 제외하고 `Sitemap: /sitemap.xml` 절대 URL을 제공한다. diff --git a/docs/update.md b/docs/update.md index e7caba3..8f5022b 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 이력 +## v1.10.44 + +- Vue Router 기본 스크롤 이동을 비활성화해 페이지 이동 전 이전 화면이 먼저 최상단으로 튀는 현상 완화. +- 클라이언트 라우트 전환 완료 후 새 화면 기준으로 최상단 또는 해시 위치 스크롤을 적용하는 플러그인 추가. +- 게시물 상세 즉시 전환·스켈레톤 방식은 유지하면서 화면 깜빡임 원인인 스크롤 적용 타이밍을 분리. + ## v1.10.43 - 사용자 화면 게시물 상세 클라이언트 이동을 즉시 전환 방식으로 변경. diff --git a/package-lock.json b/package-lock.json index a91387e..592f82f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.10.43", + "version": "1.10.44", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.10.43", + "version": "1.10.44", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 119c032..e298bf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.10.43", + "version": "1.10.44", "private": true, "type": "module", "imports": { diff --git a/plugins/site-route-scroll.client.js b/plugins/site-route-scroll.client.js new file mode 100644 index 0000000..ecc94af --- /dev/null +++ b/plugins/site-route-scroll.client.js @@ -0,0 +1,54 @@ +/** + * 공개 라우트 이동 후 새 화면 기준으로 스크롤 위치를 정리한다. + * + * Vue Router 기본 scrollBehavior는 전환 중 이전 화면을 먼저 움직일 수 있어 + * `app/router.options.js`에서 끄고, 여기서는 라우트가 확정된 뒤 프레임을 + * 기다렸다가 새 화면에서만 최상단 또는 해시 위치로 이동한다. + */ +export default defineNuxtPlugin((nuxtApp) => { + const router = nuxtApp.$router + + /** + * 브라우저 렌더 프레임을 기다린다. + * @returns {Promise} 프레임 대기 Promise + */ + const waitForFrame = () => new Promise((resolve) => { + requestAnimationFrame(() => { + requestAnimationFrame(resolve) + }) + }) + + /** + * 이동 완료 후 스크롤 위치를 적용한다. + * @param {import('vue-router').RouteLocationNormalizedLoaded} to - 이동 대상 라우트 + * @param {import('vue-router').RouteLocationNormalizedLoaded} from - 이전 라우트 + * @returns {Promise} + */ + const applyRouteScroll = async (to, from) => { + if (to.fullPath === from.fullPath) { + return + } + + await waitForFrame() + + if (to.hash) { + const targetId = decodeURIComponent(to.hash.slice(1)) + const target = document.getElementById(targetId) + + if (target) { + target.scrollIntoView({ block: 'start' }) + return + } + } + + window.scrollTo({ + top: 0, + left: 0, + behavior: 'auto' + }) + } + + router.afterEach((to, from) => { + applyRouteScroll(to, from) + }) +})