미저장 변경 이탈 확인 추가
This commit is contained in:
@@ -16,6 +16,7 @@ const isNewMember = computed(() => props.mode === 'new')
|
||||
const saveMessage = ref('')
|
||||
const saveError = ref('')
|
||||
const isSaving = ref(false)
|
||||
const savedMemberSnapshot = ref('')
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
@@ -58,6 +59,12 @@ const normalizedLabels = computed(() => [...new Set(
|
||||
.filter(Boolean)
|
||||
)])
|
||||
|
||||
/**
|
||||
* 회원 저장 요청 본문을 문자열로 직렬화한다.
|
||||
* @returns {string} 직렬화된 회원 입력값
|
||||
*/
|
||||
const serializeMemberPayload = () => JSON.stringify(getMemberPayload())
|
||||
|
||||
/**
|
||||
* 날짜 표시 형식 변환
|
||||
* @param {string | null} value - ISO 날짜 문자열
|
||||
@@ -131,6 +138,14 @@ const getMemberPayload = () => ({
|
||||
note: form.note
|
||||
})
|
||||
|
||||
const hasUnsavedMemberChanges = computed(() => serializeMemberPayload() !== savedMemberSnapshot.value)
|
||||
|
||||
const {
|
||||
isUnsavedModalOpen,
|
||||
stayOnUnsavedPage,
|
||||
leaveUnsavedPage
|
||||
} = useAdminUnsavedChangesGuard(hasUnsavedMemberChanges)
|
||||
|
||||
/**
|
||||
* 회원 기본 정보를 저장한다.
|
||||
* @returns {Promise<void>}
|
||||
@@ -156,6 +171,7 @@ const saveMember = async () => {
|
||||
body: payload
|
||||
})
|
||||
|
||||
savedMemberSnapshot.value = serializeMemberPayload()
|
||||
emit('saved', saved)
|
||||
saveMessage.value = '저장되었습니다.'
|
||||
} catch (error) {
|
||||
@@ -164,6 +180,10 @@ const saveMember = async () => {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.member, () => {
|
||||
savedMemberSnapshot.value = serializeMemberPayload()
|
||||
}, { immediate: true, flush: 'post' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -198,7 +218,7 @@ const saveMember = async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-member-form__body grid gap-8 py-8 xl:grid-cols-[280px_minmax(0,1fr)]">
|
||||
<div class="admin-member-form__body grid gap-8 py-8 xl:grid-cols-3">
|
||||
<aside class="admin-member-form__summary">
|
||||
<div class="admin-member-form__identity flex items-center gap-4">
|
||||
<img
|
||||
@@ -251,7 +271,7 @@ const saveMember = async () => {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="admin-member-form__content space-y-8">
|
||||
<div class="admin-member-form__content space-y-8 xl:col-span-2">
|
||||
<form class="admin-member-form__card rounded-xl border border-line bg-white p-5 md:p-6" @submit.prevent="saveMember">
|
||||
<div class="grid gap-5 md:grid-cols-2">
|
||||
<label class="admin-member-form__field block">
|
||||
@@ -315,5 +335,11 @@ const saveMember = async () => {
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AdminUnsavedChangesModal
|
||||
:open="isUnsavedModalOpen"
|
||||
@stay="stayOnUnsavedPage"
|
||||
@leave="leaveUnsavedPage"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -49,6 +49,7 @@ const tagInput = ref('')
|
||||
const isTagInputComposing = ref(false)
|
||||
const activeMediaPickerTab = ref('upload')
|
||||
const selectedMediaPickerUrl = ref('')
|
||||
const savedPostSnapshot = ref('')
|
||||
|
||||
/**
|
||||
* ISO 날짜를 datetime-local 입력값으로 변환
|
||||
@@ -260,6 +261,14 @@ const createPostPayload = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 게시물 입력값을 문자열로 직렬화한다.
|
||||
* @returns {string} 직렬화된 게시물 입력값
|
||||
*/
|
||||
const serializePostPayload = () => JSON.stringify(createPostPayload())
|
||||
|
||||
const hasUnsavedPostChanges = computed(() => serializePostPayload() !== savedPostSnapshot.value)
|
||||
|
||||
/**
|
||||
* 자동 저장 데이터 생성
|
||||
* @returns {Object} 자동 저장 데이터
|
||||
@@ -374,6 +383,15 @@ const discardAutosave = () => {
|
||||
autosaveStatus.value = ''
|
||||
}
|
||||
|
||||
const {
|
||||
isUnsavedModalOpen,
|
||||
stayOnUnsavedPage,
|
||||
leaveUnsavedPage,
|
||||
allowNextRouteLeave
|
||||
} = useAdminUnsavedChangesGuard(hasUnsavedPostChanges, {
|
||||
onLeaveConfirmed: discardAutosave
|
||||
})
|
||||
|
||||
/**
|
||||
* 미디어 라이브러리 목록 조회
|
||||
* @returns {Promise<void>}
|
||||
@@ -582,9 +600,19 @@ const toggleSettingsPanel = () => {
|
||||
isSettingsOpen.value = !isSettingsOpen.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 입력값을 저장 완료 기준점으로 표시한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const markSaved = () => {
|
||||
savedPostSnapshot.value = serializePostPayload()
|
||||
}
|
||||
|
||||
watch(form, scheduleAutosave, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
markSaved()
|
||||
|
||||
const savedRaw = localStorage.getItem(autosaveKey.value)
|
||||
|
||||
if (!savedRaw) {
|
||||
@@ -610,7 +638,9 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
clearAutosave: discardAutosave
|
||||
clearAutosave: discardAutosave,
|
||||
markSaved,
|
||||
allowNextRouteLeave
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -952,5 +982,10 @@ defineExpose({
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<AdminUnsavedChangesModal
|
||||
:open="isUnsavedModalOpen"
|
||||
@stay="stayOnUnsavedPage"
|
||||
@leave="leaveUnsavedPage"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
61
components/admin/AdminUnsavedChangesModal.vue
Normal file
61
components/admin/AdminUnsavedChangesModal.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['stay', 'leave'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="open"
|
||||
class="admin-unsaved-modal fixed inset-0 z-[100] flex items-center justify-center bg-black/40 px-5 py-8"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="admin-unsaved-modal-title"
|
||||
>
|
||||
<div class="admin-unsaved-modal__content relative w-full max-w-[520px] rounded-xl bg-white text-[#15171a] shadow-2xl">
|
||||
<header class="admin-unsaved-modal__header border-b border-[#e3e6e8] px-8 py-6">
|
||||
<h1 id="admin-unsaved-modal-title" class="admin-unsaved-modal__title text-xl font-semibold tracking-[-0.01em]">
|
||||
이 페이지를 떠날까요?
|
||||
</h1>
|
||||
</header>
|
||||
<button
|
||||
class="admin-unsaved-modal__close absolute right-5 top-5 grid size-8 place-items-center rounded-md text-[#4d5663] transition hover:bg-[#eff1f2] hover:text-black"
|
||||
type="button"
|
||||
title="닫기"
|
||||
aria-label="닫기"
|
||||
@click="$emit('stay')"
|
||||
>
|
||||
<svg class="h-4 w-4" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12.707 12L23.854.854a.5.5 0 00-.707-.707L12 11.293.854.146a.5.5 0 00-.707.707L11.293 12 .146 23.146a.5.5 0 00.708.708L12 12.707l11.146 11.146a.5.5 0 10.708-.706L12.707 12z" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="admin-unsaved-modal__body space-y-3 px-8 py-7 text-sm leading-6 text-[#4d5663]">
|
||||
<p>저장하지 않은 변경사항이 있습니다.</p>
|
||||
<p>떠나기 전에 저장해 주세요.</p>
|
||||
</div>
|
||||
<footer class="admin-unsaved-modal__footer flex justify-end gap-3 border-t border-[#e3e6e8] px-8 py-5">
|
||||
<button
|
||||
class="admin-unsaved-modal__stay h-10 rounded-md border border-[#d7dce0] bg-white px-4 text-sm font-semibold text-[#394047] transition hover:bg-[#eff1f2]"
|
||||
type="button"
|
||||
@click="$emit('stay')"
|
||||
>
|
||||
머무르기
|
||||
</button>
|
||||
<button
|
||||
class="admin-unsaved-modal__leave h-10 rounded-md bg-[#e5484d] px-4 text-sm font-semibold text-white transition hover:bg-[#d21a26]"
|
||||
type="button"
|
||||
@click="$emit('leave')"
|
||||
>
|
||||
나가기
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
Reference in New Issue
Block a user