// composables/useHeaderNavOverflow.ts import { ref, computed, onMounted, nextTick, watch, type Ref } from 'vue' import { useResizeObserver } from '@vueuse/core' type NavItem = { id: string | number; label: string; href?: string } interface Options { itemSelector?: string // nav 내부 개별 아이템 선택자 headerGap?: number // header 수평 gap(px) navGap?: number // nav 아이템 간 gap(px) } export function useHeaderNavOverflow( items: Ref, headerRef: Ref, logoRef: Ref, trailingRefs: Ref[], // 우측 버튼/아이콘 등 moreButtonRef: Ref, // "More" 버튼 참조 (실측용) options: Options = {} ) { const itemSelector = options.itemSelector ?? '[data-nav-item]' const headerGap = options.headerGap ?? 0 const navGap = options.navGap ?? 0 const containerWidth = ref(0) const logoWidth = ref(0) const trailingWidth = ref(0) const moreBtnWidth = ref(0) // 오버플로우 발생 시 필요한 "More" 버튼 너비 const itemWidths = ref([]) const splitIndex = ref(items.value.length) const measureStatic = () => { const header = headerRef.value if (!header) return containerWidth.value = header.clientWidth // 로고/트레일링(우측 버튼들) 폭 합 logoWidth.value = Math.ceil( logoRef.value?.getBoundingClientRect().width ?? 0 ) trailingWidth.value = trailingRefs.reduce((sum, r) => { const w = Math.ceil(r.value?.getBoundingClientRect().width ?? 0) return sum + w }, 0) // "More" 버튼 실측 (display:none이면 0이므로, visibility:hidden 상태로 자리 차지 측정 추천) moreBtnWidth.value = Math.ceil( moreButtonRef.value?.getBoundingClientRect().width ?? 0 ) } const measureItems = (navEl: HTMLElement | null) => { if (!navEl) return const nodes = Array.from(navEl.querySelectorAll(itemSelector)) itemWidths.value = nodes.map(n => Math.ceil(n.getBoundingClientRect().width) ) } const computeSplit = (navEl: HTMLElement | null) => { if (!navEl) return const available = containerWidth.value - logoWidth.value - trailingWidth.value - headerGap // header 자체 gap 여유분(필요 시) if (available <= 0) { splitIndex.value = 0 return } // 1차: 모두 넣을 수 있는지 시도 let sum = 0 let idx = items.value.length for (let i = 0; i < items.value.length; i++) { const w = itemWidths.value[i] ?? 0 const extra = i === 0 ? 0 : navGap if (sum + w + extra <= available) { sum += w + extra } else { idx = i break } } // 오버플로우가 생기면 "More" 버튼 자리도 고려해서 재계산 if (idx < items.value.length && moreBtnWidth.value > 0) { const available2 = available - moreBtnWidth.value - navGap // more와의 간격 let sum2 = 0 let idx2 = 0 for (let i = 0; i < items.value.length; i++) { const w = itemWidths.value[i] ?? 0 const extra = i === 0 ? 0 : navGap if (sum2 + w + extra <= available2) { sum2 += w + extra idx2 = i + 1 } else { break } } splitIndex.value = idx2 } else { splitIndex.value = idx } } const visibleItems = computed(() => items.value.slice(0, splitIndex.value)) const overflowItems = computed(() => items.value.slice(splitIndex.value)) const measureAll = async (navEl: HTMLElement | null) => { await nextTick() measureStatic() measureItems(navEl) computeSplit(navEl) } const observe = (el: HTMLElement | null, navEl: HTMLElement | null) => { if (!el) return useResizeObserver(el, () => { containerWidth.value = el.clientWidth computeSplit(navEl) }) } const init = (navEl: Ref) => { onMounted(() => { measureAll(navEl.value) observe(headerRef.value, navEl.value) // 폰트/i18n 변경 같은 경우에 수동 호출할 수 있도록 measureAll 노출 }) } // 아이템 배열 변경 시 재측정 watch(items, () => measureAll(currentNav.value), { deep: true }) // nav 엘리먼트를 외부에서 넘기기 위해 보조 ref const currentNav = ref(null) return { visibleItems, overflowItems, splitIndex, measureAll, currentNav, // 템플릿에서 ref 바인딩 init, } }