happiness6533

final

Showing 49 changed files with 2952 additions and 0 deletions
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
1 +{
2 + "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 + "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 +}
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.
1 +{
2 + "packagerPort": null,
3 + "packagerPid": null,
4 + "expoServerPort": null,
5 + "expoServerNgrokUrl": null,
6 + "packagerNgrokUrl": null,
7 + "ngrokPid": null
8 +}
1 +{
2 + "hostType": "lan",
3 + "lanType": "ip",
4 + "dev": true,
5 + "minify": false,
6 + "urlRandomness": null,
7 + "https": false
8 +}
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
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
1 +module.exports = function(api) {
2 + api.cache(true);
3 + return {
4 + presets: ['babel-preset-expo'],
5 + };
6 +};
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
1 +import {Dimensions} from 'react-native';
2 +
3 +export const BUTTON_COLOR = '#5051ff';
4 +export const WIDTH_OF_DEVICE = Dimensions.get('screen').width;
5 +export const HEIGHT_OF_DEVICE = Dimensions.get('screen').height;
...\ No newline at end of file ...\ No newline at end of file
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)
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
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 +}
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
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
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
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
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
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
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
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
1 +import React, {useState, useEffect} from 'react';
2 +import {View, Text, StyleSheet, Modal, TouchableHighlight} from 'react-native';
3 +import {useSelector, useDispatch} from "react-redux";
4 +import MapView, {Marker, Polyline} from 'react-native-maps';
5 +import {MaterialCommunityIcons} from "@expo/vector-icons";
6 +import styled from "styled-components";
7 +import Modal1Component from "../components/PopUp/Modal1Component";
8 +import Modal2Component from "../components/PopUp/Modal2Component";
9 +import {WIDTH_OF_DEVICE} from '../constant';
10 +import {setKickLocationsSaga, setKickRouteSaga, resetLaneReducer} from "../reducers/location";
11 +import KickMarkerComponent from "../components/KickMarkerComponent"
12 +import Modal3Component from "../components/PopUp/Modal3Component";
13 +
14 +const Map = (props) => {
15 + // 데이터
16 + const {navigation} = props;
17 + const [region, setRegion] = useState(null);
18 + const [modalNumber, setModalNumber] = useState(0);
19 + const [modalVisible, setModalVisible] = useState(false);
20 + const [showButtons, setShowButtons] = useState(false);
21 + const [serviceStates, setServiceStates] = useState([false, true, false]);
22 + const [serviceSpeeds, setServiceSpeeds] = useState([50, 60, 70]);
23 + const [servicePrice, setServicePrice] = useState(0);
24 + const [minIndexes, setMinIndexes] = useState([-1, -1, -1]);
25 + const [startTextLocation, setStartTextLocation] = useState(null);
26 + const [endTextLocation, setEndTextLocation] = useState(null);
27 + const [startLocationMarker, setStartLocationMarker] = useState(null);
28 + const [endLocationMarker, setEndLocationMarker] = useState(null);
29 + const [kickLocationMarkers, setKickLocationMarkers] = useState([[], [], []]);
30 + const {startLocation, endLocation, kickLocationsArray, time, lane} = useSelector(state => state.location);
31 +
32 + // 함수
33 + const dispatch = useDispatch();
34 + const changeRegion = () => {
35 + setRegion({
36 + latitude: startLocation.coordinate.latitude,
37 + longitude: startLocation.coordinate.longitude,
38 + latitudeDelta: 0.01,
39 + longitudeDelta: 0.01
40 + });
41 + };
42 + const closeModal = () => {
43 + setModalNumber(0);
44 + setModalVisible(false);
45 + setShowButtons(true);
46 + };
47 + const onPressButton = async (numberOfBtn) => {
48 + if (numberOfBtn === 5) {
49 + return navigation.navigate('이킥저킥');
50 + }
51 + if (numberOfBtn === 1) {
52 + return setShowButtons(!showButtons);
53 + }
54 + setModalNumber(numberOfBtn);
55 + setShowButtons(false);
56 + setModalVisible(true);
57 + };
58 + const selectKickServices = (serviceStates) => {
59 + setServiceStates(serviceStates);
60 + };
61 + const showKicks = async (serviceStates) => {
62 + try {
63 + setKickLocationMarkers([[], [], []]);
64 + await dispatch({
65 + type: setKickLocationsSaga,
66 + data: {
67 + startLocation,
68 + distance: 0.3,
69 + serviceStates,
70 + }
71 + });
72 + } catch (e) {
73 + console.error(e);
74 + }
75 + };
76 + const getDistanceFormOriginToKick = (x2, y2) => {
77 + const x1 = startLocation.coordinate.longitude;
78 + const y1 = startLocation.coordinate.latitude;
79 +
80 + return Math.sqrt(x1 - x2) + Math.sqrt(y1 - y2);
81 + };
82 + const getMinIndexes = () => {
83 + let d = 999999999;
84 + let minIndex0 = 0;
85 + let minIndex1 = 0;
86 + let minIndex2 = 2;
87 + if (serviceStates[0]) {
88 + for (let i = 0; i < kickLocationMarkers[0].length; i++) {
89 + const kickLocationMarker = kickLocationMarkers[0][i];
90 + if (d > getDistanceFormOriginToKick(parseFloat(kickLocationMarker.coordinate.latitude), parseFloat(kickLocationMarker.coordinate.latitude))) {
91 + d = getDistanceFormOriginToKick(parseFloat(kickLocationMarker.coordinate.latitude), parseFloat(kickLocationMarker.coordinate.latitude));
92 + minIndex0 = i;
93 + }
94 + }
95 + }
96 + if (serviceStates[1]) {
97 + for (let i = 0; i < kickLocationMarkers[1].length; i++) {
98 + const kickLocationMarker = kickLocationMarkers[1][i];
99 + if (d > getDistanceFormOriginToKick(parseFloat(kickLocationMarker.coordinate.latitude), parseFloat(kickLocationMarker.coordinate.latitude))) {
100 + d = getDistanceFormOriginToKick(parseFloat(kickLocationMarker.coordinate.latitude), parseFloat(kickLocationMarker.coordinate.latitude));
101 + minIndex1 = i;
102 + }
103 + }
104 + }
105 + if (serviceStates[2]) {
106 + for (let i = 0; i < kickLocationMarkers[2].length; i++) {
107 + const kickLocationMarker = kickLocationMarkers[2][i];
108 + if (d > getDistanceFormOriginToKick(parseFloat(kickLocationMarker.coordinate.latitude), parseFloat(kickLocationMarker.coordinate.latitude))) {
109 + d = getDistanceFormOriginToKick(parseFloat(kickLocationMarker.coordinate.latitude), parseFloat(kickLocationMarker.coordinate.latitude));
110 + minIndex2 = i;
111 + }
112 + }
113 + }
114 + setMinIndexes([
115 + minIndex0,
116 + minIndex1,
117 + minIndex2,
118 + ]);
119 + };
120 + const getPath = async (serviceNumber, coordinate) => {
121 + try {
122 + if (!endLocation) {
123 + return;
124 + }
125 + await dispatch({
126 + type: setKickRouteSaga,
127 + data: {
128 + startLocation: coordinate,
129 + endLocation,
130 + personalVelocity: serviceSpeeds[serviceNumber]
131 + }
132 + });
133 +
134 + if (serviceNumber === 0) {
135 + let price = 300;
136 + if (time > 1) {
137 + price += (Math.floor(time) - 1) * 150
138 + }
139 + setServicePrice(price);
140 + } else if (serviceNumber === 1) {
141 + let price = 1000;
142 + if (time > 5) {
143 + price += (Math.floor(time) - 5) * 100
144 + }
145 + setServicePrice(price);
146 + } else {
147 + let price = 1000;
148 + if (time > 5) {
149 + price += (Math.floor(time) - 5) * 100
150 + }
151 + setServicePrice(price);
152 + }
153 + } catch (e) {
154 + console.error(e);
155 + }
156 + };
157 +
158 + // 렌더링
159 + useEffect(() => {
160 + setRegion({
161 + latitude: 37.56647,
162 + longitude: 126.977963,
163 + latitudeDelta: 0.5,
164 + longitudeDelta: 0.5
165 + });
166 + return () => {
167 + dispatch({type: resetLaneReducer});
168 + };
169 + }, []);
170 + useEffect(() => {
171 + if (startLocation) {
172 + setStartLocationMarker(startLocation);
173 + showKicks(serviceStates);
174 + getMinIndexes();
175 + changeRegion();
176 + }
177 + if (endLocation) {
178 + setEndLocationMarker(endLocation);
179 + }
180 + return () => {
181 + dispatch({type: resetLaneReducer});
182 + };
183 + }, [startLocation, endLocation]);
184 + useEffect(() => {
185 + setKickLocationMarkers(kickLocationsArray);
186 + return () => {
187 + dispatch({type: resetLaneReducer});
188 + };
189 + }, [kickLocationsArray]);
190 + return (
191 + <View style={{flex: 1}}>
192 + <MapView style={styles.middleStyle}
193 + region={region}
194 + textStyle={{color: '#17174b'}}
195 + >
196 + {startLocationMarker
197 + ?
198 + <Marker coordinate={{
199 + latitude: startLocationMarker.coordinate.latitude,
200 + longitude: startLocationMarker.coordinate.longitude,
201 + }}
202 + pinColor={'red'}
203 + />
204 + :
205 + <></>
206 + }
207 + {endLocationMarker
208 + ?
209 + <Marker coordinate={{
210 + latitude: endLocationMarker.coordinate.latitude,
211 + longitude: endLocationMarker.coordinate.longitude,
212 + }}
213 + pinColor={'blue'}
214 + />
215 + :
216 + <></>
217 + }
218 + {serviceStates[0] && kickLocationMarkers[0]
219 + ?
220 + kickLocationMarkers[0].map((kickLocationMarker, index) => {
221 + return <KickMarkerComponent
222 + serviceNumber={0}
223 + kickLocationMarker={kickLocationMarker}
224 + key={`index-${index}`}
225 + index={index}
226 + minIndex={minIndexes[0]}
227 + servicePrice={servicePrice}
228 + getPath={getPath}
229 + nameOfService={'지쿠터'}
230 + infoOfService={`주행가능거리 70분\n기본요금 1분 300원\n추가요금 1분 150원`}
231 + markerImage={'../assets/gcooter.png'}
232 + />
233 + })
234 + :
235 + <></>
236 + }
237 + {serviceStates[1] && kickLocationMarkers[1]
238 + ?
239 + kickLocationMarkers[1].map((kickLocationMarker, index) => {
240 + return <KickMarkerComponent
241 + serviceNumber={1}
242 + kickLocationMarker={kickLocationMarker}
243 + key={`index-${index}`}
244 + index={index}
245 + minIndex={minIndexes[1]}
246 + servicePrice={servicePrice}
247 + getPath={getPath}
248 + nameOfService={'플라워 로드'}
249 + infoOfService={`주행가능거리 70분\n기본요금 5분 1000원\n추가요금 1분 100원`}
250 + markerImage={'../assets/flower_road.png'}
251 + />
252 + })
253 + :
254 + <></>
255 + }
256 + {serviceStates[2] && kickLocationMarkers[2]
257 + ?
258 + kickLocationMarkers[2].map((kickLocationMarker, index) => {
259 + return <KickMarkerComponent
260 + serviceNumber={2}
261 + kickLocationMarker={kickLocationMarker}
262 + key={`index-${index}`}
263 + index={index}
264 + minIndex={minIndexes[2]}
265 + servicePrice={servicePrice}
266 + getPath={getPath}
267 + nameOfService={'씽씽'}
268 + infoOfService={`주행가능거리 70분\n기본요금 5분 1000원\n추가요금 1분 100원`}
269 + markerImage={'../assets/ssing_ssing.png'}
270 + />
271 + })
272 + :
273 + <></>
274 + }
275 + {lane.length !== 0
276 + ?
277 + <Polyline
278 + coordinates={lane}
279 + strokeColor="black"
280 + strokeWidth={5}
281 + />
282 + :
283 + <></>
284 + }
285 + </MapView>
286 + {showButtons
287 + ?
288 + <>
289 + <Button5 onPress={() => onPressButton(5)}>
290 + <MaterialCommunityIcons color={'#373737'} name={'account'} size={48}/>
291 + </Button5>
292 + <Button4 onPress={() => onPressButton(4)}>
293 + <MaterialCommunityIcons color={'#373737'} name={'map-marker'} size={48}/>
294 + </Button4>
295 + <Button3 onPress={() => onPressButton(3)}>
296 + <MaterialCommunityIcons color={'#373737'} name={'road'} size={48}/>
297 + </Button3>
298 + <Button2 onPress={() => onPressButton(2)}>
299 + <MaterialCommunityIcons color={'#373737'} name={'filter'} size={48}/>
300 + </Button2>
301 + </>
302 + :
303 + <></>
304 + }
305 + <Button1 onPress={() => onPressButton(1)}>
306 + <MaterialCommunityIcons color={'#373737'} name={showButtons ? 'chevron-right' : 'chevron-left'}
307 + size={48}/>
308 + </Button1>
309 + <Modal animationType="slide" transparent={true}
310 + visible={modalVisible}>
311 + <View style={styles.centeredView}>
312 + <View style={styles.modalView}>
313 + <TouchableHighlight>
314 + {modalNumber === 4
315 + ?
316 + <Modal3Component startTextLocation={startTextLocation}
317 + setStartTextLocation={setStartTextLocation}
318 + closeModal={closeModal}
319 + setModalNumber={setModalNumber}
320 + setShowButtons={setShowButtons}
321 + setModalVisible={setModalVisible}/>
322 + :
323 + modalNumber === 3
324 + ?
325 + <Modal2Component startTextLocation={startTextLocation}
326 + setStartTextLocation={setStartTextLocation}
327 + endTextLocation={endTextLocation}
328 + setEndTextLocation={setEndTextLocation}
329 + serviceStates={serviceStates}
330 + setModalNumber={setModalNumber}
331 + setShowButtons={setShowButtons}
332 + setModalVisible={setModalVisible}
333 + closeModal={closeModal}/>
334 + :
335 + modalNumber === 2
336 + ?
337 + <Modal1Component serviceStates={serviceStates}
338 + setServiceStates={setServiceStates}
339 + selectKickServices={selectKickServices}
340 + setModalNumber={setModalNumber}
341 + setShowButtons={setShowButtons}
342 + setModalVisible={setModalVisible}/>
343 + :
344 + <></>
345 + }
346 + </TouchableHighlight>
347 + </View>
348 + </View>
349 + </Modal>
350 + <View style={styles.bottomStyle}><Text>지역 광고</Text></View>
351 + </View>
352 + )
353 +};
354 +
355 +const Button5 = styled.TouchableOpacity`
356 + position: absolute;
357 + right: 250px;
358 + top: 30px;
359 + width: 50px;
360 + height: 50px;
361 + border-radius: 5px;
362 + border: 1px solid #17174B;
363 + backgroundColor: #ffffff;
364 + justifyContent: center;
365 +`;
366 +const Button4 = styled.TouchableOpacity`
367 + position: absolute;
368 + right: 190px;
369 + top: 30px;
370 + width: 50px;
371 + height: 50px;
372 + border-radius: 5px;
373 + border: 1px solid #17174B;
374 + backgroundColor: #ffffff;
375 + justifyContent: center;
376 +`;
377 +const Button3 = styled.TouchableOpacity`
378 + position: absolute;
379 + right: 130px;
380 + top: 30px;
381 + width: 50px;
382 + height: 50px;
383 + border-radius: 5px;
384 + border: 1px solid #17174B;
385 + backgroundColor: #ffffff;
386 + justifyContent: center;
387 +`;
388 +const Button2 = styled.TouchableOpacity`
389 + position: absolute;
390 + right: 70px;
391 + top: 30px;
392 + width: 50px;
393 + height: 50px;
394 + border-radius: 5px;
395 + border: 1px solid #17174B;
396 + backgroundColor: #ffffff;
397 + justifyContent: center;
398 +`;
399 +const Button1 = styled.TouchableOpacity`
400 + position: absolute;
401 + right: 10px;
402 + top: 30px;
403 + width: 50px;
404 + height: 50px;
405 + border-radius: 5px;
406 + border: 1px solid #17174B;
407 + backgroundColor: #ffffff;
408 + justifyContent: center;
409 +`;
410 +
411 +const styles = StyleSheet.create({
412 + topStyle: {
413 + flex: 1,
414 + flexDirection: 'row',
415 + alignItems: 'center',
416 + justifyContent: 'center',
417 + paddingTop: 20,
418 + },
419 + topItem1: {
420 + flex: 1,
421 + alignItems: 'center',
422 + justifyContent: 'center',
423 + },
424 + topItem2: {
425 + flex: 5,
426 + borderRadius: 10,
427 + backgroundColor: '#ebebeb',
428 + width: 250,
429 + height: 50,
430 + borderColor: '#000000',
431 + flexDirection: 'row',
432 + justifyContent: 'space-between',
433 + },
434 + input: {
435 + padding: 10,
436 + },
437 +
438 +
439 + middleStyle: {
440 + flex: 8,
441 + width: WIDTH_OF_DEVICE,
442 + },
443 + modalView: {
444 + margin: 20,
445 + backgroundColor: 'white',
446 + borderRadius: 20,
447 + padding: 35,
448 + alignItems: 'center',
449 + shadowColor: '#000',
450 + shadowOffset: {
451 + width: 0,
452 + height: 2,
453 + },
454 + shadowOpacity: 0.25,
455 + shadowRadius: 3.84,
456 + elevation: 5,
457 + },
458 + openButton: {
459 + backgroundColor: '#F194FF',
460 + borderRadius: 20,
461 + padding: 10,
462 + elevation: 2,
463 + },
464 +
465 + modalText: {
466 + marginBottom: 15,
467 + textAlign: 'center',
468 + },
469 + textStyle: {
470 + flex: 1,
471 + fontWeight: 'bold',
472 + fontSize: 20,
473 + color: 'grey',
474 + marginBottom: 20,
475 + },
476 + bottomStyle: {
477 + flex: 1,
478 + flexDirection: 'row',
479 + alignItems: 'center',
480 + justifyContent: 'center',
481 + },
482 +});
483 +
484 +export default Map;
...\ No newline at end of file ...\ No newline at end of file
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
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
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
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
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
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
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
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 +}
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
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
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
1 +<h1><%= message %></h1>
2 +<h2><%= error.status %></h2>
3 +<pre><%= error.stack %></pre>
...\ No newline at end of file ...\ No newline at end of file