멤버 필터와 썸네일 편집 개선

This commit is contained in:
2026-05-13 11:43:38 +09:00
parent 79d0a30475
commit 6481f958f5
9 changed files with 483 additions and 39 deletions

View File

@@ -17,6 +17,8 @@ const saveMessage = ref('')
const saveError = ref('')
const isSaving = ref(false)
const savedMemberSnapshot = ref('')
const avatarInputRef = ref(null)
const isUploadingAvatar = ref(false)
const form = reactive({
username: '',
@@ -146,6 +148,57 @@ const {
leaveUnsavedPage
} = useAdminUnsavedChangesGuard(hasUnsavedMemberChanges)
/**
* 썸네일 파일 선택창을 연다.
* @returns {void}
*/
const openAvatarFilePicker = () => {
avatarInputRef.value?.click()
}
/**
* 회원 썸네일 파일을 업로드하고 폼에 반영한다.
* @param {Event} event - 파일 선택 이벤트
* @returns {Promise<void>}
*/
const uploadAvatar = async (event) => {
const target = event.target instanceof HTMLInputElement ? event.target : null
const file = target?.files?.[0]
if (!file || isUploadingAvatar.value) {
return
}
isUploadingAvatar.value = true
saveError.value = ''
saveMessage.value = ''
try {
const formData = new FormData()
formData.append('files', file)
const result = await $fetch('/admin/api/uploads', {
method: 'POST',
body: formData
})
form.avatarUrl = result.files?.[0]?.url || ''
} catch (error) {
saveError.value = error?.data?.message || '썸네일 업로드에 실패했습니다.'
} finally {
isUploadingAvatar.value = false
if (target) {
target.value = ''
}
}
}
/**
* 회원 썸네일 연결을 제거한다.
* @returns {void}
*/
const removeAvatar = () => {
form.avatarUrl = ''
}
/**
* 회원 기본 정보를 저장한다.
* @returns {Promise<void>}
@@ -221,15 +274,39 @@ watch(() => props.member, () => {
<div class="admin-member-form__body grid gap-8 py-8 xl:grid-cols-3">
<aside class="admin-member-form__summary">
<div class="admin-member-form__identity flex items-center gap-4">
<img
v-if="form.avatarUrl"
class="admin-member-form__avatar h-20 w-20 rounded-full object-cover"
:src="form.avatarUrl"
:alt="pageTitle"
>
<span v-else class="admin-member-form__avatar flex h-20 w-20 items-center justify-center rounded-full bg-[#15171a] text-2xl font-semibold text-white">
{{ memberInitial }}
</span>
<div class="admin-member-form__avatar-control group relative h-20 w-20 shrink-0">
<button
class="admin-member-form__avatar-button relative h-20 w-20 overflow-hidden rounded-full bg-[#15171a] text-white"
type="button"
:aria-label="form.avatarUrl ? '썸네일 변경' : '썸네일 등록'"
@click="openAvatarFilePicker"
>
<img
v-if="form.avatarUrl"
class="admin-member-form__avatar h-full w-full object-cover"
:src="form.avatarUrl"
:alt="pageTitle"
>
<span v-else class="admin-member-form__avatar flex h-full w-full items-center justify-center text-2xl font-semibold">
{{ memberInitial }}
</span>
<span class="admin-member-form__avatar-caption absolute inset-x-0 bottom-0 flex min-h-8 items-end justify-center bg-gradient-to-t from-black/80 via-black/35 to-transparent px-2 pb-2 text-center text-[11px] font-semibold leading-tight text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
{{ isUploadingAvatar ? '업로드 중' : form.avatarUrl ? '썸네일 변경' : '썸네일 등록' }}
</span>
</button>
<button
v-if="form.avatarUrl"
class="admin-member-form__avatar-remove absolute right-0 top-0 grid size-6 -translate-y-1 translate-x-1 place-items-center rounded-full bg-black/85 text-white opacity-0 shadow-sm transition hover:bg-[#d21a26] group-hover:opacity-100 group-focus-within:opacity-100"
type="button"
aria-label="썸네일 제거"
@click.stop="removeAvatar"
>
<svg class="h-3 w-3" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12.707 12L23.854.854a.5.5 0 00-.707-.707L12 11.293.854.146a.5.5 0 00-.707.707L11.293 12 .146 23.146a.5.5 0 00.708.708L12 12.707l11.146 11.146a.5.5 0 10.708-.706L12.707 12z" fill="currentColor" />
</svg>
</button>
<input ref="avatarInputRef" class="sr-only" type="file" accept="image/*" @change="uploadAvatar">
</div>
<div class="min-w-0">
<h2 class="truncate text-lg font-semibold text-[#15171a]">{{ pageTitle }}</h2>
<p class="mt-1 truncate text-sm text-[#657080]">{{ form.email || '이메일 없음' }}</p>
@@ -284,11 +361,6 @@ watch(() => props.member, () => {
</label>
</div>
<label class="admin-member-form__field mt-5 block">
<span class="admin-member-form__label mb-2 block text-sm font-semibold text-[#15171a]">썸네일 URL</span>
<input v-model="form.avatarUrl" class="admin-member-form__input h-12 w-full rounded-md border border-transparent bg-[#eef1f4] px-4 text-sm text-[#15171a] outline-none focus:border-[#c5ccd5] focus:bg-white focus:ring-2 focus:ring-[#dce2e8]" type="text" maxlength="500" placeholder="/uploads/members/avatars/...">
</label>
<label class="admin-member-form__field mt-5 block">
<span class="admin-member-form__label mb-2 block text-sm font-semibold text-[#15171a]">레이블</span>
<input v-model="form.labelsText" class="admin-member-form__input h-12 w-full rounded-md border border-transparent bg-[#eef1f4] px-4 text-sm text-[#15171a] outline-none focus:border-[#c5ccd5] focus:bg-white focus:ring-2 focus:ring-[#dce2e8]" type="text" placeholder="쉼표로 구분">