설정 내비게이션 상태 분리
This commit is contained in:
164
composables/useAdminSettingsNavigation.js
Normal file
164
composables/useAdminSettingsNavigation.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* 관리자 설정 화면 좌측 내비 구역 정의
|
||||||
|
* @type {ReadonlyArray<{ heading: string, items: ReadonlyArray<{ id: string, label: string, keywords: string, iconId?: string }> }>}
|
||||||
|
*/
|
||||||
|
const ADMIN_SETTINGS_NAV_GROUPS = [
|
||||||
|
{
|
||||||
|
heading: '일반',
|
||||||
|
items: [
|
||||||
|
{ id: 'admin-settings-section-title', label: '블로그 제목·설명', keywords: 'title description site name', iconId: 'title-desc' },
|
||||||
|
{ id: 'admin-settings-section-misc', label: '사이트 정보', keywords: 'logo url copyright favicon site info', iconId: 'site-info' },
|
||||||
|
{ id: 'admin-settings-section-social', label: 'SNS 정보', keywords: 'social follow sns x github instagram youtube rss', iconId: 'social' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: '게시물',
|
||||||
|
items: [
|
||||||
|
{ id: 'admin-settings-section-post', label: 'POST 설정', keywords: 'post updated date display 수정일', iconId: 'post-settings' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: '사이트',
|
||||||
|
items: [
|
||||||
|
{ id: 'admin-settings-section-home-cover', label: '메인 화면', keywords: 'home cover hero banner image', iconId: 'home-cover' },
|
||||||
|
{ id: 'admin-settings-section-brand', label: '브랜드', keywords: 'brand design accent color point 포인트 컬러', iconId: 'brand' },
|
||||||
|
{ id: 'admin-settings-section-announcement', label: '어나운스 바', keywords: 'announcement banner notice', iconId: 'announcement' },
|
||||||
|
{ id: 'admin-settings-section-site-code', label: '사이트 코드', keywords: 'ads ads.txt head footer script code adsense', iconId: 'site-code' },
|
||||||
|
{ id: 'admin-settings-section-ads', label: 'Ads', keywords: 'ads ad slot advertisement adsense 광고 애드센스', iconId: 'ads' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
heading: '콘텐츠·안전',
|
||||||
|
items: [
|
||||||
|
{ id: 'admin-settings-section-backup-summary', label: '사이트 용량', keywords: 'storage media count volume 용량 통계 사이트', iconId: 'post-export' },
|
||||||
|
{ id: 'admin-settings-section-export', label: '게시물 내보내기', keywords: 'export backup 내보내기', iconId: 'post-export' },
|
||||||
|
{ id: 'admin-settings-section-import', label: '게시물 가져오기', keywords: 'import restore 가져오기', iconId: 'post-import' },
|
||||||
|
{ id: 'admin-settings-section-spam', label: '스팸 필터', keywords: 'spam moderation comments', iconId: 'spam' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자 설정 내비게이션 검색과 스크롤 스파이 상태를 만든다.
|
||||||
|
* @returns {{ mainScrollRef: import('vue').Ref<HTMLElement|null>, settingsNavGroups: typeof ADMIN_SETTINGS_NAV_GROUPS, filteredSettingsNavGroups: import('vue').ComputedRef<typeof ADMIN_SETTINGS_NAV_GROUPS>, navSearchInput: import('vue').Ref<string>, onNavSearchInput: (event: Event) => void, onNavSearchCompositionEnd: (event: Event) => void, activeSectionId: import('vue').Ref<string>, onMainScroll: () => void, refreshActiveSection: () => void, scrollToSection: (sectionId: string) => void, cleanupSettingsNavigation: () => void }}
|
||||||
|
*/
|
||||||
|
export const useAdminSettingsNavigation = () => {
|
||||||
|
const mainScrollRef = ref(null)
|
||||||
|
const navSearchQuery = ref('')
|
||||||
|
const {
|
||||||
|
displayValue: navSearchInput,
|
||||||
|
onInput: onNavSearchInput,
|
||||||
|
onCompositionEnd: onNavSearchCompositionEnd
|
||||||
|
} = useImeSearchInput(navSearchQuery)
|
||||||
|
const activeSectionId = ref(ADMIN_SETTINGS_NAV_GROUPS[0].items[0].id)
|
||||||
|
const scrollSpySuspended = ref(false)
|
||||||
|
let scrollSpyFrame = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색어에 맞춰 내비 그룹을 필터링한다.
|
||||||
|
* @returns {typeof ADMIN_SETTINGS_NAV_GROUPS} 표시할 그룹 목록
|
||||||
|
*/
|
||||||
|
const filteredSettingsNavGroups = computed(() => {
|
||||||
|
const q = navSearchQuery.value.trim().toLowerCase()
|
||||||
|
if (!q) {
|
||||||
|
return ADMIN_SETTINGS_NAV_GROUPS
|
||||||
|
}
|
||||||
|
|
||||||
|
return ADMIN_SETTINGS_NAV_GROUPS
|
||||||
|
.map((group) => {
|
||||||
|
const items = group.items.filter((item) => {
|
||||||
|
const hay = `${item.label} ${item.keywords}`.toLowerCase()
|
||||||
|
return hay.includes(q)
|
||||||
|
})
|
||||||
|
return { ...group, items }
|
||||||
|
})
|
||||||
|
.filter((group) => group.items.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 본문 스크롤 위치에 맞춰 활성 섹션 id를 갱신한다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const updateActiveSectionFromScroll = () => {
|
||||||
|
const root = mainScrollRef.value
|
||||||
|
if (!root || scrollSpySuspended.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const marker = root.getBoundingClientRect().top + 56
|
||||||
|
let nextId = ADMIN_SETTINGS_NAV_GROUPS[0].items[0].id
|
||||||
|
const flatIds = ADMIN_SETTINGS_NAV_GROUPS.flatMap((group) => group.items.map((item) => item.id))
|
||||||
|
|
||||||
|
for (const id of flatIds) {
|
||||||
|
const el = document.getElementById(id)
|
||||||
|
if (!el) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const top = el.getBoundingClientRect().top
|
||||||
|
if (top <= marker) {
|
||||||
|
nextId = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activeSectionId.value = nextId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 스크롤 스파이 핸들러를 실행한다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const onMainScroll = () => {
|
||||||
|
if (scrollSpyFrame) {
|
||||||
|
cancelAnimationFrame(scrollSpyFrame)
|
||||||
|
}
|
||||||
|
scrollSpyFrame = requestAnimationFrame(() => {
|
||||||
|
scrollSpyFrame = null
|
||||||
|
updateActiveSectionFromScroll()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 해당 섹션으로 부드럽게 스크롤한다.
|
||||||
|
* @param {string} sectionId - 섹션 요소 id
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const scrollToSection = (sectionId) => {
|
||||||
|
const el = document.getElementById(sectionId)
|
||||||
|
if (!el) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollSpySuspended.value = true
|
||||||
|
activeSectionId.value = sectionId
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
|
window.setTimeout(() => {
|
||||||
|
scrollSpySuspended.value = false
|
||||||
|
updateActiveSectionFromScroll()
|
||||||
|
}, 520)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 예약된 스크롤 스파이 프레임을 정리한다.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
const cleanupSettingsNavigation = () => {
|
||||||
|
if (scrollSpyFrame) {
|
||||||
|
cancelAnimationFrame(scrollSpyFrame)
|
||||||
|
scrollSpyFrame = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mainScrollRef,
|
||||||
|
settingsNavGroups: ADMIN_SETTINGS_NAV_GROUPS,
|
||||||
|
filteredSettingsNavGroups,
|
||||||
|
navSearchInput,
|
||||||
|
onNavSearchInput,
|
||||||
|
onNavSearchCompositionEnd,
|
||||||
|
activeSectionId,
|
||||||
|
onMainScroll,
|
||||||
|
refreshActiveSection: updateActiveSectionFromScroll,
|
||||||
|
scrollToSection,
|
||||||
|
cleanupSettingsNavigation
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
| composables/createPostSummary.js | 게시물 요약·본문에서 목록·SEO용 짧은 설명 텍스트 생성, 본문 대체 요약의 표시용 마크다운·HTML 토큰 제거 |
|
| composables/createPostSummary.js | 게시물 요약·본문에서 목록·SEO용 짧은 설명 텍스트 생성, 본문 대체 요약의 표시용 마크다운·HTML 토큰 제거 |
|
||||||
| composables/useAdminToast.js | 관리자 우측 상단 토스트(성공·오류·안내; 모달보다 위 z-index) |
|
| composables/useAdminToast.js | 관리자 우측 상단 토스트(성공·오류·안내; 모달보다 위 z-index) |
|
||||||
| composables/useImeSearchInput.js | 한글 등 IME 조합 중(`compositionupdate`)에도 검색어가 즉시 반영되는 검색 입력 처리기(표시값·`onInput`·`onCompositionEnd`, NFC 정규화·선택적 디바운스) |
|
| composables/useImeSearchInput.js | 한글 등 IME 조합 중(`compositionupdate`)에도 검색어가 즉시 반영되는 검색 입력 처리기(표시값·`onInput`·`onCompositionEnd`, NFC 정규화·선택적 디바운스) |
|
||||||
|
| composables/useAdminSettingsNavigation.js | 관리자 설정 화면 내비게이션 검색·필터·활성 구역·스크롤 이동·스크롤 스파이 상태 |
|
||||||
|
|
||||||
## 공유 라이브러리(서버·클라이언트 공통)
|
## 공유 라이브러리(서버·클라이언트 공통)
|
||||||
|
|
||||||
@@ -258,7 +259,7 @@
|
|||||||
| pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·more vert 메뉴, 일반 태그 배지 more vert 메뉴·검색/정렬, 태그 추가 버튼), 액션 피드백 토스트 |
|
| pages/admin/tags/index.vue | 태그 관리(메인 태그 드래그 정렬 자동 저장·more vert 메뉴, 일반 태그 배지 more vert 메뉴·검색/정렬, 태그 추가 버튼), 액션 피드백 토스트 |
|
||||||
| pages/admin/tags/new.vue | 태그 생성 |
|
| pages/admin/tags/new.vue | 태그 생성 |
|
||||||
| pages/admin/tags/[id].vue | 태그 수정 |
|
| pages/admin/tags/[id].vue | 태그 수정 |
|
||||||
| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, 카드별 필드만 PATCH하고 성공 필드만 폼에 반영하는 독립 저장, 단순 설정 섹션 공통 저장 헬퍼, 설정 검색 상태·활성 구역 계산·스크롤 이동 처리, **블로그 제목·설명** 편집 상태·저장 요청·스냅샷 복원 연결, **사이트 정보** 편집 상태·저장 요청·로고 업로드 처리 연결, **SNS 정보** 편집 상태·저장 요청 연결, **POST 설정** 편집 상태·저장 요청·스냅샷 복원 연결, **메인 화면** 편집 상태·저장 요청·커버 업로드 처리 연결, **브랜드** 컬러 정규화·편집 상태·저장 요청 연결, **어나운스 바** 편집 상태·저장 요청 연결, **스팸 필터** 편집 상태·저장 요청·값 정규화 연결, **사이트 용량** 데이터 조회·새로고침 이벤트 연결, **게시물 내보내기** 요청 body 생성·작업 등록·목록 갱신·작업 액션 연결, **게시물 가져오기** 파일 검증·작업 생성 요청·작업 센터 모달 연결, 진행 중 요청 버튼 잠금 |
|
| pages/admin/settings/index.vue | 사이트 설정 Ghost형 전체 화면, 카드별 필드만 PATCH하고 성공 필드만 폼에 반영하는 독립 저장, 단순 설정 섹션 공통 저장 헬퍼, 설정 내비게이션 상태 연결, **블로그 제목·설명** 편집 상태·저장 요청·스냅샷 복원 연결, **사이트 정보** 편집 상태·저장 요청·로고 업로드 처리 연결, **SNS 정보** 편집 상태·저장 요청 연결, **POST 설정** 편집 상태·저장 요청·스냅샷 복원 연결, **메인 화면** 편집 상태·저장 요청·커버 업로드 처리 연결, **브랜드** 컬러 정규화·편집 상태·저장 요청 연결, **어나운스 바** 편집 상태·저장 요청 연결, **스팸 필터** 편집 상태·저장 요청·값 정규화 연결, **사이트 용량** 데이터 조회·새로고침 이벤트 연결, **게시물 내보내기** 요청 body 생성·작업 등록·목록 갱신·작업 액션 연결, **게시물 가져오기** 파일 검증·작업 생성 요청·작업 센터 모달 연결, 진행 중 요청 버튼 잠금 |
|
||||||
| components/admin/AdminSettingsNavigation.vue | 관리자 설정 좌측 내비게이션(설정 검색 입력·검색 결과 목록·모바일 구역 선택·구역 이동 이벤트) |
|
| components/admin/AdminSettingsNavigation.vue | 관리자 설정 좌측 내비게이션(설정 검색 입력·검색 결과 목록·모바일 구역 선택·구역 이동 이벤트) |
|
||||||
| components/admin/AdminHomeCoverSettingsCard.vue | 관리자 설정 메인 화면 카드(라이트·다크 커버 미리보기·드롭존·파일 선택·오버레이 문구·저장 이벤트) |
|
| components/admin/AdminHomeCoverSettingsCard.vue | 관리자 설정 메인 화면 카드(라이트·다크 커버 미리보기·드롭존·파일 선택·오버레이 문구·저장 이벤트) |
|
||||||
| components/admin/AdminBrandSettingsCard.vue | 관리자 설정 브랜드 컬러 카드(읽기 미리보기·색상 입력·텍스트 입력·예시 배지·저장 이벤트) |
|
| components/admin/AdminBrandSettingsCard.vue | 관리자 설정 브랜드 컬러 카드(읽기 미리보기·색상 입력·텍스트 입력·예시 배지·저장 이벤트) |
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# 업데이트 이력
|
# 업데이트 이력
|
||||||
|
|
||||||
|
## v1.11.33
|
||||||
|
|
||||||
|
- 관리자 설정 내비게이션 검색·필터·스크롤 스파이 상태를 `useAdminSettingsNavigation` composable로 분리.
|
||||||
|
- 설정 화면의 좌측 내비 구역 정의와 활성 구역 계산 로직을 페이지 컴포넌트에서 제거.
|
||||||
|
- 설정 화면은 composable에서 받은 상태와 이벤트를 내비게이션 컴포넌트에 연결하도록 정리.
|
||||||
|
|
||||||
## v1.11.32
|
## v1.11.32
|
||||||
|
|
||||||
- 관리자 설정의 단순 섹션 저장 흐름을 `saveSimpleSettingsSection` 헬퍼로 정리.
|
- 관리자 설정의 단순 섹션 저장 흐름을 `saveSimpleSettingsSection` 헬퍼로 정리.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.32",
|
"version": "1.11.33",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.32",
|
"version": "1.11.33",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.32",
|
"version": "1.11.33",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -79,15 +79,19 @@ const postExportMaxFileSizeMb = ref(500)
|
|||||||
const postImportExportPanel = ref('')
|
const postImportExportPanel = ref('')
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const { toast, showToast } = useAdminToast()
|
const { toast, showToast } = useAdminToast()
|
||||||
const mainScrollRef = ref(null)
|
|
||||||
const navSearchQuery = ref('')
|
|
||||||
const {
|
const {
|
||||||
displayValue: navSearchInput,
|
mainScrollRef,
|
||||||
onInput: onNavSearchInput,
|
settingsNavGroups,
|
||||||
onCompositionEnd: onNavSearchCompositionEnd
|
filteredSettingsNavGroups,
|
||||||
} = useImeSearchInput(navSearchQuery)
|
navSearchInput,
|
||||||
const activeSectionId = ref('admin-settings-section-title')
|
onNavSearchInput,
|
||||||
const scrollSpySuspended = ref(false)
|
onNavSearchCompositionEnd,
|
||||||
|
activeSectionId,
|
||||||
|
onMainScroll,
|
||||||
|
refreshActiveSection,
|
||||||
|
scrollToSection,
|
||||||
|
cleanupSettingsNavigation
|
||||||
|
} = useAdminSettingsNavigation()
|
||||||
|
|
||||||
const uploadFileCountSelectOptions = UPLOAD_FILE_COUNT_OPTIONS.map((option) => ({
|
const uploadFileCountSelectOptions = UPLOAD_FILE_COUNT_OPTIONS.map((option) => ({
|
||||||
value: option,
|
value: option,
|
||||||
@@ -213,7 +217,6 @@ const adsSnapshot = reactive({
|
|||||||
adPostInArticleCode: '',
|
adPostInArticleCode: '',
|
||||||
adPostBottomCode: ''
|
adPostBottomCode: ''
|
||||||
})
|
})
|
||||||
let scrollSpyFrame = null
|
|
||||||
let postExportRefreshTimer = null
|
let postExportRefreshTimer = null
|
||||||
let postImportRefreshTimer = null
|
let postImportRefreshTimer = null
|
||||||
|
|
||||||
@@ -635,129 +638,6 @@ const signupBlockedUsernamesText = computed({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
|
||||||
* 설정 화면 좌측 내비 구역 정의
|
|
||||||
* @type {ReadonlyArray<{ heading: string, items: ReadonlyArray<{ id: string, label: string, keywords: string, iconId?: string }> }>}
|
|
||||||
*/
|
|
||||||
const settingsNavGroups = [
|
|
||||||
{
|
|
||||||
heading: '일반',
|
|
||||||
items: [
|
|
||||||
{ id: 'admin-settings-section-title', label: '블로그 제목·설명', keywords: 'title description site name', iconId: 'title-desc' },
|
|
||||||
{ id: 'admin-settings-section-misc', label: '사이트 정보', keywords: 'logo url copyright favicon site info', iconId: 'site-info' },
|
|
||||||
{ id: 'admin-settings-section-social', label: 'SNS 정보', keywords: 'social follow sns x github instagram youtube rss', iconId: 'social' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '게시물',
|
|
||||||
items: [
|
|
||||||
{ id: 'admin-settings-section-post', label: 'POST 설정', keywords: 'post updated date display 수정일', iconId: 'post-settings' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '사이트',
|
|
||||||
items: [
|
|
||||||
{ id: 'admin-settings-section-home-cover', label: '메인 화면', keywords: 'home cover hero banner image', iconId: 'home-cover' },
|
|
||||||
{ id: 'admin-settings-section-brand', label: '브랜드', keywords: 'brand design accent color point 포인트 컬러', iconId: 'brand' },
|
|
||||||
{ id: 'admin-settings-section-announcement', label: '어나운스 바', keywords: 'announcement banner notice', iconId: 'announcement' },
|
|
||||||
{ id: 'admin-settings-section-site-code', label: '사이트 코드', keywords: 'ads ads.txt head footer script code adsense', iconId: 'site-code' },
|
|
||||||
{ id: 'admin-settings-section-ads', label: 'Ads', keywords: 'ads ad slot advertisement adsense 광고 애드센스', iconId: 'ads' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: '콘텐츠·안전',
|
|
||||||
items: [
|
|
||||||
{ id: 'admin-settings-section-backup-summary', label: '사이트 용량', keywords: 'storage media count volume 용량 통계 사이트', iconId: 'post-export' },
|
|
||||||
{ id: 'admin-settings-section-export', label: '게시물 내보내기', keywords: 'export backup 내보내기', iconId: 'post-export' },
|
|
||||||
{ id: 'admin-settings-section-import', label: '게시물 가져오기', keywords: 'import restore 가져오기', iconId: 'post-import' },
|
|
||||||
{ id: 'admin-settings-section-spam', label: '스팸 필터', keywords: 'spam moderation comments', iconId: 'spam' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 검색어에 맞춰 내비 그룹을 필터링한다.
|
|
||||||
* @returns {typeof settingsNavGroups} 표시할 그룹 목록
|
|
||||||
*/
|
|
||||||
const filteredSettingsNavGroups = computed(() => {
|
|
||||||
const q = navSearchQuery.value.trim().toLowerCase()
|
|
||||||
if (!q) {
|
|
||||||
return settingsNavGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
return settingsNavGroups
|
|
||||||
.map((group) => {
|
|
||||||
const items = group.items.filter((item) => {
|
|
||||||
const hay = `${item.label} ${item.keywords}`.toLowerCase()
|
|
||||||
return hay.includes(q)
|
|
||||||
})
|
|
||||||
return { ...group, items }
|
|
||||||
})
|
|
||||||
.filter((group) => group.items.length > 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 스크롤 스파이: 본문 스크롤 위치에 맞춰 활성 섹션 id를 갱신한다.
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const updateActiveSectionFromScroll = () => {
|
|
||||||
const root = mainScrollRef.value
|
|
||||||
if (!root || scrollSpySuspended.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const marker = root.getBoundingClientRect().top + 56
|
|
||||||
let nextId = settingsNavGroups[0].items[0].id
|
|
||||||
const flatIds = settingsNavGroups.flatMap((g) => g.items.map((i) => i.id))
|
|
||||||
|
|
||||||
for (const id of flatIds) {
|
|
||||||
const el = document.getElementById(id)
|
|
||||||
if (!el) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const top = el.getBoundingClientRect().top
|
|
||||||
if (top <= marker) {
|
|
||||||
nextId = id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activeSectionId.value = nextId
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 스크롤 스파이 핸들러 (rAF 디바운스)
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const onMainScroll = () => {
|
|
||||||
if (scrollSpyFrame) {
|
|
||||||
cancelAnimationFrame(scrollSpyFrame)
|
|
||||||
}
|
|
||||||
scrollSpyFrame = requestAnimationFrame(() => {
|
|
||||||
scrollSpyFrame = null
|
|
||||||
updateActiveSectionFromScroll()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 해당 섹션으로 부드럽게 스크롤한다.
|
|
||||||
* @param {string} sectionId - 섹션 요소 id
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const scrollToSection = (sectionId) => {
|
|
||||||
const el = document.getElementById(sectionId)
|
|
||||||
if (!el) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollSpySuspended.value = true
|
|
||||||
activeSectionId.value = sectionId
|
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
window.setTimeout(() => {
|
|
||||||
scrollSpySuspended.value = false
|
|
||||||
updateActiveSectionFromScroll()
|
|
||||||
}, 520)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 설정 화면을 닫고 이전 관리 화면으로 돌아간다.
|
* 설정 화면을 닫고 이전 관리 화면으로 돌아간다.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
@@ -1935,7 +1815,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}, 2000)
|
}, 2000)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
updateActiveSectionFromScroll()
|
refreshActiveSection()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1943,9 +1823,7 @@ onMounted(() => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.clearInterval(postExportRefreshTimer)
|
window.clearInterval(postExportRefreshTimer)
|
||||||
window.clearInterval(postImportRefreshTimer)
|
window.clearInterval(postImportRefreshTimer)
|
||||||
if (scrollSpyFrame) {
|
cleanupSettingsNavigation()
|
||||||
cancelAnimationFrame(scrollSpyFrame)
|
|
||||||
}
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
window.removeEventListener('keydown', onGlobalKeydown)
|
window.removeEventListener('keydown', onGlobalKeydown)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user