탭바 스크롤 연동 개선: 실시간 위치 추적 및 스냅 동작
- 스크롤 양의 50%만큼 탭바가 실시간으로 이동 - currentTranslateY 변수로 위치 추적 (DOM 읽기 제거) - 스크롤 중지 시 50% 기준으로 자동 스냅 - 점진적 opacity 변화 적용 (0.1~1) - will-change와 cubic-bezier로 성능 최적화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -296,11 +296,11 @@ footer {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
border-top: 0.1px solid var(--color-E5E5E5);
|
border-top: 0.1px solid var(--color-E5E5E5);
|
||||||
transition: transform 0.3s ease-in-out;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-tabbar.hidden {
|
.bottom-tabbar.snapping {
|
||||||
transform: translateY(100%);
|
transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button {
|
.tab-button {
|
||||||
@@ -313,14 +313,9 @@ footer {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease, opacity 0.3s ease-in-out;
|
transition: all 0.2s ease;
|
||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-tabbar.hidden .tab-button {
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-button:hover {
|
.tab-button:hover {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export const FooterNavigation = ({
|
|||||||
}: FooterProps) => {
|
}: FooterProps) => {
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
const [isFooterOn, setIsFooterOn] = useState<boolean>(true);
|
const [isFooterOn, setIsFooterOn] = useState<boolean>(true);
|
||||||
const [isFooterVisible, setIsFooterVisible] = useState<boolean>(true);
|
|
||||||
|
|
||||||
const onClickToNavigate = (path?: string) => {
|
const onClickToNavigate = (path?: string) => {
|
||||||
if(!!path){
|
if(!!path){
|
||||||
@@ -98,21 +97,59 @@ export const FooterNavigation = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const tabbar = document.querySelector('.bottom-tabbar') as HTMLElement;
|
||||||
|
if (!tabbar) return;
|
||||||
|
|
||||||
|
const TABBAR_HEIGHT = 70;
|
||||||
|
const SCROLL_THRESHOLD = 50;
|
||||||
|
const SNAP_THRESHOLD = 0.5; // 50% 보이면 표시, 아니면 숨김
|
||||||
|
const SCROLL_MULTIPLIER = 0.5; // 스크롤의 50%만큼 이동
|
||||||
|
|
||||||
let lastScrollY = 0;
|
let lastScrollY = 0;
|
||||||
|
let currentTranslateY = 0; // 별도로 추적
|
||||||
|
let isScrolling = false;
|
||||||
|
let scrollTimeout: NodeJS.Timeout;
|
||||||
let ticking = false;
|
let ticking = false;
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
const currentScrollY = window.scrollY;
|
const currentScrollY = window.scrollY;
|
||||||
|
const scrollDelta = currentScrollY - lastScrollY;
|
||||||
|
|
||||||
if (!ticking) {
|
if (!ticking) {
|
||||||
window.requestAnimationFrame(() => {
|
window.requestAnimationFrame(() => {
|
||||||
// 스크롤 다운 & 일정 거리 이상 스크롤된 경우 -> 숨김
|
// 스크롤 시작
|
||||||
if (currentScrollY > lastScrollY && currentScrollY > 50) {
|
if (!isScrolling) {
|
||||||
setIsFooterVisible(false);
|
isScrolling = true;
|
||||||
|
tabbar.classList.remove('snapping');
|
||||||
}
|
}
|
||||||
// 스크롤 업 -> 표시
|
|
||||||
else if (currentScrollY < lastScrollY) {
|
// 스크롤 방향에 따라 translateY 조정
|
||||||
setIsFooterVisible(true);
|
if (currentScrollY > SCROLL_THRESHOLD) {
|
||||||
|
// 스크롤 다운
|
||||||
|
if (scrollDelta > 0) {
|
||||||
|
currentTranslateY = Math.min(currentTranslateY + scrollDelta * SCROLL_MULTIPLIER, TABBAR_HEIGHT);
|
||||||
|
}
|
||||||
|
// 스크롤 업
|
||||||
|
else {
|
||||||
|
currentTranslateY = Math.max(currentTranslateY + scrollDelta * SCROLL_MULTIPLIER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabbar.style.transform = `translateY(${currentTranslateY}px)`;
|
||||||
|
|
||||||
|
// opacity 계산 (0 = 완전히 보임, TABBAR_HEIGHT = 완전히 숨김)
|
||||||
|
const opacityRatio = 1 - (currentTranslateY / TABBAR_HEIGHT);
|
||||||
|
const buttons = tabbar.querySelectorAll('.tab-button') as NodeListOf<HTMLElement>;
|
||||||
|
buttons.forEach(button => {
|
||||||
|
button.style.opacity = `${Math.max(0.1, opacityRatio)}`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 페이지 상단 근처에서는 완전히 표시
|
||||||
|
currentTranslateY = 0;
|
||||||
|
tabbar.style.transform = 'translateY(0)';
|
||||||
|
const buttons = tabbar.querySelectorAll('.tab-button') as NodeListOf<HTMLElement>;
|
||||||
|
buttons.forEach(button => {
|
||||||
|
button.style.opacity = '1';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
lastScrollY = currentScrollY;
|
lastScrollY = currentScrollY;
|
||||||
@@ -121,19 +158,49 @@ export const FooterNavigation = ({
|
|||||||
|
|
||||||
ticking = true;
|
ticking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 스크롤 중지 감지
|
||||||
|
clearTimeout(scrollTimeout);
|
||||||
|
scrollTimeout = setTimeout(() => {
|
||||||
|
isScrolling = false;
|
||||||
|
|
||||||
|
// 현재 위치 확인 후 snap
|
||||||
|
const visibleRatio = 1 - (currentTranslateY / TABBAR_HEIGHT);
|
||||||
|
|
||||||
|
tabbar.classList.add('snapping');
|
||||||
|
|
||||||
|
if (visibleRatio >= SNAP_THRESHOLD) {
|
||||||
|
// 50% 이상 보이면 완전히 표시
|
||||||
|
currentTranslateY = 0;
|
||||||
|
tabbar.style.transform = 'translateY(0)';
|
||||||
|
const buttons = tabbar.querySelectorAll('.tab-button') as NodeListOf<HTMLElement>;
|
||||||
|
buttons.forEach(button => {
|
||||||
|
button.style.opacity = '1';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 50% 미만이면 완전히 숨김
|
||||||
|
currentTranslateY = TABBAR_HEIGHT;
|
||||||
|
tabbar.style.transform = `translateY(${TABBAR_HEIGHT}px)`;
|
||||||
|
const buttons = tabbar.querySelectorAll('.tab-button') as NodeListOf<HTMLElement>;
|
||||||
|
buttons.forEach(button => {
|
||||||
|
button.style.opacity = '0.1';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('scroll', handleScroll);
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
clearTimeout(scrollTimeout);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ isFooterOn &&
|
{ isFooterOn &&
|
||||||
<nav className={`bottom-tabbar ${!isFooterVisible ? 'hidden' : ''}`}>
|
<nav className="bottom-tabbar">
|
||||||
{ getFooterButtonItems() }
|
{ getFooterButtonItems() }
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user