관리자 미디어 라이브러리 기본 기능 추가
This commit is contained in:
190
pages/admin/media/index.vue
Normal file
190
pages/admin/media/index.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
layout: 'admin'
|
||||
})
|
||||
|
||||
const searchText = ref('')
|
||||
const editingUrl = ref('')
|
||||
const editingName = ref('')
|
||||
const deletingUrl = ref('')
|
||||
const errorMessage = ref('')
|
||||
|
||||
const { data: mediaItems, refresh } = await useFetch('/admin/api/media', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const filteredMediaItems = computed(() => {
|
||||
const query = searchText.value.trim().toLowerCase()
|
||||
|
||||
if (!query) {
|
||||
return mediaItems.value
|
||||
}
|
||||
|
||||
return mediaItems.value.filter((item) => [
|
||||
item.name,
|
||||
item.url,
|
||||
item.category
|
||||
].some((value) => value.toLowerCase().includes(query)))
|
||||
})
|
||||
|
||||
/**
|
||||
* 파일 크기 표시 문자열 생성
|
||||
* @param {number} size - 파일 크기
|
||||
* @returns {string} 표시 문자열
|
||||
*/
|
||||
const formatFileSize = (size) => {
|
||||
if (size < 1024) {
|
||||
return `${size} B`
|
||||
}
|
||||
|
||||
if (size < 1024 * 1024) {
|
||||
return `${(size / 1024).toFixed(1)} KB`
|
||||
}
|
||||
|
||||
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 수정 상태 시작
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {void}
|
||||
*/
|
||||
const startRename = (item) => {
|
||||
editingUrl.value = item.url
|
||||
editingName.value = item.title
|
||||
errorMessage.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 파일명 변경 취소
|
||||
* @returns {void}
|
||||
*/
|
||||
const cancelRename = () => {
|
||||
editingUrl.value = ''
|
||||
editingName.value = ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 파일명 변경
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const renameMedia = async () => {
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await $fetch('/admin/api/media', {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
url: editingUrl.value,
|
||||
name: editingName.value
|
||||
}
|
||||
})
|
||||
cancelRename()
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.data?.message || '파일명을 변경하지 못했습니다.'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 미디어 삭제
|
||||
* @param {Object} item - 미디어 항목
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteMedia = async (item) => {
|
||||
if (!confirm(`"${item.name}" 파일을 삭제할까요?`)) {
|
||||
return
|
||||
}
|
||||
|
||||
deletingUrl.value = item.url
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
await $fetch('/admin/api/media', {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
url: item.url
|
||||
}
|
||||
})
|
||||
await refresh()
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.data?.message || '파일을 삭제하지 못했습니다.'
|
||||
} finally {
|
||||
deletingUrl.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-media bg-paper p-6">
|
||||
<div class="admin-media__header flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="admin-media__eyebrow text-xs font-semibold uppercase text-muted">
|
||||
Media
|
||||
</p>
|
||||
<h1 class="admin-media__title mt-2 text-3xl font-semibold">
|
||||
미디어
|
||||
</h1>
|
||||
</div>
|
||||
<input
|
||||
v-model="searchText"
|
||||
class="admin-media__search w-full rounded border border-line bg-white px-3 py-2 text-sm md:w-72"
|
||||
type="search"
|
||||
placeholder="파일명 또는 경로 검색"
|
||||
>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="admin-media__error mt-6 rounded border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
|
||||
<div v-if="filteredMediaItems.length" class="admin-media__grid mt-8 grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<article v-for="item in filteredMediaItems" :key="item.url" class="admin-media__item border border-line bg-white">
|
||||
<img class="admin-media__image aspect-[4/3] w-full bg-surface object-cover" :src="item.url" :alt="item.title">
|
||||
<div class="admin-media__body grid gap-3 p-4 text-sm">
|
||||
<div v-if="editingUrl === item.url" class="admin-media__rename grid gap-2">
|
||||
<input
|
||||
v-model="editingName"
|
||||
class="admin-media__rename-input rounded border border-line px-3 py-2 text-sm"
|
||||
type="text"
|
||||
@keydown.enter.prevent="renameMedia"
|
||||
>
|
||||
<div class="admin-media__rename-actions flex gap-2">
|
||||
<button class="admin-media__rename-save rounded bg-[#15171a] px-3 py-1.5 text-xs font-semibold text-white" type="button" @click="renameMedia">
|
||||
저장
|
||||
</button>
|
||||
<button class="admin-media__rename-cancel rounded border border-line px-3 py-1.5 text-xs font-semibold" type="button" @click="cancelRename">
|
||||
취소
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<strong class="admin-media__name break-all text-ink">{{ item.name }}</strong>
|
||||
<p class="admin-media__path break-all text-xs text-muted">{{ item.url }}</p>
|
||||
</template>
|
||||
|
||||
<p class="admin-media__meta text-xs text-muted">
|
||||
{{ item.category }} · {{ formatFileSize(item.size) }}
|
||||
</p>
|
||||
<div class="admin-media__actions flex gap-2">
|
||||
<button class="admin-media__rename-button rounded border border-line px-3 py-1.5 text-xs font-semibold" type="button" @click="startRename(item)">
|
||||
파일명 변경
|
||||
</button>
|
||||
<button
|
||||
class="admin-media__delete rounded border border-red-200 px-3 py-1.5 text-xs font-semibold text-red-700 disabled:opacity-50"
|
||||
type="button"
|
||||
:disabled="deletingUrl === item.url"
|
||||
@click="deleteMedia(item)"
|
||||
>
|
||||
{{ deletingUrl === item.url ? '삭제 중' : '삭제' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p v-else class="admin-media__empty mt-8 border border-dashed border-line bg-white p-8 text-center text-sm text-muted">
|
||||
표시할 미디어가 없습니다.
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user