From 62ceaa3591a90cb0f4a82a62e1b33fb6338b3dd9 Mon Sep 17 00:00:00 2001 From: zenn Date: Tue, 26 May 2026 11:18:44 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=ED=99=94=EB=A9=B4=EC=9D=84=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=9E=91=EC=84=B1=20=ED=99=94=EB=A9=B4=EA=B3=BC=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20v1.5.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/AdminPageForm.vue | 419 +++++++++++++++++++++-------- docs/changelog.md | 4 + docs/history.md | 4 + docs/map.md | 6 +- docs/spec.md | 4 +- docs/update.md | 7 + layouts/admin.vue | 14 +- package-lock.json | 4 +- package.json | 2 +- pages/admin/pages/[id].vue | 47 ++-- pages/admin/pages/new.vue | 16 +- 11 files changed, 357 insertions(+), 170 deletions(-) diff --git a/components/admin/AdminPageForm.vue b/components/admin/AdminPageForm.vue index 7754d9c..1bd7107 100644 --- a/components/admin/AdminPageForm.vue +++ b/components/admin/AdminPageForm.vue @@ -4,25 +4,40 @@ const props = defineProps({ type: Object, default: () => ({}) }, - submitLabel: { - type: String, - default: '저장' - }, saving: { type: Boolean, default: false + }, + deleting: { + type: Boolean, + default: false + }, + canViewPage: { + type: Boolean, + default: false + }, + publicUrl: { + type: String, + default: '' + }, + showDelete: { + type: Boolean, + default: false } }) -const emit = defineEmits(['submit']) +const emit = defineEmits(['submit', 'delete']) const slugTouched = ref(Boolean(props.initialPage.slug)) const blockEditor = ref(null) const htmlEditor = ref(null) +const editorMode = ref('write') const mediaItems = ref([]) const isMediaPickerOpen = ref(false) const isLoadingMedia = ref(false) const isUploadingFeaturedImage = ref(false) +const isSettingsOpen = ref(true) +const savedPageSnapshot = ref('') const form = reactive({ title: props.initialPage.title || '', @@ -45,6 +60,37 @@ const toSlug = (value) => value .replace(/-+/g, '-') .replace(/^-|-$/g, '') +const pageSlug = computed(() => toSlug(form.slug || form.title)) +const viewPageUrl = computed(() => props.publicUrl || (pageSlug.value ? `/pages/${pageSlug.value}` : '')) +const pageUrlHint = computed(() => `/pages/${pageSlug.value || 'page-slug'}/`) + +/** + * 페이지 폼의 저장 비교용 문자열을 생성한다. + * @returns {string} 직렬화된 폼 상태 + */ +const serializePageForm = () => JSON.stringify({ + title: form.title.trim(), + slug: pageSlug.value, + renderMode: form.renderMode, + content: form.content, + featuredImage: form.featuredImage.trim() || null +}) + +const hasUnsavedPageChanges = computed(() => serializePageForm() !== savedPageSnapshot.value) + +const headerStatusText = computed(() => { + if (props.saving) { + return 'Saving...' + } + if (hasUnsavedPageChanges.value) { + return 'Unsaved changes' + } + if (props.initialPage.id) { + return 'Saved' + } + return 'New page' +}) + watch(() => form.title, (title) => { if (!slugTouched.value) { form.slug = toSlug(title) @@ -60,6 +106,14 @@ const touchSlug = () => { form.slug = toSlug(form.slug) } +/** + * 페이지 설정 패널을 토글한다. + * @returns {void} + */ +const toggleSettingsPanel = () => { + isSettingsOpen.value = !isSettingsOpen.value +} + /** * 미디어 라이브러리 목록 조회 * @returns {Promise} @@ -139,9 +193,12 @@ const uploadFeaturedImage = async (event) => { /** * 제목 입력 후 본문 에디터로 이동 + * @param {KeyboardEvent} event - 키보드 이벤트 * @returns {void} */ -const focusContentEditor = () => { +const focusContentEditor = (event) => { + event?.preventDefault() + if (form.renderMode === 'html_document') { htmlEditor.value?.focus() return @@ -159,45 +216,140 @@ const setRenderMode = (mode) => { form.renderMode = mode } +/** + * 페이지 입력값을 생성한다. + * @returns {Object} 페이지 입력값 + */ +const createPayload = () => ({ + title: form.title.trim(), + slug: pageSlug.value, + renderMode: form.renderMode, + content: form.content, + featuredImage: form.featuredImage.trim() || null +}) + /** * 페이지 입력값 제출 * @returns {void} */ const submitPage = () => { - emit('submit', { - title: form.title.trim(), - slug: toSlug(form.slug || form.title), - renderMode: form.renderMode, - content: form.content, - featuredImage: form.featuredImage.trim() || null - }) + emit('submit', createPayload()) } + +/** + * 현재 폼 상태를 저장 완료 기준점으로 표시한다. + * @returns {void} + */ +const markSaved = () => { + savedPageSnapshot.value = serializePageForm() +} + +onMounted(markSaved) + +defineExpose({ + markSaved +})