'use client'; import './style.scss'; import '@/app/styles/community.scss'; import { useEffect, useRef } from 'react'; import BoardMeta from '@/types/forum/boardMeta'; import BoardManager from '@/types/forum/boardManager'; import { checkPermission } from '@/lib/utils/permission'; type Props = { boardMeta: BoardMeta; content: string; boardManagers?: BoardManager[]; } export default function Content({ boardMeta, content, boardManagers }: Props) { const articleRef = useRef(null); const allowTargetBlank = boardMeta.view.allowContentLinkTargetBlank; // $종목 태그 렌더 — 본문 텍스트의 `$005930(삼성전자)` 토큰을 /stock 링크 배지로 치환 useEffect(() => { const el = articleRef.current; if (!el) { return; } const pattern = /\$(\d{6})(?!\d)(?:\(([^)]{0,20})\))?/g; const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, { acceptNode(node) { if (!node.nodeValue || !node.nodeValue.includes('$')) { return NodeFilter.FILTER_REJECT; } let parent = node.parentElement; while (parent && parent !== el) { if (parent.classList && parent.classList.contains('stock-tag')) { return NodeFilter.FILTER_REJECT; } parent = parent.parentElement; } return NodeFilter.FILTER_ACCEPT; } }); const targets: Text[] = []; let cursor = walker.nextNode(); while (cursor) { targets.push(cursor as Text); cursor = walker.nextNode(); } for (const textNode of targets) { const text = textNode.nodeValue ?? ''; pattern.lastIndex = 0; if (!pattern.test(text)) { continue; } pattern.lastIndex = 0; const frag = document.createDocumentFragment(); let lastIndex = 0; let match: RegExpExecArray|null; while ((match = 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]); useEffect(() => { const el = articleRef.current; if (!el) { return; } const handler = (e: MouseEvent) => { const target = e.target as HTMLElement; // 파일 다운로드 const file = target.closest('section.file-embed') as HTMLElement; if (file && file.dataset.uuid) { // 파일 다운로드 권한 체크 if (boardManagers) { const stored = localStorage.getItem('member'); const member = stored ? JSON.parse(stored) : null; if (!checkPermission(boardMeta, boardManagers, member).canDownloadFile) { alert('파일을 다운로드할 수 있는 권한이 없습니다.'); return; } } window.location.href = `/api/forum/post/file/${file.dataset.uuid}`; } // 링크 클릭 const anchor = target.closest('a[data-uuid]') as HTMLAnchorElement; if (anchor && anchor.dataset.uuid) { e.preventDefault(); window.open(`/api/forum/post/link/${anchor.dataset.uuid}`, allowTargetBlank ? '_blank' : '_self'); } } el.addEventListener('click', handler); return () => { el.removeEventListener('click', handler); } }, [allowTargetBlank, boardMeta, boardManagers]); return (
); }