기본 메뉴 구현¶
메뉴 시스템 이해하기¶
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;
}
});
});