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 ( -
+
{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} }
-
- );
- },
-
- // 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 (
-
+
{
+ 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}