게시물 목록 페이지 전환 로딩 수정

This commit is contained in:
2026-07-24 16:08:09 +09:00
parent b38eb234d8
commit 2d40fc0574
8 changed files with 66 additions and 10 deletions

View File

@@ -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 | 관리자 사이트 설정 백업 현황용 업로드 파일 스캔·원본/자동 생성 이미지/프로필/내보내기 파일 용량 집계 |

View File

@@ -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` 글은 포함하지 않는다.

View File

@@ -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 옵션 충돌 경고를 제거.

View File

@@ -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}

4
package-lock.json generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "1.11.57",
"version": "1.11.58",
"private": true,
"type": "module",
"imports": {

View File

@@ -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
}

View File

@@ -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)
})