feat: 기본코드 bag 목록과 관리자 CRUD 분리

- /bag/code-kinds, /bag/code-details/{ck_idx} 조회 (LoginAuthFilter, Roles::canManageCodeMaster)
- admin에서는 종류·세부 목록 제거, 등록·수정·삭제만 유지 후 bag으로 리다이렉트
- 사이트 메뉴·기본코드 링크 SQL, CSV 동기화 스크립트·README 보강
- 관리자 대시보드: 발주·판매 테이블 미존재 시 통계 비활성화
- 회원 로그인 잠금(mb_login_fail_count, mb_locked_until) 및 관리자 잠금 해제

Made-with: Cursor
This commit is contained in:
taekyoungc
2026-03-30 15:07:09 +09:00
parent de8f631ca8
commit ab40a90f69
32 changed files with 1026 additions and 704 deletions

View File

@@ -1,6 +1,6 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex items-center gap-2">
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">&larr; <?= esc($kind->ck_name) ?></a>
<a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">&larr; <?= esc($kind->ck_name) ?></a>
<span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">세부코드 등록</span>
</div>
@@ -32,7 +32,7 @@
<div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx) ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
<a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div>
</form>
</div>

View File

@@ -1,6 +1,6 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex items-center gap-2">
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">&larr; <?= esc($kind->ck_name) ?></a>
<a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="text-blue-600 hover:underline text-sm">&larr; <?= esc($kind->ck_name) ?></a>
<span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">세부코드 수정</span>
</div>
@@ -39,7 +39,7 @@
<div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx) ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
<a href="<?= base_url('bag/code-details/' . (int) $kind->ck_idx) ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div>
</form>
</div>

View File

@@ -1,49 +0,0 @@
<?= view('components/print_header', ['printTitle' => '세부코드 관리 - ' . esc($kind->ck_name)]) ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<div class="flex items-center gap-2">
<a href="<?= base_url('admin/code-kinds') ?>" class="text-blue-600 hover:underline text-sm">&larr; 코드 종류</a>
<span class="text-gray-400">|</span>
<span class="text-sm font-bold text-gray-700">세부코드 — <?= esc($kind->ck_name) ?> (<?= esc($kind->ck_code) ?>)</span>
</div>
<div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx . '/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">세부코드 등록</a>
</div>
</div>
</section>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th class="w-16">번호</th>
<th class="w-24">코드</th>
<th>코드명</th>
<th class="w-20">정렬순서</th>
<th class="w-20">상태</th>
<th>등록일</th>
<th class="w-28">작업</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($list as $row): ?>
<tr>
<td class="text-center"><?= esc($row->cd_idx) ?></td>
<td class="text-center font-mono"><?= esc($row->cd_code) ?></td>
<td class="text-left pl-2"><?= esc($row->cd_name) ?></td>
<td class="text-center"><?= (int) $row->cd_sort ?></td>
<td class="text-center"><?= (int) $row->cd_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-left pl-2"><?= esc($row->cd_regdate ?? '') ?></td>
<td class="text-center">
<a href="<?= base_url('admin/code-details/edit/' . (int) $row->cd_idx) ?>" class="text-blue-600 hover:underline text-sm">수정</a>
<form action="<?= base_url('admin/code-details/delete/' . (int) $row->cd_idx) ?>" method="POST" class="inline ml-1" onsubmit="return confirm('이 세부코드를 삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>

View File

@@ -17,7 +17,7 @@
<div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">등록</button>
<a href="<?= base_url('admin/code-kinds') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
<a href="<?= base_url('bag/code-kinds') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div>
</form>
</div>

View File

@@ -25,7 +25,7 @@
<div class="flex gap-2 pt-2">
<button type="submit" class="bg-btn-search text-white px-6 py-1.5 rounded-sm text-sm shadow hover:opacity-90 transition">수정</button>
<a href="<?= base_url('admin/code-kinds') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
<a href="<?= base_url('bag/code-kinds') ?>" class="bg-gray-200 text-gray-700 px-6 py-1.5 rounded-sm text-sm hover:bg-gray-300 transition">취소</a>
</div>
</form>
</div>

View File

@@ -1,48 +0,0 @@
<?= view('components/print_header', ['printTitle' => '기본코드 종류 관리']) ?>
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<span class="text-sm font-bold text-gray-700">기본코드 종류 관리</span>
<div class="flex items-center gap-2">
<button onclick="window.print()" class="no-print border border-btn-print-border text-gray-600 px-3 py-1 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
<a href="<?= base_url('admin/code-kinds/create') ?>" class="bg-btn-search text-white px-4 py-1.5 rounded-sm flex items-center gap-1 text-sm shadow hover:opacity-90 transition border border-transparent">코드 종류 등록</a>
</div>
</div>
</section>
<div class="border border-gray-300 overflow-auto mt-2">
<table class="w-full data-table">
<thead>
<tr>
<th class="w-16">번호</th>
<th class="w-20">코드</th>
<th>코드명</th>
<th class="w-24">세부코드 수</th>
<th class="w-20">상태</th>
<th>등록일</th>
<th class="w-40">작업</th>
</tr>
</thead>
<tbody class="text-right">
<?php foreach ($list as $row): ?>
<tr>
<td class="text-center"><?= esc($row->ck_idx) ?></td>
<td class="text-center font-mono font-bold"><?= esc($row->ck_code) ?></td>
<td class="text-left pl-2"><?= esc($row->ck_name) ?></td>
<td class="text-center">
<a href="<?= base_url('admin/code-details/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline"><?= (int) ($countMap[$row->ck_idx] ?? 0) ?>개</a>
</td>
<td class="text-center"><?= (int) $row->ck_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-left pl-2"><?= esc($row->ck_regdate ?? '') ?></td>
<td class="text-center">
<a href="<?= base_url('admin/code-details/' . (int) $row->ck_idx) ?>" class="text-green-600 hover:underline text-sm mr-1">세부코드</a>
<a href="<?= base_url('admin/code-kinds/edit/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline text-sm mr-1">수정</a>
<form action="<?= base_url('admin/code-kinds/delete/' . (int) $row->ck_idx) ?>" method="POST" class="inline" onsubmit="return confirm('이 코드 종류를 삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline text-sm">삭제</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pager)): ?><div class="mt-3"><?= $pager->links() ?></div><?php endif; ?>

View File

@@ -6,6 +6,15 @@
</div>
<?php else: ?>
<?php if (! empty($s['stats_unavailable'])): ?>
<div class="border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900 mb-4">
발주·판매·재고·불출 통계 테이블이 아직 없거나 조회에 실패했습니다. MySQL에서
<code class="text-xs bg-white px-1 rounded">writable/database/order_tables.sql</code>과
<code class="text-xs bg-white px-1 rounded">writable/database/sales_tables.sql</code>을
<code class="text-xs bg-white px-1 rounded">jongryangje_dev</code> DB에 실행해 주세요.
</div>
<?php endif; ?>
<!-- 통계 카드 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
<div class="border border-gray-300 p-4 bg-white">

View File

@@ -1,6 +1,19 @@
<section class="border-b border-gray-300 p-2 shrink-0 bg-control-panel">
<span class="text-sm font-bold text-gray-700">회원 수정</span>
</section>
<?php
$editLockUntil = $member->mb_locked_until ?? null;
$editLoginLocked = $editLockUntil !== null && $editLockUntil !== '' && strtotime((string) $editLockUntil) > time();
?>
<?php if ($editLoginLocked): ?>
<div class="mt-2 p-3 bg-amber-50 border border-amber-200 text-sm text-amber-900 flex flex-wrap items-center gap-3">
<span>비밀번호 오류로 로그인이 잠겨 있습니다. (~ <?= esc(date('Y-m-d H:i', strtotime((string) $editLockUntil))) ?>)</span>
<form action="<?= base_url('admin/users/unlock-login/' . $member->mb_idx) ?>" method="POST" class="inline" onsubmit="return confirm('로그인 잠금을 해제할까요?');">
<?= csrf_field() ?>
<button type="submit" class="bg-amber-700 text-white px-3 py-1 rounded-sm text-sm hover:opacity-90">잠금 해제</button>
</form>
</div>
<?php endif; ?>
<div class="border border-gray-300 p-4 mt-2 bg-white max-w-xl">
<form action="<?= base_url('admin/users/update/' . $member->mb_idx) ?>" method="POST" class="space-y-4">
<?= csrf_field() ?>

View File

@@ -18,6 +18,7 @@
<th>이메일</th>
<th>역할</th>
<th>상태</th>
<th>로그인 잠금</th>
<th>가입일</th>
<th>관리</th>
</tr>
@@ -42,9 +43,27 @@
}
?>
</td>
<td class="text-left pl-2 text-sm">
<?php
$until = $row->mb_locked_until ?? null;
$loginLocked = $until !== null && $until !== '' && strtotime((string) $until) > time();
if ($loginLocked) {
echo '잠금~' . esc(date('Y-m-d H:i', strtotime((string) $until)));
} else {
$fail = (int) ($row->mb_login_fail_count ?? 0);
echo $fail > 0 ? '실패 ' . $fail . '회' : '—';
}
?>
</td>
<td class="text-left pl-2"><?= esc($row->mb_regdate ?? '') ?></td>
<td class="text-center">
<?php if ((int) $row->mb_state !== 0): ?>
<?php if ($loginLocked): ?>
<form action="<?= base_url('admin/users/unlock-login/' . $row->mb_idx) ?>" method="POST" class="inline mr-1" onsubmit="return confirm('로그인 잠금을 해제할까요?');">
<?= csrf_field() ?>
<button type="submit" class="text-amber-700 hover:underline">잠금해제</button>
</form>
<?php endif; ?>
<a href="<?= base_url('admin/users/edit/' . $row->mb_idx) ?>" class="text-blue-600 hover:underline">수정</a>
<form action="<?= base_url('admin/users/delete/' . $row->mb_idx) ?>" method="POST" class="inline ml-1" onsubmit="return confirm('탈퇴 처리하시겠습니까?');">
<?= csrf_field() ?>

View File

@@ -1,30 +1,8 @@
<div class="space-y-6">
<!-- 기본코드 종류 -->
<section>
<div class="flex items-center justify-between mb-2 border-b pb-1">
<h3 class="text-base font-bold text-gray-700">기본코드 종류</h3>
<a href="<?= base_url('admin/code-kinds') ?>" class="text-blue-600 hover:underline text-sm">관리 &rarr;</a>
</div>
<table class="data-table">
<thead><tr>
<th class="w-16">번호</th><th>코드</th><th>코드명</th><th>상태</th>
</tr></thead>
<tbody>
<?php if (! empty($codeKinds)): ?>
<?php foreach ($codeKinds as $i => $row): ?>
<tr>
<td class="text-center"><?= $i + 1 ?></td>
<td class="text-center"><?= esc($row->ck_code) ?></td>
<td><?= esc($row->ck_name) ?></td>
<td class="text-center"><?= ($row->ck_status ?? 'active') === 'active' ? '사용' : '미사용' ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="4" class="text-center text-gray-400 py-4">등록된 코드 종류가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
<p class="text-sm text-gray-600">
기본코드 종류·세부코드는 상단 메뉴 <strong class="font-medium text-gray-800">기본정보관리</strong>
<a href="<?= base_url('bag/code-kinds') ?>" class="text-blue-600 hover:underline">기본코드관리</a>에서 확인할 있습니다.
</p>
<!-- 봉투 단가 -->
<section>

View File

@@ -0,0 +1,65 @@
<?php
/** @var object $kind */
/** @var list<object> $list */
/** @var bool $canManage */
$canManage = ! empty($canManage);
?>
<div class="space-y-3">
<?= view('components/print_header', ['printTitle' => '세부코드 - ' . esc($kind->ck_name)]) ?>
<section class="border border-gray-300 rounded bg-control-panel p-2">
<div class="flex flex-wrap items-center justify-between gap-y-2">
<div class="flex flex-wrap items-center gap-2 text-sm">
<a href="<?= base_url('bag/code-kinds') ?>" class="text-blue-600 hover:underline">&larr; 기본코드 종류</a>
<span class="text-gray-400">|</span>
<span class="font-bold text-gray-700"><?= $canManage ? '세부코드 관리' : '세부코드 조회' ?> — <?= esc($kind->ck_name) ?> (<?= esc($kind->ck_code) ?>)</span>
</div>
<div class="flex items-center gap-2">
<button type="button" onclick="window.print()" class="no-print rounded border border-gray-300 px-3 py-1 text-sm text-gray-600 hover:bg-gray-50">인쇄</button>
<?php if ($canManage): ?>
<a href="<?= base_url('admin/code-details/' . (int) $kind->ck_idx . '/create') ?>" class="rounded border border-transparent bg-[#1c4e80] px-3 py-1.5 text-sm text-white shadow hover:opacity-90">세부코드 등록</a>
<?php endif; ?>
</div>
</div>
</section>
<div class="border border-gray-300 overflow-auto">
<table class="data-table w-full">
<thead>
<tr>
<th class="w-16">번호</th>
<th class="w-24">코드</th>
<th>코드명</th>
<th class="w-20">정렬순서</th>
<th class="w-20">상태</th>
<th>등록일</th>
<?php if ($canManage): ?>
<th class="w-28">작업</th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($list as $row): ?>
<tr>
<td class="text-center"><?= esc((string) $row->cd_idx) ?></td>
<td class="text-center font-mono"><?= esc($row->cd_code) ?></td>
<td><?= esc($row->cd_name) ?></td>
<td class="text-center"><?= (int) $row->cd_sort ?></td>
<td class="text-center"><?= (int) $row->cd_state === 1 ? '사용' : '미사용' ?></td>
<td class="text-left"><?= esc($row->cd_regdate ?? '') ?></td>
<?php if ($canManage): ?>
<td class="text-center text-sm">
<a href="<?= base_url('admin/code-details/edit/' . (int) $row->cd_idx) ?>" class="text-blue-600 hover:underline">수정</a>
<form action="<?= base_url('admin/code-details/delete/' . (int) $row->cd_idx) ?>" method="POST" class="ml-1 inline" onsubmit="return confirm('이 세부코드를 삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline">삭제</button>
</form>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pager) && $pager !== null): ?>
<div class="mt-3"><?= $pager->links() ?></div>
<?php endif; ?>
</div>

View File

@@ -0,0 +1,62 @@
<?php
/** @var list<object> $codeKinds */
/** @var array<int,int> $countMap */
/** @var bool $canManage */
$canManage = ! empty($canManage);
$colCount = $canManage ? 7 : 6;
?>
<div class="space-y-4">
<section>
<div class="flex flex-wrap items-center justify-between gap-2 mb-2 border-b pb-1">
<h3 class="text-base font-bold text-gray-700">기본코드 종류</h3>
<div class="flex flex-wrap items-center gap-2 text-xs sm:text-sm">
<?php if ($canManage): ?>
<a href="<?= base_url('admin/code-kinds/create') ?>" class="inline-flex items-center rounded bg-[#1c4e80] px-3 py-1.5 text-white shadow hover:opacity-90">코드 종류 등록</a>
<?php else: ?>
<span class="text-gray-500">세부코드는 행의 링크에서 조회할 수 있습니다.</span>
<?php endif; ?>
</div>
</div>
<table class="data-table">
<thead><tr>
<th class="w-14"><?= $canManage ? 'PK' : '번호' ?></th>
<th class="w-24">코드</th>
<th>코드명</th>
<th class="w-28">세부코드</th>
<th class="w-20">상태</th>
<th class="w-40">등록일</th>
<?php if ($canManage): ?>
<th class="w-44">작업</th>
<?php endif; ?>
</tr></thead>
<tbody>
<?php if (! empty($codeKinds)): ?>
<?php $i = 0; foreach ($codeKinds as $row): $i++; ?>
<tr>
<td class="text-center"><?= $canManage ? esc((string) $row->ck_idx) : (string) $i ?></td>
<td class="text-center font-mono"><?= esc($row->ck_code) ?></td>
<td><?= esc($row->ck_name) ?></td>
<td class="text-center">
<a href="<?= base_url('bag/code-details/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline"><?= (int) ($countMap[$row->ck_idx] ?? 0) ?>개 보기</a>
</td>
<td class="text-center"><?= (int) ($row->ck_state ?? 0) === 1 ? '사용' : '미사용' ?></td>
<td class="text-left"><?= esc($row->ck_regdate ?? '') ?></td>
<?php if ($canManage): ?>
<td class="text-center text-sm">
<a href="<?= base_url('bag/code-details/' . (int) $row->ck_idx) ?>" class="text-green-600 hover:underline mr-1">세부코드</a>
<a href="<?= base_url('admin/code-kinds/edit/' . (int) $row->ck_idx) ?>" class="text-blue-600 hover:underline mr-1">수정</a>
<form action="<?= base_url('admin/code-kinds/delete/' . (int) $row->ck_idx) ?>" method="POST" class="inline" onsubmit="return confirm('이 코드 종류를 삭제하시겠습니까?');">
<?= csrf_field() ?>
<button type="submit" class="text-red-600 hover:underline">삭제</button>
</form>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr><td colspan="<?= (string) $colCount ?>" class="text-center text-gray-400 py-4">등록된 코드 종류가 없습니다.</td></tr>
<?php endif; ?>
</tbody>
</table>
</section>
</div>

View File

@@ -64,7 +64,7 @@ body { overflow: hidden; }
</head>
<body class="bg-gray-100 text-gray-800 flex flex-col h-screen font-sans antialiased select-none">
<!-- BEGIN: Top Navigation -->
<header class="bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-20">
<header class="relative bg-white border-b border-gray-300 h-12 flex items-center justify-between px-4 shrink-0 z-50">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<div class="w-6 h-6 flex items-center justify-center shrink-0">
@@ -78,20 +78,49 @@ body { overflow: hidden; }
<nav class="hidden md:flex gap-5 text-sm font-medium text-gray-600">
<?php if (! empty($siteNavTree)): ?>
<?php foreach ($siteNavTree as $navItem): ?>
<?php $isActive = ($currentPath === trim((string) $navItem->mm_link, '/')); ?>
<?php
$navLink = trim((string) $navItem->mm_link, '/');
$isActive = ($currentPath === $navLink);
if (! $isActive && ! empty($navItem->children)) {
foreach ($navItem->children as $ch) {
$childPath = trim((string) $ch->mm_link, '/');
if ($currentPath === $childPath) {
$isActive = true;
break;
}
// 기본코드 세부는 메뉴에 직접 링크 없음 → 기본코드관리(bag/code-kinds)와 동일 메뉴군으로 표시
if ($childPath === 'bag/code-kinds' && str_starts_with($currentPath, 'bag/code-details')) {
$isActive = true;
break;
}
}
}
?>
<div class="relative group">
<a class="<?= $isActive ? 'text-blue-700 font-bold border-b-2 border-blue-700 pb-3 -mb-3' : 'hover:text-blue-600' ?>"
href="<?= base_url($navItem->mm_link) ?>">
<?= esc($navItem->mm_name) ?>
</a>
<?php if (! empty($navItem->children)): ?>
<div class="absolute left-0 top-full hidden group-hover:block bg-white border border-gray-200 rounded shadow-lg min-w-[10rem] z-30">
<?php /* pt-1: 부모와 패널 사이 호버 끊김 방지. z-50: 제목 바보다 위 */ ?>
<div class="absolute left-0 top-full z-50 hidden pt-1 min-w-[12rem] group-hover:block group-focus-within:block">
<div class="bg-white border border-gray-200 rounded shadow-lg py-1">
<?php foreach ($navItem->children as $child): ?>
<a href="<?= base_url($child->mm_link) ?>"
<?php
$childLink = trim((string) ($child->mm_link ?? ''));
?>
<?php if ($childLink !== ''): ?>
<a href="<?= base_url($childLink) ?>"
class="block px-3 py-1.5 text-sm text-gray-700 hover:bg-blue-50 whitespace-nowrap">
<?= esc($child->mm_name) ?>
</a>
<?php else: ?>
<span class="block px-3 py-1.5 text-sm text-gray-400 cursor-default whitespace-nowrap" title="관리자 메뉴 관리에서 링크를 설정해 주세요">
<?= esc($child->mm_name) ?>
</span>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>