diff --git a/app/Config/Routes.php b/app/Config/Routes.php index cd32c2b..0259e17 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -60,6 +60,7 @@ $routes->get('bag/help', 'Bag::help'); // 사용자 매뉴얼(설명서) — 로그인 사용자 전용 $routes->group('bag', ['filter' => 'loginAuth'], static function ($routes): void { $routes->get('manual', 'Bag::manual'); + $routes->get('manual/search', 'Bag::manualSearch'); // (:segment) 보다 먼저 $routes->get('manual/(:segment)', 'Bag::manualPage/$1'); }); diff --git a/app/Controllers/Bag.php b/app/Controllers/Bag.php index 896eda4..d048fbc 100644 --- a/app/Controllers/Bag.php +++ b/app/Controllers/Bag.php @@ -3588,6 +3588,17 @@ SQL); return redirect()->to($url); } + /** + * 사용자 매뉴얼 전체 검색 (JSON). q 와 일치하는 페이지·스니펫 목록. + */ + public function manualSearch(): \CodeIgniter\HTTP\ResponseInterface + { + $q = (string) ($this->request->getGet('q') ?? ''); + $results = (new \App\Libraries\ManualRenderer())->search($q); + + return $this->response->setJSON(['q' => $q, 'results' => $results]); + } + /** * 사용자 매뉴얼 개별 페이지 (slug = 화이트리스트). 미등록 slug 는 404. */ diff --git a/app/Libraries/ManualRenderer.php b/app/Libraries/ManualRenderer.php index 665d9a2..e0b3f89 100644 --- a/app/Libraries/ManualRenderer.php +++ b/app/Libraries/ManualRenderer.php @@ -58,6 +58,61 @@ class ManualRenderer return $this->config->pages[$slug] ?? null; } + /** + * 페이지 본문을 일반 텍스트로(검색용). 미존재 시 ''. + */ + public function plainText(string $slug): string + { + $html = $this->render($slug); + if ($html === null || $html === '') { + return ''; + } + $text = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + return trim((string) preg_replace('/\s+/u', ' ', $text)); + } + + /** + * 모든 매뉴얼 페이지 본문에서 질의어를 찾아 결과 반환(일치 많은 순). + * + * @return list + */ + public function search(string $q): array + { + $q = trim($q); + if ($q === '' || mb_strlen($q) < 1) { + return []; + } + $needle = mb_strtolower($q); + $out = []; + foreach ($this->pages() as $slug => $page) { + $text = $this->plainText((string) $slug); + if ($text === '') { + continue; + } + $hay = mb_strtolower($text); + $pos = mb_strpos($hay, $needle); + if ($pos === false) { + continue; + } + $hits = mb_substr_count($hay, $needle); + $start = max(0, $pos - 30); + $snippet = mb_substr($text, $start, 100); + if ($start > 0) { + $snippet = '…' . $snippet; + } + $out[] = [ + 'slug' => (string) $slug, + 'title' => (string) $page['title'], + 'snippet' => trim($snippet), + 'hits' => $hits, + ]; + } + usort($out, static fn ($a, $b): int => $b['hits'] <=> $a['hits']); + + return $out; + } + /** * slug 페이지를 HTML 로 변환해 반환. 미등록 slug·파일 없음·변환 실패 시 null. */ diff --git a/app/Views/bag/layout/workspace.php b/app/Views/bag/layout/workspace.php index e809b31..d9ea06a 100644 --- a/app/Views/bag/layout/workspace.php +++ b/app/Views/bag/layout/workspace.php @@ -105,6 +105,17 @@ if ($effectiveLgIdx) { catch (e) { return u + (u.indexOf('?') >= 0 ? '&' : '?') + 'embed=1'; } } + var STORE_KEY = 'jrj_ws_tabs'; + function persist() { + try { + var data = { + tabs: order.map(function (id) { return { url: tabs[id].url, title: tabs[id].title }; }), + active: activeId + }; + sessionStorage.setItem(STORE_KEY, JSON.stringify(data)); + } catch (e) {} + } + function activate(id) { if (!tabs[id]) return; activeId = id; @@ -113,6 +124,7 @@ if ($effectiveLgIdx) { tabs[k].btn.classList.toggle('active', k === id); }); if (empty) empty.style.display = 'none'; + persist(); } function reloadTab(id) { @@ -132,6 +144,7 @@ if ($effectiveLgIdx) { var next = order[order.length - 1]; if (next) activate(next); else { activeId = null; if (empty) empty.style.display = 'flex'; } } + persist(); } function openTab(url, title) { @@ -194,17 +207,18 @@ if ($effectiveLgIdx) { }); }); - // 브라우저 전체 새로고침/이탈 시 경고 (기본 대시보드 외 탭이 열려 있을 때) - window.addEventListener('beforeunload', function (e) { - if (order.length > 1) { - e.preventDefault(); - e.returnValue = '열어 둔 탭이 모두 닫힙니다. 계속할까요?'; - return e.returnValue; + // 첫 화면: 세션에 저장된 탭이 있으면 복원(관리자 페이지 등 전체 이동 후 복귀·새로고침 대응), + // 없으면 대시보드 탭 자동 열기 + (function restore() { + var saved = null; + try { saved = JSON.parse(sessionStorage.getItem(STORE_KEY) || 'null'); } catch (e) {} + if (saved && saved.tabs && saved.tabs.length) { + saved.tabs.forEach(function (t) { if (t && t.url) openTab(t.url, t.title); }); + if (saved.active && tabs[saved.active]) activate(saved.active); + return; } - }); - - // 첫 화면: 대시보드 탭 자동 열기 - openTab('', '업무 현황'); + openTab('', '업무 현황'); + })(); })(); diff --git a/app/Views/bag/manual.php b/app/Views/bag/manual.php index 3e61647..29d4d4b 100644 --- a/app/Views/bag/manual.php +++ b/app/Views/bag/manual.php @@ -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') ?? ''); ?>