Google 계정 연동 관리 개선

This commit is contained in:
2026-07-29 14:18:40 +09:00
parent 52ed053876
commit 3c431576b7
13 changed files with 258 additions and 16 deletions

View File

@@ -30,8 +30,10 @@ const isEditingMember = ref(props.mode === 'new')
const actionMenuOpen = ref(false)
const passwordModalOpen = ref(false)
const deleteModalOpen = ref(false)
const googleUnlinkModalOpen = ref(false)
const isUpdatingPassword = ref(false)
const isDeletingMember = ref(false)
const isUnlinkingGoogle = ref(false)
const actionError = ref('')
const roleOptions = [
@@ -109,6 +111,7 @@ const isFormEditable = computed(() => {
const currentRoleLabel = computed(() => roleOptions.find((option) => option.value === form.roleCode)?.label || '멤버')
const currentAdminRoleCode = computed(() => adminSession.value?.roleCode || '')
const isCurrentAdminPrivileged = computed(() => ['owner', 'admin'].includes(currentAdminRoleCode.value))
const isCurrentAdminOwner = computed(() => currentAdminRoleCode.value === 'owner')
const isEditingSelf = computed(() => Boolean(props.member?.id && adminSession.value?.userId)
&& String(props.member.id) === String(adminSession.value.userId))
/** 저장된 대상 회원 등급(권한 판단용) */
@@ -135,6 +138,9 @@ const canEditMemberProfile = computed(() => {
const canOpenEditMode = computed(() => !isNewMember.value && canEditMemberProfile.value && !isEditingMember.value)
const canManageMemberActions = computed(() => canEditMemberProfile.value)
const shouldRenderRoleAsText = computed(() => !isFormEditable.value || isNewMember.value || !isCurrentAdminPrivileged.value)
const googleLinked = computed(() => Boolean(props.member?.googleLinkedAt))
const googleAccountText = computed(() => props.member?.googleEmail || props.member?.googleName || 'Google 계정')
const canUnlinkGoogle = computed(() => !isNewMember.value && googleLinked.value && isCurrentAdminOwner.value)
/**
* 회원 등급 변경 권한 여부(저장 중 상태는 제외)
@@ -443,6 +449,31 @@ const closeDeleteModal = () => {
deleteModalOpen.value = false
}
/**
* Google 연동 해제 확인 모달을 연다.
* @returns {void}
*/
const openGoogleUnlinkModal = () => {
if (!canUnlinkGoogle.value) {
return
}
actionError.value = ''
googleUnlinkModalOpen.value = true
}
/**
* Google 연동 해제 확인 모달을 닫는다.
* @returns {void}
*/
const closeGoogleUnlinkModal = () => {
if (isUnlinkingGoogle.value) {
return
}
googleUnlinkModalOpen.value = false
}
/**
* 관리자 권한으로 회원 비밀번호를 변경한다.
* @returns {Promise<void>}
@@ -508,6 +539,34 @@ const deleteMember = async () => {
}
}
/**
* 소유자 권한으로 회원 Google 연동을 해제한다.
* @returns {Promise<void>}
*/
const unlinkGoogleIdentity = async () => {
if (!canUnlinkGoogle.value || isUnlinkingGoogle.value) {
return
}
actionError.value = ''
isUnlinkingGoogle.value = true
try {
const updated = await $fetch(`/admin/api/members/${props.member.id}/google`, {
method: 'DELETE'
})
googleUnlinkModalOpen.value = false
emit('saved', updated)
showToast('success', 'Google 연동을 해제했습니다.')
} catch (error) {
const message = error?.data?.message || 'Google 연동 해제에 실패했습니다.'
actionError.value = message
showToast('error', message)
} finally {
isUnlinkingGoogle.value = false
}
}
/**
* 회원 기본 정보를 저장한다.
* @returns {Promise<Object>} 저장된 회원
@@ -833,6 +892,38 @@ watch(() => props.member, () => {
</label>
</form>
<section v-if="!isNewMember" class="admin-member-form__google">
<h2 class="admin-member-form__section-title mb-4 text-xs font-semibold uppercase tracking-[0.04em] text-[#15171a]">Google 로그인</h2>
<div class="admin-member-form__google-card rounded-xl border border-line bg-white px-5 py-5 md:px-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="min-w-0">
<p class="text-sm font-semibold text-[#15171a]">
{{ googleLinked ? `${googleAccountText} 연결됨` : '연결된 Google 계정 없음' }}
</p>
<p v-if="googleLinked" class="mt-1 text-sm text-[#8a95a5]">
연결일 {{ formatDate(member?.googleLinkedAt) }}
</p>
</div>
<div class="flex shrink-0 items-center gap-2">
<span
class="admin-member-form__google-badge inline-flex h-9 items-center rounded-full px-4 text-sm font-semibold"
:class="googleLinked ? 'bg-[#eef1f4] text-[#8a95a5]' : 'bg-[#f7f8fa] text-[#b3bbc5]'"
>
{{ googleLinked ? '연결 됨' : '미연결' }}
</span>
<button
v-if="canUnlinkGoogle"
class="admin-member-form__google-unlink h-9 rounded-md border border-[#ffd2d6] px-4 text-sm font-semibold text-[#d21a26] transition hover:bg-[#fff1f2]"
type="button"
@click="openGoogleUnlinkModal"
>
연동 해제
</button>
</div>
</div>
</div>
</section>
<section v-if="!isNewMember" class="admin-member-form__activity">
<h2 class="admin-member-form__section-title mb-4 text-xs font-semibold uppercase tracking-[0.04em] text-[#15171a]">활동</h2>
<div class="admin-member-form__activity-card rounded-xl border border-line bg-white px-5 md:px-6">
@@ -936,6 +1027,35 @@ watch(() => props.member, () => {
</div>
</Teleport>
<Teleport to="body">
<div v-if="googleUnlinkModalOpen" class="admin-member-form__modal fixed inset-0 z-50 flex items-start justify-center bg-black/35 px-4 pt-10">
<section class="admin-member-form__modal-panel w-full max-w-[520px] rounded-xl bg-white text-[#15171a] shadow-[0_22px_70px_rgba(15,23,42,0.25)]">
<header class="flex items-center justify-between border-b border-line px-6 py-5">
<h2 class="text-xl font-semibold">Google 연동 해제</h2>
<button class="grid h-8 w-8 place-items-center rounded-md text-[#657080] hover:bg-[#f3f5f7]" type="button" aria-label="닫기" @click="closeGoogleUnlinkModal">
<svg class="h-4 w-4" 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>
</header>
<div class="grid gap-4 px-6 py-5">
<p class="text-sm leading-6 text-[#657080]">
<strong class="text-[#15171a]">{{ form.username || form.email }}</strong> 님의 Google 로그인 연동을 해제합니다. 해제 후에는 이 Google 계정으로 로그인할 수 없습니다.
</p>
<p v-if="actionError" class="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{{ actionError }}</p>
</div>
<footer class="flex justify-end gap-2 border-t border-line px-6 py-4">
<button class="h-10 rounded-md border border-line bg-white px-4 text-sm font-semibold text-[#3f4650]" type="button" @click="closeGoogleUnlinkModal">
취소
</button>
<button class="h-10 rounded-md bg-[#d21a26] px-4 text-sm font-semibold text-white disabled:opacity-50" type="button" :disabled="isUnlinkingGoogle" @click="unlinkGoogleIdentity">
{{ isUnlinkingGoogle ? '해제 중' : '연동 해제' }}
</button>
</footer>
</section>
</div>
</Teleport>
<Teleport to="body">
<div v-if="ownershipTransferModalOpen" class="admin-member-form__modal fixed inset-0 z-50 flex items-start justify-center bg-black/35 px-4 pt-10">
<section class="admin-member-form__modal-panel w-full max-w-[520px] rounded-xl bg-white text-[#15171a] shadow-[0_22px_70px_rgba(15,23,42,0.25)]">