Files
sam-manage/public/js/academy-glossary.js
김보곤 83c6ee8b62 feat: [academy] 전문용어 툴팁 기능 추가
- 3개 백과사전 페이지 공용 glossary-tooltip 컴포넌트
- JS TreeWalker 자동 감지 + CSS-only 풍선 툴팁
- 도메인별 용어사전: 방화셔터/IT기획/서버지식 각 25~30개
2026-02-23 10:10:46 +09:00

166 lines
5.0 KiB
JavaScript

/**
* Academy Glossary Tooltip
*
* 페이지 로드 후 콘텐츠 영역의 텍스트를 스캔하여
* 용어사전 매칭 → <span class="glossary-term"> 래핑
*/
(function () {
'use strict';
var DOMAIN = window.__GLOSSARY_DOMAIN;
var DATA = window.__GLOSSARY_DATA;
if (!DOMAIN || !DATA || !DATA[DOMAIN]) return;
var glossary = DATA[DOMAIN];
// 용어 목록: 길이 내림차순 정렬 (긴 용어 우선 매칭)
var terms = Object.keys(glossary).sort(function (a, b) {
return b.length - a.length;
});
if (terms.length === 0) return;
// 정규식 특수문자 이스케이프
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\\/]/g, '\\$&');
}
// 단일 정규식 생성
var pattern = new RegExp(
terms.map(escapeRegex).join('|'),
'g'
);
// 이미 래핑된 용어 추적 (페이지당 첫 등장만)
var matched = {};
// 건너뛸 태그
var SKIP_TAGS = {
SCRIPT: true, STYLE: true, CODE: true, PRE: true,
NAV: true, TEXTAREA: true, INPUT: true, SELECT: true
};
/**
* TreeWalker로 텍스트 노드를 순회하며 용어 래핑
*/
function processContent() {
var container = document.querySelector('.academy-content')
|| document.querySelector('[class*="max-w-"]')
|| document.querySelector('main')
|| document.body;
var walker = document.createTreeWalker(
container,
NodeFilter.SHOW_TEXT,
{
acceptNode: function (node) {
// 이미 래핑된 용어 내부 건너뛰기
var parent = node.parentElement;
if (!parent) return NodeFilter.FILTER_REJECT;
if (parent.classList && parent.classList.contains('glossary-term')) {
return NodeFilter.FILTER_REJECT;
}
// 건너뛸 태그 확인
var el = parent;
while (el) {
if (SKIP_TAGS[el.tagName]) return NodeFilter.FILTER_REJECT;
el = el.parentElement;
}
return NodeFilter.FILTER_ACCEPT;
}
}
);
var textNodes = [];
var node;
while ((node = walker.nextNode())) {
textNodes.push(node);
}
textNodes.forEach(function (textNode) {
var text = textNode.nodeValue;
if (!text || !text.trim()) return;
// 패턴 매칭 확인
pattern.lastIndex = 0;
if (!pattern.test(text)) return;
// 실제 치환
pattern.lastIndex = 0;
var frag = document.createDocumentFragment();
var lastIndex = 0;
var match;
while ((match = pattern.exec(text)) !== null) {
var term = match[0];
// 첫 등장만 래핑
if (matched[term]) {
continue;
}
matched[term] = true;
// 매칭 앞 텍스트
if (match.index > lastIndex) {
frag.appendChild(
document.createTextNode(text.slice(lastIndex, match.index))
);
}
// 용어 span 생성
var span = document.createElement('span');
span.className = 'glossary-term';
span.setAttribute('data-tooltip', glossary[term]);
span.textContent = term;
frag.appendChild(span);
lastIndex = match.index + term.length;
}
// 치환이 없었으면 건너뛰기
if (lastIndex === 0) return;
// 남은 텍스트
if (lastIndex < text.length) {
frag.appendChild(
document.createTextNode(text.slice(lastIndex))
);
}
textNode.parentNode.replaceChild(frag, textNode);
});
}
// 모바일 터치 지원
function setupMobileTouch() {
document.addEventListener('click', function (e) {
var term = e.target.closest('.glossary-term');
// 다른 곳 클릭 시 모든 활성 툴팁 제거
var actives = document.querySelectorAll('.glossary-term.is-tooltip-active');
actives.forEach(function (el) {
if (el !== term) el.classList.remove('is-tooltip-active');
});
// 용어 클릭 시 토글
if (term) {
e.preventDefault();
term.classList.toggle('is-tooltip-active');
}
});
}
// 실행
function init() {
processContent();
setupMobileTouch();
}
if ('requestIdleCallback' in window) {
requestIdleCallback(init, { timeout: 2000 });
} else {
setTimeout(init, 100);
}
})();