릴리스: v1.4.43 에디터 라우트 재로딩 및 원본 이동 확인 보강
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { Teleport, computed, inject, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { Teleport, computed, inject, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Sortable from 'sortablejs'
|
||||
import * as htmlToImage from 'html-to-image'
|
||||
@@ -56,8 +56,10 @@ const templateRequestDraftDescription = ref('')
|
||||
const isDeleteModalOpen = ref(false)
|
||||
const isGroupDeleteModalOpen = ref(false)
|
||||
const isColumnDeleteModalOpen = ref(false)
|
||||
const isNavigationConfirmModalOpen = ref(false)
|
||||
const pendingRemoveGroupId = ref('')
|
||||
const pendingRemoveColumnIndex = ref(-1)
|
||||
const pendingNavigationPath = ref('')
|
||||
const ownerId = ref('')
|
||||
const authorName = ref('')
|
||||
const authorAccountName = ref('')
|
||||
@@ -76,6 +78,8 @@ const isDeleting = ref(false)
|
||||
const poolSearchQuery = ref('')
|
||||
const selectedItemId = ref('')
|
||||
const recentDragFinishedAt = ref(0)
|
||||
const savedEditorSnapshot = ref('')
|
||||
let editorLoadToken = 0
|
||||
|
||||
const boardEl = ref(null)
|
||||
const exportBoardEl = ref(null)
|
||||
@@ -280,6 +284,33 @@ function buildGroupPayload() {
|
||||
}))
|
||||
}
|
||||
|
||||
function createEditorSnapshot() {
|
||||
return JSON.stringify({
|
||||
title: (title.value || '').trim(),
|
||||
description: description.value || '',
|
||||
isPublic: !!isPublic.value,
|
||||
showCharacterNames: !!showCharacterNames.value,
|
||||
iconSize: Number(iconSize.value || 80),
|
||||
columns: columns.value.map((column) => ({ id: column.id, name: column.name || '' })),
|
||||
groups: buildGroupPayload(),
|
||||
pool: pool.value.map((itemId) => {
|
||||
const item = itemsById.value[itemId]
|
||||
return {
|
||||
id: item?.id || itemId,
|
||||
src: item?.src || '',
|
||||
label: item?.label || '',
|
||||
origin: item?.origin || 'template',
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
function syncSavedEditorSnapshot() {
|
||||
savedEditorSnapshot.value = createEditorSnapshot()
|
||||
}
|
||||
|
||||
const hasUnsavedChanges = computed(() => canEdit.value && savedEditorSnapshot.value && createEditorSnapshot() !== savedEditorSnapshot.value)
|
||||
|
||||
function removeItemFromGroup(groupId, columnIndex, itemId) {
|
||||
if (!canEdit.value || !groupId || columnIndex == null || !itemId) return
|
||||
const targetGroup = groups.value.find((group) => group.id === groupId)
|
||||
@@ -827,6 +858,8 @@ async function persistTierList({ showModal = false } = {}) {
|
||||
authorAccountName.value = res.tierList?.authorAccountName || authorAccountName.value
|
||||
favoriteCount.value = Number(res.tierList?.favoriteCount || favoriteCount.value || 0)
|
||||
isFavorited.value = !!res.tierList?.isFavorited
|
||||
await nextTick()
|
||||
syncSavedEditorSnapshot()
|
||||
if (showModal) isSaveModalOpen.value = true
|
||||
return { ...res, savedTierListId }
|
||||
}
|
||||
@@ -879,6 +912,34 @@ function openEditMode() {
|
||||
router.push(editorPath(templateId.value, persistedTierListId.value || tierListId.value))
|
||||
}
|
||||
|
||||
function closeNavigationConfirmModal() {
|
||||
isNavigationConfirmModalOpen.value = false
|
||||
pendingNavigationPath.value = ''
|
||||
}
|
||||
|
||||
function requestEditorNavigation(path) {
|
||||
if (!path) return
|
||||
if (hasUnsavedChanges.value) {
|
||||
pendingNavigationPath.value = path
|
||||
isNavigationConfirmModalOpen.value = true
|
||||
return
|
||||
}
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function confirmNavigationDiscard() {
|
||||
const nextPath = pendingNavigationPath.value
|
||||
closeNavigationConfirmModal()
|
||||
if (!nextPath) return
|
||||
savedEditorSnapshot.value = ''
|
||||
router.push(nextPath)
|
||||
}
|
||||
|
||||
function openSourceTierList() {
|
||||
if (!sourceTierListId.value) return
|
||||
requestEditorNavigation(editorPath(templateId.value, sourceTierListId.value))
|
||||
}
|
||||
|
||||
function closeSaveModal() {
|
||||
isSaveModalOpen.value = false
|
||||
}
|
||||
@@ -1018,88 +1079,156 @@ async function requestTemplate(type) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
;(async () => {
|
||||
await auth.refresh()
|
||||
authorName.value = (auth.user?.nickname || '').trim()
|
||||
authorAccountName.value = ((auth.user?.email || '').trim().split('@')[0] || '').trim()
|
||||
function resetEditorStateForRoute() {
|
||||
destroySortables()
|
||||
if (thumbnailPreviewUrl.value) {
|
||||
URL.revokeObjectURL(thumbnailPreviewUrl.value)
|
||||
thumbnailPreviewUrl.value = ''
|
||||
}
|
||||
columns.value = [{ id: 'col-1', name: '' }]
|
||||
groups.value = normalizeLoadedGroups([], columns.value)
|
||||
pool.value = []
|
||||
itemsById.value = {}
|
||||
title.value = ''
|
||||
persistedTierListId.value = ''
|
||||
thumbnailSrc.value = ''
|
||||
pendingThumbnailFile.value = null
|
||||
description.value = ''
|
||||
isPublic.value = true
|
||||
showCharacterNames.value = false
|
||||
isSaveModalOpen.value = false
|
||||
isTemplateRequestModalOpen.value = false
|
||||
isTemplateUpdateModalOpen.value = false
|
||||
isDeleteModalOpen.value = false
|
||||
isGroupDeleteModalOpen.value = false
|
||||
isColumnDeleteModalOpen.value = false
|
||||
isNavigationConfirmModalOpen.value = false
|
||||
pendingRemoveGroupId.value = ''
|
||||
pendingRemoveColumnIndex.value = -1
|
||||
pendingNavigationPath.value = ''
|
||||
ownerId.value = ''
|
||||
authorName.value = ''
|
||||
authorAccountName.value = ''
|
||||
updatedAt.value = 0
|
||||
sourceTierListId.value = ''
|
||||
sourceSnapshotTitle.value = ''
|
||||
sourceSnapshotAuthor.value = ''
|
||||
isDragActive.value = false
|
||||
isThumbnailDragActive.value = false
|
||||
iconSize.value = 80
|
||||
isFavoriteBusy.value = false
|
||||
favoriteCount.value = 0
|
||||
isFavorited.value = false
|
||||
isRequestingTemplate.value = false
|
||||
isDeleting.value = false
|
||||
poolSearchQuery.value = ''
|
||||
selectedItemId.value = ''
|
||||
recentDragFinishedAt.value = 0
|
||||
savedEditorSnapshot.value = ''
|
||||
resetTemplateRequestDrafts()
|
||||
}
|
||||
|
||||
if (isNewTierList.value && !auth.user) {
|
||||
router.replace(loginPath(editorNewPath(templateId.value)))
|
||||
return
|
||||
}
|
||||
async function loadEditorState() {
|
||||
const loadToken = ++editorLoadToken
|
||||
resetEditorStateForRoute()
|
||||
await auth.refresh()
|
||||
if (loadToken !== editorLoadToken) return
|
||||
|
||||
let currentTemplateItems = []
|
||||
authorName.value = (auth.user?.nickname || '').trim()
|
||||
authorAccountName.value = ((auth.user?.email || '').trim().split('@')[0] || '').trim()
|
||||
|
||||
if (isNewTierList.value && !auth.user) {
|
||||
router.replace(loginPath(editorNewPath(templateId.value)))
|
||||
return
|
||||
}
|
||||
|
||||
let currentTemplateItems = []
|
||||
try {
|
||||
const topicRes = await api.getTopic(templateId.value)
|
||||
if (loadToken !== editorLoadToken) return
|
||||
|
||||
templateName.value = topicRes.topic?.name || templateId.value
|
||||
const base = (topicRes.items || []).map((img) => ({
|
||||
id: img.id,
|
||||
src: img.src,
|
||||
label: img.label,
|
||||
origin: 'template',
|
||||
}))
|
||||
currentTemplateItems = base
|
||||
const map = {}
|
||||
base.forEach((it) => (map[it.id] = it))
|
||||
itemsById.value = map
|
||||
pool.value = base.map((it) => it.id)
|
||||
} catch (e) {
|
||||
if (loadToken !== editorLoadToken) return
|
||||
error.value = '기본 템플릿 이미지를 불러오지 못했어요.'
|
||||
}
|
||||
|
||||
if (tierListId.value && tierListId.value !== 'new') {
|
||||
try {
|
||||
const topicRes = await api.getTopic(templateId.value)
|
||||
templateName.value = topicRes.topic?.name || templateId.value
|
||||
const base = (topicRes.items || []).map((img) => ({
|
||||
id: img.id,
|
||||
src: img.src,
|
||||
label: img.label,
|
||||
origin: 'template',
|
||||
}))
|
||||
currentTemplateItems = base
|
||||
const map = {}
|
||||
base.forEach((it) => (map[it.id] = it))
|
||||
itemsById.value = map
|
||||
pool.value = base.map((it) => it.id)
|
||||
} catch (e) {
|
||||
error.value = '기본 템플릿 이미지를 불러오지 못했어요.'
|
||||
}
|
||||
const res = await api.getTierList(tierListId.value)
|
||||
if (loadToken !== editorLoadToken) return
|
||||
|
||||
if (tierListId.value && tierListId.value !== 'new') {
|
||||
try {
|
||||
const res = await api.getTierList(tierListId.value)
|
||||
const t = res.tierList
|
||||
ownerId.value = t.authorId
|
||||
persistedTierListId.value = t.id || ''
|
||||
title.value = t.title
|
||||
thumbnailSrc.value = t.thumbnailSrc || ''
|
||||
description.value = t.description || ''
|
||||
isPublic.value = !!t.isPublic
|
||||
showCharacterNames.value = !!t.showCharacterNames
|
||||
iconSize.value = Number(t.iconSize || 80)
|
||||
authorName.value = t.authorName || ''
|
||||
authorAccountName.value = t.authorAccountName || ''
|
||||
updatedAt.value = Number(t.updatedAt || 0)
|
||||
sourceTierListId.value = t.sourceTierListId || ''
|
||||
sourceSnapshotTitle.value = t.sourceSnapshotTitle || ''
|
||||
sourceSnapshotAuthor.value = t.sourceSnapshotAuthor || ''
|
||||
favoriteCount.value = Number(t.favoriteCount || 0)
|
||||
isFavorited.value = !!t.isFavorited
|
||||
const t = res.tierList
|
||||
ownerId.value = t.authorId
|
||||
persistedTierListId.value = t.id || ''
|
||||
title.value = t.title
|
||||
thumbnailSrc.value = t.thumbnailSrc || ''
|
||||
description.value = t.description || ''
|
||||
isPublic.value = !!t.isPublic
|
||||
showCharacterNames.value = !!t.showCharacterNames
|
||||
iconSize.value = Number(t.iconSize || 80)
|
||||
authorName.value = t.authorName || ''
|
||||
authorAccountName.value = t.authorAccountName || ''
|
||||
updatedAt.value = Number(t.updatedAt || 0)
|
||||
sourceTierListId.value = t.sourceTierListId || ''
|
||||
sourceSnapshotTitle.value = t.sourceSnapshotTitle || ''
|
||||
sourceSnapshotAuthor.value = t.sourceSnapshotAuthor || ''
|
||||
favoriteCount.value = Number(t.favoriteCount || 0)
|
||||
isFavorited.value = !!t.isFavorited
|
||||
|
||||
if (!previewMode.value && !canEdit.value) {
|
||||
router.replace(editorPath(templateId.value, t.id, { preview: true }))
|
||||
return
|
||||
}
|
||||
|
||||
columns.value = normalizeLoadedColumns(t.groups)
|
||||
groups.value = normalizeLoadedGroups(t.groups, columns.value)
|
||||
const map = {}
|
||||
;(t.pool || []).forEach((it) => (map[it.id] = it))
|
||||
const grouped = new Set()
|
||||
groups.value.forEach((group) => group.itemIds.forEach((id) => grouped.add(id)))
|
||||
const merged = mergeLatestTemplateItemsIntoPool(
|
||||
map,
|
||||
Object.keys(map).filter((id) => !grouped.has(id)),
|
||||
currentTemplateItems,
|
||||
grouped,
|
||||
canEdit.value && !previewMode.value
|
||||
)
|
||||
itemsById.value = merged.nextMap
|
||||
pool.value = merged.nextPoolIds
|
||||
} catch (e) {
|
||||
error.value = '티어표를 불러오지 못했어요.'
|
||||
if (!previewMode.value && !canEdit.value) {
|
||||
router.replace(editorPath(templateId.value, t.id, { preview: true }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
if (canEdit.value) {
|
||||
await initSortables()
|
||||
columns.value = normalizeLoadedColumns(t.groups)
|
||||
groups.value = normalizeLoadedGroups(t.groups, columns.value)
|
||||
const map = {}
|
||||
;(t.pool || []).forEach((it) => (map[it.id] = it))
|
||||
const grouped = new Set()
|
||||
groups.value.forEach((group) => group.itemIds.forEach((id) => grouped.add(id)))
|
||||
const merged = mergeLatestTemplateItemsIntoPool(
|
||||
map,
|
||||
Object.keys(map).filter((id) => !grouped.has(id)),
|
||||
currentTemplateItems,
|
||||
grouped,
|
||||
canEdit.value && !previewMode.value
|
||||
)
|
||||
itemsById.value = merged.nextMap
|
||||
pool.value = merged.nextPoolIds
|
||||
} catch (e) {
|
||||
if (loadToken !== editorLoadToken) return
|
||||
error.value = '티어표를 불러오지 못했어요.'
|
||||
}
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
if (loadToken !== editorLoadToken) return
|
||||
|
||||
syncSavedEditorSnapshot()
|
||||
if (canEdit.value) {
|
||||
await initSortables()
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.params.topicId, route.params.tierListId, route.query.preview],
|
||||
() => {
|
||||
loadEditorState()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (thumbnailPreviewUrl.value) URL.revokeObjectURL(thumbnailPreviewUrl.value)
|
||||
@@ -1191,6 +1320,19 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isNavigationConfirmModalOpen" class="modalOverlay" @click.self="closeNavigationConfirmModal">
|
||||
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="navigationConfirmTitle">
|
||||
<div id="navigationConfirmTitle" class="modalCard__title">원본 티어표로 이동</div>
|
||||
<div class="modalCard__desc">
|
||||
아직 저장하지 않은 수정 내용이 있어요. 이대로 이동하면 현재 변경 내용은 사라집니다.
|
||||
</div>
|
||||
<div class="modalCard__actions">
|
||||
<button class="btn btn--ghost" type="button" @click="closeNavigationConfirmModal">계속 편집</button>
|
||||
<button class="btn btn--danger" type="button" @click="confirmNavigationDiscard">저장 없이 이동</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isTemplateRequestModalOpen" class="modalOverlay" @click.self="closeTemplateRequestModal">
|
||||
<div class="modalCard" role="dialog" aria-modal="true" aria-labelledby="templateRequestTitle">
|
||||
<div id="templateRequestTitle" class="modalCard__title">템플릿 등록 요청</div>
|
||||
@@ -1309,7 +1451,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
<div v-if="sourceTierListId" class="editorMain__sourceNote">
|
||||
<span>원본</span>
|
||||
<button class="editorMain__sourceLink" type="button" @click="router.push(editorPath(templateId, sourceTierListId))">{{ copiedFromLabel }}</button>
|
||||
<button class="editorMain__sourceLink" type="button" @click="openSourceTierList">{{ copiedFromLabel }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user