Showing
49 changed files
with
2468 additions
and
0 deletions
.gitignore
0 → 100644
1 | +.expo/* | ||
2 | +node_modules/**/* | ||
3 | +app.json | ||
4 | +npm-debug.* | ||
5 | +*.jks | ||
6 | +*.p8 | ||
7 | +*.p12 | ||
8 | +*.key | ||
9 | +*.mobileprovision | ||
10 | +*.orig.* | ||
11 | +web-build/ | ||
12 | + | ||
13 | +# macOS | ||
14 | +.DS_Store | ||
15 | + | ||
16 | +.idea | ||
17 | +.env | ||
18 | +env.js | ||
19 | +node_modules | ||
20 | +package-lock.json | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/.expo-shared/assets.json
0 → 100644
Kick_rn_server/.expo/README.md
0 → 100644
1 | +> Why do I have a folder named ".expo" in my project? | ||
2 | + | ||
3 | +The ".expo" folder is created when an Expo project is started using "expo start" command. | ||
4 | + | ||
5 | +> What does the "packager-info.json" file contain? | ||
6 | + | ||
7 | +The "packager-info.json" file contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator. | ||
8 | + | ||
9 | +> What does the "settings.json" file contain? | ||
10 | + | ||
11 | +The "settings.json" file contains the server configuration that is used to serve the application manifest. | ||
12 | + | ||
13 | +> Should I commit the ".expo" folder? | ||
14 | + | ||
15 | +No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine. | ||
16 | + | ||
17 | +Upon project creation, the ".expo" folder is already added to your ".gitignore" file. |
Kick_rn_server/.expo/packager-info.json
0 → 100644
Kick_rn_server/.expo/settings.json
0 → 100644
Kick_rn_server/App.js
0 → 100644
1 | +import React, {useState} from 'react'; | ||
2 | +import {View} from 'react-native'; | ||
3 | +import {AppLoading} from 'expo'; | ||
4 | +import * as SecureStore from 'expo-secure-store'; | ||
5 | +import AsyncStorage from '@react-native-community/async-storage'; | ||
6 | +import axios from "axios"; | ||
7 | +import env from './env'; | ||
8 | +import {StatusBar} from 'expo-status-bar'; | ||
9 | +import {Provider} from 'react-redux'; | ||
10 | +import store from "./store"; | ||
11 | +import {AuthProvider} from "./AuthContext"; | ||
12 | +import RootNavigation from "./navigations/RootNavigation"; | ||
13 | + | ||
14 | +export default function App() { | ||
15 | + const [isAppLoading, setIsAppLoading] = useState(true); | ||
16 | + const [user, setUser] = useState(null); | ||
17 | + | ||
18 | + const loadAssets = async () => { | ||
19 | + return new Promise(async (resolve, reject) => { | ||
20 | + try { | ||
21 | + let cookie; | ||
22 | + if (SecureStore.isAvailableAsync()) { | ||
23 | + cookie = await SecureStore.getItemAsync('cookie'); | ||
24 | + } else { | ||
25 | + cookie = await AsyncStorage.getItem('cookie'); | ||
26 | + } | ||
27 | + if (cookie) { | ||
28 | + axios.defaults.headers.Cookie = cookie; | ||
29 | + const res = await axios.get(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/user/loadMe`, { | ||
30 | + withCredentials: true | ||
31 | + }); | ||
32 | + const {user} = res.data; | ||
33 | + setUser(user); | ||
34 | + } | ||
35 | + return resolve(); | ||
36 | + } catch (e) { | ||
37 | + console.error(e); | ||
38 | + return reject(); | ||
39 | + } | ||
40 | + }) | ||
41 | + }; | ||
42 | + const onFinish = async () => { | ||
43 | + setIsAppLoading(false); | ||
44 | + }; | ||
45 | + const onError = (err) => { | ||
46 | + console.error(err); | ||
47 | + }; | ||
48 | + | ||
49 | + return ( | ||
50 | + <> | ||
51 | + {isAppLoading | ||
52 | + ? | ||
53 | + <AppLoading | ||
54 | + startAsync={loadAssets} | ||
55 | + onFinish={onFinish} | ||
56 | + onError={onError}/> | ||
57 | + : | ||
58 | + <View style={{ | ||
59 | + flex: 1, | ||
60 | + flexDirection: 'column', | ||
61 | + justifyContent: 'center' | ||
62 | + }}> | ||
63 | + <Provider store={store}> | ||
64 | + <AuthProvider user={user}> | ||
65 | + <RootNavigation/> | ||
66 | + </AuthProvider> | ||
67 | + </Provider> | ||
68 | + <StatusBar/> | ||
69 | + </View> | ||
70 | + } | ||
71 | + </> | ||
72 | + ); | ||
73 | +} | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/AuthContext.js
0 → 100644
1 | +import React, {createContext, useEffect} from 'react'; | ||
2 | +import {Text} from "react-native"; | ||
3 | +import {useDispatch, useSelector} from "react-redux"; | ||
4 | +import {loadMeReducer} from "./reducers/user"; | ||
5 | +import styled from "styled-components"; | ||
6 | + | ||
7 | +export const AuthContext = createContext({}); | ||
8 | +export const AuthProvider = (props) => { | ||
9 | + const {children, user} = props; | ||
10 | + const {info} = useSelector(state => state.info); | ||
11 | + | ||
12 | + const dispatch = useDispatch(); | ||
13 | + useEffect(() => { | ||
14 | + if (user) { | ||
15 | + dispatch({ | ||
16 | + type: loadMeReducer, | ||
17 | + data: {user} | ||
18 | + }); | ||
19 | + } | ||
20 | + }, [user]); | ||
21 | + | ||
22 | + return ( | ||
23 | + <AuthContext.Provider> | ||
24 | + {children} | ||
25 | + {info | ||
26 | + ? <Info><Text | ||
27 | + style={{ | ||
28 | + textAlign: 'center', | ||
29 | + fontSize: 14, | ||
30 | + color: '#bc324a' | ||
31 | + }}>{info}</Text></Info> | ||
32 | + : <></> | ||
33 | + } | ||
34 | + </AuthContext.Provider> | ||
35 | + ) | ||
36 | +}; | ||
37 | + | ||
38 | +const Info = styled.TouchableOpacity` | ||
39 | + position: absolute; | ||
40 | + right: 10px; | ||
41 | + bottom: 10px; | ||
42 | + width: 300px; | ||
43 | + height: 50px; | ||
44 | + border-radius: 5px; | ||
45 | + border: 1px solid #17174B; | ||
46 | + backgroundColor: #ffffff; | ||
47 | + justifyContent: center; | ||
48 | +`; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/assets/favicon.png
0 → 100644
32.1 KB
Kick_rn_server/assets/flower_road.png
0 → 100644
1.85 KB
Kick_rn_server/assets/gcooter.png
0 → 100644
3.03 KB
Kick_rn_server/assets/icon.png
0 → 100644
5.16 KB
Kick_rn_server/assets/splash.png
0 → 100644
32.1 KB
Kick_rn_server/assets/ssing_ssing.png
0 → 100644
9.51 KB
Kick_rn_server/babel.config.js
0 → 100644
1 | +import React, {useState, useCallback} from 'react'; | ||
2 | +import {Text} from 'react-native'; | ||
3 | +import {useDispatch} from "react-redux"; | ||
4 | +import {loginSaga} from "../../reducers/user"; | ||
5 | +import SquareButtonComponent from "../Base/SquareButtonComponent"; | ||
6 | +import {BUTTON_COLOR} from "../../constant"; | ||
7 | +import TextInputComponent from "../Base/TextInputComponent"; | ||
8 | + | ||
9 | +const LoginComponent = (props) => { | ||
10 | + // 데이터 | ||
11 | + const [email, setEmail] = useState(''); | ||
12 | + const [password, setPassword] = useState(''); | ||
13 | + | ||
14 | + // 함수 | ||
15 | + const dispatch = useDispatch(); | ||
16 | + const onChangeEmail = useCallback(text => { | ||
17 | + setEmail(text); | ||
18 | + }, []); | ||
19 | + const onChangePassword = useCallback((text) => { | ||
20 | + setPassword(text); | ||
21 | + }, []); | ||
22 | + const login = useCallback(() => { | ||
23 | + dispatch({ | ||
24 | + type: loginSaga, | ||
25 | + data: {email, password} | ||
26 | + }); | ||
27 | + }, [email, password]); | ||
28 | + | ||
29 | + // 렌더링 | ||
30 | + return ( | ||
31 | + <> | ||
32 | + <Text style={{ | ||
33 | + marginTop: 80, | ||
34 | + marginBottom: 20, | ||
35 | + fontSize: 24, | ||
36 | + fontWeight: 'bold', | ||
37 | + textAlign: 'center', | ||
38 | + color: '#373737' | ||
39 | + }}> | ||
40 | + 로그인 | ||
41 | + </Text> | ||
42 | + <TextInputComponent | ||
43 | + onChangeText={onChangeEmail} | ||
44 | + value={email} | ||
45 | + secureTextEntry={false} | ||
46 | + placeholder={'이메일을 쓰세요'} | ||
47 | + rules={[ | ||
48 | + v => !!v || '이메일 입력은 필수입니다', | ||
49 | + v => (v && (v.trim() !== '')) || '이메일 입력은 필수입니다', | ||
50 | + v => (v && (v.length <= 100)) || '이메일의 길이는 100자 이하입니다', | ||
51 | + v => (v && (/.+@.+/.test(v))) || '이메일의 형식이 올바르지 않습니다', | ||
52 | + ]} | ||
53 | + style1={{ | ||
54 | + marginLeft: 20, | ||
55 | + marginRight: 20, | ||
56 | + marginTop: 0, | ||
57 | + marginBottom: 0, | ||
58 | + padding: 15, | ||
59 | + }} | ||
60 | + style2={{ | ||
61 | + marginLeft: 35, | ||
62 | + marginRight: 35, | ||
63 | + }} | ||
64 | + /> | ||
65 | + <TextInputComponent | ||
66 | + onChangeText={onChangePassword} | ||
67 | + value={password} | ||
68 | + secureTextEntry={true} | ||
69 | + placeholder={'비밀번호를 쓰세요'} | ||
70 | + rules={[ | ||
71 | + v => !!v || '비밀번호 입력은 필수입니다', | ||
72 | + v => (v && (v.trim() !== '')) || '비밀번호 입력은 필수입니다', | ||
73 | + ]} | ||
74 | + style1={{ | ||
75 | + marginLeft: 20, | ||
76 | + marginRight: 20, | ||
77 | + marginTop: 0, | ||
78 | + marginBottom: 0, | ||
79 | + padding: 15, | ||
80 | + }} | ||
81 | + style2={{ | ||
82 | + marginLeft: 35, | ||
83 | + marginRight: 35, | ||
84 | + }} | ||
85 | + /> | ||
86 | + <SquareButtonComponent | ||
87 | + onPress={login} | ||
88 | + text={'로그인'} | ||
89 | + style1={{ | ||
90 | + marginLeft: 20, | ||
91 | + marginRight: 20, | ||
92 | + marginTop: 10, | ||
93 | + marginBottom: 10, | ||
94 | + padding: 15, | ||
95 | + borderRadius: 3, | ||
96 | + backgroundColor: BUTTON_COLOR | ||
97 | + }} | ||
98 | + style2={{ | ||
99 | + fontSize: 16, | ||
100 | + fontWeight: 'bold', | ||
101 | + textAlign: 'center', | ||
102 | + color: '#ffffff' | ||
103 | + }} | ||
104 | + /> | ||
105 | + </> | ||
106 | + ) | ||
107 | +}; | ||
108 | + | ||
109 | +export default LoginComponent; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +import React, {useState, useCallback} from 'react'; | ||
2 | +import {Text} from 'react-native'; | ||
3 | +import {useDispatch} from 'react-redux'; | ||
4 | +import TextInputComponent from "../Base/TextInputComponent"; | ||
5 | +import SquareButtonComponent from "../Base/SquareButtonComponent"; | ||
6 | +import {BUTTON_COLOR} from "../../constant"; | ||
7 | +import {signUpSaga} from '../../reducers/user'; | ||
8 | + | ||
9 | +const SignUpComponent = (props) => { | ||
10 | + // 데이터 | ||
11 | + const [email, setEmail] = useState(''); | ||
12 | + const [nickName, setNickName] = useState(''); | ||
13 | + const [password, setPassword] = useState(''); | ||
14 | + const [passwordCheck, setPasswordCheck] = useState(''); | ||
15 | + const [passwordCheckError, setPasswordCheckError] = useState(false); | ||
16 | + | ||
17 | + // 함수 | ||
18 | + const dispatch = useDispatch(); | ||
19 | + const onChangeEmail = useCallback((text) => { | ||
20 | + setEmail(text); | ||
21 | + }, []); | ||
22 | + const onChangeNickName = useCallback((text) => { | ||
23 | + setNickName(text); | ||
24 | + }, []); | ||
25 | + const onChangePassword = useCallback((text) => { | ||
26 | + setPassword(text); | ||
27 | + }, []); | ||
28 | + const onChangePasswordCheck = useCallback((text) => { | ||
29 | + setPasswordCheck(text); | ||
30 | + setPasswordCheckError(text !== password); | ||
31 | + }, [password]); | ||
32 | + const onSubmit = useCallback(() => { | ||
33 | + if (!(email && nickName && password && passwordCheck && !passwordCheckError)) { | ||
34 | + return; | ||
35 | + } | ||
36 | + | ||
37 | + dispatch({ | ||
38 | + type: signUpSaga, | ||
39 | + data: {email, nickName, password} | ||
40 | + }); | ||
41 | + }, [nickName, password, passwordCheck]); | ||
42 | + | ||
43 | + return ( | ||
44 | + <> | ||
45 | + <Text style={{ | ||
46 | + marginTop: 80, | ||
47 | + marginBottom: 20, | ||
48 | + fontSize: 24, | ||
49 | + fontWeight: 'bold', | ||
50 | + textAlign: 'center', | ||
51 | + color: '#373737' | ||
52 | + }}> | ||
53 | + 회원가입 | ||
54 | + </Text> | ||
55 | + <TextInputComponent | ||
56 | + onChangeText={onChangeEmail} | ||
57 | + value={email} | ||
58 | + secureTextEntry={false} | ||
59 | + placeholder={'이메일을 입력하세요'} | ||
60 | + rules={[ | ||
61 | + v => !!v || '이메일 입력은 필수입니다', | ||
62 | + v => (v && (v.trim() !== '')) || '이메일 입력은 필수입니다', | ||
63 | + v => (v && (v.length <= 100)) || '이메일의 길이는 100자 이하입니다', | ||
64 | + v => (v && (/.+@.+/.test(v))) || '이메일의 형식이 올바르지 않습니다', | ||
65 | + ]} | ||
66 | + style1={{ | ||
67 | + marginLeft: 20, | ||
68 | + marginRight: 20, | ||
69 | + marginTop: 0, | ||
70 | + marginBottom: 0, | ||
71 | + padding: 15, | ||
72 | + }} | ||
73 | + style2={{ | ||
74 | + marginLeft: 35, | ||
75 | + marginRight: 35, | ||
76 | + }} | ||
77 | + /> | ||
78 | + <TextInputComponent | ||
79 | + onChangeText={onChangeNickName} | ||
80 | + value={nickName} | ||
81 | + secureTextEntry={false} | ||
82 | + placeholder={'닉네임을 입력하세요'} | ||
83 | + rules={[ | ||
84 | + v => !!v || '닉네임 입력은 필수입니다', | ||
85 | + v => (v && (v.trim() !== '')) || '닉네임 입력은 필수입니다', | ||
86 | + v => (v && (v.length <= 10)) || '닉네임의 길이는 10자 이하입니다' | ||
87 | + ]} | ||
88 | + style1={{ | ||
89 | + marginLeft: 20, | ||
90 | + marginRight: 20, | ||
91 | + marginTop: 0, | ||
92 | + marginBottom: 0, | ||
93 | + padding: 15, | ||
94 | + }} | ||
95 | + style2={{ | ||
96 | + marginLeft: 35, | ||
97 | + marginRight: 35, | ||
98 | + }} | ||
99 | + /> | ||
100 | + <TextInputComponent | ||
101 | + onChangeText={onChangePassword} | ||
102 | + value={password} | ||
103 | + secureTextEntry={true} | ||
104 | + placeholder={'비밀번호를 입력하세요'} | ||
105 | + rules={[ | ||
106 | + v => !!v || '비밀번호 입력은 필수입니다', | ||
107 | + v => (v && (v.trim() !== '')) || '비밀번호 입력은 필수입니다', | ||
108 | + ]} | ||
109 | + style1={{ | ||
110 | + marginLeft: 20, | ||
111 | + marginRight: 20, | ||
112 | + marginTop: 0, | ||
113 | + marginBottom: 0, | ||
114 | + padding: 15, | ||
115 | + }} | ||
116 | + style2={{ | ||
117 | + marginLeft: 35, | ||
118 | + marginRight: 35, | ||
119 | + }} | ||
120 | + /> | ||
121 | + <TextInputComponent | ||
122 | + onChangeText={onChangePasswordCheck} | ||
123 | + value={passwordCheck} | ||
124 | + secureTextEntry={true} | ||
125 | + placeholder={'비밀번호를 다시 입력하세요'} | ||
126 | + inputError={passwordCheckError ? '비밀번호가 일치하지 않습니다' : null} | ||
127 | + style1={{ | ||
128 | + marginLeft: 20, | ||
129 | + marginRight: 20, | ||
130 | + marginTop: 0, | ||
131 | + marginBottom: 0, | ||
132 | + padding: 15, | ||
133 | + }} | ||
134 | + style2={{ | ||
135 | + marginLeft: 35, | ||
136 | + marginRight: 35, | ||
137 | + }} | ||
138 | + /> | ||
139 | + <SquareButtonComponent | ||
140 | + onPress={onSubmit} | ||
141 | + text={'회원가입'} | ||
142 | + style1={{ | ||
143 | + marginLeft: 20, | ||
144 | + marginRight: 20, | ||
145 | + marginTop: 10, | ||
146 | + marginBottom: 10, | ||
147 | + padding: 15, | ||
148 | + borderRadius: 3, | ||
149 | + backgroundColor: BUTTON_COLOR | ||
150 | + }} | ||
151 | + style2={{ | ||
152 | + fontSize: 16, | ||
153 | + fontWeight: 'bold', | ||
154 | + textAlign: 'center', | ||
155 | + color: '#ffffff' | ||
156 | + }} | ||
157 | + /> | ||
158 | + </> | ||
159 | + ) | ||
160 | +}; | ||
161 | + | ||
162 | +export default SignUpComponent; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +import React from 'react'; | ||
2 | +import {TouchableOpacity, View, Text} from 'react-native'; | ||
3 | + | ||
4 | +const SquareButtonComponent = (props) => { | ||
5 | + const {onPress, text, style1, style2} = props; | ||
6 | + | ||
7 | + return ( | ||
8 | + <TouchableOpacity onPress={onPress}> | ||
9 | + <View style={style1}> | ||
10 | + <Text style={style2}> | ||
11 | + {text} | ||
12 | + </Text> | ||
13 | + </View> | ||
14 | + </TouchableOpacity> | ||
15 | + ) | ||
16 | +}; | ||
17 | + | ||
18 | +export default SquareButtonComponent; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +import React, {useState, useEffect} from 'react'; | ||
2 | +import {View, TextInput, Text, StyleSheet} from 'react-native'; | ||
3 | + | ||
4 | +const TextInputComponent = (props) => { | ||
5 | + // 데이터 | ||
6 | + const {onChangeText, value, secureTextEntry, placeholder, rules, inputError, style1, style2} = props; | ||
7 | + const [error, setError] = useState(null); | ||
8 | + | ||
9 | + // 렌더링 | ||
10 | + useEffect(() => { | ||
11 | + if (rules) { | ||
12 | + let i = 0; | ||
13 | + while (true) { | ||
14 | + if (rules[i](value) !== true) { | ||
15 | + setError(rules[i](value)); | ||
16 | + break; | ||
17 | + } else { | ||
18 | + if (i === rules.length - 1) { | ||
19 | + setError(null); | ||
20 | + break; | ||
21 | + } else { | ||
22 | + i++; | ||
23 | + } | ||
24 | + } | ||
25 | + } | ||
26 | + } | ||
27 | + }, [value]); | ||
28 | + return ( | ||
29 | + <> | ||
30 | + <View style={{...styles.input, ...style1}}> | ||
31 | + <TextInput style={{...styles.inputText}} | ||
32 | + onChangeText={onChangeText} | ||
33 | + value={value} | ||
34 | + secureTextEntry={secureTextEntry} | ||
35 | + placeholder={placeholder}/> | ||
36 | + </View> | ||
37 | + <Text style={{...styles.inputError, ...style2}}>{inputError ? inputError : error}</Text> | ||
38 | + </> | ||
39 | + ) | ||
40 | +}; | ||
41 | + | ||
42 | +const styles = StyleSheet.create({ | ||
43 | + input: { | ||
44 | + borderRadius: 3, | ||
45 | + backgroundColor: '#ebebeb', | ||
46 | + }, | ||
47 | + inputText: { | ||
48 | + fontSize: 16, | ||
49 | + color: '#373737', | ||
50 | + }, | ||
51 | + inputError: { | ||
52 | + marginBottom: 5, | ||
53 | + fontSize: 14, | ||
54 | + color: '#9b0500', | ||
55 | + } | ||
56 | +}); | ||
57 | + | ||
58 | +export default TextInputComponent; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +import React from "react"; | ||
2 | +import {View, Text, Image} from "react-native"; | ||
3 | +import {Callout, Marker} from "react-native-maps"; | ||
4 | +import {useNavigation} from '@react-navigation/native'; | ||
5 | +import SquareButtonComponent from "./Base/SquareButtonComponent"; | ||
6 | +import {BUTTON_COLOR} from "../constant"; | ||
7 | + | ||
8 | +const KickMarkerComponent = (props) => { | ||
9 | + const navigation = useNavigation(); | ||
10 | + | ||
11 | + // 데이터 | ||
12 | + const {markerImage, serviceNumber, nameOfService, infoOfService, kickLocationMarker, index, minIndex, servicePrice, getPath} = props; | ||
13 | + | ||
14 | + // 함수 | ||
15 | + const goToMyPage = () => { | ||
16 | + navigation.navigate('이킥저킥'); | ||
17 | + }; | ||
18 | + | ||
19 | + // 렌더링 | ||
20 | + return ( | ||
21 | + <Marker coordinate={{ | ||
22 | + latitude: parseFloat(kickLocationMarker.coordinate.latitude), | ||
23 | + longitude: parseFloat(kickLocationMarker.coordinate.longitude), | ||
24 | + }} | ||
25 | + onPress={() => getPath(serviceNumber, { | ||
26 | + title: kickLocationMarker.title, | ||
27 | + coordinate: { | ||
28 | + latitude: parseFloat(kickLocationMarker.coordinate.latitude), | ||
29 | + longitude: parseFloat(kickLocationMarker.coordinate.longitude), | ||
30 | + } | ||
31 | + })} | ||
32 | + opacity={index === minIndex ? 1.0 : 0.6} | ||
33 | + > | ||
34 | + {serviceNumber === 0 | ||
35 | + ? | ||
36 | + <Image source={require('../assets/gcooter.png')} style={{height: 35, width: 35, borderRadius: 50}}/> | ||
37 | + : | ||
38 | + serviceNumber === 1 | ||
39 | + ? | ||
40 | + <Image source={require('../assets/flower_road.png')} | ||
41 | + style={{height: 35, width: 35, borderRadius: 50}}/> | ||
42 | + : | ||
43 | + <Image source={require('../assets/ssing_ssing.png')} | ||
44 | + style={{height: 35, width: 35, borderRadius: 50}}/> | ||
45 | + } | ||
46 | + <Callout tooltip={true} | ||
47 | + style={{width: 200}} onPress={goToMyPage}> | ||
48 | + <View style={{backgroundColor: '#ffffff', paddingLeft: 10, paddingRight: 10, paddingTop: 10, paddingBottom: 5, borderRadius: 5}}> | ||
49 | + <View style={{ | ||
50 | + backgroundColor: '#ebebeb', | ||
51 | + padding: 5, | ||
52 | + marginBottom: 10, | ||
53 | + borderRadius: 5 | ||
54 | + }}> | ||
55 | + <Text style={{ | ||
56 | + fontSize: 16, | ||
57 | + color: '#373737', | ||
58 | + fontWeight: 'bold', | ||
59 | + textAlign: 'center' | ||
60 | + }}> | ||
61 | + {index === minIndex ? '가까운' : ''} {nameOfService} | ||
62 | + </Text> | ||
63 | + {index === minIndex | ||
64 | + ? | ||
65 | + <Text style={{ | ||
66 | + fontSize: 16, | ||
67 | + color: '#373737', | ||
68 | + fontWeight: 'bold', | ||
69 | + textAlign: 'center' | ||
70 | + }}> | ||
71 | + 예상비용 {servicePrice}원 | ||
72 | + </Text> | ||
73 | + : | ||
74 | + <></>} | ||
75 | + </View> | ||
76 | + <Text style={{fontSize: 14, color: '#373737'}}> | ||
77 | + 남은 배터리 {kickLocationMarker.title}% | ||
78 | + </Text> | ||
79 | + <Text style={{fontSize: 14, color: '#373737'}}> | ||
80 | + {infoOfService} | ||
81 | + </Text> | ||
82 | + <SquareButtonComponent | ||
83 | + text={'대여하기'} | ||
84 | + style1={{ | ||
85 | + marginLeft: 0, | ||
86 | + marginRight: 0, | ||
87 | + marginTop: 5, | ||
88 | + marginBottom: 5, | ||
89 | + padding: 5, | ||
90 | + borderRadius: 3, | ||
91 | + backgroundColor: BUTTON_COLOR | ||
92 | + }} | ||
93 | + style2={{ | ||
94 | + fontSize: 16, | ||
95 | + fontWeight: 'bold', | ||
96 | + textAlign: 'center', | ||
97 | + color: '#ffffff' | ||
98 | + }} | ||
99 | + /> | ||
100 | + </View> | ||
101 | + </Callout> | ||
102 | + </Marker> | ||
103 | + ); | ||
104 | +}; | ||
105 | + | ||
106 | +export default KickMarkerComponent; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +import React, {useState} from "react"; | ||
2 | +import {View, Text, Alert} from "react-native"; | ||
3 | +import {CheckBox} from "native-base"; | ||
4 | +import SquareButtonComponent from "../Base/SquareButtonComponent"; | ||
5 | +import {BUTTON_COLOR} from "../../constant"; | ||
6 | +import {resetLaneReducer} from "../../reducers/location"; | ||
7 | +import {useDispatch} from "react-redux"; | ||
8 | + | ||
9 | +const Modal1Component = (props) => { | ||
10 | + // 데이터 | ||
11 | + const {setModalNumber, setShowButtons, setModalVisible, serviceStates, setServiceStates, selectKickServices} = props; | ||
12 | + const [showDetail, setShowDetail] = useState(true); | ||
13 | + | ||
14 | + // 함수 | ||
15 | + const dispatch = useDispatch(); | ||
16 | + const onPressCheckBox = (indexOfService) => { | ||
17 | + | ||
18 | + if (indexOfService === 1) { | ||
19 | + setServiceStates([...serviceStates.slice(0, indexOfService), !serviceStates[indexOfService], ...serviceStates.slice(indexOfService + 1)]); | ||
20 | + } else { | ||
21 | + setServiceStates([...serviceStates.slice(0, indexOfService), serviceStates[indexOfService], ...serviceStates.slice(indexOfService + 1)]); | ||
22 | + } | ||
23 | + }; | ||
24 | + const onShowDetail = () => { | ||
25 | + setShowDetail(!showDetail); | ||
26 | + }; | ||
27 | + const onDecideServices = () => { | ||
28 | + if (serviceStates.findIndex(v => v) === -1) { | ||
29 | + return Alert.alert('최소 하나 이상의 서비스를 선택하세요'); | ||
30 | + } | ||
31 | + selectKickServices(serviceStates); | ||
32 | + setModalNumber(0); | ||
33 | + setShowButtons(true); | ||
34 | + setModalVisible(false); | ||
35 | + dispatch({type: resetLaneReducer}); | ||
36 | + }; | ||
37 | + | ||
38 | + // 렌더링 | ||
39 | + return ( | ||
40 | + <View style={{'flex': 1, width: 270}}> | ||
41 | + <View style={{'flex': 1, 'flexDirection': 'column', marginBottom: 15}}> | ||
42 | + <Text style={{fontSize: 24, fontWeight: 'bold', textAlign: 'center', color: '#373737'}}> | ||
43 | + 서비스 선택 | ||
44 | + </Text> | ||
45 | + </View> | ||
46 | + <View style={{'flex': 3, 'flexDirection': 'column'}}> | ||
47 | + <View style={{'flex': 1, 'flexDirection': 'row'}}> | ||
48 | + <Text style={{fontSize: 16, fontWeight: 'bold', color: '#373737'}}>지쿠터(현재 미지원)</Text> | ||
49 | + <CheckBox checked={serviceStates[0]} color="#4844ff" | ||
50 | + onPress={() => onPressCheckBox(0)}/> | ||
51 | + </View> | ||
52 | + <View style={{'flex': 3, 'flexDirection': 'row'}}> | ||
53 | + {!showDetail | ||
54 | + ? <Text style={{fontSize: 16, color: '#373737'}}>#저렴한 가격 #최다 보유</Text> | ||
55 | + : <Text style={{ | ||
56 | + fontSize: 15, color: '#373737' | ||
57 | + }}>{`기본 1분 300 / 1분당 150\n최대 20km/h\n00-05시 새벽 할증 요금제`}</Text>} | ||
58 | + </View> | ||
59 | + </View> | ||
60 | + <View style={{'flex': 3, 'flexDirection': 'column'}}> | ||
61 | + <View style={{'flex': 1, 'flexDirection': 'row'}}> | ||
62 | + <Text style={{fontSize: 16, fontWeight: 'bold', color: '#373737'}}>플라워 로드</Text> | ||
63 | + <CheckBox checked={serviceStates[1]} color="#4844ff" | ||
64 | + onPress={() => onPressCheckBox(1)}/> | ||
65 | + </View> | ||
66 | + <View style={{'flex': 3, 'flexDirection': 'row'}}> | ||
67 | + {!showDetail | ||
68 | + ? <Text style={{fontSize: 16, color: '#373737'}}>#빠른 속도</Text> | ||
69 | + : <Text style={{ | ||
70 | + fontSize: 15, color: '#373737' | ||
71 | + }}>{`기본 5분 1000/1분당 100\n최대 25km/h\n21시 이후 최대속도 18km 제한`}</Text>} | ||
72 | + </View> | ||
73 | + </View> | ||
74 | + <View style={{'flex': 3, 'flexDirection': 'column'}}> | ||
75 | + <View style={{'flex': 1, 'flexDirection': 'row'}}> | ||
76 | + <Text style={{fontSize: 16, fontWeight: 'bold', color: '#373737'}}>씽씽(현재 미지원)</Text> | ||
77 | + <CheckBox checked={serviceStates[2]} color="#4844ff" | ||
78 | + onPress={() => onPressCheckBox(2)}/> | ||
79 | + </View> | ||
80 | + <View style={{'flex': 3, 'flexDirection': 'row'}}> | ||
81 | + {!showDetail | ||
82 | + ? <Text style={{fontSize: 16, color: '#373737'}}>#최신 기기</Text> | ||
83 | + : <Text style={{ | ||
84 | + fontSize: 15, color: '#373737' | ||
85 | + }}>{`평일/심야/주말 요금 상이\n최대 25km/h\n9월 말까지 50% 할인 행사중`}</Text>} | ||
86 | + </View> | ||
87 | + </View> | ||
88 | + <SquareButtonComponent | ||
89 | + onPress={onShowDetail} | ||
90 | + text={!showDetail ? '상세 정보 보기' : '간단 정보 보기'} | ||
91 | + style1={{ | ||
92 | + marginLeft: 0, | ||
93 | + marginRight: 0, | ||
94 | + marginTop: 10, | ||
95 | + marginBottom: 0, | ||
96 | + padding: 10, | ||
97 | + borderRadius: 3, | ||
98 | + backgroundColor: BUTTON_COLOR | ||
99 | + }} | ||
100 | + style2={{ | ||
101 | + fontSize: 16, | ||
102 | + fontWeight: 'bold', | ||
103 | + textAlign: 'center', | ||
104 | + color: '#ffffff' | ||
105 | + }} | ||
106 | + /> | ||
107 | + <SquareButtonComponent | ||
108 | + onPress={onDecideServices} | ||
109 | + text={'서비스 결정하기'} | ||
110 | + style1={{ | ||
111 | + marginLeft: 0, | ||
112 | + marginRight: 0, | ||
113 | + marginTop: 10, | ||
114 | + marginBottom: 0, | ||
115 | + padding: 10, | ||
116 | + borderRadius: 3, | ||
117 | + backgroundColor: BUTTON_COLOR | ||
118 | + }} | ||
119 | + style2={{ | ||
120 | + fontSize: 16, | ||
121 | + fontWeight: 'bold', | ||
122 | + textAlign: 'center', | ||
123 | + color: '#ffffff' | ||
124 | + }} | ||
125 | + /> | ||
126 | + </View> | ||
127 | + ); | ||
128 | +}; | ||
129 | + | ||
130 | +export default Modal1Component; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +import React from 'react'; | ||
2 | +import {View, Text} from 'react-native'; | ||
3 | +import {useDispatch} from "react-redux"; | ||
4 | +import {setStartLocationSaga, setEndLocationSaga} from "../../reducers/location"; | ||
5 | +import TextInputComponent from "../Base/TextInputComponent"; | ||
6 | +import SquareButtonComponent from "../Base/SquareButtonComponent"; | ||
7 | +import {BUTTON_COLOR} from "../../constant"; | ||
8 | + | ||
9 | +const Modal2Component = (props) => { | ||
10 | + // 데이터 | ||
11 | + const {closeModal, setShowButtons, setModalVisible, serviceStates, setModalNumber, startTextLocation, endTextLocation, setStartTextLocation, setEndTextLocation} = props; | ||
12 | + | ||
13 | + // 함수 | ||
14 | + const dispatch = useDispatch(); | ||
15 | + const onChangeStartLocation = (text) => { | ||
16 | + setStartTextLocation(text); | ||
17 | + }; | ||
18 | + const onChangeFinishLocation = (text) => { | ||
19 | + setEndTextLocation(text); | ||
20 | + }; | ||
21 | + const setLocation = async () => { | ||
22 | + try { | ||
23 | + if (startTextLocation && endTextLocation) { | ||
24 | + await dispatch({ | ||
25 | + type: setStartLocationSaga, | ||
26 | + data: { | ||
27 | + startTextLocation, | ||
28 | + } | ||
29 | + }); | ||
30 | + await dispatch({ | ||
31 | + type: setEndLocationSaga, | ||
32 | + data: { | ||
33 | + endTextLocation | ||
34 | + } | ||
35 | + }); | ||
36 | + if (serviceStates.findIndex(serviceState => serviceState) === -1) { | ||
37 | + setModalNumber(2); | ||
38 | + setShowButtons(false); | ||
39 | + setModalVisible(true); | ||
40 | + } else { | ||
41 | + setModalNumber(0); | ||
42 | + setShowButtons(true); | ||
43 | + setModalVisible(false); | ||
44 | + } | ||
45 | + } | ||
46 | + } catch (e) { | ||
47 | + console.error(e); | ||
48 | + } | ||
49 | + }; | ||
50 | + const close = () => { | ||
51 | + closeModal(); | ||
52 | + }; | ||
53 | + | ||
54 | + // 렌더링 | ||
55 | + return ( | ||
56 | + <View style={{'flex': 1, width: 270}}> | ||
57 | + <View style={{'flex': 1, 'flexDirection': 'column', marginBottom: 15}}> | ||
58 | + <Text style={{fontSize: 24, fontWeight: 'bold', textAlign: 'center', color: '#373737'}}> | ||
59 | + 출발 및 도착 설정 | ||
60 | + </Text> | ||
61 | + </View> | ||
62 | + <View style={{'flex': 9, 'flexDirection': 'column'}}> | ||
63 | + <TextInputComponent | ||
64 | + onChangeText={onChangeStartLocation} | ||
65 | + value={startTextLocation} | ||
66 | + secureTextEntry={false} | ||
67 | + placeholder={'출발 장소를 입력하세요'} | ||
68 | + style1={{ | ||
69 | + marginLeft: 0, | ||
70 | + marginRight: 0, | ||
71 | + marginTop: 0, | ||
72 | + marginBottom: 0, | ||
73 | + padding: 15, | ||
74 | + }} | ||
75 | + style2={{ | ||
76 | + marginLeft: 15, | ||
77 | + marginRight: 15, | ||
78 | + }} | ||
79 | + /> | ||
80 | + <TextInputComponent | ||
81 | + onChangeText={onChangeFinishLocation} | ||
82 | + value={endTextLocation} | ||
83 | + secureTextEntry={false} | ||
84 | + placeholder={'도착 장소를 입력하세요'} | ||
85 | + style1={{ | ||
86 | + marginLeft: 0, | ||
87 | + marginRight: 0, | ||
88 | + marginTop: 0, | ||
89 | + marginBottom: 0, | ||
90 | + padding: 15, | ||
91 | + }} | ||
92 | + style2={{ | ||
93 | + marginLeft: 15, | ||
94 | + marginRight: 15, | ||
95 | + }} | ||
96 | + /> | ||
97 | + <SquareButtonComponent | ||
98 | + onPress={setLocation} | ||
99 | + text={'출발지와 도착지 결정'} | ||
100 | + style1={{ | ||
101 | + marginLeft: 0, | ||
102 | + marginRight: 0, | ||
103 | + marginTop: 10, | ||
104 | + marginBottom: 0, | ||
105 | + padding: 10, | ||
106 | + borderRadius: 3, | ||
107 | + backgroundColor: BUTTON_COLOR | ||
108 | + }} | ||
109 | + style2={{ | ||
110 | + fontSize: 16, | ||
111 | + fontWeight: 'bold', | ||
112 | + textAlign: 'center', | ||
113 | + color: '#ffffff' | ||
114 | + }} | ||
115 | + /> | ||
116 | + <SquareButtonComponent | ||
117 | + onPress={close} | ||
118 | + text={'닫기'} | ||
119 | + style1={{ | ||
120 | + marginLeft: 0, | ||
121 | + marginRight: 0, | ||
122 | + marginTop: 10, | ||
123 | + marginBottom: 0, | ||
124 | + padding: 10, | ||
125 | + borderRadius: 3, | ||
126 | + backgroundColor: BUTTON_COLOR | ||
127 | + }} | ||
128 | + style2={{ | ||
129 | + fontSize: 16, | ||
130 | + fontWeight: 'bold', | ||
131 | + textAlign: 'center', | ||
132 | + color: '#ffffff' | ||
133 | + }} | ||
134 | + /> | ||
135 | + </View> | ||
136 | + </View> | ||
137 | + ) | ||
138 | +}; | ||
139 | + | ||
140 | +export default Modal2Component; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +import React from 'react'; | ||
2 | +import {View, Text} from 'react-native'; | ||
3 | +import {useDispatch} from "react-redux"; | ||
4 | +import {setStartLocationSaga} from "../../reducers/location"; | ||
5 | +import TextInputComponent from "../Base/TextInputComponent"; | ||
6 | +import SquareButtonComponent from "../Base/SquareButtonComponent"; | ||
7 | +import {BUTTON_COLOR} from "../../constant"; | ||
8 | + | ||
9 | +const Modal3Component = (props) => { | ||
10 | + // 데이터 | ||
11 | + const {closeModal, setModalNumber, setShowButtons, setModalVisible, startTextLocation, setStartTextLocation} = props; | ||
12 | + | ||
13 | + // 함수 | ||
14 | + const dispatch = useDispatch(); | ||
15 | + const onChangeStartLocation = (text) => { | ||
16 | + setStartTextLocation(text); | ||
17 | + }; | ||
18 | + const setLocation = async () => { | ||
19 | + try { | ||
20 | + if (startTextLocation) { | ||
21 | + await dispatch({ | ||
22 | + type: setStartLocationSaga, | ||
23 | + data: { | ||
24 | + startTextLocation, | ||
25 | + } | ||
26 | + }); | ||
27 | + closeModal(); | ||
28 | + } | ||
29 | + } catch (e) { | ||
30 | + console.error(e); | ||
31 | + } | ||
32 | + }; | ||
33 | + const close = () => { | ||
34 | + closeModal(); | ||
35 | + }; | ||
36 | + | ||
37 | + // 렌더링 | ||
38 | + return ( | ||
39 | + <View style={{'flex': 1, width: 270}}> | ||
40 | + <View style={{'flex': 1, 'flexDirection': 'column', marginBottom: 50}}> | ||
41 | + <Text style={{fontSize: 24, fontWeight: 'bold', textAlign: 'center', color: '#373737'}}> | ||
42 | + 장소 검색{'\n'}(대학교를 검색해보세요) | ||
43 | + </Text> | ||
44 | + </View> | ||
45 | + <View style={{'flex': 9, 'flexDirection': 'column'}}> | ||
46 | + <TextInputComponent | ||
47 | + onChangeText={onChangeStartLocation} | ||
48 | + value={startTextLocation} | ||
49 | + secureTextEntry={false} | ||
50 | + placeholder={'검색할 장소를 입력하세요'} | ||
51 | + style1={{ | ||
52 | + marginLeft: 0, | ||
53 | + marginRight: 0, | ||
54 | + marginTop: 0, | ||
55 | + marginBottom: 0, | ||
56 | + padding: 15, | ||
57 | + }} | ||
58 | + style2={{ | ||
59 | + marginLeft: 15, | ||
60 | + marginRight: 15, | ||
61 | + }} | ||
62 | + /> | ||
63 | + <SquareButtonComponent | ||
64 | + onPress={setLocation} | ||
65 | + text={'결정'} | ||
66 | + style1={{ | ||
67 | + marginLeft: 0, | ||
68 | + marginRight: 0, | ||
69 | + marginTop: 10, | ||
70 | + marginBottom: 0, | ||
71 | + padding: 10, | ||
72 | + borderRadius: 3, | ||
73 | + backgroundColor: BUTTON_COLOR | ||
74 | + }} | ||
75 | + style2={{ | ||
76 | + fontSize: 16, | ||
77 | + fontWeight: 'bold', | ||
78 | + textAlign: 'center', | ||
79 | + color: '#ffffff' | ||
80 | + }} | ||
81 | + /> | ||
82 | + <SquareButtonComponent | ||
83 | + text={'현재 위치로 바로가기(현재 미지원)'} | ||
84 | + style1={{ | ||
85 | + marginLeft: 0, | ||
86 | + marginRight: 0, | ||
87 | + marginTop: 10, | ||
88 | + marginBottom: 0, | ||
89 | + padding: 10, | ||
90 | + borderRadius: 3, | ||
91 | + backgroundColor: BUTTON_COLOR | ||
92 | + }} | ||
93 | + style2={{ | ||
94 | + fontSize: 16, | ||
95 | + fontWeight: 'bold', | ||
96 | + textAlign: 'center', | ||
97 | + color: '#ffffff' | ||
98 | + }} | ||
99 | + /> | ||
100 | + <SquareButtonComponent | ||
101 | + onPress={close} | ||
102 | + text={'닫기'} | ||
103 | + style1={{ | ||
104 | + marginLeft: 0, | ||
105 | + marginRight: 0, | ||
106 | + marginTop: 10, | ||
107 | + marginBottom: 0, | ||
108 | + padding: 10, | ||
109 | + borderRadius: 3, | ||
110 | + backgroundColor: BUTTON_COLOR | ||
111 | + }} | ||
112 | + style2={{ | ||
113 | + fontSize: 16, | ||
114 | + fontWeight: 'bold', | ||
115 | + textAlign: 'center', | ||
116 | + color: '#ffffff' | ||
117 | + }} | ||
118 | + /> | ||
119 | + </View> | ||
120 | + </View> | ||
121 | + ) | ||
122 | +}; | ||
123 | + | ||
124 | +export default Modal3Component; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/constant.js
0 → 100644
Kick_rn_server/debug.log
0 → 100644
1 | +[1024/142550.860:ERROR:directory_reader_win.cc(43)] FindFirstFile: 지정된 경로를 찾을 수 없습니다. (0x3) | ||
2 | +[1024/161824.551:ERROR:directory_reader_win.cc(43)] FindFirstFile: 지정된 경로를 찾을 수 없습니다. (0x3) | ||
3 | +[1024/172714.074:ERROR:directory_reader_win.cc(43)] FindFirstFile: 지정된 경로를 찾을 수 없습니다. (0x3) | ||
4 | +[1025/190018.321:ERROR:directory_reader_win.cc(43)] FindFirstFile: 지정된 경로를 찾을 수 없습니다. (0x3) | ||
5 | +[1130/200856.582:ERROR:directory_reader_win.cc(43)] FindFirstFile: 지정된 경로를 찾을 수 없습니다. (0x3) | ||
6 | +[1201/231225.210:ERROR:directory_reader_win.cc(43)] FindFirstFile: 지정된 경로를 찾을 수 없습니다. (0x3) |
Kick_rn_server/navigations/RootNavigation.js
0 → 100644
1 | +import React from "react"; | ||
2 | +import {NavigationContainer} from "@react-navigation/native"; | ||
3 | +import {createStackNavigator} from "@react-navigation/stack"; | ||
4 | + | ||
5 | +import Map from "../screens/Map"; | ||
6 | +import Auth from "../screens/Auth"; | ||
7 | +import MyPage from "../screens/MyPage"; | ||
8 | +import {useSelector} from "react-redux"; | ||
9 | + | ||
10 | +const Stack = createStackNavigator(); | ||
11 | + | ||
12 | +const RootNavigation = () => { | ||
13 | + const {me} = useSelector(state => state.user); | ||
14 | + | ||
15 | + return ( | ||
16 | + <NavigationContainer> | ||
17 | + <Stack.Navigator> | ||
18 | + <Stack.Screen name='지도' | ||
19 | + component={Map} | ||
20 | + options={{ | ||
21 | + headerShown: false | ||
22 | + }}/> | ||
23 | + <Stack.Screen name='이킥저킥' | ||
24 | + component={me ? MyPage : Auth}/> | ||
25 | + </Stack.Navigator> | ||
26 | + </NavigationContainer> | ||
27 | + ); | ||
28 | +}; | ||
29 | + | ||
30 | +export default RootNavigation; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/package.json
0 → 100644
1 | +{ | ||
2 | + "main": "node_modules/expo/AppEntry.js", | ||
3 | + "scripts": { | ||
4 | + "start": "expo start", | ||
5 | + "android": "expo start --android", | ||
6 | + "ios": "expo start --ios", | ||
7 | + "web": "expo start --web", | ||
8 | + "eject": "expo eject" | ||
9 | + }, | ||
10 | + "dependencies": { | ||
11 | + "@expo/vector-icons": "^10.0.0", | ||
12 | + "@react-native-community/async-storage": "~1.12.0", | ||
13 | + "@react-native-community/masked-view": "^0.1.10", | ||
14 | + "@react-navigation/native": "^5.1.4", | ||
15 | + "@react-navigation/stack": "^5.2.9", | ||
16 | + "axios": "^0.19.2", | ||
17 | + "expo": "^39.0.0", | ||
18 | + "expo-document-picker": "^8.4.1", | ||
19 | + "expo-location": "~9.0.0", | ||
20 | + "expo-secure-store": "~9.2.0", | ||
21 | + "expo-status-bar": "~1.0.2", | ||
22 | + "native-base": "^2.13.14", | ||
23 | + "react": "16.13.1", | ||
24 | + "react-dom": "16.13.1", | ||
25 | + "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.0.tar.gz", | ||
26 | + "react-native-gesture-handler": "^1.8.0", | ||
27 | + "react-native-maps": "0.27.1", | ||
28 | + "react-native-reanimated": "~1.13.0", | ||
29 | + "react-native-safe-area-context": "3.1.4", | ||
30 | + "react-native-screens": "~2.15.0", | ||
31 | + "react-native-tab-view": "^2.14.0", | ||
32 | + "react-native-web": "~0.13.7", | ||
33 | + "react-redux": "^7.2.0", | ||
34 | + "redux": "^4.0.5", | ||
35 | + "redux-saga": "^1.1.3", | ||
36 | + "saga": "^4.0.0-alpha", | ||
37 | + "styled-components": "^5.0.1" | ||
38 | + }, | ||
39 | + "devDependencies": { | ||
40 | + "@babel/core": "~7.9.0" | ||
41 | + }, | ||
42 | + "private": true | ||
43 | +} |
Kick_rn_server/reducers/index.js
0 → 100644
1 | +import {combineReducers} from 'redux'; | ||
2 | +import user from './user'; | ||
3 | +import location from './location'; | ||
4 | +import info from './info'; | ||
5 | + | ||
6 | +const rootReducer = combineReducers({ | ||
7 | + user, | ||
8 | + location, | ||
9 | + info, | ||
10 | +}); | ||
11 | + | ||
12 | +export default rootReducer; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/reducers/info.js
0 → 100644
1 | +export const setInfoReducer = 'setInfoReducer'; | ||
2 | +export const resetInfoReducer = 'resetInfoReducer'; | ||
3 | + | ||
4 | +export const initialState = { | ||
5 | + info: null, | ||
6 | +}; | ||
7 | + | ||
8 | +export default (state = initialState, action) => { | ||
9 | + switch (action.type) { | ||
10 | + case setInfoReducer: { | ||
11 | + const {info} = action.data; | ||
12 | + return { | ||
13 | + ...state, | ||
14 | + info, | ||
15 | + } | ||
16 | + } | ||
17 | + case resetInfoReducer: { | ||
18 | + return { | ||
19 | + ...state, | ||
20 | + info: null, | ||
21 | + }; | ||
22 | + } | ||
23 | + | ||
24 | + default: { | ||
25 | + return { | ||
26 | + ...state, | ||
27 | + }; | ||
28 | + } | ||
29 | + } | ||
30 | +}; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/reducers/location.js
0 → 100644
1 | +export const setStartLocationSaga = 'setStartLocationSaga'; | ||
2 | +export const setEndLocationSaga = 'setEndLocationSaga'; | ||
3 | +export const setKickLocationsSaga = 'setKickLocationsSaga'; | ||
4 | +export const setKickRouteSaga = 'setKickRouteSaga'; | ||
5 | + | ||
6 | +export const setStartLocationReducer = 'setStartLocationReducer'; | ||
7 | +export const setEndLocationReducer = 'setEndLocationReducer'; | ||
8 | +export const setKickLocationsReducer = 'setKickLocationsReducer'; | ||
9 | +export const setKickRouteReducer = 'setKickRouteReducer'; | ||
10 | +export const resetLaneReducer = 'resetLaneReducer'; | ||
11 | + | ||
12 | +export const initialState = { | ||
13 | + startLocation: null, | ||
14 | + endLocation: null, | ||
15 | + kickLocationsArray: [[], [], []], | ||
16 | + lane: [], | ||
17 | + distance: null, | ||
18 | + times: null, | ||
19 | +}; | ||
20 | + | ||
21 | +export default (state = initialState, action) => { | ||
22 | + switch (action.type) { | ||
23 | + case setStartLocationSaga: { | ||
24 | + return { | ||
25 | + ...state, | ||
26 | + }; | ||
27 | + } | ||
28 | + case setEndLocationSaga: { | ||
29 | + return { | ||
30 | + ...state, | ||
31 | + }; | ||
32 | + } | ||
33 | + case setKickLocationsSaga: { | ||
34 | + return { | ||
35 | + ...state, | ||
36 | + }; | ||
37 | + } | ||
38 | + case setKickRouteSaga: { | ||
39 | + return { | ||
40 | + ...state, | ||
41 | + } | ||
42 | + } | ||
43 | + | ||
44 | + case setStartLocationReducer: { | ||
45 | + const {location} = action.data; | ||
46 | + return { | ||
47 | + ...state, | ||
48 | + startLocation: location | ||
49 | + } | ||
50 | + } | ||
51 | + case setEndLocationReducer: { | ||
52 | + const {location} = action.data; | ||
53 | + return { | ||
54 | + ...state, | ||
55 | + endLocation: location | ||
56 | + } | ||
57 | + } | ||
58 | + case setKickLocationsReducer: { | ||
59 | + let {kickLocationsArray} = action.data; | ||
60 | + return { | ||
61 | + ...state, | ||
62 | + kickLocationsArray, | ||
63 | + } | ||
64 | + } | ||
65 | + case setKickRouteReducer: { | ||
66 | + let {lane, distance, time} = action.data; | ||
67 | + return { | ||
68 | + ...state, | ||
69 | + lane, distance, time, | ||
70 | + } | ||
71 | + } | ||
72 | + case resetLaneReducer: { | ||
73 | + return { | ||
74 | + ...state, | ||
75 | + lane: [], | ||
76 | + } | ||
77 | + } | ||
78 | + | ||
79 | + default: { | ||
80 | + return { | ||
81 | + ...state, | ||
82 | + }; | ||
83 | + } | ||
84 | + } | ||
85 | +}; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/reducers/user.js
0 → 100644
1 | +export const signUpSaga = 'signUpSaga'; | ||
2 | +export const loginSaga = 'loginSaga'; | ||
3 | +export const loadMeSaga = 'loadMeSaga'; | ||
4 | +export const updateUserInfoSaga = 'updateUserInfoSaga'; | ||
5 | +export const logoutSaga = 'logoutSaga'; | ||
6 | + | ||
7 | +export const loginReducer = 'loginReducer'; | ||
8 | +export const loadMeReducer = 'loadMeReducer'; | ||
9 | +export const updateUserInfoReducer = 'updateUserInfoReducer'; | ||
10 | +export const logoutReducer = 'logoutReducer'; | ||
11 | + | ||
12 | +export const initialState = { | ||
13 | + me: null, | ||
14 | +}; | ||
15 | + | ||
16 | +export default (state = initialState, action) => { | ||
17 | + switch (action.type) { | ||
18 | + case signUpSaga: { | ||
19 | + return { | ||
20 | + ...state, | ||
21 | + }; | ||
22 | + } | ||
23 | + case loginSaga: { | ||
24 | + return { | ||
25 | + ...state, | ||
26 | + }; | ||
27 | + } | ||
28 | + case loadMeSaga: { | ||
29 | + return { | ||
30 | + ...state, | ||
31 | + }; | ||
32 | + } | ||
33 | + case updateUserInfoSaga: { | ||
34 | + return { | ||
35 | + ...state, | ||
36 | + }; | ||
37 | + } | ||
38 | + case logoutSaga: { | ||
39 | + return { | ||
40 | + ...state, | ||
41 | + }; | ||
42 | + } | ||
43 | + | ||
44 | + case loginReducer: { | ||
45 | + const {user} = action.data; | ||
46 | + return { | ||
47 | + ...state, | ||
48 | + me: user, | ||
49 | + }; | ||
50 | + } | ||
51 | + case loadMeReducer: { | ||
52 | + const {user} = action.data; | ||
53 | + return { | ||
54 | + ...state, | ||
55 | + me: user, | ||
56 | + }; | ||
57 | + } | ||
58 | + case updateUserInfoReducer: { | ||
59 | + const {user} = action.data; | ||
60 | + return { | ||
61 | + ...state, | ||
62 | + me: user, | ||
63 | + } | ||
64 | + } | ||
65 | + case logoutReducer: { | ||
66 | + return { | ||
67 | + ...state, | ||
68 | + me: null, | ||
69 | + } | ||
70 | + } | ||
71 | + | ||
72 | + default: { | ||
73 | + return { | ||
74 | + ...state, | ||
75 | + }; | ||
76 | + } | ||
77 | + } | ||
78 | +}; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/sagas/index.js
0 → 100644
1 | +import {all, fork} from 'redux-saga/effects'; | ||
2 | +import user from './user'; | ||
3 | +import location from './location'; | ||
4 | + | ||
5 | +const rootSaga = function* () { | ||
6 | + yield all([ | ||
7 | + fork(user), | ||
8 | + fork(location), | ||
9 | + ]); | ||
10 | +}; | ||
11 | + | ||
12 | +export default rootSaga; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/sagas/location.js
0 → 100644
1 | +import {all, call, delay, fork, put, takeLatest} from 'redux-saga/effects'; | ||
2 | +import axios from 'axios'; | ||
3 | +import { | ||
4 | + setStartLocationSaga, | ||
5 | + setEndLocationSaga, | ||
6 | + setKickLocationsSaga, | ||
7 | + setKickRouteSaga, | ||
8 | + | ||
9 | + setStartLocationReducer, | ||
10 | + setEndLocationReducer, | ||
11 | + setKickLocationsReducer, | ||
12 | + setKickRouteReducer, | ||
13 | +} from "../reducers/location"; | ||
14 | +import { | ||
15 | + setInfoReducer, | ||
16 | + resetInfoReducer, | ||
17 | +} from '../reducers/info'; | ||
18 | +import env from '../env'; | ||
19 | + | ||
20 | +function setStartLocationAPI(data) { | ||
21 | + const {startTextLocation} = data; | ||
22 | + return axios.get(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/api/findLocation/${encodeURIComponent(startTextLocation)}`); | ||
23 | +} | ||
24 | +function* setStartLocation(action) { | ||
25 | + try { | ||
26 | + const res = yield call(setStartLocationAPI, action.data); | ||
27 | + const {location} = res.data; | ||
28 | + yield put({ | ||
29 | + type: setStartLocationReducer, | ||
30 | + data: {location} | ||
31 | + }) | ||
32 | + } catch (e) { | ||
33 | + console.error(e); | ||
34 | + if (e.response && e.response.data.info) { | ||
35 | + const {info} = e.response.data; | ||
36 | + yield put({ | ||
37 | + type: setInfoReducer, | ||
38 | + data: {info} | ||
39 | + }); | ||
40 | + yield delay(3000); | ||
41 | + yield put({ | ||
42 | + type: resetInfoReducer, | ||
43 | + }) | ||
44 | + } | ||
45 | + } | ||
46 | +} | ||
47 | +function* watchSetStartLocation() { | ||
48 | + yield takeLatest(setStartLocationSaga, setStartLocation); | ||
49 | +} | ||
50 | + | ||
51 | +function setEndLocationAPI(data) { | ||
52 | + const {endTextLocation} = data; | ||
53 | + return axios.get(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/api/findLocation/${encodeURIComponent(endTextLocation)}`); | ||
54 | +} | ||
55 | +function* setEndLocation(action) { | ||
56 | + try { | ||
57 | + const res = yield call(setEndLocationAPI, action.data); | ||
58 | + const {location} = res.data; | ||
59 | + yield put({ | ||
60 | + type: setEndLocationReducer, | ||
61 | + data: {location} | ||
62 | + }); | ||
63 | + } catch (e) { | ||
64 | + console.error(e); | ||
65 | + if (e.response && e.response.data.info) { | ||
66 | + const {info} = e.response.data; | ||
67 | + yield put({ | ||
68 | + type: setInfoReducer, | ||
69 | + data: {info} | ||
70 | + }); | ||
71 | + yield delay(3000); | ||
72 | + yield put({ | ||
73 | + type: resetInfoReducer, | ||
74 | + }) | ||
75 | + } | ||
76 | + } | ||
77 | +} | ||
78 | +function* watchSetEndLocation() { | ||
79 | + yield takeLatest(setEndLocationSaga, setEndLocation); | ||
80 | +} | ||
81 | + | ||
82 | +function setKickLocationsAPI(data) { | ||
83 | + const {startLocation, distance, serviceStates} = data; | ||
84 | + return axios.post(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/api/findKicks`, { | ||
85 | + startLocation, distance, serviceStates | ||
86 | + }); | ||
87 | +} | ||
88 | +function* setKickLocations(action) { | ||
89 | + try { | ||
90 | + const res = yield call(setKickLocationsAPI, action.data); | ||
91 | + const {kickLocationsArray} = res.data; | ||
92 | + yield put({ | ||
93 | + type: setKickLocationsReducer, | ||
94 | + data: {kickLocationsArray} | ||
95 | + }) | ||
96 | + } catch (e) { | ||
97 | + console.error(e); | ||
98 | + if (e.response && e.response.data.info) { | ||
99 | + const {info} = e.response.data; | ||
100 | + yield put({ | ||
101 | + type: setInfoReducer, | ||
102 | + data: {info} | ||
103 | + }); | ||
104 | + yield delay(3000); | ||
105 | + yield put({ | ||
106 | + type: resetInfoReducer, | ||
107 | + }) | ||
108 | + } | ||
109 | + } | ||
110 | +} | ||
111 | +function* watchSetKickLocations() { | ||
112 | + yield takeLatest(setKickLocationsSaga, setKickLocations); | ||
113 | +} | ||
114 | + | ||
115 | +function setKickRouteAPI(data) { | ||
116 | + const {startLocation, endLocation, personalVelocity} = data; | ||
117 | + return axios.post(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/api/setKickRoute`, { | ||
118 | + startLocation, | ||
119 | + endLocation, | ||
120 | + personalVelocity, | ||
121 | + }); | ||
122 | +} | ||
123 | +function* setKickRoute(action) { | ||
124 | + try { | ||
125 | + const res = yield call(setKickRouteAPI, action.data); | ||
126 | + const {lane, distance, time} = res.data; | ||
127 | + yield put({ | ||
128 | + type: setKickRouteReducer, | ||
129 | + data: {lane, distance, time} | ||
130 | + }) | ||
131 | + } catch (e) { | ||
132 | + console.error(e); | ||
133 | + if (e.response && e.response.data.info) { | ||
134 | + const {info} = e.response.data; | ||
135 | + yield put({ | ||
136 | + type: setInfoReducer, | ||
137 | + data: {info} | ||
138 | + }); | ||
139 | + yield delay(3000); | ||
140 | + yield put({ | ||
141 | + type: resetInfoReducer, | ||
142 | + }) | ||
143 | + } | ||
144 | + } | ||
145 | +} | ||
146 | +function* watchSetKickRoute() { | ||
147 | + yield takeLatest(setKickRouteSaga, setKickRoute); | ||
148 | +} | ||
149 | + | ||
150 | +export default function* locationSaga() { | ||
151 | + yield all([ | ||
152 | + fork(watchSetStartLocation), | ||
153 | + fork(watchSetEndLocation), | ||
154 | + fork(watchSetKickLocations), | ||
155 | + fork(watchSetKickRoute), | ||
156 | + ]); | ||
157 | +}; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/sagas/user.js
0 → 100644
1 | +import {all, fork, takeEvery, call, put, delay} from 'redux-saga/effects'; | ||
2 | +import axios from 'axios'; | ||
3 | +import * as SecureStore from 'expo-secure-store'; | ||
4 | +import AsyncStorage from '@react-native-community/async-storage'; | ||
5 | +import { | ||
6 | + signUpSaga, | ||
7 | + loginSaga, | ||
8 | + loadMeSaga, | ||
9 | + updateUserInfoSaga, | ||
10 | + logoutSaga, | ||
11 | + | ||
12 | + loginReducer, | ||
13 | + loadMeReducer, | ||
14 | + updateUserInfoReducer, | ||
15 | + logoutReducer, | ||
16 | +} from '../reducers/user'; | ||
17 | +import { | ||
18 | + setInfoReducer, | ||
19 | + resetInfoReducer, | ||
20 | +} from '../reducers/info'; | ||
21 | +import env from "../env"; | ||
22 | + | ||
23 | +function signUpAPI(data) { | ||
24 | + const {email, nickName, password} = data; | ||
25 | + return axios.post(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/user/signUp`, { | ||
26 | + email, nickName, password | ||
27 | + }, { | ||
28 | + withCredentials: true | ||
29 | + }); | ||
30 | +} | ||
31 | + | ||
32 | +function* signUp(action) { | ||
33 | + try { | ||
34 | + const res = yield call(signUpAPI, action.data); | ||
35 | + const {info} = res.data; | ||
36 | + yield put({ | ||
37 | + type: setInfoReducer, | ||
38 | + data: {info} | ||
39 | + }); | ||
40 | + yield delay(5000); | ||
41 | + yield put({ | ||
42 | + type: resetInfoReducer, | ||
43 | + }) | ||
44 | + } catch (e) { | ||
45 | + console.error(e); | ||
46 | + if (e.response && e.response.data.info) { | ||
47 | + const {info} = e.response.data; | ||
48 | + yield put({ | ||
49 | + type: setInfoReducer, | ||
50 | + data: {info} | ||
51 | + }); | ||
52 | + yield delay(3000); | ||
53 | + yield put({ | ||
54 | + type: resetInfoReducer, | ||
55 | + }) | ||
56 | + } | ||
57 | + } | ||
58 | +} | ||
59 | + | ||
60 | +function* signUpSagaWatch() { | ||
61 | + yield takeEvery(signUpSaga, signUp); | ||
62 | +} | ||
63 | + | ||
64 | +async function loginAPI(data) { | ||
65 | + const {email, password} = data; | ||
66 | + const res = await axios.post(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/user/login`, { | ||
67 | + email, password, | ||
68 | + }, { | ||
69 | + withCredentials: true | ||
70 | + }); | ||
71 | + const {user} = res.data; | ||
72 | + | ||
73 | + const cookie = res.headers['set-cookie'][0]; | ||
74 | + if (SecureStore.isAvailableAsync()) { | ||
75 | + await SecureStore.setItemAsync('cookie', cookie); | ||
76 | + } else { | ||
77 | + await AsyncStorage.setItem('cookie', cookie); | ||
78 | + } | ||
79 | + axios.defaults.headers.Cookie = cookie; | ||
80 | + | ||
81 | + return {user}; | ||
82 | +} | ||
83 | + | ||
84 | +function* login(action) { | ||
85 | + try { | ||
86 | + const {user} = yield call(loginAPI, action.data); | ||
87 | + yield put({ | ||
88 | + type: loginReducer, | ||
89 | + data: {user} | ||
90 | + }); | ||
91 | + } catch (e) { | ||
92 | + console.error(e); | ||
93 | + if (e.response && e.response.data.info) { | ||
94 | + const {info} = e.response.data; | ||
95 | + yield put({ | ||
96 | + type: setInfoReducer, | ||
97 | + data: {info} | ||
98 | + }); | ||
99 | + yield delay(3000); | ||
100 | + yield put({ | ||
101 | + type: resetInfoReducer, | ||
102 | + }) | ||
103 | + } | ||
104 | + } | ||
105 | +} | ||
106 | + | ||
107 | +function* loginSagaWatch() { | ||
108 | + yield takeEvery(loginSaga, login); | ||
109 | +} | ||
110 | + | ||
111 | +function loadMeAPI(data) { | ||
112 | + return axios.get(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.HOST}/user/loadMe`, { | ||
113 | + withCredentials: true | ||
114 | + }); | ||
115 | +} | ||
116 | + | ||
117 | +function* loadMe(action) { | ||
118 | + try { | ||
119 | + const res = yield call(loadMeAPI, action.data); | ||
120 | + const {user} = res.data; | ||
121 | + yield put({ | ||
122 | + type: loadMeReducer, | ||
123 | + data: {user} | ||
124 | + }); | ||
125 | + } catch (e) { | ||
126 | + console.error(e); | ||
127 | + if (e.response && e.response.data.info) { | ||
128 | + const {info} = e.response.data; | ||
129 | + yield put({ | ||
130 | + type: setInfoReducer, | ||
131 | + data: {info} | ||
132 | + }); | ||
133 | + yield delay(3000); | ||
134 | + yield put({ | ||
135 | + type: resetInfoReducer, | ||
136 | + }) | ||
137 | + } | ||
138 | + } | ||
139 | +} | ||
140 | + | ||
141 | +function* loadMeSagaWatch() { | ||
142 | + yield takeEvery(loadMeSaga, loadMe); | ||
143 | +} | ||
144 | + | ||
145 | +function logoutAPI(data) { | ||
146 | + return axios.get(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/user/logout`, { | ||
147 | + withCredentials: true | ||
148 | + }); | ||
149 | +} | ||
150 | + | ||
151 | +function* logout(action) { | ||
152 | + try { | ||
153 | + yield call(logoutAPI, action.data); | ||
154 | + yield put({ | ||
155 | + type: logoutReducer, | ||
156 | + }); | ||
157 | + } catch (e) { | ||
158 | + console.error(e); | ||
159 | + if (e.response && e.response.data.info) { | ||
160 | + const {info} = e.response.data; | ||
161 | + yield put({ | ||
162 | + type: setInfoReducer, | ||
163 | + data: {info} | ||
164 | + }); | ||
165 | + yield delay(3000); | ||
166 | + yield put({ | ||
167 | + type: resetInfoReducer, | ||
168 | + }) | ||
169 | + } | ||
170 | + } | ||
171 | +} | ||
172 | + | ||
173 | +function* logoutSagaWatch() { | ||
174 | + yield takeEvery(logoutSaga, logout); | ||
175 | +} | ||
176 | + | ||
177 | +function updateUserInfoAPI(data) { | ||
178 | + const {nickName} = data; | ||
179 | + return axios.post(`${env.HTTP_OR_HTTPS}://${env.HOST}:${env.PORT}/user/updateUserInfo`, { | ||
180 | + nickName, | ||
181 | + }, { | ||
182 | + withCredentials: true | ||
183 | + }); | ||
184 | +} | ||
185 | + | ||
186 | +function* updateUserInfo(action) { | ||
187 | + try { | ||
188 | + const res = yield call(updateUserInfoAPI, action.data); | ||
189 | + const {user} = res.data; | ||
190 | + yield put({ | ||
191 | + type: updateUserInfoReducer, | ||
192 | + data: {user} | ||
193 | + }); | ||
194 | + } catch (e) { | ||
195 | + console.error(e); | ||
196 | + if (e.response && e.response.data.info) { | ||
197 | + const {info} = e.response.data; | ||
198 | + yield put({ | ||
199 | + type: setInfoReducer, | ||
200 | + data: {info} | ||
201 | + }); | ||
202 | + yield delay(3000); | ||
203 | + yield put({ | ||
204 | + type: resetInfoReducer, | ||
205 | + }) | ||
206 | + } | ||
207 | + } | ||
208 | +} | ||
209 | + | ||
210 | +function* updateUserInfoSagaWatch() { | ||
211 | + yield takeEvery(updateUserInfoSaga, updateUserInfo); | ||
212 | +} | ||
213 | + | ||
214 | +export default function* userSaga() { | ||
215 | + yield all([ | ||
216 | + fork(signUpSagaWatch), | ||
217 | + fork(loginSagaWatch), | ||
218 | + fork(loadMeSagaWatch), | ||
219 | + fork(updateUserInfoSagaWatch), | ||
220 | + fork(logoutSagaWatch), | ||
221 | + ]); | ||
222 | +} | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/screens/Auth.js
0 → 100644
1 | +import React, {useState, useEffect} from 'react'; | ||
2 | +import {ScrollView} from 'react-native'; | ||
3 | +import LoginComponent from "../components/Auth/LoginComponent"; | ||
4 | +import SignUpComponent from "../components/Auth/SignUpComponent"; | ||
5 | +import {useSelector} from "react-redux"; | ||
6 | +import {BUTTON_COLOR} from "../constant"; | ||
7 | +import SquareButtonComponent from "../components/Base/SquareButtonComponent"; | ||
8 | + | ||
9 | +const Auth = (props) => { | ||
10 | + // 데이터 | ||
11 | + const {navigation} = props; | ||
12 | + const [tryLogin, setTryLogin] = useState(true); | ||
13 | + const {me} = useSelector(state => state.user); | ||
14 | + | ||
15 | + // 함수 | ||
16 | + const changeTryLogin = () => { | ||
17 | + setTryLogin(prev => !prev) | ||
18 | + }; | ||
19 | + | ||
20 | + // 렌더링 | ||
21 | + useEffect(() => { | ||
22 | + if (me) { | ||
23 | + navigation.navigate('마이페이지'); | ||
24 | + } | ||
25 | + }, [me]); | ||
26 | + return ( | ||
27 | + <ScrollView> | ||
28 | + { | ||
29 | + tryLogin | ||
30 | + ? | ||
31 | + <> | ||
32 | + <LoginComponent/> | ||
33 | + <SquareButtonComponent | ||
34 | + onPress={changeTryLogin} | ||
35 | + text={'회원가입'} | ||
36 | + style1={{ | ||
37 | + marginLeft: 20, | ||
38 | + marginRight: 20, | ||
39 | + marginTop: 10, | ||
40 | + marginBottom: 10, | ||
41 | + padding: 15, | ||
42 | + borderRadius: 3, | ||
43 | + backgroundColor: BUTTON_COLOR | ||
44 | + }} | ||
45 | + style2={{ | ||
46 | + fontSize: 16, | ||
47 | + fontWeight: 'bold', | ||
48 | + textAlign: 'center', | ||
49 | + color: '#ffffff' | ||
50 | + }} | ||
51 | + /> | ||
52 | + </> | ||
53 | + : | ||
54 | + <> | ||
55 | + <SignUpComponent/> | ||
56 | + <SquareButtonComponent | ||
57 | + onPress={changeTryLogin} | ||
58 | + text={'로그인'} | ||
59 | + style1={{ | ||
60 | + marginLeft: 20, | ||
61 | + marginRight: 20, | ||
62 | + marginTop: 10, | ||
63 | + marginBottom: 10, | ||
64 | + padding: 15, | ||
65 | + borderRadius: 3, | ||
66 | + backgroundColor: BUTTON_COLOR | ||
67 | + }} | ||
68 | + style2={{ | ||
69 | + fontSize: 16, | ||
70 | + fontWeight: 'bold', | ||
71 | + textAlign: 'center', | ||
72 | + color: '#ffffff' | ||
73 | + }} | ||
74 | + /> | ||
75 | + </> | ||
76 | + } | ||
77 | + </ScrollView> | ||
78 | + ) | ||
79 | +}; | ||
80 | + | ||
81 | +export default Auth; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/screens/Map.js
0 → 100644
This diff is collapsed. Click to expand it.
Kick_rn_server/screens/MyPage.js
0 → 100644
1 | +import React, {useEffect, useState, useCallback} from 'react'; | ||
2 | +import {ScrollView, Text, Alert} from 'react-native'; | ||
3 | +import {useDispatch, useSelector} from "react-redux"; | ||
4 | +import {BUTTON_COLOR} from "../constant"; | ||
5 | +import SquareButtonComponent from "../components/Base/SquareButtonComponent"; | ||
6 | +import {logoutSaga, updateUserInfoSaga} from "../reducers/user"; | ||
7 | +import TextInputComponent from "../components/Base/TextInputComponent"; | ||
8 | + | ||
9 | +const MyPage = (props) => { | ||
10 | + // 데이터 | ||
11 | + const {navigation} = props; | ||
12 | + const {me} = useSelector(state => state.user); | ||
13 | + const [nickName, setNickName] = useState(me.nickName); | ||
14 | + | ||
15 | + // 함수 | ||
16 | + const dispatch = useDispatch(); | ||
17 | + const goToPay = () => { | ||
18 | + Alert.alert('테스트 앱은 결제 기능 미지원'); | ||
19 | + }; | ||
20 | + const onChangeNickName = useCallback((text) => { | ||
21 | + setNickName(text); | ||
22 | + }, []); | ||
23 | + const updateUserInfo = () => { | ||
24 | + dispatch({type: updateUserInfoSaga, data: {nickName}}); | ||
25 | + }; | ||
26 | + const logout = () => { | ||
27 | + dispatch({type: logoutSaga}); | ||
28 | + }; | ||
29 | + | ||
30 | + // 렌더링 | ||
31 | + useEffect(() => { | ||
32 | + if (!me) { | ||
33 | + navigation.navigate('지도'); | ||
34 | + } | ||
35 | + }, [me]); | ||
36 | + return ( | ||
37 | + <ScrollView> | ||
38 | + <Text style={{ | ||
39 | + marginTop: 80, | ||
40 | + marginBottom: 20, | ||
41 | + fontSize: 24, | ||
42 | + fontWeight: 'bold', | ||
43 | + textAlign: 'center', | ||
44 | + color: '#373737' | ||
45 | + }}> | ||
46 | + 닉네임 {me.nickName}님 | ||
47 | + </Text> | ||
48 | + <TextInputComponent | ||
49 | + onChangeText={onChangeNickName} | ||
50 | + value={nickName} | ||
51 | + secureTextEntry={false} | ||
52 | + placeholder={'닉네임을 업데이트하세요'} | ||
53 | + rules={[ | ||
54 | + v => !!v || '닉네임 입력은 필수입니다', | ||
55 | + v => (v && (v.trim() !== '')) || '닉네임 입력은 필수입니다', | ||
56 | + v => (v && (v.length <= 10)) || '닉네임의 길이는 10자 이하입니다' | ||
57 | + ]} | ||
58 | + style1={{ | ||
59 | + marginLeft: 20, | ||
60 | + marginRight: 20, | ||
61 | + marginTop: 10, | ||
62 | + marginBottom: 10, | ||
63 | + padding: 15, | ||
64 | + }} | ||
65 | + style2={{ | ||
66 | + marginLeft: 35, | ||
67 | + marginRight: 35, | ||
68 | + }} | ||
69 | + /> | ||
70 | + <SquareButtonComponent | ||
71 | + onPress={updateUserInfo} | ||
72 | + text={'닉네임 업데이트'} | ||
73 | + style1={{ | ||
74 | + marginLeft: 20, | ||
75 | + marginRight: 20, | ||
76 | + marginTop: 10, | ||
77 | + marginBottom: 10, | ||
78 | + padding: 15, | ||
79 | + borderRadius: 3, | ||
80 | + backgroundColor: BUTTON_COLOR | ||
81 | + }} | ||
82 | + style2={{ | ||
83 | + fontSize: 16, | ||
84 | + fontWeight: 'bold', | ||
85 | + textAlign: 'center', | ||
86 | + color: '#ffffff' | ||
87 | + }} | ||
88 | + /> | ||
89 | + <SquareButtonComponent | ||
90 | + onPress={logout} | ||
91 | + text={'로그아웃'} | ||
92 | + style1={{ | ||
93 | + marginLeft: 20, | ||
94 | + marginRight: 20, | ||
95 | + marginTop: 10, | ||
96 | + marginBottom: 10, | ||
97 | + padding: 15, | ||
98 | + borderRadius: 3, | ||
99 | + backgroundColor: BUTTON_COLOR | ||
100 | + }} | ||
101 | + style2={{ | ||
102 | + fontSize: 16, | ||
103 | + fontWeight: 'bold', | ||
104 | + textAlign: 'center', | ||
105 | + color: '#ffffff' | ||
106 | + }} | ||
107 | + /> | ||
108 | + <SquareButtonComponent | ||
109 | + onPress={goToPay} | ||
110 | + text={'대여 및 결제 페이지'} | ||
111 | + style1={{ | ||
112 | + marginLeft: 20, | ||
113 | + marginRight: 20, | ||
114 | + marginTop: 10, | ||
115 | + marginBottom: 10, | ||
116 | + padding: 15, | ||
117 | + borderRadius: 3, | ||
118 | + backgroundColor: BUTTON_COLOR | ||
119 | + }} | ||
120 | + style2={{ | ||
121 | + fontSize: 16, | ||
122 | + fontWeight: 'bold', | ||
123 | + textAlign: 'center', | ||
124 | + color: '#ffffff' | ||
125 | + }} | ||
126 | + /> | ||
127 | + </ScrollView> | ||
128 | + ) | ||
129 | +}; | ||
130 | + | ||
131 | +export default MyPage; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_rn_server/store.js
0 → 100644
1 | +import createSagaMiddleware from "redux-saga"; | ||
2 | +import {compose, applyMiddleware, createStore} from "redux"; | ||
3 | +import rootReducer from "./reducers/index"; | ||
4 | +import rootSaga from "./sagas/index"; | ||
5 | + | ||
6 | +const sagaMiddleware = createSagaMiddleware(); | ||
7 | +const middlewares = [sagaMiddleware]; | ||
8 | +const enhancer = compose(applyMiddleware(...middlewares)); | ||
9 | +const store = createStore(rootReducer, enhancer); | ||
10 | +store.sagaTask = sagaMiddleware.run(rootSaga); | ||
11 | + | ||
12 | +export default store; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/app.js
0 → 100644
1 | +(async () => { | ||
2 | + const models = require('./models/index'); | ||
3 | + models.sequelize.sync({force: false}); | ||
4 | +})(); | ||
5 | + | ||
6 | +const express = require('express'); | ||
7 | +const path = require('path'); | ||
8 | +const morgan = require('morgan'); | ||
9 | +const helmet = require('helmet'); | ||
10 | +const hpp = require('hpp'); | ||
11 | +const cors = require('cors'); | ||
12 | +const cookieParser = require('cookie-parser'); | ||
13 | +const expressSession = require('express-session'); | ||
14 | +const httpErrors = require('http-errors'); | ||
15 | + | ||
16 | +const MongoStore = require('connect-mongo')(expressSession); | ||
17 | +const sessionMiddleware = expressSession({ | ||
18 | + resave: false, | ||
19 | + saveUninitialized: false, | ||
20 | + secret: process.env.COOKIE_SECRET, | ||
21 | + cookie: { | ||
22 | + path: '/', | ||
23 | + httpOnly: false, | ||
24 | + secure: false, | ||
25 | + }, | ||
26 | + name: 'KickCookie', | ||
27 | + store: new MongoStore({ | ||
28 | + url: process.env.NODE_ENV === 'development' | ||
29 | + ? `mongodb://${process.env.MONGO_DEV_USER}:${process.env.MONGO_DEV_PASSWORD}@${process.env.MONGO_DEV_HOST}:${process.env.MONGO_DEV_PORT}/admin` | ||
30 | + : `mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@${process.env.MONGO_HOST}:${process.env.MONGO_PORT}/admin`, | ||
31 | + dbName: process.env.NODE_ENV === 'development' ? process.env.MONGO_SESSION_DEV_DB_NAME : process.env.MONGO_SESSION_DB_NAME, | ||
32 | + }), | ||
33 | +}); | ||
34 | + | ||
35 | +const passport = require('passport'); | ||
36 | +const passportIndex = require('./passport/index'); | ||
37 | +passportIndex(passport); | ||
38 | + | ||
39 | +const app = express(); | ||
40 | +app.set('views', path.join(__dirname, 'views')); | ||
41 | +app.set('view engine', 'ejs'); | ||
42 | +if (process.env.NODE_ENV === 'development') { | ||
43 | + app.use(morgan('dev')); | ||
44 | +} else { | ||
45 | + app.use(morgan('combined')); | ||
46 | + app.use(helmet()); | ||
47 | + app.use(hpp()); | ||
48 | +} | ||
49 | +app.use(cors({ | ||
50 | + origin: '*', | ||
51 | + credentials: true | ||
52 | +})); | ||
53 | +app.use(express.json()); | ||
54 | +app.use(express.urlencoded({extended: false})); | ||
55 | +app.use(cookieParser(process.env.COOKIE_SECRET)); | ||
56 | +app.use(sessionMiddleware); | ||
57 | +app.use(passport.initialize()); | ||
58 | +app.use(passport.session()); | ||
59 | +app.use('/user', require('./routes/user')); | ||
60 | +app.use('/api', require('./routes/api')); | ||
61 | +app.use(function (req, res, next) { | ||
62 | + next(httpErrors(404)); | ||
63 | +}); | ||
64 | +app.use(function (err, req, res, next) { | ||
65 | + res.locals.message = err.message; | ||
66 | + res.locals.error = req.app.get('env') === 'development' ? err : {}; | ||
67 | + res.status(err.status || 500); | ||
68 | + res.render('error'); | ||
69 | +}); | ||
70 | + | ||
71 | +module.exports = app; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/bin/www
0 → 100644
1 | +#!/usr/bin/env node | ||
2 | + | ||
3 | +const dotenv = require('dotenv'); | ||
4 | +dotenv.config(); | ||
5 | + | ||
6 | +const debug = require('debug')('Kick_user_and_api_server'); | ||
7 | +const app = require('../app'); | ||
8 | +const http = require('http'); | ||
9 | + | ||
10 | +const normalizePort = (val) => { | ||
11 | + const port = parseInt(val, 10); | ||
12 | + | ||
13 | + if (isNaN(port)) { | ||
14 | + return val; | ||
15 | + } | ||
16 | + if (port >= 0) { | ||
17 | + return port; | ||
18 | + } | ||
19 | + | ||
20 | + return false; | ||
21 | +}; | ||
22 | +const onError = (error) => { | ||
23 | + if (error.syscall !== 'listen') { | ||
24 | + throw error; | ||
25 | + } | ||
26 | + | ||
27 | + const bind = typeof port === 'string' | ||
28 | + ? 'Pipe ' + port | ||
29 | + : 'Port ' + port; | ||
30 | + | ||
31 | + switch (error.code) { | ||
32 | + case 'EACCES': | ||
33 | + console.error(bind + ' requires elevated privileges'); | ||
34 | + process.exit(1); | ||
35 | + break; | ||
36 | + case 'EADDRINUSE': | ||
37 | + console.error(bind + ' is already in use'); | ||
38 | + process.exit(1); | ||
39 | + break; | ||
40 | + default: | ||
41 | + throw error; | ||
42 | + } | ||
43 | +}; | ||
44 | +const onListening = () => { | ||
45 | + const addr = server.address(); | ||
46 | + | ||
47 | + const bind = typeof addr === 'string' | ||
48 | + ? 'pipe ' + addr | ||
49 | + : 'port ' + addr.port; | ||
50 | + | ||
51 | + debug('Listening on ' + bind); | ||
52 | +}; | ||
53 | + | ||
54 | +const port = normalizePort(process.env.PORT); | ||
55 | +app.set('port', port); | ||
56 | +const server = http.createServer(app); | ||
57 | +server.listen(port); | ||
58 | +server.on('error', onError); | ||
59 | +server.on('listening', onListening); | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/config/config.js
0 → 100644
1 | +const config = { | ||
2 | + 'development': { | ||
3 | + 'username': process.env.MYSQL_DEV_USER, | ||
4 | + 'password': process.env.MYSQL_DEV_USER_PASSWORD, | ||
5 | + 'host': process.env.MYSQL_DEV_HOST, | ||
6 | + 'port': process.env.MYSQL_DEV_PORT, | ||
7 | + 'database': process.env.KICK_DEV_DB_NAME, | ||
8 | + 'dialect': 'mysql', | ||
9 | + 'operatorsAliases': false | ||
10 | + }, | ||
11 | + 'test': { | ||
12 | + 'username': process.env.MYSQL_DEV_USER, | ||
13 | + 'password': process.env.MYSQL_DEV_USER_PASSWORD, | ||
14 | + 'host': process.env.MYSQL_DEV_HOST, | ||
15 | + 'port': process.env.MYSQL_DEV_PORT, | ||
16 | + 'database': process.env.KICK_DEV_DB_NAME, | ||
17 | + 'dialect': 'mysql', | ||
18 | + 'operatorsAliases': false | ||
19 | + }, | ||
20 | + 'production': { | ||
21 | + 'username': process.env.MYSQL_USER, | ||
22 | + 'password': process.env.MYSQL_USER_PASSWORD, | ||
23 | + 'host': process.env.MYSQL_HOST, | ||
24 | + 'port': process.env.MYSQL_PORT, | ||
25 | + 'database': process.env.KICK_DB_NAME, | ||
26 | + 'dialect': 'mysql', | ||
27 | + 'operatorsAliases': false, | ||
28 | + 'logging': false | ||
29 | + } | ||
30 | +}; | ||
31 | + | ||
32 | +module.exports = config; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/models/index.js
0 → 100644
1 | +'use strict'; | ||
2 | + | ||
3 | +const fs = require('fs'); | ||
4 | +const path = require('path'); | ||
5 | +const Sequelize = require('sequelize'); | ||
6 | +const basename = path.basename(__filename); | ||
7 | +const env = process.env.NODE_ENV || 'development'; | ||
8 | +const config = require(__dirname + './../config/config.js')[env]; | ||
9 | +const models = {}; | ||
10 | + | ||
11 | +let sequelize; | ||
12 | +if (config.use_env_variable) { | ||
13 | + sequelize = new Sequelize(process.env[config.use_env_variable], config); | ||
14 | +} else { | ||
15 | + sequelize = new Sequelize(config.database, config.username, config.password, config); | ||
16 | +} | ||
17 | + | ||
18 | +fs | ||
19 | + .readdirSync(__dirname) | ||
20 | + .filter(file => { | ||
21 | + return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); | ||
22 | + }) | ||
23 | + .forEach(file => { | ||
24 | + const model = sequelize['import'](path.join(__dirname, file)); | ||
25 | + models[model.name] = model; | ||
26 | + }); | ||
27 | + | ||
28 | +models.User = require('./user')(sequelize, Sequelize); | ||
29 | + | ||
30 | +Object.keys(models).forEach(modelName => { | ||
31 | + if (models[modelName].associate) { | ||
32 | + models[modelName].associate(models); | ||
33 | + } | ||
34 | +}); | ||
35 | + | ||
36 | +models.sequelize = sequelize; | ||
37 | +models.Sequelize = Sequelize; | ||
38 | + | ||
39 | +module.exports = models; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/models/user.js
0 → 100644
1 | +module.exports = (sequelize, DataTypes) => { | ||
2 | + const User = sequelize.define('User', { | ||
3 | + email: { | ||
4 | + type: DataTypes.STRING(100), | ||
5 | + unique: true, | ||
6 | + allowNull: false, | ||
7 | + }, | ||
8 | + hashedPassword: { | ||
9 | + type: DataTypes.TEXT, | ||
10 | + unique: false, | ||
11 | + allowNull: false, | ||
12 | + }, | ||
13 | + nickName: { | ||
14 | + type: DataTypes.STRING(10), | ||
15 | + unique: true, | ||
16 | + allowNull: false, | ||
17 | + }, | ||
18 | + }, { | ||
19 | + timestamps: true, | ||
20 | + paranoid: false, | ||
21 | + underscored: false, | ||
22 | + charset: 'utf8mb4', | ||
23 | + collate: 'utf8mb4_general_ci', | ||
24 | + }); | ||
25 | + | ||
26 | + return User; | ||
27 | +}; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/package.json
0 → 100644
1 | +{ | ||
2 | + "name": "kick_user_and_api_server", | ||
3 | + "version": "0.0.0", | ||
4 | + "private": true, | ||
5 | + "scripts": { | ||
6 | + "dev": "cross-env NODE_ENV=development nodemon ./bin/www", | ||
7 | + "start": "cross-env NODE_ENV=production node ./bin/www" | ||
8 | + }, | ||
9 | + "dependencies": { | ||
10 | + "axios": "^0.19.2", | ||
11 | + "bcrypt": "^3.0.7", | ||
12 | + "connect-mongo": "^3.2.0", | ||
13 | + "cookie-parser": "~1.4.4", | ||
14 | + "cookie-signature": "^1.1.0", | ||
15 | + "cors": "^2.8.5", | ||
16 | + "cross-env": "^7.0.2", | ||
17 | + "debug": "~2.6.9", | ||
18 | + "ejs": "~2.6.1", | ||
19 | + "dotenv": "^8.2.0", | ||
20 | + "express": "~4.16.1", | ||
21 | + "express-session": "^1.17.0", | ||
22 | + "helmet": "^4.1.0", | ||
23 | + "hpp": "^0.2.3", | ||
24 | + "http-errors": "~1.6.3", | ||
25 | + "mongoose": "^5.9.4", | ||
26 | + "morgan": "~1.9.1", | ||
27 | + "mysql2": "^1.7.0", | ||
28 | + "passport": "^0.4.1", | ||
29 | + "passport-local": "^1.0.0", | ||
30 | + "sequelize": "^5.21.3", | ||
31 | + "sequelize-cli": "^5.5.1" | ||
32 | + }, | ||
33 | + "devDependencies": { | ||
34 | + "nodemon": "^2.0.4" | ||
35 | + } | ||
36 | +} |
Kick_user_and_api_server/passport/index.js
0 → 100644
1 | +const passport = require('passport'); | ||
2 | +const localStrategy = require('./localStrategy'); | ||
3 | +const models = require('../models/index'); | ||
4 | + | ||
5 | +module.exports = () => { | ||
6 | + passport.serializeUser((user, done) => { | ||
7 | + return done(null, user.email); | ||
8 | + }); | ||
9 | + passport.deserializeUser(async (email, done) => { | ||
10 | + try { | ||
11 | + const user = await models.User.findOne({ | ||
12 | + where: {email}, | ||
13 | + attributes: ['id', 'nickName'] | ||
14 | + }); | ||
15 | + return done(null, user); | ||
16 | + } catch (e) { | ||
17 | + console.error(e); | ||
18 | + return done(e); | ||
19 | + } | ||
20 | + }); | ||
21 | + localStrategy(passport); | ||
22 | +}; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +const LocalStrategy = require('passport-local').Strategy; | ||
2 | +const bcrypt = require('bcrypt'); | ||
3 | +const models = require('../models/index'); | ||
4 | + | ||
5 | +module.exports = (passport) => { | ||
6 | + passport.use(new LocalStrategy({ | ||
7 | + usernameField: 'email', | ||
8 | + passwordField: 'password' | ||
9 | + }, async (email, password, done) => { | ||
10 | + try { | ||
11 | + let user = await models.User.findOne({where: {email}}); | ||
12 | + if (!user) { | ||
13 | + return done(null, false, {info: '이메일 또는 비밀번호가 일치하지 않습니다'}); | ||
14 | + } | ||
15 | + const result = await bcrypt.compare(password, user.hashedPassword); | ||
16 | + if (!result) { | ||
17 | + return done(null, false, {info: '이메일 또는 비밀번호가 일치하지 않습니다'}); | ||
18 | + } | ||
19 | + user = await models.User.findOne({ | ||
20 | + where: {email}, | ||
21 | + attributes: ['id', 'email', 'nickName'] | ||
22 | + }); | ||
23 | + return done(null, user); | ||
24 | + } catch (e) { | ||
25 | + console.error(e); | ||
26 | + return done(e); | ||
27 | + } | ||
28 | + }) | ||
29 | + ); | ||
30 | +}; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/routes/api.js
0 → 100644
1 | +const express = require('express'); | ||
2 | +const router = express.Router(); | ||
3 | +const axios = require('axios'); | ||
4 | + | ||
5 | +router.get('/findLocation/:textLocation', async (req, res, next) => { | ||
6 | + try { | ||
7 | + let {textLocation} = req.params; | ||
8 | + textLocation = decodeURIComponent(textLocation); | ||
9 | + const result = await axios.get(`https://api.vworld.kr/req/address?service=address&request=getcoord&version=1.0&crs=epsg:4326&address=${encodeURIComponent(textLocation)}&refine=true&simple=false&format=json&type=road&key=${process.env.MAP_API_KEY}`); | ||
10 | + const {response} = result.data; | ||
11 | + const location = { | ||
12 | + title: textLocation, | ||
13 | + coordinate: { | ||
14 | + longitude: parseFloat(response.result.point.x), | ||
15 | + latitude: parseFloat(response.result.point.y), | ||
16 | + } | ||
17 | + }; | ||
18 | + return res.json({location}); | ||
19 | + } catch (e) { | ||
20 | + console.error(e); | ||
21 | + next(e); | ||
22 | + } | ||
23 | +}); | ||
24 | +router.post('/findKicks', async (req, res, next) => { | ||
25 | + try { | ||
26 | + const {startLocation, distance, serviceStates} = req.body; | ||
27 | + let kickLocationsArray = [[], [], []]; | ||
28 | + if (serviceStates[1]) { | ||
29 | + const result = await axios.get(`https://ext.flowerroad.ai/external/scooter/scooter_list?lat=${startLocation.coordinate.latitude}&lon=${startLocation.coordinate.longitude}&distance=${distance}`, { | ||
30 | + headers: { | ||
31 | + accept: 'application/json', | ||
32 | + 'x-api-key': process.env.KICK_API_KEY | ||
33 | + } | ||
34 | + }); | ||
35 | + const {data} = result.data; | ||
36 | + if (data.length !== 0) { | ||
37 | + kickLocationsArray[1] = data.map(kickLocation => { | ||
38 | + return { | ||
39 | + title: kickLocation.battery, | ||
40 | + coordinate: { | ||
41 | + longitude: parseFloat(kickLocation.lon), | ||
42 | + latitude: parseFloat(kickLocation.lat), | ||
43 | + }, | ||
44 | + } | ||
45 | + }); | ||
46 | + } | ||
47 | + } | ||
48 | + return res.json({kickLocationsArray}); | ||
49 | + } catch (e) { | ||
50 | + console.error(e); | ||
51 | + next(e); | ||
52 | + } | ||
53 | +}); | ||
54 | +router.post('/setKickRoute', async (req, res, next) => { | ||
55 | + try { | ||
56 | + const {startLocation, endLocation, personalVelocity} = req.body; | ||
57 | + const startLocationX = startLocation.coordinate.longitude; | ||
58 | + const startLocationY = startLocation.coordinate.latitude; | ||
59 | + const endLocationX = endLocation.coordinate.longitude; | ||
60 | + const endLocationY = endLocation.coordinate.latitude; | ||
61 | + | ||
62 | + const result = await axios.post('https://apis.openapi.sk.com/tmap/routes/pedestrian?version=1&format=json', { | ||
63 | + "startX": startLocationX, | ||
64 | + "startY": startLocationY, | ||
65 | + "endX": endLocationX, | ||
66 | + "endY": endLocationY, | ||
67 | + "speed": personalVelocity, | ||
68 | + "startName": "출발지", | ||
69 | + "endName": "도착지", | ||
70 | + "angle": 1, | ||
71 | + "endPoiId": 334852, | ||
72 | + "reqCoordType": "WGS84GEO", | ||
73 | + "searchOption": 0, | ||
74 | + "resCoordType": "WGS84GEO", | ||
75 | + }, { | ||
76 | + headers: { | ||
77 | + accept: 'application/json', | ||
78 | + 'content-type': 'application/json; charset=UTF-8', | ||
79 | + appKey: process.env.PATH_API_KEY, | ||
80 | + } | ||
81 | + }); | ||
82 | + const {features} = result.data; | ||
83 | + | ||
84 | + const drawInfoArr = []; | ||
85 | + for (let i = 0; i < features.length; i++) { | ||
86 | + const geometry = features[i].geometry; | ||
87 | + if (geometry.type === 'Point') { | ||
88 | + drawInfoArr.push(geometry.coordinates); | ||
89 | + } else { | ||
90 | + geometry.coordinates.forEach(coordinate => drawInfoArr.push(coordinate)); | ||
91 | + } | ||
92 | + } | ||
93 | + const lane = drawInfoArr.map(v => { | ||
94 | + return {longitude: v[0], latitude: v[1]} | ||
95 | + }); | ||
96 | + const distance = features[0].properties.totalDistance; | ||
97 | + const time = features[0].properties.totalTime / 60; | ||
98 | + | ||
99 | + return res.json({lane, distance, time}); | ||
100 | + } catch (e) { | ||
101 | + console.error(e); | ||
102 | + next(e); | ||
103 | + } | ||
104 | +}); | ||
105 | + | ||
106 | +module.exports = router; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
1 | +const isNotLoggedIn = (req, res, next) => { | ||
2 | + if (!req.isAuthenticated()) { | ||
3 | + next(); | ||
4 | + } else { | ||
5 | + req.logout(); | ||
6 | + req.session.destroy(); | ||
7 | + return res.status(401).json({info: '네트워크에 일시적인 장애가 발생해서 로그아웃 되었습니다. 다시 시도해주세요'}); | ||
8 | + } | ||
9 | +}; | ||
10 | + | ||
11 | +const isLoggedIn = (req, res, next) => { | ||
12 | + if (req.isAuthenticated()) { | ||
13 | + next(); | ||
14 | + } else { | ||
15 | + return res.status(401).json({}); | ||
16 | + } | ||
17 | +}; | ||
18 | + | ||
19 | +module.exports = { | ||
20 | + isNotLoggedIn, | ||
21 | + isLoggedIn, | ||
22 | +}; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/routes/user.js
0 → 100644
1 | +const express = require('express'); | ||
2 | +const router = express.Router(); | ||
3 | +const bcrypt = require('bcrypt'); | ||
4 | +const passport = require('passport'); | ||
5 | +const {isLoggedIn, isNotLoggedIn} = require("./middleware"); | ||
6 | +const models = require('../models/index'); | ||
7 | + | ||
8 | +router.post('/signUp', isNotLoggedIn, async (req, res, next) => { | ||
9 | + try { | ||
10 | + const {email, password, nickName} = req.body; | ||
11 | + let user = await models.User.findOne({where: {email}}); | ||
12 | + if (user) { | ||
13 | + return res.status(401).json({info: '중복된 이메일입니다'}); | ||
14 | + } | ||
15 | + user = await models.User.findOne({where: {nickName}}); | ||
16 | + if (user) { | ||
17 | + return res.status(401).json({info: '중복된 닉네임입니다'}); | ||
18 | + } | ||
19 | + | ||
20 | + const hashedPassword = await bcrypt.hash(password, 10); | ||
21 | + await models.User.create({ | ||
22 | + email, hashedPassword, nickName | ||
23 | + }); | ||
24 | + | ||
25 | + return res.json({info: '회원가입 완료. 로그인 하세요'}); | ||
26 | + } catch (e) { | ||
27 | + console.error(e); | ||
28 | + next(e); | ||
29 | + } | ||
30 | +}); | ||
31 | +router.post('/login', isNotLoggedIn, (req, res, next) => { | ||
32 | + passport.authenticate('local', {}, (err, user, info) => { | ||
33 | + if (err) { | ||
34 | + console.error(err); | ||
35 | + return next(err); | ||
36 | + } | ||
37 | + if (info) { | ||
38 | + return res.status(401).json({info}); | ||
39 | + } | ||
40 | + req.login(user, (err) => { | ||
41 | + if (err) { | ||
42 | + console.error(err); | ||
43 | + return next(err); | ||
44 | + } | ||
45 | + return res.json({user}); | ||
46 | + }); | ||
47 | + })(req, res, next); | ||
48 | +}); | ||
49 | +router.get('/loadMe', isLoggedIn, (req, res, next) => { | ||
50 | + try { | ||
51 | + return res.json({user: req.user}); | ||
52 | + } catch (e) { | ||
53 | + console.error(e); | ||
54 | + next(e); | ||
55 | + } | ||
56 | +}); | ||
57 | +router.post('/updateUserInfo', isLoggedIn, async (req, res, next) => { | ||
58 | + try { | ||
59 | + const {nickName} = req.body; | ||
60 | + | ||
61 | + await models.User.update({ | ||
62 | + nickName | ||
63 | + }, { | ||
64 | + where: {id: req.user.id} | ||
65 | + }); | ||
66 | + | ||
67 | + const user = await models.User.findOne({where: {id: req.user.id}}); | ||
68 | + | ||
69 | + return res.json({user}); | ||
70 | + } catch (e) { | ||
71 | + console.error(e); | ||
72 | + next(e); | ||
73 | + } | ||
74 | +}); | ||
75 | +router.get('/logout', isLoggedIn, (req, res, next) => { | ||
76 | + try { | ||
77 | + req.logout(); | ||
78 | + req.session.destroy(); | ||
79 | + return res.json({}); | ||
80 | + } catch (e) { | ||
81 | + console.error(e); | ||
82 | + next(e); | ||
83 | + } | ||
84 | +}); | ||
85 | + | ||
86 | +module.exports = router; | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
Kick_user_and_api_server/views/error.ejs
0 → 100644
-
Please register or login to post a comment