지정판매소 소메뉴 활성 상태를 단일 선택으로 보정

지정판매소 관련 형제 소메뉴가 동시에 활성화되던 문제를 해결하고, bag/admin 레이아웃 모두에서 현재 경로 기준으로 가장 구체적인 하위 메뉴 하나만 활성화되도록 통일했다.

Made-with: Cursor
This commit is contained in:
taekyoungc
2026-04-14 11:59:33 +09:00
parent 5d733ac0d8
commit 40db578e85
4 changed files with 99 additions and 17 deletions

View File

@@ -475,6 +475,68 @@ if (! function_exists('site_nav_link_matches_current')) {
}
}
if (! function_exists('menu_active_child_for_parent')) {
/**
* 같은 부모 아래 형제 소메뉴 중, 현재 요청에 해당하는 항목을 하나만 고른다.
*
* 짧은 mm_link(예: bag/designated-shops)가 긴 경로(bag/designated-shops/browse)와
* 동시에 prefix 규칙으로 매칭될 때, 가장 구체적인 경로(일치한 후보 문자열 길이 최대)만 활성으로 본다.
* 길이가 같으면 mm_num이 작은 항목을 선택(동일 URL이 여러 메뉴에 매핑된 경우 등).
*
* @param object{children?: array<int, object>} $parentNavItem
* @param list<string> $dashboardPathAliases
*
* @return object|null 활성으로 표시할 자식 노드(mm_idx 등 포함), 없으면 null
*/
function menu_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object
{
$children = $parentNavItem->children ?? [];
if ($children === []) {
return null;
}
$best = null;
$bestLen = -1;
$bestMmNum = PHP_INT_MAX;
foreach ($children as $child) {
$mmLink = $child->mm_link ?? null;
$maxLen = -1;
foreach (menu_link_candidate_paths($mmLink, $currentPath) as $cand) {
if (menu_single_path_matches_request($cand, $currentPath, $dashboardPathAliases)) {
$maxLen = max($maxLen, strlen($cand));
}
}
if ($maxLen < 0) {
continue;
}
$mmNum = (int) ($child->mm_num ?? 0);
if ($maxLen > $bestLen || ($maxLen === $bestLen && $mmNum < $bestMmNum)) {
$bestLen = $maxLen;
$bestMmNum = $mmNum;
$best = $child;
}
}
return $best;
}
}
if (! function_exists('site_nav_active_child_for_parent')) {
/**
* 사이트 상단 메뉴 전용 호환 래퍼.
*
* @param object{children?: array<int, object>} $parentNavItem
* @param list<string> $dashboardPathAliases
*
* @return object|null
*/
function site_nav_active_child_for_parent(object $parentNavItem, string $currentPath, array $dashboardPathAliases = []): ?object
{
return menu_active_child_for_parent($parentNavItem, $currentPath, $dashboardPathAliases);
}
}
if (! function_exists('session_user_nav_display')) {
/**
* 상단 메뉴바용: 로그인 사용자 이름·역할 표시

View File

@@ -93,14 +93,10 @@ body { overflow: hidden; }
<?php
$hasChildren = ! empty($navItem->children);
$parentLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
$activeChild = $hasChildren ? menu_active_child_for_parent($navItem, $currentPath, []) : null;
$parentIsCurrent = $adminNavItemIsCurrent($navItem->mm_link ?? null);
if (! $parentIsCurrent && $hasChildren) {
foreach ($navItem->children as $ch) {
if ($adminNavItemIsCurrent($ch->mm_link ?? null)) {
$parentIsCurrent = true;
break;
}
}
if (! $parentIsCurrent && $activeChild !== null) {
$parentIsCurrent = true;
}
?>
<div class="relative group">
@@ -115,7 +111,8 @@ body { overflow: hidden; }
<?php foreach ($navItem->children as $child): ?>
<?php
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
$childIsCurrent = $adminNavItemIsCurrent($child->mm_link ?? null);
$childIsCurrent = $activeChild !== null
&& (int) ($child->mm_idx ?? 0) === (int) ($activeChild->mm_idx ?? -1);
?>
<?php if ($childLink !== ''): ?>
<a href="<?= base_url($childLink) ?>"

View File

@@ -71,14 +71,12 @@ body { overflow: hidden; }
<?php foreach ($siteNavTree as $navItem): ?>
<?php
$navLink = menu_link_preferred_href_path($navItem->mm_link ?? null, $currentPath);
$activeChild = ! empty($navItem->children)
? menu_active_child_for_parent($navItem, $currentPath, $dashboardPathAliases)
: null;
$isActive = site_nav_link_matches_current($navItem->mm_link ?? null, $currentPath, $dashboardPathAliases);
if (! $isActive && ! empty($navItem->children)) {
foreach ($navItem->children as $ch) {
if (site_nav_link_matches_current($ch->mm_link ?? null, $currentPath, $dashboardPathAliases)) {
$isActive = true;
break;
}
}
if (! $isActive && $activeChild !== null) {
$isActive = true;
}
?>
<div class="relative group">
@@ -93,7 +91,8 @@ body { overflow: hidden; }
<?php foreach ($navItem->children as $child): ?>
<?php
$childLink = menu_link_preferred_href_path($child->mm_link ?? null, $currentPath);
$childCurrent = menu_link_matches_request($child->mm_link ?? null, $currentPath, $dashboardPathAliases);
$childCurrent = $activeChild !== null
&& (int) ($child->mm_idx ?? 0) === (int) ($activeChild->mm_idx ?? -1);
?>
<?php if ($childLink !== ''): ?>
<a href="<?= base_url($childLink) ?>"

View File

@@ -41,7 +41,31 @@ test.describe('관리자 패널 — 지자체관리자', () => {
test('지정판매소 목록 접근', async ({ page }) => {
await page.goto('/bag/designated-shops');
await expect(page).toHaveURL(/\/admin\/designated-shops/);
await expect(page).toHaveURL(/\/bag\/designated-shops$/);
await expect(page.getByText('지정판매소 관리').first()).toBeVisible();
await expect(page.getByRole('link', { name: '지정판매소 등록' })).toBeVisible();
});
test('지정판매소 조회 전용(browse) 접근', async ({ page }) => {
await page.goto('/bag/designated-shops/browse');
await expect(page).toHaveURL(/\/bag\/designated-shops\/browse/);
await expect(page.getByText('지정판매소 조회').first()).toBeVisible();
await expect(page.getByRole('link', { name: '지정판매소 등록' })).toHaveCount(0);
});
test('지정판매소 소메뉴는 현재 경로 1개만 활성화', async ({ page }) => {
const activeSubmenu = page.locator('nav a.text-blue-700.font-semibold.bg-blue-50');
await page.goto('/bag/designated-shops');
await expect(page).toHaveURL(/\/bag\/designated-shops$/);
await expect(activeSubmenu.filter({ hasText: '지정판매소 관리' })).toHaveCount(1);
await expect(activeSubmenu.filter({ hasText: '지정판매소 바코드출력' })).toHaveCount(0);
await expect(activeSubmenu.filter({ hasText: '지정판매소 조회' })).toHaveCount(0);
await page.goto('/bag/designated-shops/browse');
await expect(page).toHaveURL(/\/bag\/designated-shops\/browse/);
await expect(activeSubmenu.filter({ hasText: '지정판매소 조회' })).toHaveCount(1);
await expect(activeSubmenu.filter({ hasText: '지정판매소 관리' })).toHaveCount(0);
});
test('지자체 관리는 Super Admin 전용 — 지자체관리자 접근 시 리다이렉트', async ({ page }) => {