From 6481f958f5d5775657404ee4c024c69032e7b398 Mon Sep 17 00:00:00 2001 From: zenn Date: Wed, 13 May 2026 11:43:38 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=ED=95=84=ED=84=B0?= =?UTF-8?q?=EC=99=80=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20=ED=8E=B8=EC=A7=91=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/AdminMemberForm.vue | 100 ++++- components/admin/AdminUnsavedChangesModal.vue | 2 +- docs/history.md | 6 + docs/map.md | 6 +- docs/spec.md | 4 +- docs/update.md | 9 + package-lock.json | 4 +- package.json | 2 +- pages/admin/members/index.vue | 389 +++++++++++++++++- 9 files changed, 483 insertions(+), 39 deletions(-) diff --git a/components/admin/AdminMemberForm.vue b/components/admin/AdminMemberForm.vue index 1f191cf..bd5889e 100644 --- a/components/admin/AdminMemberForm.vue +++ b/components/admin/AdminMemberForm.vue @@ -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} + */ +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} @@ -221,15 +274,39 @@ watch(() => props.member, () => {