diff --git a/package-lock.json b/package-lock.json index 00d8c4d..6853b37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@capacitor/app": "^8.0.0", "@capacitor/core": "^8.3.4", "@capacitor/geolocation": "^8.2.0", + "@capgo/capacitor-social-login": "^8.3.36", "@quasar/extras": "^1.16.12", "@stomp/stompjs": "^7.3.0", "pinia": "^2.3.0", @@ -187,6 +188,18 @@ "integrity": "sha512-/C1FUo8/OkKuAT4nCIu/34ny9siNHr9qtFezu4kxm6GY1wNFxrCFWjfYx5C1tUhVGz3fxBABegupkpjXvjCHrw==", "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": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", diff --git a/package.json b/package.json index b4f3dac..4109df8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@capacitor/app": "^8.0.0", "@capacitor/core": "^8.3.4", "@capacitor/geolocation": "^8.2.0", + "@capgo/capacitor-social-login": "^8.3.36", "@quasar/extras": "^1.16.12", "@stomp/stompjs": "^7.3.0", "pinia": "^2.3.0", diff --git a/src/lib/googleAuth.ts b/src/lib/googleAuth.ts new file mode 100644 index 0000000..bfdb00c --- /dev/null +++ b/src/lib/googleAuth.ts @@ -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 { + 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 +} diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue index 208e976..8f4e6d4 100644 --- a/src/pages/LoginPage.vue +++ b/src/pages/LoginPage.vue @@ -12,8 +12,20 @@ - -
+ + +
{{ googleError }}
@@ -63,6 +75,7 @@ import { useAuthStore } from '@/stores/auth' import { authApi } from '@/api/auth' import { ApiError } from '@/api/http' import { isAppleSignInAvailable, signInWithApple } from '@/lib/appleAuth' +import { isNativeGoogleAvailable, signInWithGoogle } from '@/lib/googleAuth' const router = useRouter() const route = useRoute() @@ -71,6 +84,9 @@ const auth = useAuthStore() const googleBtn = ref(null) const googleError = ref('') +const googleClientId = ref('') +const googleLoading = ref(false) +const isNative = isNativeGoogleAvailable() const appleLoading = ref(false) const appleAvailable = isAppleSignInAvailable() const devLoading = ref(false) @@ -85,6 +101,9 @@ async function initGoogle() { googleError.value = '구글 로그인이 아직 설정되지 않았습니다.' return } + googleClientId.value = clientId + // 앱(네이티브)은 버튼 클릭 시 네이티브 로그인 → GIS 초기화 불필요 + if (isNative) return await loadGsi() const google = (window as unknown as { google: any }).google google.accounts.id.initialize({ @@ -124,6 +143,21 @@ function loadGsi(): Promise { }) } +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() { appleLoading.value = true try {