관리자 글쓰기 슬러그와 미디어 선택 UX 개선

This commit is contained in:
2026-07-08 12:37:42 +09:00
parent 976027c808
commit eb840b4224
10 changed files with 131 additions and 21 deletions

View File

@@ -97,6 +97,7 @@ const mediaPickerPagination = ref({
})
const savedPostSnapshot = ref('')
const failedDraftAutosaveSnapshot = ref('')
const slugFieldError = ref('')
const isPublishModalOpen = ref(false)
const isUnpublishModalOpen = ref(false)
const publishStatus = ref('draft')
@@ -369,6 +370,7 @@ const slugInputValue = computed({
set: (value) => {
slugTouched.value = true
form.slug = value
slugFieldError.value = ''
}
})
@@ -743,6 +745,9 @@ const hasUnsavedPostChanges = computed(() => serializePostPayload() !== savedPos
const serializedPostFingerprint = computed(() => serializePostPayload())
/** 중복 슬러그처럼 저장을 막아야 하는 필드 오류가 있는지 여부 */
const hasBlockingSlugError = computed(() => Boolean(slugFieldError.value))
const isFailedDraftAutosavePayload = computed(() => Boolean(
failedDraftAutosaveSnapshot.value
&& failedDraftAutosaveSnapshot.value === serializedPostFingerprint.value
@@ -1494,6 +1499,9 @@ const focusContentEditor = (event) => {
* @returns {void}
*/
const submitPost = () => {
if (hasBlockingSlugError.value) {
return
}
isPublishModalOpen.value = false
isUnpublishModalOpen.value = false
emit('submit', createPostPayload({ allowNewPublishTimestamp: true }))
@@ -1504,6 +1512,9 @@ const submitPost = () => {
* @returns {void}
*/
const requestToolbarUpdate = () => {
if (hasBlockingSlugError.value) {
return
}
emit('submit', createPostPayload())
}
@@ -1579,6 +1590,9 @@ const syncPublishModalStateFromForm = () => {
* @returns {void}
*/
const openPublishModal = () => {
if (hasBlockingSlugError.value) {
return
}
syncPublishModalStateFromForm()
publishModalExpandedSection.value = null
isPublishModalOpen.value = true
@@ -1617,6 +1631,9 @@ const applyPublishSelectionToForm = () => {
* @returns {void}
*/
const submitFromPublishModal = () => {
if (hasBlockingSlugError.value) {
return
}
applyPublishSelectionToForm()
submitPost()
}
@@ -1727,6 +1744,7 @@ const unpublishModalKind = computed(() => (
const markSaved = (savedPayload = null) => {
savedPostSnapshot.value = serializePostPayload(savedPayload)
failedDraftAutosaveSnapshot.value = ''
slugFieldError.value = ''
const publishedAt = resolvePayloadPublishedAt()
if (publishedAt) {
@@ -1743,6 +1761,15 @@ const markAutosaveFailed = (failedPayload = null) => {
failedDraftAutosaveSnapshot.value = serializePostPayload(failedPayload)
}
/**
* 슬러그 입력 필드 아래에 표시할 오류 문구를 설정한다.
* @param {string} message - 오류 문구
* @returns {void}
*/
const markSlugError = (message) => {
slugFieldError.value = message
}
/**
* 디바운스 타이머를 취소하고, 즉시 서버로 보낼 초안 페이로드가 있으면 반환한다(라우트 이탈 플러시용)
* @returns {Object|null} POST/PUT 본문 또는 null
@@ -1863,6 +1890,7 @@ defineExpose({
clearAutosave: clearAutosaveLocal,
createPostPayload,
markAutosaveFailed,
markSlugError,
markSaved,
allowNextRouteLeave,
takePendingAutosavePayload
@@ -1921,8 +1949,9 @@ defineExpose({
<template v-if="persistedPublishKind === 'draft'">
<template v-if="isDraftLike">
<button
class="admin-post-form__toolbar-publish rounded px-3 py-1.5 text-sm font-bold text-[#2BBA3C] transition-colors hover:bg-[#f1f3f4]"
class="admin-post-form__toolbar-publish rounded px-3 py-1.5 text-sm font-bold transition-colors disabled:cursor-default disabled:text-[#8E9CAC] disabled:hover:bg-transparent enabled:text-[#2BBA3C] enabled:hover:bg-[#f1f3f4]"
type="button"
:disabled="hasBlockingSlugError || saving || autoSaving"
@click="openPublishModal"
>
Publish
@@ -1932,7 +1961,7 @@ defineExpose({
v-else
class="admin-post-form__toolbar-update rounded px-3 py-1.5 text-sm font-bold transition-colors disabled:cursor-default disabled:text-[#8E9CAC] disabled:hover:bg-transparent enabled:text-[#394047] enabled:hover:bg-[#f1f3f4]"
type="button"
:disabled="!hasUnsavedPostChanges || saving || autoSaving"
:disabled="!hasUnsavedPostChanges || hasBlockingSlugError || saving || autoSaving"
@click="requestToolbarUpdate"
>
Update
@@ -1943,7 +1972,7 @@ defineExpose({
<button
class="admin-post-form__toolbar-update rounded px-3 py-1.5 text-sm font-bold transition-colors disabled:cursor-default disabled:text-[#8E9CAC] disabled:hover:bg-transparent enabled:text-[#394047] enabled:hover:bg-[#f1f3f4]"
type="button"
:disabled="!hasUnsavedPostChanges || saving || autoSaving"
:disabled="!hasUnsavedPostChanges || hasBlockingSlugError || saving || autoSaving"
@click="requestToolbarUpdate"
>
Update
@@ -1962,7 +1991,7 @@ defineExpose({
<button
class="admin-post-form__toolbar-update rounded px-3 py-1.5 text-sm font-bold transition-colors disabled:cursor-default disabled:text-[#8E9CAC] disabled:hover:bg-transparent enabled:text-[#394047] enabled:hover:bg-[#f1f3f4]"
type="button"
:disabled="!hasUnsavedPostChanges || saving || autoSaving"
:disabled="!hasUnsavedPostChanges || hasBlockingSlugError || saving || autoSaving"
@click="requestToolbarUpdate"
>
Update
@@ -1981,7 +2010,7 @@ defineExpose({
<button
class="admin-post-form__toolbar-update rounded px-3 py-1.5 text-sm font-bold transition-colors disabled:cursor-default disabled:text-[#8E9CAC] disabled:hover:bg-transparent enabled:text-[#394047] enabled:hover:bg-[#f1f3f4]"
type="button"
:disabled="!hasUnsavedPostChanges || saving || autoSaving"
:disabled="!hasUnsavedPostChanges || hasBlockingSlugError || saving || autoSaving"
@click="requestToolbarUpdate"
>
Update
@@ -2091,7 +2120,7 @@ defineExpose({
type="text"
pattern="[a-z0-9가-힣]+(-[a-z0-9가-힣]+)*"
:required="!isSlugAutoFromTitle"
:aria-describedby="isSlugAutoFromTitle ? 'admin-post-form-slug-auto-hint' : undefined"
:aria-describedby="slugFieldError ? 'admin-post-form-slug-error' : (isSlugAutoFromTitle ? 'admin-post-form-slug-auto-hint' : undefined)"
@blur="commitSlugInput"
>
</label>
@@ -2106,6 +2135,13 @@ defineExpose({
{{ postUrlHint }}
<span v-if="hasUnsavedSlugChange" class="admin-post-form__post-url-hint-unsaved text-[#8e9cac]"> (저장 후 반영)</span>
</p>
<p
v-if="slugFieldError"
id="admin-post-form-slug-error"
class="admin-post-form__post-url-error text-xs font-semibold text-red-600"
>
{{ slugFieldError }}
</p>
</div>
<label class="admin-post-form__field grid gap-2 text-sm">
@@ -2459,7 +2495,7 @@ defineExpose({
<div class="admin-post-form__media-picker-body min-h-0 flex-1 overflow-y-auto p-5">
<div
v-if="activeMediaPickerTab === 'upload'"
class="admin-post-form__media-upload-zone grid min-h-[420px] place-items-center border border-dashed border-[#cfd5da] bg-white text-center"
class="admin-post-form__media-upload-zone grid h-full min-h-[420px] place-items-center border border-dashed border-[#cfd5da] bg-[#fafafa] text-center"
:class="isUploadingFeaturedImage ? 'select-none border-[#8e9cac] bg-[#f3f5f7]' : ''"
@dragover.prevent
@drop.prevent="dropFeaturedImage"
@@ -2753,7 +2789,7 @@ defineExpose({
<button
class="rounded bg-[#15171a] px-5 py-2.5 text-[14px] font-semibold text-white transition-colors hover:bg-black disabled:cursor-not-allowed disabled:bg-[#d7dce0] disabled:text-[#8e9cac]"
type="button"
:disabled="saving || autoSaving"
:disabled="hasBlockingSlugError || saving || autoSaving"
@click="submitFromPublishModal"
>
{{ saving || autoSaving ? '저장 중…' : '최종 확인하고 저장 →' }}