| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- '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<HTMLDivElement>(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 (
- <div
- ref={ref}
- className={className}
- dangerouslySetInnerHTML={{ __html: content }}
- ></div>
- );
- }
|