From be19b312340d3d1a8d7056eafffa4636bfa1cbdb Mon Sep 17 00:00:00 2001 From: ParkWonYeop Date: Sun, 28 Dec 2025 16:34:27 +0900 Subject: [PATCH] . --- src/components/post/MarkdownRenderer.tsx | 301 +++++++---------------- 1 file changed, 85 insertions(+), 216 deletions(-) diff --git a/src/components/post/MarkdownRenderer.tsx b/src/components/post/MarkdownRenderer.tsx index 82a7ba8..18b7cad 100644 --- a/src/components/post/MarkdownRenderer.tsx +++ b/src/components/post/MarkdownRenderer.tsx @@ -1,57 +1,25 @@ 'use client'; -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; -import remarkBreaks from 'remark-breaks'; -import remarkMath from 'remark-math'; -import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; -import rehypeSlug from 'rehype-slug'; -import rehypeKatex from 'rehype-katex'; -import rehypeAutolinkHeadings from 'rehype-autolink-headings'; +import rehypeSanitize from 'rehype-sanitize'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; -import { Copy, Check, Terminal, ExternalLink, Info, AlertTriangle, XCircle, Lightbulb, Link as LinkIcon } from 'lucide-react'; +import { Copy, Check, Terminal, ExternalLink } from 'lucide-react'; import { clsx } from 'clsx'; -import 'katex/dist/katex.min.css'; +import rehypeSlug from 'rehype-slug'; interface MarkdownRendererProps { content: string; } -// GitHub Style Alerts 설정 -const ALERT_VARIANTS = { - NOTE: { color: 'bg-blue-50 border-blue-500 text-blue-800', icon: Info, title: 'Note' }, - TIP: { color: 'bg-green-50 border-green-500 text-green-800', icon: Lightbulb, title: 'Tip' }, - IMPORTANT: { color: 'bg-purple-50 border-purple-500 text-purple-800', icon: AlertTriangle, title: 'Important' }, - WARNING: { color: 'bg-yellow-50 border-yellow-500 text-yellow-800', icon: AlertTriangle, title: 'Warning' }, - CAUTION: { color: 'bg-red-50 border-red-500 text-red-800', icon: XCircle, title: 'Caution' }, -}; - export default function MarkdownRenderer({ content }: MarkdownRendererProps) { - // Sanitize 스키마 확장 - // 주의: 모든 태그에 style과 className을 허용하는 것은 보안상(XSS) 주의가 필요합니다. - const sanitizeSchema = useMemo(() => ({ - ...defaultSchema, - attributes: { - ...defaultSchema.attributes, - '*': ['className', 'style'], - span: ['className', 'style'], - div: ['className', 'style'], - }, - tagNames: [...(defaultSchema.tagNames || []), 'math', 'mi', 'mn', 'mo', 'msup', 'msub', 'mfrac', 'row'], - }), []); - return ( -
+
; + if (!inline && match) { + return ( + + ); } return ( - + {children} ); }, - // 2. 인용구 + GitHub Alerts 처리 (TypeScript 오류 수정 및 로직 개선) - blockquote({ children }: any) { - const childArray = React.Children.toArray(children); - const firstChild = childArray[0]; - let alertType: keyof typeof ALERT_VARIANTS | null = null; - - // [수정] 타입스크립트 안전성 확보 및 파싱 로직 강화 - // React.isValidElement로 체크 후, ReactElement로 단언(assertion)하여 props 접근 - if (React.isValidElement(firstChild)) { - const element = firstChild as React.ReactElement; - - // 보통 blockquote > p 구조임 - if (element.type === 'p' && element.props.children) { - // p 태그의 첫 번째 자식이 텍스트인지 확인 - const content = element.props.children; - const firstText = Array.isArray(content) ? content[0] : content; - - if (typeof firstText === 'string') { - const match = firstText.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/); - if (match) { - alertType = match[1] as keyof typeof ALERT_VARIANTS; - } - } - } - } - - // Alert 타입이 감지되면 스타일링된 컴포넌트 반환 - if (alertType) { - const variant = ALERT_VARIANTS[alertType]; - const Icon = variant.icon; - - // [수정] [!NOTE] 텍스트를 안전하게 제거 - const restContent = React.Children.map(children, (child, index) => { - if (index === 0 && React.isValidElement(child)) { - const element = child as React.ReactElement; - const content = element.props.children; - - // 내용이 배열일 수도 있고 문자열일 수도 있음 - if (Array.isArray(content)) { - // 첫 번째 텍스트 노드에서 [!NOTE] 부분만 제거 - const [first, ...rest] = content; - if (typeof first === 'string') { - const newText = first.replace(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s?/, ''); - return React.cloneElement(element, { - ...element.props, - children: [newText, ...rest] - }); - } - } else if (typeof content === 'string') { - const newText = content.replace(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s?/, ''); - return React.cloneElement(element, { - ...element.props, - children: newText - }); - } - } - return child; - }); - - return ( -
-
- - {variant.title} -
-
- {restContent} -
-
- ); - } - - // 일반 인용구 + // 2. 인용구 + blockquote({ children }) { return ( -
+
{children}
); @@ -154,20 +55,12 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) { // 3. 링크 a({ href, children }) { const isExternal = href?.startsWith('http'); - if (href?.startsWith('#')) { - return ( - - ) - } - return ( - {children} {isExternal && } @@ -179,89 +72,70 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) { table({ children }) { return (
- +
{children}
); }, thead({ children }) { - return {children}; + return {children}; }, th({ children }) { - return {children}; + return {children}; }, td({ children }) { - return {children}; + return {children}; }, - // 5. 이미지 + // 5. 이미지 (비율 유지 및 중앙 정렬) img({ src, alt }) { return ( -
-
- {alt} -
- {alt &&
{alt}
} -
- ); - }, - - // 6. 리스트 (Task List 지원) - ul({ children, className }) { - const isTaskList = className?.includes('contains-task-list'); - return
    {children}
; - }, - ol({ children }) { - return
    {children}
; - }, - li({ children, className, ...props }: any) { - if (className?.includes('task-list-item')) { - return
  • {children}
  • - } - return
  • {children}
  • ; - }, - // [수정] 체크박스 ReadOnly 속성 추가 - input({ type, ...props }: any) { - if (type === 'checkbox') { - return ( - + {alt} { + e.currentTarget.style.display = 'none'; + }} /> - ) - } - return ; + {alt && {alt}} + + ); }, - // 7. 헤딩 - h1({ children, id }: any) { + // 6. 리스트 스타일 + ul({ children }) { + return
      {children}
    ; + }, + ol({ children, ...props }: any) { return ( -
    -

    {children}

    -
    +
      + {children} +
    ); }, - h2({ children, id }: any) { - return ( -
    -

    {children}

    -
    - ); + li({ children }) { + return
  • {children}
  • ; }, - h3({ children, id }: any) { - return

    {children}

    ; + + // 7. 헤딩 스타일 (🛠️ 수정: ...props를 전달해야 id가 붙어서 목차 이동이 작동함) + h1({ children, ...props }: any) { + return

    {children}

    ; + }, + h2({ children, ...props }: any) { + return

    {children}

    ; + }, + h3({ children, ...props }: any) { + return

    {children}

    ; }, - p({ children }) { - return

    {children}

    ; - } }} > {content} @@ -281,53 +155,48 @@ function CodeBlock({ language, code }: { language: string; code: string }) { }; return ( -
    -
    +
    +
    -
    -
    -
    -
    -
    -
    - - {language} +
    +
    +
    +
    + {language && ( +
    + + {language} +
    + )}
    - +
    - -
    + +
    {code}