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 };
}
};