'use client'; // D2 $종목 태그 렌더러 — 서버가 내려준 본문(HTML) 안의 `$005930(삼성전자)` 토큰을 // 클릭 가능한 종목 배지(/stock/005930 링크)로 치환한다. // 파싱은 서버가 저장 시 수행하고, 프론트는 표시 토큰을 렌더만 한다(본문 원문 불변). // dangerouslySetInnerHTML 로 삽입된 HTML 의 텍스트 노드만 순회해 치환(스크립트/속성 미변조). import '@/app/styles/community.scss'; import { useEffect, useRef } from 'react'; interface Props { content: string; className?: string; } // $ + 6자리 숫자 + (선택) (종목명). 6자리 뒤에 숫자가 이어지면 코드로 보지 않음(경계, 서버 파서와 동일). const TAG_PATTERN = /\$(\d{6})(?!\d)(?:\(([^)]{0,20})\))?/g; export default function StockTagContent({ content, className }: Props) { const ref = useRef(null); useEffect(() => { const root = ref.current; if (!root) { return; } // 이미 배지로 치환된 노드는 건너뛰기 위해 앵커 내부 텍스트는 제외 const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode(node) { if (!node.nodeValue || !node.nodeValue.includes('$')) { return NodeFilter.FILTER_REJECT; } // 이미 종목 태그 앵커 안이면 스킵 let parent = node.parentElement; while (parent && parent !== root) { if (parent.classList && parent.classList.contains('stock-tag')) { return NodeFilter.FILTER_REJECT; } parent = parent.parentElement; } return NodeFilter.FILTER_ACCEPT; } }); const targets: Text[] = []; let current = walker.nextNode(); while (current) { targets.push(current as Text); current = walker.nextNode(); } for (const textNode of targets) { const text = textNode.nodeValue ?? ''; TAG_PATTERN.lastIndex = 0; if (!TAG_PATTERN.test(text)) { continue; } TAG_PATTERN.lastIndex = 0; const frag = document.createDocumentFragment(); let lastIndex = 0; let match: RegExpExecArray|null; while ((match = TAG_PATTERN.exec(text)) !== null) { const [full, code, name] = match; if (match.index > lastIndex) { frag.appendChild(document.createTextNode(text.slice(lastIndex, match.index))); } const anchor = document.createElement('a'); anchor.href = `/stock/${code}`; anchor.className = 'stock-tag'; const nameSpan = document.createElement('span'); nameSpan.textContent = name ? name : code; anchor.appendChild(nameSpan); const codeSpan = document.createElement('span'); codeSpan.className = 'stock-tag__code'; codeSpan.textContent = code; anchor.appendChild(codeSpan); frag.appendChild(anchor); lastIndex = match.index + full.length; } if (lastIndex < text.length) { frag.appendChild(document.createTextNode(text.slice(lastIndex))); } textNode.parentNode?.replaceChild(frag, textNode); } }, [content]); return (
); }