diff --git a/Time-Up/App.tsx b/Time-Up/App.tsx index 08f1418..8fdab4f 100644 --- a/Time-Up/App.tsx +++ b/Time-Up/App.tsx @@ -3,7 +3,7 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { Platform, useWindowDimensions } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { Provider as PaperProvider } from 'react-native-paper'; @@ -27,6 +27,7 @@ import SelectWakeupAlarmVibratePage from './src/pages/Alarm/WakeUp/SelectWakeupA import WakeUpAlarmDetailPage from './src/pages/Alarm/WakeUp/WakeUpAlarmDetailPage'; import WakeUpAlarmPage from './src/pages/Alarm/WakeUp/WakeUpAlarmPage'; import CalendarPage from './src/pages/CalendarPage'; +import DiaryDetailPage from './src/pages/Diary/DiaryDetailPage'; import DiaryPage from './src/pages/Diary/DiaryPage'; import DiaryWritePage from './src/pages/Diary/DiaryWritePage'; import EditAlarmPage from './src/pages/Mypage/EditAlarmPage'; @@ -41,9 +42,10 @@ import SetLocationPage from './src/pages/SetLocationPage'; import SetRemindAlarmPage from './src/pages/SetPage/SetRemindAlarmPage'; import SetScheduleRepeatPage from './src/pages/SetScheduleRepeatPage'; import ViewScheduleDetailPage from './src/pages/ViewScheduleDetailPage'; -import DiaryDetailPage from './src/pages/Diary/DiaryDetailPage'; +import { saveFCMPushToken } from './src/apis/pushToken'; import { getAccessToken } from './src/utils/storage'; +import { requestWebPushToken } from './src/utils/webPush'; const queryClient = new QueryClient(); @@ -52,6 +54,10 @@ export default function App() { const { height: screenHeight, width: screenWidth } = useWindowDimensions(); const [mapsLoaded, setMapsLoaded] = useState(Platform.OS !== 'web'); const [initialRoute, setInitialRoute] = useState(null); + //const { registerForPushNotificationsAsync } = NotificationContext(); + const webPushInitRef = useRef(false); + const nativePushInitRef = useRef(false); + useEffect(() => { if (Platform.OS === 'web' && !window.google) { const script = document.createElement('script'); @@ -70,6 +76,74 @@ export default function App() { const token = getAccessToken(); setInitialRoute(token ? 'CalendarPage' : 'OnboardingPage'); }, []); + +// // 앱 토큰 발급 +// useEffect(() => { +// if (Platform.OS === 'web') return; +// if (nativePushInitRef.current) return; +// nativePushInitRef.current = true; + +// let cancelled = false; + +// (async () => { +// try { +// // 기기 권한 요청 + Expo push token 발급 +// const token = await registerForPushNotificationsAsync(); +// if (!token || cancelled) return; + +// // 로그인 토큰 없으면 서버 저장 skip (선택) +// const access = await getAccessToken(); +// if (!access) { +// console.warn('로그인 토큰 없음: push-token 저장 생략'); +// return; +// } + +// // 서버 저장 +// // const res = await saveExpoPushToken(token); +// // if ((res as any)?.result === 'Error') { +// // console.warn('push-token 저장 실패:', (res as any)?.error); +// // } else { +// // console.log('push-token 저장 성공'); +// // } +// } catch (e) { +// console.warn('모바일 푸시 토큰 발급/저장 실패:', e); +// } +// })(); + +// return () => { cancelled = true; }; +// }, []); + + useEffect(() => { + if (Platform.OS !== 'web') return; + if (webPushInitRef.current) return; + webPushInitRef.current = true; + let cancelled = false; + + (async () => { + try { + const token = await requestWebPushToken(); + if (!token || cancelled) { + console.warn('웹 푸시 권한 거부 또는 토큰 없음'); + return; + } + console.log('Web FCM Token:', token); + + const res = await saveFCMPushToken(token); + if ((res as any)?.result === 'Error') { + console.warn('웹 push-token 저장 실패:', (res as any)?.error); + } else { + console.log('웹 push-token 저장 성공'); + } + } catch (e) { + console.warn('웹 푸시 토큰 발급/저장 실패:', e); + } + })(); + + return () => { + cancelled = true; + }; + }, []); + if (!mapsLoaded || !initialRoute) return null; const content = ( diff --git a/Time-Up/app.json b/Time-Up/app.json index 51158cf..be7d6b7 100644 --- a/Time-Up/app.json +++ b/Time-Up/app.json @@ -17,9 +17,11 @@ "foregroundImage": "./assets/icon.png", "backgroundColor": "#ffffff" }, + "googleServicesFile": "./google-services.json", "edgeToEdgeEnabled": true, "package": "com.anit7215.TimeUp", - "useNextNotificationsApi": true + "useNextNotificationsApi": true, + "permissions": ["POST_NOTIFICATIONS"] }, "web": { "bundler": "metro", diff --git a/Time-Up/package-lock.json b/Time-Up/package-lock.json index 7b7bd11..b683c5f 100644 --- a/Time-Up/package-lock.json +++ b/Time-Up/package-lock.json @@ -11,6 +11,8 @@ "@expo/vector-icons": "^14.1.0", "@gorhom/bottom-sheet": "^5.1.6", "@react-native-async-storage/async-storage": "2.1.2", + "@react-native-firebase/app": "^23.1.0", + "@react-native-firebase/messaging": "^23.1.0", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.14", @@ -38,6 +40,7 @@ "expo-symbols": "~0.4.5", "expo-system-ui": "~5.0.10", "expo-web-browser": "~14.2.0", + "firebase": "^12.1.0", "lodash": "^4.17.21", "moment-timezone": "^0.6.0", "nativewind": "^4.1.23", @@ -3725,6 +3728,628 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@firebase/ai": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.1.0.tgz", + "integrity": "sha512-4HvFr4YIzNFh0MowJLahOjJDezYSTjQar0XYVu/sAycoxQ+kBsfXuTPRLVXCYfMR5oNwQgYe4Q2gAOYKKqsOyA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.18", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.18.tgz", + "integrity": "sha512-iN7IgLvM06iFk8BeFoWqvVpRFW3Z70f+Qe2PfCJ7vPIgLPjHXDE774DhCT5Y2/ZU/ZbXPDPD60x/XPWEoZLNdg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.24.tgz", + "integrity": "sha512-jE+kJnPG86XSqGQGhXXYt1tpTbCTED8OQJ/PQ90SEw14CuxRxx/H+lFbWA1rlFtFSsTCptAJtgyRBwr/f00vsw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.18", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.1.tgz", + "integrity": "sha512-jxTrDbxnGoX7cGz7aP9E7v9iKvBbQfZ8Gz4TH3SfrrkcyIojJM3+hJnlbGnGxHrABts844AxRcg00arMZEyA6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.1.tgz", + "integrity": "sha512-BEy1L6Ufd85ZSP79HDIv0//T9p7d5Bepwy+2mKYkgdXBGKTbFm2e2KxyF1nq4zSQ6RRBxWi0IY0zFVmoBTZlUA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.1", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.0.tgz", + "integrity": "sha512-J0lGSxXlG/lYVi45wbpPhcWiWUMXevY4fvLZsN1GHh+po7TZVng+figdHBVhFheaiipU8HZyc7ljw1jNojM2nw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.0", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", + "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat/node_modules/@react-native-async-storage/async-storage": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", + "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.60 <1.0" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.0.tgz", + "integrity": "sha512-5zl0+/h1GvlCSLt06RMwqFsd7uqRtnNZt4sW99k2rKRd6k/ECObIWlEnvthm2cuOSnUmwZknFqtmd1qyYSLUuQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.4", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.0.tgz", + "integrity": "sha512-4O7v4VFeSEwAZtLjsaj33YrMHMRjplOIYC2CiYsF6o/MboOhrhe01VrTt8iY9Y5EwjRHuRz4pS6jMBT8LfQYJA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.0.tgz", + "integrity": "sha512-2/LH5xIbD8aaLOWSFHAwwAybgSzHIM0dB5oVOL0zZnxFG1LctX2bc1NIAaPk1T+Zo9aVkLKUlB5fTXTkVUQprQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.0.tgz", + "integrity": "sha512-VPgtvoGFywWbQqtvgJnVWIDFSHV1WE6Hmyi5EGI+P+56EskiGkmnw6lEqc/MEUfGpPGdvmc4I9XMU81uj766/g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.0", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", + "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", + "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.6.tgz", + "integrity": "sha512-Yelp5xd8hM4NO1G1SuWrIk4h5K42mNwC98eWZ9YLVu6Z0S6hFk1mxotAdCRmH2luH8FASlYgLLq6OQLZ4nbnCA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.19.tgz", + "integrity": "sha512-y7PZAb0l5+5oIgLJr88TNSelxuASGlXyAKj+3pUc4fDuRIdPNBoONMHaIUa9rlffBR5dErmaD2wUBJ7Z1a513Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.6.6", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.4.tgz", + "integrity": "sha512-6m8+P+dE/RPl4OPzjTxcTbQ0rGeRyeTvAi9KwIffBVCiAMKrfXfLZaqD1F+m8t4B5/Q5aHsMozOgirkH1F5oMQ==", + "license": "Apache-2.0" + }, "node_modules/@gorhom/bottom-sheet": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.1.8.tgz", @@ -3764,6 +4389,37 @@ "react-native": "*" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -4502,6 +5158,70 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@react-native-async-storage/async-storage": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz", @@ -4514,6 +5234,40 @@ "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, + "node_modules/@react-native-firebase/app": { + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-23.1.0.tgz", + "integrity": "sha512-y3cW4HNFFO6KOCka2ihRWOTaGSsmTH/jqODzIZBcreN4vsmhnjrEFDRsGIkT7LCka26zwUGFZ3cvADQ2UkVEfw==", + "license": "Apache-2.0", + "dependencies": { + "firebase": "12.1.0" + }, + "peerDependencies": { + "expo": ">=47.0.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@react-native-firebase/messaging": { + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/@react-native-firebase/messaging/-/messaging-23.1.0.tgz", + "integrity": "sha512-MfTC9gIzRNub11GojezdCH5KsCMbfY+uKdmGyim9/8Ng+nP8lGOdVNlSycycCss/zyyV6TTfsAUWoeLo9jkZyA==", + "license": "Apache-2.0", + "peerDependencies": { + "@react-native-firebase/app": "23.1.0", + "expo": ">=47.0.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, "node_modules/@react-native-masked-view/masked-view": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.2.9.tgz", @@ -10662,6 +11416,18 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -10859,6 +11625,80 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.1.0.tgz", + "integrity": "sha512-oZucxvfWKuAW4eHHRqGKzC43fLiPqPwHYBHPRNsnkgonqYaq0VurYgqgBosRlEulW+TWja/5Tpo2FpUU+QrfEQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.1.0", + "@firebase/analytics": "0.10.18", + "@firebase/analytics-compat": "0.2.24", + "@firebase/app": "0.14.1", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.1", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.0", + "@firebase/auth-compat": "0.6.0", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.0", + "@firebase/firestore-compat": "0.4.0", + "@firebase/functions": "0.13.0", + "@firebase/functions-compat": "0.4.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.6.6", + "@firebase/remote-config-compat": "0.2.19", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, + "node_modules/firebase/node_modules/@firebase/auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.0.tgz", + "integrity": "sha512-5j7+ua93X+IRcJ1oMDTClTo85l7Xe40WSkoJ+shzPrX7OISlVWLdE1mKC57PSD+/LfAbdhJmvKixINBw2ESK6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/firebase/node_modules/@react-native-async-storage/async-storage": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.24.0.tgz", + "integrity": "sha512-W4/vbwUOYOjco0x3toB8QCr7EjIP6nE9G7o8PMguvvjYT5Awg09lyV4enACRx4s++PPulBiBSjL0KTFx2u0Z/g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.60 <1.0" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -11545,6 +12385,12 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -11573,6 +12419,12 @@ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -13108,6 +13960,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -13222,6 +14080,12 @@ "node": ">=4" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -15172,6 +16036,30 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -18386,6 +19274,12 @@ "defaults": "^1.0.3" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", @@ -18395,6 +19289,29 @@ "node": ">=8" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-fetch": { "version": "3.6.20", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", diff --git a/Time-Up/package.json b/Time-Up/package.json index 807deb7..ad5c3a5 100644 --- a/Time-Up/package.json +++ b/Time-Up/package.json @@ -8,12 +8,15 @@ "android": "expo run:android", "ios": "expo run:ios", "web": "expo start --web", - "lint": "expo lint" + "lint": "expo lint", + "build:web": "expo export -p web" }, "dependencies": { "@expo/vector-icons": "^14.1.0", "@gorhom/bottom-sheet": "^5.1.6", "@react-native-async-storage/async-storage": "2.1.2", + "@react-native-firebase/app": "^23.1.0", + "@react-native-firebase/messaging": "^23.1.0", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.14", @@ -28,6 +31,7 @@ "expo-auth-session": "^6.2.1", "expo-blur": "~14.1.5", "expo-constants": "~17.1.7", + "expo-dev-client": "~5.2.4", "expo-device": "~7.1.4", "expo-font": "~13.3.2", "expo-haptics": "~14.1.4", @@ -40,6 +44,7 @@ "expo-symbols": "~0.4.5", "expo-system-ui": "~5.0.10", "expo-web-browser": "~14.2.0", + "firebase": "^12.1.0", "lodash": "^4.17.21", "moment-timezone": "^0.6.0", "nativewind": "^4.1.23", @@ -67,8 +72,7 @@ "react-native-vector-icons": "^10.3.0", "react-native-web": "~0.20.0", "react-native-webview": "13.13.5", - "zustand": "^5.0.7", - "expo-dev-client": "~5.2.4" + "zustand": "^5.0.7" }, "devDependencies": { "@babel/core": "^7.27.7", diff --git a/Time-Up/public/firebase-messaging-sw.js b/Time-Up/public/firebase-messaging-sw.js new file mode 100644 index 0000000..d1101cf --- /dev/null +++ b/Time-Up/public/firebase-messaging-sw.js @@ -0,0 +1,34 @@ +// public/firebase-messaging-sw.js +importScripts('https://www.gstatic.com/firebasejs/9.23.0/firebase-app-compat.js'); +importScripts('https://www.gstatic.com/firebasejs/9.23.0/firebase-messaging-compat.js'); + +firebase.initializeApp({ + apiKey: "AIzaSyAEAKbupll7pRrAuhCtI0SbHRAx-rXj7oM", + authDomain: "timeup-80358.firebaseapp.com", + projectId: "timeup-80358", + messagingSenderId: "482152461783", + appId: "1:482152461783:web:8ee854bcf8cad1ab3bc792", +}); + +const messaging = firebase.messaging(); + +// 앱이 닫혀있거나 백그라운드일 때 도착하는 푸시 처리 +messaging.onBackgroundMessage((payload) => { + // 서버가 notification을 보낼 수도, data만 보낼 수도 있으니 둘 다 대비 + const title = + payload?.notification?.title || + payload?.data?.title || + 'Time-Up'; + + const body = + payload?.notification?.body || + payload?.data?.body || + '알림이 도착했습니다.'; + + // 필요하면 아이콘/액션 추가 + self.registration.showNotification(title, { + body, + // icon: '/icon-192.png', + // data: payload?.data || {}, + }); +}); \ No newline at end of file diff --git a/Time-Up/public/index.html b/Time-Up/public/index.html new file mode 100644 index 0000000..a0639d2 --- /dev/null +++ b/Time-Up/public/index.html @@ -0,0 +1,47 @@ + + + + + + + + %WEB_TITLE% + + + + + + +
+ + + + + \ No newline at end of file diff --git a/Time-Up/src/apis/alarmApi.ts b/Time-Up/src/apis/alarmApi.ts index 67b8fef..f692ff2 100644 --- a/Time-Up/src/apis/alarmApi.ts +++ b/Time-Up/src/apis/alarmApi.ts @@ -8,7 +8,9 @@ import { PostMyAlarmResponse, PutMyAlarmRequest, PutMyAlarmResponse, + ToggleAutoAlarmActivationResponse, ToggleMyAlarmActivationResponse, + ToggleWakeupAlarmActivationResponse, UpdateWakeupAlarmRequest, WakeupAlarmSummary } from '@/src/types/alarm'; @@ -45,7 +47,7 @@ export const putMyAlarm = async ( }; // 내 알람 활성/비활성화 -export const toggleMyAlarmActivation = async ( +export const patchToggleMyAlarmActivation = async ( alarmId: number, ): Promise => { const token = await getAccessToken(); @@ -61,147 +63,76 @@ export const toggleMyAlarmActivation = async ( return res.data; }; -// 내 알람 삭제 응답 -export const deleteMyAlarm = async ( - alarmId: number -): Promise => { +// 기상알람 활성/비활성화 요청 +export const patchToggleWakeupAlarmActive = async ( + wakeupAlarmId: number +): Promise => { const token = await getAccessToken(); - const res = await axiosInstance.delete( - `/alarm/${alarmId}/my-delete`, + const res = await axiosInstance.patch( + `/alarm/${wakeupAlarmId}/wakeup-active`, + null, { headers: { Authorization: `Bearer ${token}`, }, - params: { - alarm_id: alarmId, - }, } ); return res.data; }; -// 기상알람 수정 요청 -// src/apis/alarmApi.ts -export const putWakeupAlarm = async (wakeupAlarmId: number, body: UpdateWakeupAlarmRequest) => { +// 자동알람 활성/비활성화 요청 +export const patchtoggleAutoAlarmActivation = async ( + autoAlarmId: number, +): Promise => { const token = await getAccessToken(); - const res = await axiosInstance.put( - `/alarm/${wakeupAlarmId}/wakeup`, - body, + const res = await axiosInstance.patch( + `/alarm/${autoAlarmId}/auto-active`, + null, { headers: { Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', }, } ); return res.data; }; - -// 기상알람 활성/비활성화 응답 -export interface ToggleWakeupActiveResponse { - result: 'Success' | 'Fail'; - status: number; - success: ( - // 간단 응답 형태 - { is_active: boolean; message?: string } - // 혹은 상세 객체 형태(명세 예시) - | { - wakeup_alarm_id: number; - user_id: number; - day: number; - wakeup_time: string; - is_active: boolean; - is_repeating: boolean; - is_vibrating: boolean; - is_sound: boolean; - repeat_interval: number | null; - repeat_count: number | null; - sound_id: number | null; - vibration_type: string; - memo: string | null; - created_at: string | null; - updated_at: string | null; - } - ) | null; - error: { errorCode: string; message: string } | null; -} - -// 기상알람 활성/비활성화 요청 -export const patchToggleWakeupAlarmActive = async ( - wakeupAlarmId: number -): Promise => { +// 내 알람 삭제 응답 +export const deleteMyAlarm = async ( + alarmId: number +): Promise => { const token = await getAccessToken(); - - const res = await axiosInstance.patch( - `/alarm/${wakeupAlarmId}/wakeup-active`, - null, + const res = await axiosInstance.delete( + `/alarm/${alarmId}/my-delete`, { headers: { Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - Accept: 'application/json', }, params: { - wakeup_alarm_id: wakeupAlarmId, + alarm_id: alarmId, }, } ); - return res.data; }; -// 자동알람 활성/비활성화 -export interface ToggleAutoAlarmResponse { - result: 'Success' | 'Fail'; - status: number; - success: null | { - auto_alarm_id: number; - schedule_id: number; - wakeup_time: string; // ISO - is_active: boolean; - is_repeating: boolean; - repeat_interval: number; - repeat_count: number; - created_at: string; - is_sound: boolean; - is_vibrating: boolean; - sound_id: number; - vibration_type: string; - }; - error: null | { - errorCode: string; - message: string; - }; -} - -export async function toggleAutoAlarmActivation(autoAlarmId: number) { +// 기상알람 수정 요청 +export const putWakeupAlarm = async (wakeupAlarmId: number, body: UpdateWakeupAlarmRequest) => { const token = await getAccessToken(); - - // Query 에 alarm_id 가 필요하다고 해서 동일 값으로 전달함. - const res = await axiosInstance.patch( - `/alarm/${autoAlarmId}/auto-active`, - null, + const res = await axiosInstance.put( + `/alarm/${wakeupAlarmId}/wakeup`, + body, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, - params: { - alarm_id: autoAlarmId, - }, } ); + return res.data; +}; - if (res.data.result !== 'Success' || !res.data.success) { - const msg = res.data.error?.message ?? '자동 알람 토글 실패'; - throw new Error(msg); - } - - return res.data.success; // { is_active, wakeup_time, ... } -} - -// 알람 조회. 내 알람(myAlams), 기상알람(wakeup) +// 알람 조회. 내 알람(myAlams), 기상알람(wakeup), 자동알람(autoAlarms) export const getMyAlarms = async (): Promise<{ myAlarms: MyAlarmSummary[]; wakeupAlarms: WakeupAlarmSummary[]; @@ -219,53 +150,4 @@ export const getMyAlarms = async (): Promise<{ } throw new Error(res.data.error?.message ?? '알람 목록을 불러오지 못했습니다.'); -}; - -// 푸쉬 알람 -// 임의 전달 값들 -export interface PostWakeupAlarmPushRequest { - repeat_count: number; - repeat_interval: number; - push_message: string; -} - -export interface PostWakeupAlarmPushSuccess { - wakeup_alarm_id: number; - total_push_sent: number; - scheduled_push_times: string[]; - message: string; -} - -export interface ApiResponse { - result: 'Success' | 'Fail'; - status: number; - success: T | null; - error: { - errorCode: string; - message: string; - } | null; -} - -// POST: 기상알람 푸시 알람 API -export const postWakeupAlarmPush = async ( - wakeupAlarmId: number, - body: PostWakeupAlarmPushRequest -): Promise> => { - const token = await getAccessToken(); - - const res = await axiosInstance.post>( - `/alarm/{wakeup_alarm_id}/push`, - body, - { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - params: { - wakeup_alarm_id: wakeupAlarmId, - }, - } - ); - - return res.data; }; \ No newline at end of file diff --git a/Time-Up/src/apis/pushAlarm.ts b/Time-Up/src/apis/pushAlarm.ts new file mode 100644 index 0000000..f47cb71 --- /dev/null +++ b/Time-Up/src/apis/pushAlarm.ts @@ -0,0 +1 @@ +// src/apis/pushAlarm.ts diff --git a/Time-Up/src/apis/pushToken.ts b/Time-Up/src/apis/pushToken.ts index dda01fc..4ce31b6 100644 --- a/Time-Up/src/apis/pushToken.ts +++ b/Time-Up/src/apis/pushToken.ts @@ -1,20 +1,25 @@ // src/apis/pushToken.ts -// import { axiosInstance } from '@/src/apis/axiosInstance'; -// import { getAccessToken } from '@/src/utils/storage'; +import { axiosInstance } from '@/src/apis/axiosInstance'; +import { getAccessToken } from '@/src/utils/storage'; +import { SavePushTokenRequest, SavePushTokenResponse } from '../types/pushToken'; -// export async function saveExpoPushToken(token: string) { -// const accessToken = await getAccessToken(); -// // 백엔드 스펙에 맞게 URL/바디 키 이름을 바꿔주세요. -// // 기상, 자동, 내, 일정, 피드백 알람 별 분기 처리? -// // POST /alarm/{wakeup_alarm_id}/push { expo_push_token: string } -// // POST /alarm/{auto_alarm_id}/push -// // POST /alarm/{alarm_id}/push -// // POST /alarm/{schedule_id}/push -// // POST /alarm/{alarm_id}/feedback/push -// const res = await axiosInstance.post( -// '/alarm/{wakeup_alarm_id}/push', -// { expo_push_token: token }, -// { headers: { Authorization: `Bearer ${accessToken}` } } -// ); -// return res.data; -// } +export async function saveFCMPushToken(token: string): Promise { + if (!token) { + throw new Error('Push token is empty.'); + } + + const accessToken = await getAccessToken(); + + const res = await axiosInstance.post( + '/alarm/push-token', + { token } as SavePushTokenRequest, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + } + ); + + return res.data; +} \ No newline at end of file diff --git a/Time-Up/src/contexts/AlarmContext.tsx b/Time-Up/src/contexts/AlarmContext.tsx index 9f5c9a9..a5e0b72 100644 --- a/Time-Up/src/contexts/AlarmContext.tsx +++ b/Time-Up/src/contexts/AlarmContext.tsx @@ -1,5 +1,5 @@ // src/contexts/AlarmContext.tsx -import { deleteMyAlarm, patchToggleWakeupAlarmActive, toggleAutoAlarmActivation, toggleMyAlarmActivation } from '@/src/apis/alarmApi'; +import { deleteMyAlarm, patchToggleMyAlarmActivation, patchToggleWakeupAlarmActive, patchtoggleAutoAlarmActivation } from '@/src/apis/alarmApi'; import { transformAlarmResponseToItem, transformWakeupSummaryToAlarmItem } from '@/src/utils/alarmTransform'; import moment from 'moment'; import React, { createContext, useContext, useState } from 'react'; @@ -28,7 +28,6 @@ interface AlarmContextProps { autoAlarms: AutoAlarmSummary[]; setAutoAlarms: React.Dispatch>; - toggleAutoAlarmActiveById: (autoAlarmId: number) => Promise; selectedAlarmId: number | null; setSelectedAlarmId: (id: number | null) => void; @@ -43,9 +42,11 @@ interface AlarmContextProps { value: AlarmItem[K] ) => void; - toggleAlarmActivation: (alarmId: number) => Promise; + toggleMyAlarmActivation: (myalarmId: number) => Promise; + toggleWakeupAlarmActivation: (wakeupAlarmId: number) => Promise; + toggleAutoAlarmActivation: (autoAlarmId: number) => Promise; + deleteAlarmById: (alarmId: number) => Promise; - toggleWakeupAlarmActiveById: (wakeupAlarmId: number) => Promise; isLoadingAlarms: boolean; refreshAlarms: () => Promise; @@ -88,7 +89,7 @@ export const AlarmProvider: React.FC<{ children: React.ReactNode }> = ({ childre const mappedMy = myList.map((a, idx) => { const serverId = (a as any).alarm_id ?? (a as any).my_alarm_id; - const tsId = new Date(a.my_alarm_time).getTime() + idx; // 최후의 수단임.. 이거 쓰면 안됨.. 에러 방지용..^^;; + const tsId = new Date(a.my_alarm_time).getTime() + idx; // 최후의 수단.. 이거 쓰면 안됨.. 에러 방지용..;; // 화면 id는 서버ID가 있으면 서버ID로 고정 const uiId = serverId ?? tsId; const item = transformAlarmResponseToItem(a); @@ -97,13 +98,13 @@ export const AlarmProvider: React.FC<{ children: React.ReactNode }> = ({ childre const mappedWake = wakeList.map((w, idx) => { const serverId = (w as any).wakeup_alarm_id; const dayFromApi = typeof (w as any).day === 'number' ? (w as any).day : new Date(w.wakeup_time).getDay(); - const dayId = ((dayFromApi % 7) + 7) % 7; // 안전한 0~6 + const dayId = ((dayFromApi % 7) + 7) % 7; const item = transformWakeupSummaryToAlarmItem(w); return { ...item, - id: dayId, // 화면용 id = 요일(0~6) - serverId: (w as any).wakeup_alarm_id, // 서버ID 따로 보관(삭제/수정용) + id: dayId, + serverId: (w as any).wakeup_alarm_id, }; }); @@ -123,7 +124,6 @@ export const AlarmProvider: React.FC<{ children: React.ReactNode }> = ({ childre // }, []); - // 아래 서버 id로 바꾸기? const updateAlarmField = (alarmId: number, field: K, value: AlarmItem[K]) => { setMyAlarms((prev) => prev.map((a) => (a.id === alarmId ? { ...a, [field]: value } : a))); }; @@ -138,68 +138,74 @@ export const AlarmProvider: React.FC<{ children: React.ReactNode }> = ({ childre ); }; - const toggleAlarmActivation = async (alarmId: number) => { + const toggleMyAlarmActivation = async (myalarmId: number) => { try { - await toggleMyAlarmActivation(alarmId); - setMyAlarms((prev) => prev.map((a) => (a.id === alarmId ? { ...a, isActive: !a.isActive } : a))); - // 서버 상태와 불일치 우려가 있으면 다음 줄을 사용: - // await refreshAlarms(); - console.log(`알람 ${alarmId}의 상태를 토글했습니다.`); + await patchToggleMyAlarmActivation(myalarmId); + setMyAlarms((prev) => prev.map((a) => (a.id === myalarmId ? { ...a, isActive: !a.isActive } : a))); + console.log(`알람 ${myalarmId}의 상태를 토글했습니다.`); } catch (error) { - console.error(`알람 ${alarmId} 토글 실패:`, error); + console.error(`알람 ${myalarmId} 토글 실패:`, error); } }; - const toggleWakeupAlarmActiveById = async (wakeupAlarmId: number) => { + const toggleWakeupAlarmActivation = async (wakeupAlarmId: number) => { try { - await patchToggleWakeupAlarmActive(wakeupAlarmId); // 서버 토글 - setWakeupAlarms(prev => - prev.map(a => (a.serverId === wakeupAlarmId ? { ...a, isActive: !a.isActive } : a)) - ); // 로컬 반영 - console.log(`기상 알람 ${wakeupAlarmId} 상태를 토글했습니다.`); + await patchToggleWakeupAlarmActive(wakeupAlarmId); + setWakeupAlarms((prev) => prev.map((a) => (a.id === wakeupAlarmId ? { ...a, isActive: !a.isActive } : a))); + console.log(`알람 ${wakeupAlarmId}의 상태를 토글했습니다.`); } catch (error) { - console.error(`기상 알람 ${wakeupAlarmId} 토글 실패:`, error); - throw error; + console.error(`알람 ${wakeupAlarmId} 토글 실패:`, error); } }; - // 자동알람 토글 - const toggleAutoAlarmActiveById = async (autoAlarmId: number) => { - setAutoAlarms(prev => - prev.map(a => - a.auto_alarm_id === autoAlarmId ? { ...a, is_active: !a.is_active } : a - ) - ); - + // 자동알람은 alamitem에 관리 안 해서 동일한 로직 안 됨?? + const toggleAutoAlarmActivation = async (autoAlarmId: number) => { try { - const updated = await toggleAutoAlarmActivation(autoAlarmId); // API 호출 - // 서버가 내려준 최신 is_active로 동기화(신뢰도 ↑) - setAutoAlarms(prev => - prev.map(a => - a.auto_alarm_id === autoAlarmId ? { ...a, is_active: updated.is_active, wakeup_time: updated.wakeup_time } : a - ) - ); - // 상단 메인 토글(스위치) 기본값도 맞춰줌 (가장 가까운 알람 기준이면 MyAlarmPage에서 메모로 처리) - setAutoAlarmOn(updated.is_active); - } catch (e) { - console.error('자동알람 토글 실패:', e); - // 실패 시 롤백 - setAutoAlarms(prev => - prev.map(a => - a.auto_alarm_id === autoAlarmId ? { ...a, is_active: !a.is_active } : a - ) - ); - throw e; + await patchtoggleAutoAlarmActivation(autoAlarmId); + setAutoAlarms((prev) => prev.map((a) => (a.auto_alarm_id === autoAlarmId ? { ...a, is_active: !a.is_active } : a))); + console.log(`알람 ${autoAlarmId}의 상태를 토글했습니다.`); + } catch (error) { + console.error(`알람 ${autoAlarmId} 토글 실패:`, error); } }; + // 자동알람 토글 + // const toggleAutoAlarmActiveById = async (autoAlarmId: number) => { + // setAutoAlarms(prev => + // prev.map(a => + // a.auto_alarm_id === autoAlarmId ? { ...a, is_active: !a.is_active } : a + // ) + // ); + + // try { + // const updated = await patchtoggleAutoAlarmActivation(autoAlarmId); // API 호출 + // // 서버가 내려준 최신 is_active로 동기화(신뢰도 ↑) + // setAutoAlarms(prev => + // prev.map(a => + // a.auto_alarm_id === autoAlarmId ? { ...a, is_active: updated.is_active, wakeup_time: updated.wakeup_time } : a + // ) + // ); + // // 상단 메인 토글(스위치) 기본값도 맞춰줌 (가장 가까운 알람 기준이면 MyAlarmPage에서 메모로 처리) + // setAutoAlarmOn(updated.is_active); + // } catch (e) { + // console.error('자동알람 토글 실패:', e); + // // 실패 시 롤백 + // setAutoAlarms(prev => + // prev.map(a => + // a.auto_alarm_id === autoAlarmId ? { ...a, is_active: !a.is_active } : a + // ) + // ); + // throw e; + // } + // }; + const deleteAlarmById = async (alarmId: number) => { try { const target = myAlarms.find(a => a.id === alarmId); - const remoteId = target?.serverId ?? target?.id; // 우선 서버 ID 사용 + const remoteId = target?.serverId ?? target?.id; if (!remoteId) throw new Error('서버 ID 없음'); - await deleteMyAlarm(remoteId); // 서버와 동기화 + await deleteMyAlarm(remoteId); setMyAlarms(prev => prev.filter(a => a.id !== alarmId)); console.log(`알람 ${alarmId} 삭제 완료 (서버ID: ${remoteId})`); } catch (error) { @@ -226,14 +232,14 @@ export const AlarmProvider: React.FC<{ children: React.ReactNode }> = ({ childre setSelectedAlarmDate, updateAlarmField, updateWakeupAlarmField, - toggleAlarmActivation, + toggleMyAlarmActivation, deleteAlarmById, isLoadingAlarms, refreshAlarms, autoAlarms, setAutoAlarms, - toggleAutoAlarmActiveById, - toggleWakeupAlarmActiveById, + toggleWakeupAlarmActivation, + toggleAutoAlarmActivation, }} > {children} diff --git a/Time-Up/src/contexts/NotificationContext.tsx b/Time-Up/src/contexts/NotificationContext.tsx index 69baf73..cc46fb4 100644 --- a/Time-Up/src/contexts/NotificationContext.tsx +++ b/Time-Up/src/contexts/NotificationContext.tsx @@ -1,4 +1,75 @@ // src/contexts/NotificationContext.tsx +import { DevicePushToken, registerForDevicePushToken } from '@/src/utils/notification'; +import * as Notifications from 'expo-notifications'; +import React, { createContext, useContext, useEffect, useRef, useState } from 'react'; +// import { useAlarmContext } from '@/src/contexts/AlarmContext'; // 알람 동기화가 필요하면 사용 +async function saveDevicePushToken(params: { token: string; type: 'fcm' | 'apns' }) { + // 서버에 {token, type} 저장하는 API로 바꿔주세요. + // await axiosInstance.post('/push-token', params) + console.log('will save device token to server:', params); +} + +interface NotificationContextValue { + devicePushToken: DevicePushToken | null; // {type, token} + lastNotification: Notifications.Notification | null; +} + +const NotificationContext = createContext(undefined); + +export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [devicePushToken, setDevicePushToken] = useState(null); + const [lastNotification, setLastNotification] = useState(null); + const receivedListener = useRef(null); + const responseListener = useRef(null); + + useEffect(() => { + (async () => { + const tokenInfo = await registerForDevicePushToken(); + if (tokenInfo) { + setDevicePushToken(tokenInfo); + try { + await saveDevicePushToken({ token: tokenInfo.token, type: tokenInfo.type }); + console.log('Device push token saved:', tokenInfo); + } catch (e) { + console.warn('Failed to save device token:', e); + } + } + })(); + + // 앱 포그라운드 수신 + receivedListener.current = Notifications.addNotificationReceivedListener((notification) => { + setLastNotification(notification); + // const data = notification.request.content.data as any; + // 필요 시 알람 상태 동기화 로직 작성 + }); + + // 알림 클릭 응답 + responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => { + const data = response.notification.request.content.data as any; + console.log('notification tapped:', data); + // 필요 시 네비게이션 처리 + }); + + return () => { + receivedListener.current?.remove(); + responseListener.current?.remove(); + }; + }, []); + + return ( + + {children} + + ); +}; + +export const useNotificationContext = () => { + const ctx = useContext(NotificationContext); + if (!ctx) throw new Error('useNotificationContext must be used within NotificationProvider'); + return ctx; +}; + + // import { saveExpoPushToken } from '@/src/apis/pushToken'; // import { useAlarmContext } from '@/src/contexts/AlarmContext'; // import { registerForPushNotificationsAsync } from '@/src/utils/notification'; @@ -20,7 +91,7 @@ // const { updateAlarmField } = useAlarmContext(); -// //debugger; +// // //console.log('NotificationProvider 렌더링됨'); // useEffect(() => { // (async () => { diff --git a/Time-Up/src/pages/Alarm/AlarmPage.tsx b/Time-Up/src/pages/Alarm/AlarmPage.tsx index b9c19fd..c44d9f9 100644 --- a/Time-Up/src/pages/Alarm/AlarmPage.tsx +++ b/Time-Up/src/pages/Alarm/AlarmPage.tsx @@ -12,3 +12,35 @@ export default function AlarmPage() { return ; } + +// // src/pages/AlarmPage.tsx +// import { useAlarmContext } from '@/src/contexts/AlarmContext'; +// import React, { useEffect, useRef } from 'react'; +// import { View } from 'react-native'; +// import useAppNavigation from '../../hooks/useAppNavigation'; + +// export default function AlarmPage() { +// const navigation = useAppNavigation(); +// const { refreshAlarms } = useAlarmContext(); +// const syncedOnceRef = useRef(false); + +// useEffect(() => { +// // 알람 동기화 +// if (!syncedOnceRef.current) { +// syncedOnceRef.current = true; +// (async () => { +// try { +// await refreshAlarms(); +// console.log('알람 동기화 성공'); +// } catch (e) { +// console.warn('초기 알람 동기화 실패(무시 가능):', e); +// } +// })(); +// } + +// // WakeUpAlarmPage로 이동 +// navigation.navigate('WakeUpAlarmPage'); +// }, [navigation, refreshAlarms]); + +// return ; +// } diff --git a/Time-Up/src/pages/Alarm/My/EditMyAlarmPage.tsx b/Time-Up/src/pages/Alarm/My/EditMyAlarmPage.tsx index a5b5bfa..6439b55 100644 --- a/Time-Up/src/pages/Alarm/My/EditMyAlarmPage.tsx +++ b/Time-Up/src/pages/Alarm/My/EditMyAlarmPage.tsx @@ -12,7 +12,7 @@ import { toPutMyAlarmRequest } from '@/src/utils/alarmTransform'; import { BottomSheetModal, BottomSheetView } from '@gorhom/bottom-sheet'; import moment from 'moment'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Dimensions, Platform, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { Alert, Dimensions, Platform, Text, TextInput, TouchableOpacity, View } from 'react-native'; import { Calendar } from 'react-native-calendars'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -24,7 +24,8 @@ import IconRepeat from '../../../../assets/images/AlarmRepeat.svg'; export default function EditMyAlarmPage() { const navigation = useAppNavigation(); const { height } = Dimensions.get('window'); - const { selectedAlarmId, myAlarms, selectedAlarmDate, setSelectedAlarmDate, updateAlarmField, toggleAlarmActivation } = useAlarmContext(); + const { selectedAlarmId, myAlarms, selectedAlarmDate, setSelectedAlarmDate, + updateAlarmField, toggleMyAlarmActivation } = useAlarmContext(); const [currentDate, setCurrentDate] = useState(selectedAlarmDate); const [isEditingTitle, setIsEditingTitle] = useState(false); const [selectedItem, setSelectedItem] = useState(null) @@ -44,8 +45,13 @@ export default function EditMyAlarmPage() { alarmToEdit?.date ?? { fullDate: '2025-06-30', dayOfWeek: '월' } ); const [memo, setMemo] = useState(alarmToEdit?.memo ?? ''); + const [isEditingMemo, setIsEditingMemo] = useState(false); const [isActive, setIsActive] = useState(alarmToEdit?.isActive ?? true); + const [SoundOn, SoundSetOn] = useState(alarmToEdit?.isSound ?? false); + const [VibrateOn, VibrateSetOn] = useState(alarmToEdit?.isVibrating ?? false); + const [RepeatOn, RepeatSetOn] = useState(alarmToEdit?.isRepeating ?? false); + const handleCancel = () => { console.log(`${title} 알람 편집을 취소합니다`); navigation.goBack(); @@ -80,11 +86,20 @@ export default function EditMyAlarmPage() { console.log('수정 요청할 alarm_id:', selectedAlarmId); const res = await putMyAlarm(selectedAlarmId, patchBody); console.log('알람 수정 성공:', res); - } catch (error) { + navigation.navigate('MyAlarmPage'); + } catch (error: any) { + const status = error?.response?.status; + const apiErr = error?.response?.data?.error; + const apiMsg = error?.response?.data?.message; + + if ((status === 422 || status === 400) && + (apiErr === 'BusinessLogicError')) { + Alert.alert('저장 실패', '과거 시간에는 알람을 설정할 수 없습니다.'); + return; + } + else Alert.alert('저장 실패', '문제가 발생했습니다. 잠시 후 다시 시도해 주세요.'); console.error('알람 수정 실패:', error); } - - navigation.navigate('MyAlarmPage'); } } @@ -92,7 +107,7 @@ export default function EditMyAlarmPage() { if (!selectedAlarmId) return; try { - await toggleAlarmActivation(selectedAlarmId); + await toggleMyAlarmActivation(selectedAlarmId); const newState = !isActive; setIsActive(newState); updateAlarmField(selectedAlarmId, 'isActive', newState); @@ -102,11 +117,34 @@ export default function EditMyAlarmPage() { } }, [selectedAlarmId, isActive]); - const handleSelectSound = () => { navigation.navigate('SelectMyAlarmSoundPage'); }; + const handleSoundToggleSwitch = useCallback(() => { + const newState = !SoundOn; + SoundSetOn(newState); + if (selectedAlarmId != null) { + updateAlarmField(selectedAlarmId, 'isSound', newState); + } + }, [SoundOn, selectedAlarmId]); + + const handleVibrateToggleSwitch = useCallback(() => { + const newState = !VibrateOn; + VibrateSetOn(newState); + if (selectedAlarmId != null) { + updateAlarmField(selectedAlarmId, 'isVibrating', newState); + } + }, [VibrateOn, selectedAlarmId]); + + const handleRepeatToggleSwitch = useCallback(() => { + const newState = !RepeatOn; + RepeatSetOn(newState); + if (selectedAlarmId != null) { + updateAlarmField(selectedAlarmId, 'isRepeating', newState); + } + }, [RepeatOn, selectedAlarmId]); + const handleSelectVibrate = () => { navigation.navigate('SelectMyAlarmVibratePage'); }; @@ -143,18 +181,6 @@ export default function EditMyAlarmPage() { }; - const handleMemoCancel = () => { - bottomSheetModalRef.current?.dismiss(); - setSelectedItem(null); - console.log(`메모 취소됨`); - }; - - const handleMemoSave = () => { - bottomSheetModalRef.current?.dismiss(); - setSelectedItem(null); - console.log(`메모 저장됨: ${memo}`); - }; - useEffect(() => { if (selectedAlarmDate) { const dayOfWeek = ['일', '월', '화', '수', '목', '금', '토'][ @@ -278,25 +304,6 @@ export default function EditMyAlarmPage() { )} - - {(selectedItem === '메모') && ( - - setMemo(text)} - /> - - - - - - )} - @@ -366,72 +373,99 @@ export default function EditMyAlarmPage() { - - - - 알람음 + + + + + + 알람음 + + - - + + {alarmToEdit?.sound ?? '선택'} + + + + + + + + 진동 + + - - - - - 진동 - - - + + {alarmToEdit?.vibrate ?? '선택'} - - + + - - - - 다시 울림 + + + + + 다시 울림 + + - - {alarmToEdit?.repeat ?? '선택'} - - + + + {alarmToEdit?.repeat ?? '선택'} + + + - { - setSelectedItem("메모") - bottomSheetModalRef.current?.present() - }}> - + > 메모 - - {memo.trim() !== '' ? memo : '입력'} - - + + {isEditingMemo ? ( + setIsEditingMemo(false)} + returnKeyType="done" + onSubmitEditing={() => setIsEditingMemo(false)} + /> + ) : ( + setIsEditingMemo(true)} className="w-full"> + + {memo.trim() !== '' ? memo : '입력'} + + + )} + - + diff --git a/Time-Up/src/pages/Alarm/My/MyAlarmDetailPage.tsx b/Time-Up/src/pages/Alarm/My/MyAlarmDetailPage.tsx index ed730bf..beafec6 100644 --- a/Time-Up/src/pages/Alarm/My/MyAlarmDetailPage.tsx +++ b/Time-Up/src/pages/Alarm/My/MyAlarmDetailPage.tsx @@ -18,7 +18,8 @@ import IconRepeat from '../../../../assets/images/AlarmRepeat.svg'; export default function MyAlarmDetailPage() { const navigation = useAppNavigation(); const { height } = Dimensions.get('window'); - const { selectedAlarmId, myAlarms, setMyAlarms, deleteAlarmById, toggleAlarmActivation, updateAlarmField } = useAlarmContext(); + const { selectedAlarmId, myAlarms, setMyAlarms, deleteAlarmById, toggleMyAlarmActivation, + updateAlarmField } = useAlarmContext(); const alarm = myAlarms.find(a => a.id === selectedAlarmId); @@ -26,7 +27,7 @@ export default function MyAlarmDetailPage() { if (!alarm) return; try { - await toggleAlarmActivation(alarm.id); + await toggleMyAlarmActivation(alarm.id); const newState = !alarm.isActive; updateAlarmField(alarm.id, 'isActive', newState); console.log(`알람 ${alarm.id}번이 ${newState ? '활성화' : '비활성화'}되었습니다.`); @@ -53,13 +54,11 @@ export default function MyAlarmDetailPage() { try { await deleteAlarmById(selectedAlarmId); - // 여기 아래를 서버 id로 넘기면? setMyAlarms(prev => prev.filter(a => a.id !== selectedAlarmId)); navigation.navigate('MyAlarmPage'); } catch (error: any) { if (error.response?.status === 404) { console.warn(`알람 ${selectedAlarmId}는 이미 존재하지 않습니다.`); - // 여기 아래를 서버 id로 넘기면? setMyAlarms(prev => prev.filter(a => a.id !== selectedAlarmId)); navigation.navigate('MyAlarmPage'); } else { @@ -99,26 +98,42 @@ export default function MyAlarmDetailPage() { - - - - 알람음 + + + + 알람음 + + + { }} /> + - + {alarm.sound && alarm.sound !== '선택' ? alarm.sound : '-'} + - - - 진동 + + + + 진동 + + + { }} /> + - + {alarm.vibrate && alarm.vibrate !== '선택' ? alarm.vibrate : '-'} @@ -126,10 +141,15 @@ export default function MyAlarmDetailPage() { - - - - 다시 울림 + + + + + 다시 울림 + + + { }} /> + {alarm.repeat && alarm.repeat !== '선택' ? alarm.repeat : '-'} @@ -150,7 +170,7 @@ export default function MyAlarmDetailPage() { - + diff --git a/Time-Up/src/pages/Alarm/My/MyAlarmPage.tsx b/Time-Up/src/pages/Alarm/My/MyAlarmPage.tsx index c891611..75a9cb9 100644 --- a/Time-Up/src/pages/Alarm/My/MyAlarmPage.tsx +++ b/Time-Up/src/pages/Alarm/My/MyAlarmPage.tsx @@ -21,8 +21,7 @@ export default function MyAlarmPage() { autoAlarmOn, setAutoAlarmOn, autoAlarms, setAutoAlarms, myAlarms, setMyAlarms, - setSelectedAlarmId, updateAlarmField, toggleAlarmActivation, - toggleAutoAlarmActiveById, + setSelectedAlarmId, updateAlarmField, toggleMyAlarmActivation, toggleAutoAlarmActivation, } = useAlarmContext(); useEffect(() => { @@ -33,22 +32,22 @@ export default function MyAlarmPage() { '/alarm/alarmlist', { headers: { Authorization: `Bearer ${token}` } } ); - - // 전체 응답 출력 - console.log('전체 알람 응답:', res.data); - - console.log('기상 알람 목록:', res.data.success?.wakeup_alarms ?? []); - console.log('자동 알람 목록:', res.data.success?.auto_alarms ?? []); - console.log('내 알람 목록:', res.data.success?.my_alarms ?? []); - - } catch (err) { - console.error('전체 알람 조회 실패:', err); - } - }; - - fetchAlarms(); - }, []); - + + // 전체 응답 출력 + console.log('전체 알람 응답:', res.data); + + console.log('기상 알람 목록:', res.data.success?.wakeup_alarms ?? []); + console.log('자동 알람 목록:', res.data.success?.auto_alarms ?? []); + console.log('내 알람 목록:', res.data.success?.my_alarms ?? []); + + } catch (err) { + console.error('전체 알람 조회 실패:', err); + } + }; + + fetchAlarms(); + }, []); + // 가장 가까운 자동 알람 선택 const nextAutoAlarm = React.useMemo(() => { if (!autoAlarms || autoAlarms.length === 0) return null; @@ -57,32 +56,43 @@ export default function MyAlarmPage() { ); return sorted[0]; }, [autoAlarms]); - + // 날짜/시간 포맷터 (KST 기준, 디바이스 로컬 시간 사용) const formatAutoDateLine = (iso: string) => { const d = new Date(iso); const days = ['일', '월', '화', '수', '목', '금', '토']; - return `${d.getMonth() + 1}월 ${d.getDate()}일 (${days[d.getDay()]})`; + return `${d.getUTCMonth() + 1}월 ${d.getUTCDate()}일 (${days[d.getUTCDay()]})`; }; + const formatAutoTimeLine = (iso: string) => { const d = new Date(iso); - const h = d.getHours(); - const m = d.getMinutes(); + const h = d.getUTCHours(); + const m = d.getUTCMinutes(); const period = h < 12 ? '오전' : '오후'; const hh = (h % 12) || 12; return `${period} ${String(hh).padStart(2, '0')} : ${String(m).padStart(2, '0')}`; }; + // const handleToggleAutoAlarm = async (autoAlarmId: number, currentState: boolean) => { + // try { + // await toggleAutoAlarmActivation(autoAlarmId); + // const newState = !currentState; + // updateAlarmField(autoAlarmId, 'isActive', newState); + // console.log(`알람 ${autoAlarmId}번이 ${newState ? '활성화' : '비활성화'}되었습니다.`); + // } catch (error) { + // console.error(`알람 ${autoAlarmId}번 토글 실패:`, error); + // } + // }; - const handleToggleAutoAlarm = () => { - if (!autoAlarmOn) { - console.log('자동알람이 켜졌습니다.'); - } else { - console.log('자동알람이 꺼졌습니다.'); + const handleToggleAutoAlarm = async (autoAlarmId: number) => { + try { + await toggleAutoAlarmActivation(autoAlarmId); + } catch (error) { + console.error(`자동 알람 ${autoAlarmId} 토글 실패:`, error); } - setAutoAlarmOn((prev) => !prev); }; + // const handleToggleAutoAlarm = async () => { // try { // if (nextAutoAlarm?.auto_alarm_id != null) { @@ -114,19 +124,18 @@ export default function MyAlarmPage() { navigation.navigate('MyAlarmDetailPage'); }; - const handleToggleAlarm = async (id: number, currentState: boolean) => { + const handleToggleAlarm = async (myalarmId: number, currentState: boolean) => { try { - await toggleAlarmActivation(id); + await toggleMyAlarmActivation(myalarmId); const newState = !currentState; - updateAlarmField(id, 'isActive', newState); - console.log(`알람 ${id}번이 ${newState ? '활성화' : '비활성화'}되었습니다.`); + updateAlarmField(myalarmId, 'isActive', newState); + console.log(`알람 ${myalarmId}번이 ${newState ? '활성화' : '비활성화'}되었습니다.`); } catch (error) { - console.error(`알람 ${id}번 토글 실패:`, error); + console.error(`알람 ${myalarmId}번 토글 실패:`, error); } }; const handleNewAlarm = async () => { - //debugger; // 날짜-시간 조정하기. 오전 오후 시간 계산? 우선 안전하게 하루 뒤로 지정해 둠. try { const now = new Date(); @@ -179,7 +188,10 @@ export default function MyAlarmPage() { 내일의 자동 알람 - + nextAutoAlarm?.auto_alarm_id && handleToggleAutoAlarm(nextAutoAlarm.auto_alarm_id)} disabled={!nextAutoAlarm} /> + (null) const bottomSheetModalRef = useRef(null); const snapPoints = useMemo(() => ['65%'], []) @@ -32,11 +33,8 @@ export default function EditWakeUpAlarmPage() { console.log('handleSheetChanges', index) }, []) - // 테스트 초기값 설정. 알람 상세 설정 상태 관리 구현 후 제거하기. - // 제목, 시간, 날짜, 사운드, 진동, 반복, 메모, 알람 온오프 구현 완료. + // 제목, 시간, 날짜, 사운드, 진동, 반복, 메모, 알람 온오프 상태 관리 const alarmToEdit = wakeupAlarms.find(a => a.serverId === selectedAlarmId); - //const alarmToEdit = wakeupAlarms.find(a => a.id === selectedAlarmId); - const [title, setTitle] = useState(alarmToEdit?.title ?? ''); const [time, setTime] = useState( alarmToEdit?.time ?? { period: '오전', hour: 7, minute: 0 } @@ -45,8 +43,12 @@ export default function EditWakeUpAlarmPage() { alarmToEdit?.date ?? { fullDate: '2025-06-30', dayOfWeek: '월' } ); const [memo, setMemo] = useState(alarmToEdit?.memo ?? ''); + const [isEditingMemo, setIsEditingMemo] = useState(false); const [isActive, setIsActive] = useState(alarmToEdit?.isActive ?? true); - const [isToggling, setIsToggling] = useState(false); + + const [SoundOn, SoundSetOn] = useState(alarmToEdit?.isSound ?? false); + const [VibrateOn, VibrateSetOn] = useState(alarmToEdit?.isVibrating ?? false); + const [RepeatOn, RepeatSetOn] = useState(alarmToEdit?.isRepeating ?? false); const handleToggleSwitchForDay = (day: Day) => { setWeekdaySwitchStates((prev) => { @@ -72,11 +74,11 @@ export default function EditWakeUpAlarmPage() { if (!alarmToEdit) return; try { - updateWakeupAlarmField (selectedAlarmId, 'title', title); - updateWakeupAlarmField (selectedAlarmId, 'time', time); - updateWakeupAlarmField (selectedAlarmId, 'date', date); - updateWakeupAlarmField (selectedAlarmId, 'memo', memo); - updateWakeupAlarmField (selectedAlarmId, 'isActive', isActive); + updateWakeupAlarmField(selectedAlarmId, 'title', title); + updateWakeupAlarmField(selectedAlarmId, 'time', time); + updateWakeupAlarmField(selectedAlarmId, 'date', date); + updateWakeupAlarmField(selectedAlarmId, 'memo', memo); + updateWakeupAlarmField(selectedAlarmId, 'isActive', isActive); // API 바디 변환 const requestBody = toPutWakeupAlarmRequest({ @@ -130,20 +132,29 @@ export default function EditWakeUpAlarmPage() { console.log(`시간 선택됨: ${time.period} ${String(time.hour).padStart(2, '0')}시 ${String(time.minute).padStart(2, '0')}분`); }; + const handleSoundToggleSwitch = useCallback(() => { + const newState = !SoundOn; + SoundSetOn(newState); + if (selectedAlarmId != null) { + updateWakeupAlarmField(selectedAlarmId, 'isSound', newState); + } + }, [SoundOn, selectedAlarmId]); - const handleMemoCancel = () => { - bottomSheetModalRef.current?.dismiss(); - setSelectedItem(null); - console.log(`메모 취소됨`); - }; - - const handleMemoSave = () => { - bottomSheetModalRef.current?.dismiss(); - setSelectedItem(null); - console.log(`메모 저장됨: ${memo}`); - }; - + const handleVibrateToggleSwitch = useCallback(() => { + const newState = !VibrateOn; + VibrateSetOn(newState); + if (selectedAlarmId != null) { + updateWakeupAlarmField(selectedAlarmId, 'isVibrating', newState); + } + }, [VibrateOn, selectedAlarmId]); + const handleRepeatToggleSwitch = useCallback(() => { + const newState = !RepeatOn; + RepeatSetOn(newState); + if (selectedAlarmId != null) { + updateWakeupAlarmField(selectedAlarmId, 'isRepeating', newState); + } + }, [RepeatOn, selectedAlarmId]); return ( @@ -183,25 +194,6 @@ export default function EditWakeUpAlarmPage() { )} - - {(selectedItem === '메모') && ( - - setMemo(text)} - /> - - - - - - )} - @@ -270,72 +262,100 @@ export default function EditWakeUpAlarmPage() { - - - - 알람음 + + + + + + 알람음 + + - - + + {alarmToEdit?.sound ?? '선택'} + + + + + + + + 진동 + + - - - - - 진동 - - - + + {alarmToEdit?.vibrate ?? '선택'} - - + + - - - - 다시 울림 + + + + + 다시 울림 + + - - {alarmToEdit?.repeat ?? '선택'} - - + + + {alarmToEdit?.repeat ?? '선택'} + + + - { - setSelectedItem("메모") - bottomSheetModalRef.current?.present() - }}> - + > 메모 - - {memo.trim() !== '' ? memo : '입력'} - - + + {isEditingMemo ? ( + setIsEditingMemo(false)} + returnKeyType="done" + onSubmitEditing={() => setIsEditingMemo(false)} + /> + ) : ( + setIsEditingMemo(true)} className="w-full"> + + {memo.trim() !== '' ? memo : '입력'} + + + )} + + diff --git a/Time-Up/src/pages/Alarm/WakeUp/WakeUpAlarmDetailPage.tsx b/Time-Up/src/pages/Alarm/WakeUp/WakeUpAlarmDetailPage.tsx index 4da0c03..bda7440 100644 --- a/Time-Up/src/pages/Alarm/WakeUp/WakeUpAlarmDetailPage.tsx +++ b/Time-Up/src/pages/Alarm/WakeUp/WakeUpAlarmDetailPage.tsx @@ -111,22 +111,39 @@ export default function WakeUpAlarmDetailPage() { - - - 알람음 + + + + 알람음 + + + { }} /> + - + {alarm.sound && alarm.sound !== '선택' ? alarm.sound : '-'} + - - - 진동 + + + + 진동 + + + { }} /> + - + {alarm.vibrate && alarm.vibrate !== '선택' ? alarm.vibrate : '-'} @@ -134,12 +151,17 @@ export default function WakeUpAlarmDetailPage() { - - - - 다시 울림 + + + + + 다시 울림 + + + { }} /> + - + {alarm.repeat && alarm.repeat !== '선택' ? alarm.repeat : '-'} diff --git a/Time-Up/src/pages/Alarm/WakeUp/WakeUpAlarmPage.tsx b/Time-Up/src/pages/Alarm/WakeUp/WakeUpAlarmPage.tsx index 1d62531..34e51e8 100644 --- a/Time-Up/src/pages/Alarm/WakeUp/WakeUpAlarmPage.tsx +++ b/Time-Up/src/pages/Alarm/WakeUp/WakeUpAlarmPage.tsx @@ -5,7 +5,7 @@ import { GetAllAlarmsResponse } from '@/src/types/alarm'; import { formatTime } from '@/src/utils/AlarmFormat'; import { getAccessToken } from '@/src/utils/storage'; import { LinearGradient } from 'expo-linear-gradient'; -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { Text, TouchableOpacity, View } from 'react-native'; import BottomLayout from '../../../Layouts/BottomLayout'; import ToggleSwitch from '../../../components/common/ToggleSwitch'; @@ -16,7 +16,9 @@ const weekdays: Day[] = ['월', '화', '수', '목', '금', '토', '일']; export default function WakeUpAlarmPage() { const navigation = useAppNavigation(); - const { setSelectedDay, weekdaySwitchStates, setWeekdaySwitchStates, autoAlarmOn, setAutoAlarmOn, wakeupAlarms, setSelectedAlarmId, autoAlarms } = useAlarmContext(); + const { setSelectedDay, weekdaySwitchStates, toggleWakeupAlarmActivation, + toggleAutoAlarmActivation, updateAlarmField, wakeupAlarms, setSelectedAlarmId, + autoAlarms, updateWakeupAlarmField } = useAlarmContext(); const dayToIndex: Record = { '일': 0, '월': 1, '화': 2, '수': 3, '목': 4, '금': 5, '토': 6, @@ -76,23 +78,38 @@ export default function WakeUpAlarmPage() { return `${period} ${String(hh).padStart(2, '0')} : ${String(m).padStart(2, '0')}`; }; - const handleToggleAutoAlarm = () => { - if (!autoAlarmOn) { - console.log('자동알람이 켜졌습니다.'); - } else { - console.log('자동알람이 꺼졌습니다.'); + const handleToggleAutoAlarm = async (autoAlarmId: number) => { + try { + await toggleAutoAlarmActivation(autoAlarmId); + } catch (error) { + console.error(`자동 알람 ${autoAlarmId} 토글 실패:`, error); } - setAutoAlarmOn((prev) => !prev); }; - const handleToggleSwitchForDay = (day: Day) => { - setWeekdaySwitchStates(prev => { - const next = { ...prev, [day]: !prev[day] }; - console.log(`${day} 기상알람이 ${next[day] ? '켜졌습니다' : '꺼졌습니다'}.`); - return next; - }); + const handleToggleSwitchForDay = async (currentState: boolean, wakeupAlarmId?: number) => { + if (wakeupAlarmId == null) { + console.warn("wakeupAlarmId가 없습니다. 토글을 실행하지 않습니다."); + return; + } + + try { + await toggleWakeupAlarmActivation(wakeupAlarmId); + const newState = !currentState; + updateWakeupAlarmField(wakeupAlarmId, 'isActive', newState); + console.log(`알람 ${wakeupAlarmId}번이 ${newState ? '활성화' : '비활성화'}되었습니다.`); + } catch (error) { + console.error(`알람 ${wakeupAlarmId}번 토글 실패:`, error); + } }; + // const handleToggleSwitchForDay = (day: Day) => { + // setWeekdaySwitchStates(prev => { + // const next = { ...prev, [day]: !prev[day] }; + // console.log(`${day} 기상알람이 ${next[day] ? '켜졌습니다' : '꺼졌습니다'}.`); + // return next; + // }); + // }; + const handleMyPage = () => { console.log('내 알람 페이지로 이동합니다.'); navigation.navigate('MyAlarmPage'); @@ -120,12 +137,32 @@ export default function WakeUpAlarmPage() { navigation.navigate('WakeUpAlarmDetailPage'); }; + const { refreshAlarms } = useAlarmContext(); + const syncedOnceRef = useRef(false); + + // useEffect(() => { + // // 알람 동기화 + // if (!syncedOnceRef.current) { + // syncedOnceRef.current = true; + // (async () => { + // try { + // await refreshAlarms(); + // console.log('알람 동기화 성공'); + // } catch (e) { + // console.warn('초기 알람 동기화 실패(무시 가능):', e); + // } + // })(); + // } + // }, [navigation, refreshAlarms]); + return ( 내일의 자동 알람 - + nextAutoAlarm?.auto_alarm_id && handleToggleAutoAlarm(nextAutoAlarm.auto_alarm_id)} disabled={!nextAutoAlarm} /> {formattedTime} - handleToggleSwitchForDay(day)} - disabled={false} - /> + {alarm ? ( + handleToggleSwitchForDay(alarm.isActive, alarm.serverId)} + disabled={false} + /> + ) : ( + { }} disabled={true} /> + )} ); diff --git a/Time-Up/src/types/pushToken.ts b/Time-Up/src/types/pushToken.ts new file mode 100644 index 0000000..7dc9cd1 --- /dev/null +++ b/Time-Up/src/types/pushToken.ts @@ -0,0 +1,46 @@ +// src/types/pushToken.ts +export interface SavePushTokenRequest { + token: string; +} + +export interface SavePushTokenSuccessResponse { + result: 'Success'; + status: 200; + success: number; + error: null; +} + +export interface SavePushTokenErrorResponse { + result: 'Error'; + status: number; + error: string; + success?: number; +} + +export type SavePushTokenResponse = + | SavePushTokenSuccessResponse + | SavePushTokenErrorResponse; + +// 임의 전달 값들 +// export interface PostWakeupAlarmPushRequest { +// repeat_count: number; +// repeat_interval: number; +// push_message: string; +// } + +// export interface PostWakeupAlarmPushSuccess { +// wakeup_alarm_id: number; +// total_push_sent: number; +// scheduled_push_times: string[]; +// message: string; +// } + +// export interface ApiResponse { +// result: 'Success' | 'Fail'; +// status: number; +// success: T | null; +// error: { +// errorCode: string; +// message: string; +// } | null; +// } \ No newline at end of file diff --git a/Time-Up/src/utils/notification.ts b/Time-Up/src/utils/notification.ts index 2a552eb..c9aad3b 100644 --- a/Time-Up/src/utils/notification.ts +++ b/Time-Up/src/utils/notification.ts @@ -1,4 +1,37 @@ -// // src/utils/notification.ts +// src/utils/notification.ts +import * as Device from 'expo-device'; +import * as Notifications from 'expo-notifications'; +import { Platform } from 'react-native'; + +export type DevicePushToken = { type: 'fcm' | 'apns'; token: string }; + +export async function registerForDevicePushToken(): Promise { + if (!Device.isDevice) return null; + + // Android 채널 (중요도 등) + if (Platform.OS === 'android') { + await Notifications.setNotificationChannelAsync('default', { + name: 'default', + importance: Notifications.AndroidImportance.MAX, + }); + } + + // 권한 요청 + let { status } = await Notifications.getPermissionsAsync(); + if (status !== 'granted') { + ({ status } = await Notifications.requestPermissionsAsync({ + ios: { allowAlert: true, allowBadge: true, allowSound: true }, + })); + } + if (status !== 'granted') return null; + + // 디바이스 푸시 토큰 + const { type, data } = await Notifications.getDevicePushTokenAsync(); + // Android => type 'fcm', iOS => type 'apns' : apns 나중에. + return { type: type as 'fcm' | 'apns', token: data }; +} + + // import * as Device from 'expo-device'; // import * as Notifications from 'expo-notifications'; // import { Platform } from 'react-native'; @@ -18,7 +51,7 @@ // // Expo Push Token 발급 + 안드 채널 설정 // export async function registerForPushNotificationsAsync(): Promise { -// //debugger; +// // if (!Device.isDevice) { // alert('푸시 알림은 실제 기기에서만 작동합니다.'); // return null; diff --git a/Time-Up/src/utils/webPush.ts b/Time-Up/src/utils/webPush.ts new file mode 100644 index 0000000..f3a7ce0 --- /dev/null +++ b/Time-Up/src/utils/webPush.ts @@ -0,0 +1,69 @@ +// src/utils/webPush.ts +import { initializeApp } from 'firebase/app'; +import { getMessaging, getToken } from 'firebase/messaging'; + +const firebaseConfig = { + apiKey: "AIzaSyAEAKbupll7pRrAuhCtI0SbHRAx-rXj7oM", + authDomain: "timeup-80358.firebaseapp.com", + projectId: "timeup-80358", + messagingSenderId: "482152461783", + appId: "1:482152461783:web:8ee854bcf8cad1ab3bc792", +}; + +const app = initializeApp(firebaseConfig); +export const messaging = getMessaging(app); + +// 1) 푸시 권한 요청 + 2) 서비스워커 등록 + 3) FCM 웹 토큰 발급 +// export async function requestWebPushToken(): Promise { +// if (!('Notification' in window)) return null; + +// const permission = await Notification.requestPermission(); +// if (permission !== 'granted') return null; + +// // 이미 public/index.html에서 /firebase-messaging-sw.js 를 등록했지만, +// // 안전하게 등록 객체를 getToken에 넘겨 위치를 명시. (Firebase 공식 지원) +// const registration = await navigator.serviceWorker.getRegistration('/') +// ?? await navigator.serviceWorker.register('/firebase-messaging-sw.js'); + +// const token = await getToken(messaging, { +// vapidKey: 'BK6LHOnQzm8od0iib8gPwgv7K6eI43xNDDhpVRLkNTXWZ4jVfxCfaTwy-HPNqHeoiPyM3C3o7sUhPIOHBSetyUc', +// serviceWorkerRegistration: registration, +// }); + +// return token || null; +// } + +export async function requestWebPushToken(): Promise { + if (!('Notification' in window)) return null; + + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + console.warn('웹 푸시 권한 거부됨'); + return null; + } + + let registration = await navigator.serviceWorker.getRegistration('/'); + if (!registration) { + registration = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/' }); + } + + // 2) 활성화(ready)까지 대기 + await navigator.serviceWorker.ready; + + try { + const token = await getToken(messaging, { + vapidKey: 'BK6LHOnQzm8od0iib8gPwgv7K6eI43xNDDhpVRLkNTXWZ4jVfxCfaTwy-HPNqHeoiPyM3C3o7sUhPIOHBSetyUc', + serviceWorkerRegistration: registration, + }); + if (!token) { + console.warn('토큰이 비어 있음 (권한/HTTPS/도메인/스코프 확인 필요)'); + return null; + } + console.log('Web FCM Token:', token); // 정상 발급 + return token; + } catch (e: any) { + console.error('getToken 실패:', e?.code, e?.message, e); + return null; + } +} +