v0.0.86: 미리보기 패딩, 태그 한글 유지, SEO 자동, 태그 관리 토스트

This commit is contained in:
2026-05-12 09:56:52 +09:00
parent bd71ca860c
commit 79fb354d91
10 changed files with 212 additions and 102 deletions

View File

@@ -96,8 +96,6 @@ const form = reactive({
excerpt: props.initialPost.excerpt || '',
content: props.initialPost.content || '',
featuredImage: props.initialPost.featuredImage || '',
seoTitle: props.initialPost.seoTitle || '',
seoDescription: props.initialPost.seoDescription || '',
noindex: Boolean(props.initialPost.noindex),
status: props.initialPost.status || 'draft',
publishedAt: toDateTimeLocalValue(props.initialPost.publishedAt),
@@ -150,6 +148,24 @@ const toSlug = (value) => value
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
/**
* 게시물 태그 입력 토큰 정규화(한글 유지, 공백은 하이픈으로)
* @param {string} value - 원본 문자열
* @returns {string} 정규화된 태그 문자열
*/
const normalizeTagToken = (value) => {
const raw = String(value).normalize('NFC').trim().toLowerCase()
if (!raw) {
return ''
}
return raw
.replace(/\s+/g, '-')
.replace(/[^a-z0-9가-힣-]+/g, '')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
}
watch(() => form.title, (title) => {
if (!slugTouched.value) {
form.slug = toSlug(title)
@@ -166,14 +182,29 @@ const touchSlug = () => {
}
/**
* 쉼표 구분 태그 문자열을 슬러그 배열로 변환
* 쉼표 구분 태그 문자열을 토큰 배열로 변환
* @param {string} value - 태그 입력 문자열
* @returns {Array<string>} 태그 슬러그 목록
* @returns {Array<string>} 태그 토큰 목록
*/
const parseTags = (value) => [...new Set(value
.split(',')
.map((tag) => toSlug(tag))
.filter(Boolean))]
const parseTags = (value) => {
const seen = new Set()
const out = []
for (const part of value.split(',')) {
const tag = normalizeTagToken(part)
if (!tag) {
continue
}
const dedupeKey = tag.toLowerCase()
if (seen.has(dedupeKey)) {
continue
}
seen.add(dedupeKey)
out.push(tag)
}
return out
}
const selectedTags = computed(() => parseTags(form.tagsText))
@@ -218,8 +249,8 @@ const createPostPayload = () => {
excerpt: form.excerpt.trim(),
content: form.content,
featuredImage: form.featuredImage.trim() || null,
seoTitle: form.seoTitle.trim(),
seoDescription: form.seoDescription.trim(),
seoTitle: form.title.trim(),
seoDescription: form.excerpt.trim(),
canonicalUrl: '',
noindex: form.noindex,
ogImage: null,
@@ -239,8 +270,6 @@ const createAutosavePayload = () => ({
excerpt: form.excerpt,
content: form.content,
featuredImage: form.featuredImage,
seoTitle: form.seoTitle,
seoDescription: form.seoDescription,
noindex: form.noindex,
status: form.status,
publishedAt: form.publishedAt,
@@ -258,8 +287,6 @@ const isEmptyAutosavePayload = (payload) => ![
payload.excerpt,
payload.content,
payload.featuredImage,
payload.seoTitle,
payload.seoDescription,
payload.tagsText
].some((value) => String(value || '').trim())
@@ -416,7 +443,7 @@ const removeFeaturedImage = () => {
* @returns {void}
*/
const addTagFromInput = () => {
const nextTag = toSlug(tagInput.value)
const nextTag = normalizeTagToken(tagInput.value)
if (!nextTag) {
tagInput.value = ''
@@ -794,43 +821,16 @@ defineExpose({
</label>
</div>
<div class="admin-post-form__seo grid gap-3 border-t border-[#e3e6e8] pt-5 text-sm">
<div class="admin-post-form__search-visibility grid gap-3 border-t border-[#e3e6e8] pt-5 text-sm">
<div>
<h2 class="admin-post-form__section-title text-sm font-semibold text-ink">
SEO
검색 노출
</h2>
<p class="admin-post-form__section-description mt-1 text-xs text-muted">
검색 결과와 공유 미리보기에 사용할 기본 메타 정보를 설정합니다.
메타 제목·설명은 저장 제목과 요약을 그대로 사용합니다.
</p>
</div>
<label class="admin-post-form__field grid gap-2">
<span class="admin-post-form__label font-medium">SEO 제목</span>
<input
v-model="form.seoTitle"
class="admin-post-form__input h-[38px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2 transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac] focus:outline-none"
type="text"
maxlength="80"
placeholder="비워두면 글 제목을 사용"
>
<span class="admin-post-form__hint text-xs text-muted">
{{ form.seoTitle.length }}/80
</span>
</label>
<label class="admin-post-form__field grid gap-2">
<span class="admin-post-form__label font-medium">SEO 설명</span>
<textarea
v-model="form.seoDescription"
class="admin-post-form__textarea min-h-[108px] rounded border border-[#e3e6e8] bg-[#eff1f2] px-3 py-2 transition-colors hover:border-[#c8ced3] focus:border-[#8e9cac] focus:outline-none"
maxlength="180"
placeholder="비워두면 요약을 사용"
/>
<span class="admin-post-form__hint text-xs text-muted">
{{ form.seoDescription.length }}/180
</span>
</label>
<label class="admin-post-form__checkbox flex items-start gap-2 text-sm">
<input
v-model="form.noindex"