Files
nice-app-web/src/shared/ui/menu/index.tsx
Jay Sheen edf8ced12c 즐겨찾기 메뉴 편집 기능 개선
- 즐겨찾기가 1개 남았을 때 해제 방지
- 편집 모드에서 메뉴 클릭 기능 비활성화
- 즐겨찾기 변경 시 모든 카테고리의 체크박스 상태 동기화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-24 10:14:16 +09:00

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>
</>
);
};