3bebe77cab
CI / build (push) Failing after 15m8s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
183 lines
5.3 KiB
Vue
183 lines
5.3 KiB
Vue
<template>
|
|
<!-- 로그인은 MainLayout(QLayout) 밖의 독립 화면 → QPage 를 자체 QLayout 으로 감싼다. -->
|
|
<q-layout view="hHh lpR fFf">
|
|
<q-page-container>
|
|
<q-page class="flex flex-center column q-pa-lg bg-brand">
|
|
<div class="column items-center q-mb-xl">
|
|
<q-avatar size="88px" color="white" text-color="primary" icon="pets" />
|
|
<div class="text-h4 text-weight-bold text-white q-mt-md">산책갈개</div>
|
|
<div class="text-white q-mt-xs" style="opacity: 0.9">
|
|
우리 강아지 성향에 맞는 동네 산책 메이트
|
|
</div>
|
|
</div>
|
|
|
|
<q-card flat class="login-card q-pa-lg">
|
|
<!-- 구글 로그인 버튼(GIS 렌더링 영역) -->
|
|
<div ref="googleBtn" class="flex flex-center q-mb-md"></div>
|
|
<div v-if="googleError" class="text-caption text-negative text-center q-mb-md">
|
|
{{ googleError }}
|
|
</div>
|
|
|
|
<!-- 애플 로그인 (네이티브 iOS 전용) -->
|
|
<q-btn
|
|
v-if="appleAvailable"
|
|
class="full-width q-py-sm"
|
|
color="black"
|
|
text-color="white"
|
|
unelevated
|
|
icon="mdi-apple"
|
|
label="Apple로 계속하기"
|
|
:loading="appleLoading"
|
|
@click="onApple"
|
|
/>
|
|
|
|
<!-- 개발용 로그인 (dev 빌드 전용) -->
|
|
<template v-if="isDev">
|
|
<q-separator class="q-my-md" />
|
|
<q-btn
|
|
class="full-width"
|
|
color="secondary"
|
|
text-color="accent"
|
|
flat
|
|
icon="build"
|
|
label="개발용 로그인 (local)"
|
|
:loading="devLoading"
|
|
@click="onDevLogin"
|
|
/>
|
|
</template>
|
|
</q-card>
|
|
|
|
<div class="text-caption text-white q-mt-lg" style="opacity: 0.8">
|
|
계속 진행하면 이용약관 및 개인정보 처리방침에 동의하게 됩니다.
|
|
</div>
|
|
</q-page>
|
|
</q-page-container>
|
|
</q-layout>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useQuasar } from 'quasar'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { authApi } from '@/api/auth'
|
|
import { ApiError } from '@/api/http'
|
|
import { isAppleSignInAvailable, signInWithApple } from '@/lib/appleAuth'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const $q = useQuasar()
|
|
const auth = useAuthStore()
|
|
|
|
const googleBtn = ref<HTMLElement | null>(null)
|
|
const googleError = ref('')
|
|
const appleLoading = ref(false)
|
|
const appleAvailable = isAppleSignInAvailable()
|
|
const devLoading = ref(false)
|
|
const isDev = import.meta.env.DEV
|
|
|
|
onMounted(initGoogle)
|
|
|
|
async function initGoogle() {
|
|
try {
|
|
const { clientId } = await authApi.googleClientId()
|
|
if (!clientId) {
|
|
googleError.value = '구글 로그인이 아직 설정되지 않았습니다.'
|
|
return
|
|
}
|
|
await loadGsi()
|
|
const google = (window as unknown as { google: any }).google
|
|
google.accounts.id.initialize({
|
|
client_id: clientId,
|
|
callback: async (res: { credential: string }) => {
|
|
try {
|
|
await auth.loginGoogle(res.credential)
|
|
redirectAfterLogin()
|
|
} catch (e) {
|
|
notifyError(e)
|
|
}
|
|
},
|
|
})
|
|
if (googleBtn.value) {
|
|
google.accounts.id.renderButton(googleBtn.value, {
|
|
theme: 'outline',
|
|
size: 'large',
|
|
width: 260,
|
|
text: 'continue_with',
|
|
locale: 'ko',
|
|
})
|
|
}
|
|
} catch {
|
|
googleError.value = '구글 로그인 초기화에 실패했습니다. (서버 연결 확인)'
|
|
}
|
|
}
|
|
|
|
function loadGsi(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
if ((window as unknown as { google?: unknown }).google) return resolve()
|
|
const s = document.createElement('script')
|
|
s.src = 'https://accounts.google.com/gsi/client'
|
|
s.async = true
|
|
s.onload = () => resolve()
|
|
s.onerror = () => reject(new Error('GSI load failed'))
|
|
document.head.appendChild(s)
|
|
})
|
|
}
|
|
|
|
async function onApple() {
|
|
appleLoading.value = true
|
|
try {
|
|
const { identityToken, name } = await signInWithApple()
|
|
await auth.loginApple(identityToken, name)
|
|
redirectAfterLogin()
|
|
} catch (e) {
|
|
// 사용자가 시트를 취소한 경우는 조용히 무시.
|
|
if (isAppleCancel(e)) return
|
|
notifyError(e)
|
|
} finally {
|
|
appleLoading.value = false
|
|
}
|
|
}
|
|
|
|
/** 애플 로그인 시트 취소 판별 — 플러그인이 취소를 에러로 reject 한다. */
|
|
function isAppleCancel(e: unknown): boolean {
|
|
const msg = (e instanceof Error ? e.message : String(e)).toLowerCase()
|
|
return msg.includes('cancel') || msg.includes('1001') || msg.includes('popup_closed')
|
|
}
|
|
|
|
async function onDevLogin() {
|
|
devLoading.value = true
|
|
try {
|
|
const token = import.meta.env.VITE_DEV_LOGIN_TOKEN || 'dev-local-token'
|
|
await auth.devLogin(token)
|
|
redirectAfterLogin()
|
|
} catch (e) {
|
|
notifyError(e)
|
|
} finally {
|
|
devLoading.value = false
|
|
}
|
|
}
|
|
|
|
function redirectAfterLogin() {
|
|
const redirect = (route.query.redirect as string) || '/'
|
|
router.replace(redirect)
|
|
}
|
|
|
|
function notifyError(e: unknown) {
|
|
const message = e instanceof ApiError ? e.message : '로그인에 실패했습니다.'
|
|
$q.notify({ color: 'negative', message, icon: 'error', position: 'top' })
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.bg-brand {
|
|
background: linear-gradient(160deg, #6db3e8 0%, #3e92cc 100%);
|
|
min-height: 100vh;
|
|
}
|
|
.login-card {
|
|
width: 100%;
|
|
max-width: 320px;
|
|
border-radius: 16px;
|
|
}
|
|
</style>
|