릴리스: v0.1.38 아이템 이름 수정과 티어표 썸네일 추가
This commit is contained in:
@@ -26,6 +26,9 @@ const pool = ref([])
|
||||
const itemsById = ref({})
|
||||
|
||||
const title = ref('')
|
||||
const thumbnailSrc = ref('')
|
||||
const pendingThumbnailFile = ref(null)
|
||||
const thumbnailPreviewUrl = ref('')
|
||||
const description = ref('')
|
||||
const isPublic = ref(true)
|
||||
const error = ref('')
|
||||
@@ -45,6 +48,7 @@ const groupListEl = ref(null)
|
||||
const poolEl = ref(null)
|
||||
const groupDropEls = ref({})
|
||||
const fileEl = ref(null)
|
||||
const thumbnailFileEl = ref(null)
|
||||
const groupSortable = ref(null)
|
||||
const poolSortable = ref(null)
|
||||
const dropSortables = ref([])
|
||||
@@ -67,6 +71,7 @@ const effectiveTitle = computed(() => {
|
||||
if (customTitle) return customTitle
|
||||
return (gameName.value || gameId.value || 'Tier Maker').trim()
|
||||
})
|
||||
const displayThumbnailUrl = computed(() => thumbnailPreviewUrl.value || (thumbnailSrc.value ? resolveItemSrc({ src: thumbnailSrc.value }) : ''))
|
||||
const untitledWarning = computed(
|
||||
() =>
|
||||
canEdit.value &&
|
||||
@@ -237,6 +242,31 @@ function openFile() {
|
||||
fileEl.value?.click()
|
||||
}
|
||||
|
||||
function openThumbnailFile() {
|
||||
if (!canEdit.value) return
|
||||
thumbnailFileEl.value?.click()
|
||||
}
|
||||
|
||||
function onThumbnailChange(event) {
|
||||
const file = event.target.files?.[0]
|
||||
if (thumbnailPreviewUrl.value) {
|
||||
URL.revokeObjectURL(thumbnailPreviewUrl.value)
|
||||
thumbnailPreviewUrl.value = ''
|
||||
}
|
||||
pendingThumbnailFile.value = file || null
|
||||
if (file) thumbnailPreviewUrl.value = URL.createObjectURL(file)
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
function clearThumbnail() {
|
||||
if (thumbnailPreviewUrl.value) {
|
||||
URL.revokeObjectURL(thumbnailPreviewUrl.value)
|
||||
thumbnailPreviewUrl.value = ''
|
||||
}
|
||||
pendingThumbnailFile.value = null
|
||||
thumbnailSrc.value = ''
|
||||
}
|
||||
|
||||
function onFileChange(e) {
|
||||
const files = Array.from(e.target.files || [])
|
||||
if (!files.length) return
|
||||
@@ -322,12 +352,25 @@ async function uploadPendingCustomItems() {
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadPendingThumbnail() {
|
||||
if (!pendingThumbnailFile.value) return thumbnailSrc.value || ''
|
||||
const data = await api.uploadTierListThumbnail(pendingThumbnailFile.value)
|
||||
if (thumbnailPreviewUrl.value) {
|
||||
URL.revokeObjectURL(thumbnailPreviewUrl.value)
|
||||
thumbnailPreviewUrl.value = ''
|
||||
}
|
||||
pendingThumbnailFile.value = null
|
||||
thumbnailSrc.value = data.thumbnailSrc || ''
|
||||
return thumbnailSrc.value
|
||||
}
|
||||
|
||||
function buildPayload(existingId) {
|
||||
const finalTitle = effectiveTitle.value
|
||||
return {
|
||||
id: existingId || undefined,
|
||||
gameId: gameId.value,
|
||||
title: finalTitle,
|
||||
thumbnailSrc: thumbnailSrc.value || '',
|
||||
description: (description.value || '').trim(),
|
||||
isPublic: !!isPublic.value,
|
||||
groups: groups.value.map((g) => ({ id: g.id, name: g.name, itemIds: g.itemIds })),
|
||||
@@ -340,6 +383,7 @@ async function save() {
|
||||
isSaving.value = true
|
||||
try {
|
||||
await uploadPendingCustomItems()
|
||||
await uploadPendingThumbnail()
|
||||
const payload = buildPayload(tierListId.value && tierListId.value !== 'new' ? tierListId.value : null)
|
||||
const res = await api.saveTierList(payload)
|
||||
if (tierListId.value === 'new') history.replaceState({}, '', `/editor/${gameId.value}/${res.tierList.id}`)
|
||||
@@ -405,6 +449,7 @@ onMounted(() => {
|
||||
const t = res.tierList
|
||||
ownerId.value = t.authorId
|
||||
title.value = t.title
|
||||
thumbnailSrc.value = t.thumbnailSrc || ''
|
||||
description.value = t.description || ''
|
||||
isPublic.value = !!t.isPublic
|
||||
authorName.value = t.authorName || ''
|
||||
@@ -430,6 +475,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (thumbnailPreviewUrl.value) URL.revokeObjectURL(thumbnailPreviewUrl.value)
|
||||
destroySortables()
|
||||
})
|
||||
</script>
|
||||
@@ -440,6 +486,17 @@ onUnmounted(() => {
|
||||
<div class="kicker">{{ gameName || gameId }}</div>
|
||||
<input v-model="title" class="titleInput" placeholder="티어표 이름을 입력하세요" :readonly="!canEdit" />
|
||||
<div v-if="untitledWarning" class="titleNotice">{{ untitledWarning }}</div>
|
||||
<div v-if="canEdit" class="thumbComposer">
|
||||
<input ref="thumbnailFileEl" type="file" accept="image/*" class="hidden" @change="onThumbnailChange" />
|
||||
<div class="thumbComposer__preview">
|
||||
<img v-if="displayThumbnailUrl" class="thumbComposer__image" :src="displayThumbnailUrl" alt="썸네일 미리보기" />
|
||||
<div v-else class="thumbComposer__empty">썸네일 없음</div>
|
||||
</div>
|
||||
<div class="thumbComposer__actions">
|
||||
<button class="btn btn--ghost" @click="openThumbnailFile">썸네일 선택</button>
|
||||
<button class="btn btn--danger" :disabled="!pendingThumbnailFile && !thumbnailSrc" @click="clearThumbnail">썸네일 제거</button>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-model="description"
|
||||
class="descInput"
|
||||
@@ -612,6 +669,36 @@ onUnmounted(() => {
|
||||
line-height: 1.5;
|
||||
color: rgba(251, 191, 36, 0.94);
|
||||
}
|
||||
.thumbComposer {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
width: min(100%, 920px);
|
||||
}
|
||||
.thumbComposer__preview {
|
||||
width: min(100%, 360px);
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.thumbComposer__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.thumbComposer__empty {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: rgba(255, 255, 255, 0.62);
|
||||
}
|
||||
.thumbComposer__actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
|
||||
Reference in New Issue
Block a user