From 8c5ccc94ec194f32313d0ab80e6bce74dc06bcb3 Mon Sep 17 00:00:00 2001
From: zenn
Date: Sun, 3 May 2026 10:10:09 +0900
Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EB=AC=BC=20OG=20=EC=9D=B4?=
=?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components/admin/AdminPostForm.vue | 91 +++++++++++++++++++++--
db/migrations/009_add_post_og_image.sql | 2 +
docs/history.md | 8 ++
docs/map.md | 5 +-
docs/spec.md | 5 +-
docs/todo.md | 1 -
docs/update.md | 8 ++
package-lock.json | 4 +-
package.json | 2 +-
pages/post/[slug].vue | 32 +++++++-
server/repositories/content-repository.js | 4 +
server/utils/admin-post-input.js | 1 +
server/utils/content-schema.js | 1 +
server/utils/media-library.js | 7 ++
14 files changed, 157 insertions(+), 14 deletions(-)
create mode 100644 db/migrations/009_add_post_og_image.sql
diff --git a/components/admin/AdminPostForm.vue b/components/admin/AdminPostForm.vue
index 1d59548..fc14722 100644
--- a/components/admin/AdminPostForm.vue
+++ b/components/admin/AdminPostForm.vue
@@ -21,8 +21,10 @@ const slugTouched = ref(Boolean(props.initialPost.slug))
const blockEditor = ref(null)
const mediaItems = ref([])
const isMediaPickerOpen = ref(false)
+const mediaPickerTarget = ref('featuredImage')
const isLoadingMedia = ref(false)
const isUploadingFeaturedImage = ref(false)
+const isUploadingOgImage = ref(false)
const autosaveTimer = ref(null)
const autosaveNotice = ref(null)
const autosaveStatus = ref('')
@@ -78,6 +80,7 @@ const form = reactive({
seoDescription: props.initialPost.seoDescription || '',
canonicalUrl: props.initialPost.canonicalUrl || '',
noindex: Boolean(props.initialPost.noindex),
+ ogImage: props.initialPost.ogImage || '',
status: props.initialPost.status || 'draft',
publishedAt: toDateTimeLocalValue(props.initialPost.publishedAt),
tagsText: props.initialPost.tags?.join(', ') || ''
@@ -152,6 +155,7 @@ const createPostPayload = () => {
seoDescription: form.seoDescription.trim(),
canonicalUrl: form.canonicalUrl.trim(),
noindex: form.noindex,
+ ogImage: form.ogImage.trim() || null,
status: form.status,
publishedAt,
tags: parseTags(form.tagsText)
@@ -172,6 +176,7 @@ const createAutosavePayload = () => ({
seoDescription: form.seoDescription,
canonicalUrl: form.canonicalUrl,
noindex: form.noindex,
+ ogImage: form.ogImage,
status: form.status,
publishedAt: form.publishedAt,
tagsText: form.tagsText
@@ -188,6 +193,10 @@ const isEmptyAutosavePayload = (payload) => ![
payload.excerpt,
payload.content,
payload.featuredImage,
+ payload.seoTitle,
+ payload.seoDescription,
+ payload.canonicalUrl,
+ payload.ogImage,
payload.tagsText
].some((value) => String(value || '').trim())
@@ -293,7 +302,8 @@ const fetchMediaItems = async () => {
* 대표 이미지 선택 창 열기
* @returns {Promise}
*/
-const openMediaPicker = async () => {
+const openMediaPicker = async (target = 'featuredImage') => {
+ mediaPickerTarget.value = target
isMediaPickerOpen.value = true
await fetchMediaItems()
}
@@ -311,8 +321,8 @@ const closeMediaPicker = () => {
* @param {Object} item - 미디어 항목
* @returns {void}
*/
-const selectFeaturedImage = (item) => {
- form.featuredImage = item.url
+const selectPickedImage = (item) => {
+ form[mediaPickerTarget.value] = item.url
closeMediaPicker()
}
@@ -324,6 +334,14 @@ const removeFeaturedImage = () => {
form.featuredImage = ''
}
+/**
+ * OG 이미지 삭제
+ * @returns {void}
+ */
+const removeOgImage = () => {
+ form.ogImage = ''
+}
+
/**
* 대표 이미지 파일 업로드
* @param {Event} event - 파일 입력 이벤트
@@ -352,6 +370,34 @@ const uploadFeaturedImage = async (event) => {
}
}
+/**
+ * OG 이미지 파일 업로드
+ * @param {Event} event - 파일 입력 이벤트
+ * @returns {Promise}
+ */
+const uploadOgImage = async (event) => {
+ const files = event.target.files
+
+ if (!files?.length) {
+ return
+ }
+
+ const formData = new FormData()
+ formData.append('files', files[0])
+ isUploadingOgImage.value = true
+
+ try {
+ const result = await $fetch('/admin/api/uploads', {
+ method: 'POST',
+ body: formData
+ })
+ form.ogImage = result.files?.[0]?.url || ''
+ } finally {
+ event.target.value = ''
+ isUploadingOgImage.value = false
+ }
+}
+
/**
* 제목 입력 후 본문 에디터로 이동
* @returns {void}
@@ -557,7 +603,7 @@ defineExpose({
{{ form.featuredImage }}
-
+
+
+
OG 이미지
+
+
+
+
+ {{ form.ogImage }}
+
+
+
+ 변경
+
+
+
+ 삭제
+
+
+
+
+
+
+ 미디어에서 선택
+
+
+
+
@@ -609,7 +688,7 @@ defineExpose({