ryuyoonju
07-29
1차커밋
@8f3ab1079c33d86d5853c52c05abbc5259c5096f
+++ App.js
... | ... | @@ -0,0 +1,28 @@ |
1 | + | |
2 | +import React from 'react'; | |
3 | +import { NavigationContainer } from '@react-navigation/native'; | |
4 | +import { createStackNavigator } from '@react-navigation/stack'; | |
5 | + | |
6 | +import LoginScreen from './src/screen/LoginScreen'; | |
7 | +import SelectionScreen from './src/screen/SelectionScreen'; | |
8 | +import CameraScreen from './src/screen/CameraScreen'; | |
9 | +import GpsScreen from './src/screen/GpsScreen'; | |
10 | +import AnalysisScreen from './src/screen/AnalysisScreen'; | |
11 | +import AgreementScreen from './src/screen/AgreementScreen'; | |
12 | + | |
13 | +const Stack = createStackNavigator(); | |
14 | + | |
15 | +export default function App() { | |
16 | + return ( | |
17 | + <NavigationContainer> | |
18 | + <Stack.Navigator initialRouteName="Login"> | |
19 | + <Stack.Screen name="Login" component={LoginScreen} /> | |
20 | + <Stack.Screen name="Selection" component={SelectionScreen} /> | |
21 | + <Stack.Screen name="Camera" component={CameraScreen} /> | |
22 | + <Stack.Screen name="Gps" component={GpsScreen} /> | |
23 | + <Stack.Screen name="Analysis" component={AnalysisScreen} /> | |
24 | + <Stack.Screen name="Agreement" component={AgreementScreen} /> | |
25 | + </Stack.Navigator> | |
26 | + </NavigationContainer> | |
27 | + ); | |
28 | +}(파일 끝에 줄바꿈 문자 없음) |
--- App.tsx
... | ... | @@ -1,84 +0,0 @@ |
1 | -import React from 'react'; | |
2 | -import {NavigationContainer} from '@react-navigation/native'; | |
3 | -import { | |
4 | - createNativeStackNavigator, | |
5 | - NativeStackNavigationProp, | |
6 | -} from '@react-navigation/native-stack'; | |
7 | -import {useNavigation} from '@react-navigation/native'; | |
8 | - | |
9 | -import SendLocation from './src/screens/SendLocation.tsx'; | |
10 | -import CameraScreen from './src/screens/CameraScreen.tsx'; | |
11 | -import LoginScreen from './src/screens/LoginScreen.tsx'; | |
12 | -import AgreementScreen from './src/screens/AgreementScreen.tsx'; | |
13 | - | |
14 | -import AnalysisScreen from './src/screens/AnalysisScreen.tsx'; | |
15 | - | |
16 | -import {verifyTokens} from './src/utils/loginUtils.ts'; | |
17 | -import {RootStackParam} from './src/utils/RootStackParam.ts'; | |
18 | - | |
19 | -const Stack = createNativeStackNavigator<RootStackParam>(); | |
20 | - | |
21 | -const RootNavigator = () => { | |
22 | - const navigation = useNavigation<NativeStackNavigationProp<RootStackParam>>(); | |
23 | - | |
24 | - React.useEffect(() => { | |
25 | - const checkTokens = async () => { | |
26 | - await verifyTokens(navigation); | |
27 | - }; | |
28 | - checkTokens(); | |
29 | - }, [navigation]); | |
30 | - | |
31 | - return ( | |
32 | - <Stack.Navigator | |
33 | - initialRouteName="LoginScreen" | |
34 | - screenOptions={{ | |
35 | - headerShown: false, | |
36 | - }}> | |
37 | - <Stack.Screen name="SendLocation" component={SendLocation} /> | |
38 | - <Stack.Screen | |
39 | - name="CameraScreen" | |
40 | - component={CameraScreen} | |
41 | - options={{ | |
42 | - headerShown: true, | |
43 | - headerTransparent: true, | |
44 | - headerTintColor: '#ffffff', | |
45 | - title: '', | |
46 | - headerStyle: { | |
47 | - backgroundColor: 'rgba(255, 255, 255, 0.3)', | |
48 | - }, | |
49 | - }} | |
50 | - /> | |
51 | - <Stack.Screen name="LoginScreen" component={LoginScreen} /> | |
52 | - <Stack.Screen | |
53 | - name="AgreementScreen" | |
54 | - component={AgreementScreen} | |
55 | - options={{ | |
56 | - headerShown: true, | |
57 | - headerTransparent: true, | |
58 | - headerTintColor: '#cccccc', | |
59 | - title: '', | |
60 | - headerStyle: { | |
61 | - backgroundColor: 'rgba(255, 255, 255, 0.3)', | |
62 | - }, | |
63 | - }} | |
64 | - /> | |
65 | - <Stack.Screen | |
66 | - name="AnalysisScreen" | |
67 | - component={AnalysisScreen} | |
68 | - options={{ | |
69 | - headerShown: true, | |
70 | - headerTintColor: '#424242', | |
71 | - title: '운행 히스토리', | |
72 | - }} | |
73 | - /> | |
74 | - </Stack.Navigator> | |
75 | - ); | |
76 | -}; | |
77 | - | |
78 | -export default function App() { | |
79 | - return ( | |
80 | - <NavigationContainer> | |
81 | - <RootNavigator /> | |
82 | - </NavigationContainer> | |
83 | - ); | |
84 | -} |
--- android/app/src/main/AndroidManifest.xml
+++ android/app/src/main/AndroidManifest.xml
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 |
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> |
6 | 6 |
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> |
7 | 7 |
<uses-permission android:name="android.permission.CAMERA" /> |
8 |
+ <uses-permission android:name="android.permission.RECORD_AUDIO" /> |
|
8 | 9 |
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> |
9 | 10 |
<uses-permission android:name="android.permission.WAKE_LOCK" /> |
10 | 11 |
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> |
--- android/app/src/main/assets/index.android.bundle
+++ android/app/src/main/assets/index.android.bundle
This diff is too big to display. |
--- android/build.gradle
+++ android/build.gradle
... | ... | @@ -6,6 +6,7 @@ |
6 | 6 |
targetSdkVersion = 34 |
7 | 7 |
ndkVersion = "26.1.10909125" |
8 | 8 |
kotlinVersion = "1.9.22" |
9 |
+ playServicesLocationVersion = "21.0.1" |
|
9 | 10 |
} |
10 | 11 |
repositories { |
11 | 12 |
google() |
--- android/gradlew
+++ android/gradlew
100644 | 100755 | File mode has changed |
... | ... | @@ -1,249 +1,249 @@ |
1 |
-#!/bin/sh |
|
2 |
- |
|
3 |
-# |
|
4 |
-# Copyright © 2015-2021 the original authors. |
|
5 |
-# |
|
6 |
-# Licensed under the Apache License, Version 2.0 (the "License"); |
|
7 |
-# you may not use this file except in compliance with the License. |
|
8 |
-# You may obtain a copy of the License at |
|
9 |
-# |
|
10 |
-# https://www.apache.org/licenses/LICENSE-2.0 |
|
11 |
-# |
|
12 |
-# Unless required by applicable law or agreed to in writing, software |
|
13 |
-# distributed under the License is distributed on an "AS IS" BASIS, |
|
14 |
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
15 |
-# See the License for the specific language governing permissions and |
|
16 |
-# limitations under the License. |
|
17 |
-# |
|
18 |
- |
|
19 |
-############################################################################## |
|
20 |
-# |
|
21 |
-# Gradle start up script for POSIX generated by Gradle. |
|
22 |
-# |
|
23 |
-# Important for running: |
|
24 |
-# |
|
25 |
-# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is |
|
26 |
-# noncompliant, but you have some other compliant shell such as ksh or |
|
27 |
-# bash, then to run this script, type that shell name before the whole |
|
28 |
-# command line, like: |
|
29 |
-# |
|
30 |
-# ksh Gradle |
|
31 |
-# |
|
32 |
-# Busybox and similar reduced shells will NOT work, because this script |
|
33 |
-# requires all of these POSIX shell features: |
|
34 |
-# * functions; |
|
35 |
-# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», |
|
36 |
-# «${var#prefix}», «${var%suffix}», and «$( cmd )»; |
|
37 |
-# * compound commands having a testable exit status, especially «case»; |
|
38 |
-# * various built-in commands including «command», «set», and «ulimit». |
|
39 |
-# |
|
40 |
-# Important for patching: |
|
41 |
-# |
|
42 |
-# (2) This script targets any POSIX shell, so it avoids extensions provided |
|
43 |
-# by Bash, Ksh, etc; in particular arrays are avoided. |
|
44 |
-# |
|
45 |
-# The "traditional" practice of packing multiple parameters into a |
|
46 |
-# space-separated string is a well documented source of bugs and security |
|
47 |
-# problems, so this is (mostly) avoided, by progressively accumulating |
|
48 |
-# options in "$@", and eventually passing that to Java. |
|
49 |
-# |
|
50 |
-# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, |
|
51 |
-# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; |
|
52 |
-# see the in-line comments for details. |
|
53 |
-# |
|
54 |
-# There are tweaks for specific operating systems such as AIX, CygWin, |
|
55 |
-# Darwin, MinGW, and NonStop. |
|
56 |
-# |
|
57 |
-# (3) This script is generated from the Groovy template |
|
58 |
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt |
|
59 |
-# within the Gradle project. |
|
60 |
-# |
|
61 |
-# You can find Gradle at https://github.com/gradle/gradle/. |
|
62 |
-# |
|
63 |
-############################################################################## |
|
64 |
- |
|
65 |
-# Attempt to set APP_HOME |
|
66 |
- |
|
67 |
-# Resolve links: $0 may be a link |
|
68 |
-app_path=$0 |
|
69 |
- |
|
70 |
-# Need this for daisy-chained symlinks. |
|
71 |
-while |
|
72 |
- APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path |
|
73 |
- [ -h "$app_path" ] |
|
74 |
-do |
|
75 |
- ls=$( ls -ld "$app_path" ) |
|
76 |
- link=${ls#*' -> '} |
|
77 |
- case $link in #( |
|
78 |
- /*) app_path=$link ;; #( |
|
79 |
- *) app_path=$APP_HOME$link ;; |
|
80 |
- esac |
|
81 |
-done |
|
82 |
- |
|
83 |
-# This is normally unused |
|
84 |
-# shellcheck disable=SC2034 |
|
85 |
-APP_BASE_NAME=${0##*/} |
|
86 |
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) |
|
87 |
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit |
|
88 |
- |
|
89 |
-# Use the maximum available, or set MAX_FD != -1 to use that value. |
|
90 |
-MAX_FD=maximum |
|
91 |
- |
|
92 |
-warn () { |
|
93 |
- echo "$*" |
|
94 |
-} >&2 |
|
95 |
- |
|
96 |
-die () { |
|
97 |
- echo |
|
98 |
- echo "$*" |
|
99 |
- echo |
|
100 |
- exit 1 |
|
101 |
-} >&2 |
|
102 |
- |
|
103 |
-# OS specific support (must be 'true' or 'false'). |
|
104 |
-cygwin=false |
|
105 |
-msys=false |
|
106 |
-darwin=false |
|
107 |
-nonstop=false |
|
108 |
-case "$( uname )" in #( |
|
109 |
- CYGWIN* ) cygwin=true ;; #( |
|
110 |
- Darwin* ) darwin=true ;; #( |
|
111 |
- MSYS* | MINGW* ) msys=true ;; #( |
|
112 |
- NONSTOP* ) nonstop=true ;; |
|
113 |
-esac |
|
114 |
- |
|
115 |
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar |
|
116 |
- |
|
117 |
- |
|
118 |
-# Determine the Java command to use to start the JVM. |
|
119 |
-if [ -n "$JAVA_HOME" ] ; then |
|
120 |
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then |
|
121 |
- # IBM's JDK on AIX uses strange locations for the executables |
|
122 |
- JAVACMD=$JAVA_HOME/jre/sh/java |
|
123 |
- else |
|
124 |
- JAVACMD=$JAVA_HOME/bin/java |
|
125 |
- fi |
|
126 |
- if [ ! -x "$JAVACMD" ] ; then |
|
127 |
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME |
|
128 |
- |
|
129 |
-Please set the JAVA_HOME variable in your environment to match the |
|
130 |
-location of your Java installation." |
|
131 |
- fi |
|
132 |
-else |
|
133 |
- JAVACMD=java |
|
134 |
- if ! command -v java >/dev/null 2>&1 |
|
135 |
- then |
|
136 |
- die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
|
137 |
- |
|
138 |
-Please set the JAVA_HOME variable in your environment to match the |
|
139 |
-location of your Java installation." |
|
140 |
- fi |
|
141 |
-fi |
|
142 |
- |
|
143 |
-# Increase the maximum file descriptors if we can. |
|
144 |
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then |
|
145 |
- case $MAX_FD in #( |
|
146 |
- max*) |
|
147 |
- # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. |
|
148 |
- # shellcheck disable=SC2039,SC3045 |
|
149 |
- MAX_FD=$( ulimit -H -n ) || |
|
150 |
- warn "Could not query maximum file descriptor limit" |
|
151 |
- esac |
|
152 |
- case $MAX_FD in #( |
|
153 |
- '' | soft) :;; #( |
|
154 |
- *) |
|
155 |
- # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. |
|
156 |
- # shellcheck disable=SC2039,SC3045 |
|
157 |
- ulimit -n "$MAX_FD" || |
|
158 |
- warn "Could not set maximum file descriptor limit to $MAX_FD" |
|
159 |
- esac |
|
160 |
-fi |
|
161 |
- |
|
162 |
-# Collect all arguments for the java command, stacking in reverse order: |
|
163 |
-# * args from the command line |
|
164 |
-# * the main class name |
|
165 |
-# * -classpath |
|
166 |
-# * -D...appname settings |
|
167 |
-# * --module-path (only if needed) |
|
168 |
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. |
|
169 |
- |
|
170 |
-# For Cygwin or MSYS, switch paths to Windows format before running java |
|
171 |
-if "$cygwin" || "$msys" ; then |
|
172 |
- APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) |
|
173 |
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) |
|
174 |
- |
|
175 |
- JAVACMD=$( cygpath --unix "$JAVACMD" ) |
|
176 |
- |
|
177 |
- # Now convert the arguments - kludge to limit ourselves to /bin/sh |
|
178 |
- for arg do |
|
179 |
- if |
|
180 |
- case $arg in #( |
|
181 |
- -*) false ;; # don't mess with options #( |
|
182 |
- /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath |
|
183 |
- [ -e "$t" ] ;; #( |
|
184 |
- *) false ;; |
|
185 |
- esac |
|
186 |
- then |
|
187 |
- arg=$( cygpath --path --ignore --mixed "$arg" ) |
|
188 |
- fi |
|
189 |
- # Roll the args list around exactly as many times as the number of |
|
190 |
- # args, so each arg winds up back in the position where it started, but |
|
191 |
- # possibly modified. |
|
192 |
- # |
|
193 |
- # NB: a `for` loop captures its iteration list before it begins, so |
|
194 |
- # changing the positional parameters here affects neither the number of |
|
195 |
- # iterations, nor the values presented in `arg`. |
|
196 |
- shift # remove old arg |
|
197 |
- set -- "$@" "$arg" # push replacement arg |
|
198 |
- done |
|
199 |
-fi |
|
200 |
- |
|
201 |
- |
|
202 |
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
|
203 |
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' |
|
204 |
- |
|
205 |
-# Collect all arguments for the java command: |
|
206 |
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, |
|
207 |
-# and any embedded shellness will be escaped. |
|
208 |
-# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be |
|
209 |
-# treated as '${Hostname}' itself on the command line. |
|
210 |
- |
|
211 |
-set -- \ |
|
212 |
- "-Dorg.gradle.appname=$APP_BASE_NAME" \ |
|
213 |
- -classpath "$CLASSPATH" \ |
|
214 |
- org.gradle.wrapper.GradleWrapperMain \ |
|
215 |
- "$@" |
|
216 |
- |
|
217 |
-# Stop when "xargs" is not available. |
|
218 |
-if ! command -v xargs >/dev/null 2>&1 |
|
219 |
-then |
|
220 |
- die "xargs is not available" |
|
221 |
-fi |
|
222 |
- |
|
223 |
-# Use "xargs" to parse quoted args. |
|
224 |
-# |
|
225 |
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed. |
|
226 |
-# |
|
227 |
-# In Bash we could simply go: |
|
228 |
-# |
|
229 |
-# readarray ARGS < <( xargs -n1 <<<"$var" ) && |
|
230 |
-# set -- "${ARGS[@]}" "$@" |
|
231 |
-# |
|
232 |
-# but POSIX shell has neither arrays nor command substitution, so instead we |
|
233 |
-# post-process each arg (as a line of input to sed) to backslash-escape any |
|
234 |
-# character that might be a shell metacharacter, then use eval to reverse |
|
235 |
-# that process (while maintaining the separation between arguments), and wrap |
|
236 |
-# the whole thing up as a single "set" statement. |
|
237 |
-# |
|
238 |
-# This will of course break if any of these variables contains a newline or |
|
239 |
-# an unmatched quote. |
|
240 |
-# |
|
241 |
- |
|
242 |
-eval "set -- $( |
|
243 |
- printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | |
|
244 |
- xargs -n1 | |
|
245 |
- sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | |
|
246 |
- tr '\n' ' ' |
|
247 |
- )" '"$@"' |
|
248 |
- |
|
249 |
-exec "$JAVACMD" "$@" |
|
1 |
+#!/bin/sh |
|
2 |
+ |
|
3 |
+# |
|
4 |
+# Copyright © 2015-2021 the original authors. |
|
5 |
+# |
|
6 |
+# Licensed under the Apache License, Version 2.0 (the "License"); |
|
7 |
+# you may not use this file except in compliance with the License. |
|
8 |
+# You may obtain a copy of the License at |
|
9 |
+# |
|
10 |
+# https://www.apache.org/licenses/LICENSE-2.0 |
|
11 |
+# |
|
12 |
+# Unless required by applicable law or agreed to in writing, software |
|
13 |
+# distributed under the License is distributed on an "AS IS" BASIS, |
|
14 |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
15 |
+# See the License for the specific language governing permissions and |
|
16 |
+# limitations under the License. |
|
17 |
+# |
|
18 |
+ |
|
19 |
+############################################################################## |
|
20 |
+# |
|
21 |
+# Gradle start up script for POSIX generated by Gradle. |
|
22 |
+# |
|
23 |
+# Important for running: |
|
24 |
+# |
|
25 |
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is |
|
26 |
+# noncompliant, but you have some other compliant shell such as ksh or |
|
27 |
+# bash, then to run this script, type that shell name before the whole |
|
28 |
+# command line, like: |
|
29 |
+# |
|
30 |
+# ksh Gradle |
|
31 |
+# |
|
32 |
+# Busybox and similar reduced shells will NOT work, because this script |
|
33 |
+# requires all of these POSIX shell features: |
|
34 |
+# * functions; |
|
35 |
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», |
|
36 |
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»; |
|
37 |
+# * compound commands having a testable exit status, especially «case»; |
|
38 |
+# * various built-in commands including «command», «set», and «ulimit». |
|
39 |
+# |
|
40 |
+# Important for patching: |
|
41 |
+# |
|
42 |
+# (2) This script targets any POSIX shell, so it avoids extensions provided |
|
43 |
+# by Bash, Ksh, etc; in particular arrays are avoided. |
|
44 |
+# |
|
45 |
+# The "traditional" practice of packing multiple parameters into a |
|
46 |
+# space-separated string is a well documented source of bugs and security |
|
47 |
+# problems, so this is (mostly) avoided, by progressively accumulating |
|
48 |
+# options in "$@", and eventually passing that to Java. |
|
49 |
+# |
|
50 |
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, |
|
51 |
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; |
|
52 |
+# see the in-line comments for details. |
|
53 |
+# |
|
54 |
+# There are tweaks for specific operating systems such as AIX, CygWin, |
|
55 |
+# Darwin, MinGW, and NonStop. |
|
56 |
+# |
|
57 |
+# (3) This script is generated from the Groovy template |
|
58 |
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt |
|
59 |
+# within the Gradle project. |
|
60 |
+# |
|
61 |
+# You can find Gradle at https://github.com/gradle/gradle/. |
|
62 |
+# |
|
63 |
+############################################################################## |
|
64 |
+ |
|
65 |
+# Attempt to set APP_HOME |
|
66 |
+ |
|
67 |
+# Resolve links: $0 may be a link |
|
68 |
+app_path=$0 |
|
69 |
+ |
|
70 |
+# Need this for daisy-chained symlinks. |
|
71 |
+while |
|
72 |
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path |
|
73 |
+ [ -h "$app_path" ] |
|
74 |
+do |
|
75 |
+ ls=$( ls -ld "$app_path" ) |
|
76 |
+ link=${ls#*' -> '} |
|
77 |
+ case $link in #( |
|
78 |
+ /*) app_path=$link ;; #( |
|
79 |
+ *) app_path=$APP_HOME$link ;; |
|
80 |
+ esac |
|
81 |
+done |
|
82 |
+ |
|
83 |
+# This is normally unused |
|
84 |
+# shellcheck disable=SC2034 |
|
85 |
+APP_BASE_NAME=${0##*/} |
|
86 |
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) |
|
87 |
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit |
|
88 |
+ |
|
89 |
+# Use the maximum available, or set MAX_FD != -1 to use that value. |
|
90 |
+MAX_FD=maximum |
|
91 |
+ |
|
92 |
+warn () { |
|
93 |
+ echo "$*" |
|
94 |
+} >&2 |
|
95 |
+ |
|
96 |
+die () { |
|
97 |
+ echo |
|
98 |
+ echo "$*" |
|
99 |
+ echo |
|
100 |
+ exit 1 |
|
101 |
+} >&2 |
|
102 |
+ |
|
103 |
+# OS specific support (must be 'true' or 'false'). |
|
104 |
+cygwin=false |
|
105 |
+msys=false |
|
106 |
+darwin=false |
|
107 |
+nonstop=false |
|
108 |
+case "$( uname )" in #( |
|
109 |
+ CYGWIN* ) cygwin=true ;; #( |
|
110 |
+ Darwin* ) darwin=true ;; #( |
|
111 |
+ MSYS* | MINGW* ) msys=true ;; #( |
|
112 |
+ NONSTOP* ) nonstop=true ;; |
|
113 |
+esac |
|
114 |
+ |
|
115 |
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar |
|
116 |
+ |
|
117 |
+ |
|
118 |
+# Determine the Java command to use to start the JVM. |
|
119 |
+if [ -n "$JAVA_HOME" ] ; then |
|
120 |
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then |
|
121 |
+ # IBM's JDK on AIX uses strange locations for the executables |
|
122 |
+ JAVACMD=$JAVA_HOME/jre/sh/java |
|
123 |
+ else |
|
124 |
+ JAVACMD=$JAVA_HOME/bin/java |
|
125 |
+ fi |
|
126 |
+ if [ ! -x "$JAVACMD" ] ; then |
|
127 |
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME |
|
128 |
+ |
|
129 |
+Please set the JAVA_HOME variable in your environment to match the |
|
130 |
+location of your Java installation." |
|
131 |
+ fi |
|
132 |
+else |
|
133 |
+ JAVACMD=java |
|
134 |
+ if ! command -v java >/dev/null 2>&1 |
|
135 |
+ then |
|
136 |
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. |
|
137 |
+ |
|
138 |
+Please set the JAVA_HOME variable in your environment to match the |
|
139 |
+location of your Java installation." |
|
140 |
+ fi |
|
141 |
+fi |
|
142 |
+ |
|
143 |
+# Increase the maximum file descriptors if we can. |
|
144 |
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then |
|
145 |
+ case $MAX_FD in #( |
|
146 |
+ max*) |
|
147 |
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. |
|
148 |
+ # shellcheck disable=SC2039,SC3045 |
|
149 |
+ MAX_FD=$( ulimit -H -n ) || |
|
150 |
+ warn "Could not query maximum file descriptor limit" |
|
151 |
+ esac |
|
152 |
+ case $MAX_FD in #( |
|
153 |
+ '' | soft) :;; #( |
|
154 |
+ *) |
|
155 |
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. |
|
156 |
+ # shellcheck disable=SC2039,SC3045 |
|
157 |
+ ulimit -n "$MAX_FD" || |
|
158 |
+ warn "Could not set maximum file descriptor limit to $MAX_FD" |
|
159 |
+ esac |
|
160 |
+fi |
|
161 |
+ |
|
162 |
+# Collect all arguments for the java command, stacking in reverse order: |
|
163 |
+# * args from the command line |
|
164 |
+# * the main class name |
|
165 |
+# * -classpath |
|
166 |
+# * -D...appname settings |
|
167 |
+# * --module-path (only if needed) |
|
168 |
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. |
|
169 |
+ |
|
170 |
+# For Cygwin or MSYS, switch paths to Windows format before running java |
|
171 |
+if "$cygwin" || "$msys" ; then |
|
172 |
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) |
|
173 |
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) |
|
174 |
+ |
|
175 |
+ JAVACMD=$( cygpath --unix "$JAVACMD" ) |
|
176 |
+ |
|
177 |
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh |
|
178 |
+ for arg do |
|
179 |
+ if |
|
180 |
+ case $arg in #( |
|
181 |
+ -*) false ;; # don't mess with options #( |
|
182 |
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath |
|
183 |
+ [ -e "$t" ] ;; #( |
|
184 |
+ *) false ;; |
|
185 |
+ esac |
|
186 |
+ then |
|
187 |
+ arg=$( cygpath --path --ignore --mixed "$arg" ) |
|
188 |
+ fi |
|
189 |
+ # Roll the args list around exactly as many times as the number of |
|
190 |
+ # args, so each arg winds up back in the position where it started, but |
|
191 |
+ # possibly modified. |
|
192 |
+ # |
|
193 |
+ # NB: a `for` loop captures its iteration list before it begins, so |
|
194 |
+ # changing the positional parameters here affects neither the number of |
|
195 |
+ # iterations, nor the values presented in `arg`. |
|
196 |
+ shift # remove old arg |
|
197 |
+ set -- "$@" "$arg" # push replacement arg |
|
198 |
+ done |
|
199 |
+fi |
|
200 |
+ |
|
201 |
+ |
|
202 |
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. |
|
203 |
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' |
|
204 |
+ |
|
205 |
+# Collect all arguments for the java command: |
|
206 |
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, |
|
207 |
+# and any embedded shellness will be escaped. |
|
208 |
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be |
|
209 |
+# treated as '${Hostname}' itself on the command line. |
|
210 |
+ |
|
211 |
+set -- \ |
|
212 |
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \ |
|
213 |
+ -classpath "$CLASSPATH" \ |
|
214 |
+ org.gradle.wrapper.GradleWrapperMain \ |
|
215 |
+ "$@" |
|
216 |
+ |
|
217 |
+# Stop when "xargs" is not available. |
|
218 |
+if ! command -v xargs >/dev/null 2>&1 |
|
219 |
+then |
|
220 |
+ die "xargs is not available" |
|
221 |
+fi |
|
222 |
+ |
|
223 |
+# Use "xargs" to parse quoted args. |
|
224 |
+# |
|
225 |
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed. |
|
226 |
+# |
|
227 |
+# In Bash we could simply go: |
|
228 |
+# |
|
229 |
+# readarray ARGS < <( xargs -n1 <<<"$var" ) && |
|
230 |
+# set -- "${ARGS[@]}" "$@" |
|
231 |
+# |
|
232 |
+# but POSIX shell has neither arrays nor command substitution, so instead we |
|
233 |
+# post-process each arg (as a line of input to sed) to backslash-escape any |
|
234 |
+# character that might be a shell metacharacter, then use eval to reverse |
|
235 |
+# that process (while maintaining the separation between arguments), and wrap |
|
236 |
+# the whole thing up as a single "set" statement. |
|
237 |
+# |
|
238 |
+# This will of course break if any of these variables contains a newline or |
|
239 |
+# an unmatched quote. |
|
240 |
+# |
|
241 |
+ |
|
242 |
+eval "set -- $( |
|
243 |
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | |
|
244 |
+ xargs -n1 | |
|
245 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | |
|
246 |
+ tr '\n' ' ' |
|
247 |
+ )" '"$@"' |
|
248 |
+ |
|
249 |
+exec "$JAVACMD" "$@" |
--- jest.config.js
+++ jest.config.js
... | ... | @@ -1,3 +1,4 @@ |
1 | 1 |
module.exports = { |
2 | 2 |
preset: 'react-native', |
3 |
+ setupFiles: ["../../node_modules/react-native-gesture-handler/jestSetup.js"], |
|
3 | 4 |
}; |
--- package-lock.json
+++ package-lock.json
... | ... | @@ -9,10 +9,8 @@ |
9 | 9 |
"version": "0.0.1", |
10 | 10 |
"dependencies": { |
11 | 11 |
"@react-native-async-storage/async-storage": "^1.23.1", |
12 |
- "@react-native-camera-roll/camera-roll": "^7.8.1", |
|
13 |
- "@react-native-community/geolocation": "^3.2.1", |
|
14 | 12 |
"@react-navigation/native": "^6.1.17", |
15 |
- "@react-navigation/native-stack": "^6.9.26", |
|
13 |
+ "@react-navigation/stack": "^6.4.1", |
|
16 | 14 |
"axios": "^1.7.2", |
17 | 15 |
"base64-js": "^1.5.1", |
18 | 16 |
"crypto-js": "^4.2.0", |
... | ... | @@ -22,6 +20,8 @@ |
22 | 20 |
"react-native-background-actions": "^4.0.0", |
23 | 21 |
"react-native-chart-kit": "^6.12.0", |
24 | 22 |
"react-native-fs": "^2.20.0", |
23 |
+ "react-native-geolocation-service": "^5.3.1", |
|
24 |
+ "react-native-gesture-handler": "^2.17.1", |
|
25 | 25 |
"react-native-gifted-charts": "^1.4.10", |
26 | 26 |
"react-native-linear-gradient": "^2.8.3", |
27 | 27 |
"react-native-reanimated": "^3.11.0", |
... | ... | @@ -30,7 +30,7 @@ |
30 | 30 |
"react-native-shadow-2": "^7.1.0", |
31 | 31 |
"react-native-svg": "^15.3.0", |
32 | 32 |
"react-native-vector-icons": "^10.1.0", |
33 |
- "react-native-vision-camera": "4.0.5" |
|
33 |
+ "react-native-vision-camera": "^4.0.5" |
|
34 | 34 |
}, |
35 | 35 |
"devDependencies": { |
36 | 36 |
"@babel/core": "^7.20.0", |
... | ... | @@ -3528,6 +3528,17 @@ |
3528 | 3528 |
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", |
3529 | 3529 |
"dev": true |
3530 | 3530 |
}, |
3531 |
+ "node_modules/@egjs/hammerjs": { |
|
3532 |
+ "version": "2.0.17", |
|
3533 |
+ "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", |
|
3534 |
+ "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", |
|
3535 |
+ "dependencies": { |
|
3536 |
+ "@types/hammerjs": "^2.0.36" |
|
3537 |
+ }, |
|
3538 |
+ "engines": { |
|
3539 |
+ "node": ">=0.8.0" |
|
3540 |
+ } |
|
3541 |
+ }, |
|
3531 | 3542 |
"node_modules/@eslint-community/eslint-utils": { |
3532 | 3543 |
"version": "4.4.0", |
3533 | 3544 |
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", |
... | ... | @@ -4518,17 +4529,6 @@ |
4518 | 4529 |
"react-native": "^0.0.0-0 || >=0.60 <1.0" |
4519 | 4530 |
} |
4520 | 4531 |
}, |
4521 |
- "node_modules/@react-native-camera-roll/camera-roll": { |
|
4522 |
- "version": "7.8.1", |
|
4523 |
- "resolved": "https://registry.npmjs.org/@react-native-camera-roll/camera-roll/-/camera-roll-7.8.1.tgz", |
|
4524 |
- "integrity": "sha512-voVmDlzBS1MVADFk/Vb9GzWzhpoF4ESSjU9qwDNxf/ZA9tirAzX/M0TKcz2V+iFHeRR68cHtZ9tHUhQVG7kJcA==", |
|
4525 |
- "engines": { |
|
4526 |
- "node": ">= 18.17.0" |
|
4527 |
- }, |
|
4528 |
- "peerDependencies": { |
|
4529 |
- "react-native": ">=0.59" |
|
4530 |
- } |
|
4531 |
- }, |
|
4532 | 4532 |
"node_modules/@react-native-community/cli-clean": { |
4533 | 4533 |
"version": "12.3.2", |
4534 | 4534 |
"resolved": "https://registry.npmjs.org/@react-native-community/cli-clean/-/cli-clean-12.3.2.tgz", |
... | ... | @@ -5278,18 +5278,6 @@ |
5278 | 5278 |
"joi": "^17.2.1" |
5279 | 5279 |
} |
5280 | 5280 |
}, |
5281 |
- "node_modules/@react-native-community/geolocation": { |
|
5282 |
- "version": "3.2.1", |
|
5283 |
- "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.2.1.tgz", |
|
5284 |
- "integrity": "sha512-/+HNzuRl4UCMma7KK+KYL8k2nxAGuW+DGxqmqfpiqKBlCkCUbuFHaZZdqVD6jpsn9r/ghe583ECLmd9SV9I4Bw==", |
|
5285 |
- "engines": { |
|
5286 |
- "node": ">=18.0.0" |
|
5287 |
- }, |
|
5288 |
- "peerDependencies": { |
|
5289 |
- "react": "*", |
|
5290 |
- "react-native": "*" |
|
5291 |
- } |
|
5292 |
- }, |
|
5293 | 5281 |
"node_modules/@react-native/assets-registry": { |
5294 | 5282 |
"version": "0.73.1", |
5295 | 5283 |
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.73.1.tgz", |
... | ... | @@ -5802,9 +5790,9 @@ |
5802 | 5790 |
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" |
5803 | 5791 |
}, |
5804 | 5792 |
"node_modules/@react-navigation/elements": { |
5805 |
- "version": "1.3.30", |
|
5806 |
- "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.30.tgz", |
|
5807 |
- "integrity": "sha512-plhc8UvCZs0UkV+sI+3bisIyn78wz9O/BiWZXpounu72k/R/Sj5PuZYFJ1fi6psvriUveMCGh4LeZckAZu2qiQ==", |
|
5793 |
+ "version": "1.3.31", |
|
5794 |
+ "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.31.tgz", |
|
5795 |
+ "integrity": "sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ==", |
|
5808 | 5796 |
"peerDependencies": { |
5809 | 5797 |
"@react-navigation/native": "^6.0.0", |
5810 | 5798 |
"react": "*", |
... | ... | @@ -5827,22 +5815,6 @@ |
5827 | 5815 |
"react-native": "*" |
5828 | 5816 |
} |
5829 | 5817 |
}, |
5830 |
- "node_modules/@react-navigation/native-stack": { |
|
5831 |
- "version": "6.9.26", |
|
5832 |
- "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.9.26.tgz", |
|
5833 |
- "integrity": "sha512-++dueQ+FDj2XkZ902DVrK79ub1vp19nSdAZWxKRgd6+Bc0Niiesua6rMCqymYOVaYh+dagwkA9r00bpt/U5WLw==", |
|
5834 |
- "dependencies": { |
|
5835 |
- "@react-navigation/elements": "^1.3.30", |
|
5836 |
- "warn-once": "^0.1.0" |
|
5837 |
- }, |
|
5838 |
- "peerDependencies": { |
|
5839 |
- "@react-navigation/native": "^6.0.0", |
|
5840 |
- "react": "*", |
|
5841 |
- "react-native": "*", |
|
5842 |
- "react-native-safe-area-context": ">= 3.0.0", |
|
5843 |
- "react-native-screens": ">= 3.0.0" |
|
5844 |
- } |
|
5845 |
- }, |
|
5846 | 5818 |
"node_modules/@react-navigation/native/node_modules/escape-string-regexp": { |
5847 | 5819 |
"version": "4.0.0", |
5848 | 5820 |
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", |
... | ... | @@ -5861,6 +5833,52 @@ |
5861 | 5833 |
"dependencies": { |
5862 | 5834 |
"nanoid": "^3.1.23" |
5863 | 5835 |
} |
5836 |
+ }, |
|
5837 |
+ "node_modules/@react-navigation/stack": { |
|
5838 |
+ "version": "6.4.1", |
|
5839 |
+ "resolved": "https://registry.npmjs.org/@react-navigation/stack/-/stack-6.4.1.tgz", |
|
5840 |
+ "integrity": "sha512-upMEHOKMtuMu4c9gmoPlO/JqI6mDlSqwXg1aXKOTQLXAF8H5koOLRfrmi7AkdiE9A7lDXWUAZoGuD9O88cYvDQ==", |
|
5841 |
+ "dependencies": { |
|
5842 |
+ "@react-navigation/elements": "^1.3.31", |
|
5843 |
+ "color": "^4.2.3", |
|
5844 |
+ "warn-once": "^0.1.0" |
|
5845 |
+ }, |
|
5846 |
+ "peerDependencies": { |
|
5847 |
+ "@react-navigation/native": "^6.0.0", |
|
5848 |
+ "react": "*", |
|
5849 |
+ "react-native": "*", |
|
5850 |
+ "react-native-gesture-handler": ">= 1.0.0", |
|
5851 |
+ "react-native-safe-area-context": ">= 3.0.0", |
|
5852 |
+ "react-native-screens": ">= 3.0.0" |
|
5853 |
+ } |
|
5854 |
+ }, |
|
5855 |
+ "node_modules/@react-navigation/stack/node_modules/color": { |
|
5856 |
+ "version": "4.2.3", |
|
5857 |
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", |
|
5858 |
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", |
|
5859 |
+ "dependencies": { |
|
5860 |
+ "color-convert": "^2.0.1", |
|
5861 |
+ "color-string": "^1.9.0" |
|
5862 |
+ }, |
|
5863 |
+ "engines": { |
|
5864 |
+ "node": ">=12.5.0" |
|
5865 |
+ } |
|
5866 |
+ }, |
|
5867 |
+ "node_modules/@react-navigation/stack/node_modules/color-convert": { |
|
5868 |
+ "version": "2.0.1", |
|
5869 |
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", |
|
5870 |
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", |
|
5871 |
+ "dependencies": { |
|
5872 |
+ "color-name": "~1.1.4" |
|
5873 |
+ }, |
|
5874 |
+ "engines": { |
|
5875 |
+ "node": ">=7.0.0" |
|
5876 |
+ } |
|
5877 |
+ }, |
|
5878 |
+ "node_modules/@react-navigation/stack/node_modules/color-name": { |
|
5879 |
+ "version": "1.1.4", |
|
5880 |
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", |
|
5881 |
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" |
|
5864 | 5882 |
}, |
5865 | 5883 |
"node_modules/@sideway/address": { |
5866 | 5884 |
"version": "4.1.5", |
... | ... | @@ -5980,6 +5998,11 @@ |
5980 | 5998 |
"dependencies": { |
5981 | 5999 |
"@types/node": "*" |
5982 | 6000 |
} |
6001 |
+ }, |
|
6002 |
+ "node_modules/@types/hammerjs": { |
|
6003 |
+ "version": "2.0.45", |
|
6004 |
+ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.45.tgz", |
|
6005 |
+ "integrity": "sha512-qkcUlZmX6c4J8q45taBKTL3p+LbITgyx7qhlPYOdOHZB7B31K0mXbP5YA7i7SgDeEGuI9MnumiKPEMrxg8j3KQ==" |
|
5983 | 6006 |
}, |
5984 | 6007 |
"node_modules/@types/istanbul-lib-coverage": { |
5985 | 6008 |
"version": "2.0.6", |
... | ... | @@ -7398,7 +7421,6 @@ |
7398 | 7421 |
"version": "1.9.1", |
7399 | 7422 |
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", |
7400 | 7423 |
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", |
7401 |
- "dev": true, |
|
7402 | 7424 |
"dependencies": { |
7403 | 7425 |
"color-name": "^1.0.0", |
7404 | 7426 |
"simple-swizzle": "^0.2.2" |
... | ... | @@ -9817,6 +9839,19 @@ |
9817 | 9839 |
"engines": { |
9818 | 9840 |
"node": ">= 8" |
9819 | 9841 |
} |
9842 |
+ }, |
|
9843 |
+ "node_modules/hoist-non-react-statics": { |
|
9844 |
+ "version": "3.3.2", |
|
9845 |
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", |
|
9846 |
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", |
|
9847 |
+ "dependencies": { |
|
9848 |
+ "react-is": "^16.7.0" |
|
9849 |
+ } |
|
9850 |
+ }, |
|
9851 |
+ "node_modules/hoist-non-react-statics/node_modules/react-is": { |
|
9852 |
+ "version": "16.13.1", |
|
9853 |
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", |
|
9854 |
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" |
|
9820 | 9855 |
}, |
9821 | 9856 |
"node_modules/html-escaper": { |
9822 | 9857 |
"version": "2.0.2", |
... | ... | @@ -14045,6 +14080,26 @@ |
14045 | 14080 |
} |
14046 | 14081 |
} |
14047 | 14082 |
}, |
14083 |
+ "node_modules/react-native-geolocation-service": { |
|
14084 |
+ "version": "5.3.1", |
|
14085 |
+ "resolved": "https://registry.npmjs.org/react-native-geolocation-service/-/react-native-geolocation-service-5.3.1.tgz", |
|
14086 |
+ "integrity": "sha512-LTXPtPNmrdhx+yeWG47sAaCgQc3nG1z+HLLHlhK/5YfOgfLcAb9HAkhREPjQKPZOUx8pKZMIpdGFUGfJYtimXQ==" |
|
14087 |
+ }, |
|
14088 |
+ "node_modules/react-native-gesture-handler": { |
|
14089 |
+ "version": "2.17.1", |
|
14090 |
+ "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.17.1.tgz", |
|
14091 |
+ "integrity": "sha512-pWfniN6NuVKUq40KACuD3NCMe+bWNQCpD3cmxL6aLSCTwPKYmf7l4Lp0/E/almpjvxhybJZtFLU0w4tmxnIKaA==", |
|
14092 |
+ "dependencies": { |
|
14093 |
+ "@egjs/hammerjs": "^2.0.17", |
|
14094 |
+ "hoist-non-react-statics": "^3.3.0", |
|
14095 |
+ "invariant": "^2.2.4", |
|
14096 |
+ "prop-types": "^15.7.2" |
|
14097 |
+ }, |
|
14098 |
+ "peerDependencies": { |
|
14099 |
+ "react": "*", |
|
14100 |
+ "react-native": "*" |
|
14101 |
+ } |
|
14102 |
+ }, |
|
14048 | 14103 |
"node_modules/react-native-gifted-charts": { |
14049 | 14104 |
"version": "1.4.10", |
14050 | 14105 |
"resolved": "https://registry.npmjs.org/react-native-gifted-charts/-/react-native-gifted-charts-1.4.10.tgz", |
... | ... | @@ -15013,7 +15068,6 @@ |
15013 | 15068 |
"version": "0.2.2", |
15014 | 15069 |
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", |
15015 | 15070 |
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", |
15016 |
- "dev": true, |
|
15017 | 15071 |
"dependencies": { |
15018 | 15072 |
"is-arrayish": "^0.3.1" |
15019 | 15073 |
} |
... | ... | @@ -15021,8 +15075,7 @@ |
15021 | 15075 |
"node_modules/simple-swizzle/node_modules/is-arrayish": { |
15022 | 15076 |
"version": "0.3.2", |
15023 | 15077 |
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", |
15024 |
- "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", |
|
15025 |
- "dev": true |
|
15078 |
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" |
|
15026 | 15079 |
}, |
15027 | 15080 |
"node_modules/sisteransi": { |
15028 | 15081 |
"version": "1.0.5", |
--- package.json
+++ package.json
... | ... | @@ -11,10 +11,8 @@ |
11 | 11 |
}, |
12 | 12 |
"dependencies": { |
13 | 13 |
"@react-native-async-storage/async-storage": "^1.23.1", |
14 |
- "@react-native-camera-roll/camera-roll": "^7.8.1", |
|
15 |
- "@react-native-community/geolocation": "^3.2.1", |
|
16 | 14 |
"@react-navigation/native": "^6.1.17", |
17 |
- "@react-navigation/native-stack": "^6.9.26", |
|
15 |
+ "@react-navigation/stack": "^6.4.1", |
|
18 | 16 |
"axios": "^1.7.2", |
19 | 17 |
"base64-js": "^1.5.1", |
20 | 18 |
"crypto-js": "^4.2.0", |
... | ... | @@ -24,6 +22,8 @@ |
24 | 22 |
"react-native-background-actions": "^4.0.0", |
25 | 23 |
"react-native-chart-kit": "^6.12.0", |
26 | 24 |
"react-native-fs": "^2.20.0", |
25 |
+ "react-native-geolocation-service": "^5.3.1", |
|
26 |
+ "react-native-gesture-handler": "^2.17.1", |
|
27 | 27 |
"react-native-gifted-charts": "^1.4.10", |
28 | 28 |
"react-native-linear-gradient": "^2.8.3", |
29 | 29 |
"react-native-reanimated": "^3.11.0", |
... | ... | @@ -32,7 +32,7 @@ |
32 | 32 |
"react-native-shadow-2": "^7.1.0", |
33 | 33 |
"react-native-svg": "^15.3.0", |
34 | 34 |
"react-native-vector-icons": "^10.1.0", |
35 |
- "react-native-vision-camera": "4.0.5" |
|
35 |
+ "react-native-vision-camera": "^4.0.5" |
|
36 | 36 |
}, |
37 | 37 |
"devDependencies": { |
38 | 38 |
"@babel/core": "^7.20.0", |
+++ src/api/ApiUtils.js
... | ... | @@ -0,0 +1,96 @@ |
1 | +import axios from 'axios'; | |
2 | +import AsyncStorage from '@react-native-async-storage/async-storage'; | |
3 | + | |
4 | +// API 인스턴스 생성 | |
5 | +const apiInstance = axios.create({ | |
6 | + baseURL: 'http://takensoftai.iptime.org:15857', | |
7 | + headers: { | |
8 | + 'Content-Type': 'application/json', | |
9 | + } | |
10 | +}); | |
11 | + | |
12 | +const ApiUtils = { | |
13 | + register: async (userData) => { | |
14 | + try { | |
15 | + const response = await apiInstance.post('/auth/register', userData); | |
16 | + return response.data; | |
17 | + } catch (error) { | |
18 | + console.error('Register error:', error.response || error); | |
19 | + throw error.response?.data || error; | |
20 | + } | |
21 | + }, | |
22 | + | |
23 | + login: async (credentials) => { | |
24 | + try { | |
25 | + const response = await apiInstance.post('/auth/login', credentials); | |
26 | + return response.data; | |
27 | + } catch (error) { | |
28 | + console.error('Login error:', error.response || error); | |
29 | + throw error.response?.data || error; | |
30 | + } | |
31 | + }, | |
32 | + | |
33 | + uploadPhoto: async (uri) => { | |
34 | + const formData = new FormData(); | |
35 | + formData.append('photo', { | |
36 | + uri, | |
37 | + type: 'image/jpeg', | |
38 | + name: 'photo.jpg', | |
39 | + }); | |
40 | + | |
41 | + try { | |
42 | + const response = await apiInstance.post('/upload', formData, { | |
43 | + headers: { | |
44 | + 'Content-Type': 'multipart/form-data', | |
45 | + }, | |
46 | + }); | |
47 | + return response.data; | |
48 | + } catch (error) { | |
49 | + console.error('Upload photo error:', error.response || error); | |
50 | + throw error.response?.data || error; | |
51 | + } | |
52 | + }, | |
53 | + | |
54 | + sendTripLog: async (data, navigation) => { | |
55 | + try { | |
56 | + // AsyncStorage에서 토큰 가져오기 | |
57 | + const tokenString = await AsyncStorage.getItem('token'); | |
58 | + if (tokenString) { | |
59 | + const tokenObject = JSON.parse(tokenString); | |
60 | + const accessToken = tokenObject.accessToken; | |
61 | + | |
62 | + if (!accessToken) { | |
63 | + await AsyncStorage.removeItem('token'); | |
64 | + // 로그인 페이지로 리디렉션 | |
65 | + navigation.navigate('Login'); | |
66 | + return; | |
67 | + } | |
68 | + | |
69 | + // 데이터 전송 | |
70 | + try { | |
71 | + const response = await apiInstance.post('/action/gps_update', data, { | |
72 | + headers: { | |
73 | + 'Content-Type': 'application/json', | |
74 | + Authorization: `Bearer ${accessToken}`, // Bearer 스키마를 사용하도록 수정 | |
75 | + }, | |
76 | + }); | |
77 | + return response.data; | |
78 | + } catch (error) { | |
79 | + console.error('Send trip log error:', error.response?.status, error.response?.data || error.message); | |
80 | + throw error.response?.data || error; | |
81 | + } | |
82 | + } else { | |
83 | + console.log("토큰이 스토리지에 없습니다."); | |
84 | + // 로그인 페이지로 리디렉션 | |
85 | + navigation.navigate('Login'); | |
86 | + } | |
87 | + } catch (error) { | |
88 | + console.error('Token retrieval error:', error.message); | |
89 | + // 로그인 페이지로 리디렉션 | |
90 | + navigation.navigate('Login'); | |
91 | + } | |
92 | + } | |
93 | + | |
94 | +}; | |
95 | + | |
96 | +export default ApiUtils; |
--- src/screens/AnalysisComponent.tsx
+++ src/component/AnalysisComponent.js
... | ... | @@ -1,13 +1,9 @@ |
1 |
-import React, {useState} from 'react'; |
|
2 |
-import {StyleSheet, View, Text, TouchableOpacity} from 'react-native'; |
|
3 |
-import {BarChart, LineChart} from 'react-native-gifted-charts'; |
|
4 |
- |
|
5 |
-// 타입 정의 |
|
6 |
-type ScoreProps = {}; |
|
1 |
+import React, { useState } from 'react'; |
|
2 |
+import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; |
|
3 |
+import { BarChart, LineChart } from 'react-native-gifted-charts'; |
|
7 | 4 |
|
8 | 5 |
export default function AnalysisComponent() { |
9 |
- const [selectedComponent, setSelectedComponent] = |
|
10 |
- useState<string>('MainMark'); |
|
6 |
+ const [selectedComponent, setSelectedComponent] = useState('MainMark'); |
|
11 | 7 |
|
12 | 8 |
const renderComponent = () => { |
13 | 9 |
switch (selectedComponent) { |
... | ... | @@ -75,15 +71,15 @@ |
75 | 71 |
|
76 | 72 |
// 주요 감점 요인 차트 |
77 | 73 |
const mainData = [ |
78 |
- {value: 60, label: '급가속'}, |
|
79 |
- {value: 90, label: '급감속'}, |
|
80 |
- {value: 88, label: '급출발'}, |
|
81 |
- {value: 75, label: '급정지'}, |
|
82 |
- {value: 97, label: '안전모착용'}, |
|
83 |
- {value: 80, label: '충돌위험'}, |
|
74 |
+ { value: 60, label: '급가속' }, |
|
75 |
+ { value: 90, label: '급감속' }, |
|
76 |
+ { value: 88, label: '급출발' }, |
|
77 |
+ { value: 75, label: '급정지' }, |
|
78 |
+ { value: 97, label: '안전모착용' }, |
|
79 |
+ { value: 80, label: '충돌위험' }, |
|
84 | 80 |
]; |
85 | 81 |
|
86 |
-const MainMark: React.FC<ScoreProps> = () => { |
|
82 |
+function MainMark() { |
|
87 | 83 |
return ( |
88 | 84 |
<BarChart |
89 | 85 |
horizontal |
... | ... | @@ -96,7 +92,7 @@ |
96 | 92 |
barBorderTopRightRadius={10} |
97 | 93 |
barBorderTopLeftRadius={10} |
98 | 94 |
labelsExtraHeight={40} |
99 |
- xAxisLabelTextStyle={{fontSize: 12, marginLeft: -35, textAlign: 'left'}} |
|
95 |
+ xAxisLabelTextStyle={{ fontSize: 12, marginLeft: -35, textAlign: 'left' }} |
|
100 | 96 |
stepHeight={25} |
101 | 97 |
spacing={15} |
102 | 98 |
showGradient |
... | ... | @@ -106,19 +102,19 @@ |
106 | 102 |
height={230} |
107 | 103 |
/> |
108 | 104 |
); |
109 |
-}; |
|
105 |
+} |
|
110 | 106 |
|
111 | 107 |
// 거리별 감점 현황 차트 |
112 | 108 |
const distanceData = [ |
113 |
- {value: 15, label: '10km'}, |
|
114 |
- {value: 30, label: '20km'}, |
|
115 |
- {value: 23, label: '30km'}, |
|
116 |
- {value: 40, label: '40km'}, |
|
117 |
- {value: 16, label: '50km'}, |
|
118 |
- {value: 50, label: '60km'}, |
|
109 |
+ { value: 15, label: '10km' }, |
|
110 |
+ { value: 30, label: '20km' }, |
|
111 |
+ { value: 23, label: '30km' }, |
|
112 |
+ { value: 40, label: '40km' }, |
|
113 |
+ { value: 16, label: '50km' }, |
|
114 |
+ { value: 50, label: '60km' }, |
|
119 | 115 |
]; |
120 | 116 |
|
121 |
-const DistanceMark: React.FC<ScoreProps> = () => { |
|
117 |
+function DistanceMark() { |
|
122 | 118 |
return ( |
123 | 119 |
<LineChart |
124 | 120 |
areaChart |
... | ... | @@ -134,7 +130,7 @@ |
134 | 130 |
noOfSections={5} |
135 | 131 |
/> |
136 | 132 |
); |
137 |
-}; |
|
133 |
+} |
|
138 | 134 |
|
139 | 135 |
// 운행 점수 비교 차트 |
140 | 136 |
const driveData = [ |
... | ... | @@ -143,7 +139,7 @@ |
143 | 139 |
label: '이전 운행 점수', |
144 | 140 |
frontColor: '#FFA311', |
145 | 141 |
topLabelComponent: () => ( |
146 |
- <Text style={{color: '#bdbdbd', fontSize: 15}}>65</Text> |
|
142 |
+ <Text style={{ color: '#bdbdbd', fontSize: 15 }}>65</Text> |
|
147 | 143 |
), |
148 | 144 |
}, |
149 | 145 |
{ |
... | ... | @@ -151,11 +147,12 @@ |
151 | 147 |
label: '현재 운행 점수', |
152 | 148 |
frontColor: '#0F4BD3', |
153 | 149 |
topLabelComponent: () => ( |
154 |
- <Text style={{color: '#bdbdbd', fontSize: 15}}>94</Text> |
|
150 |
+ <Text style={{ color: '#bdbdbd', fontSize: 15 }}>94</Text> |
|
155 | 151 |
), |
156 | 152 |
}, |
157 | 153 |
]; |
158 |
-const DriveScore: React.FC<ScoreProps> = () => { |
|
154 |
+ |
|
155 |
+function DriveScore() { |
|
159 | 156 |
return ( |
160 | 157 |
<BarChart |
161 | 158 |
data={driveData} |
... | ... | @@ -168,7 +165,7 @@ |
168 | 165 |
yAxisColor="#959595" |
169 | 166 |
/> |
170 | 167 |
); |
171 |
-}; |
|
168 |
+} |
|
172 | 169 |
|
173 | 170 |
const styles = StyleSheet.create({ |
174 | 171 |
buttonContainer: { |
+++ src/component/RadioButton.js
... | ... | @@ -0,0 +1,61 @@ |
1 | +import React from 'react'; | |
2 | +import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; | |
3 | + | |
4 | +const RadioButton = ({ | |
5 | + isSelected, | |
6 | + buttonColor, | |
7 | + selectedButtonColor, | |
8 | + labelColor, | |
9 | + style, | |
10 | + wrapStyle, | |
11 | + children, | |
12 | + onPress, | |
13 | +}) => { | |
14 | + const color = isSelected | |
15 | + ? selectedButtonColor || '#3872ef' | |
16 | + : buttonColor || '#ccc'; | |
17 | + | |
18 | + return ( | |
19 | + <TouchableOpacity onPress={onPress} style={[styles.container, wrapStyle]}> | |
20 | + <View style={[styles.circle, { borderColor: color }]}> | |
21 | + {isSelected && ( | |
22 | + <View style={[styles.checkedCircle, { backgroundColor: color }]} /> | |
23 | + )} | |
24 | + </View> | |
25 | + {children && ( | |
26 | + <Text style={[styles.label, { color: labelColor || '#000' }, style]}> | |
27 | + {children} | |
28 | + </Text> | |
29 | + )} | |
30 | + </TouchableOpacity> | |
31 | + ); | |
32 | +}; | |
33 | + | |
34 | +const styles = StyleSheet.create({ | |
35 | + container: { | |
36 | + flexDirection: 'row', | |
37 | + alignItems: 'center', | |
38 | + marginBottom: 10, | |
39 | + }, | |
40 | + circle: { | |
41 | + height: 20, | |
42 | + width: 20, | |
43 | + borderRadius: 12, | |
44 | + borderWidth: 2, | |
45 | + borderColor: '#ccc', | |
46 | + alignItems: 'center', | |
47 | + justifyContent: 'center', | |
48 | + marginRight: 10, | |
49 | + }, | |
50 | + checkedCircle: { | |
51 | + width: 10, | |
52 | + height: 10, | |
53 | + borderRadius: 6, | |
54 | + backgroundColor: '#2196f3', | |
55 | + }, | |
56 | + label: { | |
57 | + fontSize: 16, | |
58 | + }, | |
59 | +}); | |
60 | + | |
61 | +export default RadioButton; |
--- src/component/RadioButton.tsx
... | ... | @@ -1,77 +0,0 @@ |
1 | -import * as React from 'react'; | |
2 | -import {View, Text, TouchableOpacity, StyleSheet} from 'react-native'; | |
3 | - | |
4 | -export interface RadioButtonProps { | |
5 | - children?: React.ReactNode; | |
6 | - isSelected?: boolean | undefined; | |
7 | - labelHorizontal?: boolean | undefined; | |
8 | - buttonColor?: string | undefined; | |
9 | - selectedButtonColor?: string | undefined; | |
10 | - labelColor?: string | undefined; | |
11 | - style?: any; | |
12 | - wrapStyle?: any; | |
13 | - onPress?: () => void; | |
14 | -} | |
15 | - | |
16 | -export class RadioButton extends React.Component<RadioButtonProps> { | |
17 | - render() { | |
18 | - const { | |
19 | - isSelected, | |
20 | - buttonColor, | |
21 | - selectedButtonColor, | |
22 | - labelColor, | |
23 | - style, | |
24 | - wrapStyle, | |
25 | - children, | |
26 | - onPress, | |
27 | - } = this.props; | |
28 | - | |
29 | - const color = isSelected | |
30 | - ? selectedButtonColor || '#3872ef' | |
31 | - : buttonColor || '#ccc'; | |
32 | - | |
33 | - return ( | |
34 | - <TouchableOpacity onPress={onPress} style={[styles.container, wrapStyle]}> | |
35 | - <View style={[styles.circle, {borderColor: color}]}> | |
36 | - {isSelected && ( | |
37 | - <View style={[styles.checkedCircle, {backgroundColor: color}]} /> | |
38 | - )} | |
39 | - </View> | |
40 | - {children && ( | |
41 | - <Text style={[styles.label, {color: labelColor || '#000'}, style]}> | |
42 | - {children} | |
43 | - </Text> | |
44 | - )} | |
45 | - </TouchableOpacity> | |
46 | - ); | |
47 | - } | |
48 | -} | |
49 | - | |
50 | -const styles = StyleSheet.create({ | |
51 | - container: { | |
52 | - flexDirection: 'row', | |
53 | - alignItems: 'center', | |
54 | - marginBottom: 10, | |
55 | - }, | |
56 | - circle: { | |
57 | - height: 20, | |
58 | - width: 20, | |
59 | - borderRadius: 12, | |
60 | - borderWidth: 2, | |
61 | - borderColor: '#ccc', | |
62 | - alignItems: 'center', | |
63 | - justifyContent: 'center', | |
64 | - marginRight: 10, | |
65 | - }, | |
66 | - checkedCircle: { | |
67 | - width: 10, | |
68 | - height: 10, | |
69 | - borderRadius: 6, | |
70 | - backgroundColor: '#2196f3', | |
71 | - }, | |
72 | - label: { | |
73 | - fontSize: 16, | |
74 | - }, | |
75 | -}); | |
76 | - | |
77 | -export default RadioButton; |
+++ src/screen/AgreementScreen.js
... | ... | @@ -0,0 +1,184 @@ |
1 | +import { | |
2 | + StyleSheet, | |
3 | + ScrollView, | |
4 | + View, | |
5 | + Text, | |
6 | + TextInput, | |
7 | + TouchableOpacity, | |
8 | + Alert, | |
9 | +} from 'react-native'; | |
10 | +import React, { useState } from 'react'; | |
11 | +import RadioButton from '../component/RadioButton'; | |
12 | +import Api from '../api/ApiUtils'; | |
13 | +import { useNavigation } from '@react-navigation/native'; | |
14 | + | |
15 | +export default function AgreementScreen() { | |
16 | + const navigation = useNavigation(); | |
17 | + const [agreementData, setAgreement] = useState({ | |
18 | + id: '', | |
19 | + password: '', | |
20 | + email: '', | |
21 | + sex: null, | |
22 | + phone: '', | |
23 | + }); | |
24 | + | |
25 | + const radio_props = [ | |
26 | + { label: '여성', value: '여성' }, | |
27 | + { label: '남성', value: '남성' }, | |
28 | + ]; | |
29 | + | |
30 | + const handleInputChange = (field, value) => { | |
31 | + setAgreement(prevAgreement => ({ | |
32 | + ...prevAgreement, | |
33 | + [field]: value, | |
34 | + })); | |
35 | + }; | |
36 | + | |
37 | + const handleSubmit = async () => { | |
38 | + try { | |
39 | + const response = await Api.register(agreementData); | |
40 | + console.log(response); | |
41 | + navigation.navigate('Login'); | |
42 | + } catch (error) { | |
43 | + Alert.alert('가입 실패', error.message); | |
44 | + } | |
45 | + }; | |
46 | + | |
47 | + // 전화번호 포맷팅 | |
48 | + const formatPhoneNumber = (text) => { | |
49 | + const cleaned = text.replace(/\D/g, ''); // Remove all non-numeric characters | |
50 | + if (cleaned.length === 11) { | |
51 | + const match = cleaned.match(/^(\d{3})(\d{4})(\d{4})$/); | |
52 | + if (match) { | |
53 | + return `${match[1]}-${match[2]}-${match[3]}`; | |
54 | + } | |
55 | + } | |
56 | + return text; | |
57 | + }; | |
58 | + | |
59 | + const handlePhoneChange = (text) => { | |
60 | + const formatted = formatPhoneNumber(text); | |
61 | + handleInputChange('phone', formatted); | |
62 | + }; | |
63 | + | |
64 | + return ( | |
65 | + <View style={styles.container}> | |
66 | + <Text style={styles.logoPoint}>회원 가입</Text> | |
67 | + <ScrollView contentContainerStyle={styles.scrollContainer}> | |
68 | + <View style={styles.formGroup}> | |
69 | + <Text style={styles.inputLabel}>아이디</Text> | |
70 | + <TextInput | |
71 | + style={styles.input} | |
72 | + placeholder="아이디를 입력하세요" | |
73 | + placeholderTextColor={'#aaa'} | |
74 | + onChangeText={text => handleInputChange('id', text)} | |
75 | + /> | |
76 | + </View> | |
77 | + <View style={styles.formGroup}> | |
78 | + <Text style={styles.inputLabel}>비밀번호</Text> | |
79 | + <TextInput | |
80 | + style={styles.input} | |
81 | + placeholder="비밀번호를 입력하세요" | |
82 | + secureTextEntry | |
83 | + placeholderTextColor={'#aaa'} | |
84 | + onChangeText={text => handleInputChange('password', text)} | |
85 | + /> | |
86 | + </View> | |
87 | + <View style={styles.formGroup}> | |
88 | + <Text style={styles.inputLabel}>이메일</Text> | |
89 | + <TextInput | |
90 | + style={styles.input} | |
91 | + placeholder="이메일을 입력하세요" | |
92 | + placeholderTextColor={'#aaa'} | |
93 | + onChangeText={text => handleInputChange('email', text)} | |
94 | + /> | |
95 | + </View> | |
96 | + <View style={styles.formGroup}> | |
97 | + <Text style={styles.inputLabel}>성별</Text> | |
98 | + <View style={styles.radioForm}> | |
99 | + {radio_props.map((radio, index) => ( | |
100 | + <RadioButton | |
101 | + key={index} | |
102 | + isSelected={agreementData.sex === radio.value} | |
103 | + onPress={() => handleInputChange('sex', radio.value)} | |
104 | + buttonColor={'#B0C4DE'} | |
105 | + selectedButtonColor={'#007AFF'} | |
106 | + labelColor={'#333'} | |
107 | + style={styles.radioButton}> | |
108 | + {radio.label} | |
109 | + </RadioButton> | |
110 | + ))} | |
111 | + </View> | |
112 | + </View> | |
113 | + <View style={styles.formGroup}> | |
114 | + <Text style={styles.inputLabel}>전화번호</Text> | |
115 | + <TextInput | |
116 | + style={styles.input} | |
117 | + placeholder="전화번호를 입력하세요" | |
118 | + placeholderTextColor={'#aaa'} | |
119 | + onChangeText={handlePhoneChange} | |
120 | + value={agreementData.phone} | |
121 | + keyboardType="phone-pad" | |
122 | + /> | |
123 | + </View> | |
124 | + <TouchableOpacity style={styles.button} onPress={handleSubmit}> | |
125 | + <Text style={styles.buttonText}>등록하기</Text> | |
126 | + </TouchableOpacity> | |
127 | + </ScrollView> | |
128 | + </View> | |
129 | + ); | |
130 | +} | |
131 | + | |
132 | +const styles = StyleSheet.create({ | |
133 | + container: { | |
134 | + flex: 1, | |
135 | + backgroundColor: '#f8f8f8', | |
136 | + }, | |
137 | + scrollContainer: { | |
138 | + padding: 16, | |
139 | + alignItems: 'center', | |
140 | + }, | |
141 | + logoPoint: { | |
142 | + fontSize: 28, | |
143 | + color: '#007AFF', | |
144 | + fontWeight: 'bold', | |
145 | + marginVertical: 20, | |
146 | + }, | |
147 | + formGroup: { | |
148 | + width: '100%', | |
149 | + marginBottom: 20, | |
150 | + }, | |
151 | + inputLabel: { | |
152 | + color: '#666', | |
153 | + marginBottom: 8, | |
154 | + fontSize: 16, | |
155 | + }, | |
156 | + input: { | |
157 | + borderColor: '#ddd', | |
158 | + borderRadius: 8, | |
159 | + borderWidth: 1, | |
160 | + padding: 12, | |
161 | + backgroundColor: '#fff', | |
162 | + fontSize: 16, | |
163 | + }, | |
164 | + radioForm: { | |
165 | + flexDirection: 'row', | |
166 | + justifyContent: 'flex-start', | |
167 | + marginVertical: 12, | |
168 | + }, | |
169 | + radioButton: { | |
170 | + marginRight: 20, | |
171 | + }, | |
172 | + button: { | |
173 | + backgroundColor: '#007AFF', | |
174 | + padding: 15, | |
175 | + borderRadius: 8, | |
176 | + width: '100%', | |
177 | + alignItems: 'center', | |
178 | + }, | |
179 | + buttonText: { | |
180 | + color: '#fff', | |
181 | + fontSize: 16, | |
182 | + fontWeight: 'bold', | |
183 | + }, | |
184 | +}); |
--- src/screens/AnalysisScreen.tsx
+++ src/screen/AnalysisScreen.js
... | ... | @@ -1,18 +1,6 @@ |
1 |
-import {StyleSheet, View, Text, Image} from 'react-native'; |
|
1 |
+import { StyleSheet, View, Text, Image } from 'react-native'; |
|
2 | 2 |
import React from 'react'; |
3 |
-import AnalysisComponent from './AnalysisComponent'; |
|
4 |
- |
|
5 |
-// 타입 정의 |
|
6 |
-type ScoreProps = { |
|
7 |
- score: number; |
|
8 |
- distance: number; |
|
9 |
- time: number; |
|
10 |
-}; |
|
11 |
- |
|
12 |
-type TotalBoxProps = { |
|
13 |
- distance?: number; |
|
14 |
- time?: number; |
|
15 |
-}; |
|
3 |
+import AnalysisComponent from '../component/AnalysisComponent'; |
|
16 | 4 |
|
17 | 5 |
// 더미데이터 |
18 | 6 |
const data = { |
... | ... | @@ -37,7 +25,7 @@ |
37 | 25 |
} |
38 | 26 |
|
39 | 27 |
// 운행 점수 컴포넌트 |
40 |
-const Score: React.FC<ScoreProps> = ({score}) => { |
|
28 |
+function Score({ score }) { |
|
41 | 29 |
return ( |
42 | 30 |
<View style={styles.scoreContainer}> |
43 | 31 |
<View style={styles.scoreTextBox}> |
... | ... | @@ -49,10 +37,10 @@ |
49 | 37 |
<Image source={require('../asset/bicycle.png')} style={styles.image} /> |
50 | 38 |
</View> |
51 | 39 |
); |
52 |
-}; |
|
40 |
+} |
|
53 | 41 |
|
54 | 42 |
// 총 이동거리, 주행시간 컴포넌트 |
55 |
-const TotalBox: React.FC<TotalBoxProps> = ({distance, time}) => { |
|
43 |
+function TotalBox({ distance, time }) { |
|
56 | 44 |
return ( |
57 | 45 |
<View style={styles.totalBoxContainer}> |
58 | 46 |
<View style={styles.totalBoxLabelContainer}> |
... | ... | @@ -74,7 +62,7 @@ |
74 | 62 |
</View> |
75 | 63 |
</View> |
76 | 64 |
); |
77 |
-}; |
|
65 |
+} |
|
78 | 66 |
|
79 | 67 |
const styles = StyleSheet.create({ |
80 | 68 |
container: { |
+++ src/screen/CameraScreen.js
... | ... | @@ -0,0 +1,125 @@ |
1 | +import React, { useEffect, useState, useRef } from 'react'; | |
2 | +import { View, Button, StyleSheet, Alert, Text, Linking } from 'react-native'; | |
3 | +import { Camera, useCameraDevices } from 'react-native-vision-camera'; | |
4 | +import { useNavigation } from '@react-navigation/native'; | |
5 | + | |
6 | +const CameraScreen = () => { | |
7 | + const [hasPermission, setHasPermission] = useState(false); | |
8 | + const [isDeviceReady, setIsDeviceReady] = useState(false); | |
9 | + const devices = useCameraDevices(); | |
10 | + const device = devices.front; | |
11 | + const camera = useRef(null); | |
12 | + const navigation = useNavigation(); | |
13 | + | |
14 | + // Function to check and request camera permission | |
15 | + const checkPermission = async () => { | |
16 | + const cameraPermission = await Camera.getCameraPermissionStatus(); | |
17 | + console.log(cameraPermission) | |
18 | + if (cameraPermission === 'granted') { | |
19 | + setHasPermission(true); | |
20 | + setIsDeviceReady(true); | |
21 | + } else if (cameraPermission === 'not-determined') { | |
22 | + const newCameraPermission = await Camera.requestCameraPermission(); | |
23 | + if (newCameraPermission === 'granted') { | |
24 | + setHasPermission(true); | |
25 | + setIsDeviceReady(true); | |
26 | + } else { | |
27 | + setHasPermission(false); | |
28 | + Alert.alert('카메라 권한 필요', '카메라 권한이 필요합니다.', [ | |
29 | + { text: '설정으로 이동', onPress: () => Linking.openSettings() }, | |
30 | + { text: '취소', style: 'cancel' }, | |
31 | + ]); | |
32 | + } | |
33 | + } else { | |
34 | + setHasPermission(false); | |
35 | + Alert.alert('카메라 권한 필요', '카메라 권한이 필요합니다.', [ | |
36 | + { text: '설정으로 이동', onPress: () => Linking.openSettings() }, | |
37 | + { text: '취소', style: 'cancel' }, | |
38 | + ]); | |
39 | + } | |
40 | + }; | |
41 | + | |
42 | + useEffect(() => { | |
43 | + // Check permission once on component mount | |
44 | + checkPermission(); | |
45 | + }, []); | |
46 | + | |
47 | + useEffect(() => { | |
48 | + // Update the device readiness when the camera device changes | |
49 | + if (device) { | |
50 | + setIsDeviceReady(true); | |
51 | + } else { | |
52 | + setIsDeviceReady(false); | |
53 | + } | |
54 | + }, [device]); | |
55 | + | |
56 | + const handleCapture = async () => { | |
57 | + if (camera.current) { | |
58 | + try { | |
59 | + const photo = await camera.current.takePhoto({ | |
60 | + flash: 'off', | |
61 | + qualityPrioritization: 'speed', | |
62 | + }); | |
63 | + console.log('Photo taken:', photo); | |
64 | + navigation.navigate('Gps'); // Move to the next screen after capturing photo | |
65 | + } catch (error) { | |
66 | + console.error('Photo capture error:', error.message); | |
67 | + Alert.alert('오류', '사진 캡처 중 오류가 발생했습니다.'); | |
68 | + } | |
69 | + } | |
70 | + }; | |
71 | + | |
72 | + if (!hasPermission) { | |
73 | + return ( | |
74 | + <View style={styles.permissionContainer}> | |
75 | + <Text>카메라 권한을 요청 중입니다...</Text> | |
76 | + </View> | |
77 | + ); | |
78 | + } | |
79 | + | |
80 | + return ( | |
81 | + <View style={styles.container}> | |
82 | + {isDeviceReady && device ? ( | |
83 | + <Camera | |
84 | + ref={camera} | |
85 | + style={StyleSheet.absoluteFill} | |
86 | + device={device} | |
87 | + photo={true} | |
88 | + video={false} | |
89 | + isActive={true} | |
90 | + /> | |
91 | + ) : ( | |
92 | + <View style={styles.buttonContainer}> | |
93 | + <Text>카메라 장치 준비 중...</Text> | |
94 | + <Button title="다음 페이지로 이동" onPress={() => navigation.navigate('Gps')} /> | |
95 | + </View> | |
96 | + )} | |
97 | + <Button title="Capture" onPress={handleCapture} style={styles.captureButton} /> | |
98 | + </View> | |
99 | + ); | |
100 | +}; | |
101 | + | |
102 | +const styles = StyleSheet.create({ | |
103 | + container: { | |
104 | + flex: 1, | |
105 | + justifyContent: 'center', | |
106 | + alignItems: 'center', | |
107 | + }, | |
108 | + buttonContainer: { | |
109 | + justifyContent: 'center', | |
110 | + alignItems: 'center', | |
111 | + flex: 1, | |
112 | + }, | |
113 | + captureButton: { | |
114 | + position: 'absolute', | |
115 | + bottom: 20, | |
116 | + alignSelf: 'center', | |
117 | + }, | |
118 | + permissionContainer: { | |
119 | + flex: 1, | |
120 | + justifyContent: 'center', | |
121 | + alignItems: 'center', | |
122 | + }, | |
123 | +}); | |
124 | + | |
125 | +export default CameraScreen; |
+++ src/screen/GpsScreen.js
... | ... | @@ -0,0 +1,236 @@ |
1 | +import React, { useState, useEffect } from 'react'; | |
2 | +import { View, Text, StyleSheet, Alert, TouchableOpacity } from 'react-native'; | |
3 | +import AsyncStorage from '@react-native-async-storage/async-storage'; | |
4 | +import crypto from 'crypto-js'; | |
5 | +import { startBackgroundTask, stopBackgroundTask, locations } from '../services/LocationService'; | |
6 | +import Api from '../api/ApiUtils'; | |
7 | +import { useNavigation } from '@react-navigation/native'; | |
8 | +import Icon from 'react-native-vector-icons/FontAwesome'; // FontAwesome 아이콘 사용 | |
9 | + | |
10 | +const GpsScreen = () => { | |
11 | + const [isMeasuring, setIsMeasuring] = useState(false); | |
12 | + const [elapsedTime, setElapsedTime] = useState(0); // 초 단위의 경과 시간 | |
13 | + const [tripId, setTripId] = useState(''); // trip_id 상태 추가 | |
14 | + const [userId, setUserId] = useState(''); // user_id 상태 추가 | |
15 | + const navigation = useNavigation(); // useNavigation 훅으로 navigation 객체 가져오기 | |
16 | + | |
17 | + useEffect(() => { | |
18 | + let timer; | |
19 | + | |
20 | + if (isMeasuring) { | |
21 | + timer = setInterval(() => { | |
22 | + setElapsedTime(prevElapsedTime => prevElapsedTime + 1); | |
23 | + }, 1000); // 1초마다 실행 | |
24 | + } else { | |
25 | + clearInterval(timer); | |
26 | + } | |
27 | + | |
28 | + return () => { | |
29 | + clearInterval(timer); | |
30 | + }; | |
31 | + }, [isMeasuring]); | |
32 | + | |
33 | + useEffect(() => { | |
34 | + // 앱이 처음 실행될 때 user_id를 AsyncStorage에서 가져옴 | |
35 | + const fetchUserId = async () => { | |
36 | + try { | |
37 | + const storedUserId = await AsyncStorage.getItem('user_id'); | |
38 | + if (storedUserId !== null) { | |
39 | + setUserId(storedUserId); | |
40 | + } | |
41 | + } catch (error) { | |
42 | + console.error('Error fetching user_id from AsyncStorage:', error); | |
43 | + } | |
44 | + }; | |
45 | + | |
46 | + fetchUserId(); | |
47 | + }, []); | |
48 | + | |
49 | + const generateTripId = () => { | |
50 | + // 해시 값을 생성 | |
51 | + const rand = Math.random().toString(); | |
52 | + const date = new Date(); | |
53 | + const sha256Hash = crypto.SHA256(rand + ',' + date.toString()).toString(crypto.enc.Hex); | |
54 | + return sha256Hash; | |
55 | + }; | |
56 | + | |
57 | + const handleStart = async () => { | |
58 | + try { | |
59 | + const newTripId = generateTripId(); | |
60 | + setTripId(newTripId); // trip_id 상태 업데이트 | |
61 | + await startBackgroundTask(); | |
62 | + setIsMeasuring(true); | |
63 | + } catch (error) { | |
64 | + console.error('Error starting background task:', error); | |
65 | + Alert.alert('백그라운드 작업 오류', '백그라운드 작업을 시작할 수 없습니다.'); | |
66 | + setIsMeasuring(false); | |
67 | + } | |
68 | + }; | |
69 | + | |
70 | + const handleStop = async () => { | |
71 | + setIsMeasuring(false); | |
72 | + setElapsedTime(0); // 시간 초기화 | |
73 | + console.log('Measuring stopped.'); | |
74 | + | |
75 | + try { | |
76 | + await stopBackgroundTask(); | |
77 | + | |
78 | + const dataToSend = { | |
79 | + user_id: userId, // AsyncStorage에서 가져온 user_id 사용 | |
80 | + trip_id: tripId, // 상태에서 trip_id 가져오기 | |
81 | + trip_log: locations, | |
82 | + }; | |
83 | + | |
84 | + console.log('Data to send:', dataToSend); | |
85 | + | |
86 | + // 서버로 전송 | |
87 | + try { | |
88 | + const response = await Api.sendTripLog(dataToSend, navigation); | |
89 | + console.log(":::::", response); | |
90 | + } catch (error) { | |
91 | + Alert.alert('로그인 실패', error.message); | |
92 | + } | |
93 | + | |
94 | + } catch (error) { | |
95 | + console.error('Error stopping background task:', error); | |
96 | + } | |
97 | + }; | |
98 | + | |
99 | + const handleLogout = () => { | |
100 | + AsyncStorage.removeItem('token'); | |
101 | + navigation.navigate('Login'); | |
102 | + Alert.alert('로그아웃', '로그아웃되었습니다.'); | |
103 | + }; | |
104 | + | |
105 | + const handleHistory = () => { | |
106 | + navigation.navigate('Analysis'); | |
107 | + Alert.alert('히스토리', '히스토리 화면으로 이동합니다.'); | |
108 | + }; | |
109 | + | |
110 | + // 시간 형식으로 변환 | |
111 | + const formatTime = (seconds) => { | |
112 | + const hours = Math.floor(seconds / 3600); | |
113 | + const minutes = Math.floor((seconds % 3600) / 60); | |
114 | + const secs = seconds % 60; | |
115 | + return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; | |
116 | + }; | |
117 | + | |
118 | + return ( | |
119 | + <View style={styles.container}> | |
120 | + <View style={styles.timerContainer}> | |
121 | + <Text style={styles.time}>{formatTime(elapsedTime)}</Text> | |
122 | + </View> | |
123 | + <View style={styles.buttonRowContainer}> | |
124 | + <TouchableOpacity | |
125 | + style={[styles.button, styles.startButton, isMeasuring && styles.buttonDisabled]} | |
126 | + onPress={handleStart} | |
127 | + disabled={isMeasuring} | |
128 | + > | |
129 | + <Icon name="play" size={20} color="#FFFFFF" /> | |
130 | + <Text style={styles.buttonText}>측정 시작</Text> | |
131 | + </TouchableOpacity> | |
132 | + <TouchableOpacity | |
133 | + style={[styles.button, styles.stopButton, !isMeasuring && styles.buttonDisabled]} | |
134 | + onPress={handleStop} | |
135 | + disabled={!isMeasuring} | |
136 | + > | |
137 | + <Icon name="stop" size={20} color="#FFFFFF" /> | |
138 | + <Text style={styles.buttonText}>측정 종료</Text> | |
139 | + </TouchableOpacity> | |
140 | + </View> | |
141 | + <View style={styles.fullWidthButtonContainer}> | |
142 | + <TouchableOpacity | |
143 | + style={[styles.fullWidthButton, styles.historyButton]} | |
144 | + onPress={handleHistory} | |
145 | + > | |
146 | + <Text style={styles.fullWidthButtonText}>분석결과</Text> | |
147 | + </TouchableOpacity> | |
148 | + <TouchableOpacity | |
149 | + style={[styles.fullWidthButton, styles.logoutButton]} | |
150 | + onPress={handleLogout} | |
151 | + > | |
152 | + <Text style={styles.blackbuttonText}>로그아웃</Text> | |
153 | + </TouchableOpacity> | |
154 | + </View> | |
155 | + </View> | |
156 | + ); | |
157 | +}; | |
158 | + | |
159 | +const styles = StyleSheet.create({ | |
160 | + container: { | |
161 | + flex: 1, | |
162 | + justifyContent: 'center', | |
163 | + alignItems: 'center', | |
164 | + padding: 20, | |
165 | + backgroundColor: '#F7F7F7', // 배경색 | |
166 | + }, | |
167 | + timerContainer: { | |
168 | + marginVertical: 30, | |
169 | + padding: 20, | |
170 | + borderRadius: 10, | |
171 | + width: '100%', | |
172 | + alignItems: 'center', | |
173 | + }, | |
174 | + time: { | |
175 | + fontSize: 68, | |
176 | + fontWeight: 'bold', | |
177 | + color: '#007AFF', // 타이머 색상 | |
178 | + }, | |
179 | + buttonRowContainer: { | |
180 | + flexDirection: 'row', | |
181 | + width: '100%', | |
182 | + justifyContent: 'space-between', | |
183 | + paddingHorizontal: 20, | |
184 | + marginBottom: 20, | |
185 | + }, | |
186 | + button: { | |
187 | + flex: 1, | |
188 | + paddingVertical: 15, | |
189 | + borderRadius: 10, | |
190 | + marginHorizontal: 5, | |
191 | + alignItems: 'center', | |
192 | + justifyContent: 'center', // 중앙 정렬 | |
193 | + }, | |
194 | + startButton: { | |
195 | + backgroundColor: '#007AFF', // 시작 버튼 색상 | |
196 | + }, | |
197 | + stopButton: { | |
198 | + backgroundColor: '#FF3B30', // 종료 버튼 색상 | |
199 | + }, | |
200 | + fullWidthButtonContainer: { | |
201 | + width: '100%', | |
202 | + paddingHorizontal: 20, | |
203 | + }, | |
204 | + fullWidthButton: { | |
205 | + paddingVertical: 15, | |
206 | + borderRadius: 10, | |
207 | + marginVertical: 5, | |
208 | + alignItems: 'center', | |
209 | + }, | |
210 | + logoutButton: { | |
211 | + backgroundColor: '#eeeeee', // 로그아웃 버튼 색상 (연한 무채색) | |
212 | + }, | |
213 | + historyButton: { | |
214 | + backgroundColor: '#CAF4FF', // 분석결과 버튼 색상 (연한 파란색) | |
215 | + }, | |
216 | + buttonText: { | |
217 | + color: '#FFFFFF', | |
218 | + fontWeight: 'bold', | |
219 | + fontSize: 16, | |
220 | + }, | |
221 | + blackbuttonText: { | |
222 | + fontWeight: 'bold', | |
223 | + fontSize: 16, | |
224 | + | |
225 | + }, | |
226 | + fullWidthButtonText: { | |
227 | + color:'#007AFF', | |
228 | + fontWeight: 'bold', | |
229 | + fontSize: 16, | |
230 | + }, | |
231 | + buttonDisabled: { | |
232 | + opacity: 0.5, | |
233 | + }, | |
234 | +}); | |
235 | + | |
236 | +export default GpsScreen; |
+++ src/screen/LoginScreen.js
... | ... | @@ -0,0 +1,112 @@ |
1 | +import React, { useState } from 'react'; | |
2 | +import { View, TextInput, Text, TouchableOpacity, Alert, StyleSheet } from 'react-native'; | |
3 | +import Api from '../api/ApiUtils'; | |
4 | +import AsyncStorage from '@react-native-async-storage/async-storage'; | |
5 | +import { useNavigation } from '@react-navigation/native'; | |
6 | + | |
7 | +const LoginScreen = () => { | |
8 | + const [userId, setUserId] = useState(''); | |
9 | + const [password, setPassword] = useState(''); | |
10 | + const navigation = useNavigation(); | |
11 | + | |
12 | + const handleLogin = async () => { | |
13 | + const credentials = { | |
14 | + id: userId, | |
15 | + password: password, | |
16 | + }; | |
17 | + try { | |
18 | + const response = await Api.login(credentials); | |
19 | + console.log(response); | |
20 | + if(response.result === 'success'){ | |
21 | + await AsyncStorage.setItem('token', JSON.stringify({accessToken:response.token})); | |
22 | + await AsyncStorage.setItem('user_id', credentials.id); | |
23 | + navigation.navigate('Selection'); | |
24 | + }else{ | |
25 | + Alert.alert('로그인 정보를 다시 한번 확인해주세요.'); | |
26 | + } | |
27 | + | |
28 | + | |
29 | + } catch (error) { | |
30 | + Alert.alert('로그인 실패', error.message); | |
31 | + } | |
32 | + }; | |
33 | + | |
34 | + return ( | |
35 | + <View style={styles.container}> | |
36 | + <Text style={styles.logo}>로그인</Text> | |
37 | + <TextInput | |
38 | + style={styles.input} | |
39 | + placeholder="아이디" | |
40 | + value={userId} | |
41 | + onChangeText={setUserId} | |
42 | + /> | |
43 | + <TextInput | |
44 | + style={styles.input} | |
45 | + placeholder="비밀번호" | |
46 | + secureTextEntry | |
47 | + value={password} | |
48 | + onChangeText={setPassword} | |
49 | + /> | |
50 | + <TouchableOpacity style={styles.button} onPress={handleLogin}> | |
51 | + <Text style={styles.buttonText}>로그인</Text> | |
52 | + </TouchableOpacity> | |
53 | + <View style={styles.footer}> | |
54 | + <TouchableOpacity | |
55 | + onPress={() => navigation.navigate('Agreement')} | |
56 | + > | |
57 | + <Text style={styles.footerText}>회원가입</Text> | |
58 | + </TouchableOpacity> | |
59 | + </View> | |
60 | + </View> | |
61 | + ); | |
62 | +}; | |
63 | + | |
64 | +const styles = StyleSheet.create({ | |
65 | + container: { | |
66 | + flex: 1, | |
67 | + justifyContent: 'center', | |
68 | + padding: 20, | |
69 | + backgroundColor: '#F7F7F7', | |
70 | + }, | |
71 | + logo: { | |
72 | + fontSize: 28, | |
73 | + color: '#007AFF', | |
74 | + fontWeight: 'bold', | |
75 | + marginBottom: 30, | |
76 | + textAlign: 'center', | |
77 | + }, | |
78 | + input: { | |
79 | + height: 50, | |
80 | + borderColor: '#CED4DA', | |
81 | + borderWidth: 1, | |
82 | + borderRadius: 10, | |
83 | + paddingHorizontal: 15, | |
84 | + marginBottom: 15, | |
85 | + backgroundColor: '#FFFFFF', | |
86 | + fontSize: 16, | |
87 | + }, | |
88 | + button: { | |
89 | + backgroundColor: '#007AFF', | |
90 | + paddingVertical: 15, | |
91 | + borderRadius: 10, | |
92 | + alignItems: 'center', | |
93 | + marginBottom: 20, | |
94 | + }, | |
95 | + buttonText: { | |
96 | + color: '#FFFFFF', | |
97 | + fontSize: 16, | |
98 | + fontWeight: 'bold', | |
99 | + }, | |
100 | + footer: { | |
101 | + alignItems: 'center', | |
102 | + marginTop: 20, | |
103 | + }, | |
104 | + footerText: { | |
105 | + fontSize: 16, | |
106 | + color: '#666', | |
107 | + marginBottom: 10, | |
108 | + }, | |
109 | + | |
110 | +}); | |
111 | + | |
112 | +export default LoginScreen; |
+++ src/screen/SelectionScreen.js
... | ... | @@ -0,0 +1,53 @@ |
1 | +import React from 'react'; | |
2 | +import { View, Text, TouchableOpacity, StyleSheet, Platform } from 'react-native'; | |
3 | + | |
4 | +const SelectionScreen = ({ navigation }) => { | |
5 | + return ( | |
6 | + <View style={styles.container}> | |
7 | + <TouchableOpacity style={styles.startButton} onPress={() => navigation.navigate('Camera')}> | |
8 | + <Text style={styles.buttonText}>운행시작</Text> | |
9 | + </TouchableOpacity> | |
10 | + <TouchableOpacity style={styles.historyButton} onPress={() => navigation.navigate('Analysis')}> | |
11 | + <Text style={styles.blueButtonText}>히스토리</Text> | |
12 | + </TouchableOpacity> | |
13 | + </View> | |
14 | + ); | |
15 | +}; | |
16 | + | |
17 | +const styles = StyleSheet.create({ | |
18 | + container: { | |
19 | + flex: 1, | |
20 | + justifyContent: 'center', // 중앙 정렬 | |
21 | + alignItems: 'center', | |
22 | + backgroundColor: '#fff', // 배경색 (흰색) | |
23 | + }, | |
24 | + startButton: { | |
25 | + backgroundColor: '#007AFF', // iOS 스타일의 버튼 색상 | |
26 | + borderRadius: 10, | |
27 | + padding: 15, | |
28 | + width: '80%', // 버튼 너비 | |
29 | + alignItems: 'center', | |
30 | + marginVertical: 10, // 버튼 간격 | |
31 | + }, | |
32 | + historyButton: { | |
33 | + backgroundColor: '#CAF4FF', | |
34 | + borderRadius: 10, | |
35 | + padding: 15, | |
36 | + width: '80%', // 버튼 너비 | |
37 | + alignItems: 'center', | |
38 | + marginVertical: 10, // 버튼 간격 | |
39 | + }, | |
40 | + buttonText: { | |
41 | + color: '#FFFFFF', // 버튼 텍스트 색상 | |
42 | + fontSize: 18, | |
43 | + fontWeight: '600', // 텍스트 두께 | |
44 | + }, | |
45 | + | |
46 | + blueButtonText: { | |
47 | + color: '#007AFF', // 버튼 텍스트 색상 | |
48 | + fontSize: 18, | |
49 | + fontWeight: '600', // 텍스트 두께 | |
50 | + }, | |
51 | +}); | |
52 | + | |
53 | +export default SelectionScreen; |
--- src/screens/AgreementScreen.tsx
... | ... | @@ -1,176 +0,0 @@ |
1 | -import { | |
2 | - StyleSheet, | |
3 | - ScrollView, | |
4 | - View, | |
5 | - Text, | |
6 | - TextInput, | |
7 | - TouchableOpacity, | |
8 | -} from 'react-native'; | |
9 | -import React from 'react'; | |
10 | -import RadioButton from '../component/RadioButton'; | |
11 | -import {agreementAPI} from '../utils/loginUtils'; | |
12 | -import {useNavigation} from '@react-navigation/native'; | |
13 | -import {NativeStackNavigationProp} from '@react-navigation/native-stack'; | |
14 | -import {RootStackParam} from '../utils/RootStackParam'; | |
15 | - | |
16 | -export default function AgreementScreen() { | |
17 | - const navigation = useNavigation<NativeStackNavigationProp<RootStackParam>>(); | |
18 | - const [agreementData, setAgreement] = React.useState({ | |
19 | - id: '', | |
20 | - password: '', | |
21 | - email: '', | |
22 | - sex: null, | |
23 | - phone: '', | |
24 | - }); | |
25 | - | |
26 | - const radio_props = [ | |
27 | - {label: '여성', value: '여성'}, | |
28 | - {label: '남성', value: '남성'}, | |
29 | - ]; | |
30 | - | |
31 | - const handleInputChange = (field: string, value: string | number | null) => { | |
32 | - setAgreement(prevAgreement => ({ | |
33 | - ...prevAgreement, | |
34 | - [field]: value, | |
35 | - })); | |
36 | - }; | |
37 | - | |
38 | - const handleSubmit = async () => { | |
39 | - console.log(agreementData); // | |
40 | - try { | |
41 | - const response = await agreementAPI(agreementData); | |
42 | - console.log('Response from server:', response); | |
43 | - // if (response.Authorization) { | |
44 | - // navigation.navigate('LoginScreen'); | |
45 | - // } | |
46 | - navigation.navigate('LoginScreen'); | |
47 | - } catch (error) { | |
48 | - console.error('Error submitting agreement:', error); | |
49 | - } | |
50 | - }; | |
51 | - | |
52 | - // 전화번호 포맷팅 | |
53 | - const formatPhoneNumber = (text: string) => { | |
54 | - const cleaned = text.replace(/\D/g, ''); // Remove all non-numeric characters | |
55 | - if (cleaned.length === 11) { | |
56 | - const match = cleaned.match(/^(\d{3})(\d{4})(\d{4})$/); | |
57 | - if (match) { | |
58 | - return `${match[1]}-${match[2]}-${match[3]}`; | |
59 | - } | |
60 | - } | |
61 | - return text; | |
62 | - }; | |
63 | - | |
64 | - const handlePhoneChange = (text: string) => { | |
65 | - const formatted = formatPhoneNumber(text); | |
66 | - handleInputChange('phone', formatted); | |
67 | - }; | |
68 | - | |
69 | - return ( | |
70 | - <View style={styles.container}> | |
71 | - <Text style={styles.logoPoint}>회원 가입</Text> | |
72 | - <ScrollView> | |
73 | - <Text style={styles.inputText}> 아이디</Text> | |
74 | - <TextInput | |
75 | - style={styles.input} | |
76 | - placeholder="아이디를 입력하세요" | |
77 | - placeholderTextColor={'#cccccc'} | |
78 | - onChangeText={text => handleInputChange('id', text)} | |
79 | - /> | |
80 | - <Text style={styles.inputText}> 비밀번호 </Text> | |
81 | - <TextInput | |
82 | - style={styles.input} | |
83 | - placeholder="비밀번호를 입력하세요" | |
84 | - secureTextEntry={true} | |
85 | - placeholderTextColor={'#cccccc'} | |
86 | - onChangeText={text => handleInputChange('password', text)} | |
87 | - /> | |
88 | - <Text style={styles.inputText}> 이메일 </Text> | |
89 | - <TextInput | |
90 | - style={styles.input} | |
91 | - placeholder="이메일을 입력하세요" | |
92 | - placeholderTextColor={'#cccccc'} | |
93 | - onChangeText={text => handleInputChange('email', text)} | |
94 | - /> | |
95 | - <Text style={styles.inputText}> 성별 </Text> | |
96 | - <View style={styles.radioForm}> | |
97 | - {radio_props.map((radio, index) => ( | |
98 | - <RadioButton | |
99 | - key={index} | |
100 | - isSelected={agreementData.sex === radio.value} | |
101 | - onPress={() => handleInputChange('sex', radio.value)} | |
102 | - buttonColor={'#2196f3'} | |
103 | - selectedButtonColor={'#2196f3'} | |
104 | - labelColor={'#000'} | |
105 | - style={styles.radioButton}> | |
106 | - {radio.label} | |
107 | - </RadioButton> | |
108 | - ))} | |
109 | - </View> | |
110 | - <Text style={styles.inputText}> 전화번호 </Text> | |
111 | - <TextInput | |
112 | - style={styles.input} | |
113 | - placeholder="전화번호를 입력하세요" | |
114 | - placeholderTextColor={'#cccccc'} | |
115 | - onChangeText={handlePhoneChange} | |
116 | - value={agreementData.phone} | |
117 | - keyboardType="phone-pad" | |
118 | - /> | |
119 | - <TouchableOpacity style={styles.button} onPress={handleSubmit}> | |
120 | - <Text style={styles.buttonText}> 등록하기 </Text> | |
121 | - </TouchableOpacity> | |
122 | - </ScrollView> | |
123 | - </View> | |
124 | - ); | |
125 | -} | |
126 | - | |
127 | -const styles = StyleSheet.create({ | |
128 | - container: { | |
129 | - flex: 1, | |
130 | - paddingHorizontal: 16, | |
131 | - paddingVertical: 50, | |
132 | - justifyContent: 'center', | |
133 | - backgroundColor: '#ffffff', | |
134 | - }, | |
135 | - logoPoint: { | |
136 | - fontSize: 25, | |
137 | - color: '#3872ef', | |
138 | - fontWeight: 'bold', | |
139 | - textAlign: 'center', | |
140 | - marginTop: 20, | |
141 | - marginBottom: 30, | |
142 | - }, | |
143 | - inputText: { | |
144 | - color: '#959595', | |
145 | - marginVertical: 10, | |
146 | - }, | |
147 | - input: { | |
148 | - borderColor: '#959595', | |
149 | - borderRadius: 10, | |
150 | - borderWidth: 1, | |
151 | - marginBottom: 10, | |
152 | - padding: 10, | |
153 | - backgroundColor: '#ffffff', | |
154 | - }, | |
155 | - radioForm: { | |
156 | - flexDirection: 'row', | |
157 | - justifyContent: 'flex-start', | |
158 | - marginBottom: 16, | |
159 | - marginLeft: 10, | |
160 | - marginTop: 12, | |
161 | - }, | |
162 | - radioButton: { | |
163 | - marginRight: 20, | |
164 | - }, | |
165 | - button: { | |
166 | - backgroundColor: '#3872ef', | |
167 | - padding: 10, | |
168 | - borderRadius: 10, | |
169 | - marginTop: 30, | |
170 | - marginBottom: 10, | |
171 | - alignItems: 'center', | |
172 | - }, | |
173 | - buttonText: { | |
174 | - color: '#fff', | |
175 | - }, | |
176 | -}); |
--- src/screens/CameraScreen.tsx
... | ... | @@ -1,130 +0,0 @@ |
1 | -import { | |
2 | - StyleSheet, | |
3 | - View, | |
4 | - Text, | |
5 | - TouchableOpacity, | |
6 | - Alert, | |
7 | - Linking, | |
8 | -} from 'react-native'; | |
9 | -import React from 'react'; | |
10 | -import useCameraApi from '../utils/useCameraAPI.ts'; | |
11 | -import {Camera, useCameraDevice} from 'react-native-vision-camera'; | |
12 | -import Icon from 'react-native-vector-icons/Entypo.js'; | |
13 | -// import {CameraRoll} from '@react-native-camera-roll/camera-roll'; | |
14 | - | |
15 | -export default function CameraScreen() { | |
16 | - const { | |
17 | - requestStoragePermission, | |
18 | - isPermission, | |
19 | - requestGPSPermission, | |
20 | - isGpsPermission, | |
21 | - sendPhotoFile, | |
22 | - } = useCameraApi(); | |
23 | - const device = useCameraDevice('back'); | |
24 | - const camera = React.useRef<Camera>(null); | |
25 | - | |
26 | - // const [photoPath, setPhotoPath] = React.useState<string | null>(null); | |
27 | - | |
28 | - React.useEffect(() => { | |
29 | - // 카메라 및 위치 권한 확인 | |
30 | - const checkPermissions = async () => { | |
31 | - await requestStoragePermission(permissionGranted => { | |
32 | - if (!permissionGranted) { | |
33 | - Alert.alert( | |
34 | - '카메라 권한이 없습니다.', | |
35 | - '카메라를 사용하려면 권한을 허용해야 합니다.', | |
36 | - [{text: '설정으로 이동', onPress: () => Linking.openSettings()}], | |
37 | - ); | |
38 | - console.log('카메라 권한이 거부되었습니다.'); | |
39 | - } | |
40 | - }); | |
41 | - | |
42 | - await requestGPSPermission(permissionGranted => { | |
43 | - if (!permissionGranted) { | |
44 | - Alert.alert( | |
45 | - '위치 권한이 없습니다.', | |
46 | - '위치를 사용하려면 권한을 허용해야 합니다.', | |
47 | - [{text: '설정으로 이동', onPress: () => Linking.openSettings()}], | |
48 | - ); | |
49 | - console.log('위치 권한이 거부되었습니다.'); | |
50 | - } | |
51 | - }); | |
52 | - }; | |
53 | - | |
54 | - // 디바이스 확인 | |
55 | - if (!device) { | |
56 | - Alert.alert( | |
57 | - '카메라 디바이스가 없습니다.', | |
58 | - '카메라 디바이스를 찾을 수 없습니다.', | |
59 | - ); | |
60 | - } | |
61 | - checkPermissions(); | |
62 | - }, [requestStoragePermission, requestGPSPermission, device]); | |
63 | - | |
64 | - // 사진 찍기 | |
65 | - const takePhotoAsync = async () => { | |
66 | - if (camera.current) { | |
67 | - try { | |
68 | - const photo = await camera.current.takePhoto(); | |
69 | - // console.log('Photo:', photo); // photo 확인 | |
70 | - // setPhotoPath(photo.path); | |
71 | - // await CameraRoll.save(`file://${photo.path}`, { | |
72 | - // type: 'photo', | |
73 | - // }); | |
74 | - | |
75 | - // 사진 전송 | |
76 | - await sendPhotoFile(photo.path); | |
77 | - } catch (error) { | |
78 | - console.error('Error taking photo:', error); | |
79 | - } | |
80 | - } | |
81 | - }; | |
82 | - | |
83 | - const handleTakePhoto = async () => { | |
84 | - await takePhotoAsync(); | |
85 | - }; | |
86 | - | |
87 | - return ( | |
88 | - <View style={{flex: 1}}> | |
89 | - {isPermission && isGpsPermission ? ( | |
90 | - device ? ( | |
91 | - <> | |
92 | - <Camera | |
93 | - style={{flex: 1}} | |
94 | - device={device} | |
95 | - isActive={isPermission} | |
96 | - ref={camera} | |
97 | - photo={true} | |
98 | - enableLocation={true} | |
99 | - /> | |
100 | - {/* 찍은 사진 렌더링 */} | |
101 | - {/* {photoPath ? ( | |
102 | - <Image source={{uri: `file://${photoPath}`}} style={{flex: 1}} /> | |
103 | - ) : ( | |
104 | - <></> | |
105 | - )} */} | |
106 | - <TouchableOpacity | |
107 | - style={styles.takePicture} | |
108 | - onPress={handleTakePhoto}> | |
109 | - <Icon name="circular-graph" size={60} color={'white'} /> | |
110 | - </TouchableOpacity> | |
111 | - </> | |
112 | - ) : ( | |
113 | - <Text>디바이스 없다</Text> | |
114 | - ) | |
115 | - ) : ( | |
116 | - <></> | |
117 | - )} | |
118 | - </View> | |
119 | - ); | |
120 | -} | |
121 | - | |
122 | -const styles = StyleSheet.create({ | |
123 | - takePicture: { | |
124 | - position: 'absolute', | |
125 | - bottom: '5%', | |
126 | - width: '100%', | |
127 | - alignItems: 'center', | |
128 | - justifyContent: 'center', | |
129 | - }, | |
130 | -}); |
--- src/screens/LoginScreen.tsx
... | ... | @@ -1,130 +0,0 @@ |
1 | -import { | |
2 | - StyleSheet, | |
3 | - ScrollView, | |
4 | - View, | |
5 | - Text, | |
6 | - TextInput, | |
7 | - TouchableOpacity, | |
8 | -} from 'react-native'; | |
9 | -import React from 'react'; | |
10 | -import {useNavigation} from '@react-navigation/native'; | |
11 | -import {NativeStackNavigationProp} from '@react-navigation/native-stack'; | |
12 | -import {useLoginAPI} from '../utils/loginUtils'; | |
13 | -import {RootStackParam} from '../utils/RootStackParam'; | |
14 | -import {useFocusEffect} from '@react-navigation/native'; | |
15 | -import AsyncStorage from '@react-native-async-storage/async-storage'; | |
16 | - | |
17 | -export default function LoginScreen() { | |
18 | - const navigation = useNavigation<NativeStackNavigationProp<RootStackParam>>(); | |
19 | - const {loginAPI} = useLoginAPI(navigation); | |
20 | - | |
21 | - const [loginData, setLogin] = React.useState({ | |
22 | - id: '', | |
23 | - password: '', | |
24 | - }); | |
25 | - | |
26 | - const handleInputChange = (field: string, value: string | number | null) => { | |
27 | - setLogin(prevLogin => ({ | |
28 | - ...prevLogin, | |
29 | - [field]: value, | |
30 | - })); | |
31 | - }; | |
32 | - | |
33 | - useFocusEffect( | |
34 | - React.useCallback(() => { | |
35 | - setLogin({ | |
36 | - id: '', | |
37 | - password: '', | |
38 | - }); | |
39 | - }, []), | |
40 | - ); | |
41 | - | |
42 | - // 로그인 | |
43 | - const handleSubmit = async () => { | |
44 | - console.log(loginData); // | |
45 | - try { | |
46 | - const response = await loginAPI(loginData); | |
47 | - await AsyncStorage.setItem('user_id', loginData.id); | |
48 | - console.log('Response from server:', response); | |
49 | - } catch (error) { | |
50 | - console.error('Error submitting agreement:', error); | |
51 | - } | |
52 | - }; | |
53 | - | |
54 | - return ( | |
55 | - <View style={styles.container}> | |
56 | - <Text style={styles.logoPoint}>로그인</Text> | |
57 | - <ScrollView> | |
58 | - <Text style={styles.inputText}> 아이디</Text> | |
59 | - <TextInput | |
60 | - style={styles.input} | |
61 | - placeholder="아이디를 입력하세요" | |
62 | - placeholderTextColor={'#cccccc'} | |
63 | - onChangeText={text => handleInputChange('id', text)} | |
64 | - /> | |
65 | - <Text style={styles.inputText}> 비밀번호 </Text> | |
66 | - <TextInput | |
67 | - style={styles.input} | |
68 | - placeholder="비밀번호를 입력하세요" | |
69 | - secureTextEntry={true} | |
70 | - placeholderTextColor={'#cccccc'} | |
71 | - onChangeText={text => handleInputChange('password', text)} | |
72 | - /> | |
73 | - <TouchableOpacity style={styles.button} onPress={handleSubmit}> | |
74 | - <Text style={styles.buttonText}> 로그인 </Text> | |
75 | - </TouchableOpacity> | |
76 | - <TouchableOpacity | |
77 | - style={styles.toAgreement} | |
78 | - onPress={() => navigation.navigate('AgreementScreen')}> | |
79 | - <Text> 회원가입 하러 가기 </Text> | |
80 | - </TouchableOpacity> | |
81 | - </ScrollView> | |
82 | - </View> | |
83 | - ); | |
84 | -} | |
85 | - | |
86 | -const styles = StyleSheet.create({ | |
87 | - container: { | |
88 | - flex: 1, | |
89 | - paddingHorizontal: 16, | |
90 | - paddingVertical: 50, | |
91 | - justifyContent: 'center', | |
92 | - backgroundColor: '#ffffff', | |
93 | - }, | |
94 | - logoPoint: { | |
95 | - fontSize: 25, | |
96 | - color: '#3872ef', | |
97 | - fontWeight: 'bold', | |
98 | - textAlign: 'center', | |
99 | - marginTop: 20, | |
100 | - marginBottom: 30, | |
101 | - }, | |
102 | - inputText: { | |
103 | - color: '#959595', | |
104 | - marginVertical: 10, | |
105 | - }, | |
106 | - input: { | |
107 | - borderColor: '#959595', | |
108 | - borderRadius: 10, | |
109 | - borderWidth: 1, | |
110 | - marginBottom: 10, | |
111 | - padding: 10, | |
112 | - backgroundColor: '#ffffff', | |
113 | - }, | |
114 | - button: { | |
115 | - backgroundColor: '#3872ef', | |
116 | - padding: 10, | |
117 | - borderRadius: 10, | |
118 | - marginTop: 30, | |
119 | - marginBottom: 10, | |
120 | - }, | |
121 | - buttonText: { | |
122 | - color: '#ffffff', | |
123 | - textAlign: 'center', | |
124 | - }, | |
125 | - toAgreement: { | |
126 | - marginTop: 30, | |
127 | - alignItems: 'center', | |
128 | - color: '#ccc', | |
129 | - }, | |
130 | -}); |
--- src/screens/SendLocation.tsx
... | ... | @@ -1,403 +0,0 @@ |
1 | -import React, { useEffect, useRef, useState } from 'react'; | |
2 | -import { | |
3 | - StyleSheet, | |
4 | - View, | |
5 | - Text, | |
6 | - TouchableOpacity, | |
7 | - Dimensions, | |
8 | -} from 'react-native'; | |
9 | -import { Shadow } from 'react-native-shadow-2'; | |
10 | -import Icon from 'react-native-vector-icons/FontAwesome5'; | |
11 | -import Geolocation from '@react-native-community/geolocation'; | |
12 | -import { sendLocationData } from '../utils/useGeolocationAPI'; | |
13 | -import { useNavigation } from '@react-navigation/native'; | |
14 | -import { NativeStackNavigationProp } from '@react-navigation/native-stack'; | |
15 | -import { RootStackParam } from '../utils/RootStackParam'; | |
16 | -import AsyncStorage from '@react-native-async-storage/async-storage'; | |
17 | -import BackgroundService from 'react-native-background-actions'; | |
18 | -import crypto from 'crypto-js'; | |
19 | - | |
20 | -let intervalId: NodeJS.Timeout | null = null; | |
21 | - | |
22 | -export default function SendLocation() { | |
23 | - const navigation = useNavigation<NativeStackNavigationProp<RootStackParam>>(); | |
24 | - const [time, setTime] = useState<number>(0); | |
25 | - const [time2, setTime2] = useState<number>(0); | |
26 | - const [isRunning, setIsRunning] = useState<boolean>(false); | |
27 | - const [locations, setLocations] = useState<{ | |
28 | - longitudes: string[], | |
29 | - latitudes: string[], | |
30 | - timestamps: string[] | |
31 | - }>({ longitudes: [], latitudes: [], timestamps: [] }); | |
32 | - const locationsRef = useRef(locations); | |
33 | - const [hash, setHash] = useState(""); | |
34 | - const tripIdRef = useRef<string>(hash); | |
35 | - const userIdRef = useRef<string>(''); | |
36 | - const [watchId, setWatchId] = useState<number | null>(null); | |
37 | - | |
38 | - // `hash`가 업데이트될 때마다 `tripIdRef`도 업데이트 | |
39 | - useEffect(() => { | |
40 | - tripIdRef.current = hash; | |
41 | - }, [hash]); | |
42 | - | |
43 | - // `locations`가 업데이트될 때마다 `locationsRef`도 업데이트 | |
44 | - useEffect(() => { | |
45 | - locationsRef.current = locations; | |
46 | - }, [locations]); | |
47 | - | |
48 | - // 컴포넌트 초기화 및 `isRunning` 상태 변경 시 초기화 | |
49 | - useEffect(() => { | |
50 | - const initialize = async () => { | |
51 | - // `user_id`를 AsyncStorage에서 가져옴 | |
52 | - const userId = await AsyncStorage.getItem('user_id'); | |
53 | - if (userId) { | |
54 | - userIdRef.current = userId; | |
55 | - } | |
56 | - | |
57 | - // 해시 값을 생성 | |
58 | - const rand = Math.random().toString(); | |
59 | - const date = new Date(); | |
60 | - const sha256Hash = crypto.SHA256(rand + ',' + date.toString()).toString(crypto.enc.Hex); | |
61 | - setHash(sha256Hash); | |
62 | - | |
63 | - // `isRunning` 상태에 따라 백그라운드 서비스 시작 또는 중지 | |
64 | - if (isRunning) { | |
65 | - await startBackgroundService(); | |
66 | - } else { | |
67 | - await stopBackgroundService(); | |
68 | - } | |
69 | - }; | |
70 | - | |
71 | - initialize(); | |
72 | - }, [isRunning]); | |
73 | - | |
74 | - // `time2` 밀리초 동안 대기하는 함수 | |
75 | - const sleep = (time2: number) => new Promise(resolve => setTimeout(() => resolve(true), time2)); | |
76 | - | |
77 | - // 위치 데이터를 서버로 전송하는 함수 | |
78 | - const sendData = async () => { | |
79 | - const currentLocations = { ...locationsRef.current }; | |
80 | - // 전송할 위치 데이터가 있는지 확인 | |
81 | - if (currentLocations.longitudes.length === 0 || currentLocations.latitudes.length === 0 || currentLocations.timestamps.length === 0) { | |
82 | - console.log("No data to send."); | |
83 | - return; | |
84 | - } | |
85 | - await sendLocationData(userIdRef.current, tripIdRef.current, currentLocations, navigation); | |
86 | - setLocations({ longitudes: [], latitudes: [], timestamps: [] }); | |
87 | - }; | |
88 | - | |
89 | - // 백그라운드 서비스에서 실행될 함수 | |
90 | - const veryIntensiveTask = async (taskDataArguments: any) => { | |
91 | - const { delay } = taskDataArguments; | |
92 | - | |
93 | - while (BackgroundService.isRunning()) { | |
94 | - await sleep(delay); | |
95 | - await sendData(); | |
96 | - } | |
97 | - }; | |
98 | - | |
99 | - // 백그라운드 서비스 옵션 | |
100 | - const options = { | |
101 | - taskName: '측정 서비스', | |
102 | - taskTitle: '측정 중', | |
103 | - taskDesc: '측정 진행 중... ', | |
104 | - taskIcon: { | |
105 | - name: 'ic_launcher', | |
106 | - type: 'mipmap', | |
107 | - }, | |
108 | - color: '#ff00ff', | |
109 | - linkingURI: 'sendLocation', | |
110 | - parameters: { | |
111 | - delay: 15000, | |
112 | - }, | |
113 | - }; | |
114 | - | |
115 | - // 백그라운드 서비스를 시작하는 함수 | |
116 | - const startBackgroundService = async () => { | |
117 | - return new Promise<void>((resolve, reject) => { | |
118 | - let latestPosition: GeolocationPosition | null = null; // 마지막 위치를 저장할 변수 | |
119 | - let locationCheckInterval: NodeJS.Timeout | null = null; // 위치 체크 인터벌을 저장할 변수 | |
120 | - let watchId: number | null = null; // 위치 감시 ID를 저장할 변수 | |
121 | - let lastUpdateTime = Date.now(); // 마지막 업데이트 시간을 저장할 변수 | |
122 | - | |
123 | - // 위치 업데이트를 처리하는 함수 | |
124 | - const handleLocationUpdate = (position: GeolocationPosition) => { | |
125 | - latestPosition = position; // 최신 위치를 업데이트 | |
126 | - lastUpdateTime = Date.now(); // 마지막 업데이트 시간 갱신 | |
127 | - | |
128 | - const { longitude, latitude } = position.coords; | |
129 | - const time = new Date().toISOString(); | |
130 | - | |
131 | - // 한국 시간으로 변환하는 함수 | |
132 | - const formatDate = (time) => { | |
133 | - const date = new Date(time); | |
134 | - const year = date.getFullYear(); | |
135 | - const month = `0${date.getMonth() + 1}`.slice(-2); | |
136 | - const day = `0${date.getDate()}`.slice(-2); | |
137 | - const hours = `0${date.getHours()}`.slice(-2); | |
138 | - const minutes = `0${date.getMinutes()}`.slice(-2); | |
139 | - const seconds = `0${date.getSeconds()}`.slice(-2); | |
140 | - const milliseconds = `00${date.getMilliseconds()}`.slice(-3); | |
141 | - | |
142 | - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; | |
143 | - }; | |
144 | - | |
145 | - const timestamp = formatDate(time); | |
146 | - | |
147 | - // 위치 데이터를 상태에 추가 | |
148 | - setLocations(prevData => ({ | |
149 | - longitudes: [...prevData.longitudes, longitude.toString()], | |
150 | - latitudes: [...prevData.latitudes, latitude.toString()], | |
151 | - timestamps: [...prevData.timestamps, timestamp] | |
152 | - })); | |
153 | - }; | |
154 | - | |
155 | - // 1초마다 위치 업데이트를 확인하고 3초 동안 업데이트가 없을 경우 마지막 위치를 추가 | |
156 | - locationCheckInterval = setInterval(() => { | |
157 | - if (latestPosition && (Date.now() - lastUpdateTime > 3000)) { | |
158 | - const { longitude, latitude } = latestPosition.coords; | |
159 | - const time = new Date().toISOString(); | |
160 | - const timestamp = formatDate(time); | |
161 | - | |
162 | - setLocations(prevData => ({ | |
163 | - longitudes: [...prevData.longitudes, longitude.toString()], | |
164 | - latitudes: [...prevData.latitudes, latitude.toString()], | |
165 | - timestamps: [...prevData.timestamps, timestamp] | |
166 | - })); | |
167 | - lastUpdateTime = Date.now(); | |
168 | - } | |
169 | - }, 1000); | |
170 | - | |
171 | - // 위치 업데이트를 시작하는 함수 | |
172 | - const startWatchingPosition = () => { | |
173 | - watchId = Geolocation.watchPosition( | |
174 | - handleLocationUpdate, | |
175 | - error => { | |
176 | - console.log('watchPosition Error:', error); | |
177 | - reject(error); | |
178 | - }, | |
179 | - { | |
180 | - enableHighAccuracy: false, // 높은 정확도를 사용할지 여부 | |
181 | - distanceFilter: 0, // 최소 거리 필터 | |
182 | - interval: 1000, // 업데이트 간격 (밀리초) | |
183 | - } | |
184 | - ); | |
185 | - }; | |
186 | - | |
187 | - // watchId가 유효한 경우 백그라운드 서비스 시작 | |
188 | - if (watchId === null) { | |
189 | - (async () => { | |
190 | - try { | |
191 | - await BackgroundService.start(veryIntensiveTask, options); | |
192 | - startWatchingPosition(); // 위치 업데이트 시작 | |
193 | - resolve(); | |
194 | - } catch (error) { | |
195 | - reject(error); | |
196 | - } | |
197 | - })(); | |
198 | - } | |
199 | - | |
200 | - // 백그라운드 서비스를 중지할 때의 정리 함수 | |
201 | - return () => { | |
202 | - if (locationCheckInterval !== null) { | |
203 | - clearInterval(locationCheckInterval); | |
204 | - locationCheckInterval = null; | |
205 | - } | |
206 | - if (watchId !== null) { | |
207 | - Geolocation.clearWatch(watchId); | |
208 | - watchId = null; | |
209 | - } | |
210 | - BackgroundService.stop(); | |
211 | - }; | |
212 | - }); | |
213 | - }; | |
214 | - | |
215 | - // 백그라운드 서비스를 중지하는 함수 | |
216 | - const stopBackgroundService = async () => { | |
217 | - if (watchId !== null) { | |
218 | - Geolocation.clearWatch(watchId); | |
219 | - setWatchId(null); | |
220 | - } | |
221 | - if (intervalId) { | |
222 | - clearInterval(intervalId); | |
223 | - intervalId = null; | |
224 | - } | |
225 | - await BackgroundService.stop(); | |
226 | - }; | |
227 | - | |
228 | - // 측정을 시작하는 함수 | |
229 | - const handleStart = () => { | |
230 | - setIsRunning(true); | |
231 | - intervalId = setInterval(() => { | |
232 | - setTime(prevTime => prevTime + 1); | |
233 | - }, 1000); | |
234 | - }; | |
235 | - | |
236 | - // 측정을 중지하는 함수 | |
237 | - const handleStop = () => { | |
238 | - // `watchPosition` 제거 | |
239 | - if (watchId !== null) { | |
240 | - Geolocation.clearWatch(watchId); // watchId 제거 | |
241 | - setWatchId(null); // watchId 초기화 | |
242 | - } | |
243 | - | |
244 | - // `intervalId` 제거 | |
245 | - if (intervalId) { | |
246 | - clearInterval(intervalId); | |
247 | - intervalId = null; | |
248 | - } | |
249 | - | |
250 | - // 상태 초기화 | |
251 | - setTime(0); | |
252 | - setIsRunning(false); | |
253 | - | |
254 | - // 백그라운드 서비스가 실행 중인지 확인 | |
255 | - if (BackgroundService.isRunning()) { | |
256 | - // 백그라운드 서비스 중지 | |
257 | - BackgroundService.stop().then(() => { | |
258 | - console.log("Background service stopped successfully."); | |
259 | - // 중지 후 즉시 데이터 전송 | |
260 | - sendData().then(() => { | |
261 | - console.log("Data sent successfully after stopping background service."); | |
262 | - }).catch(error => { | |
263 | - console.log("Error sending data after stopping background service:", error); | |
264 | - }); | |
265 | - }).catch(error => { | |
266 | - console.log("Error stopping background service:", error); | |
267 | - }); | |
268 | - } else { | |
269 | - // 백그라운드 서비스가 실행 중이 아니면 데이터만 전송 | |
270 | - sendData().then(() => { | |
271 | - console.log("Data sent successfully when background service is not running."); | |
272 | - }).catch(error => { | |
273 | - console.log("Error sending data when background service is not running:", error); | |
274 | - }); | |
275 | - } | |
276 | - }; | |
277 | - | |
278 | - // 시간을 형식화하는 함수 | |
279 | - const formatTime = (time: number): string => { | |
280 | - const hours = Math.floor(time / 3600); | |
281 | - const minutes = Math.floor((time % 3600) / 60); | |
282 | - const seconds = time % 60; | |
283 | - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; | |
284 | - }; | |
285 | - | |
286 | - return ( | |
287 | - <View style={styles.container}> | |
288 | - <View style={{ alignItems: 'center' }}> | |
289 | - <Shadow | |
290 | - distance={8} | |
291 | - startColor={'#3872ef33'} | |
292 | - endColor={'#ffffff'} | |
293 | - offset={[5, 6]} | |
294 | - containerStyle={styles.watchContainer}> | |
295 | - <View style={styles.watchContainer}> | |
296 | - <Text style={{ fontWeight: 'bold', fontSize: 20 }}> | |
297 | - 측정 경과 시간 | |
298 | - </Text> | |
299 | - <Text style={styles.time}> {formatTime(time)} </Text> | |
300 | - </View> | |
301 | - </Shadow> | |
302 | - <View style={styles.buttonContainer}> | |
303 | - <View style={styles.row}> | |
304 | - <TouchableOpacity style={styles.button} onPress={handleStart}> | |
305 | - <Icon name="play" size={25} color={'#717171'} /> | |
306 | - </TouchableOpacity> | |
307 | - <Text> 측정 시작 </Text> | |
308 | - </View> | |
309 | - <View style={styles.row}> | |
310 | - <TouchableOpacity style={styles.button} onPress={handleStop}> | |
311 | - <Icon name="power-off" size={25} color={'#717171'} /> | |
312 | - </TouchableOpacity> | |
313 | - <Text> 측정 종료 </Text> | |
314 | - </View> | |
315 | - </View> | |
316 | - <TouchableOpacity | |
317 | - onPress={() => navigation.navigate('CameraScreen')} | |
318 | - style={styles.navButton}> | |
319 | - <Text> 카메라 촬영 </Text> | |
320 | - </TouchableOpacity> | |
321 | - <TouchableOpacity | |
322 | - style={styles.navButton} | |
323 | - onPress={() => navigation.navigate('AnalysisScreen')}> | |
324 | - <Text> 분석 결과 </Text> | |
325 | - </TouchableOpacity> | |
326 | - <TouchableOpacity | |
327 | - style={styles.logout} | |
328 | - onPress={() => { | |
329 | - AsyncStorage.removeItem('token'); | |
330 | - navigation.navigate('LoginScreen'); | |
331 | - }}> | |
332 | - <Text> 로그아웃 </Text> | |
333 | - </TouchableOpacity> | |
334 | - </View> | |
335 | - </View> | |
336 | - ); | |
337 | -} | |
338 | - | |
339 | -const styles = StyleSheet.create({ | |
340 | - container: { | |
341 | - flex: 1, | |
342 | - paddingHorizontal: 16, | |
343 | - paddingVertical: 50, | |
344 | - justifyContent: 'center', | |
345 | - backgroundColor: '#ffffff', | |
346 | - }, | |
347 | - watchContainer: { | |
348 | - alignItems: 'center', | |
349 | - justifyContent: 'center', | |
350 | - borderRadius: | |
351 | - Math.round( | |
352 | - Dimensions.get('window').width + Dimensions.get('window').height, | |
353 | - ) * 0.35, | |
354 | - width: Dimensions.get('window').width * 0.7, | |
355 | - height: Dimensions.get('window').width * 0.7, | |
356 | - backgroundColor: '#ffffff', | |
357 | - }, | |
358 | - time: { | |
359 | - marginTop: 30, | |
360 | - color: '#3872ef', | |
361 | - fontWeight: 'bold', | |
362 | - fontSize: 50, | |
363 | - }, | |
364 | - buttonContainer: { | |
365 | - flexDirection: 'row', | |
366 | - justifyContent: 'space-between', | |
367 | - paddingHorizontal: 10, | |
368 | - marginTop: 65, | |
369 | - marginBottom: 70, | |
370 | - width: '55%', | |
371 | - }, | |
372 | - row: { | |
373 | - alignItems: 'center', | |
374 | - }, | |
375 | - button: { | |
376 | - backgroundColor: '#F0F0F0', | |
377 | - justifyContent: 'center', | |
378 | - alignItems: 'center', | |
379 | - borderRadius: 100, | |
380 | - width: 60, | |
381 | - height: 60, | |
382 | - marginBottom: 10, | |
383 | - }, | |
384 | - navButton: { | |
385 | - backgroundColor: '#f0f0f0', | |
386 | - alignItems: 'center', | |
387 | - justifyContent: 'center', | |
388 | - height: Dimensions.get('screen').height * 0.05, | |
389 | - width: Dimensions.get('screen').width * 0.7, | |
390 | - marginTop: 15, | |
391 | - borderRadius: 10, | |
392 | - }, | |
393 | - logout: { | |
394 | - borderWidth: 3, | |
395 | - borderColor: '#f0f0f0', | |
396 | - alignItems: 'center', | |
397 | - justifyContent: 'center', | |
398 | - height: Dimensions.get('screen').height * 0.05, | |
399 | - width: Dimensions.get('screen').width * 0.7, | |
400 | - marginTop: 15, | |
401 | - borderRadius: 10, | |
402 | - }, | |
403 | -}); |
+++ src/services/LocationService.js
... | ... | @@ -0,0 +1,160 @@ |
1 | +import React, { useEffect } from 'react'; | |
2 | +import { Platform, PermissionsAndroid, AppState } from 'react-native'; | |
3 | +import Geolocation from 'react-native-geolocation-service'; | |
4 | +import BackgroundActions from 'react-native-background-actions'; | |
5 | + | |
6 | +let watchId = null; // 구독 ID를 저장할 변수 | |
7 | +const locations = { | |
8 | + latitude: [], | |
9 | + longitude: [], | |
10 | + timestamp: [] | |
11 | +}; | |
12 | +let intervalId = null; // Interval ID를 저장할 변수 | |
13 | +// 위치 추적 옵션 설정 | |
14 | +const locationOptions = { | |
15 | + accuracy: { | |
16 | + ios: 'best', | |
17 | + android: 'mediumAccuracy', | |
18 | + }[Platform.OS], | |
19 | + distanceFilter: 0, // meters | |
20 | + interval: 1000, // 1 seconds | |
21 | + fastestInterval: 1000, // 5 seconds | |
22 | + forceRequestLocation: true | |
23 | + | |
24 | +}; | |
25 | + | |
26 | +// 위치 권한 요청 함수 | |
27 | +const requestLocationPermission = async () => { | |
28 | + if (Platform.OS === 'ios') { | |
29 | + const permission = await new Promise((resolve) => { | |
30 | + Geolocation.requestAuthorization((status) => { | |
31 | + resolve(status); | |
32 | + }); | |
33 | + }); | |
34 | + return permission; | |
35 | + } else { | |
36 | + const granted = await PermissionsAndroid.request( | |
37 | + PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION | |
38 | + ); | |
39 | + return granted === PermissionsAndroid.RESULTS.GRANTED ? 'granted' : 'denied'; | |
40 | + } | |
41 | +}; | |
42 | + | |
43 | +// 위치 서비스 시작 함수 | |
44 | +const startLocationService = async () => { | |
45 | + console.log('그다음 위치서비스가 시작되는거야'); | |
46 | + try { | |
47 | + const permission = await requestLocationPermission(); | |
48 | + if (permission !== 'granted') { | |
49 | + console.error('Location permission not granted'); | |
50 | + return; // 권한이 없으면 함수 종료 | |
51 | + } | |
52 | + | |
53 | + if (watchId === null) { | |
54 | + watchId = Geolocation.watchPosition( | |
55 | + (position) => { | |
56 | + const { latitude, longitude } = position.coords; | |
57 | + const time = new Date().toISOString(); | |
58 | + const timestamp = formatDate(time); | |
59 | + locations.latitude.push(latitude); | |
60 | + locations.longitude.push(longitude); | |
61 | + // locations.latitude.push(String(latitude)); | |
62 | + // locations.longitude.push(String(longitude)); | |
63 | + locations.timestamp.push(timestamp); | |
64 | + console.log('Location updated:', { latitude, longitude, timestamp }); | |
65 | + }, | |
66 | + (error) => { | |
67 | + console.error('Error watching position:', error); | |
68 | + }, | |
69 | + locationOptions | |
70 | + ); | |
71 | + | |
72 | + console.log('Location service started.'); | |
73 | + } | |
74 | + } catch (error) { | |
75 | + console.error('Error starting location service:', error); | |
76 | + } | |
77 | +}; | |
78 | + | |
79 | +// 위치 서비스 중지 함수 | |
80 | +const stopLocationService = async () => { | |
81 | + try { | |
82 | + if (watchId !== null) { | |
83 | + Geolocation.clearWatch(watchId); | |
84 | + watchId = null; | |
85 | + } | |
86 | + console.log('Location service stopped.'); | |
87 | + } catch (error) { | |
88 | + console.error('Error stopping location service:', error); | |
89 | + } | |
90 | +}; | |
91 | + | |
92 | +// 백그라운드에서 실행할 작업 | |
93 | +const backgroundTask = async (taskData) => { | |
94 | + console.log('백그라운드에서 이게 시작되는거고'); | |
95 | + await new Promise(async (resolve) => { | |
96 | + await startLocationService(); | |
97 | + | |
98 | + intervalId = setInterval(() => { | |
99 | + console.log('Background task running'); | |
100 | + }, 10000); // 10초마다 로그 출력ㄴ | |
101 | + | |
102 | + }); | |
103 | +}; | |
104 | + | |
105 | +// 백그라운드 작업 시작 함수 | |
106 | +const startBackgroundTask = async () => { | |
107 | + try { | |
108 | + await BackgroundActions.start(backgroundTask, options); | |
109 | + console.log('Background task started.'); | |
110 | + } catch (error) { | |
111 | + console.error('Error starting background task:', error); | |
112 | + } | |
113 | +}; | |
114 | + | |
115 | +// 백그라운드 작업 중지 함수 | |
116 | +const stopBackgroundTask = async () => { | |
117 | + try { | |
118 | + await BackgroundActions.stop(); | |
119 | + await stopLocationService(); // 위치 서비스 중지 | |
120 | + clearInterval(intervalId); // Interval 중지 | |
121 | + console.log('Background task stopped and location service stopped.'); | |
122 | + } catch (error) { | |
123 | + console.error('Error stopping background task:', error); | |
124 | + } | |
125 | +}; | |
126 | + | |
127 | +// 백그라운드 서비스 옵션 | |
128 | +const options = { | |
129 | + taskName: '측정 서비스', | |
130 | + taskTitle: '측정 중', | |
131 | + taskDesc: '측정 진행 중... ', | |
132 | + taskIcon: { | |
133 | + name: 'ic_launcher', | |
134 | + type: 'mipmap', | |
135 | + }, | |
136 | + color: '#ff00ff', | |
137 | + linkingURI: 'sendLocation', | |
138 | + parameters: { | |
139 | + delay: 1000, | |
140 | + }, | |
141 | +}; | |
142 | + | |
143 | +// 한국 시간으로 변환하는 함수 | |
144 | +const formatDate = (time) => { | |
145 | + const date = new Date(time); | |
146 | + const year = date.getFullYear(); | |
147 | + const month = `0${date.getMonth() + 1}`.slice(-2); | |
148 | + const day = `0${date.getDate()}`.slice(-2); | |
149 | + const hours = `0${date.getHours()}`.slice(-2); | |
150 | + const minutes = `0${date.getMinutes()}`.slice(-2); | |
151 | + const seconds = `0${date.getSeconds()}`.slice(-2); | |
152 | + const milliseconds = `00${date.getMilliseconds()}`.slice(-3); | |
153 | + | |
154 | + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; | |
155 | +}; | |
156 | + | |
157 | + | |
158 | + | |
159 | +export { startBackgroundTask, stopBackgroundTask, locations }; | |
160 | + |
--- src/styles/GlobalStyles.tsx
... | ... | @@ -1,55 +0,0 @@ |
1 | -export const container = { | |
2 | - flex: 1, | |
3 | - paddingHorizontal: 16, | |
4 | - paddingVertical: 50, | |
5 | - justifyContent: 'center', | |
6 | - backgroundColor: '#ffffff', | |
7 | -}; | |
8 | - | |
9 | -export const ItemFlex = { | |
10 | - flex: 1, | |
11 | -}; | |
12 | -export const ItemFlex2 = { | |
13 | - flex: 2, | |
14 | -}; | |
15 | - | |
16 | -export const direction = { | |
17 | - flexDirection: 'row', | |
18 | - justifyContent: 'space-between', | |
19 | -}; | |
20 | - | |
21 | -export const directionAlign = { | |
22 | - flexDirection: 'row', | |
23 | - justifyContent: 'space-between', | |
24 | - alignItems: 'center', | |
25 | - borderBottomWidth: 1, | |
26 | - borderBottomColor: '#eeeeee', | |
27 | - paddingVertical: 20, | |
28 | -}; | |
29 | - | |
30 | -export const pageTitleBox = { | |
31 | - paddingVertical: 20, | |
32 | - borderBottomWidth: 1, | |
33 | - borderBottomColor: '#cccccc', | |
34 | - backgroundColor: '#ffffff', | |
35 | -}; | |
36 | -export const pageTitle = { | |
37 | - fontSize: 15, | |
38 | - fontWeight: 'bold', | |
39 | - textAlign: 'center', | |
40 | - color: '#333333', | |
41 | -}; | |
42 | -export const marginBottom = { | |
43 | - marginBottom: 20, | |
44 | -}; | |
45 | -export const marginRight = { | |
46 | - marginRight: 10, | |
47 | -}; | |
48 | - | |
49 | -export const textColor = { | |
50 | - color: '#333333', | |
51 | -}; | |
52 | - | |
53 | -export const padding = { | |
54 | - paddingHorizontal: 16, | |
55 | -}; |
--- src/utils/RootStackParam.ts
... | ... | @@ -1,7 +0,0 @@ |
1 | -export type RootStackParam = { | |
2 | - SendLocation: undefined; | |
3 | - CameraScreen: undefined; | |
4 | - AgreementScreen: undefined; | |
5 | - LoginScreen: undefined; | |
6 | - AnalysisScreen: undefined; | |
7 | -}; |
--- src/utils/loginUtils.ts
... | ... | @@ -1,107 +0,0 @@ |
1 | -import {Alert} from 'react-native'; | |
2 | -import AsyncStorage from '@react-native-async-storage/async-storage'; | |
3 | -import {NativeStackNavigationProp} from '@react-navigation/native-stack'; | |
4 | -import {RootStackParam} from './RootStackParam'; | |
5 | - | |
6 | -// API 주소 | |
7 | -const url = 'http://takensoftai.iptime.org:15857/auth'; | |
8 | - | |
9 | -// 회원가입 | |
10 | -export const agreementAPI = async (agreementData: any) => { | |
11 | - try { | |
12 | - const response = await fetch(`${url}/register`, { | |
13 | - method: 'POST', | |
14 | - headers: { | |
15 | - 'Content-Type': 'application/json', | |
16 | - }, | |
17 | - body: JSON.stringify(agreementData), | |
18 | - }); | |
19 | - | |
20 | - if (!response.ok) { | |
21 | - throw new Error('Network response was not ok'); | |
22 | - } | |
23 | - | |
24 | - const data = await response.json(); | |
25 | - | |
26 | - return data; | |
27 | - } catch (error) { | |
28 | - console.error('Register Error:', error); | |
29 | - } | |
30 | -}; | |
31 | - | |
32 | -// 로그인 | |
33 | -export const useLoginAPI = ( | |
34 | - navigation: NativeStackNavigationProp<RootStackParam>, | |
35 | -) => { | |
36 | - const loginAPI = async (loginData: any) => { | |
37 | - try { | |
38 | - // console.log('data | ', loginData); | |
39 | - const response = await fetch(`${url}/login`, { | |
40 | - method: 'POST', | |
41 | - headers: { | |
42 | - 'Content-Type': 'application/json', | |
43 | - }, | |
44 | - body: JSON.stringify(loginData), | |
45 | - }); | |
46 | - | |
47 | - const data = await response.json(); | |
48 | - console.log(`data : ${data}`); | |
49 | - | |
50 | - if (data.result === 'success') { | |
51 | - console.log('login 성공'); | |
52 | - // console.log('userInfo', userInfoData); | |
53 | - // 로그인 성공 시 토큰 스토리지에 저장 | |
54 | - AsyncStorage.setItem( | |
55 | - 'token', | |
56 | - JSON.stringify({ | |
57 | - accessToken: data.token, | |
58 | - }), | |
59 | - ); | |
60 | - navigation.reset({routes: [{name: 'SendLocation'}]}); | |
61 | - } else { | |
62 | - // 로그인 실패 시 처리 | |
63 | - Alert.alert('로그인 정보를 다시 한 번 확인해주세요.'); | |
64 | - } | |
65 | - | |
66 | - return data; | |
67 | - } catch (error) { | |
68 | - console.error('로그인 오류:', error); | |
69 | - throw error; | |
70 | - } | |
71 | - }; | |
72 | - | |
73 | - return {loginAPI}; | |
74 | -}; | |
75 | - | |
76 | -// 로컬 저장소에서 가져오기 | |
77 | -export const getTokenFromStorage = async () => { | |
78 | - try { | |
79 | - const tokenString = await AsyncStorage.getItem('token'); | |
80 | - if (tokenString) { | |
81 | - const tokenObject = JSON.parse(tokenString); | |
82 | - const accessToken = tokenObject.accessToken; | |
83 | - // console.log(`accessToken: ${accessToken}`); | |
84 | - return accessToken; | |
85 | - } else { | |
86 | - console.log('토큰이 AsyncStorage에 없습니다.'); | |
87 | - return null; | |
88 | - } | |
89 | - } catch (error) { | |
90 | - console.error('액세스 토큰 가져오는 중 오류 발생:', error); | |
91 | - return null; | |
92 | - } | |
93 | -}; | |
94 | - | |
95 | -export const verifyTokens = async ( | |
96 | - navigation: NativeStackNavigationProp<RootStackParam>, | |
97 | -) => { | |
98 | - const token = await getTokenFromStorage(); | |
99 | - | |
100 | - // 토큰이 없는 경우 | |
101 | - if (!token) { | |
102 | - navigation.reset({routes: [{name: 'LoginScreen'}]}); | |
103 | - } else { | |
104 | - // 토큰이 있는 경우 | |
105 | - navigation.reset({routes: [{name: 'SendLocation'}]}); | |
106 | - } | |
107 | -}; |
--- src/utils/useCameraAPI.ts
... | ... | @@ -1,139 +0,0 @@ |
1 | -import React from 'react'; | |
2 | -import {Linking} from 'react-native'; | |
3 | -import {Camera} from 'react-native-vision-camera'; | |
4 | -// import RNFS from 'react-native-fs'; | |
5 | -import {getTokenFromStorage} from './loginUtils'; | |
6 | - | |
7 | -// API 주소 | |
8 | -const url = 'http://takensoftai.iptime.org:15857/action/image_summit'; | |
9 | - | |
10 | -type PermissionCallback = (result: boolean) => void; | |
11 | -type GpsPermissionCallback = (result: boolean) => void; | |
12 | - | |
13 | -interface UseCameraApi { | |
14 | - requestStoragePermission: (callback: PermissionCallback) => Promise<void>; | |
15 | - isPermission: boolean; | |
16 | - requestGPSPermission: (callback: GpsPermissionCallback) => Promise<void>; | |
17 | - isGpsPermission: boolean; | |
18 | - sendPhotoFile: (photoPath: string) => Promise<void>; | |
19 | -} | |
20 | - | |
21 | -// 권한 요청 | |
22 | -export default function useCameraApi(): UseCameraApi { | |
23 | - const [isPermission, setIsPermission] = React.useState<boolean>(false); | |
24 | - const [isGpsPermission, setIsGpsPermission] = React.useState<boolean>(false); | |
25 | - | |
26 | - // 카메라 권한 요청 | |
27 | - const requestStoragePermission = async (callback: PermissionCallback) => { | |
28 | - try { | |
29 | - const cameraPermission = await Camera.getCameraPermissionStatus(); | |
30 | - | |
31 | - let newCameraPermission = cameraPermission; | |
32 | - | |
33 | - if ( | |
34 | - cameraPermission === 'not-determined' || | |
35 | - cameraPermission === 'denied' | |
36 | - ) { | |
37 | - newCameraPermission = await Camera.requestCameraPermission(); | |
38 | - } | |
39 | - | |
40 | - const permissionGranted = newCameraPermission === 'granted'; | |
41 | - setIsPermission(permissionGranted); | |
42 | - callback(permissionGranted); | |
43 | - | |
44 | - if (!permissionGranted) { | |
45 | - await Linking.openSettings(); | |
46 | - } | |
47 | - } catch (err) { | |
48 | - console.log(`카메라 권한 요청 에러 : ${err}`); | |
49 | - } | |
50 | - }; | |
51 | - | |
52 | - // 위치 권한 요청 | |
53 | - const requestGPSPermission = async (callback: GpsPermissionCallback) => { | |
54 | - try { | |
55 | - const gpsPermissionStatus = await Camera.getLocationPermissionStatus(); | |
56 | - | |
57 | - let newGpsPermission = gpsPermissionStatus; | |
58 | - | |
59 | - if ( | |
60 | - newGpsPermission === 'not-determined' || | |
61 | - newGpsPermission === 'denied' || | |
62 | - newGpsPermission === 'restricted' | |
63 | - ) { | |
64 | - newGpsPermission = await Camera.requestLocationPermission(); | |
65 | - } | |
66 | - | |
67 | - const gpsPermissionGranted = newGpsPermission === 'granted'; | |
68 | - setIsGpsPermission(gpsPermissionGranted); | |
69 | - callback(gpsPermissionGranted); | |
70 | - | |
71 | - if (!gpsPermissionGranted) { | |
72 | - await Linking.openSettings(); | |
73 | - } | |
74 | - } catch (err) { | |
75 | - console.log(`위치 권한 요청 에러 : ${err}`); | |
76 | - } | |
77 | - }; | |
78 | - | |
79 | - // 사진 전송 | |
80 | - const sendPhotoFile = async (photoPath: string) => { | |
81 | - try { | |
82 | - const token = await getTokenFromStorage(); | |
83 | - if (!token) { | |
84 | - console.log('토큰 없음'); | |
85 | - return; | |
86 | - } | |
87 | - | |
88 | - if (!photoPath || typeof photoPath !== 'string') { | |
89 | - console.log('사진 경로 없음'); | |
90 | - return; | |
91 | - } | |
92 | - | |
93 | - const pathSplit = photoPath.split('/').pop(); | |
94 | - if (!pathSplit) { | |
95 | - console.log('사진 경로 split 불가'); | |
96 | - return; | |
97 | - } | |
98 | - const fileName = pathSplit.split('.')[0]; | |
99 | - | |
100 | - const formData = new FormData(); | |
101 | - formData.append('file', { | |
102 | - uri: `file://${photoPath}`, | |
103 | - name: `${fileName}.jpg`, | |
104 | - type: 'image/jpg', | |
105 | - }); | |
106 | - | |
107 | - try { | |
108 | - const response = await fetch(url, { | |
109 | - method: 'POST', | |
110 | - body: formData, | |
111 | - headers: { | |
112 | - 'Content-Type': 'multipart/form-data', | |
113 | - Authorization: `${token}`, | |
114 | - }, | |
115 | - }); | |
116 | - const responseJson = await response.json(); | |
117 | - if (response.status === 200) { | |
118 | - console.log( | |
119 | - 'Upload Successful', | |
120 | - `(${response.status})`, | |
121 | - responseJson, | |
122 | - ); | |
123 | - } else { | |
124 | - console.log('Upload failed', responseJson); | |
125 | - } | |
126 | - } catch (error) { | |
127 | - console.log('Upload error', error); | |
128 | - } | |
129 | - } catch (error) {} | |
130 | - }; | |
131 | - | |
132 | - return { | |
133 | - requestStoragePermission, | |
134 | - isPermission, | |
135 | - requestGPSPermission, | |
136 | - isGpsPermission, | |
137 | - sendPhotoFile, | |
138 | - }; | |
139 | -} |
--- src/utils/useGeolocationAPI.ts
... | ... | @@ -1,62 +0,0 @@ |
1 | -import React from 'react'; | |
2 | -import { getTokenFromStorage, verifyTokens } from './loginUtils'; | |
3 | -import AsyncStorage from '@react-native-async-storage/async-storage'; | |
4 | -import { NativeStackNavigationProp } from '@react-navigation/native-stack'; | |
5 | -import { RootStackParam } from '../utils/RootStackParam'; | |
6 | - | |
7 | -// API 주소 | |
8 | -const url = 'http://takensoftai.iptime.org:15857/action/gps_update'; | |
9 | - | |
10 | -export const sendLocationData = async ( | |
11 | - userId: string, | |
12 | - tripId: string, | |
13 | - locations: { longitudes: string[], latitudes: string[], timestamps: string[] }, | |
14 | - navigation: NativeStackNavigationProp<RootStackParam>, | |
15 | -) => { | |
16 | - try { | |
17 | - const token = await getTokenFromStorage(); | |
18 | - if (!token) { | |
19 | - await AsyncStorage.removeItem('token'); | |
20 | - verifyTokens(navigation); | |
21 | - return; | |
22 | - } | |
23 | - | |
24 | - const requestBody = { | |
25 | - user_id: userId, | |
26 | - trip_id: tripId, | |
27 | - trip_log: { | |
28 | - latitude: locations.latitudes, | |
29 | - longitude: locations.longitudes, | |
30 | - timestamp: locations.timestamps, | |
31 | - }, | |
32 | - }; | |
33 | - | |
34 | - console.log('Request body:', requestBody); | |
35 | - | |
36 | - try { | |
37 | - const response = await fetch(url, { | |
38 | - method: 'POST', | |
39 | - headers: { | |
40 | - 'Content-Type': 'application/json', | |
41 | - Authorization: `${token}`, | |
42 | - }, | |
43 | - body: JSON.stringify(requestBody), | |
44 | - }); | |
45 | - | |
46 | - const data = await response.json(); | |
47 | - console.log('Response:', data); | |
48 | - | |
49 | - if (!response.ok) { | |
50 | - console.log('Server response error:', data.msg || response.statusText); | |
51 | - } | |
52 | - if (data.result === 'fail') { | |
53 | - await AsyncStorage.removeItem('token'); | |
54 | - } | |
55 | - } catch (error) { | |
56 | - console.log('Server request error:', error); | |
57 | - } | |
58 | - } catch (error) { | |
59 | - console.log('Token retrieval error:', error); | |
60 | - } | |
61 | -}; | |
62 | - |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?