Skip to content

feat: badge api 연동 #85

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Sep 26, 2024
Merged
1 change: 1 addition & 0 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"react-native-dotenv": "^3.4.11",
"react-native-element-dropdown": "^2.12.1",
"react-native-gesture-handler": "^2.17.1",
"react-native-image-crop-picker": "^0.41.2",
"react-native-image-picker": "^7.1.2",
"react-native-linear-gradient": "^2.8.3",
"react-native-mail": "^6.1.1",
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { NavigationContainer } from '@react-navigation/native';
import StackNavigator from '@routes/StackNavigator';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Alert } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

const queryClient = new QueryClient({
Expand All @@ -10,6 +11,11 @@ const queryClient = new QueryClient({
retry: false,
throwOnError: true,
},
mutations: {
onError: () => {
Alert.alert('오류가 발생했어요', '잠시뒤에 시도해보세요.');
},
},
},
});
export default function App() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Alert } from 'react-native';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Asset } from 'react-native-image-picker';
import { Image } from 'react-native-image-crop-picker';
import { REGION_MAPPER } from '@/constants/CITY';
import { KoreaLocationName } from '@/types/map';
import { AppStorage } from '@/utils/storage';
Expand All @@ -10,7 +10,7 @@ import QUERY_KEYS from '@/constants/QUERY_KEYS';

interface MutationRequestParams {
region: KoreaLocationName;
image: Asset;
image: Image;
}

export default function useRecordRepresentativeMutation() {
Expand Down
65 changes: 65 additions & 0 deletions packages/react-native/src/apis/queries/mypage/useMyBadgeQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import useAuthAxios from '@/apis/useAuthAxios';
import { badgePath } from '@/components/common/Badge';
import { Region } from '@/constants/CITY';
import QUERY_KEYS from '@/constants/QUERY_KEYS';
import { ServerResponse } from '@/types/response';

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 || Region.CHUNGNAM:
return '충청';
case Region.GYEONGBUK || Region.GYEONGNAM:
return '경상';
case Region.JEONBUK || Region.JEONNAM:
return '전라';
case Region.JEJU:
default:
return '제주';
}
};

export default function useMyBadgeQuery() {
const authAxios = useAuthAxios();
const getBadges = async () => {
const result =
await authAxios.get<ServerResponse<BadgeResponse[]>>('/api/user/badge');

const badges = result.data.result.map((badge) => ({
count: badge.count,
badgeRegion: badgeMapper(badge),
}));

return badges;
};

return useSuspenseQuery({
queryKey: [QUERY_KEYS.MY_BADGES],
queryFn: getBadges,
});
}
1 change: 1 addition & 0 deletions packages/react-native/src/constants/QUERY_KEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const QUERY_KEYS = {
SPOT_DETAIL: 'spotDetail',
SEARCH: 'search',
MY_SPOTS: 'mySpots',
MY_BADGES: 'myBadges',
};

export default QUERY_KEYS;
19 changes: 18 additions & 1 deletion packages/react-native/src/hooks/useGallery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CameraRoll } from '@react-native-camera-roll/camera-roll';
import { Alert, Linking, Platform } from 'react-native';
import ImageCropPicker from 'react-native-image-crop-picker';
import { Asset, launchImageLibrary } from 'react-native-image-picker';
import {
check,
Expand Down Expand Up @@ -126,5 +127,21 @@ export default function useGallery() {
return response.assets[0].uri as GetPhotoReturnType<T>;
};

return { savePhoto, getPhoto };
const getCropPhoto = async () => {
const hasPermission = await hasGalleryPermission('read');
if (!hasPermission) return Promise.reject();
try {
const result = await ImageCropPicker.openPicker({
width: 300,
height: 300,
cropping: true,
mediaType: 'photo',
});
return result;
} catch (err) {
return null;
}
};

return { savePhoto, getPhoto, getCropPhoto };
}
43 changes: 32 additions & 11 deletions packages/react-native/src/pages/Maps/Maps.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { useRef, useState } from 'react';
import { Dimensions, View, TouchableOpacity, Alert } from 'react-native';
import {
Dimensions,
View,
TouchableOpacity,
Alert,
ScrollView,

Check warning on line 7 in packages/react-native/src/pages/Maps/Maps.tsx

View workflow job for this annotation

GitHub Actions / build

'ScrollView' is defined but never used
Image as RNImage,
} from 'react-native';
import { Font } from 'design-system';
import { geoPath, geoMercator } from 'd3-geo';
import { Svg, G, Path, Image, Defs, Pattern } from 'react-native-svg';
Expand Down Expand Up @@ -29,28 +36,28 @@
KoreaLocationName,
{ size: number; x: number; y: number }
> = {
강원도: { size: 0.47, x: -20, y: 30 },
강원도: { size: 0.5, x: -60, y: 0 },
경기도: { size: 0.4, x: -80, y: 60 },
경상남도: { size: 0.3, x: 80, y: 120 },
경상북도: { size: 0.4, x: 40, y: -140 },
경상남도: { size: 0.3, x: 80, y: 100 },
경상북도: { size: 0.45, x: 20, y: -180 },
광주광역시: { size: 0.08, x: 4, y: 4 },
대구광역시: { size: 0.2, x: -10, y: 8 },
대전광역시: { size: 0.1, x: -6, y: -8 },
부산광역시: { size: 0.15, x: -10, y: 60 },
서울특별시: { size: 0.1, x: 0, y: 20 },
서울특별시: { size: 0.15, x: -10, y: 30 },
세종특별자치시: { size: 0.1, x: 15, y: 50 },
울산광역시: { size: 0.2, x: -20, y: 40 },
인천광역시: { size: 0.2, x: -20, y: -15 },
전라남도: { size: 0.35, x: -80, y: 20 },
전라북도: { size: 0.25, x: 0, y: 130 },
전라남도: { size: 0.35, x: -80, y: 85 },
전라북도: { size: 0.25, x: 0, y: 120 },
제주특별자치도: { size: 0.3, x: 40, y: 80 },
충청남도: { size: 0.3, x: -50, y: -20 },
충청북도: { size: 0.27, x: -50, y: 0 },
충청남도: { size: 0.3, x: -50, y: -30 },
충청북도: { size: 0.4, x: -40, y: 160 },
};

export default withSuspense(function Maps({ navigation }: MapsMainProps) {
const [region, setRegion] = useState<KoreaLocationName>();
const { getPhoto, savePhoto } = useGallery();
const { savePhoto, getCropPhoto } = useGallery();
const [isButtonClicked, setButtonClicked] = useState(false);
const [showBottomSheet, toggleBottomSheet] = useToggle();
const ref = useRef<View>(null);
Expand All @@ -68,7 +75,7 @@

const handleAddRegionImage = async (regionName: KoreaLocationName) => {
toggleBottomSheet();
const photo = await getPhoto({ fullObject: true });
const photo = await getCropPhoto();

if (!photo) {
Alert.alert('이미지가 선택되지 않았습니다!');
Expand All @@ -93,6 +100,20 @@

return (
<View className="flex-1 bg-[#D2F3F8] relative">
<View className="hidden">
{regionImage &&
Object.values(regionImage).map(
(value, index) =>
value && (
<RNImage
key={value + index}
source={{ uri: value }}
width={100}
height={100}
/>
),
)}
</View>
<MutationLoadingModal isSubmiting={isSettingImagePending} />
<Header type="logo" />
<View className="bg-[#D2F3F8]" ref={ref}>
Expand Down
7 changes: 5 additions & 2 deletions packages/react-native/src/pages/MyPage/EditProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { Alert, TouchableOpacity, View } from 'react-native';

Check warning on line 2 in packages/react-native/src/pages/MyPage/EditProfile.tsx

View workflow job for this annotation

GitHub Actions / build

'Alert' is defined but never used
import { useRoute } from '@react-navigation/native';
import { Button, Font, TextField } from 'design-system';
import useProfileImage from '@/hooks/useProfileImage';
Expand All @@ -9,6 +9,7 @@
import useNicknameMutation from '@/apis/mutations/useNicknameMutation';
import MutationLoadingModal from '@/components/common/MutationLoadingModal';
import useProfileImageMutation from '@/apis/mutations/useProfileImageMutation';
import { AppStorage } from '@/utils/storage';

interface EditProfileProps {
navigation: StackNavigation<'MyPage/EditProfile'>;
Expand Down Expand Up @@ -76,9 +77,11 @@
닉네임으로 프로필 사진 설정하기
</Font.Bold>
</TouchableOpacity>
{/* FIXME: 로그아웃 기능 추가 */}
<TouchableOpacity
onPress={() => Alert.alert('로그아웃')}
onPress={async () => {
await AppStorage.deleteData('token');
navigation.navigate('Login');
}}
className="opacity-50"
>
<Font.Bold type="body3" color="white" underline>
Expand Down
84 changes: 50 additions & 34 deletions packages/react-native/src/pages/MyPage/MyBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,58 @@
import { useState } from 'react';
import { FlatList, View } from 'react-native';
import Badge, { badgePath } from '@/components/common/Badge';
import { Font } from 'design-system';
import Badge from '@/components/common/Badge';
import Spacing from '@/components/common/Spacing';
import BadgeListBottomSheet from '@/components/mypage/BadgeListBottomSheet';
import useMyBadgeQuery from '@/apis/queries/mypage/useMyBadgeQuery';
import withSuspense from '@/components/HOC/withSuspense';

export default function MyBadge() {
const [containerWidth, setContainerWidth] = useState(0);
const [selectedBadge, setSelectedBadge] = useState<keyof typeof badgePath>();
export default withSuspense(
function MyBadge() {
const { data: badges } = useMyBadgeQuery();
const [containerWidth, setContainerWidth] = useState(0);
// const [selectedBadge, setSelectedBadge] = useState<keyof typeof badgePath>();

const locationList = Object.keys(badgePath) as (keyof typeof badgePath)[];
const numColumns = 3;
const paddingHorizontal = 8;
const numColumns = 3;
const paddingHorizontal = 8;

return (
<>
<FlatList
data={locationList}
onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
style={{ flex: 1, backgroundColor: 'black', paddingHorizontal }}
renderItem={({ item, index }) => (
<View>
<Badge
location={item}
width={(containerWidth - paddingHorizontal * 2) / numColumns}
label={item}
onPress={() => setSelectedBadge(item)}
count={index % 4}
/>
<Spacing height={20} />
</View>
)}
keyExtractor={(item) => item}
numColumns={numColumns}
/>
<BadgeListBottomSheet
return (
<>
<FlatList
data={badges}
onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
style={{ flex: 1, backgroundColor: 'black', paddingHorizontal }}
renderItem={({ item, index }) => (

Check warning on line 24 in packages/react-native/src/pages/MyPage/MyBadge.tsx

View workflow job for this annotation

GitHub Actions / build

'index' is defined but never used
<View>
<Badge
location={item.badgeRegion}
width={(containerWidth - paddingHorizontal * 2) / numColumns}
label={item.badgeRegion}
// onPress={() => setSelectedBadge(item.badgeRegion)}
count={item.count}
/>
<Spacing height={20} />
</View>
)}
keyExtractor={(item) => item.badgeRegion}
numColumns={numColumns}
/>
{/* <BadgeListBottomSheet
selectedBadge={selectedBadge}
onClose={() => setSelectedBadge(undefined)}
/>
</>
);
}
/> */}
</>
);
},
{
fallback: (
<View className="flex-1 justify-center items-center bg-SPOT-black">
<Font type="body1" color="white">
잠시만
</Font>
<Font type="body1" color="white">
기다려주세요
</Font>
</View>
),
},
);
Loading
Loading