Ramble-FE / components / TopMenuBar / MobileMenu.tsx
MobileMenu.tsx
Raw
import { Ionicons } from "@expo/vector-icons";
import { Link, usePathname } from "expo-router";
import React, { useEffect, useRef } from "react";
import { Animated, Pressable, StyleSheet, Text } from "react-native";
import { colors, commonStyles } from "./styles";
import { HEADER_HEIGHT, MenuItem } from "./types";

interface MobileMenuProps {
    items: readonly MenuItem[];
    isOpen: boolean;
    onToggle: () => void;
    onClose: () => void;
}

export function MobileMenu({ items, isOpen, onToggle, onClose }: MobileMenuProps) {
    const pathname = usePathname();
    const slideAnimation = useRef(new Animated.Value(0)).current;

    const animatedHeight = slideAnimation.interpolate({
        inputRange: [0, 1],
        outputRange: [0, items.length * 60], // 각 메뉴 아이템당 약 60px
    });

    const animatedOpacity = slideAnimation.interpolate({
        inputRange: [0, 1],
        outputRange: [0, 1],
    });

    useEffect(() => {
        Animated.timing(slideAnimation, {
            toValue: isOpen ? 1 : 0,
            duration: isOpen ? 250 : 200,
            useNativeDriver: false,
        }).start(() => {
            if (!isOpen) {
                // 애니메이션 완료 후 상태 동기화는 부모에서 처리
            }
        });
    }, [isOpen, slideAnimation]);

    useEffect(() => {
        if (!isOpen) return;

        const handleOutsideClick = () => {
            onClose();
        };

        if (typeof document !== 'undefined') {
            document.addEventListener('click', handleOutsideClick);
            return () => document.removeEventListener('click', handleOutsideClick);
        }
    }, [isOpen, onClose]);

    return (
        <>
            {/* 모바일 메뉴 아이콘 */}
            <Pressable 
                style={styles.mobileMenuButton} 
                onPress={onToggle}
            >
                <Ionicons
                    name="menu"
                    size={28}
                    color={isOpen ? colors.primary : "black"}
                />
            </Pressable>

            {/* 모바일 메뉴 드롭다운 */}
            <Animated.View 
                style={[
                    styles.mobileDropdown,
                    {
                        height: animatedHeight,
                        opacity: animatedOpacity,
                    }
                ]}
                pointerEvents={isOpen ? 'auto' : 'none'}
            >
                {items.map((item) => (
                    <Link
                        key={item.href}
                        href={item.href}
                        onPress={onClose}
                        style={[
                            styles.mobileMenuItem,
                            pathname === item.href && styles.activeMobileMenuItem,
                        ]}
                    >
                        <Text style={[
                            commonStyles.menuText,
                            pathname === item.href && commonStyles.activeMenuText,
                        ]}>
                            {item.label}
                        </Text>
                    </Link>
                ))}
            </Animated.View>
        </>
    );
}

const styles = StyleSheet.create({
    mobileMenuButton: {
        padding: 10,
    },
    mobileDropdown: {
        position: "absolute",
        top: HEADER_HEIGHT,
        right: 0,
        width: 300,
        backgroundColor: colors.backgroundOverlay,
        borderBottomWidth: 1,
        borderBottomColor: colors.border,
        boxShadow: "0px 2px 10px rgba(0, 0, 0, 0.1)",
        overflow: "hidden",
    },
    mobileMenuItem: {
        height: 60,
        paddingVertical: 16,
        paddingHorizontal: 20,
        borderBottomWidth: 1,
        borderBottomColor: colors.borderLight,
        justifyContent: "center",
    },
    activeMobileMenuItem: {
        backgroundColor: colors.activeBackground,
    },
});