메인 대시보드에 지정판매소 지도·메뉴검색을 추가한다.
- 카카오 지도(지도 2/3 + 판매소 목록 1/3, 높이 고정·스크롤), 목록 클릭 시 줌인 - 지오코딩 폴백(정밀→도로명→지번→키워드→행정동)으로 마커 표시 - 메뉴검색: 자동완성 드롭다운 + 기본 "최근 방문 메뉴"(localStorage, 뒤로가기/bfcache 갱신) - 메뉴검색 박스 녹색(#009688), 지도와 높이 일치 - resolveLgLabel: 선택 지자체 실제 이름 사용, '(데모)' 문구 제거 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,147 @@ $donutCss = $donutStops !== [] ? implode(', ', $donutStops) : '#e5e7eb 0% 100%';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 지도(2/3) + 메뉴검색(1/3) -->
|
||||
<section class="grid grid-cols-1 lg:grid-cols-3 gap-4 items-stretch">
|
||||
<div class="lg:col-span-2">
|
||||
<?= view('bag/_dashboard_kakao_map', [
|
||||
'kakaoJsKey' => $kakaoJsKey ?? '',
|
||||
'lgLabel' => $lgLabel,
|
||||
'mapShops' => $mapShops ?? [],
|
||||
]) ?>
|
||||
</div>
|
||||
<div class="lg:col-span-1 flex">
|
||||
<!-- 메뉴검색 (자동완성) -->
|
||||
<div class="rounded-xl bg-[#009688] text-white p-4 shadow-sm w-full h-full flex flex-col">
|
||||
<strong class="text-sm font-bold flex items-center gap-1.5"><i class="fa-solid fa-magnifying-glass"></i> 메뉴검색</strong>
|
||||
<div class="relative mt-2" id="menuSearchWrap">
|
||||
<input type="search" id="mainMenuSearch" autocomplete="off" placeholder="메뉴명 입력 (예: 재고, 발주, 통계)"
|
||||
class="w-full rounded-lg px-3 py-2 pr-9 text-sm text-gray-800 border-0 focus:outline-none focus:ring-2 focus:ring-white/50"/>
|
||||
<i class="fa-solid fa-magnifying-glass absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 text-xs pointer-events-none"></i>
|
||||
<!-- 자동완성 목록 -->
|
||||
<ul id="mainMenuSearchList" class="hidden absolute left-0 right-0 top-full mt-1 z-30 max-h-60 overflow-y-auto bg-white text-gray-800 rounded-lg shadow-lg border border-gray-200 py-1"></ul>
|
||||
</div>
|
||||
<!-- 최근 방문 메뉴 (기본 표시) — 높이 고정·스크롤 -->
|
||||
<div id="recentMenus" class="mt-3 overflow-y-auto" style="max-height:105px;"></div>
|
||||
<p class="text-[11px] text-white/70 pt-2 mt-auto shrink-0">메뉴명을 입력하면 자동완성에서 선택하거나, 전체 이름 입력 후 Enter 로 이동합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var FLAT = <?= json_encode($menuFlat ?? [], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
|
||||
var input = document.getElementById('mainMenuSearch');
|
||||
var list = document.getElementById('mainMenuSearchList');
|
||||
if (!input || !list) return;
|
||||
var active = -1, current = [];
|
||||
|
||||
function norm(s) { return String(s || '').toLowerCase().replace(/\s+/g, ''); }
|
||||
function esc(s) { return String(s || '').replace(/</g, '<'); }
|
||||
function pathOf(u) { try { return (new URL(u, location.origin).pathname || '').replace(/\/+$/, '') || '/'; } catch (e) { return u; } }
|
||||
|
||||
// 최근 방문 메뉴 (localStorage 기록을 menuFlat 메뉴로 매핑)
|
||||
function recentMenuList() {
|
||||
var arr = [];
|
||||
try { arr = JSON.parse(localStorage.getItem('jrj_recent_menus') || '[]'); } catch (e) {}
|
||||
if (!Array.isArray(arr)) return [];
|
||||
var out = [], seen = {};
|
||||
arr.forEach(function (x) {
|
||||
var p = x && x.p;
|
||||
if (!p) return;
|
||||
var m = FLAT.find(function (f) { return pathOf(f.url) === p; })
|
||||
|| FLAT.find(function (f) { var fp = pathOf(f.url); return p.indexOf(fp + '/') === 0; });
|
||||
if (m && !seen[m.url]) { seen[m.url] = 1; out.push(m); }
|
||||
});
|
||||
return out.slice(0, 8);
|
||||
}
|
||||
|
||||
function renderRecent() {
|
||||
var box = document.getElementById('recentMenus');
|
||||
if (!box) return;
|
||||
var r = recentMenuList();
|
||||
if (!r.length) {
|
||||
box.innerHTML = '<p class="text-[11px] text-white/55 mt-1">최근 방문한 메뉴가 여기에 표시됩니다.</p>';
|
||||
return;
|
||||
}
|
||||
var html = '<p class="text-[11px] text-white/70 mb-1.5"><i class="fa-regular fa-clock mr-1"></i>최근 방문 메뉴</p>';
|
||||
r.forEach(function (m) {
|
||||
html += '<a href="' + m.url + '" class="block text-[12px] px-2 py-1.5 rounded bg-white/12 hover:bg-white/25 mb-1 truncate" title="' + esc(m.name) + '">' + esc(m.name) +
|
||||
(m.parent ? ' <span class="text-white/55">· ' + esc(m.parent) + '</span>' : '') + '</a>';
|
||||
});
|
||||
box.innerHTML = html;
|
||||
}
|
||||
|
||||
function matches(q) {
|
||||
var nq = norm(q);
|
||||
if (!nq) return [];
|
||||
return FLAT.filter(function (m) {
|
||||
return norm(m.name).indexOf(nq) !== -1 || norm(m.parent + m.name).indexOf(nq) !== -1;
|
||||
}).slice(0, 10);
|
||||
}
|
||||
|
||||
function render(q) {
|
||||
current = matches(q); active = -1;
|
||||
list.innerHTML = '';
|
||||
if (!current.length) { list.classList.add('hidden'); return; }
|
||||
current.forEach(function (m, i) {
|
||||
var li = document.createElement('li');
|
||||
li.setAttribute('data-url', m.url);
|
||||
li.setAttribute('data-i', i);
|
||||
li.className = 'px-3 py-2 text-sm cursor-pointer hover:bg-blue-50 flex flex-col';
|
||||
li.innerHTML = '<span class="font-semibold text-gray-800">' +
|
||||
(m.name || '').replace(/</g, '<') + '</span>' +
|
||||
(m.parent ? '<span class="text-[11px] text-gray-400">' + m.parent.replace(/</g, '<') + '</span>' : '');
|
||||
list.appendChild(li);
|
||||
});
|
||||
list.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function go(url) { if (url) window.location.href = url; }
|
||||
|
||||
function highlight() {
|
||||
Array.prototype.forEach.call(list.children, function (li, i) {
|
||||
li.classList.toggle('bg-blue-50', i === active);
|
||||
});
|
||||
}
|
||||
|
||||
input.addEventListener('input', function () { render(input.value); });
|
||||
input.addEventListener('focus', function () { if (input.value.trim()) render(input.value); });
|
||||
|
||||
input.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowDown') { e.preventDefault(); if (current.length) { active = (active + 1) % current.length; highlight(); } return; }
|
||||
if (e.key === 'ArrowUp') { e.preventDefault(); if (current.length) { active = (active - 1 + current.length) % current.length; highlight(); } return; }
|
||||
if (e.key === 'Escape') { list.classList.add('hidden'); return; }
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
var q = input.value.trim();
|
||||
if (active >= 0 && current[active]) { go(current[active].url); return; }
|
||||
// 전체 이름 정확히 일치 우선
|
||||
var exact = FLAT.filter(function (m) { return norm(m.name) === norm(q); });
|
||||
if (exact.length) { go(exact[0].url); return; }
|
||||
if (current.length) { go(current[0].url); return; }
|
||||
var any = matches(q);
|
||||
if (any.length) { go(any[0].url); return; }
|
||||
alert('일치하는 메뉴가 없습니다.');
|
||||
}
|
||||
});
|
||||
|
||||
list.addEventListener('mousedown', function (e) {
|
||||
var li = e.target.closest('li[data-url]');
|
||||
if (li) { e.preventDefault(); go(li.getAttribute('data-url')); }
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!document.getElementById('menuSearchWrap').contains(e.target)) { list.classList.add('hidden'); }
|
||||
});
|
||||
|
||||
renderRecent(); // 기본: 최근 방문 메뉴 표시
|
||||
// 뒤로가기(bfcache 복원)·탭 복귀 시에도 최근 목록 갱신
|
||||
window.addEventListener('pageshow', renderRecent);
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) renderRecent(); });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<section class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<?php if ($stockMix !== []): ?>
|
||||
<!-- 재고 구성 -->
|
||||
|
||||
Reference in New Issue
Block a user