|
|
@@ -1,107 +0,0 @@
|
|
|
-'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>
|
|
|
- );
|
|
|
-}
|