VIP 멤버십 공개 범위 적용 v1.5.5

This commit is contained in:
2026-05-26 16:22:05 +09:00
parent 6333c4254f
commit 3843e16d9f
17 changed files with 169 additions and 48 deletions

View File

@@ -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<void>}
*/
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<void>}
@@ -481,6 +527,7 @@ watch(() => props.member, () => {
<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>
<p class="mt-1 text-xs font-semibold uppercase tracking-[0.04em] text-[#9aa4b2]">{{ currentRoleLabel }}</p>
</div>
</div>
@@ -532,6 +579,23 @@ 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]">멤버 등급</span>
<select
v-model="form.roleCode"
class="admin-member-form__select 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] disabled:opacity-60"
:disabled="isNewMember || isUpdatingRole"
@change="updateMemberRole"
>
<option v-for="option in roleOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<span class="admin-member-form__hint mt-2 block text-sm text-[#8a95a5]">
VIP 이상 등급은 멤버십 게시물을 있습니다.
</span>
</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="쉼표로 구분">

View File

@@ -1659,7 +1659,7 @@ defineExpose({
</svg>
</span>
<span v-if="form.status === 'members'" class="admin-post-form__hint text-xs text-muted">
로그인한 회원에게만 공개됩니다. 등급별 제한은 이후 멤버십 등급 기능에서 확장합니다.
VIP 이상 등급 회원에게만 공개됩니다.
</span>
<span v-else-if="form.status === 'private'" class="admin-post-form__hint text-xs text-muted">
공개 사용자 화면에서는 보이지 않습니다.