[UX] #577 show VPD submenu below primary navigation
This commit is contained in:
@@ -113,10 +113,6 @@ body {
|
||||
gap: .35rem;
|
||||
}
|
||||
|
||||
.rw-menu-group {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rw-menu-trigger {
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
@@ -130,36 +126,38 @@ body {
|
||||
|
||||
.rw-menu-trigger:hover,
|
||||
.rw-menu-trigger:focus,
|
||||
.rw-menu-group.open .rw-menu-trigger {
|
||||
.rw-menu-trigger.active {
|
||||
background: var(--rw-surface-muted);
|
||||
border-color: var(--rw-border);
|
||||
color: var(--rw-text);
|
||||
}
|
||||
|
||||
.rw-menu-panel {
|
||||
background: var(--rw-surface);
|
||||
border: 1px solid var(--rw-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--rw-shadow);
|
||||
display: none;
|
||||
left: 0;
|
||||
min-width: 190px;
|
||||
padding: .35rem;
|
||||
position: absolute;
|
||||
top: calc(100% + .35rem);
|
||||
z-index: 30;
|
||||
.rw-submenu-bar {
|
||||
background: var(--rw-surface-muted);
|
||||
border-top: 1px solid var(--rw-border);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rw-menu-group.open .rw-menu-panel {
|
||||
display: grid;
|
||||
gap: .15rem;
|
||||
}
|
||||
|
||||
.rw-menu-panel .nav-link {
|
||||
border-radius: 8px;
|
||||
color: var(--rw-text);
|
||||
.rw-submenu-bar .container {
|
||||
display: block;
|
||||
padding: .55rem .65rem !important;
|
||||
}
|
||||
|
||||
.rw-submenu {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .2rem;
|
||||
min-height: 2.75rem;
|
||||
padding: .25rem 0;
|
||||
}
|
||||
|
||||
.rw-submenu[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rw-submenu .nav-link {
|
||||
color: var(--rw-text);
|
||||
padding: .4rem .7rem !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -181,14 +179,6 @@ body {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.rw-menu-panel {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.rw-menu-group.open .rw-menu-panel {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.architecture-strip {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1786,10 +1776,6 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rw-menu-group {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rw-menu-trigger {
|
||||
padding-left: .65rem;
|
||||
padding-right: .65rem;
|
||||
|
||||
@@ -73,45 +73,39 @@ function syncPolicyTemplate(select) {
|
||||
);
|
||||
}
|
||||
|
||||
function closeMenuGroup(group) {
|
||||
if (!group) {
|
||||
function initPersistentMenus() {
|
||||
const productNav = document.querySelector('[data-product-nav]');
|
||||
if (!productNav) {
|
||||
return;
|
||||
}
|
||||
group.classList.remove('open');
|
||||
group.querySelector('.rw-menu-trigger')?.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
const submenuBar = productNav.querySelector('[data-submenu-bar]');
|
||||
const triggers = Array.from(productNav.querySelectorAll('[data-submenu-trigger]'));
|
||||
const panels = Array.from(productNav.querySelectorAll('[data-submenu-panel]'));
|
||||
const path = window.location.pathname;
|
||||
|
||||
function closeAllMenuGroups(exceptGroup = null) {
|
||||
document.querySelectorAll('.rw-menu-group.open').forEach((group) => {
|
||||
if (group !== exceptGroup) {
|
||||
closeMenuGroup(group);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initPersistentMenus() {
|
||||
document.querySelectorAll('.rw-menu-trigger').forEach((button) => {
|
||||
button.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const group = button.closest('.rw-menu-group');
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
const nextOpen = !group.classList.contains('open');
|
||||
closeAllMenuGroups(group);
|
||||
group.classList.toggle('open', nextOpen);
|
||||
button.setAttribute('aria-expanded', String(nextOpen));
|
||||
const activate = (target) => {
|
||||
const hasTarget = panels.some((panel) => panel.dataset.submenuPanel === target);
|
||||
submenuBar.hidden = !hasTarget;
|
||||
triggers.forEach((trigger) => {
|
||||
const active = trigger.dataset.submenuTrigger === target;
|
||||
trigger.classList.toggle('active', active);
|
||||
trigger.setAttribute('aria-expanded', String(active));
|
||||
});
|
||||
});
|
||||
document.addEventListener('click', (event) => {
|
||||
if (!event.target.closest('.rw-menu')) {
|
||||
closeAllMenuGroups();
|
||||
}
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeAllMenuGroups();
|
||||
}
|
||||
panels.forEach((panel) => {
|
||||
panel.hidden = panel.dataset.submenuPanel !== target;
|
||||
});
|
||||
};
|
||||
|
||||
const currentPanel = panels.find((panel) => Array.from(panel.querySelectorAll('a[href]')).some((link) => {
|
||||
const href = new URL(link.href, window.location.origin).pathname;
|
||||
return path === href || (href !== '/' && path.startsWith(`${href}/`));
|
||||
}));
|
||||
if (currentPanel) {
|
||||
activate(currentPanel.dataset.submenuPanel);
|
||||
}
|
||||
|
||||
triggers.forEach((trigger) => {
|
||||
trigger.addEventListener('click', () => activate(trigger.dataset.submenuTrigger));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,49 +11,45 @@
|
||||
<script defer src="/js/app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav th:fragment="nav" class="navbar rw-nav navbar-expand-lg">
|
||||
<nav th:fragment="nav" class="navbar rw-nav navbar-expand-lg" data-product-nav>
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">VPD 권한 운영</a>
|
||||
<div class="rw-menu">
|
||||
<div class="rw-menu-group">
|
||||
<button class="rw-menu-trigger" type="button" aria-expanded="false">권한 관리</button>
|
||||
<div class="rw-menu-panel">
|
||||
<a class="nav-link" href="/users">사용자 관리</a>
|
||||
<a class="nav-link" href="/groups">그룹 관리</a>
|
||||
<a class="nav-link" href="/roles">역할 관리</a>
|
||||
<a class="nav-link" href="/permissions">접근 규칙</a>
|
||||
<a class="nav-link" href="/effective-matrix">권한 현황</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rw-menu-group">
|
||||
<button class="rw-menu-trigger" type="button" aria-expanded="false">보호 정책 관리</button>
|
||||
<div class="rw-menu-panel">
|
||||
<a class="nav-link" href="/vpd-policies">보호 상태</a>
|
||||
<a class="nav-link" href="/objects">보호 대상 관리</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rw-menu-group">
|
||||
<button class="rw-menu-trigger" type="button" aria-expanded="false">지식자료 관리</button>
|
||||
<div class="rw-menu-panel">
|
||||
<a class="nav-link" href="/vector-knowledge">지식자료 등록 및 검색</a>
|
||||
<a class="nav-link" href="/mcp-chatbot">대화형 검색</a>
|
||||
<a class="nav-link" href="/mcp-reasoning">검색 해석</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rw-menu-group">
|
||||
<button class="rw-menu-trigger" type="button" aria-expanded="false">접근 검증</button>
|
||||
<div class="rw-menu-panel">
|
||||
<a class="nav-link" href="/tokens">검증 세션</a>
|
||||
<a class="nav-link" href="/probe">접근 검증</a>
|
||||
<a class="nav-link" href="/operation-status">검증 현황</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rw-menu" aria-label="주요 메뉴">
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="access" aria-controls="submenu-access" aria-expanded="false">권한 관리</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="protection" aria-controls="submenu-protection" aria-expanded="false">보호 정책 관리</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="knowledge" aria-controls="submenu-knowledge" aria-expanded="false">지식자료 관리</button>
|
||||
<button class="rw-menu-trigger" type="button" data-submenu-trigger="verification" aria-controls="submenu-verification" aria-expanded="false">접근 검증</button>
|
||||
</div>
|
||||
<form method="post" action="/logout" class="ms-auto">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">로그아웃</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="rw-submenu-bar" data-submenu-bar hidden>
|
||||
<div class="container">
|
||||
<div id="submenu-access" class="rw-submenu" data-submenu-panel="access" role="navigation" aria-label="권한 관리 메뉴" hidden>
|
||||
<a class="nav-link" href="/users">사용자 관리</a>
|
||||
<a class="nav-link" href="/groups">그룹 관리</a>
|
||||
<a class="nav-link" href="/roles">역할 관리</a>
|
||||
<a class="nav-link" href="/permissions">접근 규칙</a>
|
||||
<a class="nav-link" href="/effective-matrix">권한 현황</a>
|
||||
</div>
|
||||
<div id="submenu-protection" class="rw-submenu" data-submenu-panel="protection" role="navigation" aria-label="보호 정책 관리 메뉴" hidden>
|
||||
<a class="nav-link" href="/vpd-policies">보호 상태</a>
|
||||
<a class="nav-link" href="/objects">보호 대상 관리</a>
|
||||
</div>
|
||||
<div id="submenu-knowledge" class="rw-submenu" data-submenu-panel="knowledge" role="navigation" aria-label="지식자료 관리 메뉴" hidden>
|
||||
<a class="nav-link" href="/vector-knowledge">지식자료 등록 및 검색</a>
|
||||
<a class="nav-link" href="/mcp-chatbot">대화형 검색</a>
|
||||
<a class="nav-link" href="/mcp-reasoning">검색 해석</a>
|
||||
</div>
|
||||
<div id="submenu-verification" class="rw-submenu" data-submenu-panel="verification" role="navigation" aria-label="접근 검증 메뉴" hidden>
|
||||
<a class="nav-link" href="/tokens">검증 세션</a>
|
||||
<a class="nav-link" href="/probe">접근 검증</a>
|
||||
<a class="nav-link" href="/operation-status">검증 현황</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -14,6 +14,7 @@ class GuidedFlowTemplateTest {
|
||||
void dashboardPrioritizesActionsAndKeepsDetailCollapsed() throws IOException {
|
||||
String html = template("dashboard.html");
|
||||
String layout = template("fragments/layout.html");
|
||||
String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js"));
|
||||
|
||||
assertThat(html)
|
||||
.contains("th:replace=\"~{fragments/layout :: head('VPD 권한 운영')}\"")
|
||||
@@ -31,7 +32,14 @@ class GuidedFlowTemplateTest {
|
||||
.contains("보호 정책 관리")
|
||||
.contains("접근 검증")
|
||||
.contains("지식자료 관리")
|
||||
.contains("data-submenu-trigger")
|
||||
.contains("data-submenu-bar")
|
||||
.contains("data-submenu-panel")
|
||||
.doesNotContain("VPD Backoffice");
|
||||
assertThat(javascript)
|
||||
.contains("data-submenu-trigger")
|
||||
.contains("data-submenu-panel")
|
||||
.doesNotContain("closeAllMenuGroups");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user