diff --git a/app/entities/post/detail/Callout.tsx b/app/entities/post/detail/Callout.tsx new file mode 100644 index 0000000..5fdfb10 --- /dev/null +++ b/app/entities/post/detail/Callout.tsx @@ -0,0 +1,22 @@ +'use client'; +import { ReactNode } from 'react'; + +interface CalloutProps { + emoji?: string; + children?: ReactNode; +} + +const Callout = ({ emoji, children }: CalloutProps) => { + return ( +
+ {emoji && ( + {emoji} + )} +
+ {children} +
+
+ ); +}; + +export default Callout; diff --git a/app/entities/post/detail/PostBody.tsx b/app/entities/post/detail/PostBody.tsx index 6204292..455baf1 100644 --- a/app/entities/post/detail/PostBody.tsx +++ b/app/entities/post/detail/PostBody.tsx @@ -3,6 +3,7 @@ import { useState } from 'react'; import LoadingIndicator from '@/app/entities/common/Loading/LoadingIndicator'; import ImageZoomOverlayContainer from '@/app/entities/common/Overlay/Image/ImageZoomOverlayContainer'; import Overlay from '@/app/entities/common/Overlay/Overlay'; +import Callout from '@/app/entities/post/detail/Callout'; import OgLinkCard from '@/app/entities/post/detail/OgLinkCard'; import PostTOC from '@/app/entities/post/detail/PostTOC'; import TagBox from '@/app/entities/post/tags/TagBox'; @@ -10,7 +11,7 @@ import useOverlay from '@/app/hooks/common/useOverlay'; import useTheme from '@/app/hooks/useTheme'; import MDEditor from '@uiw/react-md-editor'; import { - asideStyleRewrite, + asideToCallout, addDescriptionUnderImage, renderYoutubeEmbed, renderOpenGraph, @@ -79,9 +80,11 @@ const PostBody = ({ content, tags, loading }: Props) => { components={{ ogcard: ({ href }: { href?: string }) => href ? : null, + callout: ({ emoji, children }: { emoji?: string; children?: React.ReactNode }) => + {children}, } as any} rehypeRewrite={(node, index?, parent?) => { - asideStyleRewrite(node); + asideToCallout(node); renderOpenGraph(node, index, parent as Element | undefined); renderYoutubeEmbed( node, diff --git a/app/entities/post/write/BlogForm.tsx b/app/entities/post/write/BlogForm.tsx index 76efb49..3e94ad6 100644 --- a/app/entities/post/write/BlogForm.tsx +++ b/app/entities/post/write/BlogForm.tsx @@ -6,6 +6,7 @@ import { useSearchParams } from 'next/navigation'; import { useEffect, useState } from 'react'; import ImageZoomOverlayContainer from '@/app/entities/common/Overlay/Image/ImageZoomOverlayContainer'; import Overlay from '@/app/entities/common/Overlay/Overlay'; +import Callout from '@/app/entities/post/detail/Callout'; import DraftListOverlay from '@/app/entities/post/write/DraftListOverlay'; import PostMetadataForm from '@/app/entities/post/write/PostMetadataForm'; import PostWriteButtons from '@/app/entities/post/write/PostWriteButtons'; @@ -21,7 +22,7 @@ import usePost from '@/app/hooks/post/usePost'; import useTheme from '@/app/hooks/useTheme'; import useToast from '@/app/hooks/useToast'; import { - asideStyleRewrite, + asideToCallout, addDescriptionUnderImage, renderYoutubeEmbed, createImageClickHandler, @@ -289,8 +290,12 @@ const BlogForm = () => { wrapperElement: { 'data-color-mode': theme, }, + components: { + callout: ({ emoji, children }: { emoji?: string; children?: React.ReactNode }) => + {children}, + } as any, rehypeRewrite: (node, index?, parent?) => { - asideStyleRewrite(node); + asideToCallout(node); renderYoutubeEmbed( node, index || 0, diff --git a/app/lib/utils/rehypeUtils.ts b/app/lib/utils/rehypeUtils.ts index f9cede1..b27bb2c 100644 --- a/app/lib/utils/rehypeUtils.ts +++ b/app/lib/utils/rehypeUtils.ts @@ -5,27 +5,21 @@ import { SelectedImage } from '../../entities/post/detail/PostBody'; /** - * aside 태그의 첫 번째 텍스트 노드를 emoji span으로 래핑 + * aside 태그를 callout 커스텀 컴포넌트 노드로 변환 + * 첫 번째 텍스트 노드를 emoji prop으로 추출하고 나머지는 children으로 유지 */ -export const asideStyleRewrite = (node: any) => { +export const asideToCallout = (node: any) => { if (node.type === 'element' && node.tagName === 'aside') { - for (const child of [...node.children]) { - if (node.children[0] === child && child.type === 'text') { - node.children[0] = { - type: 'element', - tagName: 'span', - properties: { - className: 'aside-emoji', - }, - children: [ - { - type: 'text', - value: child.value!, - }, - ], - }; - } + const firstChild = node.children[0]; + let emoji = ''; + + if (firstChild?.type === 'text') { + emoji = firstChild.value.trim(); + node.children = node.children.slice(1); } + + node.tagName = 'callout'; + node.properties = { ...node.properties, emoji }; } };