- 웹은 기존 GIS 유지, Capacitor 네이티브(APK)는 네이티브 구글 로그인 사용 (웹뷰에서 GIS 팝업/FedCM 미동작 대응) - 받은 ID 토큰은 기존과 동일하게 백엔드 /api/auth/google 로 검증 - src/lib/googleAuth.ts + LoginPage 네이티브/웹 분기 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+13
@@ -12,6 +12,7 @@
|
|||||||
"@capacitor/app": "^8.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
"@capacitor/core": "^8.3.4",
|
"@capacitor/core": "^8.3.4",
|
||||||
"@capacitor/geolocation": "^8.2.0",
|
"@capacitor/geolocation": "^8.2.0",
|
||||||
|
"@capgo/capacitor-social-login": "^8.3.36",
|
||||||
"@quasar/extras": "^1.16.12",
|
"@quasar/extras": "^1.16.12",
|
||||||
"@stomp/stompjs": "^7.3.0",
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"pinia": "^2.3.0",
|
"pinia": "^2.3.0",
|
||||||
@@ -187,6 +188,18 @@
|
|||||||
"integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
|
"integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/@capgo/capacitor-social-login": {
|
||||||
|
"version": "8.3.36",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capgo/capacitor-social-login/-/capacitor-social-login-8.3.36.tgz",
|
||||||
|
"integrity": "sha512-Y4HYjEJablzbHHwPBrbC2UvHVPsR0bAyCAqBzVBoUEOf0UFHAHyTP3gD9JUTKP/iERU8I+6IA2RWtLqDePCNuA==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@capacitor/app": "^8.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
"@capacitor/core": "^8.3.4",
|
"@capacitor/core": "^8.3.4",
|
||||||
"@capacitor/geolocation": "^8.2.0",
|
"@capacitor/geolocation": "^8.2.0",
|
||||||
|
"@capgo/capacitor-social-login": "^8.3.36",
|
||||||
"@quasar/extras": "^1.16.12",
|
"@quasar/extras": "^1.16.12",
|
||||||
"@stomp/stompjs": "^7.3.0",
|
"@stomp/stompjs": "^7.3.0",
|
||||||
"pinia": "^2.3.0",
|
"pinia": "^2.3.0",
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
import { SocialLogin } from '@capgo/capacitor-social-login'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 네이티브(Capacitor) 구글 로그인.
|
||||||
|
* 웹뷰에서는 GIS(팝업/FedCM)가 동작하지 않으므로, 앱에서는 이 네이티브 흐름을 사용한다.
|
||||||
|
* 받은 ID 토큰은 웹 GIS 와 동일하게 백엔드 /api/auth/google 로 검증한다.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let initializedFor = ''
|
||||||
|
|
||||||
|
/** 네이티브 플랫폼(iOS/Android) 여부 — 웹은 기존 GIS 사용. */
|
||||||
|
export function isNativeGoogleAvailable(): boolean {
|
||||||
|
return Capacitor.isNativePlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 네이티브 구글 로그인 → ID 토큰 반환.
|
||||||
|
* @param webClientId 백엔드가 aud 검증에 쓰는 웹 클라이언트 ID (serverClientId)
|
||||||
|
*/
|
||||||
|
export async function signInWithGoogle(webClientId: string): Promise<string> {
|
||||||
|
if (initializedFor !== webClientId) {
|
||||||
|
await SocialLogin.initialize({ google: { webClientId } })
|
||||||
|
initializedFor = webClientId
|
||||||
|
}
|
||||||
|
const res = await SocialLogin.login({
|
||||||
|
provider: 'google',
|
||||||
|
options: { scopes: ['email', 'profile'] },
|
||||||
|
})
|
||||||
|
const result = res.result as { idToken?: string | null }
|
||||||
|
if (!result?.idToken) {
|
||||||
|
throw new Error('구글 ID 토큰을 받지 못했습니다.')
|
||||||
|
}
|
||||||
|
return result.idToken
|
||||||
|
}
|
||||||
+36
-2
@@ -12,8 +12,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<q-card flat class="login-card q-pa-lg">
|
<q-card flat class="login-card q-pa-lg">
|
||||||
<!-- 구글 로그인 버튼(GIS 렌더링 영역) -->
|
<!-- 구글 로그인: 앱(네이티브)은 버튼, 웹은 GIS 렌더링 -->
|
||||||
<div ref="googleBtn" class="flex flex-center q-mb-md"></div>
|
<q-btn
|
||||||
|
v-if="isNative"
|
||||||
|
class="full-width q-py-sm q-mb-md"
|
||||||
|
color="white"
|
||||||
|
text-color="grey-9"
|
||||||
|
unelevated
|
||||||
|
icon="mdi-google"
|
||||||
|
label="Google로 계속하기"
|
||||||
|
:loading="googleLoading"
|
||||||
|
:disable="!googleClientId"
|
||||||
|
@click="onGoogleNative"
|
||||||
|
/>
|
||||||
|
<div v-else ref="googleBtn" class="flex flex-center q-mb-md"></div>
|
||||||
<div v-if="googleError" class="text-caption text-negative text-center q-mb-md">
|
<div v-if="googleError" class="text-caption text-negative text-center q-mb-md">
|
||||||
{{ googleError }}
|
{{ googleError }}
|
||||||
</div>
|
</div>
|
||||||
@@ -63,6 +75,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { authApi } from '@/api/auth'
|
import { authApi } from '@/api/auth'
|
||||||
import { ApiError } from '@/api/http'
|
import { ApiError } from '@/api/http'
|
||||||
import { isAppleSignInAvailable, signInWithApple } from '@/lib/appleAuth'
|
import { isAppleSignInAvailable, signInWithApple } from '@/lib/appleAuth'
|
||||||
|
import { isNativeGoogleAvailable, signInWithGoogle } from '@/lib/googleAuth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -71,6 +84,9 @@ const auth = useAuthStore()
|
|||||||
|
|
||||||
const googleBtn = ref<HTMLElement | null>(null)
|
const googleBtn = ref<HTMLElement | null>(null)
|
||||||
const googleError = ref('')
|
const googleError = ref('')
|
||||||
|
const googleClientId = ref('')
|
||||||
|
const googleLoading = ref(false)
|
||||||
|
const isNative = isNativeGoogleAvailable()
|
||||||
const appleLoading = ref(false)
|
const appleLoading = ref(false)
|
||||||
const appleAvailable = isAppleSignInAvailable()
|
const appleAvailable = isAppleSignInAvailable()
|
||||||
const devLoading = ref(false)
|
const devLoading = ref(false)
|
||||||
@@ -85,6 +101,9 @@ async function initGoogle() {
|
|||||||
googleError.value = '구글 로그인이 아직 설정되지 않았습니다.'
|
googleError.value = '구글 로그인이 아직 설정되지 않았습니다.'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
googleClientId.value = clientId
|
||||||
|
// 앱(네이티브)은 버튼 클릭 시 네이티브 로그인 → GIS 초기화 불필요
|
||||||
|
if (isNative) return
|
||||||
await loadGsi()
|
await loadGsi()
|
||||||
const google = (window as unknown as { google: any }).google
|
const google = (window as unknown as { google: any }).google
|
||||||
google.accounts.id.initialize({
|
google.accounts.id.initialize({
|
||||||
@@ -124,6 +143,21 @@ function loadGsi(): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onGoogleNative() {
|
||||||
|
if (!googleClientId.value) return
|
||||||
|
googleLoading.value = true
|
||||||
|
try {
|
||||||
|
const idToken = await signInWithGoogle(googleClientId.value)
|
||||||
|
await auth.loginGoogle(idToken)
|
||||||
|
redirectAfterLogin()
|
||||||
|
} catch (e) {
|
||||||
|
if (isAppleCancel(e)) return
|
||||||
|
notifyError(e)
|
||||||
|
} finally {
|
||||||
|
googleLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onApple() {
|
async function onApple() {
|
||||||
appleLoading.value = true
|
appleLoading.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user