feat: 애플 로그인 네이티브 연동 (Sign in with Apple)

@capacitor-community/apple-sign-in 으로 identityToken 획득 → /api/auth/apple.
애플 버튼은 네이티브 iOS 에서만 노출, 시트 취소는 무시.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-11 10:41:29 +09:00
parent acc21b4ac7
commit d4fd4a67e2
4 changed files with 84 additions and 10 deletions
+36
View File
@@ -0,0 +1,36 @@
// Sign in with Apple — 네이티브(iOS) 전용 래퍼.
// @capacitor-community/apple-sign-in 플러그인으로 identityToken(JWT)을 받아온다.
// 이 토큰의 aud 는 앱 번들 ID(=capacitor appId)이며, 백엔드가 APPLE_CLIENT_ID 로 검증한다.
import { Capacitor } from '@capacitor/core'
import { SignInWithApple } from '@capacitor-community/apple-sign-in'
// capacitor.config 의 appId 와 일치. iOS 네이티브 플로우의 aud 가 된다.
const APPLE_CLIENT_ID = 'kr.sblog.dognation'
export interface AppleCredential {
identityToken: string
/** 애플은 최초 인증 시에만 이름을 준다. 이후에는 null. */
name: string | null
}
/** 현재 실행 환경이 네이티브(iOS)인지 — 애플 로그인 지원 여부. */
export function isAppleSignInAvailable(): boolean {
return Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'ios'
}
/**
* 네이티브 Sign in with Apple 을 실행하고 identityToken 을 반환한다.
* 사용자가 취소하면 플러그인이 reject 하므로 호출부에서 구분 처리한다.
*/
export async function signInWithApple(): Promise<AppleCredential> {
const { response } = await SignInWithApple.authorize({
clientId: APPLE_CLIENT_ID,
// iOS 네이티브 플로우에서는 사용되지 않지만 타입상 필요.
redirectURI: `https://${APPLE_CLIENT_ID}/apple/callback`,
scopes: 'name email',
})
// familyName + givenName (한국식). 최초 인증에만 제공되며, 없으면 null.
const name =
[response.familyName, response.givenName].filter(Boolean).join(' ').trim() || null
return { identityToken: response.identityToken, name }
}