Google 계정 로그인 연동 추가
This commit is contained in:
@@ -13,6 +13,7 @@ const errorMessage = ref('')
|
||||
const showPassword = ref(false)
|
||||
const emailInput = ref(null)
|
||||
const passwordInput = ref(null)
|
||||
const route = useRoute()
|
||||
|
||||
const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', {
|
||||
default: () => ({
|
||||
@@ -27,6 +28,30 @@ const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', {
|
||||
*/
|
||||
const canSubmitAdminLogin = computed(() => Boolean(form.email.trim()) && Boolean(form.password))
|
||||
|
||||
const googleStatusMessage = computed(() => {
|
||||
const status = String(route.query.google || '')
|
||||
if (status === 'admin-not-linked') {
|
||||
return '연결된 관리자 Google 계정을 찾을 수 없습니다. 먼저 관리자 계정에서 Google 계정을 연결해 주세요.'
|
||||
}
|
||||
if (status === 'email-unverified') {
|
||||
return 'Google 계정의 이메일 확인 상태를 확인할 수 없습니다.'
|
||||
}
|
||||
if (status === 'cancelled') {
|
||||
return 'Google 로그인이 취소되었습니다.'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
/**
|
||||
* 관리자 Google 로그인으로 이동한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const startGoogleAdminLogin = () => {
|
||||
if (import.meta.client) {
|
||||
window.location.href = '/api/auth/google/start?mode=admin&redirect=/admin'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 브라우저/비밀번호 관리자 자동완성 값을 로그인 폼 상태에 반영한다.
|
||||
* @returns {void}
|
||||
@@ -158,6 +183,20 @@ onMounted(() => {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-5 border-t border-[#1a212a] pt-5">
|
||||
<button
|
||||
class="admin-login__google-button flex h-10 w-full items-center justify-center gap-2 rounded-[8px] border border-[#1a212a] bg-white text-sm font-semibold text-[#15171a] transition hover:bg-[#f1f3f4]"
|
||||
type="button"
|
||||
@click="startGoogleAdminLogin"
|
||||
>
|
||||
<span class="text-lg font-bold text-[#4285f4]">G</span>
|
||||
Google로 로그인
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="googleStatusMessage" class="mt-4 text-right text-xs text-[#e5acb1]" aria-live="polite">
|
||||
{{ googleStatusMessage }}
|
||||
</p>
|
||||
|
||||
<NuxtLink class="mt-6 flex justify-end text-xs text-[#9ba3af] hover:opacity-80" to="/">
|
||||
사이트로 돌아가기
|
||||
</NuxtLink>
|
||||
|
||||
@@ -8,9 +8,11 @@ const removingAvatar = ref(false)
|
||||
const profileMessage = ref('')
|
||||
const passwordMessage = ref('')
|
||||
const deleteMessage = ref('')
|
||||
const googleMessage = ref('')
|
||||
const actionMenuOpen = ref(false)
|
||||
const passwordModalOpen = ref(false)
|
||||
const deleteModalOpen = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
const profileForm = reactive({
|
||||
email: '',
|
||||
@@ -24,6 +26,13 @@ const profileForm = reactive({
|
||||
commentCount: 0
|
||||
})
|
||||
|
||||
const googleProfile = reactive({
|
||||
email: '',
|
||||
name: '',
|
||||
avatarUrl: '',
|
||||
linkedAt: ''
|
||||
})
|
||||
|
||||
const avatarInputRef = ref(null)
|
||||
|
||||
const passwordForm = reactive({
|
||||
@@ -39,6 +48,10 @@ const deleteForm = reactive({
|
||||
const displayName = computed(() => profileForm.username || profileForm.email || '멤버')
|
||||
const avatarInitial = computed(() => String(displayName.value || '?').slice(0, 1).toUpperCase())
|
||||
const previousLoginText = computed(() => formatRelativeTime(profileForm.previousLastSeenAt, '이전 로그인 기록 없음'))
|
||||
const googleLinked = computed(() => Boolean(googleProfile.linkedAt))
|
||||
const googleLinkedText = computed(() => googleLinked.value
|
||||
? `${googleProfile.email || googleProfile.name || 'Google 계정'} 연결됨`
|
||||
: 'Google 계정이 연결되지 않았습니다.')
|
||||
|
||||
/**
|
||||
* 날짜 표시 형식을 변환한다.
|
||||
@@ -118,6 +131,10 @@ const loadProfile = async () => {
|
||||
profileForm.previousLastSeenAt = profile.previousLastSeenAt || ''
|
||||
profileForm.previousLastSeenIp = profile.previousLastSeenIp || ''
|
||||
profileForm.commentCount = Number(profile.commentCount || 0)
|
||||
googleProfile.email = profile.googleEmail || ''
|
||||
googleProfile.name = profile.googleName || ''
|
||||
googleProfile.avatarUrl = profile.googleAvatarUrl || ''
|
||||
googleProfile.linkedAt = profile.googleLinkedAt || ''
|
||||
} catch {
|
||||
await navigateTo('/signin')
|
||||
} finally {
|
||||
@@ -125,6 +142,34 @@ const loadProfile = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Google 계정 연결을 시작한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const startGoogleLink = () => {
|
||||
if (import.meta.client) {
|
||||
window.location.href = '/api/auth/google/start?mode=link&redirect=/settings'
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query.google,
|
||||
(status) => {
|
||||
const value = String(status || '')
|
||||
if (value === 'linked') {
|
||||
googleMessage.value = 'Google 계정이 연결되었습니다.'
|
||||
loadProfile()
|
||||
} else if (value === 'already-linked') {
|
||||
googleMessage.value = '이미 다른 계정에 연결된 Google 계정입니다.'
|
||||
} else if (value === 'email-unverified') {
|
||||
googleMessage.value = 'Google 계정의 이메일 확인 상태를 확인할 수 없습니다.'
|
||||
} else if (value === 'session-mismatch') {
|
||||
googleMessage.value = '로그인 세션이 바뀌었습니다. 다시 로그인해 주세요.'
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
/**
|
||||
* 닉네임 중복 여부를 확인한다.
|
||||
* @returns {Promise<boolean>} 사용 가능 여부
|
||||
@@ -505,6 +550,26 @@ onMounted(loadProfile)
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-page__google">
|
||||
<h2 class="settings-page__section-title mb-4 text-xs font-semibold uppercase tracking-[0.04em]">Google 계정</h2>
|
||||
<div class="settings-page__google-card rounded-[12px] border border-[var(--site-line)] bg-[var(--site-bg)] p-5 sm:p-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">{{ googleLinkedText }}</p>
|
||||
<p class="mt-1 text-sm site-muted">
|
||||
연결하면 다음 로그인부터 Google 계정으로 로그인할 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
<button class="settings-page__google-link-button h-10 rounded-[8px] border border-[var(--site-line)] px-4 text-sm font-semibold transition hover:bg-[var(--site-panel)]" type="button" @click="startGoogleLink">
|
||||
{{ googleLinked ? '다시 연결' : 'Google 계정 연결' }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="googleMessage" class="mt-4 text-sm site-muted" aria-live="polite">
|
||||
{{ googleMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const isSubmitting = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const statusMessage = ref('')
|
||||
const showPassword = ref(false)
|
||||
const route = useRoute()
|
||||
|
||||
const form = reactive({
|
||||
email: '',
|
||||
@@ -19,6 +20,33 @@ const form = reactive({
|
||||
*/
|
||||
const canSubmitSignIn = computed(() => Boolean(form.email.trim()) && Boolean(form.password))
|
||||
|
||||
const googleStatusMessage = computed(() => {
|
||||
const status = String(route.query.google || '')
|
||||
if (status === 'email-exists') {
|
||||
return '이미 가입된 이메일입니다. 기존 계정으로 로그인한 뒤 Google 계정을 연결해 주세요.'
|
||||
}
|
||||
if (status === 'email-unverified') {
|
||||
return 'Google 계정의 이메일 확인 상태를 확인할 수 없습니다.'
|
||||
}
|
||||
if (status === 'session-mismatch') {
|
||||
return '로그인 세션이 바뀌었습니다. 다시 로그인해 주세요.'
|
||||
}
|
||||
if (status === 'cancelled') {
|
||||
return 'Google 로그인이 취소되었습니다.'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
/**
|
||||
* Google 로그인으로 이동한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const startGoogleSignIn = () => {
|
||||
if (import.meta.client) {
|
||||
window.location.href = '/api/auth/google/start?mode=member&redirect=/'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인 입력값을 검증한다.
|
||||
* @returns {boolean} 검증 통과 여부
|
||||
@@ -113,9 +141,23 @@ const submitSignIn = async () => {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-5 border-t border-[#1a212a] pt-5">
|
||||
<button
|
||||
class="auth-signin__google-button flex h-10 w-full items-center justify-center gap-2 rounded-[8px] border border-[#1a212a] bg-white text-sm font-semibold text-[#15171a] transition hover:bg-[#f1f3f4]"
|
||||
type="button"
|
||||
@click="startGoogleSignIn"
|
||||
>
|
||||
<span class="text-lg font-bold text-[#4285f4]">G</span>
|
||||
Google로 로그인
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="mt-4 text-xs text-[#e5acb1]" aria-live="polite">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<p v-if="googleStatusMessage" class="mt-4 text-xs text-[#e5acb1]" aria-live="polite">
|
||||
{{ googleStatusMessage }}
|
||||
</p>
|
||||
<p v-if="statusMessage" class="mt-4 text-xs text-[#9fc4ff]" aria-live="polite">
|
||||
{{ statusMessage }}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user