feat(auth): 앱(네이티브) 구글 로그인 — @capgo/capacitor-social-login
CI / build (push) Failing after 12m48s

- 웹은 기존 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:
ByungCheol
2026-07-12 11:02:51 +09:00
parent a7b633a3e5
commit 7f18d18a11
4 changed files with 85 additions and 2 deletions
+13
View File
@@ -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",
+1
View File
@@ -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",
+35
View File
@@ -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
View File
@@ -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 {