- 즐겨찾기가 1개 남았을 때 해제 방지 - 편집 모드에서 메뉴 클릭 기능 비활성화 - 즐겨찾기 변경 시 모든 카테고리의 체크박스 상태 동기화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
335 lines
9.9 KiB
TypeScript
335 lines
9.9 KiB
TypeScript
import { PATHS } from '@/shared/constants/paths';
|
|
import { motion } from 'framer-motion';
|
|
import { IMAGE_ROOT } from '@/shared/constants/common';
|
|
import { useNavigate } from '@/shared/lib/hooks/use-navigate';
|
|
import { MenuCategory } from '@/entities/menu/ui/menu-category';
|
|
import { FavoriteWrapper } from '@/entities/home/ui/favorite-wrapper';
|
|
import { useStore } from '@/shared/model/store';
|
|
import { FilterMotionDuration, FilterMotionStyle, FilterMotionVariants, MenuItems } from '@/entities/common/model/constant';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { useLocation } from 'react-router';
|
|
import { setHomeReloadKey } from '@/pages/home/home-page';
|
|
import { useShortcutSaveMutation } from '@/entities/user/api/use-shortcut-save-mutation';
|
|
import { ShortcutSaveParams, ShortcutSaveResponse } from '@/entities/user/model/types';
|
|
|
|
// 상수 정의
|
|
const SCROLL_ANIMATION_DURATION = 800;
|
|
const SCROLL_UP_OFFSET_PX = 150;
|
|
const BUTTON_SCROLL_OFFSET = 30;
|
|
|
|
// 타입 정의
|
|
interface ShortButton {
|
|
menuId: number;
|
|
menuName: string;
|
|
index: number;
|
|
}
|
|
|
|
export interface MenuProps {
|
|
menuOn: boolean;
|
|
setMenuOn: (menuOn: boolean) => void;
|
|
favoriteEdit?: boolean;
|
|
}
|
|
|
|
export const Menu = ({
|
|
menuOn,
|
|
setMenuOn,
|
|
favoriteEdit
|
|
}: MenuProps) => {
|
|
const userMids = useStore.getState().UserStore.userMids;
|
|
const userInfo = useStore.getState().UserStore.userInfo;
|
|
const location = useLocation();
|
|
const { navigate } = useNavigate();
|
|
|
|
const { mutateAsync: shortcutSave } = useShortcutSaveMutation();
|
|
|
|
const [shortBtns, setShortBtns] = useState<ShortButton[]>([]);
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [changeMenuId, setChangeMenuId] = useState<string>();
|
|
const [shortBtnIdx, setShortBtnIdx] = useState(0);
|
|
|
|
const buttonRefs = useRef<Array<HTMLDivElement>>([]);
|
|
const scrollRef = 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 [menuIds, setMenuIds] = useState<Array<number | undefined>>([]);
|
|
|
|
const callShortcutSave = () => {
|
|
if(userInfo.usrid){
|
|
let userFavorite = useStore.getState().UserStore.userFavorite;
|
|
let menuIds = userFavorite.map((value, index) => {
|
|
return value.menuId;
|
|
});
|
|
|
|
let params: ShortcutSaveParams = {
|
|
usrid: userInfo.usrid,
|
|
isDefault: false,
|
|
menuIds: menuIds
|
|
};
|
|
shortcutSave(params).then((rs: ShortcutSaveResponse) => {
|
|
|
|
});
|
|
}
|
|
};
|
|
|
|
const onClickToNavigate = (path: string) => {
|
|
onClickToMenuClose();
|
|
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: number, index: number) => {
|
|
isButtonScrolling.current = true;
|
|
setShortBtnIdx(index);
|
|
scrollCategoryButtonToLeft(index);
|
|
|
|
const categoryElement = buttonRefs.current[index];
|
|
if (categoryElement && scrollRef.current) {
|
|
const scrollContainer = scrollRef.current;
|
|
const containerStyle = window.getComputedStyle(scrollContainer);
|
|
const paddingTop = parseFloat(containerStyle.paddingTop) || 0;
|
|
const scrollPosition = categoryElement.offsetTop - paddingTop;
|
|
|
|
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 = () => {
|
|
if(editMode){
|
|
setEditMode(false);
|
|
callShortcutSave();
|
|
|
|
// 여기에 저장 로직?
|
|
}
|
|
else{
|
|
setMenuOn(false);
|
|
if(location.pathname === PATHS.home){
|
|
setHomeReloadKey();
|
|
}
|
|
}
|
|
};
|
|
|
|
const getMenuCategory = () => {
|
|
let rs = [];
|
|
for(let i=0;i<MenuItems.length;i++){
|
|
if(MenuItems[i]){
|
|
rs.push(
|
|
<MenuCategory
|
|
key={ `menu-category-${i}` }
|
|
menuId={ MenuItems[i]?.menuId }
|
|
iconFilePath={ MenuItems[i]?.iconFilePath }
|
|
menuName={ MenuItems[i]?.menuName }
|
|
subMenu={ MenuItems[i]?.subMenu }
|
|
setMenuOn={ setMenuOn }
|
|
editMode={ editMode }
|
|
changeMenuId={ changeMenuId }
|
|
setChangeMenuId= { setChangeMenuId }
|
|
buttonRefs={ buttonRefs }
|
|
itemIndex={ i }
|
|
/>
|
|
);
|
|
}
|
|
|
|
}
|
|
return rs;
|
|
};
|
|
|
|
const shortBtnsSetting = () => {
|
|
const shortList: ShortButton[] = MenuItems.map((item, index) => ({
|
|
menuId: item.menuId,
|
|
menuName: item.menuName,
|
|
index
|
|
}));
|
|
setShortBtns(shortList);
|
|
};
|
|
|
|
const getCurrentCategoryIndex = (scrollTop: number): number => {
|
|
if (buttonRefs.current.length === 0 || !scrollRef.current) return 0;
|
|
|
|
const containerStyle = window.getComputedStyle(scrollRef.current);
|
|
const paddingTop = parseFloat(containerStyle.paddingTop) || 0;
|
|
const adjustedScrollTop = scrollTop + paddingTop;
|
|
|
|
// 뷰포트의 상단 기준점 (약간의 오프셋 추가)
|
|
const viewportTopWithOffset = adjustedScrollTop + 50;
|
|
|
|
// 현재 뷰포트에 가장 많이 보이는 카테고리 찾기
|
|
for (let i = 0; i < buttonRefs.current.length; i++) {
|
|
const element = buttonRefs.current[i];
|
|
if (!element) continue;
|
|
|
|
const currentTop = element.offsetTop;
|
|
|
|
// 마지막 카테고리인 경우
|
|
if (i === buttonRefs.current.length - 1) {
|
|
if (viewportTopWithOffset >= currentTop) {
|
|
return i;
|
|
}
|
|
} else {
|
|
// 다음 카테고리가 있는 경우
|
|
const nextElement = buttonRefs.current[i + 1];
|
|
if (nextElement) {
|
|
const nextTop = nextElement.offsetTop;
|
|
|
|
// 현재 카테고리 영역 내에 뷰포트 상단이 있으면 선택
|
|
if (viewportTopWithOffset >= currentTop && viewportTopWithOffset < nextTop) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
const menuListScroll = () => {
|
|
if (isButtonScrolling.current) return;
|
|
|
|
const scrollTop = scrollRef.current?.scrollTop || 0;
|
|
const currentIndex = getCurrentCategoryIndex(scrollTop);
|
|
|
|
if (currentIndex !== shortBtnIdx) {
|
|
setShortBtnIdx(currentIndex);
|
|
scrollCategoryButtonToLeft(currentIndex);
|
|
}
|
|
|
|
lastScrollTop.current = scrollTop;
|
|
};
|
|
|
|
useEffect(() => {
|
|
shortBtnsSetting();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if(favoriteEdit){
|
|
setEditMode(favoriteEdit)
|
|
}
|
|
},[favoriteEdit]);
|
|
|
|
// 메뉴가 열릴 때 초기화
|
|
useEffect(() => {
|
|
if (menuOn) {
|
|
// 스크롤 위치 초기화
|
|
if (scrollRef.current) {
|
|
scrollRef.current.scrollTop = 0;
|
|
}
|
|
if (shortBtnScrollRef.current) {
|
|
shortBtnScrollRef.current.scrollLeft = 0;
|
|
}
|
|
// 선택된 버튼 인덱스 초기화
|
|
setShortBtnIdx(0);
|
|
// lastScrollTop 초기화
|
|
lastScrollTop.current = 0;
|
|
}
|
|
}, [menuOn]);
|
|
|
|
return (
|
|
<>
|
|
<motion.div
|
|
className="full-menu-modal"
|
|
initial="hidden"
|
|
animate={ (menuOn)? 'visible': 'hidden' }
|
|
variants={ FilterMotionVariants }
|
|
transition={ FilterMotionDuration }
|
|
style={ FilterMotionStyle }
|
|
>
|
|
<div className="full-menu-container">
|
|
<div className="full-menu-header">
|
|
<div className="full-menu-title">
|
|
{ userMids[0] }
|
|
<span style={{marginLeft: '4px'}}>(madzoneviper)</span>
|
|
</div>
|
|
<div className="full-menu-actions">
|
|
{ !editMode &&
|
|
<button
|
|
className="full-menu-settings"
|
|
onClick={ () => onClickToNavigate(PATHS.setting) }
|
|
>
|
|
<img
|
|
src={ IMAGE_ROOT + '/ico_set.svg' }
|
|
alt="설정"
|
|
/>
|
|
</button>
|
|
}
|
|
<button
|
|
className="full-menu-close"
|
|
onClick={ () => onClickToMenuClose() }
|
|
>
|
|
<img
|
|
src={ IMAGE_ROOT + '/ico_close.svg' }
|
|
alt="닫기"
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="full-menu-top-nav">
|
|
{
|
|
<FavoriteWrapper
|
|
usingType='menu'
|
|
editMode={ editMode }
|
|
setEditMode={ setEditMode }
|
|
changeMenuId={ changeMenuId }
|
|
setMenuOn={ setMenuOn }
|
|
></FavoriteWrapper>
|
|
}
|
|
</div>
|
|
|
|
<div className="full-menu-keywords-wrap">
|
|
<div
|
|
className="full-menu-keywords"
|
|
ref={ shortBtnScrollRef }
|
|
>
|
|
{
|
|
shortBtns.map((value, index) => (
|
|
<span
|
|
key={ `short-btn-${value.menuName}` }
|
|
className={ `keyword-tag ${(shortBtnIdx === index)? 'active': ''}` }
|
|
onClick={ () => onClickToMenuNavigate(value.menuId, index) }
|
|
>{ value.menuName }</span>
|
|
))
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
className="full-menu-list"
|
|
ref={ scrollRef }
|
|
onScroll={ menuListScroll }
|
|
>
|
|
{ getMenuCategory() }
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</>
|
|
);
|
|
}; |