feat: 화면설명 소제목 스크롤·강조 + 글씨크기 메뉴 확대 + 드로어 개선

- screenHelp 앵커(?hl=)로 '이 화면 설명' 클릭 시 해당 소제목으로 스크롤·강조, 재오픈 시 재강조(postMessage)
- 글씨 크기(A−/A+)가 상단 대메뉴·좌측 사이드바까지 확대, 관리자 페이지에도 조절 기능 추가
- 화면 설명 드로어 양방향 리사이즈(좁히기 가능) + 기본 너비 2배

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
taekyoungc
2026-06-15 13:31:31 +09:00
parent 56dadb3478
commit b9dd24082c
7 changed files with 137 additions and 42 deletions

View File

@@ -40,34 +40,37 @@ class Manual extends BaseConfig
* "이 화면 설명" 버튼이 현재 경로로 알맞은 매뉴얼 페이지를 연다. * "이 화면 설명" 버튼이 현재 경로로 알맞은 매뉴얼 페이지를 연다.
* 더 긴(구체적) 접두가 우선하도록 길이 내림차순으로 매칭한다. * 더 긴(구체적) 접두가 우선하도록 길이 내림차순으로 매칭한다.
* *
* 값에 "slug#소제목힌트" 형식으로 적으면, 매뉴얼 창이 열릴 때 해당 소제목으로
* 자동 스크롤하고 잠시 강조 표시한다(힌트는 그 페이지의 H2/H3 텍스트 일부와 일치).
*
* @var array<string, string> * @var array<string, string>
*/ */
public array $screenHelp = [ public array $screenHelp = [
'bag/order/phone' => 'sales', 'bag/order/phone' => 'sales#전화 주문 접수',
'bag/order' => 'order', 'bag/order' => 'order#발주 등록',
'bag/bag-orders' => 'order', 'bag/bag-orders' => 'order#발주 현황',
'bag/receiving' => 'order', 'bag/receiving' => 'order#입고 처리',
'bag/bag-receivings' => 'order', 'bag/bag-receivings' => 'order#입고 현황',
'bag/inventory' => 'inventory', 'bag/inventory' => 'inventory#재고 현황',
'bag/sale' => 'sales', 'bag/sale' => 'sales#지정판매소 판매',
'bag/sales' => 'sales', 'bag/sales' => 'sales#지정판매소 판매',
'bag/issue' => 'sales', 'bag/issue' => 'sales#무료용 불출 처리',
'bag/bag-issues' => 'sales', 'bag/bag-issues' => 'sales#무료용 불출 처리',
'bag/bag-sales' => 'sales', 'bag/bag-sales' => 'sales#판매/반품 현황',
'bag/shop-orders' => 'sales', 'bag/shop-orders' => 'sales#전화 주문 접수',
'bag/flow' => 'reports', 'bag/flow' => 'reports',
'bag/reports' => 'reports', 'bag/reports' => 'reports#일계표',
'bag/analytics' => 'reports', 'bag/analytics' => 'reports',
'bag/designated-shops' => 'basic', 'bag/designated-shops' => 'basic#지정판매소 관리',
'bag/bag-prices' => 'basic', 'bag/bag-prices' => 'basic#단가 관리',
'bag/prices' => 'basic', 'bag/prices' => 'basic#단가 관리',
'bag/packaging-units' => 'basic', 'bag/packaging-units' => 'basic#포장 단위 관리',
'bag/code-kinds' => 'basic', 'bag/code-kinds' => 'basic#기본코드 관리',
'bag/code-details' => 'basic', 'bag/code-details' => 'basic#기본코드 관리',
'bag/managers' => 'basic', 'bag/managers' => 'basic#그 밖의 기본정보',
'bag/companies' => 'basic', 'bag/companies' => 'basic#그 밖의 기본정보',
'bag/sales-agencies' => 'basic', 'bag/sales-agencies' => 'basic#그 밖의 기본정보',
'bag/free-recipients' => 'basic', 'bag/free-recipients' => 'basic#그 밖의 기본정보',
'bag/number-lookup' => 'codes', 'bag/number-lookup' => 'codes',
]; ];
} }

View File

@@ -950,18 +950,28 @@ if (! function_exists('manual_help_url_for_path')) {
return ''; return '';
} }
$map = config(\Config\Manual::class)->screenHelp ?? []; $map = config(\Config\Manual::class)->screenHelp ?? [];
$bestSlug = ''; $bestVal = '';
$bestLen = -1; $bestLen = -1;
foreach ($map as $prefix => $slug) { foreach ($map as $prefix => $val) {
$p = strtolower((string) $prefix); $p = strtolower((string) $prefix);
if ($path === $p || str_starts_with($path . '/', $p . '/')) { if ($path === $p || str_starts_with($path . '/', $p . '/')) {
if (strlen($p) > $bestLen) { if (strlen($p) > $bestLen) {
$bestLen = strlen($p); $bestLen = strlen($p);
$bestSlug = (string) $slug; $bestVal = (string) $val;
} }
} }
} }
if ($bestVal === '') {
return '';
}
return $bestSlug !== '' ? base_url('bag/manual/' . $bestSlug) : ''; // 값은 "slug" 또는 "slug#소제목힌트" 형식. 힌트가 있으면 ?hl= 로 전달해 해당 소제목으로 스크롤·강조.
[$slug, $hint] = array_pad(explode('#', $bestVal, 2), 2, null);
$url = base_url('bag/manual/' . $slug);
if ($hint !== null && trim($hint) !== '') {
$url .= '?hl=' . rawurlencode(trim($hint));
}
return $url;
} }
} }

View File

@@ -105,6 +105,11 @@ tailwind.config = {
<?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('/')]) ?> <?= view('home/_dashboard_gov_portal_brand', ['brandHref' => base_url('/')]) ?>
<?= view('home/_dashboard_gov_portal_topnav_click', $navPartial) ?> <?= view('home/_dashboard_gov_portal_topnav_click', $navPartial) ?>
<div class="portal-header-utils" style="display:flex;align-items:center;gap:.5rem;"> <div class="portal-header-utils" style="display:flex;align-items:center;gap:.5rem;">
<div class="ws-fontctl" title="글씨 크기 조절" style="display:inline-flex;align-items:center;gap:2px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.25);border-radius:6px;padding:1px;">
<button type="button" id="wsFontMinus" title="글씨 작게" style="width:24px;height:22px;border:0;background:transparent;color:#fff;cursor:pointer;font-size:11px;line-height:1;border-radius:5px;">A</button>
<span id="wsFontPct" style="min-width:34px;text-align:center;color:#fff;font-size:.68rem;font-weight:600;">100%</span>
<button type="button" id="wsFontPlus" title="글씨 크게" style="width:24px;height:22px;border:0;background:transparent;color:#fff;cursor:pointer;font-size:14px;line-height:1;border-radius:5px;">A+</button>
</div>
<span class="user-line"> <span class="user-line">
<?php if ($effectiveLgName !== ''): ?><strong><?= esc($effectiveLgName) ?></strong> · <?php endif; ?> <?php if ($effectiveLgName !== ''): ?><strong><?= esc($effectiveLgName) ?></strong> · <?php endif; ?>
<?= esc($levelName) ?> · <?= esc($mbName) ?>님 <?= esc($levelName) ?> · <?= esc($mbName) ?>님
@@ -161,6 +166,25 @@ tailwind.config = {
window.addEventListener('pageshow', function (e) { if (e.persisted) closeStuckOverlays(); }); window.addEventListener('pageshow', function (e) { if (e.persisted) closeStuckOverlays(); });
window.addEventListener('pagehide', closeStuckOverlays); window.addEventListener('pagehide', closeStuckOverlays);
})(); })();
// 글씨 크기 조절(A/A+) — 본문 + 상단 대메뉴 + 좌측 사이드바에 zoom 적용. 사이트/워크스페이스와 배율 공유.
(function () {
var FONT_KEY = 'jrj_font_scale';
var scaleSelectors = ['.portal-header', '.sidebar', '.work-main'];
function curScale() { var s = parseInt(localStorage.getItem(FONT_KEY) || '100', 10); return (s >= 70 && s <= 150) ? s : 100; }
function applyScale(s) {
s = Math.min(150, Math.max(70, s));
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {}
var z = s / 100;
scaleSelectors.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.zoom = z; });
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%';
}
applyScale(curScale());
var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus');
if (plus) plus.addEventListener('click', function () { applyScale(curScale() + 10); });
if (minus) minus.addEventListener('click', function () { applyScale(curScale() - 10); });
window.addEventListener('storage', function (e) { if (e.key === FONT_KEY) applyScale(curScale()); });
})();
</script> </script>
</body> </body>
</html> </html>

View File

@@ -51,7 +51,7 @@ tailwind.config = {
.data-table tbody tr:hover td { background-color: #f9fafb; } .data-table tbody tr:hover td { background-color: #f9fafb; }
@media print { .no-print { display: none !important; } .embed-titlebar { display: none; } } @media print { .no-print { display: none !important; } .embed-titlebar { display: none; } }
/* 화면 설명 드로어(팝업) — 현재 화면 위 오른쪽에 겹쳐 띄움 */ /* 화면 설명 드로어(팝업) — 현재 화면 위 오른쪽에 겹쳐 띄움 */
.help-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(460px, 92vw); background: #fff; box-shadow: -8px 0 26px rgba(0,0,0,.18); z-index: 9999; display: none; flex-direction: column; } .help-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(920px, 92vw); background: #fff; box-shadow: -8px 0 26px rgba(0,0,0,.18); z-index: 9999; display: none; flex-direction: column; }
.help-drawer.open { display: flex; } .help-drawer.open { display: flex; }
.help-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: .5rem .75rem; background: #1a2b4b; color: #fff; font-size: .8rem; font-weight: 700; flex-shrink: 0; } .help-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: .5rem .75rem; background: #1a2b4b; color: #fff; font-size: .8rem; font-weight: 700; flex-shrink: 0; }
.help-drawer-head .hd-btns { display: flex; gap: 4px; } .help-drawer-head .hd-btns { display: flex; gap: 4px; }
@@ -137,7 +137,13 @@ tailwind.config = {
function withEmbedUrl(url) { try { var x = new URL(url, location.href); x.searchParams.set('embed', '1'); return x.href; } catch (e) { return url; } } function withEmbedUrl(url) { try { var x = new URL(url, location.href); x.searchParams.set('embed', '1'); return x.href; } catch (e) { return url; } }
function openHelp(url) { function openHelp(url) {
var u = withEmbedUrl(url); var u = withEmbedUrl(url);
if (dFrame.getAttribute('data-src') !== u) { dFrame.src = u; dFrame.setAttribute('data-src', u); } if (dFrame.getAttribute('data-src') !== u) {
// 새 URL → 재로드(로드 후 매뉴얼 자체 스크립트가 강조 실행)
dFrame.src = u; dFrame.setAttribute('data-src', u);
} else {
// 같은 URL → 재로드 안 함. 매뉴얼에 다시 강조하라고 알림(껐다 켜도 강조되도록)
try { dFrame.contentWindow.postMessage({ type: 'manual-hl' }, location.origin); } catch (e) {}
}
var tab = document.getElementById('helpDrawerTab'); if (tab) tab.setAttribute('href', url); var tab = document.getElementById('helpDrawerTab'); if (tab) tab.setAttribute('href', url);
drawer.classList.add('open'); drawer.classList.add('open');
} }
@@ -160,9 +166,13 @@ tailwind.config = {
// 드로어 폭 드래그 조절 // 드로어 폭 드래그 조절
(function () { (function () {
var grip = document.getElementById('helpDrawerGrip'), dragging = false; var grip = document.getElementById('helpDrawerGrip'), dragging = false;
grip.addEventListener('mousedown', function (e) { e.preventDefault(); dragging = true; document.body.style.userSelect = 'none'; }); // 드래그 중 iframe 이 마우스 이벤트를 가로채면(특히 좁힐 때) 멈추므로, 화면 전체 투명 오버레이로 이벤트를 가로챈다.
var ov = document.createElement('div');
ov.style.cssText = 'position:fixed;inset:0;z-index:10000;cursor:col-resize;display:none;';
document.body.appendChild(ov);
grip.addEventListener('mousedown', function (e) { e.preventDefault(); dragging = true; ov.style.display = 'block'; document.body.style.userSelect = 'none'; });
document.addEventListener('mousemove', function (e) { if (!dragging) return; var w = window.innerWidth - e.clientX; drawer.style.width = Math.min(window.innerWidth * 0.92, Math.max(300, w)) + 'px'; }); document.addEventListener('mousemove', function (e) { if (!dragging) return; var w = window.innerWidth - e.clientX; drawer.style.width = Math.min(window.innerWidth * 0.92, Math.max(300, w)) + 'px'; });
document.addEventListener('mouseup', function () { dragging = false; document.body.style.userSelect = ''; }); document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; });
})(); })();
// 글씨 크기(zoom) — 상단바에서 조절한 값을 적용. localStorage 공유 + storage 이벤트로 실시간 반영. // 글씨 크기(zoom) — 상단바에서 조절한 값을 적용. localStorage 공유 + storage 이벤트로 실시간 반영.

View File

@@ -217,7 +217,7 @@ tailwind.config = {
<!-- 화면 설명 드로어(팝업) --> <!-- 화면 설명 드로어(팝업) -->
<style> <style>
.help-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(460px, 92vw); background: #fff; box-shadow: -8px 0 26px rgba(0,0,0,.18); z-index: 9999; display: none; flex-direction: column; } .help-drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(920px, 92vw); background: #fff; box-shadow: -8px 0 26px rgba(0,0,0,.18); z-index: 9999; display: none; flex-direction: column; }
.help-drawer.open { display: flex; } .help-drawer.open { display: flex; }
.help-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: .5rem .75rem; background: #1a2b4b; color: #fff; font-size: .8rem; font-weight: 700; flex-shrink: 0; } .help-drawer-head { display: flex; align-items: center; justify-content: space-between; padding: .5rem .75rem; background: #1a2b4b; color: #fff; font-size: .8rem; font-weight: 700; flex-shrink: 0; }
.help-drawer-head .hd-btn { color: #fff; background: rgba(255,255,255,.14); border: 0; width: 26px; height: 26px; border-radius: 6px; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; text-decoration: none; margin-left: 4px; } .help-drawer-head .hd-btn { color: #fff; background: rgba(255,255,255,.14); border: 0; width: 26px; height: 26px; border-radius: 6px; cursor: pointer; font-size: 13px; display: inline-flex; align-items: center; justify-content: center; text-decoration: none; margin-left: 4px; }
@@ -238,7 +238,12 @@ tailwind.config = {
var drawer = document.getElementById('helpDrawer'), dFrame = document.getElementById('helpDrawerFrame'); var drawer = document.getElementById('helpDrawer'), dFrame = document.getElementById('helpDrawerFrame');
function openHelp(url) { function openHelp(url) {
var u = url; try { var x = new URL(url, location.href); x.searchParams.set('embed', '1'); u = x.href; } catch (e) {} var u = url; try { var x = new URL(url, location.href); x.searchParams.set('embed', '1'); u = x.href; } catch (e) {}
if (dFrame.getAttribute('data-src') !== u) { dFrame.src = u; dFrame.setAttribute('data-src', u); } if (dFrame.getAttribute('data-src') !== u) {
dFrame.src = u; dFrame.setAttribute('data-src', u);
} else {
// 같은 URL → 재로드 안 함. 매뉴얼에 다시 강조하라고 알림(껐다 켜도 강조되도록)
try { dFrame.contentWindow.postMessage({ type: 'manual-hl' }, location.origin); } catch (e) {}
}
drawer.classList.add('open'); drawer.classList.add('open');
} }
function closeHelp() { drawer.classList.remove('open'); } function closeHelp() { drawer.classList.remove('open'); }
@@ -251,18 +256,23 @@ tailwind.config = {
document.getElementById('helpDrawerClose').addEventListener('click', closeHelp); document.getElementById('helpDrawerClose').addEventListener('click', closeHelp);
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeHelp(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeHelp(); });
var grip = document.getElementById('helpDrawerGrip'), dragging = false; var grip = document.getElementById('helpDrawerGrip'), dragging = false;
grip.addEventListener('mousedown', function (e) { e.preventDefault(); dragging = true; document.body.style.userSelect = 'none'; }); // 드래그 중 iframe 이 마우스 이벤트를 가로채면(특히 좁힐 때) 멈추므로, 화면 전체 투명 오버레이로 이벤트를 가로챈다.
var ov = document.createElement('div');
ov.style.cssText = 'position:fixed;inset:0;z-index:10000;cursor:col-resize;display:none;';
document.body.appendChild(ov);
grip.addEventListener('mousedown', function (e) { e.preventDefault(); dragging = true; ov.style.display = 'block'; document.body.style.userSelect = 'none'; });
document.addEventListener('mousemove', function (e) { if (!dragging) return; var w = window.innerWidth - e.clientX; drawer.style.width = Math.min(window.innerWidth * 0.92, Math.max(300, w)) + 'px'; }); document.addEventListener('mousemove', function (e) { if (!dragging) return; var w = window.innerWidth - e.clientX; drawer.style.width = Math.min(window.innerWidth * 0.92, Math.max(300, w)) + 'px'; });
document.addEventListener('mouseup', function () { dragging = false; document.body.style.userSelect = ''; }); document.addEventListener('mouseup', function () { if (!dragging) return; dragging = false; ov.style.display = 'none'; document.body.style.userSelect = ''; });
// 글씨 크기 조절 — 본문(.work-main) 영역에 zoom 적용. 헤더/사이드바는 그대로. // 글씨 크기 조절 — 본문 + 상단 대메뉴 + 좌측 사이드바(메뉴)에 zoom 적용.
var FONT_KEY = 'jrj_font_scale'; var FONT_KEY = 'jrj_font_scale';
var target = document.querySelector('.work-main') || document.body; var scaleSelectors = ['.portal-header', '.sidebar', '.work-main'];
function curScale() { var s = parseInt(localStorage.getItem(FONT_KEY) || '100', 10); return (s >= 70 && s <= 150) ? s : 100; } function curScale() { var s = parseInt(localStorage.getItem(FONT_KEY) || '100', 10); return (s >= 70 && s <= 150) ? s : 100; }
function applyScale(s) { function applyScale(s) {
s = Math.min(150, Math.max(70, s)); s = Math.min(150, Math.max(70, s));
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {} try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {}
target.style.zoom = (s / 100); var z = s / 100;
scaleSelectors.forEach(function (sel) { var el = document.querySelector(sel); if (el) el.style.zoom = z; });
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%'; var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%';
} }
applyScale(curScale()); applyScale(curScale());

View File

@@ -486,11 +486,17 @@ if ($effectiveLgIdx) {
function setFontScale(s) { function setFontScale(s) {
s = Math.min(150, Math.max(70, s)); s = Math.min(150, Math.max(70, s));
try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {} try { localStorage.setItem(FONT_KEY, String(s)); } catch (e) {}
var z = s / 100;
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%'; var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = s + '%';
Object.keys(tabs).forEach(function (k) { try { tabs[k].frame.contentDocument.documentElement.style.zoom = (s / 100); } catch (e) {} }); // 탭(iframe) 내용
Object.keys(tabs).forEach(function (k) { try { tabs[k].frame.contentDocument.documentElement.style.zoom = z; } catch (e) {} });
// 셸 메뉴(상단 대메뉴 + 좌측 사이드바)도 함께 확대
['.portal-header', '.sidebar'].forEach(function (sel) {
var el = document.querySelector(sel); if (el) el.style.zoom = z;
});
} }
(function () { (function () {
var pct = document.getElementById('wsFontPct'); if (pct) pct.textContent = curFontScale() + '%'; setFontScale(curFontScale()); // 저장된 배율을 셸 메뉴에도 적용(초기 로드)
var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus'); var plus = document.getElementById('wsFontPlus'), minus = document.getElementById('wsFontMinus');
if (plus) plus.addEventListener('click', function () { setFontScale(curFontScale() + 10); }); if (plus) plus.addEventListener('click', function () { setFontScale(curFontScale() + 10); });
if (minus) minus.addEventListener('click', function () { setFontScale(curFontScale() - 10); }); if (minus) minus.addEventListener('click', function () { setFontScale(curFontScale() - 10); });

View File

@@ -44,6 +44,9 @@ $searchQ = (string) (service('request')->getGet('q') ?? '');
.manual-prose tbody tr:nth-child(even) td { background: #f9fafb; } .manual-prose tbody tr:nth-child(even) td { background: #f9fafb; }
.manual-prose hr { margin: 1.6rem 0; border: 0; border-top: 1px solid #e5e7eb; } .manual-prose hr { margin: 1.6rem 0; border: 0; border-top: 1px solid #e5e7eb; }
.manual-toc a.active { background: #1a2b4b; color: #fff; font-weight: 700; } .manual-toc a.active { background: #1a2b4b; color: #fff; font-weight: 700; }
/* "이 화면 설명"으로 들어왔을 때 해당 소제목 강조 */
.manual-prose h2.hl-flash, .manual-prose h3.hl-flash { background: #fef9c3; box-shadow: -6px 0 0 #fef9c3, 6px 0 0 #fef9c3; border-radius: 4px; animation: hlFlash 2.6s ease-out 1; }
@keyframes hlFlash { 0%, 45% { background: #fde047; box-shadow: -6px 0 0 #fde047, 6px 0 0 #fde047; } 100% { background: transparent; box-shadow: -6px 0 0 transparent, 6px 0 0 transparent; } }
@media print { @media print {
.manual-toc, .manual-actions, .manual-nav { display: none !important; } .manual-toc, .manual-actions, .manual-nav { display: none !important; }
.manual-layout { display: block !important; } .manual-layout { display: block !important; }
@@ -108,6 +111,35 @@ $searchQ = (string) (service('request')->getGet('q') ?? '');
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 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 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 Q0 = <?= json_encode($searchQ, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS) ?>;
// "이 화면 설명"으로 들어온 경우: 해당 소제목으로 스크롤 + 강조.
// 드로어를 껐다 다시 켜도(같은 URL이라 iframe 재로드 안 됨) 부모가 보낸 메시지로 다시 강조한다.
var hlFlashTimer = null;
function runHl() {
var hl = '';
try { hl = new URLSearchParams(location.search).get('hl') || ''; } catch (e) {}
hl = hl.trim();
if (!hl) return;
var norm = function (s) { return String(s || '').replace(/\s+/g, ' ').trim().toLowerCase(); };
var needle = norm(hl);
var prose = document.querySelector('.manual-prose');
if (!prose) return;
var heads = prose.querySelectorAll('h2, h3'), target = null;
for (var i = 0; i < heads.length; i++) {
if (norm(heads[i].textContent).indexOf(needle) >= 0) { target = heads[i]; break; }
}
if (!target) return;
clearTimeout(hlFlashTimer);
target.classList.remove('hl-flash');
void target.offsetWidth; // 리플로우 → 애니메이션 재시작
target.classList.add('hl-flash');
try { target.scrollIntoView({ block: 'start', behavior: 'smooth' }); } catch (e) { target.scrollIntoView(); }
hlFlashTimer = setTimeout(function () { target.classList.remove('hl-flash'); }, 2700);
}
runHl();
window.addEventListener('message', function (e) {
if (e.origin !== location.origin) return;
if (e.data && e.data.type === 'manual-hl') runHl();
});
var input = document.getElementById('manualSearchInput'); var input = document.getElementById('manualSearchInput');
var box = document.getElementById('manualSearchResults'); var box = document.getElementById('manualSearchResults');