feat(auth): iOS 애플 로그인 버튼 — Sign in with Apple(App Store 4.8 대응)

- @capacitor-community/apple-sign-in + LoginModal Apple 버튼(iOS 전용, HIG 검은 버튼)
- appleAuth 네이티브 래퍼 + authApi/auth store appleLogin
- SPM capacitor-swift-pm 8.x 로 패치(patch-package) — @capawesome 구글 플러그인과 iOS 공존
- App.entitlements(applesignin) 준비 — Xcode 'Sign in with Apple' Capability 추가 시 사용

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-04 17:01:43 +09:00
parent a7e5bf9c0f
commit 1cbf47ff87
9 changed files with 525 additions and 4 deletions
+68
View File
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
import { useUiStore } from '@/stores/ui'
import { authApi } from '@/api/authApi'
import { isNativeGoogle, nativeGoogleIdToken } from '@/native/googleAuth'
import { isNativeApple, nativeAppleCredential } from '@/native/appleAuth'
import { ID_LOGIN_ENABLED } from '@/config/features'
const auth = useAuthStore()
@@ -64,6 +65,33 @@ async function handleNativeGoogle() {
}
}
// ===== 애플 로그인 (iOS 전용) =====
const isApple = isNativeApple()
async function handleNativeApple() {
if (loading.value) return
loading.value = true
try {
const { identityToken, name } = await nativeAppleCredential()
loading.value = false
if (!identityToken) return
error.value = null
loading.value = true
await auth.appleLogin(identityToken, name, form.rememberMe)
const redirect = ui.redirectPath
ui.closeLogin()
router.push(redirect || '/')
} catch (e) {
// 사용자가 취소(USER_CANCELLED/1001 등)한 경우는 조용히 무시
const msg = `${e?.code || ''} ${e?.message || ''}`
if (!/cancel|1001/i.test(msg)) {
error.value = e.response?.data?.message || '애플 로그인에 실패했습니다.'
}
} finally {
loading.value = false
}
}
// GIS 스크립트 로드 (한 번만). 실패하면 reject.
function loadGisScript() {
if (window.google?.accounts?.id) return Promise.resolve()
@@ -208,6 +236,20 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
<div v-else ref="googleBtn" class="google-btn"></div>
</div>
<!-- 애플 로그인 iOS 전용(App Store 가이드라인 4.8 대응). 서버 설정 불필요 -->
<button
v-if="isApple"
type="button"
class="apple-native"
:disabled="loading"
@click="handleNativeApple"
>
<svg class="a-logo" viewBox="0 0 384 512" width="16" height="16" aria-hidden="true">
<path fill="currentColor" d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/>
</svg>
Apple로 계속하기
</button>
<button v-if="SOCIAL_LOGIN_ENABLED" type="button" class="naver" :disabled="loading" @click="handleNaverLogin">
네이버로 로그인 (준비 )
</button>
@@ -339,6 +381,32 @@ h2 {
.google-native .g-logo {
flex: none;
}
/* Apple 로그인 버튼 — HIG 권장(검은 배경 + 흰 로고/텍스트) */
.apple-native {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
max-width: 300px;
margin: 0.6rem auto 0;
padding: 0.6rem 1rem;
border: 1px solid #000;
border-radius: 4px;
background: #000;
color: #fff;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
}
.apple-native:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.apple-native .a-logo {
flex: none;
margin-top: -2px;
}
.divider {
display: flex;
align-items: center;