.
All checks were successful
Deploy blog-frontend / build-and-deploy (push) Successful in 1m48s

This commit is contained in:
ParkWonYeop
2025-12-28 16:18:24 +09:00
parent bfda1240bd
commit 042f72e75a
4 changed files with 69 additions and 16 deletions

30
package-lock.json generated
View File

@@ -29,6 +29,7 @@
"react-syntax-highlighter": "^16.1.0", "react-syntax-highlighter": "^16.1.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"yarn": "^1.22.22", "yarn": "^1.22.22",
"zustand": "^5.0.9" "zustand": "^5.0.9"
@@ -6275,6 +6276,20 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/mdast-util-newline-to-break": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz",
"integrity": "sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-find-and-replace": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/mdast-util-phrasing": { "node_modules/mdast-util-phrasing": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
@@ -7970,6 +7985,21 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/remark-breaks": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz",
"integrity": "sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==",
"license": "MIT",
"dependencies": {
"@types/mdast": "^4.0.0",
"mdast-util-newline-to-break": "^2.0.0",
"unified": "^11.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/remark-gfm": { "node_modules/remark-gfm": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",

View File

@@ -30,6 +30,7 @@
"react-syntax-highlighter": "^16.1.0", "react-syntax-highlighter": "^16.1.0",
"rehype-sanitize": "^6.0.0", "rehype-sanitize": "^6.0.0",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"yarn": "^1.22.22", "yarn": "^1.22.22",
"zustand": "^5.0.9" "zustand": "^5.0.9"

View File

@@ -3,12 +3,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks'; // 엔터 한 번으로 줄바꿈 되도록 추가
import rehypeSanitize from 'rehype-sanitize'; import rehypeSanitize from 'rehype-sanitize';
import rehypeSlug from 'rehype-slug';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { Copy, Check, Terminal, ExternalLink } from 'lucide-react'; import { Copy, Check, Terminal, ExternalLink } from 'lucide-react';
import { clsx } from 'clsx'; import { clsx } from 'clsx';
import rehypeSlug from 'rehype-slug';
interface MarkdownRendererProps { interface MarkdownRendererProps {
content: string; content: string;
@@ -18,7 +19,8 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
return ( return (
<div className="markdown-content"> <div className="markdown-content">
<ReactMarkdown <ReactMarkdown
remarkPlugins={[remarkGfm]} // remarkBreaks: 일반 텍스트에서 엔터 한 번을 <br/>로 변환해줍니다.
remarkPlugins={[remarkGfm, remarkBreaks]}
rehypePlugins={[rehypeSanitize, rehypeSlug]} rehypePlugins={[rehypeSanitize, rehypeSlug]}
components={{ components={{
// 1. 코드 블록 커스텀 // 1. 코드 블록 커스텀
@@ -43,7 +45,7 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
); );
}, },
// 2. 인용구 // 2. 인용구 (Blockquote)
blockquote({ children }) { blockquote({ children }) {
return ( return (
<blockquote className="border-l-4 border-blue-500 bg-blue-50 pl-4 py-3 my-6 text-gray-700 rounded-r-lg italic shadow-sm"> <blockquote className="border-l-4 border-blue-500 bg-blue-50 pl-4 py-3 my-6 text-gray-700 rounded-r-lg italic shadow-sm">
@@ -82,29 +84,27 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
return <thead className="text-xs text-gray-700 uppercase bg-gray-50 border-b border-gray-200">{children}</thead>; return <thead className="text-xs text-gray-700 uppercase bg-gray-50 border-b border-gray-200">{children}</thead>;
}, },
th({ children }) { th({ children }) {
return <th className="px-6 py-3 font-bold text-gray-900">{children}</th>; return <th className="px-6 py-3 font-bold text-gray-900 whitespace-nowrap">{children}</th>;
}, },
td({ children }) { td({ children }) {
return <td className="px-6 py-4 border-b border-gray-100 whitespace-pre-wrap">{children}</td>; return <td className="px-6 py-4 border-b border-gray-100 whitespace-pre-wrap">{children}</td>;
}, },
// 5. 이미지 (비율 유지 및 중앙 정렬) // 5. 이미지 (figure 태그 사용으로 시멘틱 개선)
img({ src, alt }) { img({ src, alt }) {
return ( return (
// 🛠️ [Fix] flex-col 추가: 이미지와 캡션을 세로로 정렬 <figure className="block my-8 flex flex-col items-center justify-center">
// items-center 추가: 가로축 중앙 정렬
<span className="block my-8 flex flex-col items-center justify-center">
<img <img
src={src} src={src}
alt={alt} alt={alt}
className="rounded-xl shadow-lg border border-gray-100 max-w-full h-auto max-h-[700px] mx-auto hover:scale-[1.01] transition-transform duration-300" className="rounded-xl shadow-lg border border-gray-100 max-w-full h-auto max-h-[700px] mx-auto hover:scale-[1.01] transition-transform duration-300 object-contain"
loading="lazy" loading="lazy"
onError={(e) => { onError={(e) => {
e.currentTarget.style.display = 'none'; e.currentTarget.style.display = 'none';
}} }}
/> />
{alt && <span className="block text-center text-sm text-gray-400 mt-2 w-full">{alt}</span>} {alt && <figcaption className="text-center text-sm text-gray-400 mt-2 w-full">{alt}</figcaption>}
</span> </figure>
); );
}, },
@@ -123,19 +123,24 @@ export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
); );
}, },
li({ children }) { li({ children }) {
return <li className="pl-1">{children}</li>; // p 태그가 내부에 생길 경우 margin 상쇄를 위해 items-start 등 조정
return <li className="pl-1 leading-relaxed">{children}</li>;
}, },
// 7. 헤딩 스타일 (🛠️ 수정: ...props를 전달해야 id가 붙어서 목차 이동이 작동함) // 7. 헤딩 스타일
h1({ children, ...props }: any) { h1({ children, ...props }: any) {
return <h1 className="text-3xl font-extrabold mt-12 mb-6 pb-4 border-b border-gray-100 text-gray-900" {...props}>{children}</h1>; return <h1 className="text-3xl font-extrabold mt-12 mb-6 pb-4 border-b border-gray-100 text-gray-900 scroll-mt-20" {...props}>{children}</h1>;
}, },
h2({ children, ...props }: any) { h2({ children, ...props }: any) {
return <h2 className="text-2xl font-bold mt-10 mb-5 pb-2 text-gray-800" {...props}>{children}</h2>; return <h2 className="text-2xl font-bold mt-10 mb-5 pb-2 text-gray-800 scroll-mt-20" {...props}>{children}</h2>;
}, },
h3({ children, ...props }: any) { h3({ children, ...props }: any) {
return <h3 className="text-xl font-bold mt-8 mb-4 text-gray-800 flex items-center gap-2 before:content-[''] before:w-1.5 before:h-6 before:bg-blue-500 before:rounded-full before:mr-1" {...props}>{children}</h3>; return <h3 className="text-xl font-bold mt-8 mb-4 text-gray-800 flex items-center gap-2 before:content-[''] before:w-1.5 before:h-6 before:bg-blue-500 before:rounded-full before:mr-1 scroll-mt-20" {...props}>{children}</h3>;
}, },
// p 태그 스타일 추가 (일반 텍스트 가독성)
p({ children }) {
return <p className="mb-4 leading-7 text-gray-700">{children}</p>;
}
}} }}
> >
{content} {content}

View File

@@ -2796,6 +2796,14 @@ mdast-util-mdxjs-esm@^2.0.0:
mdast-util-from-markdown "^2.0.0" mdast-util-from-markdown "^2.0.0"
mdast-util-to-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0"
mdast-util-newline-to-break@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/mdast-util-newline-to-break/-/mdast-util-newline-to-break-2.0.0.tgz"
integrity sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==
dependencies:
"@types/mdast" "^4.0.0"
mdast-util-find-and-replace "^3.0.0"
mdast-util-phrasing@^4.0.0: mdast-util-phrasing@^4.0.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz" resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz"
@@ -3689,6 +3697,15 @@ rehype@~13.0.0:
rehype-stringify "^10.0.0" rehype-stringify "^10.0.0"
unified "^11.0.0" unified "^11.0.0"
remark-breaks@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/remark-breaks/-/remark-breaks-4.0.0.tgz"
integrity sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==
dependencies:
"@types/mdast" "^4.0.0"
mdast-util-newline-to-break "^2.0.0"
unified "^11.0.0"
remark-gfm@^4.0.1, remark-gfm@~4.0.0: remark-gfm@^4.0.1, remark-gfm@~4.0.0:
version "4.0.1" version "4.0.1"
resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz" resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz"