릴리스: v0.1.10 관리자 업로드 UX 정리

This commit is contained in:
2026-03-19 15:42:12 +09:00
parent beaec9326b
commit f9ae036890
5 changed files with 75 additions and 13 deletions

View File

@@ -23,6 +23,8 @@ const uploadFile = ref(null)
const thumbFile = ref(null)
const itemPreviewUrl = ref('')
const thumbPreviewUrl = ref('')
const itemFileInput = ref(null)
const thumbFileInput = ref(null)
onMounted(async () => {
await auth.refresh()
@@ -60,6 +62,8 @@ function setMode(mode) {
uploadLabel.value = ''
uploadFile.value = null
thumbFile.value = null
resetFileInput('item')
resetFileInput('thumb')
clearPreviewUrl('item')
clearPreviewUrl('thumb')
}
@@ -75,8 +79,25 @@ function clearPreviewUrl(type) {
}
}
function resetFileInput(type) {
if (type === 'item' && itemFileInput.value) {
itemFileInput.value.value = ''
}
if (type === 'thumb' && thumbFileInput.value) {
thumbFileInput.value.value = ''
}
}
async function loadGame() {
resetMessages()
uploadLabel.value = ''
uploadFile.value = null
thumbFile.value = null
resetFileInput('item')
resetFileInput('thumb')
clearPreviewUrl('item')
clearPreviewUrl('thumb')
if (!selectedGameId.value) {
selectedGame.value = null
return
@@ -146,6 +167,7 @@ async function uploadThumbnail() {
if (!res.ok) throw new Error('failed')
thumbFile.value = null
resetFileInput('thumb')
clearPreviewUrl('thumb')
await refreshGames()
await loadGame()
@@ -175,6 +197,7 @@ async function uploadItem() {
uploadLabel.value = ''
uploadFile.value = null
resetFileInput('item')
clearPreviewUrl('item')
await loadGame()
success.value = '아이템이 추가됐어요.'
@@ -219,6 +242,11 @@ async function removeGame() {
const deletedName = selectedGame.value.game.name
selectedGameId.value = ''
selectedGame.value = null
uploadLabel.value = ''
uploadFile.value = null
thumbFile.value = null
resetFileInput('item')
resetFileInput('thumb')
clearPreviewUrl('item')
clearPreviewUrl('thumb')
await refreshGames()
@@ -233,6 +261,9 @@ const displayThumbnailUrl = computed(() => {
if (selectedGame.value?.game?.thumbnailSrc) return toApiUrl(selectedGame.value.game.thumbnailSrc)
return ''
})
const canApplyThumbnail = computed(() => !!thumbFile.value && !!selectedGameId.value)
const canAddItem = computed(() => !!uploadFile.value && !!selectedGameId.value)
</script>
<template>
@@ -295,30 +326,30 @@ const displayThumbnailUrl = computed(() => {
:src="displayThumbnailUrl"
:alt="selectedGame.game.name"
/>
<div v-else class="selectedThumb selectedThumb--empty">16:9 미리보기</div>
<div v-else class="selectedThumb selectedThumb--empty">썸네일 미리보기</div>
<div class="uploadPreviewMeta">
<div class="uploadPreviewTitle">대표 썸네일</div>
<div class="uploadPreviewDesc">
파일을 선택하면 먼저 미리보기로 확인되고, 업로드 버튼을 눌렀을 실제 저장됩니다.
파일을 선택하면 먼저 미리보기로 확인되고, 적용 버튼을 눌렀을 실제 저장됩니다.
</div>
</div>
</div>
<input type="file" accept="image/*" class="inputFile" @change="onThumb" />
<button class="btn" @click="uploadThumbnail">썸네일 업로드</button>
<input ref="thumbFileInput" type="file" accept="image/*" class="inputFile" @change="onThumb" />
<button class="btn" :disabled="!canApplyThumbnail" @click="uploadThumbnail">썸네일 적용</button>
</div>
<div class="section">
<div class="section__title">아이템 추가</div>
<div class="itemComposer">
<div class="itemComposer__form">
<input v-model="uploadLabel" class="input" placeholder="아이템 이름" />
<input type="file" accept="image/*" class="inputFile" @change="onFile" />
<button class="btn" @click="uploadItem">아이템 추가</button>
<input v-model="uploadLabel" class="input input--compact" placeholder="아이템 이름" />
<input ref="itemFileInput" type="file" accept="image/*" class="inputFile" @change="onFile" />
<button class="btn" :disabled="!canAddItem" @click="uploadItem">아이템 추가</button>
</div>
<div class="itemPreviewCard">
<div class="itemPreviewFrame">
<img v-if="itemPreviewUrl" class="itemPreviewImage" :src="itemPreviewUrl" alt="item preview" />
<div v-else class="itemPreviewEmpty">1:1 미리보기</div>
<div v-else class="itemPreviewEmpty">이미지를 선택해주세요</div>
</div>
<div class="thumbLabel thumbLabel--preview">{{ uploadLabel || '아이템 이름 미리보기' }}</div>
</div>
@@ -449,6 +480,9 @@ const displayThumbnailUrl = computed(() => {
outline: none;
margin-top: 10px;
}
.input--compact {
max-width: 360px;
}
.hint {
margin-top: 10px;
opacity: 0.78;
@@ -468,6 +502,10 @@ const displayThumbnailUrl = computed(() => {
cursor: pointer;
font-weight: 800;
}
.btn:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.btn--primary {
background: rgba(96, 165, 250, 0.2);
}
@@ -503,7 +541,7 @@ const displayThumbnailUrl = computed(() => {
margin-top: 10px;
}
.selectedThumb {
width: 256px;
width: min(100%, 256px);
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 16px;
@@ -518,7 +556,7 @@ const displayThumbnailUrl = computed(() => {
.itemComposer {
margin-top: 10px;
display: grid;
grid-template-columns: minmax(0, 1fr) 180px;
grid-template-columns: minmax(0, 1fr) minmax(160px, 192px);
gap: 16px;
align-items: start;
}
@@ -532,12 +570,13 @@ const displayThumbnailUrl = computed(() => {
background: rgba(255, 255, 255, 0.04);
}
.itemPreviewFrame {
width: 100%;
width: min(100%, 192px);
aspect-ratio: 1 / 1;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(0, 0, 0, 0.18);
margin: 0 auto;
}
.itemPreviewImage {
width: 100%;
@@ -589,10 +628,16 @@ const displayThumbnailUrl = computed(() => {
.itemComposer {
grid-template-columns: 1fr;
}
.itemPreviewCard {
max-width: 192px;
}
}
@media (max-width: 640px) {
.selectedThumb {
width: 100%;
width: min(100%, 256px);
}
.itemPreviewCard {
max-width: 192px;
}
.thumbGrid {
grid-template-columns: 1fr;