메뉴 스크롤 동작 개선 및 코드 리팩토링
- 메뉴 버튼과 스크롤 위치 동기화 문제 해결 - 스크롤 업/다운 시 다른 오프셋 적용으로 정확한 카테고리 선택 - 버튼 클릭 시 해당 카테고리로 정확한 스크롤 이동 - 콘솔 로그 제거 및 코드 정리 - 상수 추출 및 타입 정의 개선 - 불필요한 import 및 변수 제거 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -137,9 +137,13 @@ export const MenuCategory = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`menu-category ${menuId}`}
|
className={`menu-category ${menuId}`}
|
||||||
ref={ (element: HTMLDivElement) => { buttonRefs.current[itemIndex] = element } }
|
ref={ (element: HTMLDivElement) => {
|
||||||
|
if (element) {
|
||||||
|
buttonRefs.current[itemIndex] = element;
|
||||||
|
}
|
||||||
|
} }
|
||||||
>
|
>
|
||||||
<div className="category-header">
|
<div className="category-header">
|
||||||
<div className={ 'category-icon ' + menuIcon }></div>
|
<div className={ 'category-icon ' + menuIcon }></div>
|
||||||
|
|||||||
@@ -9,13 +9,25 @@ import { FilterMotionDuration, FilterMotionStyle, FilterMotionVariants, MenuItem
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
import { setHomeReloadKey } from '@/pages/home/home-page';
|
import { setHomeReloadKey } from '@/pages/home/home-page';
|
||||||
import { P } from 'node_modules/framer-motion/dist/types.d-Cjd591yU';
|
|
||||||
|
// 상수 정의
|
||||||
|
const SCROLL_ANIMATION_DURATION = 800;
|
||||||
|
const SCROLL_UP_OFFSET_PX = 150;
|
||||||
|
const BUTTON_SCROLL_OFFSET = 30;
|
||||||
|
|
||||||
|
// 타입 정의
|
||||||
|
interface ShortButton {
|
||||||
|
menuId: string;
|
||||||
|
menuName: string;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MenuProps {
|
export interface MenuProps {
|
||||||
menuOn: boolean;
|
menuOn: boolean;
|
||||||
setMenuOn: (menuOn: boolean) => void;
|
setMenuOn: (menuOn: boolean) => void;
|
||||||
favoriteEdit?: boolean;
|
favoriteEdit?: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
export const Menu = ({
|
export const Menu = ({
|
||||||
menuOn,
|
menuOn,
|
||||||
setMenuOn,
|
setMenuOn,
|
||||||
@@ -24,40 +36,68 @@ export const Menu = ({
|
|||||||
const userMids = useStore.getState().UserStore.userMids;
|
const userMids = useStore.getState().UserStore.userMids;
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { navigate } = useNavigate();
|
const { navigate } = useNavigate();
|
||||||
const userInfo = useStore((state) => state.UserStore.userInfo);
|
|
||||||
|
|
||||||
const [shortBtns, setShortBtns] = useState<Array<Record<string, any>>>([]);
|
const [shortBtns, setShortBtns] = useState<ShortButton[]>([]);
|
||||||
const [editMode, setEditMode] = useState<boolean>(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
const [changeMenuId, setChangeMenuId] = useState<string | undefined>();
|
const [changeMenuId, setChangeMenuId] = useState<string | undefined>();
|
||||||
const [shortBtnIdx, setShortBtnIdx] = useState<number>(0);
|
const [shortBtnIdx, setShortBtnIdx] = useState(0);
|
||||||
const [shortBoxOnScroll, setShortBoxOnScroll] = useState<boolean>(false);
|
|
||||||
const [menuListOnScroll, setMenuListOnScroll] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const buttonRefs = useRef<Array<HTMLDivElement>>([]);
|
const buttonRefs = useRef<Array<HTMLDivElement>>([]);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const shortBtnScrollRef = useRef<HTMLDivElement>(null);
|
const shortBtnScrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isButtonScrolling = useRef<boolean>(false);
|
||||||
|
const scrollTimer = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const lastScrollTop = useRef<number>(0);
|
||||||
|
|
||||||
const onClickToNavigate = (path: string) => {
|
const onClickToNavigate = (path: string) => {
|
||||||
onClickToMenuClose();
|
onClickToMenuClose();
|
||||||
navigate(path);
|
navigate(path);
|
||||||
};
|
};
|
||||||
|
const scrollCategoryButtonToLeft = (index: number) => {
|
||||||
|
const buttonElement = shortBtnScrollRef.current?.children[index] as HTMLElement;
|
||||||
|
if (buttonElement && shortBtnScrollRef.current) {
|
||||||
|
const scrollLeft = buttonElement.offsetLeft - BUTTON_SCROLL_OFFSET;
|
||||||
|
shortBtnScrollRef.current.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
left: scrollLeft,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onClickToMenuNavigate = (menuId: string, index: number) => {
|
const onClickToMenuNavigate = (menuId: string, index: number) => {
|
||||||
setShortBoxOnScroll(false);
|
isButtonScrolling.current = true;
|
||||||
setMenuListOnScroll(false);
|
|
||||||
let tops = [0, 255, 410, 565, 720, 875, 1030, 1500];
|
|
||||||
let lefts = [0, 85, 170, 275, 360, 450, 450, 450];
|
|
||||||
setShortBtnIdx(index);
|
setShortBtnIdx(index);
|
||||||
scrollRef.current?.scrollTo({
|
scrollCategoryButtonToLeft(index);
|
||||||
top: tops[index],
|
|
||||||
left: 0,
|
const categoryElement = buttonRefs.current[index];
|
||||||
behavior: 'smooth'
|
if (categoryElement && scrollRef.current) {
|
||||||
});
|
const scrollContainer = scrollRef.current;
|
||||||
shortBtnScrollRef.current?.scrollTo({
|
const containerStyle = window.getComputedStyle(scrollContainer);
|
||||||
top: 0,
|
const paddingTop = parseFloat(containerStyle.paddingTop) || 0;
|
||||||
left: lefts[index],
|
const scrollPosition = categoryElement.offsetTop - paddingTop;
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
lastScrollTop.current = scrollPosition;
|
||||||
|
|
||||||
|
scrollContainer.scrollTo({
|
||||||
|
top: scrollPosition,
|
||||||
|
left: 0,
|
||||||
|
behavior: 'smooth'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollTimer.current) {
|
||||||
|
clearTimeout(scrollTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTimer.current = setTimeout(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
const actualScrollTop = scrollRef.current.scrollTop;
|
||||||
|
lastScrollTop.current = actualScrollTop;
|
||||||
|
setShortBtnIdx(index);
|
||||||
|
}
|
||||||
|
isButtonScrolling.current = false;
|
||||||
|
}, SCROLL_ANIMATION_DURATION);
|
||||||
};
|
};
|
||||||
const onClickToMenuClose = () => {
|
const onClickToMenuClose = () => {
|
||||||
if(editMode){
|
if(editMode){
|
||||||
@@ -93,105 +133,85 @@ export const Menu = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shortBtnsSetting = () => {
|
const shortBtnsSetting = () => {
|
||||||
let shortList = [];
|
const shortList: ShortButton[] = MenuItems.map((item, index) => ({
|
||||||
for(let i=0;i<MenuItems.length;i++) {
|
menuId: item.menuId,
|
||||||
shortList.push({
|
menuName: item.menuName,
|
||||||
menuId: MenuItems[i]?.menuId,
|
index
|
||||||
menuName: MenuItems[i]?.menuName,
|
}));
|
||||||
index: i
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setShortBtns(shortList);
|
setShortBtns(shortList);
|
||||||
};
|
};
|
||||||
|
|
||||||
const shortBtnScroll = (e: any) => {
|
const getCurrentCategoryIndex = (scrollTop: number): number => {
|
||||||
if(shortBoxOnScroll){
|
if (buttonRefs.current.length === 0 || !scrollRef.current) return 0;
|
||||||
let x = shortBtnScrollRef.current?.scrollLeft;
|
|
||||||
let top = 0;
|
const containerStyle = window.getComputedStyle(scrollRef.current);
|
||||||
if(x){
|
const paddingTop = parseFloat(containerStyle.paddingTop) || 0;
|
||||||
if(x < 85){
|
const adjustedScrollTop = scrollTop + paddingTop;
|
||||||
top = 0;
|
const isScrollingUp = scrollTop < lastScrollTop.current;
|
||||||
setShortBtnIdx(0);
|
|
||||||
}
|
if (isScrollingUp) {
|
||||||
else if(x < 170){
|
// 스크롤 업: 화면 상단 기준으로 카테고리 선택
|
||||||
top = 255;
|
const checkPoint = scrollTop + paddingTop + SCROLL_UP_OFFSET_PX;
|
||||||
setShortBtnIdx(1);
|
|
||||||
}
|
for (let i = buttonRefs.current.length - 1; i >= 0; i--) {
|
||||||
else if(x < 275){
|
const element = buttonRefs.current[i];
|
||||||
top = 410;
|
if (!element) continue;
|
||||||
setShortBtnIdx(2);
|
|
||||||
}
|
const currentTop = element.offsetTop;
|
||||||
else if(x < 360){
|
|
||||||
top = 565;
|
if (checkPoint >= currentTop) {
|
||||||
setShortBtnIdx(3);
|
if (i < buttonRefs.current.length - 1) {
|
||||||
}
|
const nextElement = buttonRefs.current[i + 1];
|
||||||
else if(x < 450){
|
if (nextElement && checkPoint >= nextElement.offsetTop) {
|
||||||
top = 720;
|
continue;
|
||||||
setShortBtnIdx(4);
|
}
|
||||||
}
|
}
|
||||||
else{
|
return i;
|
||||||
top = 875;
|
}
|
||||||
setShortBtnIdx(5);
|
}
|
||||||
|
} else {
|
||||||
|
// 스크롤 다운: 카테고리 시작점 기준
|
||||||
|
for (let i = buttonRefs.current.length - 1; i >= 0; i--) {
|
||||||
|
const element = buttonRefs.current[i];
|
||||||
|
if (!element) continue;
|
||||||
|
|
||||||
|
const currentTop = element.offsetTop;
|
||||||
|
|
||||||
|
if (adjustedScrollTop >= currentTop) {
|
||||||
|
if (i === buttonRefs.current.length - 1) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextElement = buttonRefs.current[i + 1];
|
||||||
|
if (nextElement) {
|
||||||
|
const nextTop = nextElement.offsetTop;
|
||||||
|
if (adjustedScrollTop < nextTop) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollRef.current?.scrollTo({
|
|
||||||
top: top,
|
|
||||||
left: 0,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuListScroll = () => {
|
const menuListScroll = () => {
|
||||||
if(menuListOnScroll){
|
if (isButtonScrolling.current) return;
|
||||||
let y = scrollRef.current?.scrollTop;
|
|
||||||
let left = 0;
|
|
||||||
if(y){
|
|
||||||
if(y < 255){
|
|
||||||
left = 0;
|
|
||||||
}
|
|
||||||
else if(y < 410){
|
|
||||||
left = 85;
|
|
||||||
}
|
|
||||||
else if(y < 565){
|
|
||||||
left = 170;
|
|
||||||
}
|
|
||||||
else if(y < 720){
|
|
||||||
left = 275;
|
|
||||||
}
|
|
||||||
else if(y < 875){
|
|
||||||
left = 360;
|
|
||||||
}
|
|
||||||
else if(y < 1030){
|
|
||||||
left = 450;
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
left = 450;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
shortBtnScrollRef.current?.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
left: left,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const shortBoxTouchStart = () => {
|
|
||||||
setShortBoxOnScroll(true);
|
|
||||||
setMenuListOnScroll(false);
|
|
||||||
};
|
|
||||||
const shortBoxTouchEnd = () => {
|
|
||||||
|
|
||||||
};
|
const scrollTop = scrollRef.current?.scrollTop || 0;
|
||||||
const menuListTouchStart = () => {
|
const currentIndex = getCurrentCategoryIndex(scrollTop);
|
||||||
setShortBoxOnScroll(false);
|
|
||||||
setMenuListOnScroll(true);
|
|
||||||
};
|
|
||||||
const menuListTouchEnd = () => {
|
|
||||||
|
|
||||||
|
if (currentIndex !== shortBtnIdx) {
|
||||||
|
setShortBtnIdx(currentIndex);
|
||||||
|
scrollCategoryButtonToLeft(currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastScrollTop.current = scrollTop;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
shortBtnsSetting();
|
shortBtnsSetting();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -255,31 +275,26 @@ export const Menu = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="full-menu-keywords-wrap">
|
<div className="full-menu-keywords-wrap">
|
||||||
<div
|
<div
|
||||||
className="full-menu-keywords"
|
className="full-menu-keywords"
|
||||||
ref={ shortBtnScrollRef }
|
ref={ shortBtnScrollRef }
|
||||||
onScroll={ shortBtnScroll }
|
|
||||||
onTouchStart={ shortBoxTouchStart }
|
|
||||||
onTouchEnd={ shortBoxTouchEnd }
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
shortBtns.map((value, index) => (
|
shortBtns.map((value, index) => (
|
||||||
<span
|
<span
|
||||||
key={ `short-btn-${value.menuName}` }
|
key={ `short-btn-${value.menuName}` }
|
||||||
className={ `keyword-tag ${(shortBtnIdx === index)? 'active': ''}` }
|
className={ `keyword-tag ${(shortBtnIdx === index)? 'active': ''}` }
|
||||||
onClick={ () => onClickToMenuNavigate(value.menuId, value.index) }
|
onClick={ () => onClickToMenuNavigate(value.menuId, index) }
|
||||||
>{ value.menuName }</span>
|
>{ value.menuName }</span>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="full-menu-list"
|
className="full-menu-list"
|
||||||
ref={ scrollRef }
|
ref={ scrollRef }
|
||||||
onScroll={ menuListScroll }
|
onScroll={ menuListScroll }
|
||||||
onTouchStart={ menuListTouchStart }
|
|
||||||
onTouchEnd={ menuListTouchEnd }
|
|
||||||
>
|
>
|
||||||
{ getMenuCategory() }
|
{ getMenuCategory() }
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user