설정 내비게이션 상태 분리
This commit is contained in:
@@ -79,15 +79,19 @@ const postExportMaxFileSizeMb = ref(500)
|
||||
const postImportExportPanel = ref('')
|
||||
const errorMessage = ref('')
|
||||
const { toast, showToast } = useAdminToast()
|
||||
const mainScrollRef = ref(null)
|
||||
const navSearchQuery = ref('')
|
||||
const {
|
||||
displayValue: navSearchInput,
|
||||
onInput: onNavSearchInput,
|
||||
onCompositionEnd: onNavSearchCompositionEnd
|
||||
} = useImeSearchInput(navSearchQuery)
|
||||
const activeSectionId = ref('admin-settings-section-title')
|
||||
const scrollSpySuspended = ref(false)
|
||||
mainScrollRef,
|
||||
settingsNavGroups,
|
||||
filteredSettingsNavGroups,
|
||||
navSearchInput,
|
||||
onNavSearchInput,
|
||||
onNavSearchCompositionEnd,
|
||||
activeSectionId,
|
||||
onMainScroll,
|
||||
refreshActiveSection,
|
||||
scrollToSection,
|
||||
cleanupSettingsNavigation
|
||||
} = useAdminSettingsNavigation()
|
||||
|
||||
const uploadFileCountSelectOptions = UPLOAD_FILE_COUNT_OPTIONS.map((option) => ({
|
||||
value: option,
|
||||
@@ -213,7 +217,6 @@ const adsSnapshot = reactive({
|
||||
adPostInArticleCode: '',
|
||||
adPostBottomCode: ''
|
||||
})
|
||||
let scrollSpyFrame = null
|
||||
let postExportRefreshTimer = 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>}
|
||||
@@ -1935,7 +1815,7 @@ onMounted(() => {
|
||||
}
|
||||
}, 2000)
|
||||
nextTick(() => {
|
||||
updateActiveSectionFromScroll()
|
||||
refreshActiveSection()
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1943,9 +1823,7 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
window.clearInterval(postExportRefreshTimer)
|
||||
window.clearInterval(postImportRefreshTimer)
|
||||
if (scrollSpyFrame) {
|
||||
cancelAnimationFrame(scrollSpyFrame)
|
||||
}
|
||||
cleanupSettingsNavigation()
|
||||
if (import.meta.client) {
|
||||
window.removeEventListener('keydown', onGlobalKeydown)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user