v0.0.48 Thred형 북마크·회원가입 카드와 X 임베드 보강
북마크·뉴스레터 CTA 마크다운 블록과 컴포넌트를 추가하고, Twitter/X URL은 공식 embed iframe으로 렌더링한다. Callout 강조선과 이미지 캡션 색을 테마 변수에 맞춘다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,7 +29,8 @@ const createBlock = (type = 'paragraph', text = '', level = null, id = '', optio
|
||||
variant: options.variant || '',
|
||||
ordered: options.ordered || false,
|
||||
width: options.width || 'regular',
|
||||
images: options.images || []
|
||||
images: options.images || [],
|
||||
meta: options.meta && typeof options.meta === 'object' ? { ...options.meta } : {}
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -72,6 +73,96 @@ const collectFencedLines = (lines, startIndex) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 북마크 fenced 블록 본문에서 URL·제목·설명·썸네일을 파싱한다.
|
||||
* @param {string} raw - fenced 내부 텍스트
|
||||
* @returns {{url: string, title: string, description: string, thumbnail: string}} 북마크 메타
|
||||
*/
|
||||
const parseBookmarkMeta = (raw) => {
|
||||
const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean)
|
||||
const meta = {
|
||||
url: '',
|
||||
title: '',
|
||||
description: '',
|
||||
thumbnail: ''
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const kv = line.match(/^(\w+)=(.*)$/)
|
||||
|
||||
if (kv) {
|
||||
const key = kv[1].toLowerCase()
|
||||
const val = kv[2].trim()
|
||||
|
||||
if (key === 'url') {
|
||||
meta.url = val
|
||||
} else if (key === 'title') {
|
||||
meta.title = val
|
||||
} else if (key === 'description' || key === 'desc') {
|
||||
meta.description = val
|
||||
} else if (key === 'thumbnail' || key === 'image') {
|
||||
meta.thumbnail = val
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (!meta.url && /^https?:\/\//i.test(line)) {
|
||||
meta.url = line
|
||||
continue
|
||||
}
|
||||
|
||||
if (meta.url && !meta.title) {
|
||||
meta.title = line
|
||||
continue
|
||||
}
|
||||
|
||||
if (meta.url && meta.title && !meta.description) {
|
||||
meta.description = line
|
||||
}
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원가입(CTA) fenced 블록 본문에서 표시 문구를 파싱한다.
|
||||
* @param {string} raw - fenced 내부 텍스트
|
||||
* @returns {{title: string, description: string, button: string, placeholder: string}} CTA 메타
|
||||
*/
|
||||
const parseSignupMeta = (raw) => {
|
||||
const meta = {
|
||||
title: '뉴스레터에 가입하세요',
|
||||
description: '새 글이 올라오면 받아보실 수 있어요.',
|
||||
button: '구독하기',
|
||||
placeholder: 'you@example.com'
|
||||
}
|
||||
const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean)
|
||||
|
||||
for (const line of lines) {
|
||||
const kv = line.match(/^(\w+)=(.*)$/)
|
||||
|
||||
if (!kv) {
|
||||
continue
|
||||
}
|
||||
|
||||
const key = kv[1].toLowerCase()
|
||||
const val = kv[2].trim()
|
||||
|
||||
if (key === 'title') {
|
||||
meta.title = val
|
||||
} else if (key === 'description' || key === 'desc') {
|
||||
meta.description = val
|
||||
} else if (key === 'button') {
|
||||
meta.button = val
|
||||
} else if (key === 'placeholder') {
|
||||
meta.placeholder = val
|
||||
}
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
/**
|
||||
* 마크다운 문자열을 렌더링용 블록 목록으로 변환
|
||||
* @param {string} markdown - 마크다운 문자열
|
||||
@@ -105,6 +196,26 @@ const parseMarkdownBlocks = (markdown) => {
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine === ':::bookmark') {
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
const bookmarkMeta = parseBookmarkMeta(contentLines.join('\n'))
|
||||
|
||||
if (bookmarkMeta.url) {
|
||||
blocks.push(createBlock('bookmark', '', null, `block-${blocks.length}`, { meta: bookmarkMeta }))
|
||||
}
|
||||
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine === ':::signup') {
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
const signupMeta = parseSignupMeta(contentLines.join('\n'))
|
||||
blocks.push(createBlock('signup', '', null, `block-${blocks.length}`, { meta: signupMeta }))
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
|
||||
if (trimmedLine === ':::gallery') {
|
||||
const { contentLines, nextIndex } = collectFencedLines(lines, index + 1)
|
||||
const images = []
|
||||
@@ -287,6 +398,20 @@ const showNextImage = () => {
|
||||
<ProseToggle v-else-if="block.type === 'toggle'" :title="block.title || '더 보기'">
|
||||
{{ block.text }}
|
||||
</ProseToggle>
|
||||
<ProseBookmark
|
||||
v-else-if="block.type === 'bookmark' && block.meta.url"
|
||||
:url="block.meta.url"
|
||||
:title="block.meta.title"
|
||||
:description="block.meta.description"
|
||||
:thumbnail="block.meta.thumbnail"
|
||||
/>
|
||||
<ProseSignup
|
||||
v-else-if="block.type === 'signup'"
|
||||
:title="block.meta.title"
|
||||
:description="block.meta.description"
|
||||
:button-label="block.meta.button"
|
||||
:placeholder="block.meta.placeholder"
|
||||
/>
|
||||
<ProseEmbed v-else-if="block.type === 'embed'" :url="block.url" />
|
||||
<div v-else-if="block.type === 'gallery'" class="content-markdown-renderer__gallery my-8 grid grid-cols-2 gap-2 md:grid-cols-3">
|
||||
<button
|
||||
|
||||
95
components/content/ProseBookmark.vue
Normal file
95
components/content/ProseBookmark.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
thumbnail: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 북마크 카드에 표시할 호스트명을 반환한다.
|
||||
* @returns {string} www 없는 호스트 또는 빈 문자열
|
||||
*/
|
||||
const displayHost = computed(() => {
|
||||
try {
|
||||
return new URL(props.url).hostname.replace(/^www\./, '')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 썸네일이 비었을 때 파비콘 보조 URL을 만든다.
|
||||
* @returns {string} favicon 요청 URL
|
||||
*/
|
||||
const faviconUrl = computed(() => {
|
||||
if (!displayHost.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(displayHost.value)}&sz=128`
|
||||
})
|
||||
|
||||
/**
|
||||
* 실제로 표시할 이미지 주소
|
||||
* @returns {string}
|
||||
*/
|
||||
const imageSrc = computed(() => props.thumbnail || faviconUrl.value)
|
||||
|
||||
/**
|
||||
* 표시 제목(없으면 호스트·URL)
|
||||
* @returns {string}
|
||||
*/
|
||||
const displayTitle = computed(() => props.title || displayHost.value || props.url)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a
|
||||
class="prose-bookmark group prose-bookmark-card my-8 flex max-w-full flex-col overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] no-underline transition-[background-color,box-shadow] hover:bg-[color-mix(in_srgb,var(--site-panel)_86%,var(--site-text)_14%)] sm:flex-row"
|
||||
:href="url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div class="prose-bookmark__media relative h-36 w-full shrink-0 overflow-hidden bg-[color-mix(in_srgb,var(--site-line)_40%,var(--site-panel))] sm:h-auto sm:w-[min(44%,220px)] sm:min-h-[9rem]">
|
||||
<img
|
||||
v-if="imageSrc"
|
||||
class="prose-bookmark__thumb h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
||||
:src="imageSrc"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
>
|
||||
</div>
|
||||
<div class="prose-bookmark__body flex min-w-0 flex-1 flex-col justify-center gap-1 px-4 py-4 sm:px-5 sm:py-5">
|
||||
<p v-if="displayHost" class="prose-bookmark__host text-[11px] font-semibold uppercase tracking-[0.06em] text-[var(--site-muted)]">
|
||||
{{ displayHost }}
|
||||
</p>
|
||||
<p class="prose-bookmark__title text-[15px] font-semibold leading-snug text-[var(--site-text)]">
|
||||
{{ displayTitle }}
|
||||
</p>
|
||||
<p v-if="description" class="prose-bookmark__desc line-clamp-2 text-sm leading-relaxed text-[var(--site-muted)]">
|
||||
{{ description }}
|
||||
</p>
|
||||
<p class="prose-bookmark__meta mt-1 flex items-center gap-1.5 text-xs font-medium text-[var(--site-soft)]">
|
||||
<svg class="shrink-0 opacity-80" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 6h-6a2 2 0 0 0 -2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-6" />
|
||||
<path d="M11 13l9 -9" />
|
||||
<path d="M15 4h5v5" />
|
||||
</svg>
|
||||
<span class="truncate">{{ url }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<aside class="prose-callout my-8 rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)] p-5 text-[15px] leading-8 text-[var(--site-text)]">
|
||||
<aside class="prose-callout prose-callout-card my-8 rounded-[10px] border border-[var(--site-line)] border-l-[3px] border-l-[var(--site-accent)] bg-[var(--site-panel)] p-5 pl-4 text-[15px] leading-8 text-[var(--site-text)]">
|
||||
<div class="whitespace-pre-line">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,10 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const { theme } = useThemeMode()
|
||||
|
||||
/**
|
||||
* YouTube 영상 ID를 추출
|
||||
* YouTube 영상 ID를 추출한다.
|
||||
* @param {string} value - 임베드 URL
|
||||
* @returns {string} YouTube 영상 ID
|
||||
*/
|
||||
@@ -29,12 +31,55 @@ const getYouTubeId = (value) => {
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Twitter/X 게시물 ID를 추출한다.
|
||||
* @param {string} value - 트윗 URL
|
||||
* @returns {string} 상태 ID
|
||||
*/
|
||||
const getTweetId = (value) => {
|
||||
try {
|
||||
const trimmed = value.trim()
|
||||
const parsedUrl = new URL(trimmed)
|
||||
const host = parsedUrl.hostname.replace(/^www\./, '')
|
||||
|
||||
if (!['twitter.com', 'x.com', 'mobile.twitter.com'].includes(host)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const parts = parsedUrl.pathname.split('/').filter(Boolean)
|
||||
const statusIdx = parts.indexOf('status')
|
||||
|
||||
if (statusIdx >= 0 && parts[statusIdx + 1]) {
|
||||
return parts[statusIdx + 1].split(/[?#]/)[0] || ''
|
||||
}
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const youtubeId = computed(() => getYouTubeId(props.url))
|
||||
const youtubeEmbedUrl = computed(() => youtubeId.value ? `https://www.youtube.com/embed/${youtubeId.value}` : '')
|
||||
const tweetId = computed(() => getTweetId(props.url))
|
||||
|
||||
/**
|
||||
* Twitter 공식 embed iframe 주소
|
||||
* @returns {string}
|
||||
*/
|
||||
const tweetEmbedUrl = computed(() => {
|
||||
if (!tweetId.value) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const twitterTheme = theme.value === 'dark' ? 'dark' : 'light'
|
||||
|
||||
return `https://platform.twitter.com/embed/Tweet.html?id=${encodeURIComponent(tweetId.value)}&theme=${twitterTheme}&dnt=true`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="prose-embed my-8 overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]">
|
||||
<div class="prose-embed prose-embed-card my-8 overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]">
|
||||
<iframe
|
||||
v-if="youtubeEmbedUrl"
|
||||
class="prose-embed__frame aspect-video w-full"
|
||||
@@ -44,6 +89,14 @@ const youtubeEmbedUrl = computed(() => youtubeId.value ? `https://www.youtube.co
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
/>
|
||||
<iframe
|
||||
v-else-if="tweetEmbedUrl"
|
||||
:key="tweetEmbedUrl"
|
||||
class="prose-embed__tweet min-h-[420px] w-full border-0 sm:min-h-[458px]"
|
||||
:src="tweetEmbedUrl"
|
||||
title="Embedded post"
|
||||
loading="lazy"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
class="prose-embed__link block p-5 text-sm font-semibold text-[var(--site-text)] hover:opacity-70"
|
||||
|
||||
@@ -26,7 +26,7 @@ defineProps({
|
||||
<div class="overflow-hidden rounded-[10px] border border-[var(--site-line)] bg-[var(--site-panel)]">
|
||||
<img class="prose-image__media w-full object-cover" :src="src" :alt="alt">
|
||||
</div>
|
||||
<figcaption v-if="$slots.default" class="prose-image__caption mt-3 text-center text-sm text-muted">
|
||||
<figcaption v-if="$slots.default" class="prose-image__caption mt-3 text-center text-sm text-[var(--site-muted)]">
|
||||
<slot />
|
||||
</figcaption>
|
||||
</figure>
|
||||
|
||||
44
components/content/ProseSignup.vue
Normal file
44
components/content/ProseSignup.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '뉴스레터에 가입하세요'
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '새 글이 올라오면 받아보실 수 있어요.'
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '구독하기'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'you@example.com'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="prose-signup prose-signup-card my-8 rounded-[12px] border border-[var(--site-line)] bg-[var(--site-panel-strong)] px-5 py-8 text-center sm:px-8">
|
||||
<h3 class="prose-signup__title text-[clamp(1rem,0.95rem+0.25vw,1.125rem)] font-semibold leading-snug text-[var(--site-text)]">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="prose-signup__desc mt-2 text-[15px] leading-7 text-[var(--site-muted)]">
|
||||
{{ description }}
|
||||
</p>
|
||||
<form class="prose-signup__form mt-6 flex flex-col items-stretch gap-2.5 sm:flex-row sm:justify-center" action="#" @submit.prevent>
|
||||
<input
|
||||
class="site-input prose-signup__input min-h-[44px] w-full rounded-full px-4 text-sm sm:max-w-[300px] sm:flex-1"
|
||||
type="email"
|
||||
:placeholder="placeholder"
|
||||
readonly
|
||||
tabindex="-1"
|
||||
aria-label="이메일"
|
||||
>
|
||||
<button class="site-accent-button prose-signup__submit min-h-[44px] shrink-0 rounded-full px-8 text-sm font-semibold" type="button">
|
||||
{{ buttonLabel }}
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,5 +1,13 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-05-08 v0.0.48
|
||||
|
||||
### Twitter/X 공식 embed iframe과 북마크·회원가입 마크다운 블록
|
||||
|
||||
공개 본문에서 트위터 게시물은 `platform.twitter.com/embed/Tweet.html` iframe으로 표시한다. oEmbed API나 스크립트 삽입에 비해 구현이 단순하고 SSR·테마(`useThemeMode`)와 `theme` 쿼리만 맞추면 라이트/다크 일관성을 유지하기 쉽기 때문이다.
|
||||
|
||||
북마크·뉴스레터 CTA는 Ghost/Thred 스킨에서 흔한 카드 패턴이므로 `:::bookmark`·`:::signup` 확장 블록으로 저장하고 전용 Vue 컴포넌트로 렌더링한다. 메타데이터 풍부한 북마크는 추후 oEmbed나 서버 페치로 보강할 수 있도록 마크다운 키값 형식을 병행한다.
|
||||
|
||||
## 2026-05-07 v0.0.45
|
||||
|
||||
### 사용자 화면 단일 배경과 사이드바 전환 방식 결정
|
||||
|
||||
@@ -48,8 +48,10 @@
|
||||
| components/content/ProseAudio.vue | 오디오 |
|
||||
| components/content/ProseFile.vue | 파일 |
|
||||
| components/content/ProseProduct.vue | 상품 카드 |
|
||||
| components/content/ProseBookmark.vue | 본문 북마크 카드(썸네일·도메인·외부 링크) |
|
||||
| components/content/ProseSignup.vue | 본문 회원가입·뉴스레터 CTA 카드 |
|
||||
| components/content/ProseHeaderCard.vue | 헤더 카드 |
|
||||
| components/content/ProseEmbed.vue | YouTube, Twitter 임베드 |
|
||||
| components/content/ProseEmbed.vue | YouTube·Twitter/X iframe 임베드, 기타 외부 링크 |
|
||||
|
||||
## 관리자 페이지
|
||||
|
||||
|
||||
20
docs/spec.md
20
docs/spec.md
@@ -99,8 +99,10 @@ components/content/
|
||||
├── ProseAudio.vue # Audio 카드
|
||||
├── ProseFile.vue # File 카드
|
||||
├── ProseProduct.vue # Product 카드
|
||||
├── ProseBookmark.vue # 북마크 카드(썸네일·제목·도메인)
|
||||
├── ProseSignup.vue # 회원가입/뉴스레터 CTA 카드
|
||||
├── ProseHeaderCard.vue # Header 카드 (Simple/Wide/Full-width/Split)
|
||||
└── ProseEmbed.vue # Embeds (YouTube, Twitter)
|
||||
└── ProseEmbed.vue # Embeds (YouTube iframe, Twitter/X iframe, 기타 링크)
|
||||
```
|
||||
|
||||
### 공개 본문 스타일 가이드(Thred 기준)
|
||||
@@ -121,10 +123,12 @@ components/content/
|
||||
- `:::gallery` ~ `:::` fenced block 내부에 이미지 마크다운 행을 여러 개 작성
|
||||
- 렌더링: `ContentMarkdownRenderer.vue` (그리드 + 라이트박스)
|
||||
- 카드류
|
||||
- Callout: `:::callout` ~ `:::`
|
||||
- Callout: `:::callout` ~ `:::` (왼쪽 강조선은 `var(--site-accent)`)
|
||||
- Toggle: `:::toggle 제목` ~ `:::`
|
||||
- Embed: `:::embed` ~ `:::` (YouTube URL은 iframe, 그 외는 링크)
|
||||
- 렌더링: `ProseCallout.vue`, `ProseToggle.vue`, `ProseEmbed.vue`
|
||||
- Bookmark: `:::bookmark` ~ `:::` (본문은 `url=`, `title=`, `description=`, `thumbnail=` 키값 또는 첫 줄 URL·이어지는 제목/설명 줄)
|
||||
- Signup: `:::signup` ~ `:::` (선택: `title=`, `description=`, `button=`, `placeholder=`)
|
||||
- Embed: `:::embed` ~ `:::` (YouTube·YouTube Shorts URL은 iframe, `twitter.com`·`x.com` 게시물 URL은 X 공식 embed iframe, 그 외는 외부 링크 카드)
|
||||
- 렌더링: `ProseCallout.vue`, `ProseToggle.vue`, `ProseBookmark.vue`, `ProseSignup.vue`, `ProseEmbed.vue`
|
||||
|
||||
---
|
||||
|
||||
@@ -365,7 +369,11 @@ components/content/
|
||||
- 콜아웃 블록은 `:::callout` fenced block 안에 본문을 저장한다.
|
||||
- 토글 블록은 `:::toggle 제목` fenced block 안에 펼침 본문을 저장한다.
|
||||
- 임베드 블록은 `:::embed` fenced block 안에 URL을 저장한다.
|
||||
- YouTube 임베드 URL은 공개 화면에서 iframe으로 렌더링하고, 그 외 URL은 외부 링크로 표시한다.
|
||||
- YouTube 임베드 URL은 공개 화면에서 iframe으로 렌더링한다.
|
||||
- Twitter/X 게시물 URL(`twitter.com`·`x.com`·`mobile.twitter.com`, 경로에 `status` 포함)은 `platform.twitter.com/embed/Tweet.html` iframe으로 렌더링하며, 테마는 `useThemeMode()`와 동기화한다.
|
||||
- 그 외 URL은 외부 링크 텍스트 카드로 표시한다.
|
||||
- 북마크 블록은 `:::bookmark` fenced block으로 저장할 수 있으며 공개 화면에서 Thred형 가로 카드로 렌더링한다.
|
||||
- 회원가입(뉴스레터) CTA는 `:::signup` fenced block으로 저장할 수 있으며 실제 폼 연동은 후속 작업으로 분리한다.
|
||||
|
||||
### 관리자 페이지 편집
|
||||
|
||||
@@ -481,6 +489,6 @@ APP_PORT=43118
|
||||
|
||||
## 버전 관리
|
||||
|
||||
- 현재 버전: v0.0.47
|
||||
- 현재 버전: v0.0.48
|
||||
- 첫 커밋 이후 변경사항을 커밋할 때마다 패치 버전 증가
|
||||
- 메이저/마이너 버전은 구조 변경 또는 기능 묶음 단위로 결정
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
- [ ] ProseFile 실제 파일 데이터 연결
|
||||
- [ ] ProseProduct 실제 상품 카드 데이터 연결
|
||||
- [ ] ProseHeaderCard Simple/Wide/Full-width/Split 변형 구현
|
||||
- [ ] ProseEmbed Twitter 실제 렌더링 연결
|
||||
|
||||
## 데이터베이스
|
||||
|
||||
- [ ] NAS 운영 DB 연결 설정 실제 값 작성
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v0.0.48
|
||||
|
||||
- 공개 본문에 Thred형 북마크 카드(`ProseBookmark`, `:::bookmark`)와 회원가입 CTA(`ProseSignup`, `:::signup`) 추가.
|
||||
- `ProseEmbed`에서 Twitter/X 게시물 URL을 공식 embed iframe으로 렌더링하고 테마와 동기화.
|
||||
- Callout 왼쪽 강조선·이미지 캡션 변수 색상 등 카드/미디어 스타일을 Thred에 가깝게 보정.
|
||||
|
||||
## v0.0.47
|
||||
|
||||
- 공개 본문 렌더러에서 Ordered list(숫자 목록) 문법을 지원.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "0.0.47",
|
||||
"version": "0.0.48",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user