feat: 매뉴얼 검색·소메뉴 아이콘 개선·워크스페이스 탭 세션 유지
- 매뉴얼: 전체 검색 박스(slug별 hit 카운트·스니펫)와 본문 하이라이트 추가 - ManualRenderer::plainText()/search(), Bag::manualSearch(), bag/manual/search 라우트 - 사이드바 소메뉴 선택 아이콘 변경: 닫기처럼 보이던 × → ▸, + → · (정적/동적 일관) - 워크스페이스: 탭 목록을 sessionStorage에 저장·복원 - 관리자 페이지 이동 후 복귀·새로고침해도 열어둔 탭 유지(세션 범위) - 복원으로 무의미해진 beforeunload 새로고침 경고 제거 - e2e: 관리자 이동 후 탭 복원 케이스 추가 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ $slugs = array_keys($pages);
|
||||
$pos = array_search($current, $slugs, true);
|
||||
$prevSlug = ($pos !== false && $pos > 0) ? $slugs[$pos - 1] : null;
|
||||
$nextSlug = ($pos !== false && $pos < count($slugs) - 1) ? $slugs[$pos + 1] : null;
|
||||
$searchQ = (string) (service('request')->getGet('q') ?? '');
|
||||
?>
|
||||
<style>
|
||||
/* 매뉴얼 본문 스코프 스타일 (사이트 레이아웃 Tailwind 에 typography 플러그인 없음) */
|
||||
@@ -54,6 +55,16 @@ $nextSlug = ($pos !== false && $pos < count($slugs) - 1) ? $slugs[$pos + 1] : nu
|
||||
<!-- 좌측 목차 -->
|
||||
<nav class="manual-toc no-print w-56 shrink-0 sticky top-0 self-start">
|
||||
<div class="bg-title-bar text-white text-sm font-bold px-3 py-2 rounded-t">사용자 매뉴얼</div>
|
||||
<!-- 매뉴얼 전체 검색 -->
|
||||
<div class="manual-search" style="position:relative;padding:6px;border:1px solid #d1d5db;border-top:0;background:#fff;">
|
||||
<div style="position:relative;">
|
||||
<input id="manualSearchInput" type="search" autocomplete="off" placeholder="매뉴얼 검색 (예: LOT, 불출)"
|
||||
value="<?= esc($searchQ, 'attr') ?>"
|
||||
class="w-full border border-gray-300 rounded px-2 py-1.5 pr-7 text-xs focus:outline-none focus:ring-2 focus:ring-[#243a5e]/30"/>
|
||||
<i class="fa-solid fa-magnifying-glass" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);color:#9ca3af;font-size:11px;"></i>
|
||||
</div>
|
||||
<ul id="manualSearchResults" class="hidden" style="position:absolute;left:6px;right:6px;top:100%;z-index:40;max-height:300px;overflow-y:auto;background:#fff;border:1px solid #d1d5db;border-radius:6px;box-shadow:0 6px 16px rgba(0,0,0,.12);margin:2px 0 0;padding:0;list-style:none;"></ul>
|
||||
</div>
|
||||
<ul class="border border-t-0 border-gray-300 rounded-b divide-y divide-gray-100 bg-white text-sm">
|
||||
<?php foreach ($pages as $slug => $p): ?>
|
||||
<li>
|
||||
@@ -90,3 +101,75 @@ $nextSlug = ($pos !== false && $pos < count($slugs) - 1) ? $slugs[$pos + 1] : nu
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
// 현재 출처 기준 경로(앱 baseURL 호스트와 접속 호스트가 달라도 동일 출처로 요청)
|
||||
var SEARCH_URL = location.origin + <?= json_encode((string) parse_url(base_url('bag/manual/search'), PHP_URL_PATH), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||
var BASE = location.origin + <?= json_encode((string) parse_url(base_url('bag/manual/'), PHP_URL_PATH), JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
|
||||
var Q0 = <?= json_encode($searchQ, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
|
||||
var input = document.getElementById('manualSearchInput');
|
||||
var box = document.getElementById('manualSearchResults');
|
||||
|
||||
function esc(s) { return String(s || '').replace(/[&<>]/g, function (c) { return { '&': '&', '<': '<', '>': '>' }[c]; }); }
|
||||
|
||||
function doSearch(q) {
|
||||
q = (q || '').trim();
|
||||
if (!q) { box.classList.add('hidden'); box.innerHTML = ''; return; }
|
||||
fetch(SEARCH_URL + '?q=' + encodeURIComponent(q))
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
var res = (d && d.results) || [];
|
||||
if (!res.length) {
|
||||
box.innerHTML = '<li style="padding:7px 10px;color:#9ca3af;font-size:12px;">검색 결과가 없습니다.</li>';
|
||||
box.classList.remove('hidden'); return;
|
||||
}
|
||||
box.innerHTML = res.map(function (m) {
|
||||
return '<li><a href="' + BASE + encodeURIComponent(m.slug) + '?q=' + encodeURIComponent(q) +
|
||||
'" style="display:block;padding:7px 10px;border-bottom:1px solid #f1f5f9;text-decoration:none;">' +
|
||||
'<span style="font-size:12px;font-weight:700;color:#1a2b4b;">' + esc(m.title) +
|
||||
' <span style="color:#94a3b8;font-weight:400;">(' + (m.hits || 1) + ')</span></span>' +
|
||||
'<span style="display:block;font-size:11px;color:#64748b;margin-top:2px;line-height:1.4;">' + esc(m.snippet) + '</span></a></li>';
|
||||
}).join('');
|
||||
box.classList.remove('hidden');
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
if (input) {
|
||||
var timer = null;
|
||||
input.addEventListener('input', function () { clearTimeout(timer); var v = input.value; timer = setTimeout(function () { doSearch(v); }, 250); });
|
||||
input.addEventListener('focus', function () { if (input.value.trim()) doSearch(input.value); });
|
||||
document.addEventListener('click', function (e) { if (!e.target.closest('.manual-search')) box.classList.add('hidden'); });
|
||||
}
|
||||
|
||||
// 현재 페이지 본문에서 검색어 하이라이트
|
||||
if (Q0 && Q0.trim()) {
|
||||
var prose = document.querySelector('.manual-prose');
|
||||
if (prose) {
|
||||
var needle = Q0.trim().toLowerCase();
|
||||
var walker = document.createTreeWalker(prose, NodeFilter.SHOW_TEXT, null);
|
||||
var nodes = [], first = null;
|
||||
while (walker.nextNode()) { nodes.push(walker.currentNode); }
|
||||
nodes.forEach(function (node) {
|
||||
var t = node.nodeValue, lt = t.toLowerCase(), idx = lt.indexOf(needle);
|
||||
if (idx < 0) return;
|
||||
var frag = document.createDocumentFragment(), last = 0;
|
||||
while (idx >= 0) {
|
||||
frag.appendChild(document.createTextNode(t.slice(last, idx)));
|
||||
var mark = document.createElement('mark');
|
||||
mark.textContent = t.slice(idx, idx + needle.length);
|
||||
mark.style.background = '#fde68a';
|
||||
frag.appendChild(mark);
|
||||
if (!first) first = mark;
|
||||
last = idx + needle.length;
|
||||
idx = lt.indexOf(needle, last);
|
||||
}
|
||||
frag.appendChild(document.createTextNode(t.slice(last)));
|
||||
node.parentNode.replaceChild(frag, node);
|
||||
});
|
||||
if (first) { try { first.scrollIntoView({ block: 'center' }); } catch (e) {} }
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user