diff --git a/app.config.js b/app.config.js index 7d3006a..dc209ad 100644 --- a/app.config.js +++ b/app.config.js @@ -18,6 +18,12 @@ export default { supportsTablet: true, infoPlist: { ITSAppUsesNonExemptEncryption: false, + NSCameraUsageDescription: + '인증사진 촬영을 위해 카메라 접근이 필요합니다.', + NSMicrophoneUsageDescription: + '비디오 촬영을 위해 마이크 접근이 필요합니다.', + NSPhotoLibraryUsageDescription: + '인증사진을 갤러리에 저장하기 위해 사진 라이브러리 접근이 필요합니다.', }, }, android: { @@ -25,8 +31,16 @@ export default { foregroundImage: './assets/adaptive-icon.png', backgroundColor: '#ffffff', }, - edgeToEdgeEnabled: true, + edgeToEdgeEnabled: false, package: 'com.chris13404.runova', + permissions: [ + 'android.permission.CAMERA', + 'android.permission.RECORD_AUDIO', + 'android.permission.READ_EXTERNAL_STORAGE', + 'android.permission.WRITE_EXTERNAL_STORAGE', + 'android.permission.READ_MEDIA_IMAGES', + 'android.permission.READ_MEDIA_VIDEO', + ], }, web: { favicon: './assets/favicon.png', @@ -50,6 +64,16 @@ export default { process.env.EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN, }, ], + [ + 'react-native-vision-camera', + { + cameraPermissionText: + '인증사진 촬영을 위해 카메라 접근이 필요합니다.', + enableMicrophonePermission: true, + microphonePermissionText: + '비디오 촬영을 위해 마이크 접근이 필요합니다.', + }, + ], ], extra: { MAPBOX_ACCESS_TOKEN: diff --git a/apps/web/src/api/art.ts b/apps/web/src/api/art.ts new file mode 100644 index 0000000..1796a57 --- /dev/null +++ b/apps/web/src/api/art.ts @@ -0,0 +1,10 @@ +import api from '@/lib/api'; + +export type RunningArtResponse = { + results: string[]; +}; + +export async function fetchRunningArt(): Promise { + const { data } = await api.get('/api/running/art'); + return data?.results ?? []; +} diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png index 03d6f6b..1b6656f 100644 Binary files a/assets/adaptive-icon.png and b/assets/adaptive-icon.png differ diff --git a/assets/favicon.png b/assets/favicon.png index e75f697..1b6656f 100644 Binary files a/assets/favicon.png and b/assets/favicon.png differ diff --git a/assets/icon.png b/assets/icon.png index a0b1526..1b6656f 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/splash-icon.png b/assets/splash-icon.png index 03d6f6b..1b6656f 100644 Binary files a/assets/splash-icon.png and b/assets/splash-icon.png differ diff --git a/eas.json b/eas.json index a29eb10..2e64167 100644 --- a/eas.json +++ b/eas.json @@ -1,6 +1,7 @@ { "cli": { - "version": ">= 7.6.0" + "version": ">= 7.6.0", + "appVersionSource": "local" }, "build": { "development": { diff --git a/package-lock.json b/package-lock.json index 9239ec6..179ba0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@react-navigation/bottom-tabs": "^7.4.6", "@react-navigation/native": "^7.1.17", "@react-navigation/native-stack": "^7.3.25", + "@react-three/drei": "^10.7.6", + "@react-three/fiber": "^9.3.0", "@rnmapbox/maps": "^10.1.41", "@turf/turf": "^7.2.0", "axios": "^1.11.0", @@ -25,18 +27,22 @@ "expo-crypto": "^14.1.5", "expo-dev-client": "~5.2.4", "expo-file-system": "~18.1.11", + "expo-gl": "^16.0.7", "expo-linear-gradient": "~14.1.5", "expo-location": "^18.1.6", "expo-media-library": "~17.1.7", + "expo-speech": "^14.0.7", "expo-status-bar": "~2.2.3", "lucide-react-native": "^0.540.0", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.5", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.28.0", - "react-native-reanimated": "~3.14.0", + "react-native-reanimated": "^4.1.0", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", + "react-native-share": "^12.2.0", "react-native-svg": "15.11.2", "react-native-toast-message": "^2.3.3", "react-native-view-shot": "^4.0.3", @@ -45,6 +51,7 @@ "react-native-webview": "13.13.5", "react-native-worklets": "^0.5.1", "react-native-worklets-core": "^1.6.2", + "three": "^0.180.0", "zustand": "^5.0.8" }, "devDependencies": { @@ -82,19 +89,6 @@ } } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -110,30 +104,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -408,13 +402,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -507,12 +501,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -882,9 +876,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", - "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -913,9 +907,9 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz", - "integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", @@ -923,7 +917,7 @@ "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1121,16 +1115,16 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", - "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.0" + "@babel/traverse": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -1314,9 +1308,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz", - "integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1484,9 +1478,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1507,17 +1501,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -1526,17 +1520,17 @@ }, "node_modules/@babel/traverse--for-generate-function-map": { "name": "@babel/traverse", - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -1544,9 +1538,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1556,6 +1550,12 @@ "node": ">=6.9.0" } }, + "node_modules/@dimforge/rapier3d-compat": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "license": "Apache-2.0" + }, "node_modules/@egjs/hammerjs": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", @@ -1569,21 +1569,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.4", + "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "dev": true, "license": "MIT", "optional": true, @@ -1592,9 +1592,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", "optional": true, @@ -1748,9 +1748,9 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1886,21 +1886,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@eslint/eslintrc/node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 4" } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { @@ -1916,33 +1909,10 @@ "node": "*" } }, - "node_modules/@eslint/eslintrc/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", "dev": true, "license": "MIT", "engines": { @@ -1977,9 +1947,9 @@ } }, "node_modules/@expo/cli": { - "version": "0.24.20", - "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.20.tgz", - "integrity": "sha512-uF1pOVcd+xizNtVTuZqNGzy7I6IJon5YMmQidsURds1Ww96AFDxrR/NEACqeATNAmY60m8wy1VZZpSg5zLNkpw==", + "version": "0.24.21", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.21.tgz", + "integrity": "sha512-DT6K9vgFHqqWL/19mU1ofRcPoO1pn4qmgi76GtuiNU4tbBe/02mRHwFsQw7qRfFAT28If5e/wiwVozgSuZVL8g==", "license": "MIT", "dependencies": { "@0no-co/graphql.web": "^1.0.8", @@ -1996,10 +1966,11 @@ "@expo/package-manager": "^1.8.6", "@expo/plist": "^0.3.5", "@expo/prebuild-config": "^9.0.11", + "@expo/schema-utils": "^0.1.0", "@expo/spawn-async": "^1.7.2", "@expo/ws-tunnel": "^1.0.1", "@expo/xcpretty": "^4.3.0", - "@react-native/dev-middleware": "0.79.5", + "@react-native/dev-middleware": "0.79.6", "@urql/core": "^5.0.6", "@urql/exchange-retry": "^1.3.0", "accepts": "^1.3.8", @@ -2048,6 +2019,35 @@ "expo-internal": "build/bin/cli" } }, + "node_modules/@expo/cli/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/cli/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@expo/cli/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -2113,6 +2113,35 @@ "xml2js": "0.6.0" } }, + "node_modules/@expo/config-plugins/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/config-plugins/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@expo/config-plugins/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -2140,6 +2169,35 @@ "@babel/highlight": "^7.10.4" } }, + "node_modules/@expo/config/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/config/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@expo/config/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -2172,6 +2230,35 @@ "ms": "^2.1.1" } }, + "node_modules/@expo/devcert/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/devcert/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@expo/env": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@expo/env/-/env-1.0.7.tgz", @@ -2208,6 +2295,44 @@ "fingerprint": "bin/cli.js" } }, + "node_modules/@expo/fingerprint/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/fingerprint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@expo/fingerprint/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@expo/fingerprint/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -2295,6 +2420,35 @@ "resolve-from": "^5.0.0" } }, + "node_modules/@expo/metro-config/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@expo/metro-config/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@expo/metro-runtime": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-5.0.4.tgz", @@ -2305,9 +2459,9 @@ } }, "node_modules/@expo/osascript": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.2.5.tgz", - "integrity": "sha512-Bpp/n5rZ0UmpBOnl7Li3LtM7la0AR3H9NNesqL+ytW5UiqV/TbonYW3rDZY38u4u/lG7TnYflVIVQPD+iqZJ5w==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.7.tgz", + "integrity": "sha512-IClSOXxR0YUFxIriUJVqyYki7lLMIHrrzOaP01yxAL1G8pj2DWV5eW1y5jSzIcIfSCNhtGsshGd1tU/AYup5iQ==", "license": "MIT", "dependencies": { "@expo/spawn-async": "^1.7.2", @@ -2318,12 +2472,12 @@ } }, "node_modules/@expo/package-manager": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.8.6.tgz", - "integrity": "sha512-gcdICLuL+nHKZagPIDC5tX8UoDDB8vNA5/+SaQEqz8D+T2C4KrEJc2Vi1gPAlDnKif834QS6YluHWyxjk0yZlQ==", + "version": "1.9.8", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.8.tgz", + "integrity": "sha512-4/I6OWquKXYnzo38pkISHCOCOXxfeEmu4uDoERq1Ei/9Ur/s9y3kLbAamEkitUkDC7gHk1INxRWEfFNzGbmOrA==", "license": "MIT", "dependencies": { - "@expo/json-file": "^9.1.5", + "@expo/json-file": "^10.0.7", "@expo/spawn-async": "^1.7.2", "chalk": "^4.0.0", "npm-package-arg": "^11.0.0", @@ -2331,21 +2485,40 @@ "resolve-workspace-root": "^2.0.0" } }, - "node_modules/@expo/plist": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.3.5.tgz", - "integrity": "sha512-9RYVU1iGyCJ7vWfg3e7c/NVyMFs8wbl+dMWZphtFtsqyN9zppGREU3ctlD3i8KUE0sCUTVnLjCWr+VeUIDep2g==", + "node_modules/@expo/package-manager/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "license": "MIT", "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.2.3", + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@expo/package-manager/node_modules/@expo/json-file": { + "version": "10.0.7", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.7.tgz", + "integrity": "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/plist": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.3.5.tgz", + "integrity": "sha512-9RYVU1iGyCJ7vWfg3e7c/NVyMFs8wbl+dMWZphtFtsqyN9zppGREU3ctlD3i8KUE0sCUTVnLjCWr+VeUIDep2g==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.2.3", "xmlbuilder": "^15.1.1" } }, "node_modules/@expo/prebuild-config": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.11.tgz", - "integrity": "sha512-0DsxhhixRbCCvmYskBTq8czsU0YOBsntYURhWPNpkl0IPVpeP9haE5W4OwtHGzXEbmHdzaoDwNmVcWjS/mqbDw==", + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.12.tgz", + "integrity": "sha512-AKH5Scf+gEMgGxZZaimrJI2wlUJlRoqzDNn7/rkhZa5gUTnO4l6slKak2YdaH+nXlOWCNfAQWa76NnpQIfmv6Q==", "license": "MIT", "dependencies": { "@expo/config": "~11.0.13", @@ -2353,7 +2526,7 @@ "@expo/config-types": "^53.0.5", "@expo/image-utils": "^0.7.6", "@expo/json-file": "^9.1.5", - "@react-native/normalize-colors": "0.79.5", + "@react-native/normalize-colors": "0.79.6", "debug": "^4.3.1", "resolve-from": "^5.0.0", "semver": "^7.6.0", @@ -2372,6 +2545,12 @@ "node": ">=10" } }, + "node_modules/@expo/schema-utils": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.7.tgz", + "integrity": "sha512-jWHoSuwRb5ZczjahrychMJ3GWZu54jK9ulNdh1d4OzAEq672K9E5yOlnlBsfIHWHGzUAT+0CL7Yt1INiXTz68g==", + "license": "MIT" + }, "node_modules/@expo/sdk-runtime-versions": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", @@ -2448,33 +2627,19 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2520,10 +2685,22 @@ "node": ">=12" } }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" @@ -2532,6 +2709,44 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -2561,6 +2776,15 @@ "node": ">=18.0.0" } }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -2778,6 +3002,16 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2804,15 +3038,33 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz", + "integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2913,22 +3165,22 @@ } }, "node_modules/@react-native/babel-plugin-codegen": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.5.tgz", - "integrity": "sha512-Rt/imdfqXihD/sn0xnV4flxxb1aLLjPtMF1QleQjEhJsTUPpH4TFlfOpoCvsrXoDl4OIcB1k4FVM24Ez92zf5w==", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.6.tgz", + "integrity": "sha512-CS5OrgcMPixOyUJ/Sk/HSsKsKgyKT5P7y3CojimOQzWqRZBmoQfxdST4ugj7n1H+ebM2IKqbgovApFbqXsoX0g==", "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.3", - "@react-native/codegen": "0.79.5" + "@react-native/codegen": "0.79.6" }, "engines": { "node": ">=18" } }, "node_modules/@react-native/babel-preset": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.5.tgz", - "integrity": "sha512-GDUYIWslMLbdJHEgKNfrOzXk8EDKxKzbwmBXUugoiSlr6TyepVZsj3GZDLEFarOcTwH1EXXHJsixihk8DCRQDA==", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.6.tgz", + "integrity": "sha512-H+FRO+r2Ql6b5IwfE0E7D52JhkxjeGSBSUpCXAI5zQ60zSBJ54Hwh2bBJOohXWl4J+C7gKYSAd2JHMUETu+c/A==", "license": "MIT", "dependencies": { "@babel/core": "^7.25.2", @@ -2972,7 +3224,7 @@ "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", - "@react-native/babel-plugin-codegen": "0.79.5", + "@react-native/babel-plugin-codegen": "0.79.6", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" @@ -2985,11 +3237,13 @@ } }, "node_modules/@react-native/codegen": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.5.tgz", - "integrity": "sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ==", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.6.tgz", + "integrity": "sha512-iRBX8Lgbqypwnfba7s6opeUwVyaR23mowh9ILw7EcT2oLz3RqMmjJdrbVpWhGSMGq2qkPfqAH7bhO8C7O+xfjQ==", "license": "MIT", "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", @@ -3073,6 +3327,37 @@ } } }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.5.tgz", + "integrity": "sha512-WQ49TRpCwhgUYo5/n+6GGykXmnumpOkl4Lr2l2o2buWU9qPOwoiBqJAtmWEXsAug4ciw3eLiVfthn5ufs0VB0A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.5.tgz", + "integrity": "sha512-U7r9M/SEktOCP/0uS6jXMHmYjj4ESfYCkNAenBjFjjsRWekiHE+U/vRMeO+fG9gq4UCcBAUISClkQCowlftYBw==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.79.5", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@react-native/community-cli-plugin/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3100,23 +3385,32 @@ "node": ">=10" } }, + "node_modules/@react-native/community-cli-plugin/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, "node_modules/@react-native/debugger-frontend": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.5.tgz", - "integrity": "sha512-WQ49TRpCwhgUYo5/n+6GGykXmnumpOkl4Lr2l2o2buWU9qPOwoiBqJAtmWEXsAug4ciw3eLiVfthn5ufs0VB0A==", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.6.tgz", + "integrity": "sha512-lIK/KkaH7ueM22bLO0YNaQwZbT/oeqhaghOvmZacaNVbJR1Cdh/XAqjT8FgCS+7PUnbxA8B55NYNKGZG3O2pYw==", "license": "BSD-3-Clause", "engines": { "node": ">=18" } }, "node_modules/@react-native/dev-middleware": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.5.tgz", - "integrity": "sha512-U7r9M/SEktOCP/0uS6jXMHmYjj4ESfYCkNAenBjFjjsRWekiHE+U/vRMeO+fG9gq4UCcBAUISClkQCowlftYBw==", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.6.tgz", + "integrity": "sha512-BK3GZBa9c7XSNR27EDRtxrgyyA3/mf1j3/y+mPk7Ac0Myu85YNrXnC9g3mL5Ytwo0g58TKrAIgs1fF2Q5Mn6mQ==", "license": "MIT", "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.79.5", + "@react-native/debugger-frontend": "0.79.6", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", @@ -3174,18 +3468,18 @@ } }, "node_modules/@react-native/normalize-colors": { - "version": "0.79.5", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.5.tgz", - "integrity": "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ==", + "version": "0.79.6", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.6.tgz", + "integrity": "sha512-0v2/ruY7eeKun4BeKu+GcfO+SHBdl0LJn4ZFzTzjHdWES0Cn+ONqKljYaIv8p9MV2Hx/kcdEvbY4lWI34jC/mQ==", "license": "MIT" }, "node_modules/@react-navigation/bottom-tabs": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.6.tgz", - "integrity": "sha512-f4khxwcL70O5aKfZFbxyBo5RnzPFnBNSXmrrT7q9CRmvN4mHov9KFKGQ3H4xD5sLonsTBtyjvyvPfyEC4G7f+g==", + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.4.7.tgz", + "integrity": "sha512-SQ4KuYV9yr3SV/thefpLWhAD0CU2CrBMG1l0w/QKl3GYuGWdN5OQmdQdmaPZGtsjjVOb+N9Qo7Tf6210P4TlpA==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.6.3", + "@react-navigation/elements": "^2.6.4", "color": "^4.2.3" }, "peerDependencies": { @@ -3214,16 +3508,10 @@ "react": ">= 18.2.0" } }, - "node_modules/@react-navigation/core/node_modules/react-is": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", - "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", - "license": "MIT" - }, "node_modules/@react-navigation/elements": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.6.3.tgz", - "integrity": "sha512-hcPXssZg5bFD5oKX7FP0D9ZXinRgPUHkUJbTegpenSEUJcPooH1qzWJkEP22GrtO+OPDLYrCVZxEX8FcMrn4pA==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.6.4.tgz", + "integrity": "sha512-O3X9vWXOEhAO56zkQS7KaDzL8BvjlwZ0LGSteKpt1/k6w6HONG+2Wkblrb057iKmehTkEkQMzMLkXiuLmN5x9Q==", "license": "MIT", "dependencies": { "color": "^4.2.3", @@ -3261,12 +3549,12 @@ } }, "node_modules/@react-navigation/native-stack": { - "version": "7.3.25", - "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.3.25.tgz", - "integrity": "sha512-jGcgUpif0dDGwuqag6rKTdS78MiAVAy8vmQppyaAgjS05VbCfDX+xjhc8dUxSClO5CoWlDoby1c8Hw4kBfL2UA==", + "version": "7.3.26", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.3.26.tgz", + "integrity": "sha512-EjaBWzLZ76HJGOOcWCFf+h/M+Zg7M1RalYioDOb6ZdXHz7AwYNidruT3OUAQgSzg3gVLqvu5OYO0jFsNDPCZxQ==", "license": "MIT", "dependencies": { - "@react-navigation/elements": "^2.6.3", + "@react-navigation/elements": "^2.6.4", "warn-once": "^0.1.1" }, "peerDependencies": { @@ -3286,10 +3574,100 @@ "nanoid": "^3.3.11" } }, + "node_modules/@react-three/drei": { + "version": "10.7.6", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.6.tgz", + "integrity": "sha512-ZSFwRlRaa4zjtB7yHO6Q9xQGuyDCzE7whXBhum92JslcMRC3aouivp0rAzszcVymIoJx6PXmibyP+xr+zKdwLg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^3.1.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.4", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/fiber": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.3.0.tgz", + "integrity": "sha512-myPe3YL/C8+Eq939/4qIVEPBW/uxV0iiUbmjfwrs9sGKYDG8ib8Dz3Okq7BQt8P+0k4igedONbjXMQy84aDFmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.32.0", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-reconciler": "^0.31.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.25.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@rnmapbox/maps": { - "version": "10.1.41", - "resolved": "https://registry.npmjs.org/@rnmapbox/maps/-/maps-10.1.41.tgz", - "integrity": "sha512-zDSlPAYp3ZKMkexCXW3/6lf9w5+SxgNk4U0GV7v6Kp3PA7LtUkdC2A/B27iF/8TEFqA53gAtUQrIOCuOtenKTA==", + "version": "10.1.44", + "resolved": "https://registry.npmjs.org/@rnmapbox/maps/-/maps-10.1.44.tgz", + "integrity": "sha512-rcfF1lLZLGwjpPDoxmvYIakKRfcTMH/w0raq+kUynneC8KOSaareGQi7LfYcCiEHhJ9as3CVPqU0hFTwZhv5tw==", "license": "MIT", "dependencies": { "@turf/along": "6.5.0", @@ -3298,7 +3676,7 @@ "@turf/length": "6.5.0", "@turf/nearest-point-on-line": "6.5.0", "@types/geojson": "^7946.0.7", - "debounce": "^1.2.0" + "debounce": "^2.2.0" }, "peerDependencies": { "expo": ">=47.0.0", @@ -9091,10 +9469,16 @@ "url": "https://opencollective.com/turf" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, @@ -9159,6 +9543,12 @@ "integrity": "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==", "license": "MIT" }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -9226,14 +9616,20 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", + "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.12.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -9244,18 +9640,53 @@ "version": "19.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.14.tgz", "integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==", - "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, + "node_modules/@types/react-reconciler": { + "version": "0.32.1", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.32.1.tgz", + "integrity": "sha512-RsqPttsBQ+6af0nATFXJJpemYQH7kL9+xLNm1z+0MjQFDKBZDM2R6SBrjdvRmHu9i9fM6povACj57Ft+pKRNOA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/stats.js": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.180.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.180.0.tgz", + "integrity": "sha512-ykFtgCqNnY0IPvDro7h+9ZeLY+qjgUWv+qEvUt84grhenO60Hqd4hScHE7VTB9nOQ/3QM8lkbNE+4vKjEpUxKg==", + "license": "MIT", + "dependencies": { + "@dimforge/rapier3d-compat": "~0.12.0", + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.22.0" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.23.tgz", + "integrity": "sha512-GPe4AsfOSpqWd3xA/0gwoKod13ChcfV67trvxaW2krUbgb9gxQjnCx8zGshzMl8LSHZlNH5gQ8LNScsDuc7nGQ==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -9272,17 +9703,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", - "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.0.tgz", + "integrity": "sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/type-utils": "8.39.1", - "@typescript-eslint/utils": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/type-utils": "8.44.0", + "@typescript-eslint/utils": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -9296,32 +9727,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.1", + "@typescript-eslint/parser": "^8.44.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", - "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.44.0.tgz", + "integrity": "sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4" }, "engines": { @@ -9337,14 +9758,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", - "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.44.0.tgz", + "integrity": "sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.1", - "@typescript-eslint/types": "^8.39.1", + "@typescript-eslint/tsconfig-utils": "^8.44.0", + "@typescript-eslint/types": "^8.44.0", "debug": "^4.3.4" }, "engines": { @@ -9359,14 +9780,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", - "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.44.0.tgz", + "integrity": "sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1" + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9377,9 +9798,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", - "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.44.0.tgz", + "integrity": "sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==", "dev": true, "license": "MIT", "engines": { @@ -9394,15 +9815,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", - "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.44.0.tgz", + "integrity": "sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/utils": "8.44.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -9419,9 +9840,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", - "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz", + "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==", "dev": true, "license": "MIT", "engines": { @@ -9433,16 +9854,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", - "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.44.0.tgz", + "integrity": "sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.1", - "@typescript-eslint/tsconfig-utils": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/visitor-keys": "8.39.1", + "@typescript-eslint/project-service": "8.44.0", + "@typescript-eslint/tsconfig-utils": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/visitor-keys": "8.44.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -9475,16 +9896,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", - "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.44.0.tgz", + "integrity": "sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.1", - "@typescript-eslint/types": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1" + "@typescript-eslint/scope-manager": "8.44.0", + "@typescript-eslint/types": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9499,13 +9920,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", - "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.44.0.tgz", + "integrity": "sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/types": "8.44.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -9821,10 +10242,34 @@ "@urql/core": "^5.0.0" } }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.65.tgz", + "integrity": "sha512-cYrHab4d6wuVvDW5tdsfI6/o6vcLMDe6w2Citd1oS51Xxu2ycLCnVo4fqwujfKWijrZMInTJIKcXxteoy21nVA==", + "license": "BSD-3-Clause" + }, "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -10208,9 +10653,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -10285,122 +10730,18 @@ "npm": ">=6" } }, - "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "node_modules/babel-plugin-module-resolver": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz", + "integrity": "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==", + "dev": true, "license": "MIT", "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/babel-plugin-macros/node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-plugin-macros/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-plugin-macros/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/babel-plugin-module-resolver": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz", - "integrity": "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-babel-config": "^2.1.1", - "glob": "^9.3.3", - "pkg-up": "^3.1.0", - "reselect": "^4.1.7", - "resolve": "^1.22.8" - } - }, - "node_modules/babel-plugin-module-resolver/node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/babel-plugin-module-resolver/node_modules/minimatch": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", - "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/babel-plugin-module-resolver/node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" + "find-babel-config": "^2.1.1", + "glob": "^9.3.3", + "pkg-up": "^3.1.0", + "reselect": "^4.1.7", + "resolve": "^1.22.8" } }, "node_modules/babel-plugin-polyfill-corejs2": { @@ -10493,9 +10834,9 @@ } }, "node_modules/babel-preset-expo": { - "version": "13.2.3", - "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.3.tgz", - "integrity": "sha512-wQJn92lqj8GKR7Ojg/aW4+GkqI6ZdDNTDyOqhhl7A9bAqk6t0ukUOWLDXQb4p0qKJjMDV1F6gNWasI2KUbuVTQ==", + "version": "13.2.4", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.4.tgz", + "integrity": "sha512-3IKORo3KR+4qtLdCkZNDj8KeA43oBn7RRQejFGWfiZgu/NeaRUSri8YwYjZqybm7hn3nmMv9OLahlvXBX23o5Q==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -10512,7 +10853,7 @@ "@babel/plugin-transform-runtime": "^7.24.7", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", - "@react-native/babel-preset": "0.79.5", + "@react-native/babel-preset": "0.79.6", "babel-plugin-react-native-web": "~0.19.13", "babel-plugin-syntax-hermes-parser": "^0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", @@ -10551,6 +10892,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -10580,6 +10926,15 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", + "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/better-opn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", @@ -10609,6 +10964,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -10676,9 +11040,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", - "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "funding": [ { "type": "opencollective", @@ -10695,9 +11059,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001733", - "electron-to-chromium": "^1.5.199", - "node-releases": "^2.0.19", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -10717,9 +11082,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -10737,7 +11102,7 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-from": { @@ -10816,6 +11181,15 @@ "node": ">=4" } }, + "node_modules/caller-callsite/node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/caller-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", @@ -10829,12 +11203,12 @@ } }, "node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/camelcase": { @@ -10855,10 +11229,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camera-controls": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.0.tgz", + "integrity": "sha512-w5oULNpijgTRH0ARFJJ0R5ct1nUM3R3WP7/b8A6j9uTGpRfnsypc/RBMPQV8JQDPayUe37p/TZZY1PcUr4czOQ==", + "license": "MIT", + "engines": { + "node": ">=20.11.0", + "npm": ">=10.8.2" + }, + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001735", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", - "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "version": "1.0.30001743", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", + "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", "funding": [ { "type": "opencollective", @@ -10985,26 +11372,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -11197,12 +11564,12 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.45.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz", - "integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==", + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", + "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", "license": "MIT", "dependencies": { - "browserslist": "^4.25.1" + "browserslist": "^4.25.3" }, "funding": { "type": "opencollective", @@ -11210,40 +11577,37 @@ } }, "node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "license": "MIT", "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/cosmiconfig/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" + "node": ">=10" } }, - "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "cross-spawn": "^7.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" } }, "node_modules/cross-fetch": { @@ -11291,6 +11655,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", "dependencies": { "hyphenate-style-name": "^1.0.3" } @@ -11308,6 +11673,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -11455,15 +11821,21 @@ } }, "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "license": "MIT" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -11596,6 +11968,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -11703,6 +12084,12 @@ "url": "https://dotenvx.com" } }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -11736,15 +12123,15 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.203", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.203.tgz", - "integrity": "sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g==", + "version": "1.5.222", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", + "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", "license": "ISC" }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, "node_modules/encodeurl": { @@ -11792,9 +12179,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -12010,19 +12397,19 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.36.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -12361,9 +12748,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "17.21.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.3.tgz", - "integrity": "sha512-MtxYjDZhMQgsWRm/4xYLL0i2EhusWT7itDxlJ80l1NND2AL2Vi5Mvneqv/ikG9+zpran0VsVRXTEHrpLmUZRNw==", + "version": "17.23.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.23.1.tgz", + "integrity": "sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A==", "dev": true, "license": "MIT", "dependencies": { @@ -12400,6 +12787,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint-plugin-n/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint-plugin-n/node_modules/semver": { "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", @@ -12445,6 +12842,16 @@ "concat-map": "0.0.1" } }, + "node_modules/eslint-plugin-node/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint-plugin-node/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -12687,6 +13094,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -12815,19 +13232,19 @@ "license": "MIT" }, "node_modules/expo": { - "version": "53.0.20", - "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.20.tgz", - "integrity": "sha512-Nh+HIywVy9KxT/LtH08QcXqrxtUOA9BZhsXn3KCsAYA+kNb80M8VKN8/jfQF+I6CgeKyFKJoPNsWgI0y0VBGrA==", + "version": "53.0.22", + "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.22.tgz", + "integrity": "sha512-sJ2I4W/e5iiM4u/wYCe3qmW4D7WPCRqByPDD0hJcdYNdjc9HFFFdO4OAudZVyC/MmtoWZEIH5kTJP1cw9FjzYA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", - "@expo/cli": "0.24.20", + "@expo/cli": "0.24.21", "@expo/config": "~11.0.13", "@expo/config-plugins": "~10.1.2", "@expo/fingerprint": "0.13.4", "@expo/metro-config": "0.20.17", "@expo/vector-icons": "^14.0.0", - "babel-preset-expo": "~13.2.3", + "babel-preset-expo": "~13.2.4", "expo-asset": "~11.1.7", "expo-constants": "~17.1.7", "expo-file-system": "~18.1.11", @@ -13047,26 +13464,54 @@ "react": "*" } }, - "node_modules/expo-json-utils": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", - "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", - "license": "MIT" - }, - "node_modules/expo-keep-awake": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz", - "integrity": "sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA==", + "node_modules/expo-gl": { + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/expo-gl/-/expo-gl-16.0.7.tgz", + "integrity": "sha512-x4EIMgeXEzv8FZuS2FmoT+qBgdsvHphwleTwPwj+nWat84Al4N9QqLZawTz0AcFZBQLXawrpIXJ0dc+TJsYqSQ==", "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, "peerDependencies": { "expo": "*", - "react": "*" - } - }, - "node_modules/expo-linear-gradient": { - "version": "14.1.5", - "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz", - "integrity": "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==", + "react": "*", + "react-dom": "*", + "react-native": "*", + "react-native-reanimated": "*", + "react-native-web": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native-reanimated": { + "optional": true + }, + "react-native-web": { + "optional": true + } + } + }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" + }, + "node_modules/expo-keep-awake": { + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.1.4.tgz", + "integrity": "sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo-linear-gradient": { + "version": "14.1.5", + "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz", + "integrity": "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==", "license": "MIT", "peerDependencies": { "expo": "*", @@ -13124,6 +13569,35 @@ "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, + "node_modules/expo-modules-autolinking/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/expo-modules-autolinking/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/expo-modules-core": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-2.5.0.tgz", @@ -13133,6 +13607,15 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-speech": { + "version": "14.0.7", + "resolved": "https://registry.npmjs.org/expo-speech/-/expo-speech-14.0.7.tgz", + "integrity": "sha512-Ff/seq4tfj6AnGZj8Mj83EGnHj636f2mO30bPr3qQJzF9zn3ArakuUQTK8UrkVy0OS0E12z28d+9Fe+AgSUB0w==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-status-bar": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.2.3.tgz", @@ -13301,6 +13784,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -13485,6 +13974,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -13736,20 +14237,19 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dev": true, "license": "ISC", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -13768,10 +14268,26 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { - "version": "16.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", - "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -13805,6 +14321,12 @@ "dev": true, "license": "MIT" }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -13935,6 +14457,12 @@ "hermes-estree": "0.25.1" } }, + "node_modules/hls.js": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.12.tgz", + "integrity": "sha512-Pz+7IzvkbAht/zXvwLzA/stUHNqztqKvlLbfpq6ZYU68+gZ+CZMlsbQBPUviRap+3IQ41E39ke7Ia+yvhsehEQ==", + "license": "Apache-2.0" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -14022,7 +14550,8 @@ "node_modules/hyphenate-style-name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", - "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" }, "node_modules/ieee754": { "version": "1.2.1", @@ -14045,9 +14574,10 @@ "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -14068,23 +14598,32 @@ "node": ">=16.x" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "license": "MIT", "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/import-fresh/node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "license": "MIT", "engines": { "node": ">=4" @@ -14470,6 +15009,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -14683,6 +15228,27 @@ "node": ">= 0.4" } }, + "node_modules/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/its-fine/node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -15043,6 +15609,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -15451,6 +16026,16 @@ "react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0" } }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -15509,6 +16094,21 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.22.0.tgz", + "integrity": "sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==", + "license": "MIT" + }, "node_modules/metro": { "version": "0.82.5", "resolved": "https://registry.npmjs.org/metro/-/metro-0.82.5.tgz", @@ -15639,6 +16239,78 @@ "node": ">=18.18" } }, + "node_modules/metro-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/metro-config/node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/metro-config/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/metro-config/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/metro-core": { "version": "0.82.5", "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.82.5.tgz", @@ -15936,12 +16608,13 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, "license": "ISC", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=8" } }, "node_modules/minizlib": { @@ -15956,6 +16629,15 @@ "node": ">= 18" } }, + "node_modules/minizlib/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -16077,9 +16759,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "license": "MIT" }, "node_modules/normalize-path": { @@ -16363,15 +17045,6 @@ "node": ">=6" } }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/ora/node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -16431,18 +17104,6 @@ "node": ">=4" } }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/ora/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -16530,26 +17191,22 @@ "node": ">=6" } }, - "node_modules/parent-module/node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "license": "MIT", "dependencies": { + "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/parse-png": { @@ -16628,6 +17285,15 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -16841,6 +17507,12 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -16918,6 +17590,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, "node_modules/proc-log": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", @@ -16945,6 +17623,16 @@ "asap": "~2.0.6" } }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -17087,6 +17775,15 @@ "rc": "cli.js" } }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -17152,9 +17849,9 @@ } }, "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", + "integrity": "sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==", "license": "MIT" }, "node_modules/react-native": { @@ -17226,6 +17923,25 @@ "react-native": "*" } }, + "node_modules/react-native-fs": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz", + "integrity": "sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==", + "license": "MIT", + "dependencies": { + "base-64": "^0.1.0", + "utf8": "^3.0.0" + }, + "peerDependencies": { + "react-native": "*", + "react-native-windows": "*" + }, + "peerDependenciesMeta": { + "react-native-windows": { + "optional": true + } + } + }, "node_modules/react-native-gesture-handler": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.28.0.tgz", @@ -17252,24 +17968,31 @@ } }, "node_modules/react-native-reanimated": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.14.0.tgz", - "integrity": "sha512-TAxLtCfRyC/nOLeWoX/8MhdIF+Fi1e1NbLhIgEm5Kv9/gioAwSNaqLUYxjIClU1RaLwSTE8iaiHNVhTU4TS9DA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.0.tgz", + "integrity": "sha512-L8FqZn8VjZyBaCUMYFyx1Y+T+ZTbblaudpxReOXJ66RnOf52g6UM4Pa/IjwLD1XAw1FUxLRQrtpdjbkEc74FiQ==", "license": "MIT", "dependencies": { - "@babel/plugin-transform-arrow-functions": "^7.0.0-0", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", - "@babel/plugin-transform-optional-chaining": "^7.0.0-0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", - "@babel/plugin-transform-template-literals": "^7.0.0-0", - "@babel/preset-typescript": "^7.16.7", - "convert-source-map": "^2.0.0", - "invariant": "^2.2.4" + "react-native-is-edge-to-edge": "^1.2.1", + "semver": "7.7.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0", "react": "*", - "react-native": "*" + "react-native": "*", + "react-native-worklets": ">=0.5.0" + } + }, + "node_modules/react-native-reanimated/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/react-native-safe-area-context": { @@ -17297,6 +18020,15 @@ "react-native": "*" } }, + "node_modules/react-native-share": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/react-native-share/-/react-native-share-12.2.0.tgz", + "integrity": "sha512-f6MB9BsKa9xVvu0DKbxq5jw4IyYHqQeqUlCNkD8eAFoJx6SD31ObPAn7SQ6NG9AOuhCy6aYuSJYJvx25DaoMZQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/react-native-svg": { "version": "15.11.2", "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz", @@ -17363,6 +18095,7 @@ "version": "0.20.0", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.20.0.tgz", "integrity": "sha512-OOSgrw+aON6R3hRosCau/xVxdLzbjEcsLysYedka0ZON4ZZe6n9xgeN9ZkoejhARM36oTlUgHIQqxGutEJ9Wxg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", "@react-native/normalize-colors": "^0.74.1", @@ -17381,12 +18114,14 @@ "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { "version": "0.74.89", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz", - "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==" + "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==", + "license": "MIT" }, "node_modules/react-native-web/node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", - "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" }, "node_modules/react-native-webview": { "version": "13.13.5", @@ -17451,6 +18186,31 @@ "node": ">=10" } }, + "node_modules/react-native/node_modules/@react-native/codegen": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.5.tgz", + "integrity": "sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ==", + "license": "MIT", + "dependencies": { + "glob": "^7.1.1", + "hermes-parser": "0.25.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/react-native/node_modules/@react-native/normalize-colors": { + "version": "0.79.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.5.tgz", + "integrity": "sha512-nGXMNMclZgzLUxijQQ38Dm3IAEhgxuySAWQHnljFtfB0JdaMwpe0Ox9H7Tp2OgrEA+EMEv+Od9ElKlHwGKmmvQ==", + "license": "MIT" + }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { "version": "0.79.5", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.5.tgz", @@ -17547,6 +18307,21 @@ "async-limiter": "~1.0.0" } }, + "node_modules/react-reconciler": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz", + "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -17556,6 +18331,21 @@ "node": ">=0.10.0" } }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -17586,9 +18376,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2" @@ -17638,17 +18428,17 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.3.1.tgz", + "integrity": "sha512-DzcswPr252wEr7Qz8AyAVbfyBDKLoYp6eRA1We2Fa9qirRFSdtkP5sHr3yglDKy2BbA0fd2T+j/CUSKes3FeVQ==", "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -17798,12 +18588,6 @@ "node": ">=4" } }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -18342,16 +19126,10 @@ } }, "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" }, "node_modules/simple-plist": { "version": "1.3.1", @@ -18377,18 +19155,18 @@ } }, "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" } }, "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, "node_modules/sisteransi": { @@ -18528,6 +19306,32 @@ "node": ">=6" } }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats-gl/node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", @@ -18576,20 +19380,17 @@ "license": "MIT" }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/string-width-cjs": { @@ -18607,12 +19408,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -18625,6 +19420,18 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -18724,18 +19531,15 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^4.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=6" } }, "node_modules/strip-ansi-cjs": { @@ -18752,15 +19556,12 @@ } }, "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=6" } }, "node_modules/strip-bom": { @@ -18774,12 +19575,16 @@ } }, "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/structured-headers": { @@ -18831,6 +19636,35 @@ "node": ">= 6" } }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -18868,6 +19702,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, "node_modules/sweepline-intersections": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/sweepline-intersections/-/sweepline-intersections-1.5.0.tgz", @@ -18894,13 +19737,17 @@ } }, "node_modules/tapable": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", - "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tar": { @@ -18920,6 +19767,15 @@ "node": ">=18" } }, + "node_modules/tar/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/tar/node_modules/mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", @@ -18970,13 +19826,13 @@ } }, "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "version": "5.44.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", + "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -19080,6 +19936,44 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.180.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", + "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", + "license": "MIT" + }, + "node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.0.tgz", + "integrity": "sha512-kv0Byb++AXztEGsULgMAs8U2jgUdz6HPpAB/wDJnLiLlaWQX2APHhiTJIN7rqW+Of0eRgcp7jn05U1BsCP3xBA==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, "node_modules/throat": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", @@ -19087,14 +19981,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -19193,6 +20087,36 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/troika-three-text": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", + "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.4", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.4", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", + "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -19280,6 +20204,43 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -19404,16 +20365,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.39.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz", - "integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==", + "version": "8.44.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.44.0.tgz", + "integrity": "sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.39.1", - "@typescript-eslint/parser": "8.39.1", - "@typescript-eslint/typescript-estree": "8.39.1", - "@typescript-eslint/utils": "8.39.1" + "@typescript-eslint/eslint-plugin": "8.44.0", + "@typescript-eslint/parser": "8.44.0", + "@typescript-eslint/typescript-estree": "8.44.0", + "@typescript-eslint/utils": "8.44.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -19428,9 +20389,9 @@ } }, "node_modules/ua-parser-js": { - "version": "1.0.40", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", - "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", "funding": [ { "type": "opencollective", @@ -19482,9 +20443,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", + "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -19510,18 +20471,18 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "license": "MIT", "engines": { "node": ">=4" @@ -19640,6 +20601,21 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -19715,14 +20691,22 @@ "defaults": "^1.0.3" } }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, "node_modules/webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=8" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/whatwg-fetch": { "version": "3.6.20", @@ -19754,11 +20738,38 @@ "node": ">=10" } }, - "node_modules/whatwg-url/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" + "node_modules/whatwg-url-without-unicode/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } }, "node_modules/which": { "version": "2.0.2", @@ -19915,26 +20926,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -19947,26 +20938,6 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -19998,12 +20969,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/write-file-atomic/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -20120,38 +21085,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index e8ec268..b5d56dc 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "@react-navigation/bottom-tabs": "^7.4.6", "@react-navigation/native": "^7.1.17", "@react-navigation/native-stack": "^7.3.25", + "@react-three/drei": "^10.7.6", + "@react-three/fiber": "^9.3.0", "@rnmapbox/maps": "^10.1.41", "@turf/turf": "^7.2.0", "axios": "^1.11.0", @@ -29,18 +31,22 @@ "expo-crypto": "^14.1.5", "expo-dev-client": "~5.2.4", "expo-file-system": "~18.1.11", + "expo-gl": "^16.0.7", "expo-linear-gradient": "~14.1.5", "expo-location": "^18.1.6", "expo-media-library": "~17.1.7", + "expo-speech": "^14.0.7", "expo-status-bar": "~2.2.3", "lucide-react-native": "^0.540.0", "react": "19.0.0", "react-dom": "19.0.0", "react-native": "0.79.5", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.28.0", - "react-native-reanimated": "~3.14.0", + "react-native-reanimated": "^4.1.0", "react-native-safe-area-context": "5.4.0", "react-native-screens": "~4.11.1", + "react-native-share": "^12.2.0", "react-native-svg": "15.11.2", "react-native-toast-message": "^2.3.3", "react-native-view-shot": "^4.0.3", @@ -49,6 +55,7 @@ "react-native-webview": "13.13.5", "react-native-worklets": "^0.5.1", "react-native-worklets-core": "^1.6.2", + "three": "^0.180.0", "zustand": "^5.0.8" }, "devDependencies": { diff --git a/src/assets/runova logo.png b/src/assets/runova logo.png new file mode 100644 index 0000000..1b6656f Binary files /dev/null and b/src/assets/runova logo.png differ diff --git a/src/components/Camera.tsx b/src/components/Camera.tsx new file mode 100644 index 0000000..3aec1b5 --- /dev/null +++ b/src/components/Camera.tsx @@ -0,0 +1,305 @@ +import React, { useRef, useState, useEffect } from 'react'; +import { + View, + TouchableOpacity, + Alert, + ActivityIndicator, + PermissionsAndroid, + Platform, + Linking, + Dimensions, +} from 'react-native'; +import { + Camera, + useCameraDevice, + useCameraPermission, + useMicrophonePermission, + useCameraFormat, +} from 'react-native-vision-camera'; +import { X, Camera as CameraIcon, RotateCcw } from 'lucide-react-native'; +import styled from '@emotion/native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +const { width: screenWidth } = Dimensions.get('window'); + +interface CameraComponentProps { + onPhotoTaken: (photoUri: string) => void; + onClose: () => void; +} + +export default function CameraComponent({ + onPhotoTaken, + onClose, +}: CameraComponentProps) { + const camera = useRef(null); + const insets = useSafeAreaInsets(); + const [isCapturing, setIsCapturing] = useState(false); + const [isReady, setIsReady] = useState(false); + const [currentCameraPosition, setCurrentCameraPosition] = useState< + 'back' | 'front' + >('back'); + + const device = useCameraDevice(currentCameraPosition); + + const format = useCameraFormat(device, [ + { photoAspectRatio: 1 }, + { photoResolution: 'max' }, + ]); + + const { hasPermission: hasCameraPermission } = useCameraPermission(); + const { hasPermission: hasMicrophonePermission } = useMicrophonePermission(); + + const requestNativeCameraPermission = async () => { + if (Platform.OS === 'android') { + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.CAMERA, + { + title: '카메라 권한', + message: '사진 촬영을 위해 카메라 권한이 필요합니다.', + buttonNeutral: '나중에', + buttonNegative: '취소', + buttonPositive: '확인', + }, + ); + + if (granted === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) { + Alert.alert( + '카메라 권한 필요', + '카메라 권한이 거부되었습니다. 설정 > 앱 > Runova > 권한에서 카메라 권한을 허용해주세요.', + [ + { text: '취소', style: 'cancel' }, + { + text: '설정으로 이동', + onPress: () => { + Linking.openSettings(); + }, + }, + ], + ); + return false; + } + + return granted === PermissionsAndroid.RESULTS.GRANTED; + } catch (error) { + return false; + } + } else { + try { + const permission = await Camera.requestCameraPermission(); + + if (permission === 'denied') { + Alert.alert( + '카메라 권한 필요', + '카메라 권한이 거부되었습니다. 설정 > 개인정보 보호 및 보안 > 카메라에서 Runova 앱의 카메라 권한을 허용해주세요.', + [ + { text: '취소', style: 'cancel' }, + { + text: '설정으로 이동', + onPress: () => { + Linking.openSettings(); + }, + }, + ], + ); + } + + return permission === 'granted'; + } catch (error) { + return false; + } + } + }; + + useEffect(() => { + const initializeCamera = async () => { + if (!hasCameraPermission) { + const granted = await requestNativeCameraPermission(); + + if (!granted) { + Alert.alert('권한 필요', '카메라 권한이 필요합니다.', [ + { text: '확인', onPress: onClose }, + ]); + return; + } + } + + if (!hasMicrophonePermission) { + await Camera.requestMicrophonePermission(); + } + + if (!device) { + Alert.alert( + '카메라 오류', + '카메라를 찾을 수 없습니다. 앱을 재시작해주세요.', + [{ text: '확인', onPress: onClose }], + ); + return; + } + + setIsReady(true); + }; + + initializeCamera(); + }, [ + hasCameraPermission, + hasMicrophonePermission, + onClose, + device, + currentCameraPosition, + ]); + + const toggleCamera = () => { + const newPosition = currentCameraPosition === 'back' ? 'front' : 'back'; + + setCurrentCameraPosition(newPosition); + }; + + const takePhoto = async () => { + if (!camera.current || isCapturing) return; + + try { + setIsCapturing(true); + + const photo = await camera.current.takePhoto({ + flash: 'off', + }); + + const photoUri = `file://${photo.path}`; + + onPhotoTaken(photoUri); + } catch (error) { + Alert.alert('오류', '사진 촬영에 실패했습니다.'); + } finally { + setIsCapturing(false); + } + }; + + if (!isReady || !device) { + return ( + + + + 카메라를 준비하는 중... + + + ); + } + + return ( + + + + + + + + + + + + + + + + {isCapturing ? ( + + ) : ( + + )} + + + + + + ); +} + +const Container = styled.View({ + flex: 1, + backgroundColor: '#000000', +}); + +const LoadingContainer = styled.View({ + flex: 1, + justifyContent: 'center', + alignItems: 'center', +}); + +const LoadingText = styled.Text({ + color: '#ffffff', + fontSize: 16, + marginTop: 16, +}); + +const CloseButtonContainer = styled.View<{ safeAreaTop: number }>( + ({ safeAreaTop }) => ({ + position: 'absolute', + top: safeAreaTop + 20, + left: 20, + zIndex: 1, + }), +); + +const CloseButton = styled(TouchableOpacity)({ + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + justifyContent: 'center', + alignItems: 'center', +}); + +const BottomContainer = styled.View<{ safeAreaBottom: number }>( + ({ safeAreaBottom }) => ({ + position: 'absolute', + bottom: safeAreaBottom + 40, + left: 0, + right: 0, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 50, + }), +); + +const ToggleButton = styled(TouchableOpacity)({ + width: 50, + height: 50, + borderRadius: 25, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + justifyContent: 'center', + alignItems: 'center', +}); + +const CaptureButton = styled(TouchableOpacity)({ + width: 70, + height: 70, + borderRadius: 35, + backgroundColor: '#ffffff', + justifyContent: 'center', + alignItems: 'center', + borderWidth: 3, + borderColor: '#ffffff', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 5, +}); + +const PlaceholderView = styled.View({ + width: 50, + height: 50, +}); diff --git a/src/components/Header.tsx b/src/components/Header.tsx index cfd57f8..b11dc98 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -31,17 +31,12 @@ export default function Header({ if (isHome) { return ( - - - - {locationText} - - - - - - - + + + + {locationText} + + ); } @@ -74,7 +69,7 @@ const HeaderContainer = styled.View<{ isHome: boolean; safeAreaTop: number }>( flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', - paddingTop: safeAreaTop, + paddingTop: isHome ? safeAreaTop + 8 : safeAreaTop, paddingBottom: 12, paddingHorizontal: 16, backgroundColor: 'transparent', @@ -102,12 +97,14 @@ const RightSection = styled.View({ const LocationContainer = styled.TouchableOpacity({ flexDirection: 'row', alignItems: 'center', + flex: 1, }); const LocationText = styled.Text({ color: '#ffffff', fontSize: 14, marginLeft: 4, + flex: 1, }); const Title = styled.Text({ diff --git a/src/components/StarryBackground.tsx b/src/components/StarryBackground.tsx new file mode 100644 index 0000000..ffa6562 --- /dev/null +++ b/src/components/StarryBackground.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from 'react'; +import { View, Animated, Dimensions } from 'react-native'; +import styled from '@emotion/native'; + +interface Star { + id: number; + x: number; + y: number; + size: number; + animatedValue: Animated.Value; +} + +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +const Container = styled(View)({ + position: 'absolute', + top: 0, + left: 0, + width: screenWidth, + height: screenHeight, + zIndex: 1, +}); + +const StarDot = styled(Animated.View)<{ size: number; x: number; y: number }>( + ({ size, x, y }) => ({ + position: 'absolute', + left: x, + top: y, + width: size, + height: size, + backgroundColor: 'white', + borderRadius: size / 2, + }), +); + +export default function StarryBackground() { + const [stars, setStars] = useState([]); + + useEffect(() => { + const starCount = Math.floor(Math.random() * 100) + 200; + const newStars: Star[] = []; + + for (let i = 0; i < starCount; i++) { + let x, y; + + if (Math.random() < 0.3) { + const lineProgress = Math.random(); + const lineWidth = 150; + const centerX = lineProgress * screenWidth; + const centerY = lineProgress * screenHeight * 0.7 + screenHeight * 0.15; + + x = centerX + (Math.random() - 0.5) * lineWidth; + y = centerY + (Math.random() - 0.5) * lineWidth * 0.3; + } else { + x = Math.random() * screenWidth; + y = Math.random() * screenHeight; + } + + newStars.push({ + id: i, + x: Math.max(0, Math.min(screenWidth, x)), + y: Math.max(0, Math.min(screenHeight, y)), + size: Math.random() * 1.7 + 0.3, + animatedValue: new Animated.Value(Math.random() * 0.6 + 0.3), + }); + } + + setStars(newStars); + + newStars.forEach((star) => { + const createTwinkleAnimation = () => { + const minOpacity = Math.random() * 0.2 + 0.1; + const maxOpacity = Math.random() * 0.4 + 0.6; + + return Animated.sequence([ + Animated.timing(star.animatedValue, { + toValue: maxOpacity, + duration: Math.random() * 3000 + 2000, + useNativeDriver: true, + }), + Animated.timing(star.animatedValue, { + toValue: minOpacity, + duration: Math.random() * 3000 + 2000, + useNativeDriver: true, + }), + ]); + }; + + const startAnimation = () => { + createTwinkleAnimation().start(() => { + setTimeout(() => { + startAnimation(); + }, Math.random() * 2000); + }); + }; + + setTimeout(() => { + startAnimation(); + }, Math.random() * 5000); + }); + + return () => { + newStars.forEach((star) => { + star.animatedValue.stopAnimation(); + }); + }; + }, []); + + return ( + + {stars.map((star) => ( + + ))} + + ); +} diff --git a/src/constants/location.ts b/src/constants/location.ts index 9378e85..c29f8af 100644 --- a/src/constants/location.ts +++ b/src/constants/location.ts @@ -1,4 +1,4 @@ -export const LOCATION_UPDATE_INTERVAL_MS = 1000; -export const LOCATION_DISTANCE_INTERVAL_M = 10; -export const MIN_DISTANCE_THRESHOLD_M = 10; +export const LOCATION_UPDATE_INTERVAL_MS = 1500; +export const LOCATION_DISTANCE_INTERVAL_M = 5; +export const MIN_DISTANCE_THRESHOLD_M = 5; export const INITIAL_ZOOM_LEVEL = 14; diff --git a/src/hooks/api/useRecordsApi.ts b/src/hooks/api/useRecordsApi.ts index f9e02f1..bd0a428 100644 --- a/src/hooks/api/useRecordsApi.ts +++ b/src/hooks/api/useRecordsApi.ts @@ -25,14 +25,7 @@ export function useRunningRecords() { async (params: RunningRecordsRequest, reset = false) => { if (!accessToken) return; - console.log('📊 [useRunningRecords] loadRecords 호출:', { - params, - reset, - loading, - }); - if (loading) { - console.log('📊 [useRunningRecords] 이미 로딩 중이므로 스킵'); return; } @@ -50,11 +43,8 @@ export function useRunningRecords() { requestParams.cursor = params.cursor; } - console.log('📊 [useRunningRecords] API 요청 파라미터:', requestParams); const response = await searchRunningRecords(requestParams, accessToken); - console.log('📊 [useRunningRecords] API 응답:', response); - if (reset) { setRecords(response.results); } else { @@ -66,13 +56,6 @@ export function useRunningRecords() { const hasMoreData = response.nextCursor && response.nextCursor.id !== lastResultId; - console.log('📊 [useRunningRecords] 데이터 종료 조건 확인:', { - lastResultId, - nextCursor: response.nextCursor, - hasMoreData, - resultsLength: response.results.length, - }); - if (hasMoreData) { setCursor(response.nextCursor); setHasMore(true); @@ -81,7 +64,6 @@ export function useRunningRecords() { setHasMore(false); } } catch (error: unknown) { - console.error('📊 [useRunningRecords] API 오류:', error); let errorMessage = '런닝 기록을 불러오는데 실패했습니다.'; if (error && typeof error === 'object' && 'response' in error) { @@ -110,12 +92,6 @@ export function useRunningRecords() { const handleLoadMore = useCallback(() => { if (hasMore && !loading && !error && cursor) { - console.log('📊 [useRunningRecords] handleLoadMore 호출:', { - hasMore, - loading, - error, - cursor, - }); loadRecords({ cursor }, false); } }, [hasMore, loading, error, cursor, loadRecords]); @@ -147,13 +123,7 @@ export function useRunningDashboard() { async (params: RunningDashboardRequest) => { if (!accessToken) return; - console.log('📊 [useRunningDashboard] loadDashboard 호출:', { - params, - loading, - }); - if (loading) { - console.log('📊 [useRunningDashboard] 이미 로딩 중이므로 스킵'); return; } @@ -162,10 +132,9 @@ export function useRunningDashboard() { try { const response = await getRunningDashboard(params, accessToken); - console.log('📊 [useRunningDashboard] API 응답:', response); + setDashboard(response); } catch (error: unknown) { - console.error('📊 [useRunningDashboard] API 오류:', error); let errorMessage = '런닝 통계를 불러오는데 실패했습니다.'; if (error && typeof error === 'object' && 'response' in error) { diff --git a/src/hooks/useImageUpload.ts b/src/hooks/useImageUpload.ts index 230de84..b5c3cfd 100644 --- a/src/hooks/useImageUpload.ts +++ b/src/hooks/useImageUpload.ts @@ -48,8 +48,6 @@ export function useImageUpload() { presignData.key, ); - console.log('📤 [useImageUpload] publicImageUrl:', publicImageUrl); - return publicImageUrl; }; diff --git a/src/hooks/useInitialLocation.ts b/src/hooks/useInitialLocation.ts index 7fbc233..16c951b 100644 --- a/src/hooks/useInitialLocation.ts +++ b/src/hooks/useInitialLocation.ts @@ -41,29 +41,22 @@ export function useInitialLocation(options: UseInitialLocationOptions = {}) { let fetchedLocation: Location.LocationObject | null = null; try { // 무조건 현재 위치를 가져오려고 시도 - console.log('📍 현재 위치 요청 중...'); + fetchedLocation = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High, }); - console.log('📍 현재 위치 획득 성공:', fetchedLocation.coords); } catch (error) { - console.log('📍 현재 위치 실패, 마지막 알려진 위치 시도...'); try { // 현재 위치 실패 시 마지막 알려진 위치 시도 fetchedLocation = await Location.getLastKnownPositionAsync({}); if (fetchedLocation) { - console.log('📍 마지막 알려진 위치 획득:', fetchedLocation.coords); } - } catch (lastKnownError) { - console.log('📍 모든 위치 시도 실패'); - } + } catch (lastKnownError) {} } if (fetchedLocation) { setLocation(fetchedLocation); - console.log('📍 위치 설정 완료'); } else { - console.log('📍 위치 정보 없음, 기본값 사용'); } setLoading(false); })(); diff --git a/src/hooks/useLocationManager.ts b/src/hooks/useLocationManager.ts index f1118fa..6e08519 100644 --- a/src/hooks/useLocationManager.ts +++ b/src/hooks/useLocationManager.ts @@ -17,10 +17,6 @@ export function useLocationManager() { initialLocation.coords.longitude, initialLocation.coords.latitude, ]; - console.log( - '📍 useLocationManager: 위치 업데이트됨', - currentUserLocation.current, - ); } }, [initialLocation]); diff --git a/src/hooks/useLocationTracking.ts b/src/hooks/useLocationTracking.ts index ec60d11..b1cc51b 100644 --- a/src/hooks/useLocationTracking.ts +++ b/src/hooks/useLocationTracking.ts @@ -44,15 +44,9 @@ export function useLocationTracking() { const currentCoords = useRunStore.getState().routeCoordinates; if (currentCoords.length === 0 && location) { const { latitude, longitude } = location.coords; - console.log('📍 [LocationTracking] 위치 추적 시작 - 초기 위치 설정:', { - latitude, - longitude, - }); + setRouteCoordinates([[longitude, latitude]]); } else if (currentCoords.length > 0) { - console.log('📍 [LocationTracking] 위치 추적 재시작 - 기존 경로 유지:', { - coordinatesCount: currentCoords.length, - }); } const newSubscriber = await Location.watchPositionAsync( @@ -96,7 +90,6 @@ export function useLocationTracking() { ]); const pauseTracking = useCallback(() => { - console.log('⏸️ [LocationTracking] 위치 추적 일시정지'); if (subscriber) { subscriber.remove(); setSubscriber(null); @@ -105,7 +98,6 @@ export function useLocationTracking() { }, [subscriber, setSubscriber, setIsTracking]); const stopTracking = useCallback(() => { - console.log('⏹️ [LocationTracking] 위치 추적 완전 종료 - 경로 초기화됨'); if (subscriber) { subscriber.remove(); setSubscriber(null); diff --git a/src/hooks/useMapGestures.ts b/src/hooks/useMapGestures.ts index 1c44991..7bdcefc 100644 --- a/src/hooks/useMapGestures.ts +++ b/src/hooks/useMapGestures.ts @@ -2,6 +2,7 @@ import { RefObject, useRef } from 'react'; import { Alert } from 'react-native'; import Mapbox from '@rnmapbox/maps'; import { Gesture } from 'react-native-gesture-handler'; +import { runOnJS } from 'react-native-reanimated'; import { getMatchedRoute } from '@/lib/mapMatching'; import { findClosestRouteIndex } from '@/utils/draw'; import { ERASE_DISTANCE_THRESHOLD } from '@/constants/draw'; @@ -22,6 +23,72 @@ export function useMapGestures(mapRef: RefObject) { const pointsInCurrentGesture = useRef([]); const updateQueued = useRef(false); + // gesture 시작 시 초기화를 위한 JS 함수 + const initializeGesture = () => { + pointsInCurrentGesture.current = []; + setDrawnCoordinates([]); + }; + + // gesture 완료 시 처리를 위한 JS 함수 + const finalizeGesture = () => { + const finalPoints = [...pointsInCurrentGesture.current]; + pointsInCurrentGesture.current = []; + + if (drawMode === 'draw' && finalPoints.length > 1) { + addCompletedDrawing(finalPoints); + handleMatchRoute(finalPoints); + } else { + setDrawnCoordinates([]); + } + }; + + // JS 스레드에서 실행될 coordinate 변환 및 업데이트 함수 + const handleCoordinateUpdate = async (x: number, y: number) => { + if (!mapRef.current) { + return; + } + + try { + const coord = await mapRef.current.getCoordinateFromView([x, y]); + + if (!coord || !Array.isArray(coord) || coord.length !== 2) { + return; + } + + // 새로운 배열을 생성하여 immutability 문제 해결 + const newPoints = [...pointsInCurrentGesture.current, coord]; + pointsInCurrentGesture.current = newPoints; + + if (!updateQueued.current) { + updateQueued.current = true; + requestAnimationFrame(() => { + setDrawnCoordinates([...pointsInCurrentGesture.current]); + updateQueued.current = false; + }); + } + } catch (error) { + console.warn('Coordinate conversion error:', error); + } + }; + + // erase 모드용 coordinate 변환 함수 + const handleTapCoordinate = async (x: number, y: number) => { + if (!mapRef.current) return; + + try { + const tappedCoord = await mapRef.current.getCoordinateFromView([x, y]); + const { closestRouteIndex, minDistance } = findClosestRouteIndex( + tappedCoord, + matchedRoutes, + ); + if (closestRouteIndex !== -1 && minDistance < ERASE_DISTANCE_THRESHOLD) { + eraseRouteByIndex(closestRouteIndex); + } + } catch (error) { + console.warn('Coordinate conversion error:', error); + } + }; + const handleMatchRoute = async (coordinates: Position[]) => { if (coordinates.length < 2) return; setIsLoading(true); @@ -42,64 +109,28 @@ export function useMapGestures(mapRef: RefObject) { const panGesture = Gesture.Pan() .enabled(drawMode === 'draw') .onBegin(() => { - pointsInCurrentGesture.current = []; - setDrawnCoordinates([]); + // ref 수정을 JS 스레드에서 실행 + runOnJS(initializeGesture)(); }) - .onUpdate(async (event) => { - if (drawMode !== 'draw' || !mapRef.current) return; - - try { - const newCoord = await mapRef.current.getCoordinateFromView([ - event.x, - event.y, - ]); - pointsInCurrentGesture.current.push(newCoord); - - if (!updateQueued.current) { - updateQueued.current = true; - requestAnimationFrame(() => { - setDrawnCoordinates([...pointsInCurrentGesture.current]); - updateQueued.current = false; - }); - } - } catch (error) { - console.warn('Coordinate conversion error:', error); - } + .onUpdate((event) => { + if (drawMode !== 'draw') return; + + // coordinate 변환과 업데이트를 JS 스레드에서 실행 + runOnJS(handleCoordinateUpdate)(event.x, event.y); }) .onEnd(() => { - const finalPoints = pointsInCurrentGesture.current; - if (drawMode === 'draw' && finalPoints.length > 1) { - addCompletedDrawing(finalPoints); - handleMatchRoute(finalPoints); - } else { - setDrawnCoordinates([]); - } - pointsInCurrentGesture.current = []; + // gesture 완료 처리를 JS 스레드에서 실행 + runOnJS(finalizeGesture)(); }) .minDistance(1); const tapGesture = Gesture.Tap() .enabled(drawMode === 'erase') - .onEnd(async (event) => { - if (drawMode !== 'erase' || !mapRef.current) return; - try { - const tappedCoord = await mapRef.current.getCoordinateFromView([ - event.x, - event.y, - ]); - const { closestRouteIndex, minDistance } = findClosestRouteIndex( - tappedCoord, - matchedRoutes, - ); - if ( - closestRouteIndex !== -1 && - minDistance < ERASE_DISTANCE_THRESHOLD - ) { - eraseRouteByIndex(closestRouteIndex); - } - } catch (error) { - console.warn('Coordinate conversion error:', error); - } + .onEnd((event) => { + if (drawMode !== 'erase') return; + + // tap coordinate 처리를 JS 스레드에서 실행 + runOnJS(handleTapCoordinate)(event.x, event.y); }); const composedGesture = Gesture.Race(panGesture, tapGesture); diff --git a/src/hooks/useNavigation.ts b/src/hooks/useNavigation.ts new file mode 100644 index 0000000..c114a2f --- /dev/null +++ b/src/hooks/useNavigation.ts @@ -0,0 +1,221 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import * as Speech from 'expo-speech'; +import type { Position } from 'geojson'; +import type { + CourseTopologyNode, + NavigationState, + NavigationInstruction, +} from '@/types/courses.types'; +import { + findNearestNode, + getNextTurnInfo, + shouldShowTurnWarning, + generateNavigationMessage, +} from '@/utils/navigation'; + +interface UseNavigationProps { + nodes: CourseTopologyNode[]; + routeCoordinates: Position[]; + isTracking: boolean; + enabled: boolean; +} + +interface UseNavigationReturn { + navigationState: NavigationState; + navigationMessage: string | null; + shouldShowNavigation: boolean; + resetNavigation: () => void; +} + +export function useNavigation({ + nodes, + routeCoordinates, + isTracking, + enabled, +}: UseNavigationProps): UseNavigationReturn { + const [navigationState, setNavigationState] = useState({ + currentNodeIndex: -1, + nextNode: null, + distanceToNextNode: 0, + isApproachingTurn: false, + instruction: null, + }); + + const [navigationMessage, setNavigationMessage] = useState( + null, + ); + + // 이전 메시지와 시간을 추적하기 위한 ref + const lastMessageRef = useRef(null); + const lastAnnouncementTime = useRef(0); + const lastSpeechTime = useRef(0); + const lastValidatedCoordinateRef = useRef(null); + + const resetNavigation = useCallback(() => { + setNavigationState({ + currentNodeIndex: -1, + nextNode: null, + distanceToNextNode: 0, + isApproachingTurn: false, + instruction: null, + }); + setNavigationMessage(null); + lastMessageRef.current = null; + lastAnnouncementTime.current = 0; + lastSpeechTime.current = 0; + lastValidatedCoordinateRef.current = null; + }, []); + + // 실시간 네비게이션 검증 (트래킹 중이고 좌표가 실제로 이동했을 때만) + const validateNavigation = useCallback(() => { + if ( + !enabled || + !isTracking || + nodes.length === 0 || + routeCoordinates.length === 0 + ) { + return; + } + + // routeCoordinates의 마지막 위치를 사용 (실제 이동한 위치) + const lastCoordinate = routeCoordinates[routeCoordinates.length - 1]; + const currentPosition: Position = [lastCoordinate[0], lastCoordinate[1]]; + + // 이전에 검증한 좌표와 같은지 확인 + const lastValidated = lastValidatedCoordinateRef.current; + if ( + lastValidated && + Math.abs(lastValidated[0] - currentPosition[0]) < 0.000001 && + Math.abs(lastValidated[1] - currentPosition[1]) < 0.000001 + ) { + // 좌표가 거의 변하지 않았으면 검증하지 않음 + return; + } + + // 현재 좌표를 저장 + lastValidatedCoordinateRef.current = currentPosition; + + // 현재 위치에서 가장 가까운 노드 찾기 + const { nodeIndex: nearestNodeIndex } = findNearestNode( + [currentPosition[0], currentPosition[1]], + nodes, + ); + + // 다음 회전 정보 가져오기 + const { nextNodeIndex, distance, instruction } = getNextTurnInfo( + [currentPosition[0], currentPosition[1]], + nearestNodeIndex, + nodes, + ); + + const nextNode = nextNodeIndex !== null ? nodes[nextNodeIndex] : null; + const isApproachingTurn = + instruction !== null && shouldShowTurnWarning(distance, instruction); + + // 네비게이션 상태 업데이트 + setNavigationState({ + currentNodeIndex: nearestNodeIndex, + nextNode, + distanceToNextNode: distance, + isApproachingTurn, + instruction, + }); + + // 메시지 생성 및 표시 + if (isApproachingTurn && instruction) { + const message = generateNavigationMessage(instruction, distance); + + // 같은 메시지가 아니거나 5초가 지났을 때만 업데이트 + const now = Date.now(); + if ( + lastMessageRef.current !== message || + now - lastAnnouncementTime.current > 5000 + ) { + setNavigationMessage(message); + lastMessageRef.current = message; + lastAnnouncementTime.current = now; + + // TTS 음성 안내 (3초에 한 번만) + if (now - lastSpeechTime.current > 3000) { + Speech.speak(message, { + language: 'ko-KR', + pitch: 1.0, + rate: 0.8, + }); + lastSpeechTime.current = now; + } + } + } else { + // 회전점에서 멀어지면 메시지 숨김 + if (navigationMessage) { + setNavigationMessage(null); + lastMessageRef.current = null; + } + } + }, [enabled, isTracking, nodes, routeCoordinates, navigationMessage]); + + // routeCoordinates 변경 시에만 네비게이션 검증 실행 (트래킹 중일 때만) + useEffect(() => { + if ( + !enabled || + !isTracking || + nodes.length === 0 || + routeCoordinates.length === 0 + ) { + return; + } + + // routeCoordinates가 변경될 때만 검증 실행 + validateNavigation(); + }, [enabled, isTracking, nodes, routeCoordinates, validateNavigation]); + + // 코스 변경 시 네비게이션 초기화 및 즉시 검증 실행 (트래킹 중일 때만) + useEffect(() => { + if (enabled && nodes.length > 0) { + // 이전 좌표 ref 초기화 + lastValidatedCoordinateRef.current = null; + // 코스 토폴로지가 로드된 후 즉시 검증 실행 (트래킹 중이고 좌표가 있을 때만) + if (isTracking && routeCoordinates.length > 0) { + validateNavigation(); + } + } + }, [enabled, nodes, isTracking, routeCoordinates, validateNavigation]); + + // 트래킹 중단 시 TTS 중단 및 메시지 초기화 + useEffect(() => { + if (!isTracking) { + Speech.stop(); + setNavigationMessage(null); + lastMessageRef.current = null; + } + }, [isTracking]); + + // 네비게이션 비활성화 시 TTS 중단 및 메시지 초기화 + useEffect(() => { + if (!enabled) { + Speech.stop(); + setNavigationMessage(null); + lastMessageRef.current = null; + } + }, [enabled]); + + // 컴포넌트 언마운트 시 TTS 정리 + useEffect(() => { + return () => { + Speech.stop(); + }; + }, []); + + const shouldShowNavigation = + enabled && + isTracking && + navigationMessage !== null && + navigationState.instruction !== null; + + return { + navigationState, + navigationMessage, + shouldShowNavigation, + resetNavigation, + }; +} diff --git a/src/hooks/usePhotoDecoration.ts b/src/hooks/usePhotoDecoration.ts new file mode 100644 index 0000000..649a050 --- /dev/null +++ b/src/hooks/usePhotoDecoration.ts @@ -0,0 +1,274 @@ +import { useState, useRef, useEffect } from 'react'; +import { Alert, Animated, PanResponder } from 'react-native'; +import * as MediaLibrary from 'expo-media-library'; +import Share from 'react-native-share'; +import ViewShot from 'react-native-view-shot'; + +import { + PhotoDecorationState, + PanResponderConfig, + RunningStats, +} from '@/types/photoDecoration.types'; +import { + convertPathToSvgData, + formatRunningStats, + createShareOptions, +} from '@/utils/photoDecorationUtils'; + +interface UsePhotoDecorationOptions { + path?: [number, number][]; + routeStats: any; + recordId: number; + onNavigateToRunDetail: (data: any) => void; +} + +export function usePhotoDecoration({ + path, + routeStats, + recordId, + onNavigateToRunDetail, +}: UsePhotoDecorationOptions) { + const viewShotRef = useRef(null); + + // 기본 상태 - 개선된 구조 + const [state, setState] = useState({ + backgroundType: 'photo', + backgroundColor: '#000000', + colors: { + route: '#00FF00', + stats: '#ffffff', + timestamp: '#ffffff', + }, + visibility: { + time: true, + distance: true, + pace: true, + calories: true, + route: true, + timestamp: true, + }, + scales: { + stats: 1, + route: 1, + timestamp: 1, + }, + routePathData: '', + isProcessing: false, + }); + + // 애니메이션 관련 refs + const statsTranslateX = useRef(new Animated.Value(0)).current; + const statsTranslateY = useRef(new Animated.Value(0)).current; + const statsScaleAnimated = useRef(new Animated.Value(1)).current; + const routeTranslateX = useRef(new Animated.Value(0)).current; + const routeTranslateY = useRef(new Animated.Value(0)).current; + const routeScaleAnimated = useRef(new Animated.Value(1)).current; + const timestampTranslateX = useRef(new Animated.Value(0)).current; + const timestampTranslateY = useRef(new Animated.Value(0)).current; + const timestampScaleAnimated = useRef(new Animated.Value(1)).current; + + // 런닝 통계 데이터 + const runningStats: RunningStats = formatRunningStats(routeStats); + + // 상태 업데이트 함수들 + const updateState = (updates: Partial) => { + setState((prev) => ({ ...prev, ...updates })); + }; + + // 색상 업데이트 헬퍼 + const updateColors = ( + colorUpdates: Partial, + ) => { + setState((prev) => ({ + ...prev, + colors: { ...prev.colors, ...colorUpdates }, + })); + }; + + // 표시 설정 업데이트 헬퍼 + const updateVisibility = ( + visibilityUpdates: Partial, + ) => { + setState((prev) => ({ + ...prev, + visibility: { ...prev.visibility, ...visibilityUpdates }, + })); + }; + + // 크기 설정 업데이트 헬퍼 + const updateScales = ( + scaleUpdates: Partial, + ) => { + setState((prev) => ({ + ...prev, + scales: { ...prev.scales, ...scaleUpdates }, + })); + }; + + // 스케일 애니메이션 업데이트 + useEffect(() => { + statsScaleAnimated.setValue(state.scales.stats); + }, [state.scales.stats, statsScaleAnimated]); + + useEffect(() => { + routeScaleAnimated.setValue(state.scales.route); + }, [state.scales.route, routeScaleAnimated]); + + useEffect(() => { + timestampScaleAnimated.setValue(state.scales.timestamp); + }, [state.scales.timestamp, timestampScaleAnimated]); + + // 경로 데이터 처리 + useEffect(() => { + if (path && path.length > 0) { + const pathData = convertPathToSvgData(path); + updateState({ routePathData: pathData }); + } + }, [path]); + + // PanResponder 생성 함수 + const createPanResponder = ( + translateX: Animated.Value, + translateY: Animated.Value, + scale: number, + ) => { + return PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderGrant: () => { + translateX.extractOffset(); + translateY.extractOffset(); + }, + onPanResponderMove: (evt, gestureState) => { + translateX.setValue(gestureState.dx); + translateY.setValue(gestureState.dy); + }, + onPanResponderRelease: () => { + translateX.flattenOffset(); + translateY.flattenOffset(); + }, + }); + }; + + // 각 요소별 PanResponder + const statsPanResponder = createPanResponder( + statsTranslateX, + statsTranslateY, + state.scales.stats, + ); + const routePanResponder = createPanResponder( + routeTranslateX, + routeTranslateY, + state.scales.route, + ); + const timestampPanResponder = createPanResponder( + timestampTranslateX, + timestampTranslateY, + state.scales.timestamp, + ); + + // 저장 처리 + const handleSave = async () => { + try { + updateState({ isProcessing: true }); + + if (!viewShotRef.current) { + Alert.alert('오류', '이미지를 캡처할 수 없습니다.'); + return; + } + + // ViewShot으로 편집된 이미지 캡처 + const uri = await viewShotRef.current!.capture!(); + + // 미디어 라이브러리 권한 요청 + const { status } = await MediaLibrary.requestPermissionsAsync(); + if (status !== 'granted') { + Alert.alert('권한 필요', '갤러리에 저장하려면 권한이 필요합니다.'); + return; + } + + // 갤러리에 저장 + const asset = await MediaLibrary.createAssetAsync(uri); + + Alert.alert('저장 완료', '꾸민 인증사진이 갤러리에 저장되었습니다.', [ + { + text: '확인', + onPress: () => { + onNavigateToRunDetail({ + recordId, + imageUrl: uri, + stats: { + distance: runningStats.distance, + calories: runningStats.calories, + pace: parseFloat(runningStats.pace.replace(':', '.')), + runningTime: runningStats.time, + }, + }); + }, + }, + ]); + } catch (error) { + Alert.alert('오류', '사진 저장 중 오류가 발생했습니다.'); + } finally { + updateState({ isProcessing: false }); + } + }; + + // 공유 처리 + const handleShare = async () => { + if (!viewShotRef.current?.capture) { + Alert.alert('오류', '이미지를 찾을 수 없습니다.'); + return; + } + + try { + updateState({ isProcessing: true }); + + // ViewShot으로 현재 꾸민 이미지 캡처 + const uri = await viewShotRef.current.capture(); + + // react-native-share 라이브러리로 바로 공유 창 띄우기 + const shareOptions = createShareOptions(uri); + const result = await Share.open(shareOptions); + } catch (error: unknown) { + if (error instanceof Error && error.message !== 'User did not share') { + Alert.alert('오류', '공유 중 오류가 발생했습니다.'); + } + } finally { + updateState({ isProcessing: false }); + } + }; + + return { + // 상태 + state, + updateState, + updateColors, + updateVisibility, + updateScales, + runningStats, + + // refs + viewShotRef, + + // 애니메이션 values + statsTranslateX, + statsTranslateY, + statsScaleAnimated, + routeTranslateX, + routeTranslateY, + routeScaleAnimated, + timestampTranslateX, + timestampTranslateY, + timestampScaleAnimated, + + // PanResponders + statsPanResponder, + routePanResponder, + timestampPanResponder, + + // 핸들러 + handleSave, + handleShare, + }; +} diff --git a/src/hooks/useRouteSvgExport.ts b/src/hooks/useRouteSvgExport.ts index cf6fbfd..3e80ac7 100644 --- a/src/hooks/useRouteSvgExport.ts +++ b/src/hooks/useRouteSvgExport.ts @@ -50,7 +50,6 @@ export function useRouteSvgExport() { text2: '갤러리에 경로 SVG 파일을 저장했습니다.', }); } catch (error) { - console.error('Failed to save SVG route', error); Toast.show({ type: 'error', text1: 'SVG 저장 실패', diff --git a/src/hooks/useRunModals.ts b/src/hooks/useRunModals.ts index 518c790..af2a151 100644 --- a/src/hooks/useRunModals.ts +++ b/src/hooks/useRunModals.ts @@ -14,7 +14,7 @@ import type { RunningRecordRequest } from '@/types/run.types'; import type { AxiosErrorResponse } from '@/types/api.types'; type Props = { - navigation: NativeStackNavigationProp; + navigation: any; // RootStackParamList와 TabParamList 모두 지원 mapRef?: RefObject; cameraRef?: RefObject; courseId?: number; @@ -55,12 +55,10 @@ export function useRunModals({ setModal('back'); }, [setModal]); - // 공통 정리 및 뒤로가기 로직 const cleanupAndGoBack = useCallback(() => { resetLocationTracking(); resetRunState(); - clearCurrentCourse(); // currentCourseId를 undefined로 초기화 - // courseId 파라미터 초기화 + clearCurrentCourse(); navigation.setParams({ courseId: undefined }); navigation.goBack(); }, [resetLocationTracking, resetRunState, clearCurrentCourse, navigation]); @@ -82,7 +80,6 @@ export function useRunModals({ return; } - // 코스 선택 시 최소 이동 거리 체크 if (courseId && routeCoordinates.length < 2) { Toast.show({ type: 'error', @@ -143,45 +140,38 @@ export function useRunModals({ imageUrl, }; - console.log('📤 [useRunModals] 런닝 기록 저장 요청 페이로드:', { - runningRecord, - courseId, - pathLength: path.length, - startTime: startTime.toISOString(), - endTime: endTime.toISOString(), - pace: paceValue, - calories: stats.calories, - imageUrl, - }); - const response = await saveRunningRecord(runningRecord, courseId); - console.log('📥 [useRunModals] 런닝 기록 저장 응답:', response); - Toast.show({ type: 'success', text1: '저장 완료', text2: '런닝 기록이 성공적으로 저장되었습니다.', }); - cleanupAndGoBack(); + navigation.navigate('RunDetail', { + recordId: response.id, + imageUrl, + path: path, + stats: { + distance: stats.distance, + calories: stats.calories, + pace: stats.pace, + runningTime: stats.runningTime, + }, + startAt: startTime.toISOString(), + endAt: endTime.toISOString(), + }); + + resetLocationTracking(); + resetRunState(); + clearCurrentCourse(); } catch (error: unknown) { let errorMessage = '런닝 기록 저장에 실패했습니다.'; - // API 응답 로그 추가 - console.error('🚨 [useRunModals] 런닝 기록 저장 실패:', error); - if (error && typeof error === 'object' && 'response' in error) { const axiosError = error as AxiosErrorResponse; const status = axiosError.status; - console.error('🚨 [useRunModals] API 응답 상태코드:', status); - console.error('🚨 [useRunModals] API 응답 데이터:', axiosError.data); - console.error( - '🚨 [useRunModals] API 응답 상태텍스트:', - axiosError.statusText, - ); - if (status === 400) { const errorData = axiosError.data; if (errorData?.message) { @@ -198,7 +188,6 @@ export function useRunModals({ errorMessage = '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'; } } else if (error instanceof Error) { - console.error('🚨 [useRunModals] 일반 에러:', error.message); errorMessage = error.message; } diff --git a/src/navigation/RecordsStackNavigator.tsx b/src/navigation/RecordsStackNavigator.tsx new file mode 100644 index 0000000..083626e --- /dev/null +++ b/src/navigation/RecordsStackNavigator.tsx @@ -0,0 +1,19 @@ +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import Records from '@/pages/Records'; +import RecordDetail from '@/pages/RecordDetail'; + +export type RecordsStackParamList = { + RecordsMain: undefined; + RecordDetail: { recordId: number }; +}; + +const Stack = createNativeStackNavigator(); + +export default function RecordsStackNavigator() { + return ( + + + + + ); +} diff --git a/src/navigation/RunTabNavigator.tsx b/src/navigation/RunTabNavigator.tsx index a9e6472..f5c4967 100644 --- a/src/navigation/RunTabNavigator.tsx +++ b/src/navigation/RunTabNavigator.tsx @@ -3,7 +3,7 @@ import { View, Animated } from 'react-native'; import styled from '@emotion/native'; import { Play } from 'lucide-react-native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { useFocusEffect } from '@react-navigation/native'; +import { useFocusEffect, useNavigation } from '@react-navigation/native'; import { LinearGradient } from 'expo-linear-gradient'; import Mapbox from '@rnmapbox/maps'; import TabNavigation from '@/components/TabNavigation'; @@ -12,15 +12,19 @@ import { useLocationManager } from '@/hooks/useLocationManager'; import { useLongPress } from '@/hooks/useLongPress'; import { useAdjacentCourses } from '@/hooks/api/useRouteApi'; import type { CourseSearchItem } from '@/types/courses.types'; +import type { RunTabStackParamList } from '@/types/navigation.types'; import Route from '@/pages/Route'; import Draw from '@/pages/Draw'; import RouteSave from '@/pages/RouteSave'; import Detail from '@/pages/Detail'; import Run from '@/pages/Run'; +import RunDetail from '@/pages/RunDetail'; +import PhotoEdit from '@/pages/PhotoEdit'; +import PhotoDecoration from '@/pages/PhotoDecoration'; import RecommendationContainer from '@/pages/Run/_components/RecommendationContainer'; -const Stack = createNativeStackNavigator(); +const Stack = createNativeStackNavigator(); type RunTabId = 'quickstart' | 'courseselection'; @@ -29,7 +33,6 @@ const tabs: Array<{ id: RunTabId; title: string }> = [ { id: 'courseselection', title: '코스 선택하기' }, ]; -// 바로가기 메인 컴포넌트 function QuickStartMain({ navigation }: { navigation: any }) { const [activeTab, setActiveTab] = useState('quickstart'); const [recommendations, setRecommendations] = useState( @@ -48,7 +51,6 @@ function QuickStartMain({ navigation }: { navigation: any }) { const { searchAdjacent } = useAdjacentCourses(); - // 추천 경로 로드 함수 const loadRecommendations = useCallback(async () => { if (initialLocation) { try { @@ -60,7 +62,6 @@ function QuickStartMain({ navigation }: { navigation: any }) { } }, [initialLocation, searchAdjacent]); - // 탭이 포커스될 때마다 위치 새로고침 및 추천 경로 로드 useFocusEffect( useCallback(() => { if (!initialLocation && !locationLoading) { @@ -172,6 +173,9 @@ export default function RunTabNavigator() { + + + ); } diff --git a/src/navigation/TabNavigator.tsx b/src/navigation/TabNavigator.tsx index a736bcd..c36ad87 100644 --- a/src/navigation/TabNavigator.tsx +++ b/src/navigation/TabNavigator.tsx @@ -1,19 +1,22 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { getFocusedRouteNameFromRoute } from '@react-navigation/native'; +import { + getFocusedRouteNameFromRoute, + useFocusEffect, +} from '@react-navigation/native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { View } from 'react-native'; import { Star, FileText, Play, Laugh, Settings } from 'lucide-react-native'; +import { useCallback } from 'react'; import WebCommunity from '@/pages/WebCommunity'; import Home from '@/pages/Home'; -import Records from '@/pages/Records'; +import RecordsStackNavigator from '@/navigation/RecordsStackNavigator'; import RunTabNavigator from '@/navigation/RunTabNavigator'; import Run from '@/pages/Run'; import type { TabParamList } from '@/types/navigation.types'; import WebMyPage from '@/pages/WebMyPage'; -// RunTab 전용 컴포넌트 function RunTabWithReset() { return ; } @@ -68,15 +71,23 @@ export default function TabNavigator() { /> ( - - ), + component={RecordsStackNavigator} + options={({ route }) => { + const routeName = + getFocusedRouteNameFromRoute(route) ?? 'RecordsMain'; + return { + tabBarIcon: ({ color, size }) => ( + + ), + tabBarStyle: { + ...baseTabBarStyle, + display: routeName === 'RecordDetail' ? 'none' : 'flex', + }, + }; }} /> { const routeName = @@ -89,7 +100,10 @@ export default function TabNavigator() { routeName === 'Run' || routeName === 'Draw' || routeName === 'RouteSave' || - routeName === 'Detail' + routeName === 'Detail' || + routeName === 'RunDetail' || + routeName === 'PhotoEdit' || + routeName === 'PhotoDecoration' ? 'none' : 'flex', }, diff --git a/src/pages/Auth/index.tsx b/src/pages/Auth/index.tsx index fee10f0..515588f 100644 --- a/src/pages/Auth/index.tsx +++ b/src/pages/Auth/index.tsx @@ -36,7 +36,7 @@ export default function Auth() { setAuth(accessToken, user); // 로그인 성공 후 위치 미리 받아오기 - console.log('📍 로그인 성공! 위치 미리 받아오기 시작...'); + refreshLocation(); // navigation.reset({ index: 0, routes: [{ name: 'TabNavigator' }] }); diff --git a/src/pages/Detail/index.tsx b/src/pages/Detail/index.tsx index 7913620..a3dbe8b 100644 --- a/src/pages/Detail/index.tsx +++ b/src/pages/Detail/index.tsx @@ -23,7 +23,12 @@ import { useImageLoading } from '@/hooks/useImageLoading'; import { useLongPress } from '@/hooks/useLongPress'; import { useCourseData } from '@/hooks/useCourseData'; import { useShare } from '@/hooks/useShare'; -import { formatDate, formatDistance, formatTime } from '@/utils/formatters'; +import { + formatDate, + formatDistance, + formatTime, + formatTimeFromMinutes, +} from '@/utils/formatters'; import type { RouteStackParamList } from '@/navigation/RouteStackNavigator'; import type { TabParamList } from '@/types/navigation.types'; import type { @@ -201,7 +206,9 @@ export default function Detail({ route, navigation }: Props) { {courseData && 'time' in courseData ? ( 예상 시간 - {formatTime((courseData as any).time)} + + {formatTimeFromMinutes((courseData as any).time)} + ) : courseData && 'duration' in courseData && diff --git a/src/pages/Details/Details.tsx b/src/pages/Details/Details.tsx deleted file mode 100644 index cb05c4a..0000000 --- a/src/pages/Details/Details.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { Text, View } from 'react-native'; - -import type { RootStackParamList } from '@/types/navigation.types'; - -type Props = NativeStackScreenProps; - -export default function Details({ route, navigation }: Props) { - return ( - - Details #{route.params.id} - navigation.goBack()}>Back - - ); -} diff --git a/src/pages/Draw/index.tsx b/src/pages/Draw/index.tsx index f8d1918..28543f7 100644 --- a/src/pages/Draw/index.tsx +++ b/src/pages/Draw/index.tsx @@ -108,10 +108,6 @@ export default function Draw() { }; if (locationLoading || !initialLocation) { - console.log('📍 Draw 페이지 로딩 중...', { - locationLoading, - initialLocation, - }); return ; } diff --git a/src/pages/Home/_components/FloatingImageContainer.tsx b/src/pages/Home/_components/FloatingImageContainer.tsx index e654536..ff04807 100644 --- a/src/pages/Home/_components/FloatingImageContainer.tsx +++ b/src/pages/Home/_components/FloatingImageContainer.tsx @@ -2,13 +2,15 @@ import { useState, useEffect } from 'react'; import { Dimensions } from 'react-native'; import FloatingImage from './FloatingImage'; import type { FloatingImageData } from '@/types/home.types'; +import useAuthStore from '@/store/auth'; +import api from '@/lib/api'; const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); const isColliding = ( rect1: { x: number; y: number; size: number }, rect2: { x: number; y: number; size: number }, ) => { - const margin = 10; + const margin = 5; // 충돌 영역 5px return !( rect1.x + rect1.size + margin < rect2.x || rect2.x + rect2.size + margin < rect1.x || @@ -33,51 +35,129 @@ const isValidPosition = ( export default function FloatingImageContainer() { const [floatingImages, setFloatingImages] = useState([]); + const { accessToken } = useAuthStore(); useEffect(() => { - const images = [ - require('@/assets/bear.png'), - require('@/assets/chick.png'), - require('@/assets/dog.png'), - require('@/assets/woman.png'), - ]; + const generateRandomImages = async () => { + try { + if (!accessToken) { + throw new Error('No authentication token'); + } + + const response = await api.get('/api/running/art', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + const artUrls = response.data?.results ?? []; + + // null 값 필터링하고 유효한 URL만 사용 + const validArtUrls = artUrls.filter( + (url: string | null) => url !== null, + ); - const generateRandomImages = () => { - const newImages: FloatingImageData[] = []; - const maxAttempts = 500; + // API에서 이미지를 가져오지 못했을 때 fallback 이미지 사용 + const images = + validArtUrls.length > 0 + ? validArtUrls.map((url: string) => ({ uri: url })) + : [ + require('@/assets/bear.png'), + require('@/assets/chick.png'), + require('@/assets/dog.png'), + require('@/assets/woman.png'), + ]; - images.forEach((image, index) => { - const count = 1; + const newImages: FloatingImageData[] = []; - for (let i = 0; i < count; i++) { + // 5px 충돌 영역으로 이미지 배치 + images.forEach((image: any, index: number) => { let attempts = 0; let validPosition = false; let newImage: FloatingImageData | null = null; + const maxAttempts = 100; // 시도 횟수 줄임 while (attempts < maxAttempts && !validPosition) { + // 랜덤 사이즈 (80px ~ 140px) + const imageSize = Math.random() * 100 + 80; + + // 화면 안전 영역 계산 + const safeMargin = 20; + const minX = safeMargin; + const maxX = screenWidth - imageSize - safeMargin; + const minY = 120; + const maxY = screenHeight - imageSize - 180; + newImage = { - id: index * 10 + i, + id: index, source: image, - x: Math.random() * (screenWidth - 100), - y: Math.random() * (screenHeight / 2 - 250) + 150, - size: Math.random() * 60 + 100, + x: Math.random() * Math.max(0, maxX - minX) + minX, + y: Math.random() * Math.max(0, maxY - minY) + minY, + size: imageSize, rotation: Math.random() * 360, }; - if (isValidPosition(newImage, newImages)) { + // 충돌 검사 (첫 50번만 시도, 이후 강제 배치) + if (attempts < 50) { + if (isValidPosition(newImage, newImages)) { + validPosition = true; + newImages.push(newImage); + } + } else { + // 50번 시도 후에는 강제 배치 validPosition = true; newImages.push(newImage); } attempts++; } - } - }); + }); + + setFloatingImages(newImages); + } catch (error) { + // 인증 에러나 네트워크 에러 시 기본 이미지 사용 + const fallbackImages = [ + require('@/assets/bear.png'), + require('@/assets/chick.png'), + require('@/assets/dog.png'), + require('@/assets/woman.png'), + ]; + + const newImages: FloatingImageData[] = []; + const maxAttempts = 500; + + fallbackImages.forEach((image: any, index: number) => { + const count = 1; + + for (let i = 0; i < count; i++) { + let attempts = 0; + let validPosition = false; + let newImage: FloatingImageData | null = null; + + while (attempts < maxAttempts && !validPosition) { + newImage = { + id: index * 10 + i, + source: image, + x: Math.random() * (screenWidth - 100), + y: Math.random() * (screenHeight / 2 - 250) + 150, + size: Math.random() * 60 + 100, + rotation: Math.random() * 360, + }; + + if (isValidPosition(newImage, newImages)) { + validPosition = true; + newImages.push(newImage); + } + attempts++; + } + } + }); - setFloatingImages(newImages); + setFloatingImages(newImages); + } }; generateRandomImages(); - }, []); + }, [accessToken]); return ( <> diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index ca53248..e606662 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,39 +1,66 @@ import styled from '@emotion/native'; import { LinearGradient } from 'expo-linear-gradient'; -import { Bell } from 'lucide-react-native'; +import { useState, useEffect } from 'react'; +import { StatusBar } from 'react-native'; import Header from '@/components/Header'; import FloatingImageContainer from './_components/FloatingImageContainer'; +import StarryBackground from '@/components/StarryBackground'; +import { useInitialLocation } from '@/hooks/useInitialLocation'; +import { reverseGeocode } from '@/lib/geocoding'; export default function Home() { + const [address, setAddress] = useState('위치 정보를 가져오는 중...'); + const { location, loading } = useInitialLocation(); + + useEffect(() => { + const getAddressFromLocation = async () => { + if (location?.coords) { + try { + const geocodingResult = await reverseGeocode([ + location.coords.longitude, + location.coords.latitude, + ]); + setAddress(geocodingResult.address); + } catch (error) { + console.error('지오코딩 실패:', error); + setAddress('위치를 찾을 수 없습니다'); + } + } else if (!loading) { + setAddress('위치 정보를 사용할 수 없습니다'); + } + }; + + getAddressFromLocation(); + }, [location, loading]); + const handleLocationPress = () => { console.log('위치 버튼 클릭'); }; - const handleNotificationPress = () => { - console.log('알림 버튼 클릭'); - }; - return ( + + +
- - {/* 추천 경로는 바로가기 화면으로 이동됨 */} - + diff --git a/src/pages/PhotoDecoration/_components/DecorationOptions.tsx b/src/pages/PhotoDecoration/_components/DecorationOptions.tsx new file mode 100644 index 0000000..74d45e5 --- /dev/null +++ b/src/pages/PhotoDecoration/_components/DecorationOptions.tsx @@ -0,0 +1,325 @@ +import React from 'react'; +import { ScrollView, TouchableOpacity, Text, View } from 'react-native'; +import styled from '@emotion/native'; + +import { + PhotoDecorationState, + ColorOption, +} from '@/types/photoDecoration.types'; +import { + getBackgroundColorOptions, + getRouteColorOptions, +} from '@/utils/photoDecorationUtils'; +import SizeControlButtons from './SizeControlButtons'; + +export interface DecorationOptionsProps { + state: PhotoDecorationState; + updateState: (updates: Partial) => void; + updateColors: (colorUpdates: Partial) => void; + updateVisibility: ( + visibilityUpdates: Partial, + ) => void; + updateScales: (scaleUpdates: Partial) => void; +} + +export default function DecorationOptions({ + state, + updateState, + updateColors, + updateVisibility, + updateScales, +}: DecorationOptionsProps) { + const backgroundColors = getBackgroundColorOptions(); + const routeColors = getRouteColorOptions(); + + return ( + + + 편집 옵션 + + + 배경 + + updateState({ backgroundType: 'photo' })} + > + + 사진 + + + updateState({ backgroundType: 'solid' })} + > + + 단색 + + + + + + {state.backgroundType === 'solid' && ( + + 배경색 + + {backgroundColors.map((color) => ( + updateState({ backgroundColor: color.color })} + > + + + ))} + + + )} + + + 경로 색상 + + {routeColors.map((color) => ( + updateColors({ route: color.color })} + > + + + ))} + + + + + 통계 색상 + + {routeColors.map((color) => ( + updateColors({ stats: color.color })} + > + + + ))} + + + + + 타임스탬프 색상 + + {routeColors.map((color) => ( + updateColors({ timestamp: color.color })} + > + + + ))} + + + + + 런닝 통계 + + updateVisibility({ time: !state.visibility.time })} + > + 시간 + + + updateVisibility({ distance: !state.visibility.distance }) + } + > + 거리 + + updateVisibility({ pace: !state.visibility.pace })} + > + 페이스 + + + updateVisibility({ calories: !state.visibility.calories }) + } + > + 칼로리 + + + + + + 기타 요소 + + + updateVisibility({ route: !state.visibility.route }) + } + > + + 경로 이미지 + + + + updateVisibility({ timestamp: !state.visibility.timestamp }) + } + > + + 타임스탬프 + + + + + + + 크기 조절 + + {state.visibility.route && ( + + 경로 이미지 크기 + + updateScales({ route: value })} + /> + + + )} + + + 통계 크기 + + updateScales({ stats: value })} + /> + + + + {state.visibility.timestamp && ( + + 타임스탬프 크기 + + updateScales({ timestamp: value })} + /> + + + )} + + + + ); +} + +const OptionsContainer = styled.View({ + paddingHorizontal: 20, + paddingTop: 20, +}); + +const OptionsTitle = styled.Text({ + fontSize: 18, + fontWeight: 'bold', + color: '#000000', + marginBottom: 20, +}); + +const OptionSection = styled.View({ + marginBottom: 24, +}); + +const OptionLabel = styled.Text({ + fontSize: 16, + fontWeight: '600', + color: '#333333', + marginBottom: 12, +}); + +const ToggleContainer = styled.View({ + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, +}); + +const ToggleButton = styled.TouchableOpacity<{ active: boolean }>( + ({ active }) => ({ + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + backgroundColor: active ? '#2d2d2d' : '#f0f0f0', + borderWidth: 1, + borderColor: active ? '#2d2d2d' : '#e0e0e0', + }), +); + +const ToggleText = styled.Text<{ active?: boolean }>(({ active }) => ({ + fontSize: 14, + color: active ? '#FFFFFF' : '#333333', + fontWeight: '500', +})); + +const ColorContainer = styled.View({ + flexDirection: 'row', + flexWrap: 'wrap', + gap: 8, +}); + +const ColorButton = styled.TouchableOpacity<{ selected: boolean }>( + ({ selected }) => ({ + width: 40, + height: 40, + borderRadius: 20, + borderWidth: selected ? 3 : 1, + borderColor: selected ? '#2d2d2d' : '#e0e0e0', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#ffffff', + }), +); + +const ColorCircle = styled.View<{ color: string }>(({ color }) => ({ + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: color, + borderWidth: 1, + borderColor: color === '#FFFFFF' ? '#e0e0e0' : 'transparent', +})); + +const SizeControlContainer = styled.View({ + marginBottom: 16, +}); + +const SizeLabel = styled.Text({ + fontSize: 14, + color: '#333333', + marginBottom: 8, + fontWeight: '500', +}); + +const SizeSliderContainer = styled.View({ + flexDirection: 'row', + alignItems: 'center', + gap: 12, +}); diff --git a/src/pages/PhotoDecoration/_components/RouteRenderer.tsx b/src/pages/PhotoDecoration/_components/RouteRenderer.tsx new file mode 100644 index 0000000..9d17269 --- /dev/null +++ b/src/pages/PhotoDecoration/_components/RouteRenderer.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { Animated, View } from 'react-native'; +import Svg, { Path } from 'react-native-svg'; +import styled from '@emotion/native'; + +import { getDefaultPathData } from '@/utils/photoDecorationUtils'; + +interface RouteRendererProps { + showRoute: boolean; + routePathData: string; + routeColor: string; + routeTranslateX: any; // Animated.Value + routeTranslateY: any; // Animated.Value + routeScaleAnimated: any; // Animated.Value + routePanResponder: any; +} + +export default function RouteRenderer({ + showRoute, + routePathData, + routeColor, + routeTranslateX, + routeTranslateY, + routeScaleAnimated, + routePanResponder, +}: RouteRendererProps) { + if (!showRoute) { + return null; + } + + const finalPathData = routePathData || getDefaultPathData(); + + if (!routePathData) { + console.log('📊 [RouteRenderer] routePathData가 없음, 테스트 경로 사용'); + } + + return ( + + + + + + + + ); +} + +const RouteImageContainer = styled.View({ + position: 'absolute', + bottom: 20, + right: 20, + width: 120, + height: 100, + justifyContent: 'center', + alignItems: 'center', +}); diff --git a/src/pages/PhotoDecoration/_components/SizeControlButtons.tsx b/src/pages/PhotoDecoration/_components/SizeControlButtons.tsx new file mode 100644 index 0000000..8521d6d --- /dev/null +++ b/src/pages/PhotoDecoration/_components/SizeControlButtons.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { TouchableOpacity, Text, View } from 'react-native'; +import styled from '@emotion/native'; + +import { SizeControlProps } from '@/types/photoDecoration.types'; + +export default function SizeControlButtons({ + value, + onValueChange, + minimumValue = 0.5, + maximumValue = 2, + step = 0.1, +}: SizeControlProps) { + const handleDecrease = () => { + const newValue = Math.max(minimumValue, value - step); + onValueChange(newValue); + }; + + const handleIncrease = () => { + const newValue = Math.min(maximumValue, value + step); + onValueChange(newValue); + }; + + return ( + + + - + + {Math.round(value * 100)}% + = maximumValue}> + + + + + ); +} + +const SizeButtonContainer = styled.View({ + flexDirection: 'row', + alignItems: 'center', + gap: 12, +}); + +const SizeButton = styled.TouchableOpacity<{ disabled?: boolean }>( + ({ disabled }) => ({ + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: disabled ? '#e0e0e0' : '#2d2d2d', + justifyContent: 'center', + alignItems: 'center', + }), +); + +const SizeButtonText = styled.Text({ + fontSize: 18, + color: '#ffffff', + fontWeight: 'bold', +}); + +const SizeValue = styled.Text({ + fontSize: 14, + color: '#666666', + fontWeight: '500', + minWidth: 40, + textAlign: 'center', +}); diff --git a/src/pages/PhotoDecoration/_components/StatsRenderer.tsx b/src/pages/PhotoDecoration/_components/StatsRenderer.tsx new file mode 100644 index 0000000..9605f0c --- /dev/null +++ b/src/pages/PhotoDecoration/_components/StatsRenderer.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { Animated, View, Text } from 'react-native'; +import { Clock, Map, Zap } from 'lucide-react-native'; +import styled from '@emotion/native'; + +import { + RunningStats, + PhotoDecorationState, +} from '@/types/photoDecoration.types'; +import { formatNumber, formatPace } from '@/utils/formatters'; + +interface StatsRendererProps { + runningStats: RunningStats; + statsColor: string; + visibility: PhotoDecorationState['visibility']; + statsTranslateX: any; // Animated.Value + statsTranslateY: any; // Animated.Value + statsScaleAnimated: any; // Animated.Value + statsPanResponder: any; +} + +export default function StatsRenderer({ + runningStats, + statsColor, + visibility, + statsTranslateX, + statsTranslateY, + statsScaleAnimated, + statsPanResponder, +}: StatsRendererProps) { + const visibleStats = []; + + if (visibility.time) { + visibleStats.push( + + + + + {runningStats.time} + 시간 + , + ); + } + + if (visibility.distance) { + visibleStats.push( + + + + + + {formatNumber(runningStats.distance).toFixed(2)}km + + 거리 + , + ); + } + + if (visibility.pace) { + visibleStats.push( + + + + + + {typeof runningStats.pace === 'number' + ? formatPace(runningStats.pace) + : runningStats.pace} + + 페이스 + , + ); + } + + if (visibility.calories) { + visibleStats.push( + + + + + + {formatNumber(runningStats.calories).toFixed(2)} + + 칼로리 + , + ); + } + + if (visibleStats.length === 0) return null; + + return ( + + {visibleStats} + + ); +} + +const StatsContainer = styled.View<{ textColor: string }>(({ textColor }) => ({ + position: 'absolute', + bottom: 140, + left: 20, + right: 20, + + padding: 16, + flexDirection: 'row', + justifyContent: 'space-around', +})); + +const StatItem = styled.View({ + alignItems: 'center', +}); + +const StatIcon = styled.View({ + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 4, +}); + +const StatValue = styled.Text<{ textColor: string }>(({ textColor }) => ({ + fontSize: 14, + fontWeight: 'bold', + color: textColor, + marginBottom: 2, +})); + +const StatLabel = styled.Text<{ textColor: string }>(({ textColor }) => ({ + fontSize: 10, + color: + textColor === '#FFFFFF' ? 'rgba(255, 255, 255, 0.8)' : `${textColor}CC`, +})); diff --git a/src/pages/PhotoDecoration/_components/TimestampRenderer.tsx b/src/pages/PhotoDecoration/_components/TimestampRenderer.tsx new file mode 100644 index 0000000..b9badae --- /dev/null +++ b/src/pages/PhotoDecoration/_components/TimestampRenderer.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { Animated, Text } from 'react-native'; +import styled from '@emotion/native'; + +import { RunningStats } from '@/types/photoDecoration.types'; + +interface TimestampRendererProps { + showTimestamp: boolean; + runningStats: RunningStats; + timestampColor: string; + timestampTranslateX: any; // Animated.Value + timestampTranslateY: any; // Animated.Value + timestampScaleAnimated: any; // Animated.Value + timestampPanResponder: any; +} + +export default function TimestampRenderer({ + showTimestamp, + runningStats, + timestampColor, + timestampTranslateX, + timestampTranslateY, + timestampScaleAnimated, + timestampPanResponder, +}: TimestampRendererProps) { + if (!showTimestamp) { + return null; + } + + return ( + + + + {runningStats.date} + + + + ); +} + +const TimestampContainer = styled.View<{ textColor: string }>( + ({ textColor }) => ({ + position: 'absolute', + bottom: 20, + left: 20, + backgroundColor: 'transparent', + paddingHorizontal: 16, + paddingVertical: 8, + }), +); + +const TimestampText = styled.Text<{ textColor: string }>(({ textColor }) => ({ + fontSize: 18, + fontWeight: 'bold', + color: textColor, +})); diff --git a/src/pages/PhotoDecoration/index.tsx b/src/pages/PhotoDecoration/index.tsx new file mode 100644 index 0000000..544637e --- /dev/null +++ b/src/pages/PhotoDecoration/index.tsx @@ -0,0 +1,215 @@ +import React from 'react'; +import { + View, + Image, + ActivityIndicator, + Dimensions, + TouchableOpacity, +} from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { ArrowLeft, Download, Share as ShareIcon } from 'lucide-react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import styled from '@emotion/native'; +import ViewShot from 'react-native-view-shot'; + +import Header from '@/components/Header'; +import type { RunTabStackParamList } from '@/types/navigation.types'; +import { usePhotoDecoration } from '@/hooks/usePhotoDecoration'; +import StatsRenderer from './_components/StatsRenderer'; +import RouteRenderer from './_components/RouteRenderer'; +import TimestampRenderer from './_components/TimestampRenderer'; +import DecorationOptions from './_components/DecorationOptions'; + +type Props = NativeStackScreenProps; + +const { width: screenWidth } = Dimensions.get('window'); + +export default function PhotoDecoration({ route, navigation }: Props) { + const { photoUri, recordId, path, stats: routeStats } = route.params; + const insets = useSafeAreaInsets(); + + const { + state, + updateState, + updateColors, + updateVisibility, + updateScales, + runningStats, + viewShotRef, + statsTranslateX, + statsTranslateY, + statsScaleAnimated, + routeTranslateX, + routeTranslateY, + routeScaleAnimated, + timestampTranslateX, + timestampTranslateY, + timestampScaleAnimated, + statsPanResponder, + routePanResponder, + timestampPanResponder, + handleSave, + handleShare, + } = usePhotoDecoration({ + path, + routeStats, + recordId, + onNavigateToRunDetail: (data) => { + navigation.navigate('RunDetail', data); + }, + }); + + const handleBackPress = () => { + navigation.goBack(); + }; + + return ( + +
+ + + + + + {state.backgroundType === 'photo' ? ( + + ) : ( + + )} + + + + + + + + + + + + + + + + + + + + {state.isProcessing ? ( + + ) : ( + + )} + 저장하기 + + + + ); +} + +const Container = styled.View({ + flex: 1, + backgroundColor: '#ffffff', +}); + +const Content = styled.View({ + flex: 1, +}); + +const PreviewContainer = styled.View({ + padding: 20, + alignItems: 'center', +}); + +const PhotoContainer = styled.View({ + width: screenWidth - 40, + height: screenWidth - 40, // 정사각형 1:1 비율 + borderRadius: 16, + overflow: 'hidden', + position: 'relative', +}); + +const OriginalPhoto = styled(Image)({ + width: '100%', + height: '100%', + resizeMode: 'cover', +}); + +const SolidBackground = styled.View<{ color: string }>(({ color }) => ({ + width: '100%', + height: '100%', + backgroundColor: color, +})); + +const BottomContainer = styled.View<{ safeAreaBottom: number }>( + ({ safeAreaBottom }) => ({ + flexDirection: 'row', + paddingHorizontal: 20, + paddingTop: 16, + paddingBottom: safeAreaBottom + 16, + backgroundColor: '#ffffff', + borderTopWidth: 1, + borderTopColor: '#f0f0f0', + }), +); + +const ShareButton = styled(TouchableOpacity)({ + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: '#f8f9fa', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, +}); + +const SaveButton = styled(TouchableOpacity)({ + flex: 1, + height: 48, + borderRadius: 8, + backgroundColor: '#2d2d2d', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', +}); + +const SaveText = styled.Text({ + fontSize: 16, + color: '#ffffff', + marginLeft: 8, + fontWeight: '600', +}); diff --git a/src/pages/PhotoEdit/index.tsx b/src/pages/PhotoEdit/index.tsx new file mode 100644 index 0000000..e84df64 --- /dev/null +++ b/src/pages/PhotoEdit/index.tsx @@ -0,0 +1,206 @@ +import React, { useState } from 'react'; +import { + View, + Text, + Image, + TouchableOpacity, + Alert, + ActivityIndicator, + Platform, +} from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { ArrowLeft, Check, RotateCcw } from 'lucide-react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import styled from '@emotion/native'; +import * as MediaLibrary from 'expo-media-library'; + +import Header from '@/components/Header'; +import type { RunTabStackParamList } from '@/types/navigation.types'; + +type Props = NativeStackScreenProps; + +export default function PhotoEdit({ route, navigation }: Props) { + const { photoUri, recordId, path, stats } = route.params; + const insets = useSafeAreaInsets(); + const [isProcessing, setIsProcessing] = useState(false); + const [imageError, setImageError] = useState(false); + const [imageLoading, setImageLoading] = useState(true); + + const handleBackPress = () => { + navigation.goBack(); + }; + + const saveToGallery = async () => { + try { + // 미디어 라이브러리 권한 요청 + const { status } = await MediaLibrary.requestPermissionsAsync(); + if (status !== 'granted') { + Alert.alert('권한 필요', '갤러리에 저장하려면 권한이 필요합니다.'); + return false; + } + + // 갤러리에 저장 + const asset = await MediaLibrary.createAssetAsync(photoUri); + + return true; + } catch (error) { + return false; + } + }; + + const handleEditPress = () => { + navigation.navigate('PhotoDecoration', { + photoUri, + recordId, + path, + stats, + }); + }; + + const handleRetakePress = () => { + navigation.goBack(); + }; + + return ( + +
+ + + {!imageError ? ( + { + setImageError(true); + setImageLoading(false); + }} + onLoad={() => { + setImageLoading(false); + }} + /> + ) : ( + + 사진을 불러올 수 없습니다 + URI: {photoUri} + + )} + + {imageLoading && !imageError && ( + + + 사진을 불러오는 중... + + )} + + + + + + 다시 촬영 + + + + + 편집하기 + + + + ); +} + +const Container = styled.View({ + flex: 1, + backgroundColor: '#000000', +}); + +const ImageContainer = styled.View({ + flex: 1, + backgroundColor: '#000000', +}); + +const PhotoImage = styled(Image)({ + width: '100%', + height: '100%', +}); + +const BottomContainer = styled.View<{ safeAreaBottom: number }>( + ({ safeAreaBottom }) => ({ + flexDirection: 'row', + paddingHorizontal: 20, + paddingTop: 16, + paddingBottom: safeAreaBottom + 16, + backgroundColor: '#000000', + borderTopWidth: 1, + borderTopColor: '#333333', + }), +); + +const RetakeButton = styled(TouchableOpacity)({ + flex: 1, + height: 48, + borderRadius: 8, + backgroundColor: '#f8f9fa', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, +}); + +const RetakeText = styled.Text({ + fontSize: 16, + color: '#666666', + marginLeft: 8, + fontWeight: '500', +}); + +const EditButton = styled(TouchableOpacity)({ + flex: 1, + height: 48, + borderRadius: 8, + backgroundColor: '#2d2d2d', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', +}); + +const EditText = styled.Text({ + fontSize: 16, + color: '#ffffff', + marginLeft: 8, + fontWeight: '600', +}); + +const ErrorContainer = styled.View({ + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, +}); + +const ErrorText = styled.Text({ + fontSize: 16, + color: '#ffffff', + textAlign: 'center', + marginBottom: 8, +}); + +const LoadingOverlay = styled.View({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.7)', +}); + +const LoadingText = styled.Text({ + fontSize: 16, + color: '#ffffff', + marginTop: 16, +}); diff --git a/src/pages/RecordDetail/index.tsx b/src/pages/RecordDetail/index.tsx new file mode 100644 index 0000000..c29c2b8 --- /dev/null +++ b/src/pages/RecordDetail/index.tsx @@ -0,0 +1,526 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + Image, + ScrollView, + TouchableOpacity, + Alert, + ActivityIndicator, + Dimensions, +} from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { LinearGradient } from 'expo-linear-gradient'; +import { ArrowLeft, Home, Share2 } from 'lucide-react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import styled from '@emotion/native'; + +import Header from '@/components/Header'; +import { getRunningRecordDetail } from '@/services/running.service'; +import useAuthStore from '@/store/auth'; +import { + formatDate, + formatDistance, + formatTime, + formatPace, + formatNumber, +} from '@/utils/formatters'; +import { COLOR_TOKENS } from '@/constants/colors'; +import type { RecordsStackParamList } from '@/navigation/RecordsStackNavigator'; +import type { RunningRecordDetail } from '@/types/records.types'; + +type Props = NativeStackScreenProps; + +const { width: screenWidth } = Dimensions.get('window'); + +export default function RecordDetail({ route, navigation }: Props) { + const { recordId } = route.params; + const [recordDetail, setRecordDetail] = useState( + null, + ); + const [loading, setLoading] = useState(true); + const [imageError, setImageError] = useState(false); + const [imageLoading, setImageLoading] = useState(true); + const [currentImageIndex, setCurrentImageIndex] = useState(0); + const { accessToken } = useAuthStore(); + const insets = useSafeAreaInsets(); + + const availableImages = recordDetail + ? [ + ...(recordDetail.artUrl + ? [{ type: 'art', url: recordDetail.artUrl }] + : []), + ...(recordDetail.imageUrl + ? [{ type: 'photo', url: recordDetail.imageUrl }] + : []), + ] + : []; + + useEffect(() => { + fetchRecordDetail(); + }, [recordId]); + + const fetchRecordDetail = async () => { + if (!accessToken) return; + + try { + setLoading(true); + const data = await getRunningRecordDetail(recordId, accessToken); + setRecordDetail(data); + } catch (error) { + Alert.alert('오류', '런닝 기록을 불러올 수 없습니다.'); + } finally { + setLoading(false); + } + }; + + const handleBackPress = () => { + navigation.goBack(); + }; + + const handleHomePress = () => { + navigation.navigate('RecordsMain'); + }; + + const handleSharePress = () => { + Alert.alert('공유', '런닝 기록을 공유합니다.'); + }; + + const handleImageError = () => { + setImageError(true); + setImageLoading(false); + }; + + const handleImageLoad = () => { + setImageError(false); + setImageLoading(false); + }; + + const formatDuration = (duration: number): string => { + const hours = Math.floor(duration / 3600); + const minutes = Math.floor((duration % 3600) / 60); + const seconds = formatNumber(duration % 60); + + if (hours > 0) { + return `${hours}시간 ${minutes}분 ${seconds}초`; + } else { + return `${minutes}분 ${seconds}초`; + } + }; + + const formatDateTime = (dateString: string): string => { + const date = new Date(dateString); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + return `${year}년 ${month}월 ${day}일 ${hours}:${minutes}`; + }; + + if (loading) { + return ( + + + 기록을 불러오는 중... + + ); + } + + if (!recordDetail) { + return ( + + 기록을 찾을 수 없습니다. + + 돌아가기 + + + ); + } + + return ( + +
+ + + {/* 이미지 영역 */} + {availableImages.length > 0 && ( + + { + const newIndex = Math.round( + event.nativeEvent.contentOffset.x / screenWidth, + ); + setCurrentImageIndex(newIndex); + }} + > + {availableImages.map((image, index) => ( + + {imageLoading && index === currentImageIndex && ( + + + + )} + {imageError && index === currentImageIndex ? ( + + + 이미지를 불러올 수 없습니다 + + + ) : ( + setImageLoading(true)} + /> + )} + + + {image.type === 'art' ? 'GPS 아트' : '경로 사진'} + + + + ))} + + + {/* 이미지 인디케이터 */} + {availableImages.length > 1 && ( + + {availableImages.map((_, index) => ( + + ))} + + )} + + )} + + {/* 통계 정보 */} + + 런닝 통계 + + + + 거리 + {formatDistance(recordDetail.distance)} + + + + 시간 + {formatDuration(recordDetail.duration)} + + + + 평균 페이스 + {formatPace(recordDetail.pace)} + + + + 칼로리 + {formatNumber(recordDetail.calories)}kcal + + + + + {/* 코스 정보 */} + {recordDetail.course && ( + + 코스 정보 + + {recordDetail.course.title} + + + )} + + {/* 런닝 정보 */} + + 런닝 정보 + + + 시작 시간 + {formatDateTime(recordDetail.startAt)} + + + + 종료 시간 + {formatDateTime(recordDetail.endAt)} + + + + + {/* 하단 홈 버튼 */} + + ); +} + +// Emotion Styled Components +const Container = styled.View({ + flex: 1, + backgroundColor: '#ffffff', +}); + +const LoadingContainer = styled.View({ + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#ffffff', +}); + +const LoadingText = styled.Text({ + marginTop: 16, + fontSize: 16, + color: COLOR_TOKENS.gray[600], +}); + +const ErrorContainer = styled.View({ + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#ffffff', + padding: 20, +}); + +const ErrorText = styled.Text({ + fontSize: 18, + color: COLOR_TOKENS.gray[600], + textAlign: 'center', + marginBottom: 20, +}); + +const RetryButton = styled.TouchableOpacity({ + backgroundColor: COLOR_TOKENS.primary[500], + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 8, +}); + +const RetryButtonText = styled.Text({ + color: '#ffffff', + fontSize: 16, + fontWeight: '600', +}); + +const Content = styled.ScrollView({ + flex: 1, +}); + +const ImageContainer = styled.View({ + height: 400, + position: 'relative', +}); + +const ImageWrapper = styled.View<{ isArt: boolean }>(({ isArt }) => ({ + width: screenWidth, + height: 400, + position: 'relative', + backgroundColor: isArt ? '#000000' : '#ffffff', +})); + +const StyledImage = styled.Image({ + width: '100%', + height: '100%', +}); + +const ImageLoadingContainer = styled.View({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: COLOR_TOKENS.gray[100], +}); + +const ImageErrorContainer = styled.View({ + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: COLOR_TOKENS.gray[100], +}); + +const ImageErrorText = styled.Text({ + fontSize: 16, + color: COLOR_TOKENS.gray[500], +}); + +const ImageTypeIndicator = styled.View({ + position: 'absolute', + top: 16, + right: 16, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 16, +}); + +const ImageTypeText = styled.Text({ + color: '#ffffff', + fontSize: 12, + fontWeight: '600', +}); + +const ImageIndicator = styled.View({ + position: 'absolute', + bottom: 16, + left: 0, + right: 0, + flexDirection: 'row', + justifyContent: 'center', + gap: 8, +}); + +const IndicatorDot = styled.View<{ isActive: boolean }>(({ isActive }) => ({ + width: 8, + height: 8, + borderRadius: 4, + backgroundColor: isActive ? '#ffffff' : 'rgba(255, 255, 255, 0.5)', +})); + +const StatsContainer = styled.View({ + padding: 20, + backgroundColor: '#ffffff', +}); + +const StatsTitle = styled.Text({ + fontSize: 20, + fontWeight: '700', + color: COLOR_TOKENS.gray[900], + marginBottom: 16, +}); + +const StatsGrid = styled.View({ + flexDirection: 'row', + flexWrap: 'wrap', + gap: 16, +}); + +const StatItem = styled.View({ + flex: 1, + minWidth: '45%', + backgroundColor: COLOR_TOKENS.gray[50], + padding: 16, + borderRadius: 12, + alignItems: 'center', +}); + +const StatLabel = styled.Text({ + fontSize: 14, + color: COLOR_TOKENS.gray[600], + marginBottom: 4, +}); + +const StatValue = styled.Text({ + fontSize: 18, + fontWeight: '700', + color: COLOR_TOKENS.gray[900], +}); + +const CourseContainer = styled.View({ + padding: 20, + backgroundColor: '#ffffff', +}); + +const CourseTitle = styled.Text({ + fontSize: 18, + fontWeight: '700', + color: COLOR_TOKENS.gray[900], + marginBottom: 12, +}); + +const CourseInfo = styled.View({ + backgroundColor: COLOR_TOKENS.gray[50], + padding: 16, + borderRadius: 12, +}); + +const CourseName = styled.Text({ + fontSize: 16, + fontWeight: '600', + color: COLOR_TOKENS.gray[800], +}); + +const RunInfoContainer = styled.View({ + padding: 20, + backgroundColor: '#ffffff', +}); + +const RunInfoTitle = styled.Text({ + fontSize: 18, + fontWeight: '700', + color: COLOR_TOKENS.gray[900], + marginBottom: 16, +}); + +const RunInfoItem = styled.View({ + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: COLOR_TOKENS.gray[100], +}); + +const RunInfoLabel = styled.Text({ + fontSize: 16, + color: COLOR_TOKENS.gray[600], +}); + +const RunInfoValue = styled.Text({ + fontSize: 16, + fontWeight: '600', + color: COLOR_TOKENS.gray[900], +}); + +const BottomContainer = styled.View({ + padding: 20, + backgroundColor: '#ffffff', +}); + +const HomeButton = styled.TouchableOpacity({ + height: 56, + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + overflow: 'hidden', +}); + +const HomeButtonGradient = styled(LinearGradient)({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + borderRadius: 8, +}); + +const HomeButtonContent = styled.View({ + flexDirection: 'row', + alignItems: 'center', + gap: 8, + zIndex: 1, +}); + +const HomeButtonText = styled.Text({ + color: '#ffffff', + fontSize: 16, + fontWeight: '600', +}); diff --git a/src/pages/Records/_components/RecordsList.tsx b/src/pages/Records/_components/RecordsList.tsx index 083f6b5..d581a7f 100644 --- a/src/pages/Records/_components/RecordsList.tsx +++ b/src/pages/Records/_components/RecordsList.tsx @@ -42,10 +42,6 @@ export default function RecordsList({ ); const handleEndReached = useCallback(() => { - console.log('📊 [RecordsList] handleEndReached 호출:', { - hasMore, - loading, - }); if (hasMore && !loading) { onLoadMore(); } @@ -122,6 +118,7 @@ const RecordItem = styled(TouchableOpacity)({ borderRadius: 12, overflow: 'hidden', marginBottom: 120, + backgroundColor: '#000000', }); const RecordImage = styled.Image({ diff --git a/src/pages/Records/_components/StatsDisplay.tsx b/src/pages/Records/_components/StatsDisplay.tsx index ebc42b8..7a2905c 100644 --- a/src/pages/Records/_components/StatsDisplay.tsx +++ b/src/pages/Records/_components/StatsDisplay.tsx @@ -1,5 +1,10 @@ import styled from '@emotion/native'; -import { formatDistance, formatTime, formatPace } from '@/utils/formatters'; +import { + formatDistance, + formatTime, + formatPace, + formatNumber, +} from '@/utils/formatters'; import type { RunningDashboard } from '@/types/records.types'; interface StatsDisplayProps { @@ -29,9 +34,7 @@ export default function StatsDisplay({ dashboard }: StatsDisplayProps) { - - {Math.round(dashboard.totalCalories * 100) / 100}kcal - + {formatNumber(dashboard.totalCalories)}kcal 총 칼로리 diff --git a/src/pages/Records/index.tsx b/src/pages/Records/index.tsx index e9bae3d..36936a8 100644 --- a/src/pages/Records/index.tsx +++ b/src/pages/Records/index.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/native'; import { FileText } from 'lucide-react-native'; import { useState, useEffect, useCallback } from 'react'; import { ScrollView } from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; import Header from '@/components/Header'; import TimeRangeSelector from './_components/TimeRangeSelector'; import StatsDisplay from './_components/StatsDisplay'; @@ -13,9 +14,12 @@ import { getTimeRangeParams, getTimeRangeDisplayText, } from '@/hooks/api/useRecordsApi'; -import type { TimeRange } from '@/types/records.types'; +import type { TimeRange, RunningRecord } from '@/types/records.types'; +import type { RecordsStackParamList } from '@/navigation/RecordsStackNavigator'; -export default function Records() { +type Props = NativeStackScreenProps; + +export default function Records({ navigation }: Props) { const [activeRange, setActiveRange] = useState('month'); const [showDropdown, setShowDropdown] = useState(false); const [selectedWeek, setSelectedWeek] = useState(1); @@ -58,13 +62,6 @@ export default function Records() { // 초기 데이터 로드 useEffect(() => { - console.log('📊 [Records] useEffect 실행:', { - activeRange, - selectedWeek, - selectedMonth, - selectedYear, - timeRangeParams, - }); loadDashboard(timeRangeParams); loadRecords(timeRangeParams, true); }, [activeRange, selectedWeek, selectedMonth, selectedYear]); @@ -79,10 +76,12 @@ export default function Records() { setShowDropdown(!showDropdown); }, [showDropdown]); - const handleRecordPress = useCallback((record: any) => { - // TODO: 기록 상세 페이지로 이동 - console.log('Record pressed:', record); - }, []); + const handleRecordPress = useCallback( + (record: RunningRecord) => { + navigation.navigate('RecordDetail', { recordId: record.id }); + }, + [navigation], + ); const handleDetailSelection = useCallback( (value: number) => { diff --git a/src/pages/Run/_components/ControlContainer.tsx b/src/pages/Run/_components/ControlContainer.tsx index 6080218..7259bf0 100644 --- a/src/pages/Run/_components/ControlContainer.tsx +++ b/src/pages/Run/_components/ControlContainer.tsx @@ -45,19 +45,18 @@ const ControlContainer: React.FC = ({ const handleToggleTracking = () => { if (!isTracking && !startTime) { // 새로운 런닝 시작 - console.log('🎬 [ControlContainer] 새로운 런닝 시작'); startRun(); const initialStats = calculateRunStats([], new Date(), true, 0, null); setRunning({ stats: initialStats }); toggleTracking(); } else if (!isTracking && startTime) { // 일시정지된 런닝 재시작 - 기존 경로와 통계 유지 - console.log('▶️ [ControlContainer] 일시정지된 런닝 재시작'); + resumeRun(); toggleTracking(); } else if (isTracking) { // 런닝 일시정지 - 경로와 통계 유지 - console.log('⏸️ [ControlContainer] 런닝 일시정지'); + pauseRun(); toggleTracking(); } diff --git a/src/pages/Run/_components/NavigationOverlay.tsx b/src/pages/Run/_components/NavigationOverlay.tsx new file mode 100644 index 0000000..c675459 --- /dev/null +++ b/src/pages/Run/_components/NavigationOverlay.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import styled from '@emotion/native'; +import { + NavigationIcon, + ArrowLeft, + ArrowRight, + RotateCcw, +} from 'lucide-react-native'; +import type { NavigationInstruction } from '@/types/courses.types'; + +interface NavigationOverlayProps { + visible: boolean; + message: string; + instruction: NavigationInstruction | null; +} + +const NavigationOverlay: React.FC = ({ + visible, + message, + instruction, +}) => { + if (!visible || !instruction) { + return null; + } + + const getNavigationIcon = () => { + const { direction, scale } = instruction; + const iconColor = '#ffffff'; + const iconSize = 28; + + if (scale === '유턴') { + return ; + } + + return direction === '왼쪽' ? ( + + ) : ( + + ); + }; + + const getBackgroundColor = () => { + // 유턴은 더 강한 색상으로 표시 + if (instruction.scale === '유턴') { + return ['#f59e0b', '#d97706', '#b45309']; // 주황색 그라데이션 + } + + return ['#3b82f6', '#2563eb', '#1d4ed8']; // 파란색 그라데이션 + }; + + return ( + + + + + + + {getNavigationIcon()} + + {message} + + + + + ); +}; + +export default React.memo(NavigationOverlay); + +const NavigationContainer = styled.View({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + zIndex: 1002, + pointerEvents: 'none', +}); + +const NavigationAlert = styled.View<{ colors: string[] }>(({ colors }) => ({ + position: 'absolute', + top: 10, + left: 20, + right: 20, + backgroundColor: colors[0], + paddingHorizontal: 20, + paddingVertical: 16, + borderRadius: 12, + flexDirection: 'row', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, +})); + +const IconContainer = styled.View({ + marginRight: 12, + padding: 8, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', +}); + +const ContentContainer = styled.View({ + flex: 1, + flexDirection: 'row', + alignItems: 'center', +}); + +const DirectionIconContainer = styled.View({ + marginRight: 16, + alignItems: 'center', + justifyContent: 'center', +}); + +const MessageContainer = styled.View({ + flex: 1, +}); + +const NavigationMessage = styled.Text({ + color: '#ffffff', + fontSize: 16, + fontWeight: '700', + lineHeight: 20, + textAlign: 'left', +}); diff --git a/src/pages/Run/_components/StatsContainer.tsx b/src/pages/Run/_components/StatsContainer.tsx index ebf5c11..b6d688d 100644 --- a/src/pages/Run/_components/StatsContainer.tsx +++ b/src/pages/Run/_components/StatsContainer.tsx @@ -4,13 +4,12 @@ import { LinearGradient } from 'expo-linear-gradient'; import useRunStore from '@/store/run'; import { useLocationTracking } from '@/hooks/useLocationTracking'; import { calculateRunStats } from '@/utils/runStats'; -import { formatPace } from '@/utils/formatters'; -import type { Position } from 'geojson'; +import { formatPace, formatNumber } from '@/utils/formatters'; import type { RunStats } from '@/utils/runStats'; function StatsContainer() { const { routeCoordinates, isTracking } = useLocationTracking(); - const { startTime, pausedTime, pauseStartTime } = useRunStore(); + const { startTime, pausedTime, pauseStartTime, setRunning } = useRunStore(); // 로컬 상태로 통계 관리 (전역 상태 업데이트 안함) const [localStats, setLocalStats] = useState({ @@ -66,8 +65,10 @@ function StatsContainer() { if (hasChanged) { lastStatsRef.current = newStats; setLocalStats(newStats); + // 전역 store에도 통계 업데이트 + setRunning({ stats: newStats }); } - }, [startTime]); + }, [startTime, setRunning]); // 초기 통계 계산 useEffect(() => { @@ -99,11 +100,11 @@ function StatsContainer() { - {localStats.distance}m + {formatNumber(localStats.distance)}m 거리 - {localStats.calories} + {formatNumber(localStats.calories)} 칼로리 diff --git a/src/pages/Run/index.tsx b/src/pages/Run/index.tsx index 2059a54..63cdd36 100644 --- a/src/pages/Run/index.tsx +++ b/src/pages/Run/index.tsx @@ -11,10 +11,12 @@ import { useLocationTracking } from '@/hooks/useLocationTracking'; import { useRunModals } from '@/hooks/useRunModals'; import { useCourseTopologyApi } from '@/hooks/api/useCourseTopologyApi'; import { useCourseValidation } from '@/hooks/useCourseValidation'; +import { useNavigation } from '@/hooks/useNavigation'; import useRunStore from '@/store/run'; import RunMap from './_components/RunMap'; import StatsContainer from './_components/StatsContainer'; import ControlContainer from './_components/ControlContainer'; +import NavigationOverlay from './_components/NavigationOverlay'; import Modal from '@/components/Modal'; import Mapbox from '@rnmapbox/maps'; @@ -22,7 +24,17 @@ type Props = NativeStackScreenProps; export default function Run({ route, navigation }: Props) { // 전역 Store에서 현재 코스 데이터 가져오기 - const { currentCourseId, currentCourseData } = useRunStore(); + const { + currentCourseId, + currentCourseData, + courseTopology, + loading, + savingRecord, + topologyError, + saveError, + resetRunState, + startTime, + } = useRunStore(); const courseId = currentCourseId || route.params?.courseId; const { @@ -39,15 +51,6 @@ export default function Run({ route, navigation }: Props) { const cameraRef = useRef(null!); const mapRef = useRef(null); - const { - loading, - savingRecord, - topologyError, - saveError, - resetRunState, - startTime, - } = useRunStore(); - // useRunStats는 StatsContainer에서 처리 const { loadCourseTopology } = useCourseTopologyApi(courseId); @@ -63,13 +66,26 @@ export default function Run({ route, navigation }: Props) { } = useCourseValidation({ courseId, validationOptions: { - tolerance: 5, // 5미터 허용 오차 (매우 엄격하게) + tolerance: 10, // 5미터 허용 오차 (매우 엄격하게) enableDistanceCalculation: true, }, enableRealTimeValidation: true, // 실시간 검증 활성화 validationInterval: 1000, // 1초마다 검증 }); + // 네비게이션 훅 - courseId가 있고 topology 데이터가 있을 때만 활성화 + const { + navigationState, + navigationMessage, + shouldShowNavigation, + resetNavigation, + } = useNavigation({ + nodes: courseTopology?.nodes || [], + routeCoordinates, + isTracking, + enabled: !!(courseId && courseTopology), + }); + const { showExitModal, showBackModal, @@ -88,17 +104,23 @@ export default function Run({ route, navigation }: Props) { useFocusEffect( useCallback(() => { - // 런닝이 시작되지 않은 상태에서만 초기화 - const { startTime } = useRunStore.getState(); - if (!startTime) { - resetLocationTracking(); - resetRunState(); - } + // 스크린 진입 시 항상 완전 초기화 (이전 상태 완전 제거) + + resetLocationTracking(); + resetRunState(); // isTracking이 false이므로 완전 초기화 + resetNavigation(); // 네비게이션 상태도 초기화 + // courseId가 있을 때만 경로 데이터를 로드 if (courseId) { loadCourseTopology(); } - }, [resetLocationTracking, resetRunState, courseId, loadCourseTopology]), + }, [ + resetLocationTracking, + resetRunState, + resetNavigation, + courseId, + loadCourseTopology, + ]), ); // 위치가 로드되지 않았을 때 자동으로 새로고침 @@ -196,6 +218,14 @@ export default function Run({ route, navigation }: Props) { )} + {/* 네비게이션 안내 - courseId가 있고 이탈하지 않았을 때만 */} + {courseId && !isDeviating && ( + + )} ) : ( diff --git a/src/pages/RunDetail/index.tsx b/src/pages/RunDetail/index.tsx new file mode 100644 index 0000000..868f683 --- /dev/null +++ b/src/pages/RunDetail/index.tsx @@ -0,0 +1,436 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + Image, + ScrollView, + TouchableOpacity, + Alert, + ActivityIndicator, + Modal, +} from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { Share, Home, Upload } from 'lucide-react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import styled from '@emotion/native'; +import { useCameraPermission, Camera } from 'react-native-vision-camera'; +import { PermissionsAndroid, Platform, Linking } from 'react-native'; + +import Header from '@/components/Header'; +import CameraComponent from '@/components/Camera'; +import { + formatDistance, + formatTime, + formatPace, + formatNumber, +} from '@/utils/formatters'; +import { COLOR_TOKENS } from '@/constants/colors'; +import type { RunTabStackParamList } from '@/types/navigation.types'; +import { LinearGradient } from 'expo-linear-gradient'; + +type Props = NativeStackScreenProps; + +export default function RunDetail({ route, navigation }: Props) { + const { recordId, imageUrl, path, stats, startAt, endAt } = route.params; + const insets = useSafeAreaInsets(); + const [imageError, setImageError] = useState(false); + const [imageLoading, setImageLoading] = useState(true); + const [showCamera, setShowCamera] = useState(false); + const [cameraPermissionChecked, setCameraPermissionChecked] = useState(false); + + const { hasPermission: hasCameraPermission } = useCameraPermission(); + + const requestNativeCameraPermission = async () => { + if (Platform.OS === 'android') { + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.CAMERA, + { + title: '카메라 권한', + message: '인증사진 촬영을 위해 카메라 권한이 필요합니다.', + buttonNeutral: '나중에', + buttonNegative: '취소', + buttonPositive: '확인', + }, + ); + + if (granted === PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN) { + Alert.alert( + '카메라 권한 필요', + '카메라 권한이 거부되었습니다. 설정 > 앱 > Runova > 권한에서 카메라 권한을 허용해주세요.', + [ + { text: '취소', style: 'cancel' }, + { + text: '설정으로 이동', + onPress: () => { + Linking.openSettings(); + }, + }, + ], + ); + return false; + } + + return granted === PermissionsAndroid.RESULTS.GRANTED; + } catch (error) { + return false; + } + } else { + try { + const permission = await Camera.requestCameraPermission(); + + if (permission === 'denied') { + Alert.alert( + '카메라 권한 필요', + '카메라 권한이 거부되었습니다. 설정 > 개인정보 보호 및 보안 > 카메라에서 Runova 앱의 카메라 권한을 허용해주세요.', + [ + { text: '취소', style: 'cancel' }, + { + text: '설정으로 이동', + onPress: () => { + Linking.openSettings(); + }, + }, + ], + ); + } + + return permission === 'granted'; + } catch (error) { + return false; + } + } + }; + + useEffect(() => { + const checkCameraPermission = async () => { + if (Platform.OS === 'android') { + try { + const currentPermission = await PermissionsAndroid.check( + PermissionsAndroid.PERMISSIONS.CAMERA, + ); + } catch (error) { + console.error('📷 [RunDetail] 권한 상태 확인 실패:', error); + } + } + setCameraPermissionChecked(true); + }; + + checkCameraPermission(); + }, [hasCameraPermission]); + + const handleHomePress = () => { + // RunTabNavigator 내에서 QuickStartMain으로 직접 이동 + navigation.reset({ + index: 0, + routes: [{ name: 'QuickStartMain' }], + }); + }; + + const handleSharePress = () => { + Alert.alert('공유', '런닝 기록을 공유합니다.'); + }; + + const handleCameraPress = async () => { + if (!cameraPermissionChecked) { + Alert.alert('카메라 권한 확인 중', '잠시만 기다려주세요.'); + return; + } + + if (!hasCameraPermission) { + const granted = await requestNativeCameraPermission(); + + if (!granted) { + Alert.alert( + '카메라 권한 필요', + '인증사진 촬영을 위해 카메라 권한이 필요합니다.', + ); + return; + } + } + + setShowCamera(true); + }; + + const handlePhotoTaken = (photoUri: string) => { + setShowCamera(false); + navigation.navigate('PhotoEdit', { + photoUri, + recordId, + path, + stats, + }); + }; + + const handleCameraClose = () => { + setShowCamera(false); + }; + + const handleImageError = () => { + setImageError(true); + setImageLoading(false); + }; + + const handleImageLoad = () => { + setImageLoading(false); + }; + + const formatDateTime = (dateString: string): string => { + const date = new Date(dateString); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + return `${year}년 ${month}월 ${day}일 ${hours}:${minutes}`; + }; + + return ( + +
+ + + + {imageUrl && !imageError && ( + + )} + {(imageError || !imageUrl) && ( + + 런닝 기록 #{recordId} + + )} + {imageLoading && imageUrl && !imageError && ( + + + + )} + + + + 런닝 통계 + + + + 거리 + {formatDistance(stats.distance)} + + + + 시간 + {stats.runningTime} + + + + 평균 페이스 + {formatPace(stats.pace)} + + + + 칼로리 + {formatNumber(stats.calories)}kcal + + + + + {/* 런닝 정보 */} + + 런닝 정보 + + + 시작 시간 + + {startAt ? formatDateTime(startAt) : '정보 없음'} + + + + + 종료 시간 + + {endAt ? formatDateTime(endAt) : '정보 없음'} + + + + + + + + + + + 인증사진 찍기 + + + + + + + + + ); +} + +const Container = styled.View({ + flex: 1, + backgroundColor: '#ffffff', +}); + +const ImageContainer = styled.View({ + height: 400, + backgroundColor: COLOR_TOKENS.gray[100], +}); + +const RouteImage = styled(Image)({ + width: '100%', + height: '100%', + resizeMode: 'cover', +}); + +const FallbackContainer = styled.View({ + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: COLOR_TOKENS.gray[100], +}); + +const FallbackText = styled.Text({ + fontSize: 16, + color: COLOR_TOKENS.gray[500], + textAlign: 'center', +}); + +const LoadingImageContainer = styled.View({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: COLOR_TOKENS.gray[100], +}); + +const StatsContainer = styled.View({ + padding: 20, + backgroundColor: '#ffffff', +}); + +const StatsTitle = styled.Text({ + fontSize: 20, + fontWeight: '700', + color: COLOR_TOKENS.gray[900], + marginBottom: 16, +}); + +const StatsGrid = styled.View({ + flexDirection: 'row', + flexWrap: 'wrap', + gap: 16, +}); + +const StatItem = styled.View({ + flex: 1, + minWidth: '45%', + backgroundColor: COLOR_TOKENS.gray[50], + padding: 16, + borderRadius: 12, + alignItems: 'center', +}); + +const StatLabel = styled.Text({ + fontSize: 14, + color: COLOR_TOKENS.gray[600], + marginBottom: 4, +}); + +const StatValue = styled.Text({ + fontSize: 18, + fontWeight: '700', + color: COLOR_TOKENS.gray[900], +}); + +const BottomContainer = styled.View({ + padding: 20, + backgroundColor: '#ffffff', +}); + +const UploadButton = styled.TouchableOpacity({ + height: 56, + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + overflow: 'hidden', +}); + +const UploadButtonGradient = styled(LinearGradient)({ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + borderRadius: 8, +}); + +const UploadButtonContent = styled.View({ + flexDirection: 'row', + alignItems: 'center', + gap: 8, + zIndex: 1, +}); + +const UploadText = styled.Text({ + color: '#ffffff', + fontSize: 16, + fontWeight: '600', +}); + +const RunInfoContainer = styled.View({ + padding: 20, + backgroundColor: '#ffffff', +}); + +const RunInfoTitle = styled.Text({ + fontSize: 18, + fontWeight: '700', + color: COLOR_TOKENS.gray[900], + marginBottom: 16, +}); + +const RunInfoItem = styled.View({ + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: COLOR_TOKENS.gray[100], +}); + +const RunInfoLabel = styled.Text({ + fontSize: 16, + color: COLOR_TOKENS.gray[600], +}); + +const RunInfoValue = styled.Text({ + fontSize: 16, + fontWeight: '600', + color: COLOR_TOKENS.gray[900], +}); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index df5e879..e7b93f6 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -17,105 +17,30 @@ export interface RefreshTokenResponse { export function initializeGoogleSignIn(): void { try { - console.log('🔧 Google Sign-In initialization started'); - const webClientId = process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID; const iosClientId = process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID; - // CRITICAL: Always log the Web Client ID - console.error('EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID:', webClientId); - console.error('EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID:', iosClientId); - - console.log('Web Client ID status:', webClientId ? 'SET' : '❌ NOT SET'); - console.log('iOS Client ID status:', iosClientId ? 'SET' : '❌ NOT SET'); - - if (webClientId) { - console.log('Web Client ID length:', webClientId.length); - console.log( - 'Web Client ID first 20 chars:', - webClientId.substring(0, 20) + '...', - ); - } - - if (iosClientId) { - console.log('iOS Client ID length:', iosClientId.length); - console.log( - 'iOS Client ID first 20 chars:', - iosClientId.substring(0, 20) + '...', - ); - } - - if (!webClientId) { - console.error('❌ Web Client ID is NOT SET!'); - console.error( - 'Check EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID environment variable', - ); - } - - if (!iosClientId) { - console.error('❌ iOS Client ID is NOT SET!'); - console.error( - 'Check EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID environment variable', - ); - } - const config = { webClientId, iosClientId, profileImageSize: 120, }; - console.log('Google Sign-In config:', config); GoogleSignin.configure(config); - - console.log('✅ Google Sign-In initialization completed'); } catch (error) { - console.error('❌ Google Sign-In initialization error:', error); throw error; } } export async function signInWithGoogle(): Promise { try { - console.log('Google login started'); - - console.log('Checking Play Services...'); await GoogleSignin.hasPlayServices(); - console.log('Play Services check completed'); - console.log('Attempting Google login...'); const signInResult = await GoogleSignin.signIn(); - console.log('Google login successful:', signInResult); - console.log('Getting tokens...'); const tokens = await GoogleSignin.getTokens(); - console.log('Full token response:', tokens); const { idToken, accessToken } = tokens; - console.log('ID Token status:', idToken ? 'ISSUED' : 'NOT ISSUED'); - console.log('Access Token status:', accessToken ? 'ISSUED' : 'NOT ISSUED'); - - if (idToken) { - console.log('ID Token length:', idToken.length); - console.log('ID Token first 20 chars:', idToken.substring(0, 20) + '...'); - } - - if (!idToken) { - console.error('❌ ID Token was NOT issued'); - console.error('Possible causes:'); - console.error('1. Google Console OAuth client ID configuration error'); - console.error('2. Production bundle ID not registered in Google Console'); - console.error('3. Redirect URI configuration error'); - console.error('4. Wrong client ID in .env file'); - console.error('5. OAuth consent screen not published to production'); - throw new Error('Failed to receive ID token during Google login.'); - } - - console.log('Sending login request to server...'); - console.log('Request URL:', '/auth/google'); - console.log('Request payload:', { - idToken: idToken.substring(0, 20) + '...', - }); let data: GoogleSignInResponse; try { @@ -123,28 +48,13 @@ export async function signInWithGoogle(): Promise { idToken, }); data = response.data; - console.log('✅ Server login successful:', data); } catch (serverError: any) { - console.error('❌ Server login failed:', serverError); - console.error('Server response status:', serverError.response?.status); - console.error('Server response data:', serverError.response?.data); - console.error('Server response headers:', serverError.response?.headers); - if (serverError.response?.status === 400) { console.error('400 Error - Possible causes:'); - console.error('1. Invalid ID token format'); - console.error('2. Google Console OAuth configuration error'); - console.error('3. Client ID mismatch between server and app'); - console.error('4. Wrong OAuth consent screen configuration'); } else if (serverError.response?.status === 401) { console.error('401 Error - Possible causes:'); - console.error('1. ID token expired'); - console.error('2. Google token verification failed on server'); - console.error('3. Server Google OAuth configuration error'); - console.error('4. Invalid client ID or secret on server'); } else if (serverError.response?.status === 500) { console.error('500 Error - Server internal error'); - console.error('Check server logs for details'); } throw serverError; @@ -152,57 +62,27 @@ export async function signInWithGoogle(): Promise { return data; } catch (error) { - console.error('Google login overall error:', error); - - if (isErrorWithCode(error)) { - console.error('Google login code error:', error.code); - switch (error.code) { - case statusCodes.SIGN_IN_CANCELLED: - console.error('User cancelled login'); - throw new Error('User cancelled login.'); - case statusCodes.IN_PROGRESS: - console.error('Login in progress'); - throw new Error('Login in progress.'); - case statusCodes.PLAY_SERVICES_NOT_AVAILABLE: - console.error('Play Services not available'); - throw new Error( - 'Google Play Services is not available or version is outdated.', - ); - default: - console.error('Unknown Google login error:', error.code); - throw new Error('Error occurred during Google login.'); - } - } - - console.error('General login error:', error); throw new Error('Error occurred during login. Please try again.'); } } export async function refreshToken(): Promise { try { - console.log('Token refresh started'); const { data } = await api.post('/auth/refresh'); - console.log('Token refresh response:', data); if (!data.accessToken) { - console.error('Token refresh failed: accessToken is missing'); throw new Error('Token refresh failed.'); } - console.log('Token refresh successful'); return data; } catch (error) { - console.error('Token refresh error:', error); throw new Error('Error occurred during token refresh.'); } } export async function signOut(): Promise { try { - console.log('Google logout started'); await GoogleSignin.signOut(); - console.log('Google logout successful'); } catch (error) { console.error('Google logout error:', error); } @@ -210,12 +90,10 @@ export async function signOut(): Promise { export async function isSignedIn(): Promise { try { - console.log('Checking login status...'); const userInfo = await GoogleSignin.getCurrentUser(); - console.log('Current user info:', userInfo ? 'LOGGED IN' : 'NOT LOGGED IN'); + return userInfo !== null; } catch (error) { - console.error('Login status check error:', error); return false; } } diff --git a/src/services/running.service.ts b/src/services/running.service.ts index 3d56a2a..8ba5369 100644 --- a/src/services/running.service.ts +++ b/src/services/running.service.ts @@ -6,6 +6,7 @@ import type { import type { RunningRecordsRequest, RunningRecordsResponse, + RunningRecordDetail, RunningDashboard, RunningDashboardRequest, } from '@/types/records.types'; @@ -25,18 +26,6 @@ export async function createRunningRecord( const params = courseId ? { courseId } : undefined; const endpoint = '/api/running/records'; - console.log('📤 [RunningService] 런닝 기록 저장 API 요청'); - console.log('📤 [RunningService] 엔드포인트:', endpoint); - console.log('📤 [RunningService] 쿼리 파라미터:', params); - console.log('📤 [RunningService] 요청 페이로드:', { - path: `${requestData.path.length}개 좌표`, - startAt: new Date(requestData.startAt).toISOString(), - endAt: new Date(requestData.endAt).toISOString(), - pace: requestData.pace, - calories: requestData.calories, - imageUrl: requestData.imageUrl ? '있음' : '없음', - }); - const response = await api.post(endpoint, requestData, { headers: { Authorization: `Bearer ${accessToken}`, @@ -45,13 +34,8 @@ export async function createRunningRecord( params, }); - console.log('📥 [RunningService] 런닝 기록 저장 API 응답'); - console.log('📥 [RunningService] 응답 상태:', response.status); - console.log('📥 [RunningService] 응답 데이터:', response.data); - return response.data; } catch (error: unknown) { - console.error('❌ [RunningService] 런닝 기록 저장 API 오류:', error); throw error; } } @@ -69,6 +53,18 @@ export async function searchRunningRecords( return response.data; } +export async function getRunningRecordDetail( + recordId: number, + accessToken: string, +): Promise { + const response = await api.get(`/api/running/records/${recordId}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; +} + export async function getRunningDashboard( params: RunningDashboardRequest, accessToken: string, diff --git a/src/store/run.ts b/src/store/run.ts index 9785915..dc34252 100644 --- a/src/store/run.ts +++ b/src/store/run.ts @@ -187,7 +187,7 @@ const useRunStore = create((set, get) => ({ startRun: () => { const startTime = new Date(); - console.log('🏃‍♂️ [RunStore] 새로운 런닝 시작:', startTime.toISOString()); + set({ startTime, pausedTime: 0, @@ -197,7 +197,7 @@ const useRunStore = create((set, get) => ({ pauseRun: () => { const pauseStartTime = new Date(); - console.log('⏸️ [RunStore] 런닝 일시정지:', pauseStartTime.toISOString()); + set({ pauseStartTime, }); @@ -208,10 +208,7 @@ const useRunStore = create((set, get) => ({ if (state.pauseStartTime) { const pauseDuration = (new Date().getTime() - state.pauseStartTime.getTime()) / 1000; - console.log('▶️ [RunStore] 런닝 재시작:', { - pauseDuration: `${pauseDuration.toFixed(1)}초`, - totalPausedTime: `${(state.pausedTime + pauseDuration).toFixed(1)}초`, - }); + return { pausedTime: state.pausedTime + pauseDuration, pauseStartTime: null, @@ -221,7 +218,6 @@ const useRunStore = create((set, get) => ({ }), stopRun: () => { - console.log('⏹️ [RunStore] 런닝 완전 종료 - 통계 초기화됨'); set({ startTime: null, pausedTime: 0, @@ -295,9 +291,8 @@ const useRunStore = create((set, get) => ({ resetRunState: () => { set((state) => { - // 런닝이 진행 중이면 상태를 보존 - if (state.startTime) { - console.log('🔄 [RunStore] 런닝 상태 보존 - 통계 초기화되지 않음'); + // isTracking이 true면 일시정지 상태이므로 상태 보존 + if (state.isTracking) { return { ...initialUIState, ...initialErrorState, @@ -318,14 +313,13 @@ const useRunStore = create((set, get) => ({ }; } - // 런닝이 시작되지 않은 경우에만 완전 초기화 - console.log('🔄 [RunStore] 런닝 상태 완전 초기화 - 통계 초기화됨'); + // isTracking이 false면 완전 초기화 + return { ...initialUIState, ...initialErrorState, ...initialRunningState, - // 코스 데이터는 유지 (currentCourseId, currentCourseData 보존) - courseTopology: null, + ...initialCourseState, ...initialLocationTrackingState, ...initialCourseValidationState, }; diff --git a/src/types/camera.types.ts b/src/types/camera.types.ts new file mode 100644 index 0000000..15d6301 --- /dev/null +++ b/src/types/camera.types.ts @@ -0,0 +1,11 @@ +export interface CameraPhoto { + uri: string; + width: number; + height: number; + fileSize?: number; +} + +export interface PhotoEditParams { + photoUri: string; + recordId: number; +} diff --git a/src/types/courses.types.ts b/src/types/courses.types.ts index 77b2fde..96bdde8 100644 --- a/src/types/courses.types.ts +++ b/src/types/courses.types.ts @@ -109,3 +109,22 @@ export interface CourseTopologyResponse { nodes: CourseTopologyNode[]; shape: [number, number][][]; } + +// 네비게이션 관련 타입들 +export type NavigationDirection = '왼쪽' | '오른쪽'; +export type NavigationScale = '회전' | '유턴'; + +export interface NavigationInstruction { + direction: NavigationDirection; + scale: NavigationScale; + distance?: number; + type: 'turn' | 'warning'; +} + +export interface NavigationState { + currentNodeIndex: number; + nextNode: CourseTopologyNode | null; + distanceToNextNode: number; + isApproachingTurn: boolean; + instruction: NavigationInstruction | null; +} diff --git a/src/types/navigation.types.ts b/src/types/navigation.types.ts index 1fbd9b8..18afc05 100644 --- a/src/types/navigation.types.ts +++ b/src/types/navigation.types.ts @@ -17,9 +17,52 @@ export type RootStackParamList = { export type TabParamList = { Home: Record; Records: Record; - RunTab: Record; + Run: Record; Community: Record; Settings: Record; }; export type RouteTabId = 'created' | 'completed' | 'liked'; + +export type RunTabStackParamList = { + QuickStartMain: undefined; + Draw: undefined; + RouteSave: undefined; + Detail: { id: string }; + Run: { courseId?: number } | undefined; + RunDetail: { + recordId: number; + imageUrl?: string; + path?: [number, number][]; + stats: { + distance: number; + calories: number; + pace: number; + runningTime: string; + }; + startAt?: string; + endAt?: string; + }; + PhotoEdit: { + photoUri: string; + recordId: number; + path?: [number, number][]; + stats: { + distance: number; + calories: number; + pace: number; + runningTime: string; + }; + }; + PhotoDecoration: { + photoUri: string; + recordId: number; + path?: [number, number][]; + stats: { + distance: number; + calories: number; + pace: number; + runningTime: string; + }; + }; +}; diff --git a/src/types/photoDecoration.types.ts b/src/types/photoDecoration.types.ts new file mode 100644 index 0000000..906f298 --- /dev/null +++ b/src/types/photoDecoration.types.ts @@ -0,0 +1,106 @@ +import { ViewStyle } from 'react-native'; + +export interface PhotoDecorationParams { + photoUri: string; + recordId: number; + path?: [number, number][]; + stats: { + distance: number; + calories: number; + pace: number; + runningTime: string; + }; +} + +export interface RunningStats { + distance: number; + time: string; + pace: string; + calories: number; + date: string; +} + +export interface ColorOption { + id: string; + name: string; + color: string; +} + +export interface TemplateOption { + id: string; + name: string; + color: string; +} + +// 개선된 상태 구조 +export interface PhotoDecorationState { + // 배경 설정 + backgroundType: 'photo' | 'solid'; + backgroundColor: string; + + // 색상 설정 + colors: { + route: string; + stats: string; + timestamp: string; + }; + + // 표시 요소 설정 + visibility: { + time: boolean; + distance: boolean; + pace: boolean; + calories: boolean; + route: boolean; + timestamp: boolean; + }; + + // 크기 설정 + scales: { + stats: number; + route: number; + timestamp: number; + }; + + // 계산된 데이터 + routePathData: string; + + // UI 상태 + isProcessing: boolean; +} + +export interface SizeControlProps { + value: number; + onValueChange: (value: number) => void; + minimumValue?: number; + maximumValue?: number; + step?: number; +} + +export interface PanResponderConfig { + translateX: any; // Animated.Value + translateY: any; // Animated.Value + scale: number; +} + +export interface DecorationElement { + type: 'stats' | 'route' | 'timestamp'; + visible: boolean; + scale: number; + color: string; + position: { + x: number; + y: number; + }; +} + +// 업데이트 헬퍼 함수들의 타입 +export type UpdateColorsFunction = ( + colorUpdates: Partial, +) => void; +export type UpdateVisibilityFunction = ( + visibilityUpdates: Partial, +) => void; +export type UpdateScalesFunction = ( + scaleUpdates: Partial, +) => void; diff --git a/src/types/records.types.ts b/src/types/records.types.ts index 256b875..0d27c05 100644 --- a/src/types/records.types.ts +++ b/src/types/records.types.ts @@ -4,6 +4,22 @@ export interface RunningRecord { imageUrl: string; } +export interface RunningRecordDetail { + id: number; + artUrl: string; + imageUrl: string; + distance: number; + startAt: string; + endAt: string; + duration: number; + pace: number; + calories: number; + course?: { + id: number; + title: string; + }; +} + export interface RunningRecordsResponse { results: RunningRecord[]; nextCursor: { diff --git a/src/utils/courseValidation.ts b/src/utils/courseValidation.ts index ecd231d..57af830 100644 --- a/src/utils/courseValidation.ts +++ b/src/utils/courseValidation.ts @@ -87,18 +87,6 @@ export function validateLocationOnCourse( // tolerance를 고려한 최종 판정 const isOnCourse = isInside || distanceFromCourse <= tolerance; - // 디버깅 로그 - console.log('🔍 코스 검증 결과:', { - currentLocation: [ - currentLocation[0].toFixed(6), - currentLocation[1].toFixed(6), - ], - isInside, - distanceFromCourse: distanceFromCourse.toFixed(2) + 'm', - tolerance: tolerance + 'm', - finalResult: isOnCourse ? '경로 내부' : '경로 외부', - }); - const result: CourseValidationResult = { isOnCourse, distanceFromCourse, @@ -107,7 +95,6 @@ export function validateLocationOnCourse( return result; } catch (error) { - console.error('코스 검증 중 오류 발생:', error); return { isOnCourse: false, distanceFromCourse: Infinity, diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 1d98ae6..e80512b 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -17,7 +17,7 @@ export const formatDistance = (distance: number): string => { export const formatTime = (time: number): string => { const hours = Math.floor(time / 3600); const minutes = Math.floor((time % 3600) / 60); - const seconds = Math.round((time % 60) * 100) / 100; + const seconds = Math.floor(time % 60); if (hours > 0) { return `${hours}시간 ${minutes}분 ${seconds}초`; @@ -28,10 +28,30 @@ export const formatTime = (time: number): string => { return `${seconds}초`; }; +export const formatTimeFromMinutes = (timeInMinutes: number): string => { + const totalMinutes = Math.floor(timeInMinutes); + const seconds = Math.round((timeInMinutes % 1) * 60); + + const hours = Math.floor(totalMinutes / 60); + const minutes = totalMinutes % 60; + + if (hours > 0) { + return `${hours}시간 ${minutes}분`; + } + if (minutes > 0) { + return `${minutes}분`; + } + return `${seconds}초`; +}; + export const formatPace = (paceSeconds: number): string => { if (paceSeconds === 0) return '0\'00"'; const minutes = Math.floor(paceSeconds / 60); - const seconds = Math.round((paceSeconds % 60) * 100) / 100; - return `${minutes}'${seconds.toFixed(2).padStart(5, '0')}"`; + const seconds = Math.round(paceSeconds % 60); + return `${minutes}'${seconds.toString().padStart(2, '0')}"`; +}; + +export const formatNumber = (value: number): number => { + return Math.round(value * 100) / 100; }; diff --git a/src/utils/navigation.ts b/src/utils/navigation.ts new file mode 100644 index 0000000..a139a89 --- /dev/null +++ b/src/utils/navigation.ts @@ -0,0 +1,215 @@ +import type { + NavigationDirection, + NavigationScale, + NavigationInstruction, + CourseTopologyNode, +} from '@/types/courses.types'; + +// 각도 변화가 10도 미만이면, 직진으로 간주 +const STRAIGHT_THRESHOLD_DEGREE = 10; + +// 회전 안내를 위한 임계값 (미터) +const TURN_WARNING_DISTANCE = 5; + +type RotationInfo = { + direction: NavigationDirection; + scale: NavigationScale; +}; + +/** + * 베어링 각도를 기반으로 회전 정보 생성 + */ +function makeRotationInfo(bearing: number): RotationInfo { + return { + direction: bearing > 0 ? '왼쪽' : '오른쪽', + scale: Math.abs(bearing) > 150 ? '유턴' : '회전', + }; +} + +/** + * 회전 안내 메시지 생성 + */ +export function makeTurnInstruction(bearing: number): string { + if (Math.abs(bearing) < STRAIGHT_THRESHOLD_DEGREE) return ''; + const { direction, scale } = makeRotationInfo(bearing); + return `잠시후 ${direction}으로 ${scale} 있습니다. 앱을 확인해 주세요`; +} + +/** + * 경로 이탈 경고 메시지 생성 + */ +export function makeWarnInstruction(distance: number, bearing: number): string { + const { direction, scale } = makeRotationInfo(bearing); + + return `경로에서 벗어났습니다. ${direction}으로 ${scale} 후 ${distance}m 이동해주세요!`; +} + +/** + * 두 좌표 간의 거리 계산 (미터 단위) + */ +export function calculateDistance( + coord1: [number, number], + coord2: [number, number], +): number { + const R = 6371e3; // 지구 반지름 (미터) + const φ1 = (coord1[1] * Math.PI) / 180; + const φ2 = (coord2[1] * Math.PI) / 180; + const Δφ = ((coord2[1] - coord1[1]) * Math.PI) / 180; + const Δλ = ((coord2[0] - coord1[0]) * Math.PI) / 180; + + const a = + Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; +} + +/** + * 두 좌표 간의 베어링 계산 (도 단위) + */ +export function calculateBearing( + coord1: [number, number], + coord2: [number, number], +): number { + const φ1 = (coord1[1] * Math.PI) / 180; + const φ2 = (coord2[1] * Math.PI) / 180; + const Δλ = ((coord2[0] - coord1[0]) * Math.PI) / 180; + + const y = Math.sin(Δλ) * Math.cos(φ2); + const x = + Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ); + + const θ = Math.atan2(y, x); + + return ((θ * 180) / Math.PI + 360) % 360; +} + +/** + * 현재 위치에서 가장 가까운 노드 찾기 + */ +export function findNearestNode( + currentLocation: [number, number], + nodes: CourseTopologyNode[], +): { nodeIndex: number; distance: number } { + let minDistance = Infinity; + let nearestNodeIndex = 0; + + nodes.forEach((node, index) => { + const distance = calculateDistance(currentLocation, node.location); + if (distance < minDistance) { + minDistance = distance; + nearestNodeIndex = index; + } + }); + + return { nodeIndex: nearestNodeIndex, distance: minDistance }; +} + +/** + * 다음 회전 노드까지의 거리와 방향 계산 (진행 방향 고려) + */ +export function getNextTurnInfo( + currentLocation: [number, number], + currentNodeIndex: number, + nodes: CourseTopologyNode[], +): { + nextNodeIndex: number | null; + distance: number; + instruction: NavigationInstruction | null; +} { + // 다음 노드가 없으면 null 반환 + if (currentNodeIndex >= nodes.length - 1) { + return { nextNodeIndex: null, distance: 0, instruction: null }; + } + + // 현재 노드에서 다음 노드로의 진행 방향 계산 + const currentBearing = calculateBearing( + nodes[currentNodeIndex].location, + currentLocation, + ); + + // 진행 방향을 고려하여 다음 회전점 찾기 + let nextTurnNodeIndex = currentNodeIndex + 1; + let nextTurnNode = nodes[nextTurnNodeIndex]; + + // 진행 방향에 맞는 회전점을 찾을 때까지 반복 + while (nextTurnNodeIndex < nodes.length - 1) { + const nodeBearing = calculateBearing( + currentLocation, + nextTurnNode.location, + ); + + // 진행 방향과 노드 방향의 차이가 90도 이내인 경우 (앞쪽에 있는 노드) + const bearingDiff = Math.abs(currentBearing - nodeBearing); + const isAhead = bearingDiff <= 90 || bearingDiff >= 270; + + if ( + isAhead && + Math.abs(nextTurnNode.bearing) >= STRAIGHT_THRESHOLD_DEGREE + ) { + break; // 회전이 필요한 노드를 찾음 + } + + nextTurnNodeIndex++; + nextTurnNode = nodes[nextTurnNodeIndex]; + } + + // 회전점을 찾지 못했으면 null 반환 + if (nextTurnNodeIndex >= nodes.length) { + return { nextNodeIndex: null, distance: 0, instruction: null }; + } + + const distance = calculateDistance(currentLocation, nextTurnNode.location); + + // 직진이면 안내하지 않음 + if (Math.abs(nextTurnNode.bearing) < STRAIGHT_THRESHOLD_DEGREE) { + return { nextNodeIndex: nextTurnNodeIndex, distance, instruction: null }; + } + + const { direction, scale } = makeRotationInfo(nextTurnNode.bearing); + + const instruction: NavigationInstruction = { + direction, + scale, + distance: Math.round(distance), + type: 'turn', + }; + + return { + nextNodeIndex: nextTurnNodeIndex, + distance, + instruction, + }; +} + +/** + * 회전 안내가 필요한지 확인 + */ +export function shouldShowTurnWarning( + distanceToNextNode: number, + instruction: NavigationInstruction | null, +): boolean { + return ( + instruction !== null && + distanceToNextNode <= TURN_WARNING_DISTANCE && + distanceToNextNode > 0 + ); +} + +/** + * 네비게이션 안내 메시지 생성 + */ +export function generateNavigationMessage( + instruction: NavigationInstruction, + distance: number, +): string { + const { direction, scale } = instruction; + + if (instruction.type === 'warning') { + return makeWarnInstruction(distance, direction === '왼쪽' ? 90 : -90); + } + + // 5m 이내에서만 표시되므로 간단한 메시지 + return `${direction}으로 ${scale}하세요`; +} diff --git a/src/utils/photoDecorationUtils.ts b/src/utils/photoDecorationUtils.ts new file mode 100644 index 0000000..e92f2c7 --- /dev/null +++ b/src/utils/photoDecorationUtils.ts @@ -0,0 +1,127 @@ +import { + RunningStats, + ColorOption, + TemplateOption, +} from '@/types/photoDecoration.types'; +import { formatPace } from '@/utils/formatters'; + +/** + * path 배열을 SVG path 데이터로 변환 + */ +export function convertPathToSvgData(path: [number, number][]): string { + if (!path || path.length === 0) { + return ''; + } + + console.log( + '📊 [PhotoDecorationUtils] convertPathToSvgData 실행, path:', + path.length, + '개 좌표', + '첫 번째 좌표:', + path[0], + '마지막 좌표:', + path[path.length - 1], + ); + + const minLng = Math.min(...path.map((p) => p[0])); + const maxLng = Math.max(...path.map((p) => p[0])); + const minLat = Math.min(...path.map((p) => p[1])); + const maxLat = Math.max(...path.map((p) => p[1])); + + const normalizedPath = path.map(([lng, lat]) => { + const x = ((lng - minLng) / (maxLng - minLng)) * 80 + 10; + const y = ((lat - minLat) / (maxLat - minLat)) * 60 + 10; + return [x, y]; + }); + + const pathData = normalizedPath + .map(([x, y], index) => (index === 0 ? `M${x},${y}` : `L${x},${y}`)) + .join(' '); + + console.log('📊 [PhotoDecorationUtils] SVG 경로 데이터 생성 완료:', pathData); + + return pathData; +} + +/** + * 런닝 통계 데이터를 포맷팅 + */ +export function formatRunningStats(routeStats: any): RunningStats { + return routeStats + ? { + distance: routeStats.distance / 1000, + time: routeStats.runningTime, + pace: routeStats.pace ? formatPace(routeStats.pace) : '0\'00"', + calories: routeStats.calories, + date: new Date().toLocaleDateString('ko-KR'), + } + : { + distance: 0, + time: '00:00:00', + pace: '0\'00"', + calories: 0, + date: new Date().toLocaleDateString('ko-KR'), + }; +} + +/** + * 템플릿 옵션 데이터 + */ +export function getTemplateOptions(): TemplateOption[] { + return [ + { id: 'minimal', name: '미니멀', color: '#000000' }, + { id: 'vibrant', name: '바이브런트', color: '#FF6B6B' }, + { id: 'nature', name: '네이처', color: '#4ECDC4' }, + { id: 'dark', name: '다크', color: '#2C3E50' }, + ]; +} + +/** + * 배경색 옵션 데이터 + */ +export function getBackgroundColorOptions(): ColorOption[] { + return [ + { id: 'black', name: '검정', color: '#000000' }, + { id: 'white', name: '흰색', color: '#FFFFFF' }, + { id: 'blue', name: '파랑', color: '#2196F3' }, + { id: 'green', name: '초록', color: '#4CAF50' }, + { id: 'red', name: '빨강', color: '#F44336' }, + { id: 'purple', name: '보라', color: '#9C27B0' }, + { id: 'orange', name: '주황', color: '#FF9800' }, + { id: 'pink', name: '분홍', color: '#E91E63' }, + ]; +} + +/** + * 경로 색상 옵션 데이터 + */ +export function getRouteColorOptions(): ColorOption[] { + return [ + { id: 'white', name: '흰색', color: '#FFFFFF' }, + { id: 'black', name: '검정', color: '#000000' }, + { id: 'yellow', name: '노랑', color: '#FFEB3B' }, + { id: 'purple', name: '보라', color: '#9C27B0' }, + { id: 'red', name: '빨강', color: '#F44336' }, + { id: 'blue', name: '파랑', color: '#2196F3' }, + { id: 'green', name: '초록', color: '#4CAF50' }, + ]; +} + +/** + * 공유 옵션 생성 + */ +export function createShareOptions(uri: string) { + return { + title: 'Runova 런닝 기록', + message: '런닝 완주 인증! 🏃‍♂️💪\n#Runova #러닝 #운동기록', + url: `file://${uri}`, + type: 'image/*', + }; +} + +/** + * 기본 SVG 경로 데이터 (테스트용) + */ +export function getDefaultPathData(): string { + return 'M20,20 L80,20 L80,60 L20,60 Z'; +} diff --git a/src/utils/svgExport.ts b/src/utils/svgExport.ts index 87053b0..6ceb055 100644 --- a/src/utils/svgExport.ts +++ b/src/utils/svgExport.ts @@ -45,3 +45,40 @@ export function convertRoutesToSvg( return `${groupElement}`; } + +export function convertRoutesToPathData( + routes: Feature[], + options: SvgExportOptions, +): string { + const { width, height, padding = 20 } = options; + + if (routes.length === 0) { + return ''; + } + + const featureCollection: FeatureCollection = { + type: 'FeatureCollection', + features: routes, + }; + + const projection = geoMercator().fitSize( + [width - padding * 2, height - padding * 2], + featureCollection, + ); + + const pathGenerator = geoPath(projection); + + const pathData = featureCollection.features + .map((feature) => { + const path = pathGenerator(feature); + if (!path || path.includes('NaN') || path.includes('Infinity')) { + console.warn('Invalid path data generated:', path); + return null; + } + return path; + }) + .filter(Boolean) + .join(' '); + + return pathData || ''; +}