v0.0.86: 미리보기 패딩, 태그 한글 유지, SEO 자동, 태그 관리 토스트
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user