Skip to content

Commit 4e92a97

Browse files
committed
Fix: Obsidian 스타일의 콜아웃에서 링크가 제대로 동작하지 않는 문제 해결
1 parent 11a026d commit 4e92a97

9 files changed

+129
-37
lines changed

components/markdown-renderer.tsx

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ interface LinkMap {
2626

2727
function formatDate(dateString: string | undefined): string {
2828
if (!dateString) return "날짜 정보 없음";
29-
3029
try {
3130
return new Date(dateString).toLocaleDateString("ko-KR");
3231
} catch (error) {
@@ -69,35 +68,55 @@ export default function MarkdownRenderer({
6968

7069
const processUncycloLinks = (text: string) => {
7170
// 이스케이프된 링크 패턴을 정상 링크로 변환
72-
let processed = text.replace(
73-
/\\\[(.*?)\\\]\((.*?)\)/g,
74-
(_, linkText, href) => {
75-
// 링크 텍스트는 그대로, href는 URI 인코딩
76-
return `[${linkText}](${encodeURIComponent(href)})`;
77-
},
78-
);
79-
80-
// 위키링크 처리
71+
let processed = text.replace(/\\(\[|\]|\(|\))/g, '$1');
72+
73+
// 위키링크 처리 - 정규식 개선
8174
processed = processed.replace(
82-
/\[\[([^|]+)(?:\|([^\]]+))?\]\]/g,
75+
/\[\[([^|\]]+)(?:\|([^\]]+))?\]\]/g,
8376
(_, path, label) => {
8477
// 경로 정규화
8578
const cleanPath = path.replace(/\.md$/, "");
8679
// 표시할 이름이 없으면 경로의 마지막 부분을 사용
8780
const displayName = label || cleanPath.split("/").pop() || cleanPath;
88-
8981
// URI 인코딩 적용
9082
const encodedPath = encodeURIComponent(cleanPath);
91-
9283
// 인코딩된 경로로 마크다운 링크 생성
9384
return `[${displayName}](${encodedPath})`;
9485
},
9586
);
96-
9787
return processed;
9888
};
9989

90+
// 콜아웃 블록 내부의 위키링크도 처리하기 위한 특별 처리
91+
const processContentWithCallouts = (content: string) => {
92+
// 콜아웃 패턴 - 공백 포함 ("> [!type]" 형식)
93+
const calloutRegex = /(>\s\[!.*?\].*?(?:\n>.*?)*)(?:\n\n|$)/gs;
94+
95+
return content.replace(calloutRegex, (calloutBlock) => {
96+
// 전체 콜아웃 블록 내에서 모든 위키링크를 한 번에 처리
97+
return calloutBlock.replace(
98+
/(- )?\[\[([^|\]]+)(?:\|([^\]]+))?\]\]/g,
99+
(match, bulletPoint, path, label) => {
100+
// 이스케이프 문자 제거 및 경로 정규화
101+
const cleanPath = path.replace(/\\|\\.md$/, "");
102+
// 표시할 이름이 없으면 경로의 마지막 부분을 사용
103+
const displayName = label || cleanPath.split("/").pop() || cleanPath;
104+
// URI 인코딩 적용
105+
const encodedPath = encodeURIComponent(cleanPath);
106+
// 불릿 포인트가 있으면 유지
107+
const prefix = bulletPoint || "";
108+
// 인코딩된 경로로 마크다운 링크 생성
109+
return `${prefix}[${displayName}](${encodedPath})`;
110+
}
111+
);
112+
});
113+
};
114+
115+
// 처리 순서 변경: 위키링크 처리 전에 콜아웃 처리
100116
const processedContent = processUncycloLinks(content);
117+
const fullProcessedContent = processContentWithCallouts(processedContent);
118+
// 최종 처리된 콘텐츠
119+
const finalProcessedContent = fullProcessedContent;
101120

102121
const components = {
103122
h1: ({ children }: { children?: React.ReactNode }) => (
@@ -113,7 +132,6 @@ export default function MarkdownRenderer({
113132
</Badge>
114133
))}
115134
</div>
116-
117135
{/* 날짜 정보 */}
118136
<div className="flex flex-col sm:flex-row sm:justify-between text-xs sm:text-sm text-muted-foreground mb-6 sm:mb-8 pb-3 sm:pb-4 border-b">
119137
<div>작성일: {formatDate(published)}</div>
@@ -141,22 +159,18 @@ export default function MarkdownRenderer({
141159
p: ({ children }: { children?: React.ReactNode }) => {
142160
const hasBlockElement = React.Children.toArray(children).some((child) => {
143161
if (!React.isValidElement(child)) return false;
144-
145162
const childType = (child as React.ReactElement).type;
146163
const isCustomImage = childType === components.img;
147-
148164
const htmlElementType =
149165
typeof childType === "string"
150166
? childType
151167
: (childType as React.ComponentType).displayName;
152-
153168
return (
154169
isCustomImage ||
155170
(htmlElementType &&
156171
["div", "img", "pre", "table"].includes(htmlElementType))
157172
);
158173
});
159-
160174
return hasBlockElement ? (
161175
<div className="my-3 sm:my-4 md:my-5">{children}</div>
162176
) : (
@@ -178,7 +192,6 @@ export default function MarkdownRenderer({
178192
}) {
179193
const match = /language-(\w+)/.exec(className || "");
180194
const code = String(children).replace(/\n$/, "");
181-
182195
return !inline && match ? (
183196
<div className="relative my-4 sm:my-6 md:my-8 rounded-lg overflow-hidden">
184197
<div className="flex items-center justify-between bg-primary/10 text-primary px-3 sm:px-4 py-1 sm:py-2 text-xs sm:text-sm font-mono">
@@ -197,7 +210,6 @@ export default function MarkdownRenderer({
197210
{copiedCode === code ? "복사됨" : "복사"}
198211
</Button>
199212
</div>
200-
201213
<div className="overflow-auto max-w-full break-all whitespace-pre-wrap">
202214
<SyntaxHighlighter
203215
style={isDark ? nightOwl : nord}
@@ -261,7 +273,6 @@ export default function MarkdownRenderer({
261273
),
262274
a: ({ href, children }: { href?: string; children?: React.ReactNode }) => {
263275
if (!href) return <span>{children}</span>;
264-
265276
// 외부 링크 처리 (http로 시작하는 경우)
266277
if (href.startsWith("http")) {
267278
return (
@@ -275,7 +286,6 @@ export default function MarkdownRenderer({
275286
</a>
276287
);
277288
}
278-
279289
// 홈으로 가는 링크
280290
if (href === "/") {
281291
return (
@@ -288,16 +298,13 @@ export default function MarkdownRenderer({
288298
</Link>
289299
);
290300
}
291-
292301
// 내부 링크 처리
293302
if (!isMapLoaded)
294303
return <span className="text-muted-foreground">{children}</span>;
295-
296304
// 디코딩 및 정규화
297305
const decodedHref = decodeURIComponent(href);
298306
const normalizedHref = decodedHref.replace(/\.md$/, "");
299307
const targetFileName = normalizedHref.split("/").pop();
300-
301308
// 링크맵에서 검색
302309
for (const [key, value] of Object.entries(linkMap)) {
303310
const srcFileName = key.replace(/\.md$/, "").split("/").pop();
@@ -313,7 +320,6 @@ export default function MarkdownRenderer({
313320
);
314321
}
315322
}
316-
317323
// 발행되지 않은 문서 링크
318324
return (
319325
<span
@@ -380,7 +386,7 @@ export default function MarkdownRenderer({
380386
return (
381387
<div className="prose-custom">
382388
<ReactMarkdown components={components} remarkPlugins={[remarkCallout]}>
383-
{processedContent}
389+
{finalProcessedContent}
384390
</ReactMarkdown>
385391
</div>
386392
);

content/posts/project/blog/어떻게 블로그를 구현하였나.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ tags:
44
- dev
55
- project
66
createdAt: 2025-03-08 17:12:17
7-
modifiedAt: 2025-03-10 18:38:04
7+
modifiedAt: 2025-03-10 19:46:41
88
publish: project/blog
99
series: 나의 맞춤 블로그 만들기
1010
---
@@ -44,8 +44,4 @@ Python을 통해 파일 시스템을 만져본것은 처음이라 [[Aider|AI 툴
4444
> - [[Python Script로 Markdown 파일 읽어오기]]
4545
> - [[Python Script로 JSON 파일 작성하기]]
4646
47-
#### 파일을 가져와보자
48-
49-
Next.js 를 활용하여 블로그를 만들때 기본적으로 content 폴더에 파일들을 가져오게 된다.
50-
51-
#### 문제
47+
Next.js 를 활용하여 블로그를 만들때 기본적으로 content 폴더에 파일들을 가져와 빌드하게 된다.

content/posts/resource/ai/Aider.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
tags:
3+
- ai
4+
- tool
5+
- aider
6+
createdAt: 2025-03-10 08:21:08
7+
modifiedAt: 2025-03-10 18:49:05
8+
publish: resource/ai
9+
series: ""
10+
---
11+
12+
# Aider
13+
14+
공식 홈페이지:[Home | Aider](https://aider.chat/)
15+
16+
> [!quote]
17+
> Aider lets you pair program with LLMs, to edit code in your local git repository. Start a new project or work with an existing code base.
18+
19+
Aider 는 터미널을 통해 AI 와 [Pair Programming](https://namu.wiki/w/%ED%8E%98%EC%96%B4%20%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D)을 할 수 있도록 해주는 툴이다.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
tags: []
3+
createdAt: 2025-03-10 18:53:57
4+
modifiedAt: 2025-03-10 19:00:36
5+
publish: resource/python
6+
series: ""
7+
---
8+
9+
# Python Script로 JSON 파일 작성하기
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
tags: []
3+
createdAt: 2025-03-10 18:53:44
4+
modifiedAt: 2025-03-10 19:00:30
5+
publish: resource/python
6+
series: ""
7+
---
8+
9+
# Python Script로 Markdown 파일 읽어오기
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
tags: []
3+
createdAt: 2025-03-10 18:53:20
4+
modifiedAt: 2025-03-10 19:00:22
5+
publish: resource/python
6+
series: ""
7+
---
8+
9+
# Python Script로 파일 복사해오기

public/link-map.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{
22
"0.inbox/개인 블로그를 제작하게 된 이유.md": "project/blog/개인 블로그를 제작하게 된 이유",
3-
"0.inbox/어떻게 블로그를 구현하였나.md": "project/blog/어떻게 블로그를 구현하였나"
3+
"0.inbox/어떻게 블로그를 구현하였나.md": "project/blog/어떻게 블로그를 구현하였나",
4+
"0.inbox/Aider.md": "resource/ai/Aider",
5+
"0.inbox/Python Script로 파일 복사해오기.md": "resource/python/Python Script로 파일 복사해오기",
6+
"0.inbox/Python Script로 Markdown 파일 읽어오기.md": "resource/python/Python Script로 Markdown 파일 읽어오기",
7+
"0.inbox/Python Script로 JSON 파일 작성하기.md": "resource/python/Python Script로 JSON 파일 작성하기"
48
}

public/meta-data.json

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,46 @@
1717
"tags": ["blog", "dev", "project"],
1818
"series": "나의 맞춤 블로그 만들기",
1919
"createdAt": "2025-03-08 17:12:17",
20-
"modifiedAt": "2025-03-10 18:38:04"
20+
"modifiedAt": "2025-03-10 19:46:41"
21+
},
22+
{
23+
"urlPath": "resource/ai/Aider",
24+
"title": "Aider",
25+
"summary": "",
26+
"image": "",
27+
"tags": ["ai", "tool", "aider"],
28+
"series": "",
29+
"createdAt": "2025-03-10 08:21:08",
30+
"modifiedAt": "2025-03-10 18:49:05"
31+
},
32+
{
33+
"urlPath": "resource/python/Python Script로 파일 복사해오기",
34+
"title": "Python Script로 파일 복사해오기",
35+
"summary": "",
36+
"image": "",
37+
"tags": [],
38+
"series": "",
39+
"createdAt": "2025-03-10 18:53:20",
40+
"modifiedAt": "2025-03-10 19:00:22"
41+
},
42+
{
43+
"urlPath": "resource/python/Python Script로 Markdown 파일 읽어오기",
44+
"title": "Python Script로 Markdown 파일 읽어오기",
45+
"summary": "",
46+
"image": "",
47+
"tags": [],
48+
"series": "",
49+
"createdAt": "2025-03-10 18:53:44",
50+
"modifiedAt": "2025-03-10 19:00:30"
51+
},
52+
{
53+
"urlPath": "resource/python/Python Script로 JSON 파일 작성하기",
54+
"title": "Python Script로 JSON 파일 작성하기",
55+
"summary": "",
56+
"image": "",
57+
"tags": [],
58+
"series": "",
59+
"createdAt": "2025-03-10 18:53:57",
60+
"modifiedAt": "2025-03-10 19:00:36"
2161
}
2262
]

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"compilerOptions": {
3-
"target": "ES2017",
3+
"target": "ES2018",
44
"lib": ["dom", "dom.iterable", "esnext"],
55
"allowJs": true,
66
"skipLibCheck": true,

0 commit comments

Comments
 (0)