| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- '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<HTMLElement>(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 (
- <article ref={articleRef} dangerouslySetInnerHTML={{ __html: content }} className='whitespace-normal break-words'></article>
- );
- }
|