From 3843e16d9f14fd9ee8f0c37994c0778c156a037f Mon Sep 17 00:00:00 2001 From: zenn Date: Tue, 26 May 2026 16:22:05 +0900 Subject: [PATCH] =?UTF-8?q?VIP=20=EB=A9=A4=EB=B2=84=EC=8B=AD=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=20=EB=B2=94=EC=9C=84=20=EC=A0=81=EC=9A=A9=20v1.5.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/admin/AdminMemberForm.vue | 64 +++++++++++++++++++ components/admin/AdminPostForm.vue | 2 +- db/migrations/037_add_vip_member_role.sql | 6 ++ docs/changelog.md | 5 ++ docs/history.md | 6 +- docs/map.md | 13 ++-- docs/spec.md | 18 +++--- docs/todo.md | 1 - docs/update.md | 11 +++- package-lock.json | 4 +- package.json | 2 +- pages/admin/media/index.vue | 6 +- server/api/posts.get.js | 12 ++-- server/api/posts/[slug].get.js | 5 +- server/repositories/content-repository.js | 12 ++-- server/repositories/member-repository.js | 48 ++++++++++---- .../routes/admin/api/members/[id]/role.put.js | 2 +- 17 files changed, 169 insertions(+), 48 deletions(-) create mode 100644 db/migrations/037_add_vip_member_role.sql diff --git a/components/admin/AdminMemberForm.vue b/components/admin/AdminMemberForm.vue index 082c9bb..85a379e 100644 --- a/components/admin/AdminMemberForm.vue +++ b/components/admin/AdminMemberForm.vue @@ -24,13 +24,22 @@ const passwordModalOpen = ref(false) const deleteModalOpen = ref(false) const isUpdatingPassword = ref(false) const isDeletingMember = ref(false) +const isUpdatingRole = ref(false) const actionMessage = ref('') const actionError = ref('') +const roleOptions = [ + { value: 'owner', label: '소유자' }, + { value: 'admin', label: '관리자' }, + { value: 'vip', label: 'VIP' }, + { value: 'member', label: '멤버' } +] + const form = reactive({ username: '', email: '', avatarUrl: '', + roleCode: 'member', labelsText: '', note: '' }) @@ -53,6 +62,7 @@ const syncMemberForm = () => { form.username = member.username || '' form.email = member.email || '' form.avatarUrl = member.avatarUrl || '' + form.roleCode = member.roleCode || 'member' form.labelsText = Array.isArray(member.labels) ? member.labels.join(', ') : '' form.note = member.note || '' } @@ -77,6 +87,8 @@ const normalizedLabels = computed(() => [...new Set( .filter(Boolean) )]) +const currentRoleLabel = computed(() => roleOptions.find((option) => option.value === form.roleCode)?.label || '멤버') + /** * 회원 저장 요청 본문을 문자열로 직렬화한다. * @returns {string} 직렬화된 회원 입력값 @@ -328,6 +340,40 @@ const updateMemberPassword = async () => { } } +/** + * 관리자 권한으로 회원 등급을 변경한다. + * @returns {Promise} + */ +const updateMemberRole = async () => { + if (isNewMember.value || isUpdatingRole.value) { + return + } + + actionMessage.value = '' + actionError.value = '' + isUpdatingRole.value = true + + try { + const updated = await $fetch(`/admin/api/members/${props.member.id}/role`, { + method: 'PUT', + body: { + role: form.roleCode + } + }) + + emit('saved', { + ...props.member, + ...updated + }) + actionMessage.value = '멤버 등급이 변경되었습니다.' + } catch (error) { + form.roleCode = props.member?.roleCode || 'member' + actionError.value = error?.data?.message || '멤버 등급 변경에 실패했습니다.' + } finally { + isUpdatingRole.value = false + } +} + /** * 관리자 권한으로 회원을 삭제한다. * @returns {Promise} @@ -481,6 +527,7 @@ watch(() => props.member, () => {

{{ pageTitle }}

{{ form.email || '이메일 없음' }}

+

{{ currentRoleLabel }}

@@ -532,6 +579,23 @@ watch(() => props.member, () => { + +