'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 (
);
}