릴리스: v1.4.35 에디터 검색과 공유 프리뷰 보강
This commit is contained in:
@@ -22,7 +22,7 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const { toasts, dismissToast } = useToast()
|
||||
const RIGHT_RAIL_COPYRIGHT_URL = 'https://zenn.town/@murabito'
|
||||
const RIGHT_RAIL_COPYRIGHT_URL = 'https://x.com/zennbox'
|
||||
const currentTopicId = computed(() => route.params.topicId || '')
|
||||
|
||||
const leftRailCollapsed = ref(false)
|
||||
|
||||
@@ -278,9 +278,16 @@ export function useAdminTemplateManager({
|
||||
|
||||
const fileDrafts = uploadItemDrafts.value.filter((entry) => entry.kind === 'file')
|
||||
const requestDrafts = uploadItemDrafts.value.filter((entry) => entry.kind === 'request')
|
||||
const totalUploadBytes = fileDrafts.reduce((sum, entry) => sum + Number(entry.file?.size || 0), 0)
|
||||
let uploadCount = 0
|
||||
|
||||
if (fileDrafts.length) {
|
||||
console.info('[admin] template item upload start', {
|
||||
topicId: selectedTemplateId.value,
|
||||
fileCount: fileDrafts.length,
|
||||
totalBytes: totalUploadBytes,
|
||||
labels: fileDrafts.map((entry) => entry.label.trim()),
|
||||
})
|
||||
const fd = new FormData()
|
||||
fileDrafts.forEach((entry) => {
|
||||
fd.append('images', entry.file)
|
||||
@@ -291,7 +298,25 @@ export function useAdminTemplateManager({
|
||||
credentials: 'include',
|
||||
body: fd,
|
||||
})
|
||||
if (!res.ok) throw new Error('failed')
|
||||
if (!res.ok) {
|
||||
const responseText = await res.text().catch(() => '')
|
||||
console.error('[admin] template item upload failed', {
|
||||
topicId: selectedTemplateId.value,
|
||||
fileCount: fileDrafts.length,
|
||||
totalBytes: totalUploadBytes,
|
||||
status: res.status,
|
||||
body: responseText,
|
||||
})
|
||||
const uploadError = new Error('failed')
|
||||
uploadError.status = res.status
|
||||
uploadError.body = responseText
|
||||
throw uploadError
|
||||
}
|
||||
console.info('[admin] template item upload success', {
|
||||
topicId: selectedTemplateId.value,
|
||||
fileCount: fileDrafts.length,
|
||||
totalBytes: totalUploadBytes,
|
||||
})
|
||||
uploadCount += fileDrafts.length
|
||||
}
|
||||
|
||||
@@ -316,6 +341,12 @@ export function useAdminTemplateManager({
|
||||
await loadTemplate()
|
||||
success.value = `템플릿 기본 아이템 ${uploadCount}개 추가를 완료했어요.`
|
||||
} catch (e) {
|
||||
console.error('[admin] uploadItem error', {
|
||||
message: e?.message || '',
|
||||
status: e?.status || 0,
|
||||
body: e?.body || '',
|
||||
data: e?.data || null,
|
||||
})
|
||||
const apiError = e?.data?.error || ''
|
||||
if (apiError === 'no_items_selected') {
|
||||
error.value = '추가할 요청 아이템이 없어요.'
|
||||
@@ -330,6 +361,10 @@ export function useAdminTemplateManager({
|
||||
error.value = '선택한 템플릿을 찾지 못했어요.'
|
||||
return
|
||||
}
|
||||
if (e?.status === 413) {
|
||||
error.value = '한 번에 업로드한 파일 용량이 너무 커서 실패했어요.'
|
||||
return
|
||||
}
|
||||
error.value = '아이템 추가에 실패했어요.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 { api } from '../lib/api'
|
||||
import { editorNewPath, editorPath, loginPath, mePath, topicPath } from '../lib/paths'
|
||||
import { editorNewPath, editorPath, homePath, loginPath, mePath, topicPath } from '../lib/paths'
|
||||
import { toApiUrl } from '../lib/runtime'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useToast } from '../composables/useToast'
|
||||
@@ -72,6 +72,7 @@ const favoriteCount = ref(0)
|
||||
const isFavorited = ref(false)
|
||||
const isRequestingTemplate = ref(false)
|
||||
const isDeleting = ref(false)
|
||||
const poolSearchQuery = ref('')
|
||||
|
||||
const boardEl = ref(null)
|
||||
const exportBoardEl = ref(null)
|
||||
@@ -122,6 +123,7 @@ const copiedFromLabel = computed(() => {
|
||||
return parts.join(' · ') || '복사해 온 티어표'
|
||||
})
|
||||
const customItems = computed(() => getOrderedItems().filter((item) => item?.origin === 'custom'))
|
||||
const normalizedPoolSearchQuery = computed(() => poolSearchQuery.value.trim().toLowerCase())
|
||||
const hasSavedTierList = computed(() => !!(persistedTierListId.value || (tierListId.value && tierListId.value !== 'new')))
|
||||
const canRequestTemplateCreate = computed(
|
||||
() => canEdit.value && hasSavedTierList.value && templateId.value === 'freeform' && customItems.value.length > 0
|
||||
@@ -138,6 +140,7 @@ const shareTierListUrl = computed(() => {
|
||||
if (typeof window === 'undefined') return editorPath(templateId.value, savedTierListId, { preview: true })
|
||||
return new URL(editorPath(templateId.value, savedTierListId, { preview: true }), window.location.origin).toString()
|
||||
})
|
||||
const previewHomeUrl = computed(() => homePath())
|
||||
|
||||
watch(error, (message) => {
|
||||
if (!message) return
|
||||
@@ -166,8 +169,6 @@ function formatExportDate(ts) {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,6 +195,16 @@ function getOrderedItems() {
|
||||
return getOrderedItemIds().map((itemId) => itemsById.value[itemId]).filter(Boolean)
|
||||
}
|
||||
|
||||
function isPoolItemVisible(itemId) {
|
||||
const query = normalizedPoolSearchQuery.value
|
||||
if (!query) return true
|
||||
const item = itemsById.value[itemId]
|
||||
const label = String(item?.label || itemId || '').toLowerCase()
|
||||
return label.includes(query)
|
||||
}
|
||||
|
||||
const visiblePoolCount = computed(() => pool.value.filter((itemId) => isPoolItemVisible(itemId)).length)
|
||||
|
||||
function setIconSize(nextSize) {
|
||||
iconSize.value = nextSize
|
||||
}
|
||||
@@ -963,6 +974,10 @@ onUnmounted(() => {
|
||||
<template>
|
||||
<section v-if="previewMode" class="previewOnly" :style="{ '--thumb-size': `${iconSize}px` }">
|
||||
<div class="previewOnly__sheet">
|
||||
<a class="previewOnly__brand" :href="previewHomeUrl">
|
||||
<span class="previewOnly__brandMark">TM</span>
|
||||
<span class="previewOnly__brandText">Tier Maker</span>
|
||||
</a>
|
||||
<div class="previewOnly__title">{{ effectiveTitle }}</div>
|
||||
<div v-if="description" class="previewOnly__description">{{ description }}</div>
|
||||
<div v-if="columns.length > 1" class="previewOnly__columns">
|
||||
@@ -1264,12 +1279,28 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="sidebar">
|
||||
<div class="sidebar__title">아이템</div>
|
||||
<div class="sidebar__titleRow">
|
||||
<div class="sidebar__title">아이템</div>
|
||||
<div class="sidebar__count">{{ visiblePoolCount }} / {{ pool.length }}</div>
|
||||
</div>
|
||||
<div class="sidebar__hint">
|
||||
{{ canEdit ? '등록된 아이템 리스트입니다. 드래그해서 표에 넣을 수 있습니다.' : '공개 티어표는 보기 전용입니다.' }}
|
||||
</div>
|
||||
<input
|
||||
v-model="poolSearchQuery"
|
||||
class="sidebar__search"
|
||||
type="text"
|
||||
maxlength="60"
|
||||
placeholder="아이템 이름 검색"
|
||||
/>
|
||||
<div ref="poolEl" class="pool" data-list-type="pool">
|
||||
<div v-for="id in pool" :key="id" class="poolItem" :class="{ 'poolItem--readonly': !canEdit }" :data-item-id="id">
|
||||
<div
|
||||
v-for="id in pool"
|
||||
:key="id"
|
||||
class="poolItem"
|
||||
:class="{ 'poolItem--readonly': !canEdit, 'poolItem--hidden': !isPoolItemVisible(id) }"
|
||||
:data-item-id="id"
|
||||
>
|
||||
<img :src="resolveItemSrc(itemsById[id])" class="thumb" :alt="itemsById[id]?.label || id" />
|
||||
<div class="poolItem__label">{{ itemsById[id]?.label || id }}</div>
|
||||
<div v-if="!canEdit" class="poolItem__state">미배치</div>
|
||||
@@ -1454,6 +1485,31 @@ onUnmounted(() => {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.previewOnly__brand {
|
||||
width: fit-content;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
color: var(--theme-text-strong);
|
||||
}
|
||||
.previewOnly__brandMark {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--theme-accent-bg);
|
||||
color: var(--theme-accent-text);
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.previewOnly__brandText {
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
.previewOnly__title {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
@@ -2443,6 +2499,30 @@ onUnmounted(() => {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.sidebar__titleRow {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
.sidebar__count {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--theme-text-soft);
|
||||
}
|
||||
.sidebar__search {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 11px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--theme-border);
|
||||
background: var(--theme-card-bg);
|
||||
color: var(--theme-text);
|
||||
}
|
||||
.sidebar__search::placeholder {
|
||||
color: var(--theme-text-faint);
|
||||
}
|
||||
.pool {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
|
||||
@@ -2490,6 +2570,9 @@ onUnmounted(() => {
|
||||
text-transform: uppercase;
|
||||
color: var(--theme-text-soft);
|
||||
}
|
||||
.poolItem--hidden {
|
||||
display: none;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user