diff --git a/components/admin/AdminPostForm.vue b/components/admin/AdminPostForm.vue index 90a40a6..a98f18a 100644 --- a/components/admin/AdminPostForm.vue +++ b/components/admin/AdminPostForm.vue @@ -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} 태그 슬러그 목록 + * @returns {Array} 태그 토큰 목록 */ -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({ -
+

- SEO + 검색 노출

- 검색 결과와 공유 미리보기에 사용할 기본 메타 정보를 설정합니다. + 메타 제목·설명은 저장 시 글 제목과 요약을 그대로 사용합니다.

- - -