관리자 에디터를 마크다운 우선 방식으로 개편

This commit is contained in:
2026-05-14 15:12:23 +09:00
parent 88a0860078
commit eab81697e5
11 changed files with 706 additions and 41 deletions

View File

@@ -389,6 +389,63 @@ const parseMarkdownBlocks = (markdown) => {
const blocks = computed(() => parseMarkdownBlocks(props.content))
const activeLightboxImage = computed(() => activeLightboxImages.value[activeLightboxIndex.value])
/**
* 인라인 마크다운을 표시 세그먼트로 변환한다.
* @param {string} value - 원본 문자열
* @returns {Array<{ type: string, text: string, href?: string }>} 인라인 세그먼트
*/
const parseInlineSegments = (value) => {
const source = String(value || '')
const segments = []
const pattern = /(\[([^\]]+)\]\((https?:\/\/[^)\s]+|\/[^)\s]+)\)|\*\*([^*]+)\*\*|`([^`]+)`|\*([^*]+)\*)/g
let lastIndex = 0
let match = pattern.exec(source)
while (match) {
if (match.index > lastIndex) {
segments.push({
type: 'text',
text: source.slice(lastIndex, match.index)
})
}
if (match[2] && match[3]) {
segments.push({
type: 'link',
text: match[2],
href: match[3]
})
} else if (match[4]) {
segments.push({
type: 'strong',
text: match[4]
})
} else if (match[5]) {
segments.push({
type: 'code',
text: match[5]
})
} else if (match[6]) {
segments.push({
type: 'em',
text: match[6]
})
}
lastIndex = pattern.lastIndex
match = pattern.exec(source)
}
if (lastIndex < source.length) {
segments.push({
type: 'text',
text: source.slice(lastIndex)
})
}
return segments.length ? segments : [{ type: 'text', text: source }]
}
/**
* 라이트박스를 연다
* @param {Array<Object>} images - 이미지 목록
@@ -432,14 +489,32 @@ const showNextImage = () => {
<div class="content-markdown-renderer">
<template v-for="block in blocks" :key="block.id">
<ProseHeading v-if="block.type === 'heading'" :level="block.level">
{{ block.text }}
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-heading-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<code v-else-if="segment.type === 'code'" class="rounded bg-[var(--site-panel)] px-1 py-0.5 text-[0.9em]">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
</ProseHeading>
<ProseBlockquote v-else-if="block.type === 'quote'" :variant="block.variant || 'default'">
{{ block.text }}
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-quote-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<code v-else-if="segment.type === 'code'" class="rounded bg-[var(--site-panel)] px-1 py-0.5 text-[0.9em]">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
</ProseBlockquote>
<ProseList v-else-if="block.type === 'list'" :ordered="block.ordered || false">
<li v-for="(item, itemIndex) in block.text" :key="`${block.id}-${itemIndex}`">
{{ item }}
<template v-for="(segment, segmentIndex) in parseInlineSegments(item)" :key="`${block.id}-${itemIndex}-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<code v-else-if="segment.type === 'code'" class="rounded bg-[var(--site-panel)] px-1 py-0.5 text-[0.9em]">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
</li>
</ProseList>
<ProseImage v-else-if="block.type === 'image'" :src="block.url" :alt="block.alt" :variant="block.width">
@@ -451,10 +526,22 @@ const showNextImage = () => {
:emoji="block.calloutEmoji"
:background="block.calloutBackground"
>
{{ block.text }}
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-callout-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<code v-else-if="segment.type === 'code'" class="rounded bg-black/5 px-1 py-0.5 text-[0.9em]">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
</ProseCallout>
<ProseToggle v-else-if="block.type === 'toggle'" :title="block.title || '더 보기'">
{{ block.text }}
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-toggle-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<code v-else-if="segment.type === 'code'" class="rounded bg-[var(--site-panel)] px-1 py-0.5 text-[0.9em]">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
</ProseToggle>
<ProseBookmark
v-else-if="block.type === 'bookmark' && block.meta.url"
@@ -488,7 +575,13 @@ const showNextImage = () => {
><code>{{ block.text }}</code></pre>
<hr v-else-if="block.type === 'divider'" class="content-markdown-renderer__divider my-10 border-line">
<p v-else class="content-markdown-renderer__paragraph my-5 text-[15px] leading-8 text-[var(--site-text)]">
{{ block.text }}
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-paragraph-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>
<code v-else-if="segment.type === 'code'" class="rounded bg-[var(--site-panel)] px-1 py-0.5 text-[0.9em]">{{ segment.text }}</code>
<a v-else-if="segment.type === 'link'" class="text-[var(--site-accent)] underline underline-offset-4" :href="segment.href" target="_blank" rel="noreferrer">{{ segment.text }}</a>
<template v-else>{{ segment.text }}</template>
</template>
</p>
</template>