diff --git a/src/assets/color.js b/src/assets/color.js index 4a809dc..4eec338 100644 --- a/src/assets/color.js +++ b/src/assets/color.js @@ -10,4 +10,5 @@ export const COLOR_TEXT60GRAY = '#666666'; export const COLOR_NAVY = '#003C71'; export const COLOR_LIGHTGRAY = '#dddddd'; export const COLOR_RED = '#FF0000'; -export const COLOR_BLUE = '#0000FF'; \ No newline at end of file +export const COLOR_BLUE = '#0000FF'; +export const COLOR_ORANGE = '#FF6A13'; \ No newline at end of file diff --git a/src/assets/image.png b/src/assets/image.png new file mode 100644 index 0000000..b7104b9 Binary files /dev/null and b/src/assets/image.png differ diff --git a/src/assets/svg.js b/src/assets/svg.js index cbb347c..2c7a21d 100644 --- a/src/assets/svg.js +++ b/src/assets/svg.js @@ -83,12 +83,12 @@ export const svgXml = { `, - starFill:` + starFill: ` `, - starEmpty:` + starEmpty: ` @@ -196,12 +196,50 @@ export const svgXml = { `, + starGrey: ` + + + + `, + heartGrey: ` + + + + `, + emptyHeartGrey: ` + + + + `, camera: ` - - + + + + `, + phone: ` + + + + `, + pen: ` + + + + + `, + location: ` + + + + + `, + clock: ` + + + `, emptyStar: ` @@ -256,5 +294,11 @@ export const svgXml = { `, + close: ` + + + + + `, }, }; diff --git a/src/components/ListModal.js b/src/components/ListModal.js index 5faac60..4af3573 100644 --- a/src/components/ListModal.js +++ b/src/components/ListModal.js @@ -39,7 +39,35 @@ import AppContext from './AppContext'; const windowWidth = Dimensions.get('window').width; export default function ListModal(props) { - const {visible, setVisible, title, value, setValue, valueList} = props; + const {visible, setVisible, title, value, setValue, valueList, setLocation} = + props; + + const getMyLocation = async () => { + const platformPermissions = PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION; + + try { + let result = await request(platformPermissions); + console.log(result); + } catch (err) { + console.warn(err); + } + + Geolocation.getCurrentPosition( + position => { + console.log(position.coords.latitude, position.coords.longitude); + const {latitude, longitude} = position.coords; + + setLocation({ + latitude: latitude, + longitude: longitude, + }); + }, + error => { + console.log(error.code, error.message); + }, + {enableHighAccuracy: true, timeout: 15000, maximumAge: 10000}, + ); + }; return ( { + onPress={async () => { + if (item == '가까운 순') { + getMyLocation(); + } setValue(item); }}> {item == value ? ( diff --git a/src/components/StoreCompo.js b/src/components/StoreCompo.js index 2f70fe4..28c7d1c 100644 --- a/src/components/StoreCompo.js +++ b/src/components/StoreCompo.js @@ -50,10 +50,10 @@ export default function StoreCompo(props) { { @@ -153,7 +153,7 @@ export default function StoreCompo(props) { fontSize: 11, color: COLOR_TEXT70GRAY, }}> - {storeData.firstReview.reviewer + ' 님'} + {storeData.representativeReviewContent.reviewer + ' 님'} - {storeData.firstReview.body} + {storeData.representativeReviewContent.body} ) : ( diff --git a/src/components/TodayPick.js b/src/components/TodayPick.js index 6b532f3..3ff4588 100644 --- a/src/components/TodayPick.js +++ b/src/components/TodayPick.js @@ -86,7 +86,7 @@ export default function TodayPick(props) { ref={scrollViewRef}> {todaysPick.map((pickData, index) => { return ( - + ); })} @@ -103,7 +103,7 @@ const styles = StyleSheet.create({ marginTop: 15, width: windowWidth - 32, padding: 12, - paddingHorizontal: 10, + paddingHorizontal: 0, backgroundColor: COLOR_WHITE, borderRadius: 10, shadowOffset: { @@ -116,6 +116,7 @@ const styles = StyleSheet.create({ }, todayPickTitle: { fontSize: 20, + paddingHorizontal: 10, color: COLOR_TEXT70GRAY, fontWeight: '700', }, diff --git a/src/navigation/BottomTabNavigator.js b/src/navigation/BottomTabNavigator.js index 888d4a9..913ffa1 100644 --- a/src/navigation/BottomTabNavigator.js +++ b/src/navigation/BottomTabNavigator.js @@ -29,7 +29,6 @@ import HomeScreen from '../screens/home/HomeScreen'; import ListMainScreen from '../screens/list/ListMainScreen'; import MapScreen from '../screens/map/MapScreen'; import MypageScreen from '../screens/mypage/MypageScreen'; -import SearchScreen from '../screens/map/SearchScreen'; import UserDataChangeScreen from '../screens/mypage/UserDataChangeScreen'; const BottomTab = createBottomTabNavigator(); @@ -94,7 +93,6 @@ function MapNavigator() { cardStyleInterpolator: customCardStyleInterpolator, }}> - ); } diff --git a/src/navigation/MainStackNavigator.js b/src/navigation/MainStackNavigator.js index 1d25653..e4c7f48 100644 --- a/src/navigation/MainStackNavigator.js +++ b/src/navigation/MainStackNavigator.js @@ -17,6 +17,7 @@ import CheckEmailScreen from '../screens/signup/CheckEmailScreen'; import ProfileSetScreen from '../screens/signup/ProfileSetScreen'; import CheckEmailScreen2 from '../screens/signup/CheckEmailScreen2'; import ResetPasswordScreen from '../screens/signup/ResetPasswordScreen'; +import SearchScreen from '../screens/detail/SearchScreen'; import BottomTabNavigator from './BottomTabNavigator'; @@ -56,6 +57,7 @@ export default function MainStackNavigator() { + { + if (rating === 0) { + setShowRatingError(true); + return; + } + if (reviewContent.trim() === '') { + setShowContentError(true); + return; + } + + try { + const response = await axios.post( + `${API_URL}/v1/restaurants/${storeData.id}/reviews`, + { + content: reviewContent, + imageUrls: reviewImage, + rating: rating, + }, + { + headers: { Authorization: `Bearer ${context.accessToken}` }, + }, + ); + console.log('Review submitted successfully:', response.data); + navigation.goBack(); + } catch (error) { + console.error('Error submitting review:', error); + } + }; + + const uploadImage = async (image) => { + if (reviewImage.length >= 3) { + setShowImageError(true); + return; + } + + let imageData = ''; + await RNFS.readFile(image.path, 'base64') + .then((data) => { + console.log('encoded', data); + imageData = data; + }) + .catch((err) => { + console.error(err); + }); + + try { + const response = await axios.post(`${IMG_URL}/v1/upload-image`, { + images: [ + { + imageData: imageData, + location: 'test', + }, + ], + }); + + console.log('response image:', response.data); + + if (response.data.result != 'SUCCESS') { + console.log('Error: No return data'); + return; + } + + setReviewImage((prevImages) => [...prevImages, response.data.data[0].imageUrl]); + } catch (error) { + console.log('Error:', error); + } + }; + + const removeImage = (index) => { + setReviewImage((prevImages) => prevImages.filter((_, i) => i !== index)); + }; + + const DottedLine = () => { + return ( + + {[...Array(20)].map((_, index) => ( + + ))} + + ); + }; + const DottedLine = () => { return ( @@ -64,12 +155,18 @@ export default function ReviewWriteScreen(props) { return ( <>
- - < View style={styles.headerContainer}> + + {storeData.name} {[...Array(5)].map((_, index) => ( - setRating(index + 1)}> + { + setRating(index + 1); + setShowRatingError(false); + }} + > - - 사진 첨부하기 - - - - - 완료 - - + + {showRatingError && 평점을 매겨주세요} + { + console.log('리뷰 사진 추가', reviewImage); + if (reviewImage.length >= 3) { + console.log('Error: Maximum 3 images allowed'); + return; + } + ImagePicker.openPicker({ + width: 400, + height: 400, + cropping: true, + multiple: true, + }).then((images) => { + images.forEach((image) => uploadImage(image)); + }).catch((error) => { + console.error('Image Picker Error:', error); + }); + }}> + + 사진 첨부하기 + + {showImageError && 사진은 최대 3개만 넣어주세요} + { + setReviewContent(text); + setShowContentError(false); + }} + /> + {showContentError && 리뷰 내용을 작성해주세요} + + + + {reviewImage.map((image, index) => ( + + + removeImage(index)}> + X + + + ))} + + + + + 완료 + + + ); } @@ -100,7 +242,6 @@ export default function ReviewWriteScreen(props) { const styles = StyleSheet.create({ entire: { backgroundColor: COLOR_WHITE, - // justifyContent: 'center', alignItems: 'center', height: '100%', }, @@ -110,7 +251,11 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', width: '100%', paddingHorizontal: 16, - marginVertical: 24, + marginVertical: 20, + }, + Container: { + width: '100%', + marginHorizontal: 16, }, storeName: { fontSize: 22, @@ -121,20 +266,50 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, photoButton: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', backgroundColor: COLOR_WHITE, borderColor: COLOR_PRIMARY, borderWidth: 1, padding: 12, borderRadius: 8, - alignItems: 'center', - marginTop: 8, + marginTop: 6, marginBottom: 16, width: '92%', - height: '', }, photoButtonText: { color: COLOR_PRIMARY, fontSize: 16, + marginLeft: 4, + }, + imageScrollView: { + height: 120, + }, + imageContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + imageWrapper: { + position: 'relative', + marginRight: 8, + }, + image: { + width: 120, + height: 120, + borderRadius: 10, + }, + removeButton: { + position: 'absolute', + top: 5, + right: 5, + backgroundColor: 'rgba(0,0,0,0.5)', + padding: 4, + borderRadius: 5, + }, + removeButtonText: { + color: COLOR_WHITE, + fontSize: 12, }, reviewInput: { backgroundColor: COLOR_WHITE, @@ -143,11 +318,15 @@ const styles = StyleSheet.create({ borderRadius: 8, padding: 12, width: '92%', - height: 150, + height: 160, textAlignVertical: 'top', marginVertical: 16, fontSize: 16, }, + errorText: { + color: COLOR_ORANGE, + fontSize: 10, + }, dottedContainer: { flexDirection: 'row', width: '92%', @@ -167,7 +346,7 @@ const styles = StyleSheet.create({ padding: 16, borderRadius: 32, alignItems: 'center', - width: '90%', + width: '92%', marginBottom: 16, shadowColor: COLOR_TEXT_BLACK, shadowOffset: { width: 0, height: 3 }, @@ -180,4 +359,4 @@ const styles = StyleSheet.create({ fontSize: 18, fontWeight: '600', }, -}); +}); \ No newline at end of file diff --git a/src/screens/detail/SearchScreen.js b/src/screens/detail/SearchScreen.js new file mode 100644 index 0000000..75badb3 --- /dev/null +++ b/src/screens/detail/SearchScreen.js @@ -0,0 +1,323 @@ +/* eslint-disable react/no-unstable-nested-components */ +/* eslint-disable react/self-closing-comp */ +/* eslint-disable react-native/no-inline-styles */ +import React, {useState, useCallback, useEffect, useContext} from 'react'; +import { + View, + Text, + SafeAreaView, + ScrollView, + StyleSheet, + Switch, + TouchableOpacity, + Keyboard, +} from 'react-native'; +import { + COLOR_WHITE, + COLOR_BACKGROUND, + COLOR_GRAY, + COLOR_PRIMARY, + COLOR_TEXT_BLACK, + COLOR_TEXT70GRAY, + COLOR_TEXT60GRAY, +} from '../../assets/color'; +import AnimatedButton from '../../components/AnimationButton'; +import Header from '../../components/Header'; +import {useNavigation} from '@react-navigation/native'; +import MapView, {PROVIDER_GOOGLE, Marker} from 'react-native-maps'; +import {BlurView} from '@react-native-community/blur'; +import {SvgXml} from 'react-native-svg'; +import {svgXml} from '../../assets/svg'; +import MapDart from '../../components/MapDart'; +import Modal from 'react-native-modal'; +import {Dimensions} from 'react-native'; +import {FlatList, TextInput} from 'react-native-gesture-handler'; +import StoreCompo from '../../components/StoreCompo'; +import axios, {AxiosError} from 'axios'; +import {API_URL, AUTO_COMPLETE} from '@env'; +import AppContext from '../../components/AppContext'; + +const windowWidth = Dimensions.get('window').width; + +export default function SearchScreen(props) { + const navigation = useNavigation(); + const context = useContext(AppContext); + + const {route} = props; + const setSearch = route.params?.setSearch; + + const [searchText, setSearchText] = useState(''); + const [recentSearch, setRecentSearch] = useState([]); + + const [autoCompleteData, setAutoCompleteData] = useState([]); + + useEffect(() => { + initRecentSearch(); + }, []); + + const autocomplete = async inputString => { + console.log('검색어:', inputString); + try { + const params = { + query: inputString, + }; + + const queryString = new URLSearchParams(params).toString(); + + const response = await axios.get(`${AUTO_COMPLETE}?${queryString}`, { + headers: {Authorization: `Bearer ${context.accessToken}`}, + }); + + console.log('response:', response.data.results); + + setAutoCompleteData(response.data.results); + } catch (e) { + console.log('error', e); + } + }; + + const initRecentSearch = async () => { + try { + const response = await axios.get(`${API_URL}/v1/recents`, { + headers: {Authorization: `Bearer ${context.accessToken}`}, + }); + + console.log('response:', response.data.data.recentQueries); + + setRecentSearch(response.data.data.recentQueries); + } catch (e) { + if (axios.isAxiosError(e)) { + console.log('Axios error:', e.response ? e.response.data : e.message); + } else { + console.log('splash error', e.message); + } + } + }; + + const deleteRecentSearch = async query => { + try { + console.log('context.accessToken:', context.accessToken); + + const response = await axios.delete(`${API_URL}/v1/recents`, { + headers: {Authorization: `Bearer ${context.accessToken}`}, + data: {query: query}, + }); + + console.log('response:', response.data); + } catch (e) { + if (axios.isAxiosError(e)) { + console.log('Axios error:', e.response ? e.response.data : e.message); + } else { + console.log('splash error', e.message); + } + } + }; + + return ( + <> +
+ + + {/* 검색창 */} + + { + setSearch(searchText); + navigation.goBack(); + }} + style={{ + padding: 8, + }}> + + + { + setSearchText(text); + autocomplete(text); + }} + blurOnSubmit={false} + maxLength={200} + value={searchText} + onSubmitEditing={() => { + setSearch(searchText); + navigation.goBack(); + }} + textAlignVertical="center" + autoCapitalize="none" + autoComplete="off" + autoCorrect={false} + numberOfLines={1} + /> + + + + {/* 최근 검색어 부분 */} + {searchText.length == 0 ? ( + + + + 최근 검색어 + + { + console.log('최근 검색어 전부 삭제'); + setRecentSearch([]); + + for (let i = 0; i < recentSearch.length; i++) { + deleteRecentSearch(recentSearch[i].query); + } + }} + style={{marginLeft: 10}}> + + 전체 삭제 + + + + + + + + {recentSearch.map((item, index) => { + return ( + <> + { + console.log('press item:', item.query); + setSearch(item.query); + navigation.goBack(); + }}> + {item.query} + { + console.log('삭제'); + deleteRecentSearch(item.query); + setRecentSearch(prevQueries => + prevQueries.filter(q => q.query !== item.query), + ); + }}> + + + + + + + ); + })} + + + + ) : ( + + { + return ( + { + setSearch(item.org_display); + navigation.goBack(); + }}> + + {item.org_display} + + ); + }} + /> + + )} + + + ); +} + +const styles = StyleSheet.create({ + entire: { + flex: 1, + backgroundColor: COLOR_BACKGROUND, + alignItems: 'center', + }, + textInput: { + marginLeft: 10, + flex: 1, + fontSize: 12, + color: COLOR_TEXT_BLACK, + padding: 0, + }, + recentHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginHorizontal: 16, + width: windowWidth - 32, + // backgroundColor: 'blue', + }, + filterButton: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: 3, + paddingHorizontal: 7, + borderRadius: 15, + backgroundColor: COLOR_PRIMARY, + height: 24, + }, + recentText: { + fontSize: 12, + color: COLOR_WHITE, + marginRight: 5, + }, + searchArea: { + // backgroundColor: 'blue', + flex: 1, + width: windowWidth, + }, + touchArea: { + // backgroundColor: 'blue', + flex: 1, + width: windowWidth, + }, + listButton: { + // backgroundColor: 'blue', + padding: 8, + paddingHorizontal: 16, + flexDirection: 'row', + }, + buttonText: { + fontSize: 12, + color: COLOR_TEXT_BLACK, + fontWeight: 'normal', + marginLeft: 2, + }, +}); diff --git a/src/screens/detail/StoreDetailScreen.js b/src/screens/detail/StoreDetailScreen.js index d2bce8c..44a14dd 100644 --- a/src/screens/detail/StoreDetailScreen.js +++ b/src/screens/detail/StoreDetailScreen.js @@ -22,8 +22,12 @@ import { COLOR_BACKGROUND, COLOR_GRAY, COLOR_PRIMARY, + COLOR_TEXT_DARKGRAY, COLOR_TEXT70GRAY, + COLOR_TEXT60GRAY, COLOR_TEXT_BLACK, + COLOR_LIGHTGRAY, + COLOR_ORANGE, } from '../../assets/color'; import AnimatedButton from '../../components/AnimationButton'; import {useNavigation} from '@react-navigation/native'; @@ -34,39 +38,551 @@ import AppContext from '../../components/AppContext'; import axios from 'axios'; import {API_URL} from '@env'; import {Dimensions} from 'react-native'; -import TodayPick from '../../components/TodayPick'; -import FoodCategory from '../../components/FoodCategory'; -import KingoPass from '../../components/KingoPass'; +import ImageModal from 'react-native-image-modal'; +import { Modal, TouchableHighlight } from 'react-native'; const windowWidth = Dimensions.get('window').width; export default function StoreDetailScreen(props) { const navigation = useNavigation(); - const {route} = props; - const storeData = route.params?.data; + const context = useContext(AppContext); + const { route } = props; + const restaurantId = route.params?.data?.id || 1; + const [restaurant, setRestaurant] = useState(null); + const [isHearted, setIsHearted] = useState(false); + const [heartCount, setHeartCount] = useState(0); + const [menuList, setMenuList] = useState([]); + const [reviewList, setReviewList] = useState([]); + const [displayedMenuList, setDisplayedMenuList] = useState([]); + const [displayedReviewList, setDisplayedReviewList] = useState([]); + const [menuCount, setMenuCount] = useState(4); + const [reviewCount, setReviewCount] = useState(4); + const [modalVisible, setModalVisible] = useState(false); - console.log('storeData:', storeData); + useEffect(() => { + restaurantDetail(); + handleHeartPress(); + }, []); - return ( - <> -
- - {storeData.name} - { - navigation.navigate('ReviewWrite', {data: storeData}); - }}> - 리뷰쓰기버튼 - - - - ); -} + useEffect(() => { + setDisplayedMenuList(menuList.slice(0, menuCount)); + }, [menuList, menuCount]); -const styles = StyleSheet.create({ - entire: { - backgroundColor: COLOR_BACKGROUND, - // justifyContent: 'center', - alignItems: 'center', - }, -}); + useEffect(() => { + setDisplayedReviewList(reviewList.slice(0, reviewCount)); + }, [reviewList, reviewCount]); + + const restaurantDetail = async () => { + console.log('Id: ', restaurantId); + try { + const response = await axios.get(`${API_URL}/v1/restaurants/${restaurantId}`, { + headers: { Authorization: `Bearer ${context.accessToken}` }, + }); + const responseReview = await axios.get(`${API_URL}/v1/restaurants/${restaurantId}/reviews`, { + headers: { Authorization: `Bearer ${context.accessToken}` }, + }); + const data = response.data.data; + const dataReview = responseReview.data.data; + + console.log('data: ', data.restaurant.isLike); + console.log('review: ', dataReview); + + setRestaurant(data); + setIsHearted(data.restaurant.isLike); + setHeartCount(data.restaurant.likeCount); + setMenuList(data.restaurant.detailInfo.menus); + setReviewList(dataReview.reviews.content); + } catch (error) { + console.error('Error fetching restaurant details:', error); + } + }; + + const handleHeartPress = async () => { + try { + const newHeartedState = !isHearted; + setIsHearted(newHeartedState); + setHeartCount(newHeartedState ? heartCount + 1 : heartCount - 1); + + await axios.post( + `${API_URL}/v1/restaurants/${restaurantId}/like`, + { + isLike: newHeartedState, + }, + { + headers: { Authorization: `Bearer ${context.accessToken}` }, + }, + ); + + console.log('isLike: ', newHeartedState); + } catch (error) { + console.error('Error updating heart count:', error); + setIsHearted(!newHeartedState); + setHeartCount(newHeartedState ? heartCount - 1 : heartCount + 1); + } + }; + + const handleLoadMoreMenus = () => { + setMenuCount(menuCount + 4); + }; + + const handleLoadMoreReviews = () => { + setReviewCount(reviewCount + 4); + }; + + if (!restaurant) { + return Loading...; + } + + const renderMenuItem = ({item}) => ( + <> + + {item.imageUrl ? ( + + ) : ( + + (빈 이미지) + + )} + + {item.name} + {item.description} + {item.price} 원 + + + + + ); + + const renderReviewItem = ({item}) => { + const rating = item.rating; + const likeCount = item.likeCount; + const viewCount = item.viewCount; + + const renderStars = () => { + const stars = []; + for (let i = 0; i < 5; i++) { + stars.push( + + ); + } + return stars; + }; + + return ( + <> + + + + + {item.profileImageUrl ? ( + + ) : ( + + )} + {item.username} + + + + {`좋아요 ${likeCount}`} + {renderStars()} + + + {item.content} + + + ( + + )} + keyExtractor={(image, index) => `${item.id}-${index}`} + /> + + + ); + }; + + const ListHeader = () => ( + <> + + + + + + + + + {restaurant.restaurant.name} + {restaurant.restaurant.categories} + + + 찜 {heartCount} + · + 리뷰 {restaurant.restaurant.reviewCount} + + + + + setModalVisible(true)} + > + + 전화 + + + + + + + + + + {restaurant.restaurant.ratingAvg.toFixed(1)} + + + { + navigation.navigate('ReviewWrite', { data: restaurant.restaurant }); + }}> + + 리뷰 + + + + + + + 위치: {restaurant.restaurant.detailInfo.address} + + + + + + 전화번호: {restaurant.restaurant.detailInfo.contactNumber} + + + + + + 메뉴 + {menuList.length} + + item.name} + /> + {menuCount < menuList.length && ( + + 메뉴 더보기 + + )} + + + + 리뷰 + {reviewList.length} + + item.id} + ListFooterComponent={ + reviewCount < reviewList.length && ( + + 리뷰 더보기 + + ) + } + /> + + + + ); + + return ( + <> +
+ + { + setModalVisible(!modalVisible); + }} + > + + 전화번호: {restaurant.restaurant.detailInfo.contactNumber} + { + setModalVisible(!modalVisible); + }} + > + 닫기 + + + + + ); + } + + const styles = StyleSheet.create({ + entire: { + backgroundColor: COLOR_BACKGROUND, + alignItems: 'center', + marginHorizontal: -3, + }, + storeImageContainer: { + width: '100%', + height: 240, + }, + storeImage: { + width: '100%', + height: '100%', + }, + storeInfo: { + width: '100%', + padding: 16, + backgroundColor: COLOR_WHITE, + elevation: 3, + marginBottom: 16, + }, + storeHeader: { + alignItems: 'right', + }, + storeName: { + fontSize: 24, + color: COLOR_TEXT_BLACK, + fontWeight: 'bold', + marginVertical: 6, + }, + storeCategory: { + fontSize: 16, + color: COLOR_TEXT_DARKGRAY, + marginVertical: 10, + }, + storeReview: { + fontSize: 15, + color: COLOR_TEXT70GRAY, + marginBottom: 16, + marginRight: 6, + }, + contactContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + width: '100%', + marginVertical: 8, + paddingHorizontal: 16, + }, + contactButton: { + alignItems: 'center', + }, + contactButtonIcon: { + marginVertical: 6, + marginHorizontal: 2, + }, + contactButtonText: { + fontSize: 15, + marginTop: 4, + color: COLOR_TEXT70GRAY, + }, + verticalDivider: { + width: 1, + height: '100%', + backgroundColor: COLOR_LIGHTGRAY, + marginHorizontal: 16, + }, + horizontalDivider: { + width: '100%', + height: 1, + backgroundColor: COLOR_LIGHTGRAY, + }, + storeAddress: { + fontSize: 15, + color: COLOR_TEXT_DARKGRAY, + marginVertical: 8, + }, + storeHours: { + fontSize: 15, + color: COLOR_TEXT_DARKGRAY, + marginVertical: 7, + }, + storePhoneNum: { + fontSize: 15, + color: COLOR_TEXT_DARKGRAY, + marginVertical: 7, + }, + section: { + width: '92%', + backgroundColor: COLOR_WHITE, + borderRadius: 10, + padding: 12, + marginBottom: 16, + shadowColor: COLOR_TEXT_BLACK, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + elevation: 4, + }, + sectionTitle: { + flexDirection: 'row', + }, + sectionTitleText: { + marginTop: 2, + fontSize: 20, + fontWeight: 'bold', + color: COLOR_TEXT_BLACK, + marginRight: 5, + }, + sectionTitleNumText: { + marginTop: 2, + fontSize: 20, + fontWeight: 'bold', + color: COLOR_GRAY, + }, + sectionItem: { + flexDirection: 'row', + }, + menuImage: { + width: 90, + height: 90, + borderRadius: 12, + marginVertical: 12, + }, + menuImagePlaceholder: { + width: 90, + height: 90, + borderRadius: 12, + marginVertical: 12, + alignItems: 'center', + justifyContent: 'center', + }, + menuImagePlaceholderText: { + color: COLOR_LIGHTGRAY, + fontSize: 16, + }, + menuTextContainer: { + marginLeft: 10, + justifyContent: 'center', + }, + menuTitle: { + fontSize: 18, + color: COLOR_TEXT_BLACK, + fontWeight: '500', + }, + menuDescription: { + fontSize: 16, + color: COLOR_TEXT_DARKGRAY, + }, + menuPrice: { + fontSize: 17, + color: COLOR_TEXT_BLACK, + fontWeight: '600', + }, + reviewTextContainer: { + justifyContent: 'center', + marginVertical: 12, + }, + reviewAuthor: { + fontSize: 17, + color: COLOR_TEXT_BLACK, + marginRight: 4, + }, + reviewAuthor2: { + fontSize: 12, + color: COLOR_GRAY, + }, + reviewAuthorImage: { + width: 30, + height: 30, + borderRadius: 25, + }, + reviewAuthorImagePlaceholder: { + width: 30, + height: 30, + borderRadius: 25, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: COLOR_LIGHTGRAY, + marginRight: 6, + }, + reviewDate: { + fontSize: 12, + color: COLOR_TEXT_DARKGRAY, + marginVertical: 4, + }, + reviewStats: { + fontSize: 12, + color: COLOR_TEXT60GRAY, + marginRight: 6, + }, + reviewText: { + fontSize: 15, + color: COLOR_TEXT_BLACK, + marginTop: 6, + }, + reviewImage: { + width: 80, + height: 80, + borderRadius: 12, + margin: 7, + }, + loadMoreText: { + textAlign: 'center', + color: COLOR_PRIMARY, + fontSize: 16, + marginTop: 10, + }, + modalView: { + margin: 20, + backgroundColor: "white", + borderRadius: 20, + padding: 35, + alignItems: "center", + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + closeButton: { + backgroundColor: COLOR_PRIMARY, + borderRadius: 20, + padding: 10, + elevation: 2, + }, + textStyle: { + color: "white", + fontWeight: "bold", + textAlign: "center", + }, + modalText: { + marginBottom: 15, + textAlign: "center", + }, + }); diff --git a/src/screens/home/HomeScreen.js b/src/screens/home/HomeScreen.js index b0e7839..1e2741f 100644 --- a/src/screens/home/HomeScreen.js +++ b/src/screens/home/HomeScreen.js @@ -44,103 +44,7 @@ export default function HomeScreen() { const navigation = useNavigation(); const context = useContext(AppContext); - const [todaysPick, setTodaysPick] = useState([ - { - categories: [], - detailInfo: { - address: '경기 수원시 장안구 화산로233번길 46 1층', - contactNumber: '031-293-9294', - menus: [Array], - operatingInfos: [Array], - }, - discountContent: '', - id: 1, - isLike: false, - likeCount: 0, - name: '목구멍 율전점', - operatingEndTime: '', - operatingStartTime: '', - ratingAvg: 0, - representativeImageUrl: - 'https://search.pstatic.net/common/?autoRotate=true&type=w560_sharpen&src=https://ldb-phinf.pstatic.net/20230615_253/1686790793946ISiOc_JPEG/3%B9%F8.jpg', - representativeMenu: { - description: '시원 상큼한 하이볼3종', - imageUrl: - 'https://search.pstatic.net/common/?autoRotate=true&quality=95&type=f320_320&src=https://ldb-phinf.pstatic.net/20230525_34/16849976756701d50P_JPEG/Screenshot_20230525_095732_Samsung_Internet.jpg', - isRepresentative: true, - name: '하이볼(레몬, 자몽, 얼그레이)', - price: 7000, - }, - representativeReviewContent: null, - reviewCount: 0, - }, - { - categories: [], - detailInfo: { - address: '경기 수원시 장안구 율전로98번길 9', - contactNumber: '0507-1479-8592', - menus: [Array], - operatingInfos: [Array], - }, - discountContent: '', - id: 2, - isLike: false, - likeCount: 0, - name: '고기굽는교실 율전3반', - operatingEndTime: '', - operatingStartTime: '', - ratingAvg: 0, - representativeImageUrl: - 'https://search.pstatic.net/common/?autoRotate=true&type=w560_sharpen&src=https://ldb-phinf.pstatic.net/20230915_138/1694761539253MqBUj_JPEG/temp_file.jpg', - representativeMenu: null, - representativeReviewContent: null, - reviewCount: 0, - }, - { - categories: [], - detailInfo: { - address: '경기 수원시 장안구 율전로108번길 11 1층', - contactNumber: '0507-1460-0903', - menus: [Array], - operatingInfos: [Array], - }, - discountContent: '', - id: 3, - isLike: false, - likeCount: 0, - name: '봉수육', - operatingEndTime: '', - operatingStartTime: '', - ratingAvg: 0, - representativeImageUrl: - 'https://search.pstatic.net/common/?autoRotate=true&type=w560_sharpen&src=https://ldb-phinf.pstatic.net//20170607_114/1496834895537QbEYi_JPEG/IMG_0230.', - representativeMenu: null, - representativeReviewContent: null, - reviewCount: 0, - }, - { - categories: [], - detailInfo: { - address: '경기 수원시 장안구 율전로108번길 9 율전미주타운 1층 102호', - contactNumber: '0507-1315-4231', - menus: [Array], - operatingInfos: [Array], - }, - discountContent: '음료 한정 테이크 아웃 30% 가격 할인', - id: 4, - isLike: false, - likeCount: 0, - name: '자명문', - operatingEndTime: '', - operatingStartTime: '', - ratingAvg: 0, - representativeImageUrl: - 'https://search.pstatic.net/common/?autoRotate=true&type=w560_sharpen&src=https://ldb-phinf.pstatic.net/20201124_92/16062108769605dpF3_JPEG/xSKJpp_WcxT4xlbe8Jsq6g-O.jpeg.jpg', - representativeMenu: null, - representativeReviewContent: null, - reviewCount: 0, - }, - ]); + const [todaysPick, setTodaysPick] = useState([]); const [kingoPassData, setkingoPassData] = useState([]); @@ -177,9 +81,9 @@ export default function HomeScreen() { headers: {Authorization: `Bearer ${context.accessToken}`}, }); - console.log('response:', response.data); + console.log('response:', response.data.data.restaurants); - // setTodaysPick(response.data.data.restaurants.content); + setTodaysPick(response.data.data.restaurants); } catch (e) { console.log('error', e); } diff --git a/src/screens/list/ListMainScreen.js b/src/screens/list/ListMainScreen.js index 7226f75..7cdae01 100644 --- a/src/screens/list/ListMainScreen.js +++ b/src/screens/list/ListMainScreen.js @@ -59,7 +59,12 @@ export default function ListMainScreen() { const [sort, setSort] = useState('기본 순'); const [selectSale, setSelectSale] = useState(false); const [likedStore, setLikedStore] = useState(false); + const [search, setSearch] = useState(''); + const [myLocation, setMyLocation] = useState({ + latitude: 37.297861, + longitude: 126.971458, + }); const [pageNumber, setPageNumber] = useState(0); const catrgory = [ @@ -86,6 +91,7 @@ export default function ListMainScreen() { const getStoreDatas = async p => { try { // console.log('context.accessToken:', context.accessToken); + setPageNumber(p + 1); let discountForSkku = false; @@ -103,7 +109,6 @@ export default function ListMainScreen() { const params = { discountForSkku: discountForSkku, like: like, - sort: 'BASIC', page: pageNumber, }; @@ -188,6 +193,30 @@ export default function ListMainScreen() { break; } + switch (sort) { + case '가까운 순': + params.customSort = 'CLOSELY_DESC'; + params.latitude = myLocation.latitude; + params.longitude = myLocation.longitude; + break; + case '평점 높은 순': + params.customSort = 'RATING_DESC'; + break; + case '댓글 많은 순': + params.customSort = 'REVIEW_COUNT_DESC'; + break; + case '찜 많은 순': + params.customSort = 'LIKE_COUNT_DESC'; + break; + case '기본 순': + params.customSort = 'BASIC'; + break; + } + + if (search !== '') { + params.query = search; + } + const queryString = new URLSearchParams(params).toString(); const response = await axios.get( @@ -197,7 +226,7 @@ export default function ListMainScreen() { }, ); - console.log('response:', response.data.data.restaurants.content[0]); + // console.log('response:', response.data.data.restaurants.content[0]); if (p == 0) { setStoreDartDatas(response.data.data.restaurants.content); @@ -214,6 +243,9 @@ export default function ListMainScreen() { const onEndReached = () => { console.log('onEndReached', pageNumber); + if (pageNumber === 0) { + return; + } getStoreDatas(pageNumber); }; @@ -234,6 +266,8 @@ export default function ListMainScreen() { sort, storeScoreNaver, replyNumNaver, + myLocation, + search, ]); const listHeader = () => { @@ -255,8 +289,8 @@ export default function ListMainScreen() { justifyContent: 'center', }} onPress={() => { - //TODO: 리스트화면에도 검색 화면 추가하기 - // navigation.navigate('Search'); + setSearch(''); + navigation.navigate('Search', {setSearch: setSearch}); }}> - {'율전의 맛집은 과연 어디?'} + {search !== '' ? ( + {search} + ) : ( + + {'율전의 맛집은 과연 어디?'} + + )} @@ -709,6 +749,7 @@ export default function ListMainScreen() { '댓글 많은 순', '찜 많은 순', ]} + setLocation={setMyLocation} /> {/* 댓글수 모달 */} diff --git a/src/screens/map/MapScreen.js b/src/screens/map/MapScreen.js index ea6be20..5a65dac 100644 --- a/src/screens/map/MapScreen.js +++ b/src/screens/map/MapScreen.js @@ -73,6 +73,7 @@ export default function MapScreen() { const [selectSale, setSelectSale] = useState(false); const [likedStore, setLikedStore] = useState(false); + const [search, setSearch] = useState(''); const closeStoreModalVisible = () => { setStoreModalVisible(false); @@ -224,6 +225,10 @@ export default function MapScreen() { break; } + if (search !== '') { + params.query = search; + } + const queryString = new URLSearchParams(params).toString(); const response = await axios.get( @@ -256,6 +261,7 @@ export default function MapScreen() { likedStore, storeScoreNaver, replyNumNaver, + search, ]); return ( @@ -306,7 +312,8 @@ export default function MapScreen() { justifyContent: 'center', }} onPress={() => { - navigation.navigate('Search'); + setSearch(''); + navigation.navigate('Search', {setSearch: setSearch}); }}> - {'율전의 맛집은 과연 어디?'} + {search !== '' ? ( + {search} + ) : ( + + {'율전의 맛집은 과연 어디?'} + + )} diff --git a/src/screens/map/SearchScreen.js b/src/screens/map/SearchScreen.js deleted file mode 100644 index 92b6f72..0000000 --- a/src/screens/map/SearchScreen.js +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-disable react/self-closing-comp */ -/* eslint-disable react-native/no-inline-styles */ -import React, {useState, useCallback, useEffect} from 'react'; -import { - View, - Text, - SafeAreaView, - ScrollView, - StyleSheet, - Switch, -} from 'react-native'; -import { - COLOR_WHITE, - COLOR_BACKGROUND, - COLOR_GRAY, - COLOR_PRIMARY, - COLOR_TEXT_BLACK, - COLOR_TEXT70GRAY, - COLOR_TEXT60GRAY, -} from '../../assets/color'; -import AnimatedButton from '../../components/AnimationButton'; -import Header from '../../components/Header'; -import {useNavigation} from '@react-navigation/native'; -import MapView, {PROVIDER_GOOGLE, Marker} from 'react-native-maps'; -import {BlurView} from '@react-native-community/blur'; -import {SvgXml} from 'react-native-svg'; -import {svgXml} from '../../assets/svg'; -import MapDart from '../../components/MapDart'; -import Modal from 'react-native-modal'; -import {Dimensions} from 'react-native'; -import {TextInput} from 'react-native-gesture-handler'; -import StoreCompo from '../../components/StoreCompo'; - -const windowWidth = Dimensions.get('window').width; - -export default function SearchScreen() { - const navigation = useNavigation(); - - const [searchText, setSearchText] = useState(''); - - //TODO: 검색어를 받아와서 검색하는 함수 - function searchStore(inputString) { - console.log('검색어:', inputString); - } - - return ( - <> -
- - - {/* 검색창 */} - - - { - searchStore(searchText); - }} - style={{ - padding: 8, - }}> - - - {}} - placeholder={'율전의 맛집은 과연 어디?'} - placeholderTextColor={'#888888'} - style={styles.textInput} - onChangeText={text => { - setSearchText(text); - }} - blurOnSubmit={false} - maxLength={200} - value={searchText} - onSubmitEditing={() => { - console.log('검색 제출'); - searchStore(searchText); - }} - // multiline={true} - textAlignVertical="center" - autoCapitalize="none" - autoComplete="off" - autoCorrect={false} - numberOfLines={1} - /> - - - - - - ); -} - -const styles = StyleSheet.create({ - entire: { - flex: 1, - backgroundColor: COLOR_BACKGROUND, - alignItems: 'center', - }, - textInput: { - marginLeft: 10, - flex: 1, - fontSize: 12, - color: COLOR_TEXT_BLACK, - padding: 0, - }, -}); diff --git a/src/screens/signup/ProfileSetScreen.js b/src/screens/signup/ProfileSetScreen.js index 31d6a9c..2189e1f 100644 --- a/src/screens/signup/ProfileSetScreen.js +++ b/src/screens/signup/ProfileSetScreen.js @@ -58,7 +58,7 @@ export default function ProfileSetScreen(props) { // 회원가입 하고 토큰 저장하는 부분 const response = await axios.post(`${API_URL}/v1/users/email/sign-up`, { email: signUpData.email, - nickname: signUpData.name, + nickname: signUpData.nickname, password: signUpData.password, profileImageUrl: profileImage, }); diff --git a/src/screens/signup/SplashScreen.js b/src/screens/signup/SplashScreen.js index 5ad7252..c0d8bee 100644 --- a/src/screens/signup/SplashScreen.js +++ b/src/screens/signup/SplashScreen.js @@ -1,5 +1,12 @@ import React, {useState, useCallback, useEffect, useContext} from 'react'; -import {View, Text, SafeAreaView, ScrollView, StyleSheet} from 'react-native'; +import { + View, + Text, + SafeAreaView, + ScrollView, + StyleSheet, + Image, +} from 'react-native'; import { COLOR_WHITE, COLOR_BACKGROUND, @@ -67,7 +74,11 @@ export default function SplashScreen() { //이 함수형 컴포넌트가 화면에 보여지는 부분 return ( - 먹구스꾸 + + {'맛있는 음식을'} + {' 먹구스꾸'} + + @@ -82,12 +93,18 @@ const styles = StyleSheet.create({ backgroundColor: COLOR_BACKGROUND, alignItems: 'center', }, - textMain: { - fontSize: 35, - color: COLOR_TEXT70GRAY, + textMainColor: { + fontSize: 33, + color: '#A4D65E', fontWeight: 'bold', textAlign: 'center', - marginTop: 76, + }, + textMain: { + fontSize: 33, + fontFamily: 'NotoSansKR-Regular', + color: COLOR_PRIMARY, + fontWeight: 'normal', + textAlign: 'center', }, buttonContainer: { position: 'absolute', @@ -96,4 +113,9 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + image: { + width: 400, + height: 400, + marginTop: 25, + }, });