릴리스: v1.4.40 뷰어 모드 전환 및 템플릿 병합 보강
This commit is contained in:
@@ -44,7 +44,7 @@ const isAdmin = computed(() => authReady.value && !!auth.user?.isAdmin)
|
||||
const isPreviewMode = computed(() => route.query.preview === '1')
|
||||
const isAdminRoute = computed(() => String(route.name || '').startsWith('admin'))
|
||||
const usesLocalRightRail = computed(
|
||||
() => !isPreviewMode.value && (['editEditor', 'newEditor'].includes(String(route.name || '')) || isAdminRoute.value)
|
||||
() => ['editEditor', 'newEditor'].includes(String(route.name || '')) || isAdminRoute.value
|
||||
)
|
||||
const isRightRailOverlay = computed(() => viewportWidth.value <= 1200)
|
||||
const isMobileLayout = computed(() => viewportWidth.value <= 860)
|
||||
|
||||
@@ -8,6 +8,7 @@ import addColumnRightIcon from '../assets/icons/add_column_right.svg'
|
||||
import addRowBelowIcon from '../assets/icons/add_row_below.svg'
|
||||
import addPhotoAlternateIcon from '../assets/icons/add_photo_alternate.svg'
|
||||
import shareIcon from '../assets/icons/share.svg'
|
||||
import RightRailAd from '../components/RightRailAd.vue'
|
||||
import { api } from '../lib/api'
|
||||
import { editorNewPath, editorPath, loginPath, mePath, topicPath } from '../lib/paths'
|
||||
import { toApiUrl } from '../lib/runtime'
|
||||
@@ -86,7 +87,8 @@ const poolSortable = ref(null)
|
||||
const dropSortables = ref([])
|
||||
|
||||
const isNewTierList = computed(() => tierListId.value === 'new')
|
||||
const canEdit = computed(() => !!auth.user && (!ownerId.value || ownerId.value === auth.user.id))
|
||||
const isOwnTierList = computed(() => !!auth.user && !!ownerId.value && ownerId.value === auth.user.id)
|
||||
const canEdit = computed(() => !!auth.user && !previewMode.value && (!ownerId.value || ownerId.value === auth.user.id))
|
||||
const iconSizeOptions = [48, 64, 80, 96, 112]
|
||||
const hasCustomTitle = computed(() => !!(title.value || '').trim())
|
||||
const fallbackTimestamp = computed(() => (updatedAt.value ? updatedAt.value : Date.now()))
|
||||
@@ -114,7 +116,9 @@ const untitledWarning = computed(
|
||||
'제목 없이 저장된 티어표는 무분별한 도배 방지를 위해 관리자에 의해 임의 삭제될 수 있어요.'
|
||||
)
|
||||
const canFavorite = computed(() => !!auth.user && !isNewTierList.value && !canEdit.value)
|
||||
const canDuplicate = computed(() => !!auth.user && hasSavedTierList.value)
|
||||
const canDuplicate = computed(() => !!auth.user && hasSavedTierList.value && !isOwnTierList.value)
|
||||
const canSwitchToViewerMode = computed(() => isOwnTierList.value && hasSavedTierList.value && !previewMode.value)
|
||||
const canSwitchToEditMode = computed(() => isOwnTierList.value && hasSavedTierList.value && previewMode.value)
|
||||
const copiedFromLabel = computed(() => {
|
||||
if (!sourceTierListId.value) return ''
|
||||
const parts = []
|
||||
@@ -193,6 +197,20 @@ function getOrderedItems() {
|
||||
return getOrderedItemIds().map((itemId) => itemsById.value[itemId]).filter(Boolean)
|
||||
}
|
||||
|
||||
function mergeLatestTemplateItemsIntoPool(savedItemsMap, savedPoolIds, currentTemplateItems, groupedIds, editable) {
|
||||
const nextMap = { ...(savedItemsMap || {}) }
|
||||
const nextPoolIds = Array.isArray(savedPoolIds) ? [...savedPoolIds] : []
|
||||
if (!editable) return { nextMap, nextPoolIds }
|
||||
|
||||
;(currentTemplateItems || []).forEach((item) => {
|
||||
if (!item?.id || nextMap[item.id]) return
|
||||
nextMap[item.id] = item
|
||||
if (!groupedIds?.has(item.id)) nextPoolIds.push(item.id)
|
||||
})
|
||||
|
||||
return { nextMap, nextPoolIds }
|
||||
}
|
||||
|
||||
function isPoolItemVisible(itemId) {
|
||||
const query = normalizedPoolSearchQuery.value
|
||||
if (!query) return true
|
||||
@@ -756,6 +774,16 @@ async function copyShareUrl() {
|
||||
}
|
||||
}
|
||||
|
||||
function openViewerMode() {
|
||||
if (!canSwitchToViewerMode.value) return
|
||||
router.push(editorPath(templateId.value, persistedTierListId.value || tierListId.value, { preview: true }))
|
||||
}
|
||||
|
||||
function openEditMode() {
|
||||
if (!canSwitchToEditMode.value) return
|
||||
router.push(editorPath(templateId.value, persistedTierListId.value || tierListId.value))
|
||||
}
|
||||
|
||||
function closeSaveModal() {
|
||||
isSaveModalOpen.value = false
|
||||
}
|
||||
@@ -906,6 +934,7 @@ onMounted(() => {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTemplateItems = []
|
||||
try {
|
||||
const topicRes = await api.getTopic(templateId.value)
|
||||
templateName.value = topicRes.topic?.name || templateId.value
|
||||
@@ -915,6 +944,7 @@ onMounted(() => {
|
||||
label: img.label,
|
||||
origin: 'template',
|
||||
}))
|
||||
currentTemplateItems = base
|
||||
const map = {}
|
||||
base.forEach((it) => (map[it.id] = it))
|
||||
itemsById.value = map
|
||||
@@ -943,14 +973,27 @@ onMounted(() => {
|
||||
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))
|
||||
itemsById.value = map
|
||||
const grouped = new Set()
|
||||
groups.value.forEach((group) => group.itemIds.forEach((id) => grouped.add(id)))
|
||||
pool.value = Object.keys(itemsById.value).filter((id) => !grouped.has(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 = '티어표를 불러오지 못했어요.'
|
||||
}
|
||||
@@ -1014,6 +1057,31 @@ onUnmounted(() => {
|
||||
<span>{{ formatExportDate(fallbackTimestamp) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Teleport :to="localRightRailTarget">
|
||||
<template v-if="globalRightRailOpen">
|
||||
<RightRailAd />
|
||||
|
||||
<div class="viewerSidebar__section">
|
||||
<div class="viewerSidebar__eyebrow">Viewer Mode</div>
|
||||
<div class="viewerSidebar__title">공유 티어표 보기</div>
|
||||
<p class="viewerSidebar__desc">
|
||||
{{ canSwitchToEditMode ? '지금은 드래그 없는 뷰어 모드입니다. 원하면 바로 수정 모드로 돌아갈 수 있어요.' : '드래그나 편집 없이 완성본만 보는 뷰어 모드입니다.' }}
|
||||
</p>
|
||||
<div class="viewerSidebar__actions">
|
||||
<button v-if="hasSavedTierList" class="btn btn--ghost viewerSidebar__button" type="button" @click="copyShareUrl">
|
||||
공유하기
|
||||
</button>
|
||||
<button v-if="canDuplicate" class="btn btn--save viewerSidebar__button" type="button" @click="duplicateCurrentTierList">
|
||||
내 티어표로 복사
|
||||
</button>
|
||||
<button v-if="canSwitchToEditMode" class="btn btn--save viewerSidebar__button" type="button" @click="openEditMode">
|
||||
수정 모드로 전환
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Teleport>
|
||||
</section>
|
||||
|
||||
<template v-else>
|
||||
@@ -1400,6 +1468,7 @@ onUnmounted(() => {
|
||||
<button v-if="canEdit" class="btn btn--save editorSidebar__button" :disabled="isSaving" @click="save">{{ isSaving ? '저장중...' : '저장' }}</button>
|
||||
</div>
|
||||
<div class="editorSidebar__utilityLinks">
|
||||
<button v-if="canSwitchToViewerMode" class="editorSidebar__utilityLink" @click="openViewerMode">뷰어 모드로 보기</button>
|
||||
<button v-if="hasSavedTierList" class="editorSidebar__utilityLink editorSidebar__utilityLink--share" @click="copyShareUrl">
|
||||
<SvgIcon :src="shareIcon" :size="16" />
|
||||
<span>공유하기</span>
|
||||
@@ -1584,6 +1653,48 @@ onUnmounted(() => {
|
||||
color: var(--theme-text-soft);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.viewerSidebar__section {
|
||||
margin-top: auto;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 18px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-pill-bg);
|
||||
}
|
||||
|
||||
.viewerSidebar__eyebrow {
|
||||
color: var(--theme-text-faint);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.viewerSidebar__title {
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--theme-text-strong);
|
||||
}
|
||||
|
||||
.viewerSidebar__desc {
|
||||
margin: 0;
|
||||
color: var(--theme-text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.viewerSidebar__actions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.viewerSidebar__button {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
margin-top: 0;
|
||||
}
|
||||
.toggleSwitch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -96,7 +96,8 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="pageHead">
|
||||
<section class="pageWrap">
|
||||
<section class="pageHead">
|
||||
<div class="pageHead__main">
|
||||
<div class="pageHead__eyebrow">Collection</div>
|
||||
<h2 class="pageHead__title">{{ topicTitle }}</h2>
|
||||
@@ -106,10 +107,10 @@ watch(
|
||||
<input v-model="query" class="input" placeholder="제목 또는 작성자 검색" @keydown.enter.prevent="submitSearch" />
|
||||
<button class="btn" @click="submitSearch">검색</button>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<section class="panel">
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<section class="panel">
|
||||
<div v-if="tierLists.length === 0" class="empty">아직 공개 티어표가 없어요.</div>
|
||||
<div v-else class="list" :class="{ 'list--table': isListView }">
|
||||
<article v-for="t in tierLists" :key="t.id" class="boardCard" :class="{ 'boardCard--list': isListView }">
|
||||
@@ -137,6 +138,7 @@ watch(
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user