기본 메뉴 구현

기본 메뉴 구현

메뉴 시스템 이해하기

Rhymix/XE의 메뉴는 계층 구조로 되어 있으며, 최대 3단계까지 지원합니다.

메뉴 구조

메인 메뉴 (1차)
├── 서브 메뉴 (2차)
│   └── 하위 메뉴 (3차)
└── 서브 메뉴 (2차)

기본 메뉴 출력

단순한 1단 메뉴

<nav class="main-menu">
    <ul>
        <li loop="$main_menu->list => $key, $val" class="active"|cond="$val['selected']">
            <a href="{$val['href']}" target="_blank"|cond="$val['open_window'] == 'Y'">
                {$val['link']}
            </a>
        </li>
    </ul>
</nav>

2단 드롭다운 메뉴

<nav class="dropdown-menu">
    <ul class="nav-level-1">
        <li loop="$main_menu->list => $key1, $val1" 
            class="has-dropdown {$val1['selected'] ? 'active' : ''}"|cond="$val1['list']">

            <a href="{$val1['href']}" target="_blank"|cond="$val1['open_window'] == 'Y'">
                {$val1['link']}
                <span class="arrow" cond="$val1['list']">▼</span>
            </a>

            <!-- 2차 메뉴 -->
            <ul class="nav-level-2" cond="$val1['list']">
                <li loop="$val1['list'] => $key2, $val2" 
                    class="{$val2['selected'] ? 'active' : ''}">
                    <a href="{$val2['href']}">{$val2['link']}</a>
                </li>
            </ul>
        </li>
    </ul>
</nav>

3단 메가 메뉴

<nav class="mega-menu">
    <ul class="level-1">
        <li loop="$main_menu->list => $key1, $val1" class="has-mega"|cond="$val1['list']">
            <a href="{$val1['href']}">{$val1['link']}</a>

            <!-- 메가 메뉴 패널 -->
            <div class="mega-panel" cond="$val1['list']">
                <div class="container">
                    <div class="row">
                        <!-- 2차 메뉴별 컬럼 -->
                        <div class="col" loop="$val1['list'] => $key2, $val2">
                            <h3>
                                <a href="{$val2['href']}">{$val2['link']}</a>
                            </h3>

                            <!-- 3차 메뉴 -->
                            <ul cond="$val2['list']">
                                <li loop="$val2['list'] => $key3, $val3">
                                    <a href="{$val3['href']}">{$val3['link']}</a>
                                </li>
                            </ul>
                        </div>
                    </div>
                </div>
            </div>
        </li>
    </ul>
</nav>

CSS 스타일링

기본 가로 메뉴

/* 1단 메뉴 */
.main-menu ul {
    display: flex;
    list-style: none;
    margin: 0;
    padding: 0;
}

.main-menu li {
    position: relative;
}

.main-menu a {
    display: block;
    padding: 15px 20px;
    color: #333;
    text-decoration: none;
    transition: all 0.3s;
}

.main-menu a:hover,
.main-menu .active > a {
    color: #007bff;
    background-color: #f8f9fa;
}

/* 2단 드롭다운 */
.dropdown-menu .nav-level-2 {
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 200px;
    background: #fff;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-10px);
    transition: all 0.3s;
}

.dropdown-menu li:hover .nav-level-2 {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.dropdown-menu .nav-level-2 a {
    padding: 10px 20px;
    border-bottom: 1px solid #eee;
}

/* 메가 메뉴 */
.mega-menu .mega-panel {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: #fff;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);
    display: none;
    z-index: 1000;
}

.mega-menu li:hover .mega-panel {
    display: block;
}

.mega-panel .row {
    display: flex;
    padding: 30px 0;
}

.mega-panel .col {
    flex: 1;
    padding: 0 20px;
}

.mega-panel h3 {
    margin: 0 0 15px;
    font-size: 16px;
}

.mega-panel ul {
    list-style: none;
    padding: 0;
}

.mega-panel ul a {
    padding: 5px 0;
    color: #666;
}

JavaScript 인터랙션

드롭다운 메뉴 제어

jQuery(function($) {
    // 마우스 호버 효과
    $('.dropdown-menu > ul > li').hover(
        function() {
            $(this).find('.nav-level-2').stop(true, true).fadeIn(200);
        },
        function() {
            $(this).find('.nav-level-2').stop(true, true).fadeOut(200);
        }
    );

    // 키보드 접근성
    $('.dropdown-menu a').focus(function() {
        $(this).parents('li').addClass('focus');
    }).blur(function() {
        $(this).parents('li').removeClass('focus');
    });

    // 터치 디바이스 대응
    if ('ontouchstart' in window) {
        $('.dropdown-menu > ul > li > a').on('click', function(e) {
            var $parent = $(this).parent();
            if ($parent.hasClass('has-dropdown') && !$parent.hasClass('open')) {
                e.preventDefault();
                $parent.addClass('open').siblings().removeClass('open');
            }
        });
    }
});

메가 메뉴 지연 효과

jQuery(function($) {
    var megaTimer;

    $('.mega-menu > ul > li').hover(
        function() {
            var $this = $(this);
            clearTimeout(megaTimer);

            // 다른 메뉴 닫기
            $('.mega-menu .mega-panel').hide();

            // 현재 메뉴 열기
            $this.find('.mega-panel').show();
        },
        function() {
            var $panel = $(this).find('.mega-panel');

            // 지연 후 닫기
            megaTimer = setTimeout(function() {
                $panel.hide();
            }, 300);
        }
    );

    // 메가 패널에 마우스가 있으면 닫지 않음
    $('.mega-panel').hover(
        function() {
            clearTimeout(megaTimer);
        },
        function() {
            $(this).hide();
        }
    );
});

반응형 메뉴

모바일 햄버거 메뉴

<!-- 모바일 메뉴 토글 버튼 -->
<button class="menu-toggle" aria-label="메뉴 열기">
    <span class="bar"></span>
    <span class="bar"></span>
    <span class="bar"></span>
</button>

<!-- 모바일 메뉴 -->
<nav class="mobile-menu">
    <ul>
        <li loop="$main_menu->list => $key1, $val1">
            <a href="{$val1['href']}" class="menu-link">
                {$val1['link']}
            </a>

            <!-- 서브메뉴 토글 버튼 -->
            <button class="submenu-toggle" cond="$val1['list']" aria-label="서브메뉴 열기">
                <span class="icon">+</span>
            </button>

            <!-- 서브메뉴 -->
            <ul class="submenu" cond="$val1['list']">
                <li loop="$val1['list'] => $key2, $val2">
                    <a href="{$val2['href']}">{$val2['link']}</a>
                </li>
            </ul>
        </li>
    </ul>
</nav>

모바일 메뉴 스크립트

jQuery(function($) {
    // 메뉴 토글
    $('.menu-toggle').click(function() {
        $('body').toggleClass('menu-open');
        $('.mobile-menu').toggleClass('active');
        $(this).toggleClass('active');
    });

    // 서브메뉴 토글
    $('.submenu-toggle').click(function() {
        var $submenu = $(this).siblings('.submenu');
        var $icon = $(this).find('.icon');

        $submenu.slideToggle(300);
        $icon.text($icon.text() === '+' ? '-' : '+');

        // 다른 서브메뉴 닫기
        $(this).parent().siblings().find('.submenu').slideUp(300);
        $(this).parent().siblings().find('.icon').text('+');
    });

    // 메뉴 외부 클릭 시 닫기
    $(document).on('click', function(e) {
        if (!$(e.target).closest('.mobile-menu, .menu-toggle').length) {
            $('body').removeClass('menu-open');
            $('.mobile-menu').removeClass('active');
            $('.menu-toggle').removeClass('active');
        }
    });
});

반응형 CSS

/* 데스크탑 */
@media (min-width: 992px) {
    .menu-toggle,
    .mobile-menu {
        display: none;
    }
}

/* 모바일 */
@media (max-width: 991px) {
    .main-menu,
    .dropdown-menu,
    .mega-menu {
        display: none;
    }

    /* 햄버거 버튼 */
    .menu-toggle {
        display: block;
        background: none;
        border: none;
        padding: 10px;
        cursor: pointer;
    }

    .menu-toggle .bar {
        display: block;
        width: 25px;
        height: 3px;
        background: #333;
        margin: 5px 0;
        transition: all 0.3s;
    }

    .menu-toggle.active .bar:nth-child(1) {
        transform: rotate(45deg) translate(5px, 5px);
    }

    .menu-toggle.active .bar:nth-child(2) {
        opacity: 0;
    }

    .menu-toggle.active .bar:nth-child(3) {
        transform: rotate(-45deg) translate(7px, -6px);
    }

    /* 모바일 메뉴 */
    .mobile-menu {
        position: fixed;
        top: 0;
        right: -300px;
        width: 300px;
        height: 100vh;
        background: #fff;
        box-shadow: -2px 0 10px rgba(0,0,0,0.1);
        transition: right 0.3s;
        overflow-y: auto;
        z-index: 9999;
    }

    .mobile-menu.active {
        right: 0;
    }

    .mobile-menu ul {
        list-style: none;
        padding: 0;
        margin: 0;
    }

    .mobile-menu a {
        display: block;
        padding: 15px 20px;
        border-bottom: 1px solid #eee;
        color: #333;
        text-decoration: none;
    }

    .submenu-toggle {
        position: absolute;
        right: 0;
        top: 0;
        width: 50px;
        height: 50px;
        background: none;
        border: none;
        cursor: pointer;
    }

    .mobile-menu .submenu {
        display: none;
        background: #f8f9fa;
    }

    .mobile-menu .submenu a {
        padding-left: 40px;
    }
}

접근성 개선

ARIA 속성 추가

<nav class="main-menu" role="navigation" aria-label="주 메뉴">
    <ul role="menubar">
        <li loop="$main_menu->list => $key1, $val1" role="none">
            <a href="{$val1['href']}" 
               role="menuitem"
               aria-haspopup="true"|cond="$val1['list']"
               aria-expanded="false"|cond="$val1['list']">
                {$val1['link']}
            </a>

            <ul role="menu" cond="$val1['list']" aria-label="{$val1['link']} 하위 메뉴">
                <li loop="$val1['list'] => $key2, $val2" role="none">
                    <a href="{$val2['href']}" role="menuitem">{$val2['link']}</a>
                </li>
            </ul>
        </li>
    </ul>
</nav>

키보드 내비게이션

// 키보드 접근성 개선
jQuery(function($) {
    var $menu = $('.main-menu');
    var $items = $menu.find('> ul > li');

    // Tab 키 이동
    $items.find('> a').on('keydown', function(e) {
        var $li = $(this).parent();
        var $submenu = $li.find('> ul');

        switch(e.keyCode) {
            case 40: // 아래 화살표
                if ($submenu.length) {
                    e.preventDefault();
                    $submenu.find('a:first').focus();
                }
                break;

            case 27: // ESC
                if ($li.hasClass('open')) {
                    e.preventDefault();
                    $li.removeClass('open');
                    $(this).focus();
                }
                break;
        }
    });
});