Files
frontend/apps/dm_new/app/(landing)/reuse/_hooks/useScrollSpy.ts
T
2026-03-03 11:39:24 +09:00

67 lines
2.0 KiB
TypeScript

/**
* IntersectionObserver로 각 section의 화면 노출 비율을 계산해서 가장 많이 보이는 section을 activeId로 만든다
*/
"use client";
import { useEffect, useRef, useState } from "react";
const DEFAULT_THRESHOLD = [0, 0.01, 0.1, 0.25, 0.5, 0.75];
export function useScrollSpy(sectionIds: string[], options?: IntersectionObserverInit) {
const [activeId, setActiveId] = useState(sectionIds[0] ?? "");
const ratiosRef = useRef<Map<string, number>>(new Map());
const activeIdRef = useRef(activeId);
useEffect(() => {
activeIdRef.current = activeId;
}, [activeId]);
const threshold = options?.threshold ?? DEFAULT_THRESHOLD;
const thresholdKey = Array.isArray(threshold) ? threshold.join("|") : String(threshold);
useEffect(() => {
const els = sectionIds
.map((id) => document.getElementById(id))
.filter(Boolean) as HTMLElement[];
if (!els.length) return;
const DEFAULT_ROOT_MARGIN = "83px 0px -65% 0px";
// 초기값 세팅(안정화)
ratiosRef.current.clear();
els.forEach((el) => ratiosRef.current.set(el.id, 0));
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
const id = (e.target as HTMLElement).id;
ratiosRef.current.set(id, e.isIntersecting ? e.intersectionRatio : 0);
}
// ✅ 전체 섹션 중 가장 ratio 큰 항목을 active로
let bestId = activeIdRef.current;
let bestRatio = -1;
for (const [id, ratio] of ratiosRef.current.entries()) {
if (ratio > bestRatio) {
bestRatio = ratio;
bestId = id;
}
}
if (bestId) setActiveId(bestId);
},
{
root: options?.root ?? null,
threshold,
rootMargin: options?.rootMargin ?? DEFAULT_ROOT_MARGIN,
}
);
els.forEach((el) => io.observe(el));
return () => io.disconnect();
}, [sectionIds, options?.root, options?.rootMargin, threshold, thresholdKey]);
return activeId;
}