Compare commits
2 Commits
acc21b4ac7
...
768aba282d
| Author | SHA1 | Date | |
|---|---|---|---|
| 768aba282d | |||
| d4fd4a67e2 |
@@ -0,0 +1,26 @@
|
|||||||
|
# GitLab CI — dognation 프론트 (Vue3 + TS + Vite / Capacitor)
|
||||||
|
# dev 브랜치 푸시에서만 실행. build 스크립트가 vue-tsc 타입체크 + vite 빌드를 함께 수행.
|
||||||
|
image: node:22
|
||||||
|
|
||||||
|
variables:
|
||||||
|
npm_config_cache: "$CI_PROJECT_DIR/.npm"
|
||||||
|
|
||||||
|
cache:
|
||||||
|
key: "$CI_COMMIT_REF_SLUG"
|
||||||
|
paths:
|
||||||
|
- .npm/
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- build
|
||||||
|
|
||||||
|
build:
|
||||||
|
stage: build
|
||||||
|
rules:
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "dev"'
|
||||||
|
script:
|
||||||
|
- npm ci
|
||||||
|
- npm run build # vue-tsc --noEmit && vite build
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- dist/
|
||||||
|
expire_in: 1 week
|
||||||
Generated
+26
@@ -8,6 +8,7 @@
|
|||||||
"name": "dognation_pt",
|
"name": "dognation_pt",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor-community/apple-sign-in": "^7.1.0",
|
||||||
"@capacitor/app": "^8.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
"@capacitor/core": "^8.3.4",
|
"@capacitor/core": "^8.3.4",
|
||||||
"@quasar/extras": "^1.16.12",
|
"@quasar/extras": "^1.16.12",
|
||||||
@@ -83,6 +84,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||||
},
|
},
|
||||||
|
"node_modules/@capacitor-community/apple-sign-in": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@capacitor-community/apple-sign-in/-/apple-sign-in-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-df7cA7vfvvvilTtpJKeVvxO31W+py5t3HK9233GBD5MvgAgtGvCqeQ3pI0noNkVsKPF9B1g/po8Mph6s4bVSmw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/scriptjs": "0.0.2",
|
||||||
|
"scriptjs": "^2.5.9"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@capacitor/core": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@capacitor/android": {
|
"node_modules/@capacitor/android": {
|
||||||
"version": "8.4.1",
|
"version": "8.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.4.1.tgz",
|
||||||
@@ -1560,6 +1574,12 @@
|
|||||||
"undici-types": "~8.3.0"
|
"undici-types": "~8.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/scriptjs": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/scriptjs/-/scriptjs-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-GFrUgzGYfNX3VWZwMyzr0JrBwzLv5Kes4BQBvo6hXdRy14/GBbpCiVwwB7v1o2xIEvFf+9GyznmYhuesQQjSag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/slice-ansi": {
|
"node_modules/@types/slice-ansi": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",
|
||||||
@@ -3191,6 +3211,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/scriptjs": {
|
||||||
|
"version": "2.5.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/scriptjs/-/scriptjs-2.5.9.tgz",
|
||||||
|
"integrity": "sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.8.5",
|
"version": "7.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"typecheck": "vue-tsc --noEmit"
|
"typecheck": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor-community/apple-sign-in": "^7.1.0",
|
||||||
"@capacitor/app": "^8.0.0",
|
"@capacitor/app": "^8.0.0",
|
||||||
"@capacitor/core": "^8.3.4",
|
"@capacitor/core": "^8.3.4",
|
||||||
"@quasar/extras": "^1.16.12",
|
"@quasar/extras": "^1.16.12",
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
+20
-9
@@ -15,8 +15,9 @@
|
|||||||
{{ googleError }}
|
{{ googleError }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 애플 로그인 -->
|
<!-- 애플 로그인 (네이티브 iOS 전용) -->
|
||||||
<q-btn
|
<q-btn
|
||||||
|
v-if="appleAvailable"
|
||||||
class="full-width q-py-sm"
|
class="full-width q-py-sm"
|
||||||
color="black"
|
color="black"
|
||||||
text-color="white"
|
text-color="white"
|
||||||
@@ -56,6 +57,7 @@ import { useQuasar } from 'quasar'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
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'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -65,6 +67,7 @@ const auth = useAuthStore()
|
|||||||
const googleBtn = ref<HTMLElement | null>(null)
|
const googleBtn = ref<HTMLElement | null>(null)
|
||||||
const googleError = ref('')
|
const googleError = ref('')
|
||||||
const appleLoading = ref(false)
|
const appleLoading = ref(false)
|
||||||
|
const appleAvailable = isAppleSignInAvailable()
|
||||||
const devLoading = ref(false)
|
const devLoading = ref(false)
|
||||||
const isDev = import.meta.env.DEV
|
const isDev = import.meta.env.DEV
|
||||||
|
|
||||||
@@ -117,16 +120,24 @@ function loadGsi(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onApple() {
|
async function onApple() {
|
||||||
// 네이티브(iOS)에서는 Capacitor Sign in with Apple 플러그인으로 identityToken 을 받아
|
|
||||||
// auth.loginApple(identityToken, name) 을 호출하도록 연동 예정.
|
|
||||||
appleLoading.value = true
|
appleLoading.value = true
|
||||||
$q.notify({
|
try {
|
||||||
color: 'grey-8',
|
const { identityToken, name } = await signInWithApple()
|
||||||
message: '애플 로그인은 네이티브(iOS) 플러그인 연동이 필요합니다.',
|
await auth.loginApple(identityToken, name)
|
||||||
icon: 'mdi-apple',
|
redirectAfterLogin()
|
||||||
position: 'top',
|
} catch (e) {
|
||||||
})
|
// 사용자가 시트를 취소한 경우는 조용히 무시.
|
||||||
|
if (isAppleCancel(e)) return
|
||||||
|
notifyError(e)
|
||||||
|
} finally {
|
||||||
appleLoading.value = false
|
appleLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 애플 로그인 시트 취소 판별 — 플러그인이 취소를 에러로 reject 한다. */
|
||||||
|
function isAppleCancel(e: unknown): boolean {
|
||||||
|
const msg = (e instanceof Error ? e.message : String(e)).toLowerCase()
|
||||||
|
return msg.includes('cancel') || msg.includes('1001') || msg.includes('popup_closed')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDevLogin() {
|
async function onDevLogin() {
|
||||||
|
|||||||
Reference in New Issue
Block a user