diff --git a/src/assets/color.js b/src/assets/color.js
index 95c667e..c1fb22b 100644
--- a/src/assets/color.js
+++ b/src/assets/color.js
@@ -9,3 +9,4 @@ export const COLOR_TEXT70GRAY = '#4D4D4D';
export const COLOR_TEXT60GRAY = '#666666';
export const COLOR_NAVY = '#003C71';
export const COLOR_LIGHTGRAY = '#dddddd';
+export const COLOR_ORANGE = '#FF6A13';
\ No newline at end of file
diff --git a/src/assets/svg.js b/src/assets/svg.js
index cbb347c..f68d93f 100644
--- a/src/assets/svg.js
+++ b/src/assets/svg.js
@@ -196,12 +196,50 @@ export const svgXml = {
`,
+ starGrey: `
+
+ `,
+ heartGrey: `
+
+ `,
+ emptyHeartGrey: `
+
+ `,
camera: `
+ `,
+ phone: `
+
+ `,
+ pen: `
+
+ `,
+ location: `
+
+ `,
+ clock: `
+
`,
emptyStar: `
diff --git a/src/screens/detail/ReviewWriteScreen.js b/src/screens/detail/ReviewWriteScreen.js
index 39d1634..63e7a27 100644
--- a/src/screens/detail/ReviewWriteScreen.js
+++ b/src/screens/detail/ReviewWriteScreen.js
@@ -27,6 +27,7 @@ import {
COLOR_TEXT70GRAY,
COLOR_TEXT_BLACK,
COLOR_LIGHTGRAY,
+ COLOR_ORANGE,
} from '../../assets/color';
import AnimatedButton from '../../components/AnimationButton';
import {useNavigation} from '@react-navigation/native';
@@ -35,22 +36,112 @@ import {svgXml} from '../../assets/svg';
import Header from '../../components/Header';
import AppContext from '../../components/AppContext';
import axios from 'axios';
-import {API_URL} from '@env';
+import { API_URL, IMG_URL } from '@env';
import {Dimensions} from 'react-native';
import TodayPick from '../../components/TodayPick';
import FoodCategory from '../../components/FoodCategory';
import KingoPass from '../../components/KingoPass';
+import ImagePicker from 'react-native-image-crop-picker';
+import RNFS from 'react-native-fs'
const windowWidth = Dimensions.get('window').width;
export default function ReviewWriteScreen(props) {
const navigation = useNavigation();
- const {route} = props;
+ const context = useContext(AppContext);
+ const { route } = props;
const storeData = route.params?.data;
const [rating, setRating] = useState(0);
+ const [reviewContent, setReviewContent] = useState('');
+ const [showRatingError, setShowRatingError] = useState(false);
+ const [showContentError, setShowContentError] = useState(false);
+ const [showImageError, setShowImageError] = useState(false);
+ const [reviewImage, setReviewImage] = useState([]);
console.log('storeData:', storeData);
+ const handleReviewSubmit = async () => {
+ 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/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",
+ },
+ });