Files
dognation_PT/src/pages/LoginPage.vue
T
sb 3bebe77cab
CI / build (push) Failing after 15m8s
chore: UI 문구 '우리 개' → '우리 강아지'
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:00:34 +09:00

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>