diff --git a/packages/react-native/src/apis/mutations/useAddTripPlan.ts b/packages/react-native/src/apis/mutations/useAddTripPlan.ts index c58187b1..9ac70ecb 100644 --- a/packages/react-native/src/apis/mutations/useAddTripPlan.ts +++ b/packages/react-native/src/apis/mutations/useAddTripPlan.ts @@ -49,7 +49,7 @@ export default function useAddTripPlan() { await queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.TRIP_PLANS], }); - navigate.navigate('TripPlanner/Main'); + navigate.goBack(); }, }); } diff --git a/packages/react-native/src/apis/queries/mypage/useBadgeHistoryQuery.ts b/packages/react-native/src/apis/queries/mypage/useBadgeHistoryQuery.ts new file mode 100644 index 00000000..26072bf4 --- /dev/null +++ b/packages/react-native/src/apis/queries/mypage/useBadgeHistoryQuery.ts @@ -0,0 +1,38 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import useAuthAxios from '@/apis/useAuthAxios'; +import { badgePath } from '@/components/common/Badge'; +import { BadgeAcquisition } from '@/constants/BADGE_ACQUISITION'; +import { City, Region, REVERSE_BADGE_MAPPER } from '@/constants/CITY'; +import QUERY_KEYS from '@/constants/QUERY_KEYS'; +import { ServerResponse } from '@/types/response'; + +interface BadgeHistoryResponse { + region: Region; + city: City; + acquisitionType: BadgeAcquisition; + createdAt: string; +} + +export default function useBadgeHistoryQuery({ + region, +}: { + region: keyof typeof badgePath; +}) { + const badgeRegions = REVERSE_BADGE_MAPPER[region]; + const params = Array.isArray(badgeRegions) + ? badgeRegions.join(',') + : badgeRegions; + const authAxios = useAuthAxios(); + + const getBadgeHistory = async () => { + const result = await authAxios.get>( + `/api/user/badge/acquisition?regions=${params}`, + ); + return result.data.result; + }; + + return useSuspenseQuery({ + queryKey: [QUERY_KEYS.BADGE_HISTORY, region], + queryFn: getBadgeHistory, + }); +} diff --git a/packages/react-native/src/apis/queries/mypage/useMyBadgeQuery.ts b/packages/react-native/src/apis/queries/mypage/useMyBadgeQuery.ts index 1285dab0..9f76d312 100644 --- a/packages/react-native/src/apis/queries/mypage/useMyBadgeQuery.ts +++ b/packages/react-native/src/apis/queries/mypage/useMyBadgeQuery.ts @@ -1,67 +1,27 @@ import { useSuspenseQuery } from '@tanstack/react-query'; import useAuthAxios from '@/apis/useAuthAxios'; -import { badgePath } from '@/components/common/Badge'; -import { Region } from '@/constants/CITY'; +import { BADGE_MAPPER, Region } from '@/constants/CITY'; import QUERY_KEYS from '@/constants/QUERY_KEYS'; import { ServerResponse } from '@/types/response'; +import unique from '@/utils/unique'; interface BadgeResponse { region: Region; count: number; } -const badgeMapper = (badge: BadgeResponse): keyof typeof badgePath => { - switch (badge.region) { - case Region.SEOUL: - return '서울'; - case Region.GYEONGGI: - return '경기'; - case Region.INCHEON: - return '인천'; - case Region.GANGWON: - return '강원'; - case Region.SEJONG: - return '세종'; - case Region.DAEJEON: - return '대전'; - case Region.GWANGJU: - return '광주'; - case Region.DAEGU: - return '대구'; - case Region.ULSAN: - return '울산'; - case Region.BUSAN: - return '부산'; - - case Region.CHUNGBUK: - case Region.CHUNGNAM: - return '충청'; - - case Region.GYEONGBUK: - case Region.GYEONGNAM: - return '경상'; - - case Region.JEONBUK: - case Region.JEONNAM: - return '전라'; - - case Region.JEJU: - default: - return '제주'; - } -}; - export default function useMyBadgeQuery() { const authAxios = useAuthAxios(); const getBadges = async () => { const result = await authAxios.get>('/api/user/badge'); + const badges = result.data.result.map((badge) => ({ count: badge.count, - badgeRegion: badgeMapper(badge), + badgeRegion: BADGE_MAPPER[badge.region], })); - return badges; + return unique(badges, 'badgeRegion'); }; return useSuspenseQuery({ diff --git a/packages/react-native/src/apis/queries/mypage/useMyLevelQuery.ts b/packages/react-native/src/apis/queries/mypage/useMyLevelQuery.ts new file mode 100644 index 00000000..1ff8a497 --- /dev/null +++ b/packages/react-native/src/apis/queries/mypage/useMyLevelQuery.ts @@ -0,0 +1,23 @@ +import { useSuspenseQuery } from '@tanstack/react-query'; +import useAuthAxios from '@/apis/useAuthAxios'; +import QUERY_KEYS from '@/constants/QUERY_KEYS'; +import { ServerResponse } from '@/types/response'; + +interface LevelResponse { + profileLevel: string; + description: string; +} + +export default function useMyLevelQuery() { + const authAxios = useAuthAxios(); + const getMyLevel = async () => { + const result = + await authAxios.get>('/api/user/level'); + return result.data.result; + }; + + return useSuspenseQuery({ + queryKey: [QUERY_KEYS.MY_LEVEL], + queryFn: getMyLevel, + }); +} diff --git a/packages/react-native/src/components/mypage/BadgeListBottomSheet.tsx b/packages/react-native/src/components/mypage/BadgeListBottomSheet.tsx index 9fb814da..86b947da 100644 --- a/packages/react-native/src/components/mypage/BadgeListBottomSheet.tsx +++ b/packages/react-native/src/components/mypage/BadgeListBottomSheet.tsx @@ -5,16 +5,8 @@ import { badgePath } from '../common/Badge'; import BadgeListItem from './BadgeListItem'; import BottomSheet from '../common/BottomSheet'; -// FIXME: 실제 데이터 받아오기: selectedBadge를 기준으로 데이터를 받아오면 됨 -const mockData = [ - { id: 1, title: '안동1', date: '2024. 08. 16.', content: 'SPOT! 퀴즈 정답' }, - { id: 2, title: '안동2', date: '2024. 08. 16.', content: 'SPOT! 퀴즈 정답' }, - { id: 3, title: '안동3', date: '2024. 08. 16.', content: 'SPOT! 퀴즈 정답' }, - { id: 4, title: '안동4', date: '2024. 08. 16.', content: 'SPOT! 퀴즈 정답' }, -]; - interface BadgeListBottomSheetProps { - selectedBadge?: keyof typeof badgePath; + selectedBadge: keyof typeof badgePath; onClose: () => void; } @@ -37,24 +29,7 @@ export default function BadgeListBottomSheet({ showsVerticalScrollIndicator={false} style={{ paddingHorizontal: 16 }} > - {selectedBadge && - mockData.map((badgeInfo, index) => ( - <> - - {index !== mockData.length - 1 && ( - - )} - - ))} + ); diff --git a/packages/react-native/src/components/mypage/BadgeListItem.tsx b/packages/react-native/src/components/mypage/BadgeListItem.tsx index 42a0369a..8ec3c8f0 100644 --- a/packages/react-native/src/components/mypage/BadgeListItem.tsx +++ b/packages/react-native/src/components/mypage/BadgeListItem.tsx @@ -2,37 +2,55 @@ import { Font } from 'design-system'; import { View } from 'react-native'; import Badge, { badgePath } from '../common/Badge'; import Spacing from '../common/Spacing'; +import withSuspense from '../HOC/withSuspense'; +import useBadgeHistoryQuery from '@/apis/queries/mypage/useBadgeHistoryQuery'; +import { getDisplayRegion } from '@/utils/getDisplayRegionName'; +import { ACQUISITION_MAPPER } from '@/constants/BADGE_ACQUISITION'; interface BadgeListItemProps { - location: keyof typeof badgePath; - title: string; - date: string; - content: string; + selectedBadge: keyof typeof badgePath; } -export default function BadgeListItem({ - location, - title, - date, - content, -}: BadgeListItemProps) { - return ( - - - +export default withSuspense( + function BadgeListItem({ selectedBadge }: BadgeListItemProps) { + const { data: badgeHistory } = useBadgeHistoryQuery({ + region: selectedBadge, + }); + + return badgeHistory.map((badgeInfo, index) => ( + + + + + + + {getDisplayRegion({ + locationEnum: badgeInfo.region, + cityEnum: badgeInfo.city, + onlyCity: true, + }) || ''} + + + + {badgeInfo.createdAt} + + + {ACQUISITION_MAPPER[badgeInfo.acquisitionType]} + + - - - {title} - - - - {date} + )); + }, + { + fallback: ( + + + 잠시만 - - {content} + + 기다려주세요 - - ); -} + ), + }, +); diff --git a/packages/react-native/src/constants/BADGE_ACQUISITION.ts b/packages/react-native/src/constants/BADGE_ACQUISITION.ts index 7c154c84..92995fbe 100644 --- a/packages/react-native/src/constants/BADGE_ACQUISITION.ts +++ b/packages/react-native/src/constants/BADGE_ACQUISITION.ts @@ -4,3 +4,9 @@ export enum BadgeAcquisition { BY_CAMERA_FILTER, BY_RECORD, } + +export const ACQUISITION_MAPPER: Record = { + [BadgeAcquisition.BY_QUIZ]: 'Spot! 퀴즈 정답', + [BadgeAcquisition.BY_CAMERA_FILTER]: 'Spot! 카메라 필터', + [BadgeAcquisition.BY_RECORD]: '소중한 여행 기록 작성', +}; diff --git a/packages/react-native/src/constants/CITY.ts b/packages/react-native/src/constants/CITY.ts index 11f40dfe..8284ea75 100644 --- a/packages/react-native/src/constants/CITY.ts +++ b/packages/react-native/src/constants/CITY.ts @@ -461,3 +461,23 @@ export const BADGE_MAPPER: Record = { [Region.BUSAN]: '부산', [Region.JEJU]: '제주', }; + +export const REVERSE_BADGE_MAPPER: Record< + keyof typeof badgePath, + Region | Region[] +> = { + 서울: Region.SEOUL, + 경기: Region.GYEONGGI, + 인천: Region.INCHEON, + 강원: Region.GANGWON, + 충청: [Region.CHUNGNAM, Region.CHUNGBUK], + 세종: Region.SEJONG, + 대전: Region.DAEJEON, + 전라: [Region.JEONNAM, Region.JEONBUK], + 광주: Region.GWANGJU, + 경상: [Region.GYEONGBUK, Region.GYEONGNAM], + 대구: Region.DAEGU, + 울산: Region.ULSAN, + 부산: Region.BUSAN, + 제주: Region.JEJU, +}; diff --git a/packages/react-native/src/constants/QUERY_KEYS.ts b/packages/react-native/src/constants/QUERY_KEYS.ts index bd5f24f4..8f469b28 100644 --- a/packages/react-native/src/constants/QUERY_KEYS.ts +++ b/packages/react-native/src/constants/QUERY_KEYS.ts @@ -15,6 +15,8 @@ const QUERY_KEYS = { SEARCH: 'search', MY_SPOTS: 'mySpots', MY_BADGES: 'myBadges', + MY_LEVEL: 'myLevel', + BADGE_HISTORY: 'badgeHistory', SCHEDULES: 'schedules', }; diff --git a/packages/react-native/src/pages/MyPage.tsx b/packages/react-native/src/pages/MyPage.tsx index c3d057c0..9d96e2ad 100644 --- a/packages/react-native/src/pages/MyPage.tsx +++ b/packages/react-native/src/pages/MyPage.tsx @@ -9,6 +9,7 @@ import { StackNavigation } from '@/types/navigation'; import CogWheelIcon from '@/assets/CogWheelIcon'; import useProfileImage from '@/hooks/useProfileImage'; import withSuspense from '@/components/HOC/withSuspense'; +import useMyLevelQuery from '@/apis/queries/mypage/useMyLevelQuery'; interface MyPageProps { navigation: StackNavigation<'MyPage/Profile'>; @@ -17,6 +18,7 @@ interface MyPageProps { export default withSuspense(function MyPage({ navigation }: MyPageProps) { const { profile } = useProfileQuery(); const { ProfileImage } = useProfileImage(); + const { data: myLevel } = useMyLevelQuery(); return ( @@ -36,7 +38,7 @@ export default withSuspense(function MyPage({ navigation }: MyPageProps) { - + diff --git a/packages/react-native/src/pages/MyPage/MyBadge.tsx b/packages/react-native/src/pages/MyPage/MyBadge.tsx index 3eb5053b..9430df5f 100644 --- a/packages/react-native/src/pages/MyPage/MyBadge.tsx +++ b/packages/react-native/src/pages/MyPage/MyBadge.tsx @@ -1,16 +1,18 @@ import { useState } from 'react'; import { FlatList, View } from 'react-native'; import { Font } from 'design-system'; -import Badge from '@/components/common/Badge'; +import Badge, { badgePath } from '@/components/common/Badge'; import Spacing from '@/components/common/Spacing'; import useMyBadgeQuery from '@/apis/queries/mypage/useMyBadgeQuery'; import withSuspense from '@/components/HOC/withSuspense'; +import BadgeListBottomSheet from '@/components/mypage/BadgeListBottomSheet'; export default withSuspense( function MyBadge() { const { data: badges } = useMyBadgeQuery(); const [containerWidth, setContainerWidth] = useState(0); - // const [selectedBadge, setSelectedBadge] = useState(); + const [selectedBadge, setSelectedBadge] = + useState(); const numColumns = 3; const paddingHorizontal = 8; @@ -27,7 +29,11 @@ export default withSuspense( location={item.badgeRegion} width={(containerWidth - paddingHorizontal * 2) / numColumns} label={item.badgeRegion} - // onPress={() => setSelectedBadge(item.badgeRegion)} + onPress={() => { + if (item.count) { + setSelectedBadge(item.badgeRegion); + } + }} count={item.count} /> @@ -36,10 +42,12 @@ export default withSuspense( keyExtractor={(item) => item.badgeRegion} numColumns={numColumns} /> - {/* setSelectedBadge(undefined)} - /> */} + {selectedBadge && ( + setSelectedBadge(undefined)} + /> + )} ); }, diff --git a/packages/react-native/src/pages/TripPlanner/TripPlannerDetail.tsx b/packages/react-native/src/pages/TripPlanner/TripPlannerDetail.tsx index 6d6027fb..fd63fcac 100644 --- a/packages/react-native/src/pages/TripPlanner/TripPlannerDetail.tsx +++ b/packages/react-native/src/pages/TripPlanner/TripPlannerDetail.tsx @@ -40,7 +40,7 @@ export default withSuspense(function TripPlannerDetail() { - 일정 + 나의 일정 (array: T[], key: keyof T) { + return array.reduce((uniq, item) => { + return uniq.some((element) => element[key] === item[key]) + ? uniq + : [...uniq, item]; + }, []); +} + +export default unique;