diff --git a/packages/react-native/package.json b/packages/react-native/package.json index 2cf4ae3c..87797e2b 100644 --- a/packages/react-native/package.json +++ b/packages/react-native/package.json @@ -40,6 +40,7 @@ "react-native-gesture-handler": "^2.17.1", "react-native-image-picker": "^7.1.2", "react-native-linear-gradient": "^2.8.3", + "react-native-mail": "^6.1.1", "react-native-pager-view": "^6.3.3", "react-native-permissions": "^4.1.5", "react-native-reanimated": "^3.14.0", diff --git a/packages/react-native/src/apis/queries/detail/useAroundSpotQuery.ts b/packages/react-native/src/apis/queries/detail/useAroundSpotQuery.ts index 262f8a03..832acf98 100644 --- a/packages/react-native/src/apis/queries/detail/useAroundSpotQuery.ts +++ b/packages/react-native/src/apis/queries/detail/useAroundSpotQuery.ts @@ -12,7 +12,7 @@ interface UseAroundSpotQueryParams { interface AroundSpotResponse { attraction: SpotResponse[]; restaurant: SpotResponse[]; - accomodation: SpotResponse[]; + accommodation: SpotResponse[]; } export default function useAroundSpotQuery({ diff --git a/packages/react-native/src/components/common/Card.tsx b/packages/react-native/src/components/common/Card.tsx index 465dcb82..574898c5 100644 --- a/packages/react-native/src/components/common/Card.tsx +++ b/packages/react-native/src/components/common/Card.tsx @@ -1,16 +1,24 @@ -import { Alert, ImageBackground, TouchableOpacity, View } from 'react-native'; +import { useState } from 'react'; +import { ImageBackground, TouchableOpacity, View } from 'react-native'; import HeartIcon from '@assets/HeartIcon'; import { useNavigation } from '@react-navigation/native'; import { Font } from 'design-system'; import { SpotCardData } from '@/types/spot'; import { StackNavigation } from '@/types/navigation'; import { getDisplayRegion } from '@/utils/getDisplayRegionName'; +import useSpotLikeMutation from '@/apis/mutations/useSpotLikeMutation'; +import MutationLoadingModal from './MutationLoadingModal'; interface CardProps { data: SpotCardData; size?: number; } +interface CardLike { + likeCount: number; + isLiked: boolean; +} + function Default({ data, size = 260 }: CardProps) { const { isLiked, @@ -23,7 +31,31 @@ function Default({ data, size = 260 }: CardProps) { id, workId, } = data; + + const [cardLike, setCardLike] = useState({ + isLiked, + likeCount, + }); const navigation = useNavigation>(); + const { like, isLikePending, isCancelLikePending, cancelLike } = + useSpotLikeMutation({ contentId }); + + const handleClickLike = () => { + if (cardLike.isLiked) { + setCardLike((prev) => ({ + isLiked: !prev.isLiked, + likeCount: prev.likeCount - 1, + })); + cancelLike({ id }); + return; + } + + setCardLike((prev) => ({ + isLiked: !prev.isLiked, + likeCount: prev.likeCount + 1, + })); + like({ id }); + }; return ( + @@ -41,17 +76,16 @@ function Default({ data, size = 260 }: CardProps) { Alert.alert('좋아요', `${id}`)} + onPress={handleClickLike} > - {likeCount} + {cardLike.likeCount} diff --git a/packages/react-native/src/components/common/SpotDetailBottomSheet.tsx b/packages/react-native/src/components/common/SpotDetailBottomSheet.tsx index 6216dff2..4e966ae6 100644 --- a/packages/react-native/src/components/common/SpotDetailBottomSheet.tsx +++ b/packages/react-native/src/components/common/SpotDetailBottomSheet.tsx @@ -20,7 +20,7 @@ export default function SpotDetailBottomSheet({ const { data } = useSpotDetailQuery({ id: selectedDetailSpotId }); return ( diff --git a/packages/react-native/src/constants/EMAIL_CONTENTS.ts b/packages/react-native/src/constants/EMAIL_CONTENTS.ts new file mode 100644 index 00000000..73c78322 --- /dev/null +++ b/packages/react-native/src/constants/EMAIL_CONTENTS.ts @@ -0,0 +1,26 @@ +const EMAIL_CONTENTS = { + BUG: { + title: '[버그 신고] SPOT!에 문의하기', + body: ` + 문제 발생 일시: + + 상세 내용: + + 문제 상황 이미지 및 영상 : + + * 상황에 대한 자세한 설명이 작성되어 있지 않은 경우 문제 해결 및 답변이 어려운 점 참고 부탁드립니다. + * 문의 관련 스크린샷 또는 녹화본을 첨부하시면 빠른 문제확인 및 해결이 가능합니다.`, + }, + + NORMAL: { + title: '[일반 문의] SPOT!에 문의하기', + body: ` + 문의할 내용: + + 관련 이미지 및 영상 : + + * 문의 관련 스크린샷 또는 녹화본을 첨부하시면 빠른 문제확인 및 해결이 가능합니다.`, + }, +} as const; + +export default EMAIL_CONTENTS; diff --git a/packages/react-native/src/hooks/useSortByStartDate.ts b/packages/react-native/src/hooks/useSortByStartDate.ts new file mode 100644 index 00000000..95c336c9 --- /dev/null +++ b/packages/react-native/src/hooks/useSortByStartDate.ts @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; + +type orderType = 'ascending' | 'descending'; + +interface ObjectWithStartDate { + startDate: string; +} + +interface UseSortParams { + defaultData: T; +} + +export default function useSortByStartDate({ + defaultData, +}: UseSortParams) { + const [data, setData] = useState(defaultData); + const [order, setOrder] = useState(); + + const sort = (type: orderType) => { + setData((prev) => { + const sortedData = [...prev].sort((a, b) => { + return type === 'ascending' + ? new Date(a.startDate).getTime() - new Date(b.startDate).getTime() + : new Date(b.startDate).getTime() - new Date(a.startDate).getTime(); + }); + return sortedData as T; + }); + }; + + const toggleSortOrder = (type?: orderType) => { + if (!type) { + setOrder((prev) => (prev === 'descending' ? 'ascending' : 'descending')); + return; + } + + setOrder(type); + }; + + useEffect(() => { + setData(defaultData); + if (order) { + sort(order); + } + }, [order, defaultData]); + + return { data, order, toggleSortOrder }; +} diff --git a/packages/react-native/src/pages/Detail.tsx b/packages/react-native/src/pages/Detail.tsx index d2118b59..908f32bb 100644 --- a/packages/react-native/src/pages/Detail.tsx +++ b/packages/react-native/src/pages/Detail.tsx @@ -1,6 +1,8 @@ import { Font } from 'design-system'; import { ImageBackground, TouchableOpacity, View } from 'react-native'; import { useNavigation, useRoute } from '@react-navigation/native'; +import { useHeaderHeight } from '@react-navigation/elements'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { StackNavigation, StackRouteProps } from '@/types/navigation'; import withSuspense from '@/components/HOC/withSuspense'; import DetailTabNavigator from '@/routes/DetailTabNavigator'; @@ -10,6 +12,7 @@ import useDetailQuery from '@/apis/queries/detail/useDetailQuery'; import useSpotLikeMutation from '@/apis/mutations/useSpotLikeMutation'; import MutationLoadingModal from '@/components/common/MutationLoadingModal'; import { getDisplayRegion } from '@/utils/getDisplayRegionName'; +import Header from '@/components/common/Header'; const Detail = withSuspense(() => { const route = useRoute>(); @@ -21,7 +24,6 @@ const Detail = withSuspense(() => { useSpotLikeMutation({ contentId: paramsContentId }); const { - image, title, longitude, latitude, @@ -29,20 +31,25 @@ const Detail = withSuspense(() => { addr1, addr2, overview, - posterUrl, likeCount, isLiked, city, region, dist, contentTypeId, + image, + posterUrl, } = data; + const headerHeight = useHeaderHeight(); + const insets = useSafeAreaInsets(); + + const defaultPaddingTop = + headerHeight - insets.top > 0 ? headerHeight - insets.top : 0; + const handleAddPlan = () => { navigation.navigate('Home/AddSpot', { spots: [ - // TODO: useDetailQuery 반환타입과 호환되지 않음 - // 추후 api응답 변경시 재변경 빌표 { contentId: Number(contentId), title, @@ -64,13 +71,22 @@ const Detail = withSuspense(() => { - + {(image || posterUrl) && ( + + + + )} + + - - - {title} diff --git a/packages/react-native/src/pages/Detail/DetailSpot.tsx b/packages/react-native/src/pages/Detail/DetailSpot.tsx index c9f83221..8b07b32c 100644 --- a/packages/react-native/src/pages/Detail/DetailSpot.tsx +++ b/packages/react-native/src/pages/Detail/DetailSpot.tsx @@ -91,7 +91,7 @@ export default withSuspense(function DetailSpot() { /> ( ; } export default withSuspense(function Records({ navigation }: RecordsProps) { - const [showBottomSheet, toggleBottomSheet] = useToggle(); - const [selectedRecord, setSelectedRecord] = useState(); - const sort = () => { - // TODO: 실제 구현 필요(현재 UI없음) - }; - const route = useRoute>(); - const { data: recordsData } = useRecordsQuery({ location: route.params.location, }); + const { data, toggleSortOrder } = useSortByStartDate({ + defaultData: recordsData, + }); const { deleteMutate } = useRecordMutation({ location: route.params.location, }); + const [showBottomSheet, toggleBottomSheet] = useToggle(); + const [selectedRecord, setSelectedRecord] = useState(); + const handleOpenOption = (selectedCardData: RecordResponse) => { setSelectedRecord(selectedCardData); toggleBottomSheet(true); @@ -50,7 +50,10 @@ export default withSuspense(function Records({ navigation }: RecordsProps) {
+ toggleSortOrder()} + className="px-4" + > } @@ -73,10 +76,7 @@ export default withSuspense(function Records({ navigation }: RecordsProps) { paddingRight: LOG_PADDING_X, }} > - + )} diff --git a/packages/react-native/src/pages/MyPage.tsx b/packages/react-native/src/pages/MyPage.tsx index 2810508b..77ce602d 100644 --- a/packages/react-native/src/pages/MyPage.tsx +++ b/packages/react-native/src/pages/MyPage.tsx @@ -52,9 +52,9 @@ export default withSuspense(function MyPage({ navigation }: MyPageProps) { > - + {/* - + */} ); diff --git a/packages/react-native/src/pages/Setting.tsx b/packages/react-native/src/pages/Setting.tsx index a377c202..c6d33727 100644 --- a/packages/react-native/src/pages/Setting.tsx +++ b/packages/react-native/src/pages/Setting.tsx @@ -1,10 +1,30 @@ +import { useEffect, useState } from 'react'; import { Font } from 'design-system'; import { TouchableOpacity, View } from 'react-native'; +import Mailer from 'react-native-mail'; import BackGroundGradient from '@/layouts/BackGroundGradient'; import Header from '@/components/common/Header'; +import { Agree } from './TOS'; +import TOSBottomSheet from '@/components/toc/TOSBottomSheet'; +import EMAIL_CONTENTS from '@/constants/EMAIL_CONTENTS'; -// FIXME: 실제 네비게이션 연결 export default function Setting() { + const [TOSType, setTOSType] = useState(); + const [mailType, setMailType] = useState(); + + useEffect(() => { + if (!mailType) return; + + Mailer.mail( + { + subject: EMAIL_CONTENTS[mailType].title, + recipients: ['alicee0047@gmail.com'], + body: EMAIL_CONTENTS[mailType].body, + }, + () => {}, + ); + }, [mailType]); + return (
@@ -29,17 +49,26 @@ export default function Setting() { gap: 4, }} > - + 앱 버전 정보 - - + + 1.0.0 + + + setTOSType('TOS')} + > 서비스 이용약관 - + setTOSType('privacyCollection')} + > 개인정보 취급 방침 @@ -66,17 +95,23 @@ export default function Setting() { gap: 4, }} > - + {/* 공지사항 - - + */} + setMailType('NORMAL')} + > 문의하기 - + setMailType('BUG')} + > 신고하기 @@ -89,6 +124,10 @@ export default function Setting() { + setTOSType(undefined)} + /> ); } diff --git a/packages/react-native/src/pages/TripPlanner/TripPlanner.tsx b/packages/react-native/src/pages/TripPlanner/TripPlanner.tsx index 042939cc..1ccd7854 100644 --- a/packages/react-native/src/pages/TripPlanner/TripPlanner.tsx +++ b/packages/react-native/src/pages/TripPlanner/TripPlanner.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { View, TouchableOpacity, Dimensions } from 'react-native'; import { Font, FloatingPlusButton } from 'design-system'; import { useNavigation } from '@react-navigation/native'; @@ -13,43 +13,22 @@ import { StackNavigation } from '@/types/navigation'; import withSuspense from '@/components/HOC/withSuspense'; import EmptyPlan from '@/components/tripPlan/EmptyPlan'; import TripPlannerBottomSheet from '@/components/tripPlan/TripPlannerBottomSheet'; - -type orderType = 'ascending' | 'descending'; +import useSortByStartDate from '@/hooks/useSortByStartDate'; const { height } = Dimensions.get('window'); export default withSuspense(function TripPlanner() { const { data: defaultData } = useTripPlansQuery(); - const [data, setData] = useState(defaultData); - const [order, setOrder] = useState('ascending'); const [selectedPlan, setSelectedPlan] = useState(); - + const { data, toggleSortOrder } = useSortByStartDate({ defaultData }); const navigation = useNavigation>(); - const sort = (type: orderType) => { - setData((prev) => - type === 'ascending' - ? prev.sort( - (a, b) => - new Date(b.startDate).getTime() - new Date(a.startDate).getTime(), - ) - : prev.sort( - (a, b) => - new Date(a.startDate).getTime() - new Date(b.startDate).getTime(), - ), - ); - }; - const handleClickCardOption = (selectedCardData: TripPlanResponse) => { setSelectedPlan(selectedCardData); }; const isEmpty = data.length === 0; - useEffect(() => { - sort(order); - }, [order]); - return ( @@ -66,13 +45,7 @@ export default withSuspense(function TripPlanner() { My Trip - - setOrder((prev) => - prev === 'ascending' ? 'descending' : 'ascending', - ) - } - > + toggleSortOrder()}> diff --git a/yarn.lock b/yarn.lock index 2ab2a97f..3cee8222 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14749,6 +14749,13 @@ __metadata: languageName: node linkType: hard +"react-native-mail@npm:^6.1.1": + version: 6.1.1 + resolution: "react-native-mail@npm:6.1.1" + checksum: 6d00aa02b5eb0d4826fb3bf0f6ca42969395fa1feb8c59631d3fbb2b0c3200a02b860745ce863ca9da4eb141026c824499b4eda4b7b48460a97ba5b72bc4cdb3 + languageName: node + linkType: hard + "react-native-pager-view@npm:^6.3.3": version: 6.3.3 resolution: "react-native-pager-view@npm:6.3.3" @@ -14984,6 +14991,7 @@ __metadata: react-native-gesture-handler: ^2.17.1 react-native-image-picker: ^7.1.2 react-native-linear-gradient: ^2.8.3 + react-native-mail: ^6.1.1 react-native-pager-view: ^6.3.3 react-native-permissions: ^4.1.5 react-native-reanimated: ^3.14.0