diff --git a/apps/native/App.tsx b/apps/native/App.tsx index 7294a42a..530bd271 100644 --- a/apps/native/App.tsx +++ b/apps/native/App.tsx @@ -10,7 +10,7 @@ import { colors } from '@theme/tokens'; import '@/app/providers/global.css'; import '@/app/providers/api'; -import { useLoadAssets, useDeepLinkHandler } from '@hooks'; +import { useLoadAssets, useDeepLinkHandler, useOTAUpdate } from '@hooks'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { Text, TextInput } from 'react-native'; import Toast from 'react-native-toast-message'; @@ -48,6 +48,7 @@ if ((TextInput as any).defaultProps == null) (TextInput as any).defaultProps = { export default function App() { const { isReady } = useLoadAssets(); + useOTAUpdate(); // FCM 푸시 알림 딥링크 핸들러 useDeepLinkHandler(); diff --git a/apps/native/app.config.ts b/apps/native/app.config.ts index 54e213aa..3fb197c9 100644 --- a/apps/native/app.config.ts +++ b/apps/native/app.config.ts @@ -75,8 +75,17 @@ const isDev = const config: ExpoConfig = { name: 'Pointer', slug: 'pointer', - version: '1.0.0', + version: '1.2.0', orientation: 'portrait', + runtimeVersion: { + policy: 'fingerprint', + }, + updates: { + url: 'https://u.expo.dev/76a68921-8c65-4e50-98b0-fb5ef457ab7e', + enabled: true, + fallbackToCacheTimeout: 0, + checkAutomatically: 'NEVER', + }, icon: './assets/images/icon.png', scheme: 'pointer', userInterfaceStyle: 'automatic', @@ -128,6 +137,7 @@ const config: ExpoConfig = { backgroundColor: '#F8F9FC', }, ], + 'expo-updates', [ '@react-native-google-signin/google-signin', { diff --git a/apps/native/eas.json b/apps/native/eas.json index c9bfc4a3..7c70967a 100644 --- a/apps/native/eas.json +++ b/apps/native/eas.json @@ -7,15 +7,18 @@ "development": { "developmentClient": true, "distribution": "internal", + "channel": "development", "env": { "EXPO_NO_CAPABILITY_SYNC": "1" } }, "preview": { - "distribution": "internal" + "distribution": "internal", + "channel": "preview" }, "production": { - "autoIncrement": true + "autoIncrement": true, + "channel": "production" } }, "submit": { diff --git a/apps/native/package.json b/apps/native/package.json index 5b26b5de..6a034a65 100644 --- a/apps/native/package.json +++ b/apps/native/package.json @@ -17,6 +17,7 @@ "test": "jest", "sync-webview-html": "pnpm --filter @repo/pointer-content-renderer build && mkdir -p ./assets/webview && cp ../../packages/pointer-content-renderer/dist/index.html ./assets/webview/content.html", "eas-build-post-install": "pnpm sync-webview-html", + "eas-update": "sh -c 'test -n \"$1\" || { echo \"Usage: pnpm eas-update [eas update args...]\"; exit 1; }; pnpm sync-webview-html && eas update --channel \"$1\" \"${@:2}\"' --", "openapi": "sh -c 'set -a; [ -f .env ] && . ./.env; set +a; test -n \"$OPENAPI_BASIC_AUTH_USERNAME\" && test -n \"$OPENAPI_BASIC_AUTH_PASSWORD\" && curl -fsS -u \"$OPENAPI_BASIC_AUTH_USERNAME:$OPENAPI_BASIC_AUTH_PASSWORD\" https://dev.api.math-pointer.com/v3/api-docs -o /tmp/native-openapi.json && pnpm dlx openapi-typescript /tmp/native-openapi.json --output ./src/types/api/schema.d.ts && prettier --write ./src/types/api/schema.d.ts'" }, "dependencies": { @@ -66,6 +67,7 @@ "expo-status-bar": "~3.0.8", "expo-symbols": "~1.0.7", "expo-system-ui": "~6.0.8", + "expo-updates": "^29.0.17", "expo-web-browser": "~15.0.9", "lottie-react-native": "^7.3.4", "lucide-react-native": "^0.554.0", diff --git a/apps/native/src/features/student/menu/screens/MenuScreen.tsx b/apps/native/src/features/student/menu/screens/MenuScreen.tsx index 01c8df3c..37b245c8 100644 --- a/apps/native/src/features/student/menu/screens/MenuScreen.tsx +++ b/apps/native/src/features/student/menu/screens/MenuScreen.tsx @@ -4,6 +4,7 @@ import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { type NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useQueryClient } from '@tanstack/react-query'; import { Bell, Headset, Megaphone, ThumbsUp, History } from 'lucide-react-native'; +import Constants from 'expo-constants'; import { TanstackQueryClient, useGetMe, useGetNoticeCount } from '@apis'; import { useAuthStore } from '@stores'; @@ -105,7 +106,9 @@ const MenuScreen = () => { /> - 1.0.1 최신 버전 + + {Constants.nativeAppVersion ?? Constants.expoConfig?.version ?? 'unknown'} + diff --git a/apps/native/src/hooks/index.ts b/apps/native/src/hooks/index.ts index 939008fe..287bcb26 100644 --- a/apps/native/src/hooks/index.ts +++ b/apps/native/src/hooks/index.ts @@ -4,4 +4,5 @@ export { default as useFcmToken } from './useFcmToken'; export { default as useInvalidateStudyData } from './useInvalidateStudyData'; export { default as useIsTablet } from './useIsTablet'; export { default as useLoadAssets } from './useLoadAssets'; +export { default as useOTAUpdate } from './useOTAUpdate'; export { default as useInvalidateAll } from './useInvalidateAll'; diff --git a/apps/native/src/hooks/useOTAUpdate.ts b/apps/native/src/hooks/useOTAUpdate.ts new file mode 100644 index 00000000..b5c7c08d --- /dev/null +++ b/apps/native/src/hooks/useOTAUpdate.ts @@ -0,0 +1,34 @@ +import { useEffect } from 'react'; +import * as Updates from 'expo-updates'; + +/** + * OTA 업데이트 체크 및 적용 훅. + * 앱 시작 시 백그라운드에서 업데이트를 확인하고, + * 새 업데이트가 있으면 다음 앱 실행 시 자동 적용. + */ +const useOTAUpdate = () => { + useEffect(() => { + // 개발 환경에서는 스킵 + if (__DEV__) return; + + const checkForUpdate = async () => { + try { + const update = await Updates.checkForUpdateAsync(); + + if (update.isAvailable) { + await Updates.fetchUpdateAsync(); + // 기본: 다음 앱 실행 시 적용 + // 즉시 적용이 필요하면 아래 주석 해제: + // await Updates.reloadAsync(); + } + } catch (error) { + // 업데이트 체크 실패는 무시 (네트워크 오류 등) + console.warn('OTA 업데이트 체크 실패:', error); + } + }; + + checkForUpdate(); + }, []); +}; + +export default useOTAUpdate; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6716aa45..c9b1b220 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -320,6 +320,9 @@ importers: expo-system-ui: specifier: ~6.0.8 version: 6.0.9(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)) + expo-updates: + specifier: ^29.0.17 + version: 29.0.17(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) expo-web-browser: specifier: ~15.0.9 version: 15.0.10(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)) @@ -2044,14 +2047,6 @@ packages: '@ide/backoff@1.0.0': resolution: {integrity: sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==} - '@isaacs/balanced-match@4.0.1': - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - - '@isaacs/brace-expansion@5.0.0': - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -3943,6 +3938,7 @@ packages: '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} @@ -4046,6 +4042,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@4.1.0: + resolution: {integrity: sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==} + arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -5231,6 +5230,9 @@ packages: peerDependencies: expo: '*' + expo-eas-client@1.0.8: + resolution: {integrity: sha512-5or11NJhSeDoHHI6zyvQDW2cz/yFyE+1Cz8NTs5NK8JzC7J0JrkUgptWtxyfB6Xs/21YRNifd3qgbBN3hfKVgA==} + expo-file-system@19.0.21: resolution: {integrity: sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==} peerDependencies: @@ -5297,6 +5299,11 @@ packages: peerDependencies: expo: '*' + expo-manifests@1.0.11: + resolution: {integrity: sha512-6zItytTewN37Cjhp3glUg0ozrgW2GwB8x9wtfzUNoJIMmxO38nnGdTLMaotYhRqdf5PP2Dzdmej1HDHXVNUpRw==} + peerDependencies: + expo: '*' + expo-modules-autolinking@3.0.24: resolution: {integrity: sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==} hasBin: true @@ -5368,6 +5375,9 @@ packages: react: '*' react-native: '*' + expo-structured-headers@5.0.0: + resolution: {integrity: sha512-RmrBtnSphk5REmZGV+lcdgdpxyzio5rJw8CXviHE6qH5pKQQ83fhMEcigvrkBdsn2Efw2EODp4Yxl1/fqMvOZw==} + expo-symbols@1.0.8: resolution: {integrity: sha512-7bNjK350PaQgxBf0owpmSYkdZIpdYYmaPttDBb2WIp6rIKtcEtdzdfmhsc2fTmjBURHYkg36+eCxBFXO25/1hw==} peerDependencies: @@ -5389,6 +5399,14 @@ packages: peerDependencies: expo: '*' + expo-updates@29.0.17: + resolution: {integrity: sha512-9h78cs6Q2rs/dEY7zAgyEm/m6J5rHy8RNpRyhilEAvrzrGLHChVZJT+bSR2RwNJg1DtwUNEjCgZrxDlM7LnNkg==} + hasBin: true + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + expo-web-browser@15.0.10: resolution: {integrity: sha512-fvDhW4bhmXAeWFNFiInmsGCK83PAqAcQaFyp/3pE/jbdKmFKoRCWr46uZGIfN4msLK/OODhaQ/+US7GSJNDHJg==} peerDependencies: @@ -6754,10 +6772,6 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -11090,12 +11104,6 @@ snapshots: '@ide/backoff@1.0.0': {} - '@isaacs/balanced-match@4.0.1': {} - - '@isaacs/brace-expansion@5.0.0': - dependencies: - '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -13564,6 +13572,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@4.1.0: {} + arg@5.0.2: {} argparse@1.0.10: @@ -15185,6 +15195,8 @@ snapshots: dependencies: expo: 54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-eas-client@1.0.8: {} + expo-file-system@19.0.21(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)): dependencies: expo: 54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -15249,6 +15261,14 @@ snapshots: transitivePeerDependencies: - supports-color + expo-manifests@1.0.11(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + dependencies: + '@expo/config': 12.0.13 + expo: 54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-json-utils: 0.15.0 + transitivePeerDependencies: + - supports-color + expo-modules-autolinking@3.0.24: dependencies: '@expo/spawn-async': 1.7.2 @@ -15340,6 +15360,8 @@ snapshots: react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-structured-headers@5.0.0: {} + expo-symbols@1.0.8(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)): dependencies: expo: 54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -15361,6 +15383,28 @@ snapshots: dependencies: expo: 54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-updates@29.0.17(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + '@expo/code-signing-certificates': 0.0.6 + '@expo/plist': 0.4.8 + '@expo/spawn-async': 1.7.2 + arg: 4.1.0 + chalk: 4.1.2 + debug: 4.4.3(supports-color@10.2.2) + expo: 54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-eas-client: 1.0.8 + expo-manifests: 1.0.11(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo-structured-headers: 5.0.0 + expo-updates-interface: 2.0.0(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + getenv: 2.0.0 + glob: 13.0.0 + ignore: 5.3.2 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0) + resolve-from: 5.0.0 + transitivePeerDependencies: + - supports-color + expo-web-browser@15.0.10(expo@54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)): dependencies: expo: 54.0.31(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.21)(react-native-webview@13.16.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -15662,7 +15706,7 @@ snapshots: glob@13.0.0: dependencies: - minimatch: 10.1.1 + minimatch: 10.2.4 minipass: 7.1.2 path-scurry: 2.0.1 @@ -17140,10 +17184,6 @@ snapshots: mimic-response@3.1.0: {} - minimatch@10.1.1: - dependencies: - '@isaacs/brace-expansion': 5.0.0 - minimatch@10.2.4: dependencies: brace-expansion: 5.0.4